Materials
This commit is contained in:
1 parent
3cd4ba3f38
commit
f3ca6b5721
49 files changed
+13055
-1351
No files matched your search
+80
-6
@@ -13,26 +13,37 @@ endif()
|
|||||||
option(ENGINE_BUILD_RENDER "Build the window/renderer (raylib), run() and the examples" ON)
|
option(ENGINE_BUILD_RENDER "Build the window/renderer (raylib), run() and the examples" ON)
|
||||||
option(ENGINE_BUILD_TESTS "Build the headless test executable" ON)
|
option(ENGINE_BUILD_TESTS "Build the headless test executable" ON)
|
||||||
option(ENGINE_BUILD_EXAMPLES "Build the examples (needs ENGINE_BUILD_RENDER)" ON)
|
option(ENGINE_BUILD_EXAMPLES "Build the examples (needs ENGINE_BUILD_RENDER)" ON)
|
||||||
|
option(ENGINE_BUILD_GFX "Generate eng::gfx, the engine's equivalent of every raylib/rlgl/raymath function, from raylib's headers (needs ENGINE_BUILD_RENDER)" ON)
|
||||||
|
option(ENGINE_BUILD_GPU_CHECK "Build tools/gpu_check: runs the renderer on a software OpenGL driver (Linux, Mesa EGL) and compares its pixels with a CPU model" OFF)
|
||||||
option(ENGINE_SANITIZE "Build the engine's own code with ASan + UBSan (not the dependencies)" OFF)
|
option(ENGINE_SANITIZE "Build the engine's own code with ASan + UBSan (not the dependencies)" OFF)
|
||||||
|
|
||||||
include(cmake/deps.cmake)
|
include(cmake/deps.cmake)
|
||||||
|
include(cmake/gfx.cmake)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# engine_core: ECS, math, input state, resources, physics (Jolt), SVG (pugixml) and
|
# engine_core: ECS, math, input state, resources, textures and materials, physics (Jolt), SVG (pugixml) and
|
||||||
# extrusion (Clipper2). No window and no OpenGL, so it links and runs headless.
|
# extrusion (Clipper2), plus everything the renderer decides without a GPU: visibility,
|
||||||
|
# lights and shadow fitting, ambient zones and the per-frame draw plan.
|
||||||
|
# No window and no OpenGL, so it links and runs headless.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
add_library(engine_core STATIC
|
add_library(engine_core STATIC
|
||||||
src/ecs.cpp
|
src/ecs.cpp
|
||||||
|
src/visibility.cpp
|
||||||
src/input.cpp
|
src/input.cpp
|
||||||
src/resources.cpp
|
src/resources.cpp
|
||||||
src/physics.cpp
|
src/physics.cpp
|
||||||
src/svg.cpp
|
src/svg.cpp
|
||||||
src/extrude.cpp
|
src/extrude.cpp
|
||||||
src/spawn.cpp)
|
src/spawn.cpp
|
||||||
|
src/scene.cpp
|
||||||
|
src/ambient.cpp
|
||||||
|
src/texture.cpp
|
||||||
|
src/material.cpp
|
||||||
|
src/frame.cpp)
|
||||||
target_include_directories(engine_core PUBLIC include PRIVATE src)
|
target_include_directories(engine_core PUBLIC include PRIVATE src)
|
||||||
target_link_libraries(engine_core
|
target_link_libraries(engine_core
|
||||||
PUBLIC EnTT::EnTT
|
PUBLIC EnTT::EnTT
|
||||||
PRIVATE Jolt Clipper2 pugixml::pugixml raylib)
|
PRIVATE Jolt Clipper2 pugixml::pugixml)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# engine_render: raylib window + GPU-instanced renderer behind eng::run().
|
# engine_render: raylib window + GPU-instanced renderer behind eng::run().
|
||||||
@@ -43,6 +54,20 @@ if(ENGINE_BUILD_RENDER)
|
|||||||
target_include_directories(engine_render PRIVATE src)
|
target_include_directories(engine_render PRIVATE src)
|
||||||
target_link_libraries(engine_render PUBLIC engine_core PRIVATE raylib)
|
target_link_libraries(engine_render PUBLIC engine_core PRIVATE raylib)
|
||||||
|
|
||||||
|
# eng::gfx (include <engine/gfx.hpp>): generated from raylib's own headers, see cmake/gfx.cmake. Optional: when it
|
||||||
|
# cannot be generated the engine still builds, without it.
|
||||||
|
if(ENGINE_BUILD_GFX)
|
||||||
|
engine_configure_gfx()
|
||||||
|
if(ENGINE_GFX_OK)
|
||||||
|
add_library(engine_gfx STATIC ${ENGINE_GFX_SOURCES})
|
||||||
|
target_include_directories(engine_gfx PUBLIC "${ENGINE_GFX_INCLUDE_DIR}")
|
||||||
|
target_compile_definitions(engine_gfx PRIVATE ${ENGINE_GFX_DEFINITIONS})
|
||||||
|
target_link_libraries(engine_gfx PUBLIC engine_core PRIVATE raylib)
|
||||||
|
add_library(engine::gfx ALIAS engine_gfx)
|
||||||
|
target_link_libraries(engine_render PUBLIC engine_gfx)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
add_library(engine INTERFACE)
|
add_library(engine INTERFACE)
|
||||||
target_link_libraries(engine INTERFACE engine_render)
|
target_link_libraries(engine INTERFACE engine_render)
|
||||||
add_library(engine::engine ALIAS engine)
|
add_library(engine::engine ALIAS engine)
|
||||||
@@ -61,16 +86,65 @@ endif()
|
|||||||
|
|
||||||
add_library(engine::core ALIAS engine_core)
|
add_library(engine::core ALIAS engine_core)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# gpu_check (optional): src/render.cpp and src/app.cpp compiled against a software OpenGL
|
||||||
|
# implementation of the raylib calls they make (tools/gpu_check/gl_shim.cpp) instead of raylib itself,
|
||||||
|
# then run on Mesa's llvmpipe through EGL - no display, no GPU. It renders scenes and compares the pixels
|
||||||
|
# with an independent CPU model of the lighting, exact shadow ray tests and the CPU ambient grid.
|
||||||
|
# Only raylib's HEADERS are used (raylib is not linked, the shim provides those symbols).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if(ENGINE_BUILD_GPU_CHECK)
|
||||||
|
if(NOT ENGINE_BUILD_RENDER OR NOT TARGET raylib)
|
||||||
|
message(FATAL_ERROR "ENGINE_BUILD_GPU_CHECK needs ENGINE_BUILD_RENDER (it uses raylib's headers)")
|
||||||
|
endif()
|
||||||
|
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
message(FATAL_ERROR "ENGINE_BUILD_GPU_CHECK needs Linux (EGL + Mesa)")
|
||||||
|
endif()
|
||||||
|
find_package(PNG REQUIRED)
|
||||||
|
add_executable(gpu_check
|
||||||
|
tools/gpu_check/render_check.cpp
|
||||||
|
tools/gpu_check/gl_shim.cpp
|
||||||
|
src/render.cpp
|
||||||
|
src/app.cpp)
|
||||||
|
target_include_directories(gpu_check PRIVATE src tools/gpu_check
|
||||||
|
$<TARGET_PROPERTY:raylib,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||||
|
target_link_libraries(gpu_check PRIVATE engine_core PNG::PNG ${CMAKE_DL_LIBS})
|
||||||
|
if(ENGINE_BUILD_TESTS)
|
||||||
|
add_test(NAME gpu_check COMMAND gpu_check)
|
||||||
|
set_tests_properties(gpu_check PROPERTIES
|
||||||
|
ENVIRONMENT "ENGINE_GPU_CHECK_OUT=${CMAKE_BINARY_DIR}/gpu_check_out"
|
||||||
|
LABELS gpu)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
if(ENGINE_BUILD_TESTS)
|
if(ENGINE_BUILD_TESTS)
|
||||||
enable_testing()
|
enable_testing()
|
||||||
add_executable(engine_tests tests/tests.cpp)
|
add_executable(engine_tests tests/tests.cpp)
|
||||||
target_link_libraries(engine_tests PRIVATE engine_core)
|
target_link_libraries(engine_tests PRIVATE engine_core)
|
||||||
|
if(TARGET engine_gfx)
|
||||||
|
# Only the generated HEADERS (the hand-written eng::gfx helpers are tested here); nothing of raylib is linked.
|
||||||
|
target_include_directories(engine_tests PRIVATE "${ENGINE_GFX_INCLUDE_DIR}")
|
||||||
|
endif()
|
||||||
add_test(NAME engine_tests COMMAND engine_tests)
|
add_test(NAME engine_tests COMMAND engine_tests)
|
||||||
|
|
||||||
|
# The generated eng::gfx wrappers against a recording stand-in for raylib: no window, no GPU, raylib is not linked
|
||||||
|
# (only its headers are used). Checks every wrapper passes every argument, in order, unchanged.
|
||||||
|
if(TARGET engine_gfx AND ENGINE_GFX_SELFTEST_SOURCES)
|
||||||
|
add_executable(gfx_selftest ${ENGINE_GFX_SELFTEST_SOURCES})
|
||||||
|
target_include_directories(gfx_selftest PRIVATE
|
||||||
|
"${ENGINE_GFX_INCLUDE_DIR}" "${ENGINE_GFX_SELFTEST_INCLUDE_DIR}" "${ENGINE_GFX_RAYLIB_INCLUDE}"
|
||||||
|
$<TARGET_PROPERTY:engine_core,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||||
|
target_compile_definitions(gfx_selftest PRIVATE ${ENGINE_GFX_DEFINITIONS})
|
||||||
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
target_compile_options(gfx_selftest PRIVATE -ffp-contract=off) # raymath results are compared bit for bit
|
||||||
|
endif()
|
||||||
|
add_test(NAME gfx_selftest COMMAND gfx_selftest)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Sanitizers for our own code only.
|
# Sanitizers for our own code only.
|
||||||
if(ENGINE_SANITIZE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
if(ENGINE_SANITIZE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
foreach(t engine_core engine_render demo instancing engine_tests)
|
foreach(t engine_core engine_render engine_gfx demo instancing engine_tests gfx_selftest gpu_check)
|
||||||
if(TARGET ${t})
|
if(TARGET ${t})
|
||||||
target_compile_options(${t} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
|
target_compile_options(${t} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
|
||||||
target_link_options(${t} PRIVATE -fsanitize=address,undefined)
|
target_link_options(${t} PRIVATE -fsanitize=address,undefined)
|
||||||
@@ -80,7 +154,7 @@ endif()
|
|||||||
|
|
||||||
# Warnings for our own code only (never for the dependencies).
|
# Warnings for our own code only (never for the dependencies).
|
||||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
foreach(t engine_core engine_render demo instancing engine_tests)
|
foreach(t engine_core engine_render demo instancing engine_tests gpu_check)
|
||||||
if(TARGET ${t})
|
if(TARGET ${t})
|
||||||
target_compile_options(${t} PRIVATE -Wall -Wextra -Wno-unused-parameter)
|
target_compile_options(${t} PRIVATE -Wall -Wextra -Wno-unused-parameter)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "x64-Debug",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"configurationType": "Debug",
|
||||||
|
"inheritEnvironments": [ "msvc_x64_x64" ],
|
||||||
|
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||||
|
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||||
|
"cmakeCommandArgs": "",
|
||||||
|
"buildCommandArgs": "",
|
||||||
|
"ctestCommandArgs": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x64-Release",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"configurationType": "RelWithDebInfo",
|
||||||
|
"buildRoot": "${projectDir}\\out\\build\\${name}",
|
||||||
|
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||||
|
"cmakeCommandArgs": "",
|
||||||
|
"buildCommandArgs": "",
|
||||||
|
"ctestCommandArgs": "",
|
||||||
|
"inheritEnvironments": [ "msvc_x64_x64" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
# engine
|
# engine
|
||||||
|
|
||||||
A small C++20 3D engine: an EnTT-based entity/component layer with Unity-style callbacks (no `virtual`),
|
A small C++20 3D engine: an EnTT-based entity/component layer with Unity-style callbacks (no `virtual`),
|
||||||
Jolt physics as components, GPU-instanced raylib rendering, and a pipeline that turns **labelled SVG
|
Jolt physics as components, a GPU-instanced raylib renderer with real-time lighting, soft shadows and
|
||||||
drawings into 3D models** (pugixml parser + Clipper2 geometry).
|
**per-region ambient light**, a pipeline that turns **labelled SVG drawings into 3D models** (pugixml parser +
|
||||||
|
Clipper2 geometry), and **`eng::gfx`**: the engine's own equivalent of every raylib function, generated from the
|
||||||
|
raylib headers you build against.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
#include <engine/engine.hpp>
|
#include <engine/engine.hpp>
|
||||||
@@ -19,6 +21,10 @@ int main() {
|
|||||||
cube.add<MeshRenderer>().mesh = Resources::box(); // shared, instanced on the GPU
|
cube.add<MeshRenderer>().mesh = Resources::box(); // shared, instanced on the GPU
|
||||||
cube.add<Spinner>();
|
cube.add<Spinner>();
|
||||||
|
|
||||||
|
Entity sun = world.createEntity("sun");
|
||||||
|
sun.add<Light>(Light::directional()); // casts shadows by default
|
||||||
|
sun.transform().lookAt({-0.4f, -1, -0.3f});
|
||||||
|
|
||||||
Entity camera = world.createEntity("camera");
|
Entity camera = world.createEntity("camera");
|
||||||
camera.add<Camera>();
|
camera.add<Camera>();
|
||||||
camera.position() = {0, 2, 6};
|
camera.position() = {0, 2, 6};
|
||||||
@@ -32,7 +38,7 @@ No window, renderer, raylib call or loop in game code. `main()` sets up a world,
|
|||||||
|
|
||||||
| library | version | used for |
|
| library | version | used for |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| raylib | 6.0 | window, input, rendering (only inside `engine_render`) |
|
| raylib | 6.0 | window, input, rendering (only inside `engine_render`), and the source of the generated `eng::gfx` |
|
||||||
| Jolt Physics | 5.6.0 | rigid bodies, character controller, joints, raycasts |
|
| Jolt Physics | 5.6.0 | rigid bodies, character controller, joints, raycasts |
|
||||||
| EnTT | 4.0.0 | component storage |
|
| EnTT | 4.0.0 | component storage |
|
||||||
| Clipper2 | 2.0.1 | fill normalisation (islands + holes), stroke outlines, patch hit tests |
|
| Clipper2 | 2.0.1 | fill normalisation (islands + holes), stroke outlines, patch hit tests |
|
||||||
@@ -46,18 +52,66 @@ Everything is fetched by CMake at pinned tags; you need a C++20 compiler and CMa
|
|||||||
cmake -S . -B build
|
cmake -S . -B build
|
||||||
cmake --build build -j
|
cmake --build build -j
|
||||||
ctest --test-dir build --output-on-failure # headless: no window, no GL needed
|
ctest --test-dir build --output-on-failure # headless: no window, no GL needed
|
||||||
./build/demo # WASD, SPACE, right-mouse orbit, F shoots
|
./build/demo # see the key list below
|
||||||
./build/instancing # 20,000 animated cubes, one draw call
|
./build/instancing # 20,000 animated cubes, one draw call
|
||||||
```
|
```
|
||||||
|
|
||||||
* Linux needs the X11/GL dev packages for raylib, e.g. Ubuntu:
|
* Linux needs the X11/GL dev packages for raylib, e.g. Ubuntu:
|
||||||
`sudo apt install libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev`.
|
`sudo apt install libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev`.
|
||||||
* Link the **`engine`** target from your own CMake project (`target_link_libraries(game PRIVATE engine)`);
|
* Link the **`engine`** target from your own CMake project (`target_link_libraries(game PRIVATE engine)`);
|
||||||
`engine_core` alone is the headless engine (ECS, physics, SVG, resources) with no raylib/GL.
|
`engine_core` alone is the headless engine (ECS, physics, SVG, resources, and everything the renderer decides
|
||||||
* `-DENGINE_BUILD_RENDER=OFF` builds only `engine_core`; `-DENGINE_SANITIZE=ON` builds the engine's own code with ASan + UBSan.
|
without a GPU: visibility, lights, shadow fitting, ambient zones) with no raylib/GL.
|
||||||
* Smoke tests / screenshots without code changes: `ENGINE_MAX_FRAMES=120 ENGINE_SCREENSHOT=out.png ./build/demo`
|
* Smoke tests / screenshots without code changes: `ENGINE_MAX_FRAMES=120 ENGINE_SCREENSHOT=out.png ./build/demo`
|
||||||
(works with any app that uses `run()`; under CI use `xvfb-run` with Mesa software GL).
|
(works with any app that uses `run()`; under CI use `xvfb-run` with Mesa software GL).
|
||||||
|
|
||||||
|
| CMake option | default | meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `ENGINE_BUILD_RENDER` | ON | window + renderer (raylib), `run()`, the examples. OFF builds only `engine_core` |
|
||||||
|
| `ENGINE_BUILD_TESTS` | ON | `engine_tests` (and `gfx_selftest`, see below) |
|
||||||
|
| `ENGINE_BUILD_EXAMPLES` | ON | `demo`, `instancing` (needs `ENGINE_BUILD_RENDER`) |
|
||||||
|
| `ENGINE_BUILD_GFX` | ON | generate `eng::gfx` from raylib's headers at configure time (needs `ENGINE_BUILD_RENDER`) |
|
||||||
|
| `ENGINE_GFX_STRICT` | OFF | make any problem generating `eng::gfx` a configure error instead of a warning |
|
||||||
|
| `ENGINE_GFX_GEN_EXECUTABLE` | | a prebuilt `gfx_gen` (tools/gfx_gen), needed when cross-compiling |
|
||||||
|
| `ENGINE_BUILD_GPU_CHECK` | OFF | `gpu_check`: the renderer on a software OpenGL driver, pixel-compared with a CPU model (Linux, Mesa EGL, libpng) |
|
||||||
|
| `ENGINE_SANITIZE` | OFF | build the engine's own code (not the dependencies) with ASan + UBSan |
|
||||||
|
| `ENGINE_CPU_LEVEL` | auto | instruction-set level, see below |
|
||||||
|
|
||||||
|
`ctest` runs `engine_tests` (headless suite), `gfx_selftest` (when `eng::gfx` was generated) and, if enabled, `gpu_check`.
|
||||||
|
The configure log says what happened to `eng::gfx` (lines starting `engine: eng::gfx` / `eng::gfx:`).
|
||||||
|
|
||||||
|
### The demo
|
||||||
|
|
||||||
|
A labelled SVG becomes the level; a Jolt character walks around it. It shows lighting (a sun and two spot lights
|
||||||
|
that cast shadows, a point lamp, fog, tone mapping), ambient zones of every shape (one follows the player), materials
|
||||||
|
(textures made in code: stone, planks, plaster, grass, roof tiles, and a row of spheres that differ in one number each),
|
||||||
|
hide/show, colliders that follow rotating and scaling parents (a windmill, a breathing block) and `eng::gfx` drawing calls.
|
||||||
|
|
||||||
|
The level has a park, a **house** south of the start and a walled **garden** in the south-east corner, to see how
|
||||||
|
ambient zones nest and blend:
|
||||||
|
|
||||||
|
* **The park is an outdoor area of its own** (a big `Replace` zone at the lowest priority). Every other zone is blended over it.
|
||||||
|
* **The garden is an outdoor part inside that outdoor part, with purple light**: a `Replace` zone with a purple sky and
|
||||||
|
ground colour, fading over 1.5 units outside its walls. There is no purple light source anywhere; the colour comes from
|
||||||
|
the area, and it tints the garden's stone, the paving, the spheres and, softly, the grass and the wall beyond its edge.
|
||||||
|
* **The house is 90% dark, with some light**: a `Multiply` zone with `intensity = 0.1` keeps 10% of whatever ambient light
|
||||||
|
the zones below it give (so a house built inside the garden would be dark *and* purple). The light in it: a point lamp in
|
||||||
|
each room (the west one flickers: it edits its shared bulb `Material` every frame), a small warm `Add` zone around each
|
||||||
|
lamp, and the sun through the window in the west wall (the roof and walls shadow it from the rest).
|
||||||
|
* The `Z` key switches all zones off to compare with the plain hemisphere ambient; `M` switches the materials off.
|
||||||
|
|
||||||
|
| key | |
|
||||||
|
|---|---|
|
||||||
|
| `W A S D`, `SPACE` | walk, jump |
|
||||||
|
| right mouse + drag, wheel | orbit, zoom |
|
||||||
|
| `F` / left click | shoot a box |
|
||||||
|
| `Q` / `E` | turn the sun |
|
||||||
|
| `H` | hide / show the decor (`Entity::setVisible` on the children of the level tagged "decor") |
|
||||||
|
| `C` | hide / show the player's nose (`MeshRenderer::childrenVisible`) |
|
||||||
|
| `L` | headlamp on / off (a hidden `Light` is off) |
|
||||||
|
| `Z` | ambient zones on / off (`AmbientZone::setComponentEnabled`) |
|
||||||
|
| `M` | materials on / off (every renderer that has a `Material` gets `nullptr` and back: the plain look) |
|
||||||
|
| `G` | `eng::gfx` overlay on / off (only when `eng::gfx` is part of the build) |
|
||||||
|
|
||||||
### CPU level (read this if a binary dies with "Illegal instruction")
|
### CPU level (read this if a binary dies with "Illegal instruction")
|
||||||
|
|
||||||
Jolt exports its SIMD flags (`-mavx2 -mfma -mbmi -mlzcnt -mf16c -mpopcnt` by default) as PUBLIC compile
|
Jolt exports its SIMD flags (`-mavx2 -mfma -mbmi -mlzcnt -mf16c -mpopcnt` by default) as PUBLIC compile
|
||||||
@@ -80,7 +134,7 @@ still crashes, run `gdb -batch -ex run -ex 'x/i $pc' ./build/engine_tests` and l
|
|||||||
|
|
||||||
Everything game code needs is behind `#include <engine/engine.hpp>`. EnTT, Jolt, Clipper2, pugixml and raylib
|
Everything game code needs is behind `#include <engine/engine.hpp>`. EnTT, Jolt, Clipper2, pugixml and raylib
|
||||||
never appear in a public signature (EnTT lives inside the private parts of `World`/`Entity`; Jolt is behind a
|
never appear in a public signature (EnTT lives inside the private parts of `World`/`Entity`; Jolt is behind a
|
||||||
pimpl; raylib is only inside `engine_render`).
|
pimpl; raylib is only inside `engine_render` and the generated `eng::gfx` wrappers).
|
||||||
|
|
||||||
### Entry point
|
### Entry point
|
||||||
|
|
||||||
@@ -98,7 +152,7 @@ adds a default one. For headless use (servers, tests) call `world.update(dt)` yo
|
|||||||
* **World**: `createEntity(name, parent)`, `hierarchy()` (the invisible root: query the whole scene), `update(dt)`, `physics()`.
|
* **World**: `createEntity(name, parent)`, `hierarchy()` (the invisible root: query the whole scene), `update(dt)`, `physics()`.
|
||||||
* **Hierarchy** (component): `parent() firstChild() previousSibling() nextSibling()` (methods returning `Entity`; `parent()` is null for top-level entities), `entityByName(name)`, `entityByTag(TagType)`, `entitiesWith<...>()`, `entities()`, `children()`, `setParent(Hierarchy&, position, keepWorld=false)`. Queries search *below* the node, excluding the node itself.
|
* **Hierarchy** (component): `parent() firstChild() previousSibling() nextSibling()` (methods returning `Entity`; `parent()` is null for top-level entities), `entityByName(name)`, `entityByTag(TagType)`, `entitiesWith<...>()`, `entities()`, `children()`, `setParent(Hierarchy&, position, keepWorld=false)`. Queries search *below* the node, excluding the node itself.
|
||||||
* **EntityIterator**: iterator *and* range over a subtree in pre-order, optionally filtered; works in range-for.
|
* **EntityIterator**: iterator *and* range over a subtree in pre-order, optionally filtered; works in range-for.
|
||||||
* **Entity**: `destroy() clone(name) has<T...>() get<T>() add<T>(T) world() transform() hierarchy() parent() position() rotation() scale() entityEnabled() enableEntity() disableEntity() setEntityEnabled(bool)`; extras: `tryGet<T>() emplace<T>(args...) remove<T>() name() tag() setParent() entityActive()`.
|
* **Entity**: `destroy() clone(name) has<T...>() get<T>() add<T>(T) world() transform() hierarchy() parent() position() rotation() scale() entityEnabled() enableEntity() disableEntity() setEntityEnabled(bool)`; extras: `tryGet<T>() emplace<T>(args...) remove<T>() name() tag() setParent() entityActive()`, and `setVisible(bool, includeChildren = true) isVisible()` (see *Showing and hiding*).
|
||||||
* **Component**: base class with `entity()` and the same shorthands (`destroy()`/`clone()` act on the whole entity), `deltaTime()`, `componentEnabled() enableComponent() disableComponent() setComponentEnabled(bool)`.
|
* **Component**: base class with `entity()` and the same shorthands (`destroy()`/`clone()` act on the whole entity), `deltaTime()`, `componentEnabled() enableComponent() disableComponent() setComponentEnabled(bool)`.
|
||||||
* **Transform**: public local `position / rotation / scale`; `translateTo/By`, `rotateTo/By`, `scaleTo/By(vec3, Locality)`; `localPosition() localRotation() localScale() worldPosition() worldRotation() worldScale()`; extras `lookAt`, `forward/right/up`, `setWorldPose`, `localMatrix/worldMatrix`.
|
* **Transform**: public local `position / rotation / scale`; `translateTo/By`, `rotateTo/By`, `scaleTo/By(vec3, Locality)`; `localPosition() localRotation() localScale() worldPosition() worldRotation() worldScale()`; extras `lookAt`, `forward/right/up`, `setWorldPose`, `localMatrix/worldMatrix`.
|
||||||
* **EntityData**: `name`, `tag`, `enabled` (entity-wide flag; a disabled entity disables its whole subtree).
|
* **EntityData**: `name`, `tag`, `enabled` (entity-wide flag; a disabled entity disables its whole subtree).
|
||||||
@@ -112,6 +166,8 @@ struct Mover : Component { // plain struct, no virtual
|
|||||||
void lateUpdate() {}
|
void lateUpdate() {}
|
||||||
void destroyed() {} // all components of the entity still exist here
|
void destroyed() {} // all components of the entity still exist here
|
||||||
void collisionEnter(Entity other) {} // + collisionExit / triggerEnter / triggerExit
|
void collisionEnter(Entity other) {} // + collisionExit / triggerEnter / triggerExit
|
||||||
|
void drawGizmos() {} // 3D immediate drawing inside the camera's view (eng::gfx)
|
||||||
|
void drawGui() {} // 2D immediate drawing on top of the frame (eng::gfx)
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -121,12 +177,259 @@ struct Mover : Component { // plain struct, no virtual
|
|||||||
`mousePosition()`, `mouseDelta()`, `mouseWheel()`, `windowSize()`, and `quit()`. Read it from `update()`/`lateUpdate()`
|
`mousePosition()`, `mouseDelta()`, `mouseWheel()`, `windowSize()`, and `quit()`. Read it from `update()`/`lateUpdate()`
|
||||||
(a `fixedUpdate()` runs 0..N times per frame and can miss one-frame events).
|
(a `fixedUpdate()` runs 0..N times per frame and can miss one-frame events).
|
||||||
|
|
||||||
### Rendering components
|
## Rendering
|
||||||
|
|
||||||
`MeshRenderer{ MeshPtr mesh; Rgba tint; }`, `Camera` (perspective/orthographic), `Light` (directional, aimed with
|
Rendering is driven by components; the renderer reads them every frame, so changing a field (or toggling a
|
||||||
`lookAt`), and `Text` (screen-space label: set `value`, `position`, `size`, `color` - that is how HUDs are made).
|
component) is all it takes. Everything on this page is plain data, and everything the renderer *decides* (what is
|
||||||
|
visible, which lights are shaded, how the shadow maps are fitted, what the ambient grid contains) is computed on the
|
||||||
|
CPU by `engine_core` and unit-tested headless; `engine_render` only uploads and draws.
|
||||||
|
|
||||||
### Resources (nothing is duplicated)
|
| component | what it does |
|
||||||
|
|---|---|
|
||||||
|
| `MeshRenderer` | draws a `MeshPtr` with a tint and a surface: a shared `Material` (maps and numbers) or its own roughness, metallic, emission; shadow flags, unlit |
|
||||||
|
| `Camera` | perspective or orthographic; `run()` renders with the first enabled one, looking down the entity's -Z |
|
||||||
|
| `Light` | `Directional` (sun), `Point` or `Spot`; colour, intensity, range, cone, shadow settings |
|
||||||
|
| `Environment` | scene-wide look: hemisphere ambient, exposure, tone mapping, fog, shadow quality, zone grid resolution |
|
||||||
|
| `AmbientZone` | a region of space (box, sphere, capsule, cylinder, any polygon, any closed mesh) with its own ambient light |
|
||||||
|
| `Text` | screen-space label (HUDs, read-outs) |
|
||||||
|
| `Visibility` | draws nothing; gives a plain group entity the hide/show switches of a renderer |
|
||||||
|
|
||||||
|
`MeshRenderer`, `Light`, `Text` and `Visibility` derive from `Renderable` (the hide/show switches below). `Material` and `Texture`
|
||||||
|
are shared resources, not components (see Materials).
|
||||||
|
|
||||||
|
### Showing and hiding
|
||||||
|
|
||||||
|
Hiding is **visual only**: scripts keep running, physics keeps simulating, colliders keep colliding. A hidden mesh draws
|
||||||
|
nothing and casts no shadow; a hidden light is off. (To switch an entity off completely use
|
||||||
|
`Entity::setEntityEnabled(false)`: no callbacks, no body, no drawing, whole subtree.)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
entity.setVisible(false); // hides this entity AND everything below it
|
||||||
|
entity.setVisible(false, false); // hides only this entity's own renderers; children stay visible
|
||||||
|
entity.setVisible(true);
|
||||||
|
bool shown = entity.isVisible(); // drawn right now: not hidden here, nor by an ancestor
|
||||||
|
|
||||||
|
auto& mesh = entity.get<MeshRenderer>(); // or per component, on any renderer (MeshRenderer, Light, Text, Visibility)
|
||||||
|
mesh.visible = false; // this component draws nothing
|
||||||
|
mesh.childrenVisible = false; // everything BELOW this entity is hidden; the mesh itself still follows `visible`
|
||||||
|
mesh.setVisible(false, true); // both at once (what Entity::setVisible calls)
|
||||||
|
```
|
||||||
|
|
||||||
|
* `visible` controls the component itself, `childrenVisible` the subtree below its entity, so a parent can stay visible
|
||||||
|
while its children are hidden (the demo's `C` key hides a character's nose that way) and the other way round.
|
||||||
|
* A child cannot override an ancestor that hides its subtree: `isVisible()` / `Renderable::visibleInHierarchy()` tell you
|
||||||
|
what actually happens.
|
||||||
|
* `Entity::setVisible(false)` on a plain group entity (no renderer at all) adds a `Visibility` component to carry the switch;
|
||||||
|
`entity.add<Visibility>()` does the same by hand. Disabling a `Renderable` component (`disableComponent()`) also stops
|
||||||
|
it from hiding children.
|
||||||
|
* `AmbientZone` is not drawn, so hiding does not affect it: disable the component (or the entity) to switch a zone off.
|
||||||
|
|
||||||
|
### Materials
|
||||||
|
|
||||||
|
A surface is described in one of two ways. **Plain numbers on the `MeshRenderer`** (the default; matte, no maps):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
MeshRenderer& m = e.add<MeshRenderer>();
|
||||||
|
m.mesh = Resources::sphere(0.5f);
|
||||||
|
m.tint = {200, 60, 40, 255}; // multiplied with the mesh's vertex colours (and with a material's albedo)
|
||||||
|
m.roughness = 0.3f; // 0 mirror-like highlights .. 1 fully diffuse (default 0.85: matte)
|
||||||
|
m.metallic = 1.0f; // 0 plastic/stone/wood .. 1 metal (the colour tints the reflection)
|
||||||
|
m.emission = 2.0f; // self-lit: adds colour * emission regardless of lights (glow: 1..3, max 4)
|
||||||
|
m.castShadows = true; m.receiveShadows = true;
|
||||||
|
m.unlit = false; // true: ignore all lighting, draw the plain colour (gizmos, skies, UI-in-world)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or **a `Material`**: every property is a number, a texture, or both (the texture's value is multiplied by the number, so a
|
||||||
|
map keeps its range and the number turns it up or down).
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
MaterialPtr brick = Material::create();
|
||||||
|
brick->albedoMap = Resources::texture("brick_albedo.png"); // colour
|
||||||
|
brick->normalMap = Resources::texture("brick_normal.png"); // bumps
|
||||||
|
brick->roughnessMap = Resources::texture("brick_rough.png");
|
||||||
|
brick->roughness = 1.0f; // x the map
|
||||||
|
brick->specular = 0.3f; // bricks hardly shine
|
||||||
|
brick->uvScale = {0.5f, 0.5f}; // repeats per unit
|
||||||
|
e.get<MeshRenderer>().material = brick; // as many renderers as you like share it
|
||||||
|
|
||||||
|
MaterialPtr gold = Material::create(); // numbers only
|
||||||
|
gold->albedo = {1.0f, 0.78f, 0.34f}; gold->metalness = 1.0f; gold->roughness = 0.28f;
|
||||||
|
```
|
||||||
|
|
||||||
|
| property | number | map (channel) | meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| albedo | `albedo` (colour) | `albedoMap` (RGB, alpha = opacity) | the colour of the surface under plain white light; multiplies the mesh's vertex colours and the renderer's `tint` |
|
||||||
|
| diffuse | `diffuse` (1) | `diffuseMap` (R) | how much of the light it scatters in all directions; 0 = none (a surface that only reflects) |
|
||||||
|
| specular | `specular` (0.5), `specularColor` | `specularMap` (R) | how strongly a non-metal reflects light like glass: reflectance F0 = 0.08 x specular x specularColor (0.5 is the 4% of plastic and glass); ambient reflections follow it too |
|
||||||
|
| metalness | `metalness` (0) | `metalnessMap` (R) | 0 = plastic/stone/wood .. 1 = metal (reflects in its albedo colour, no diffuse) |
|
||||||
|
| roughness | `roughness` (0.85) | `roughnessMap` (R) | 0 = mirror-like highlights .. 1 = fully matte |
|
||||||
|
| normals | `normalStrength` (1) | `normalMap` | small bumps without geometry: the map leans the surface by the slope (x, y) / z of its tangent-space normal, scaled by the strength |
|
||||||
|
| emission | `emission` (0) | | self-lit: adds albedo x emission whatever the lights do (a bulb: 2) |
|
||||||
|
|
||||||
|
* **Colour spaces.** `albedoMap` and the colour numbers are gamma-encoded like every colour in the engine; the other maps
|
||||||
|
hold numbers as they are. **Normal maps** are tangent-space, RGB = (x, y, z) x 0.5 + 0.5 with +x towards the right of the
|
||||||
|
image, +y towards its **top** and +z out of the surface (a flat map is (128, 128, 255); the usual "OpenGL" convention).
|
||||||
|
Texture coordinates have (0, 0) at the **top-left** corner of an image and v growing downwards (as in raylib).
|
||||||
|
* **Where the texture coordinates come from** (`Material::uv`): `UvMode::Mesh` is the mesh's own (`MeshData::texcoords`;
|
||||||
|
`Resources::box` and `sphere` have them: a box shows a texture once per world unit on every face, not stretched however long
|
||||||
|
it is; a sphere wraps it once around). `UvMode::World` projects the textures from the world along x, y and z (triplanar),
|
||||||
|
blended where a surface turns: **no UVs and no unwrapping needed**, and neighbouring pieces line up seamlessly. The extruded SVG
|
||||||
|
shapes have no UVs, so they use it. `UvMode::Auto` (the default) is `Mesh` when the mesh has UVs, else `World`. `uvScale`
|
||||||
|
(repeats per unit) and `uvOffset` apply to both. A world-projected texture stays where it is when the object moves (give a
|
||||||
|
moving object a mesh with UVs).
|
||||||
|
* **Textures** (`texture.hpp`) are shared RGBA8 images, one GPU copy each, however many materials use them.
|
||||||
|
`Resources::texture(path, options)` reads an image file (PNG, JPG, BMP, TGA, ... through raylib; it needs no window and works
|
||||||
|
while the world is being built; cached by path, size and modification time; throws `std::runtime_error` when it cannot read
|
||||||
|
the file). Without a file: `Texture::create(w, h, rgba)`, `Texture::solid(colour)`, `Texture::checker(...)` and
|
||||||
|
`Texture::normalMapFromHeight(heightMap, strength)` (see the demo, which makes all of its textures in code). A program with
|
||||||
|
no window can install its own decoder with `setImageLoader`. `TextureOptions`: `Repeat` / `Clamp`, `Smooth` (mipmaps,
|
||||||
|
trilinear, 8x anisotropic: no shimmer on floors at a grazing angle) / `Pixelated` (nearest, for pixel art).
|
||||||
|
* **Materials are shared and editable**: change a field and every renderer that uses the material follows from the next frame
|
||||||
|
(a flickering lamp is one assignment per frame; see `Flicker` in the demo). `material->clone()` makes a new material
|
||||||
|
(new id) with the same numbers and textures to change a few. Materials cannot be copied (a copy would share the id).
|
||||||
|
* **Draw calls**: one instanced draw per mesh, material and handedness (before: per mesh and handedness); the shadow passes
|
||||||
|
only need depth and ignore materials. Many objects with one material are still one draw call.
|
||||||
|
* With a `material` set, the renderer's own `roughness`, `metallic` and `emission` are not used (the `tint` still is).
|
||||||
|
A default `Material::create()` draws exactly like a renderer without one.
|
||||||
|
|
||||||
|
Colours are the usual 0..1 "screen" values; the shader lights them in linear light (gamma 2.2) and encodes the result
|
||||||
|
again, so `intensity = 1` sun light on a surface facing it shows its plain colour.
|
||||||
|
|
||||||
|
### Lights
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
e.add<Light>(Light::directional({1.0f, 0.95f, 0.85f}, 1.1f)); // the sun: rays along the entity's -Z
|
||||||
|
e.add<Light>(Light::point({1.0f, 0.62f, 0.28f}, 10.0f, 12.0f)); // colour, intensity, range
|
||||||
|
e.add<Light>(Light::spot({0.55f, 0.8f, 1.0f}, 380.0f, 40.0f, 32.0f)); // colour, intensity, range, cone angle (degrees)
|
||||||
|
e.transform().lookAt(target); // aim sun and spot lights
|
||||||
|
```
|
||||||
|
|
||||||
|
| field | meaning |
|
||||||
|
|---|---|
|
||||||
|
| `type`, `color`, `intensity` | sun: `intensity` 1 lights a surface facing it with its plain colour. Point/spot lights fall off with the inverse square of the distance, so `intensity` is the brightness one world unit away |
|
||||||
|
| `range` | point/spot: the distance at which the light has faded to exactly nothing (smooth window, no hard edge) |
|
||||||
|
| `spotAngleDeg`, `spotSoftness` | spot: full cone angle; 0 = hard edge .. 1 = fades from the axis outwards |
|
||||||
|
| `castShadows` | sun and spot lights (point lights do not cast shadows) |
|
||||||
|
| `shadowBias` | multiplier on the automatic depth bias: raise it for acne, lower it for floating shadows |
|
||||||
|
| `visible` / `setVisible` | a hidden light is off |
|
||||||
|
|
||||||
|
A world without any `Light` gets a default sun, so a bare scene is not black.
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
The first enabled `Environment` in the world is used (defaults apply without one).
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Environment& env = world.createEntity("environment").add<Environment>();
|
||||||
|
env.ambientSky = {0.40f, 0.46f, 0.58f}; // hemisphere ambient: surfaces facing up get the sky colour,
|
||||||
|
env.ambientGround = {0.22f, 0.19f, 0.16f}; // surfaces facing down the ground colour, blended by the normal
|
||||||
|
env.ambientIntensity = 1.0f;
|
||||||
|
env.exposure = 1.0f;
|
||||||
|
env.toneMapping = ToneMapping::Aces; // None (clamp) | Reinhard | Aces
|
||||||
|
env.fogColor = {0.62f, 0.70f, 0.82f};
|
||||||
|
env.fogDensity = 0.004f; // exponential-squared fog, 0 = off
|
||||||
|
env.shadows = true; // master switch
|
||||||
|
env.shadowResolution = 2048; // texels per side, rounded to a power of two in [256, 4096]
|
||||||
|
env.shadowDistance = 45.0f; // how far from the camera the sun's shadow reaches
|
||||||
|
env.shadowSoftness = 1.5f; // filter radius in texels (0 = hard edges)
|
||||||
|
env.shadowStrength = 1.0f; // 0 = no visible shadow .. 1 = fully dark
|
||||||
|
env.maxShadowLights = 3; // shadow maps per frame (0..3): the sun first, then spot lights
|
||||||
|
env.zoneCellSize = 0.5f; // ambient zone grid resolution, world units per cell
|
||||||
|
env.zoneMaxCells = 262144; // the grid is coarsened automatically beyond this many cells
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ambient zones
|
||||||
|
|
||||||
|
An `AmbientZone` gives a region of space its own ambient light: a dark cellar inside a bright level, a warm glow around a
|
||||||
|
lamp, a cold shade under a bridge. Zones can have **any shape**, are **configured and switched by components**, and are
|
||||||
|
evaluated **per pixel**, so a large mesh that crosses a zone boundary shades correctly across it.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Entity cellar = world.createEntity("cellar");
|
||||||
|
cellar.position() = {-9.5f, 3, -9};
|
||||||
|
AmbientZone& z = cellar.add<AmbientZone>(AmbientZone::box({7, 6, 6}, /*sky*/ {0.07f, 0.08f, 0.09f}, /*ground*/ {0.03f, 0.03f, 0.03f}));
|
||||||
|
z.mode = ZoneMode::Multiply; // darken what is below it
|
||||||
|
z.falloff = 2.0f; // fades in over 2 world units outside its boundary
|
||||||
|
|
||||||
|
cellar.get<AmbientZone>().setComponentEnabled(false); // switch it off (and on again)
|
||||||
|
```
|
||||||
|
|
||||||
|
| `shape` | defined by | notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `Box` | `size` | centred on the entity |
|
||||||
|
| `Sphere` | `radius` | an ellipsoid when the entity is scaled non-uniformly |
|
||||||
|
| `Capsule` | `radius`, `height` (end to end) | along local Y, centred |
|
||||||
|
| `Cylinder` | `radius`, `height` | along local Y, centred |
|
||||||
|
| `Polygon` | `polygon` (outline on the local XZ plane), `height` | extruded from y = 0 up to `height`; any simple outline, concave included (a star works) |
|
||||||
|
| `Mesh` | `mesh` | any **closed** mesh in local space, e.g. an extruded SVG shape: `AmbientZone::fromMesh(model->find("pyramid")->mesh, sky, ground)` |
|
||||||
|
|
||||||
|
Factories: `AmbientZone::box / sphere / polygonal / fromMesh`; the other shapes are set through the fields.
|
||||||
|
|
||||||
|
| field | meaning |
|
||||||
|
|---|---|
|
||||||
|
| `sky`, `ground`, `intensity` | the zone's ambient light from above and from below (blended by the surface normal, like the Environment's), times `intensity` |
|
||||||
|
| `mode` | `Replace`: the zone's ambient takes the place of what is below it. `Add`: adds it. `Multiply`: tints / darkens what is below (1 = unchanged) |
|
||||||
|
| `falloff` | world units over which the zone fades in outside its boundary (smooth); 0 = hard edge |
|
||||||
|
| `priority` | overlapping zones are applied lowest priority first, so the highest ends up on top (equal priorities: hierarchy order) |
|
||||||
|
|
||||||
|
* The shape lives in the entity's **local space**, so moving, rotating or scaling the entity (or any ancestor) moves, turns
|
||||||
|
and resizes the zone: a zone parented to the player follows the player.
|
||||||
|
* Where no zone applies the Environment's hemisphere ambient is used, and it is the base the zones are blended onto.
|
||||||
|
* **How it works**: all zones are baked into one 3D grid (`Environment::zoneCellSize`, coarsened automatically beyond
|
||||||
|
`zoneMaxCells` or a texture-size limit), stored as a 2D atlas, and sampled with trilinear filtering in the pixel shader.
|
||||||
|
Closed meshes are voxelised once per mesh and size and cached.
|
||||||
|
* **Cost**: nothing is rebaked while nothing changes, and **a zone that moves (a zone on the player) costs what the zone
|
||||||
|
covers, not what the grid holds**. Each zone's shape field is cached until that zone's own shape or transform changes; a
|
||||||
|
bake works out the colour only of the cells that a changed zone covered before or covers now (every other cell keeps the
|
||||||
|
colour it had: it is a function of the zones that reach it, and those are unchanged), rewrites only the texels that came
|
||||||
|
out different, and the renderer uploads only those rectangles to the GPU (`AmbientGrid::updated`) instead of both whole
|
||||||
|
atlases. On the demo's level (~100,000 cells; a sphere zone of 2 units walking at 6 units per second; one core) a bake
|
||||||
|
takes about 0.3 ms instead of 1.0-1.3 ms, and the upload is 12-24 KB per frame instead of 0.9-1 MB.
|
||||||
|
Two things that used to rebuild everything when a zone walked now change rarely:
|
||||||
|
* **The grid's extent is rounded out to blocks of cells** (8 across, 4 up; smaller blocks, down to exact, when the cell
|
||||||
|
budget has no room for them, so the resolution never drops because of it). A zone walking out of the others' bounding
|
||||||
|
box used to resize the grid on every cell it crossed (a full rebake and new textures: 62 times in a 14-second walk along
|
||||||
|
the demo's south edge); now it does so when it crosses a block edge (10 times).
|
||||||
|
* **The colour scale is rounded up to a step of a ladder** (1, 1.25, 1.5, 1.75, 2, 2.5, ... four steps per octave). Texels
|
||||||
|
are stored divided by the brightest colour in the grid, so an exact scale changed (and re-encoded every texel) whenever
|
||||||
|
a bright zone and a moving one overlapped by a little more or less. On the ladder it only changes when the brightest
|
||||||
|
colour crosses a rung (and the encoding keeps its 8 bits: the loss is below one step of the gamma curve).
|
||||||
|
The grid is still exactly what a bake from scratch gives (both are functions of the zones alone; the tests compare them
|
||||||
|
byte for byte after every edit). A bake that changes the grid's layout (the zone crossed a block edge, a zone appeared far
|
||||||
|
away, `zoneCellSize` changed) still costs a few milliseconds (it re-encodes and re-uploads everything). Raise
|
||||||
|
`zoneCellSize` for big worlds: cost scales with the cell count.
|
||||||
|
|
||||||
|
### How a frame is lit
|
||||||
|
|
||||||
|
* **Shading** is physically based: GGX specular with Smith/Schlick visibility, Schlick Fresnel, an energy-conserving
|
||||||
|
diffuse term and a metal/rough workflow (`metallic`, `roughness`). Ambient (hemisphere or zone) lights the diffuse term
|
||||||
|
and gives a soft, Fresnel-weighted reflection of the sky/ground colours (there are no environment maps or probes).
|
||||||
|
Emission is added on top; then exponential-squared fog, exposure and tone mapping.
|
||||||
|
* **Lights**: up to 16 lights are shaded per frame: suns first (at most 4, strongest first), then the point and spot
|
||||||
|
lights that matter most to the camera. Lights beyond the budget, hidden lights and lights with no brightness are dropped.
|
||||||
|
* **Shadows**: up to 3 shadow maps per frame (`Environment::maxShadowLights`): the first shadow-casting sun, then
|
||||||
|
shadow-casting spot lights in order. The sun's map is an orthographic fit to the part of the camera's view up to
|
||||||
|
`shadowDistance`, snapped to whole texels so shadows do not shimmer while the camera moves or turns, and it fades out
|
||||||
|
towards the far end. Spot maps cover the cone. Filtering is a rotated 12-tap disk (`shadowSoftness` texels of radius) with
|
||||||
|
normal-offset and slope-scaled bias against acne and light leaks.
|
||||||
|
* **Per mesh**: `castShadows`, `receiveShadows`, `unlit`. Hidden meshes cast no shadow.
|
||||||
|
* **Materials** (see above) change what goes into that model, not the model: albedo (number x map x vertex colour x tint), the
|
||||||
|
diffuse weight, F0 = mix(0.08 x specular x specularColor, albedo, metalness), roughness, the normal map's tilt of the
|
||||||
|
shading normal (shadows keep biasing along the geometry's own normal) and emission. A draw without a material evaluates
|
||||||
|
none of it and produces the same pixels as before materials existed.
|
||||||
|
* Colours are authored as 0..1 values and lit in linear light (gamma 2.2).
|
||||||
|
|
||||||
|
### Camera
|
||||||
|
|
||||||
|
`Camera{ fovDeg = 60, nearPlane, farPlane, orthographic, orthoSize }` (vertical field of view, or vertical extent in world
|
||||||
|
units when orthographic). It looks down the entity's -Z.
|
||||||
|
|
||||||
|
### Text
|
||||||
|
|
||||||
|
`Text` (screen-space label): set `value`, `position` (pixels from the top-left), `size`, `color` - that is how HUDs are made.
|
||||||
|
It is a `Renderable`, so it hides like any renderer.
|
||||||
|
|
||||||
|
## Resources (nothing is duplicated)
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
MeshPtr m = Resources::box({1, 2, 3}); // same arguments -> the same shared object
|
MeshPtr m = Resources::box({1, 2, 3}); // same arguments -> the same shared object
|
||||||
@@ -139,20 +442,92 @@ The cache holds weak references: a resource lives exactly as long as something u
|
|||||||
afterwards. Sharing pays off three times:
|
afterwards. Sharing pays off three times:
|
||||||
|
|
||||||
* **CPU**: one `Mesh` however many entities use it (clones share too).
|
* **CPU**: one `Mesh` however many entities use it (clones share too).
|
||||||
* **GPU**: one buffer per `Mesh`, and every entity using it is drawn with **GPU instancing**: one draw call per distinct mesh, per-instance transform and tint in a single instance buffer uploaded once per frame, top-down transform pass (O(N)), frustum culling per instance. `showStats` prints `draw calls / instances / culled`. (`./build/instancing`: 20,000 cubes -> 1 draw call.)
|
* **GPU**: one buffer per `Mesh`, and every entity using it is drawn with **GPU instancing**: one draw call per distinct mesh, per-instance transform, tint and material in a single instance buffer uploaded once per frame, top-down transform pass (O(N)), frustum culling per instance. `showStats` prints `draw calls / instances / culled`. (`./build/instancing`: 20,000 cubes -> 1 draw call.)
|
||||||
* **Physics**: Jolt collision shapes are shared by bodies that ask for the same shape (type, size, scale, mesh).
|
* **Physics**: Jolt collision shapes are shared by bodies that ask for the same shape (type, size, scale, mesh).
|
||||||
|
|
||||||
### Physics
|
## Physics
|
||||||
|
|
||||||
* A `Collider` makes a body. With a `RigidBody` (`Static / Kinematic / Dynamic`) it behaves as requested; without one it is static. One `Collider` per entity.
|
* A `Collider` makes a body. With a `RigidBody` (`Static / Kinematic / Dynamic`) it behaves as requested; without one it is static. One `Collider` per entity.
|
||||||
* Shapes: `Box, Sphere, Capsule, Cylinder, ConvexHull, Mesh` (triangle meshes are static-only; asked to be dynamic they fall back to a convex hull). Shapes are baked with the entity's world scale when the body is created; change a setting, then call `rebuild()`.
|
* Shapes: `Box, Sphere, Capsule, Cylinder, ConvexHull, Mesh` (triangle meshes are static-only; asked to be dynamic they fall back to a convex hull).
|
||||||
* Sync per fixed step: entity -> body (kinematic bodies are *moved*, static/dynamic bodies are teleported only if you moved the entity), simulate, then dynamic bodies -> entity.
|
* **Colliders follow their entity's transform.** The shape is sized from the entity's **world** scale (parents included) and its `offset` scales with it: scale the entity, or any ancestor, and the collider grows or shrinks with it; rotate or move a parent and the collider goes along. A box scales per axis, a sphere stays round (the largest axis wins), a capsule or cylinder takes its width from the larger of X/Z and its length from Y, a hull or mesh scales per axis.
|
||||||
|
* **Mirroring** (a negative scale on the entity or any ancestor) flips whatever can land on the other side of the entity: an off-centre `offset`, a `ConvexHull`, a `Mesh`. A centred box, sphere, capsule or cylinder looks the same mirrored, so flipping those costs nothing (no new shape). A hull scaled alike on every axis, and any mesh, is a cheap wrapper around one shared shape; a hull stretched unevenly gets its own, built from the scaled points so it keeps the proportions of its mesh.
|
||||||
|
* Editing `shape`, `size`, `radius`, `height`, `offset` or `mesh` on a live collider takes effect by itself on the next step: no `rebuild()`. Static and kinematic bodies swap the shape in place; a dynamic body is rebuilt with its linear and angular velocity carried over (its contact events start over: `collisionEnter` fires again). Shapes stay shared (and are released again) while a scale animates.
|
||||||
|
* Surface and body settings (`friction`, `restitution`, `trigger`, `mass`, damping, ...) are read when the body is created: change them at runtime, then call `rebuild()` on the component.
|
||||||
|
* Sync per fixed step: entity -> body (kinematic bodies are *moved*, static/dynamic bodies are teleported only if you moved the entity: a change under 0.1 mm or 0.01 degrees is ignored and slow drifts add up until they count), simulate, then dynamic bodies -> entity. That write-back is position and rotation only: the scale you authored, and the picture it makes, stay as they are, whichever signs the scales on the entity or its parents have. Animate kinematic parts from `fixedUpdate()` so they get a pose per step.
|
||||||
* `Collider::trigger = true` makes a sensor. Contact events call `collisionEnter/Exit(Entity other)` and `triggerEnter/Exit(Entity other)` on the components of both entities.
|
* `Collider::trigger = true` makes a sensor. Contact events call `collisionEnter/Exit(Entity other)` and `triggerEnter/Exit(Entity other)` on the components of both entities.
|
||||||
* `CharacterController` (Jolt `CharacterVirtual`): origin at the feet; call `move(velocity)` **every frame** (it is used by the next frame's steps, then forgotten), `jump(speed)`, read `grounded()`. Slopes, stairs and gravity are handled inside the step.
|
* `CharacterController` (Jolt `CharacterVirtual`): origin at the feet; call `move(velocity)` **every frame** (it is used by the next frame's steps, then forgotten), `jump(speed)`, read `grounded()`. Slopes, stairs and gravity are handled inside the step. Its capsule follows the entity's scale (radius from X/Z, height from Y) and keeps its fall speed when it is rebuilt.
|
||||||
* `Joint`: `Fixed, Point, Hinge, Distance, Slider`, created in world space from the current poses.
|
* `Joint`: `Fixed, Point, Hinge, Distance, Slider`, created in world space from the current poses.
|
||||||
* `world.physics().raycast(origin, dir, maxDist, hit)`, `setGravity()`.
|
* `world.physics().raycast(origin, dir, maxDist, hit)`, `setGravity()`.
|
||||||
|
* Hiding an entity does not touch its collider (see *Showing and hiding*).
|
||||||
|
|
||||||
### Semantics worth knowing
|
## `eng::gfx`: raylib's API, the engine's way
|
||||||
|
|
||||||
|
`#include <engine/gfx.hpp>` (the umbrella `engine.hpp` includes it for you when it is part of the build, and defines
|
||||||
|
`ENGINE_HAS_GFX`). Every raylib function from `raylib.h`, and from `rlgl.h` and `raymath.h` when present, is available as
|
||||||
|
`eng::gfx::<name>`:
|
||||||
|
|
||||||
|
| raylib | engine |
|
||||||
|
|---|---|
|
||||||
|
| `DrawCircle(...)`, `LoadTexture(...)` | `gfx::drawCircle(...)`, `gfx::loadTexture(...)`: first letter lower-case |
|
||||||
|
| `rlBegin(...)`, `Vector3Add(...)` | `gfx::rlBegin(...)`, `gfx::vector3Add(...)` |
|
||||||
|
| `KEY_SPACE`, `FLAG_VSYNC_HINT`, enumerators, `#define` constants | `gfx::KEY_SPACE`, `gfx::FLAG_VSYNC_HINT`: raylib's names |
|
||||||
|
| `RED`, `SKYBLUE`, ... | `gfx::colors::Red`, `gfx::colors::SkyBlue` |
|
||||||
|
| `Vector2`, `Vector3`, `Quaternion`, `Color`, `BoundingBox` | the engine's own `Vec2`, `Vec3`, `Quat`, `Rgba`, `Aabb` (the same type, layout checked at compile time) |
|
||||||
|
| `Matrix` passed by value | `Mat4` (converted by the wrapper) |
|
||||||
|
| everything else (`Texture2D`, `Font`, `Model`, `Camera3D`, `Rectangle`, `Ray`, ...) | raylib's own layout under raylib's own name, in `eng::gfx` |
|
||||||
|
| `const char*` parameters | an extra overload taking `std::string` |
|
||||||
|
| `TextFormat(...)`, `TraceLog(...)` | `gfx::textFormat(...)` returns a `std::string`; `gfx::traceLog(level, fmt, ...)` takes printf arguments |
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Overlay : Component {
|
||||||
|
Entity camera, target;
|
||||||
|
|
||||||
|
void drawGizmos() { // 3D, inside the camera's view
|
||||||
|
gfx::drawSphereWires(target.transform().worldPosition(), 1.0f, 12, 12, gfx::colors::Orange);
|
||||||
|
}
|
||||||
|
void drawGui() { // 2D, on top of the finished frame
|
||||||
|
const Vec2 p = gfx::worldToScreen(camera, target.transform().worldPosition());
|
||||||
|
gfx::drawText("target", static_cast<int>(p.x), static_cast<int>(p.y), 20, gfx::colors::White);
|
||||||
|
gfx::drawFPS(10, 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules of the road:
|
||||||
|
|
||||||
|
* **Draw from `drawGizmos()` / `drawGui()`.** `run()` owns the window, the frame and the 3D camera: `InitWindow`,
|
||||||
|
`CloseWindow`, `BeginDrawing`, `EndDrawing`, `BeginMode3D`, `ClearBackground`, `SetTargetFPS`, ... exist in `eng::gfx` for
|
||||||
|
completeness, but the engine calls them itself; use `RunConfig` for the window.
|
||||||
|
* Anything that needs the window (`loadTexture`, `loadFont`, `loadModel`, `loadShader`, audio, ...) must run after it
|
||||||
|
exists: from a component callback during `run()`, not before it.
|
||||||
|
* Do not include `<raylib.h>`, `<rlgl.h>` or `<raymath.h>` **before** `<engine/gfx.hpp>` in the same file: they define macros
|
||||||
|
(`RED`, `PI`, ...) that would corrupt the declarations, and the headers stop you with an `#error`. Including them after is
|
||||||
|
fine, and qualifying names (`gfx::...`) is always safe. A file that includes `engine.hpp` and `raylib.h` together keeps
|
||||||
|
working: the umbrella header then simply leaves `eng::gfx` out (test `ENGINE_HAS_GFX` if your code must also build without it).
|
||||||
|
* Glue between the engine and raylib, hand-written in `gfx.hpp`:
|
||||||
|
`gfx::toCamera3D(entity)` (or `toCamera3D(transform, camera)`) gives the raylib `Camera3D` of a camera entity from its
|
||||||
|
**world** transform and `Camera` component; `gfx::worldToScreen(cameraEntity, pos)` and
|
||||||
|
`gfx::screenRay(cameraEntity, screenPos)` do the projections.
|
||||||
|
|
||||||
|
**Where it comes from.** The wrappers are not written by hand: `tools/gfx_gen` reads `raylib.h` (+ `rlgl.h`, `raymath.h`)
|
||||||
|
of the raylib that is being built, at configure time, with its own small C preprocessor (so `#if`/`#ifdef` blocks follow the
|
||||||
|
same defines as your compiler), and writes the public headers and the wrapper sources, so `eng::gfx` always matches the
|
||||||
|
raylib version you build against. Because nothing generated can be trusted blindly, every step is checked and every failure
|
||||||
|
degrades gracefully instead of breaking the engine:
|
||||||
|
|
||||||
|
* every generated source is compiled against raylib's real headers (including exact-type checks of struct fields that the
|
||||||
|
engine's types stand in for); an optional group (`rlgl`, `raymath`) that fails is left out with a warning, a failing core
|
||||||
|
group switches `eng::gfx` off (the engine still builds, without it);
|
||||||
|
* the number of functions found is compared with the number of `RLAPI` / `RMAPI` declarations in the headers;
|
||||||
|
* a generated self-test (a recording stand-in for raylib that checks every wrapper passes every argument, in order,
|
||||||
|
unchanged; hundreds of checks) is built and run once at configure time, and again as the `gfx_selftest` ctest;
|
||||||
|
* results are cached on a fingerprint of the generator, the headers and the definitions, so re-configuring is instant.
|
||||||
|
|
||||||
|
Items that cannot be wrapped safely (variadic functions other than `TextFormat`/`TraceLog`, function-pointer parameters,
|
||||||
|
types the generator does not know) are skipped and listed in `<build>/gfx_gen/out/gfx_report.txt` and in the configure
|
||||||
|
log. `-DENGINE_GFX_STRICT=ON` turns any degradation into an error; `-DENGINE_BUILD_GFX=OFF` turns `eng::gfx` off.
|
||||||
|
|
||||||
|
## Semantics worth knowing
|
||||||
|
|
||||||
* Angles are **degrees**, Euler order is Unity's (Z, then X, then Y). Right-handed, Y up, forward = -Z.
|
* Angles are **degrees**, Euler order is Unity's (Z, then X, then Y). Right-handed, Y up, forward = -Z.
|
||||||
* `Locality::Local` for `*To` sets the stored local value; for `translateBy/rotateBy` it means *the entity's own axes* (Unity's `Space.Self`). `Locality::World` for `*To` sets the world value; for `*By` it works along world axes. `scaleBy` multiplies component-wise either way (world scale is lossy under rotated, non-uniformly scaled parents).
|
* `Locality::Local` for `*To` sets the stored local value; for `translateBy/rotateBy` it means *the entity's own axes* (Unity's `Space.Self`). `Locality::World` for `*To` sets the world value; for `*By` it works along world axes. `scaleBy` multiplies component-wise either way (world scale is lossy under rotated, non-uniformly scaled parents).
|
||||||
@@ -160,6 +535,7 @@ afterwards. Sharing pays off three times:
|
|||||||
* **Callbacks must be public.** They are detected at compile time (C++20 concepts) when a component type is first added; the World keeps plain function pointers per type. A private `update()` is silently not called.
|
* **Callbacks must be public.** They are detected at compile time (C++20 concepts) when a component type is first added; the World keeps plain function pointers per type. A private `update()` is silently not called.
|
||||||
* `destroy()` is deferred to the end of the frame while `World::update` runs (safe inside callbacks) and immediate otherwise; a destroyed entity takes its subtree with it. Disabled entities/components get no callbacks, and their bodies leave the simulation.
|
* `destroy()` is deferred to the end of the frame while `World::update` runs (safe inside callbacks) and immediate otherwise; a destroyed entity takes its subtree with it. Disabled entities/components get no callbacks, and their bodies leave the simulation.
|
||||||
* `add<T>(T)` replaces an existing component of that type and calls `awake()` again. References from `get<T>()` are invalidated by removing components of that type; don't hold them across `destroy()`/`remove()`.
|
* `add<T>(T)` replaces an existing component of that type and calls `awake()` again. References from `get<T>()` are invalidated by removing components of that type; don't hold them across `destroy()`/`remove()`.
|
||||||
|
* Hiding (`setVisible`, `visible`, `childrenVisible`) is visual only; enabling/disabling (`setEntityEnabled`, `setComponentEnabled`) switches behaviour off.
|
||||||
|
|
||||||
## SVG -> 3D
|
## SVG -> 3D
|
||||||
|
|
||||||
@@ -185,19 +561,63 @@ Decisions made where the brief left room (each lives in one place and is easy to
|
|||||||
* `color` defaults to the SVG `fill`. Stroke-only shapes (`fill:none`) become outlines via Clipper2 offsetting (`stroke-width`, `linecap`, `linejoin`), so a drawn line becomes a wall. Fill rules (`nonzero`/`evenodd`) are honoured, so holes and islands-in-holes work.
|
* `color` defaults to the SVG `fill`. Stroke-only shapes (`fill:none`) become outlines via Clipper2 offsetting (`stroke-width`, `linecap`, `linejoin`), so a drawn line becomes a wall. Fill rules (`nonzero`/`evenodd`) are honoured, so holes and islands-in-holes work.
|
||||||
* Mapping: world X = svg x, world Z = svg y, Y = elevation, so the drawing looks identical from above. `ExtrudeOptions::center` puts the viewBox centre at the origin.
|
* Mapping: world X = svg x, world Z = svg y, Y = elevation, so the drawing looks identical from above. `ExtrudeOptions::center` puts the viewBox centre at the origin.
|
||||||
* Supported SVG: `g`, `path` (all commands incl. arcs), `rect` (with rx/ry), `circle`, `ellipse`, `line`, `polyline`, `polygon`, `transform`, `style`/presentation attributes, `display:none`. Skipped with a warning (`Model::warnings`): `<use>`, `<text>`, `<image>`; CSS stylesheets, gradients, clip paths and masks are ignored.
|
* Supported SVG: `g`, `path` (all commands incl. arcs), `rect` (with rx/ry), `circle`, `ellipse`, `line`, `polyline`, `polygon`, `transform`, `style`/presentation attributes, `display:none`. Skipped with a warning (`Model::warnings`): `<use>`, `<text>`, `<image>`; CSS stylesheets, gradients, clip paths and masks are ignored.
|
||||||
* Output meshes are closed, consistently wound surfaces: walls get smooth normals across edges bending less than `smoothAngleDeg`, caps are flat.
|
* Output meshes are closed, consistently wound surfaces, so they also work as `AmbientZone` meshes. **Shading normals** are averaged per welded position across edges bending less than `smoothAngleDeg` (default 40) and left crisp beyond it: circles and arcs shade as true curves, gentle lofts (`e=`) shade smoothly across levels, and sharp edges (wall/cap, ledges, pyramid faces) stay crisp. Caps of flat shapes are flat.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
* `engine_tests` (`tests/tests.cpp`, headless, no window or GL): 67 tests over the ECS and transforms, extrusion and SVG
|
||||||
|
(including the normals), visibility rules, light budgeting and shadow-map fitting (coverage, texel snapping), ambient zones
|
||||||
|
(every shape against its definition, blending, falloff, mesh voxelisation, the incremental bake against a from-scratch
|
||||||
|
bake byte for byte, exact gamma encoding, hostile input; a zone that walks costs what it covers, the rounded grid extent,
|
||||||
|
the colour-scale ladder, and a GPU copy that is only ever updated the way the renderer does it stays equal to the grid),
|
||||||
|
textures (sampling, wrapping, decoders, sharing), mesh texture coordinates, materials (defaults, the clamped values the
|
||||||
|
shader gets, which UV source a draw uses, batches split per material), the frame planner (what a frame draws and every shader
|
||||||
|
uniform), collider maths (world-scale dimensions, which scales need a new shape, and the write-back rotation for every
|
||||||
|
combination of mirrored entity and mirrored parent), the `eng::gfx` camera helpers and, with the real physics backend,
|
||||||
|
colliders following their entities: rays are dropped on every kind of collider (box, hull, triangle mesh; scaled, offset,
|
||||||
|
turned, mirrored, under flipped parents, kinematic, spinning) to check that each sits exactly where its entity is drawn,
|
||||||
|
plus scaled sensors, dynamic bodies that are rescaled at runtime and characters whose capsule follows their scale.
|
||||||
|
* `gfx_selftest`: the generated `eng::gfx` wrappers against a recording stand-in for raylib.
|
||||||
|
* `gpu_check` (`-DENGINE_BUILD_GPU_CHECK=ON`, Linux): `src/render.cpp` compiled against a software OpenGL implementation of the
|
||||||
|
raylib calls it makes (`tools/gpu_check/gl_shim.cpp`), run on Mesa's llvmpipe through EGL (no display, no GPU). It renders
|
||||||
|
scenes and compares the pixels with an independent CPU model of the lighting, exact shadow ray tests and the CPU ambient grid.
|
||||||
|
For materials the model evaluates what `material.hpp` documents (every property as a number and as a map, the normal map's
|
||||||
|
tilt direction, the texture orientation) and is compared on every face of a box, with the mesh's UVs and with the world
|
||||||
|
projection, on an instance mirrored in x, unlit, with clamped / pixelated textures, a texture decoded from a PNG file, and the
|
||||||
|
draw calls of one mesh with four materials.
|
||||||
|
* `-DENGINE_SANITIZE=ON` runs all of the above with ASan + UBSan on the engine's own code.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
cmake/cpu.cmake deps.cmake CPU-level probe; pinned FetchContent declarations
|
CMakeLists.txt targets: engine_core, engine_render, engine_gfx (generated), engine (what you link), demo, instancing, tests
|
||||||
include/engine/ engine.hpp (the one header) + math ecs input graphics model resources physics app
|
cmake/ cpu.cmake (CPU-level probe), deps.cmake (pinned FetchContent), gfx.cmake (eng::gfx: generate, check, cache)
|
||||||
include/engine/detail/ internal: svg parser, extrusion, raw input state (not included by engine.hpp)
|
include/engine/ engine.hpp (the one header) + math ecs input texture material graphics model resources physics app, gfx.hpp (eng::gfx glue)
|
||||||
src/ ecs input resources physics(Jolt, pimpl) svg(pugixml) extrude(Clipper2) spawn | render app (raylib)
|
include/engine/detail/ internal, not included by engine.hpp: svg parser, extrusion, collider maths, scene gathering / light
|
||||||
|
budget / shadow fitting / ambient zones (scene.hpp), the frame planner (frame.hpp), raw input state
|
||||||
|
src/ ecs visibility input resources texture material physics (Jolt, pimpl) svg (pugixml) extrude (Clipper2) spawn
|
||||||
|
scene ambient frame (lights, shadows, zones, draw plan; headless) | render app shaders (raylib)
|
||||||
|
tools/gfx_gen/ the generator behind eng::gfx: C preprocessor, declaration parser, wrapper + self-test emitters
|
||||||
|
tools/gpu_check/ optional: the renderer on a software OpenGL driver, pixel-compared with a CPU model
|
||||||
examples/demo, instancing the clean entry point in practice
|
examples/demo, instancing the clean entry point in practice
|
||||||
tests/tests.cpp headless suite: 31 tests, 359 checks
|
tests/tests.cpp headless suite: 67 tests
|
||||||
```
|
```
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
One `Collider` per entity (no compound colliders from child entities); the lit shader is GLSL 330 (desktop GL only, one directional light, no shadows, no texture support yet); `destroy()` while iterating an `EntityIterator` outside `World::update` is not safe.
|
* One `Collider` per entity (no compound colliders from child entities).
|
||||||
|
* Colliders follow rotation, uniform and per-axis scale and mirroring through any hierarchy. The one thing they cannot
|
||||||
|
follow exactly is a *dynamic* body under a parent that is both rotated and scaled unevenly (a sheared parent): a rigid
|
||||||
|
body cannot shear, so it is shown with the closest rotation.
|
||||||
|
* Rebuilding a dynamic body for a new scale restarts its contact events (`collisionEnter` fires again for what it touches).
|
||||||
|
* The lit shader is GLSL 330 (desktop GL only). Materials: six maps (albedo, diffuse, specular, metalness, roughness, normal; one
|
||||||
|
texture unit each, so a draw uses 11 of the 16 a GL 3.3 driver must have), RGBA8 textures decoded as gamma 2.2 (not the exact
|
||||||
|
sRGB curve), no emission, ambient-occlusion, height/parallax or detail maps; an albedo map's alpha makes the surface
|
||||||
|
translucent but transparent objects are not sorted. `UvMode::World` samples every map three times per pixel (one per
|
||||||
|
axis) and a world-projected texture does not move with its object.
|
||||||
|
* Lighting: at most 16 lights and 3 shadow maps per frame; point lights do not cast shadows; no global illumination,
|
||||||
|
environment maps or light probes (ambient is the hemisphere / zone colour); ambient zones are a baked 3D grid (resolution
|
||||||
|
= `zoneCellSize`), so detail finer than a cell is smoothed and `Mesh` zones must be closed meshes.
|
||||||
|
* `destroy()` while iterating an `EntityIterator` outside `World::update` is not safe.
|
||||||
|
* `eng::gfx` wraps what the generator can wrap safely; anything else is listed in `gfx_report.txt`. Functions that own the
|
||||||
|
frame or window (`BeginDrawing`, `InitWindow`, ...) are there but belong to `run()`.
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mkdir build -p
|
|
||||||
cd build
|
|
||||||
cmake .. -DUSE_AVX2=OFF -DUSE_AVX=OFF
|
|
||||||
cmake --build .
|
|
||||||
cd ..
|
|
||||||
+334
@@ -0,0 +1,334 @@
|
|||||||
|
# eng::gfx - the engine's equivalent of every raylib function.
|
||||||
|
#
|
||||||
|
# The wrappers are not written by hand. tools/gfx_gen reads raylib.h (+ rlgl.h and raymath.h) of the raylib that is
|
||||||
|
# being built, at configure time, and writes include/engine/gfx_api.hpp (+ gfx_rlgl_api.hpp, gfx_raymath_api.hpp)
|
||||||
|
# and the wrapper sources, so the wrappers always match the raylib version in use.
|
||||||
|
#
|
||||||
|
# Because nothing here can be trusted blindly, every step is checked, and every failure degrades gracefully instead
|
||||||
|
# of breaking the build of the engine:
|
||||||
|
# * the generator is compiled and run (a prebuilt one can be given with -DENGINE_GFX_GEN_EXECUTABLE=... when cross-compiling);
|
||||||
|
# * each generated source is compiled against raylib's real headers; a failing OPTIONAL group (rlgl, raymath) is
|
||||||
|
# dropped and the rest is regenerated, a failing core group switches eng::gfx off;
|
||||||
|
# * the generated self-test (a recording stand-in for raylib that checks every wrapper passes every argument, in
|
||||||
|
# order, unchanged - see tools/gfx_gen/emit_test.hpp) is built and run once;
|
||||||
|
# * the number of functions found is compared with the number of RLAPI / RMAPI lines in the headers.
|
||||||
|
# The configure log says what happened. -DENGINE_GFX_STRICT=ON turns every degradation into an error.
|
||||||
|
#
|
||||||
|
# Results are cached on a fingerprint of (generator sources, raylib headers, definitions): re-configuring is instant.
|
||||||
|
#
|
||||||
|
# Usage (CMakeLists.txt): engine_configure_gfx() -> sets ENGINE_GFX_OK and, when ON:
|
||||||
|
# ENGINE_GFX_SOURCES wrapper sources ENGINE_GFX_INCLUDE_DIR generated headers
|
||||||
|
# ENGINE_GFX_RAYLIB_INCLUDE raylib's header directory ENGINE_GFX_DEFINITIONS definitions raylib's headers are read with
|
||||||
|
# ENGINE_GFX_SELFTEST_SOURCES (empty when the self-test could not be built)
|
||||||
|
include_guard(GLOBAL)
|
||||||
|
|
||||||
|
set(ENGINE_GFX_STRICT OFF CACHE BOOL "Fail the configure step when eng::gfx cannot be generated (default: warn and build the engine without it)")
|
||||||
|
set(ENGINE_GFX_GEN_EXECUTABLE "" CACHE FILEPATH "A prebuilt gfx_gen to use instead of building one (needed when cross-compiling)")
|
||||||
|
|
||||||
|
function(_engine_gfx_problem what)
|
||||||
|
if(ENGINE_GFX_STRICT)
|
||||||
|
message(FATAL_ERROR "eng::gfx: ${what}")
|
||||||
|
endif()
|
||||||
|
message(WARNING "eng::gfx: ${what}\n"
|
||||||
|
" The engine is built without eng::gfx. (-DENGINE_GFX_STRICT=ON makes this an error, -DENGINE_BUILD_GFX=OFF turns eng::gfx off.)")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# The first few lines of a compiler log that look like errors.
|
||||||
|
function(_engine_gfx_errors log out_var)
|
||||||
|
string(REGEX MATCHALL "[^\n]*(error|Error|ERROR|undefined reference|LNK[0-9]+)[^\n]*" _lines "${log}")
|
||||||
|
list(LENGTH _lines _n)
|
||||||
|
if(_n GREATER 8)
|
||||||
|
list(SUBLIST _lines 0 8 _lines)
|
||||||
|
endif()
|
||||||
|
string(REPLACE ";" "\n " _text "${_lines}")
|
||||||
|
if(_text STREQUAL "")
|
||||||
|
string(SUBSTRING "${log}" 0 600 _text)
|
||||||
|
endif()
|
||||||
|
set(${out_var} "${_text}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(_engine_gfx_count_lines file regex out_var)
|
||||||
|
file(STRINGS "${file}" _m REGEX "${regex}")
|
||||||
|
list(LENGTH _m _n)
|
||||||
|
set(${out_var} ${_n} PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(engine_configure_gfx)
|
||||||
|
set(ENGINE_GFX_OK OFF PARENT_SCOPE)
|
||||||
|
|
||||||
|
# ---- raylib's headers ----------------------------------------------------------------------------------------
|
||||||
|
set(_dirs)
|
||||||
|
if(raylib_SOURCE_DIR)
|
||||||
|
list(APPEND _dirs "${raylib_SOURCE_DIR}/src")
|
||||||
|
endif()
|
||||||
|
get_target_property(_ifdirs raylib INTERFACE_INCLUDE_DIRECTORIES)
|
||||||
|
if(_ifdirs)
|
||||||
|
foreach(_d IN LISTS _ifdirs)
|
||||||
|
string(REGEX REPLACE "^\\$<BUILD_INTERFACE:(.*)>$" "\\1" _d "${_d}")
|
||||||
|
if(NOT _d MATCHES "\\$<")
|
||||||
|
list(APPEND _dirs "${_d}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
set(_hdr_dir "")
|
||||||
|
foreach(_d IN LISTS _dirs)
|
||||||
|
if(EXISTS "${_d}/raylib.h")
|
||||||
|
set(_hdr_dir "${_d}")
|
||||||
|
break()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
if(_hdr_dir STREQUAL "")
|
||||||
|
_engine_gfx_problem("raylib.h was not found (looked in: ${_dirs})")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
set(_raylib_h "${_hdr_dir}/raylib.h")
|
||||||
|
set(_rlgl_h "")
|
||||||
|
set(_raymath_h "")
|
||||||
|
if(EXISTS "${_hdr_dir}/rlgl.h")
|
||||||
|
set(_rlgl_h "${_hdr_dir}/rlgl.h")
|
||||||
|
endif()
|
||||||
|
if(EXISTS "${_hdr_dir}/raymath.h")
|
||||||
|
set(_raymath_h "${_hdr_dir}/raymath.h")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Definitions raylib's headers are read with (they decide e.g. the type of rlVertexBuffer::indices).
|
||||||
|
set(_defs)
|
||||||
|
get_target_property(_cd raylib INTERFACE_COMPILE_DEFINITIONS)
|
||||||
|
if(_cd)
|
||||||
|
foreach(_x IN LISTS _cd)
|
||||||
|
if(_x AND NOT _x MATCHES "\\$<")
|
||||||
|
list(APPEND _defs "${_x}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
if(DEFINED GRAPHICS AND GRAPHICS MATCHES "^GRAPHICS_API_") # raylib's own CMake option
|
||||||
|
list(APPEND _defs "${GRAPHICS}")
|
||||||
|
endif()
|
||||||
|
list(REMOVE_DUPLICATES _defs)
|
||||||
|
|
||||||
|
set(_bin "${CMAKE_BINARY_DIR}/gfx_gen")
|
||||||
|
set(_out "${_bin}/out")
|
||||||
|
set(_src_of_raylib gfx_core.cpp)
|
||||||
|
set(_src_of_rlgl gfx_rlgl.cpp)
|
||||||
|
set(_src_of_raymath gfx_raymath.cpp)
|
||||||
|
set(_inc_flags)
|
||||||
|
foreach(_x IN LISTS _defs)
|
||||||
|
list(APPEND _inc_flags "-D${_x}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# ---- fingerprint -----------------------------------------------------------------------------------------------
|
||||||
|
set(_gen_dir "${PROJECT_SOURCE_DIR}/tools/gfx_gen")
|
||||||
|
file(GLOB _gen_files "${_gen_dir}/*.cpp" "${_gen_dir}/*.hpp")
|
||||||
|
list(SORT _gen_files)
|
||||||
|
set(_key "v1")
|
||||||
|
foreach(_f IN LISTS _gen_files _raylib_h _rlgl_h _raymath_h)
|
||||||
|
file(SHA256 "${_f}" _h)
|
||||||
|
string(APPEND _key "|${_h}")
|
||||||
|
endforeach()
|
||||||
|
string(APPEND _key "|${_defs}|${CMAKE_CXX_COMPILER_ID}${CMAKE_CXX_COMPILER_VERSION}|${ENGINE_GFX_GEN_EXECUTABLE}")
|
||||||
|
string(SHA256 _key "${_key}")
|
||||||
|
|
||||||
|
if("${ENGINE_GFX_CACHE_KEY}" STREQUAL "${_key}" AND ENGINE_GFX_CACHE_GROUPS MATCHES "raylib"
|
||||||
|
AND EXISTS "${_out}/gfx_core.cpp" AND EXISTS "${_out}/include/engine/gfx_api.hpp")
|
||||||
|
set(_groups ${ENGINE_GFX_CACHE_GROUPS})
|
||||||
|
set(_selftest "${ENGINE_GFX_CACHE_SELFTEST}")
|
||||||
|
set(_summary "${ENGINE_GFX_CACHE_SUMMARY}")
|
||||||
|
message(STATUS "engine: eng::gfx ${_summary} (cached)")
|
||||||
|
else()
|
||||||
|
# ---- the generator -------------------------------------------------------------------------------------------
|
||||||
|
if(ENGINE_GFX_GEN_EXECUTABLE)
|
||||||
|
set(_gen "${ENGINE_GFX_GEN_EXECUTABLE}")
|
||||||
|
else()
|
||||||
|
set(_gen "${_bin}/gfx_gen${CMAKE_EXECUTABLE_SUFFIX}")
|
||||||
|
message(STATUS "engine: building the eng::gfx generator")
|
||||||
|
unset(ENGINE_GFX_GEN_BUILT CACHE)
|
||||||
|
try_compile(ENGINE_GFX_GEN_BUILT "${_bin}/gen_build"
|
||||||
|
SOURCES "${_gen_dir}/gfx_gen.cpp"
|
||||||
|
CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF
|
||||||
|
COPY_FILE "${_gen}"
|
||||||
|
OUTPUT_VARIABLE _gen_log)
|
||||||
|
if(NOT ENGINE_GFX_GEN_BUILT)
|
||||||
|
_engine_gfx_errors("${_gen_log}" _err)
|
||||||
|
_engine_gfx_problem("the generator tools/gfx_gen could not be compiled:\n ${_err}")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ---- generate, compile-check, drop what does not compile --------------------------------------------------
|
||||||
|
set(_groups raylib)
|
||||||
|
if(_rlgl_h)
|
||||||
|
list(APPEND _groups rlgl)
|
||||||
|
endif()
|
||||||
|
if(_raymath_h)
|
||||||
|
list(APPEND _groups raymath)
|
||||||
|
endif()
|
||||||
|
set(_try_incs "${_out}/include\;${PROJECT_SOURCE_DIR}/include\;${_hdr_dir}")
|
||||||
|
|
||||||
|
set(_ok FALSE)
|
||||||
|
foreach(_round RANGE 1 4)
|
||||||
|
set(_args --raylib "${_raylib_h}" --out "${_out}")
|
||||||
|
if("rlgl" IN_LIST _groups)
|
||||||
|
list(APPEND _args --rlgl "${_rlgl_h}")
|
||||||
|
endif()
|
||||||
|
if("raymath" IN_LIST _groups)
|
||||||
|
list(APPEND _args --raymath "${_raymath_h}")
|
||||||
|
endif()
|
||||||
|
list(APPEND _args ${_inc_flags})
|
||||||
|
execute_process(COMMAND "${_gen}" ${_args}
|
||||||
|
RESULT_VARIABLE _rc OUTPUT_VARIABLE _gen_out ERROR_VARIABLE _gen_err)
|
||||||
|
if(NOT _rc EQUAL 0)
|
||||||
|
if(_groups STREQUAL "raylib")
|
||||||
|
_engine_gfx_problem("gfx_gen failed on ${_raylib_h} (${_rc}): ${_gen_err}")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
message(WARNING "eng::gfx: gfx_gen failed (${_rc}: ${_gen_err}); retrying with raylib.h alone")
|
||||||
|
set(_groups raylib)
|
||||||
|
continue()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# compile every group against the REAL headers (the generated code is what must fit them)
|
||||||
|
set(_bad)
|
||||||
|
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
|
||||||
|
foreach(_g IN LISTS _groups)
|
||||||
|
unset(ENGINE_GFX_TRY_${_g} CACHE)
|
||||||
|
try_compile(ENGINE_GFX_TRY_${_g} "${_bin}/check_${_g}"
|
||||||
|
SOURCES "${_out}/${_src_of_${_g}}"
|
||||||
|
CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${_try_incs}"
|
||||||
|
COMPILE_DEFINITIONS ${_inc_flags}
|
||||||
|
CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF
|
||||||
|
OUTPUT_VARIABLE _log_${_g})
|
||||||
|
if(NOT ENGINE_GFX_TRY_${_g})
|
||||||
|
_engine_gfx_errors("${_log_${_g}}" _err)
|
||||||
|
if(_g STREQUAL "raylib")
|
||||||
|
_engine_gfx_problem("the wrappers generated from raylib.h do not compile against it:\n ${_err}")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
message(WARNING "eng::gfx: the wrappers generated from ${_g}.h do not compile against it, so ${_g}.h is left out:\n ${_err}")
|
||||||
|
list(APPEND _bad ${_g})
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
unset(CMAKE_TRY_COMPILE_TARGET_TYPE)
|
||||||
|
if(NOT _bad)
|
||||||
|
set(_ok TRUE)
|
||||||
|
break()
|
||||||
|
endif()
|
||||||
|
if(ENGINE_GFX_STRICT)
|
||||||
|
message(FATAL_ERROR "eng::gfx: generated wrappers for '${_bad}' do not compile (ENGINE_GFX_STRICT)")
|
||||||
|
endif()
|
||||||
|
list(REMOVE_ITEM _groups ${_bad})
|
||||||
|
endforeach()
|
||||||
|
if(NOT _ok)
|
||||||
|
_engine_gfx_problem("could not settle on a set of headers that compile")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ---- cross-check: did the generator see every function the headers declare? ----------------------------------
|
||||||
|
file(STRINGS "${_out}/gfx_report.txt" _sum_lines REGEX "^GFX_SUMMARY ")
|
||||||
|
set(_summary "")
|
||||||
|
foreach(_line IN LISTS _sum_lines)
|
||||||
|
if(_line MATCHES "^GFX_SUMMARY (raylib|rlgl|raymath) declared=([0-9]+) wrapped=([0-9]+) skipped=([0-9]+)")
|
||||||
|
set(_grp "${CMAKE_MATCH_1}")
|
||||||
|
set(_declared "${CMAKE_MATCH_2}")
|
||||||
|
set(_wrapped "${CMAKE_MATCH_3}")
|
||||||
|
set(_skipped "${CMAKE_MATCH_4}")
|
||||||
|
if(_grp STREQUAL "raylib")
|
||||||
|
_engine_gfx_count_lines("${_raylib_h}" "^RLAPI[ \t]" _expect)
|
||||||
|
elseif(_grp STREQUAL "rlgl")
|
||||||
|
_engine_gfx_count_lines("${_rlgl_h}" "^RLAPI[ \t]" _expect)
|
||||||
|
else()
|
||||||
|
_engine_gfx_count_lines("${_raymath_h}" "^RMAPI[ \t]" _expect)
|
||||||
|
endif()
|
||||||
|
if(NOT _declared EQUAL _expect)
|
||||||
|
set(_msg "gfx_gen found ${_declared} ${_grp} functions but the header has ${_expect} ${_grp} declaration lines (lines inside #if blocks that are off for this build are not wrapped, on purpose; anything else is a function the generator did not see); the report ${_out}/gfx_report.txt lists what was skipped")
|
||||||
|
if(ENGINE_GFX_STRICT)
|
||||||
|
message(FATAL_ERROR "eng::gfx: ${_msg}")
|
||||||
|
else()
|
||||||
|
message(WARNING "eng::gfx: ${_msg}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
string(APPEND _summary "${_grp} ${_wrapped}/${_declared}, ")
|
||||||
|
if(_skipped GREATER 0)
|
||||||
|
message(STATUS "engine: eng::gfx skipped ${_skipped} ${_grp} function(s) it cannot wrap safely; see ${_out}/gfx_report.txt")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
string(REGEX REPLACE ", $" "" _summary "${_summary}")
|
||||||
|
set(_summary "wrapped ${_summary} functions")
|
||||||
|
|
||||||
|
# ---- the self-test: build it (compile + link against the recording stand-in) and run it once ---------------
|
||||||
|
set(_selftest OFF)
|
||||||
|
set(_st_srcs)
|
||||||
|
foreach(_g IN LISTS _groups)
|
||||||
|
list(APPEND _st_srcs "${_out}/${_src_of_${_g}}")
|
||||||
|
endforeach()
|
||||||
|
list(APPEND _st_srcs "${_out}/selftest/gfx_mock.cpp" "${_out}/selftest/gfx_selftest.cpp")
|
||||||
|
set(_st_flags ${_inc_flags})
|
||||||
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
list(APPEND _st_flags -ffp-contract=off) # the raymath oracles compare bit for bit
|
||||||
|
endif()
|
||||||
|
set(_st_incs "${_out}/include\;${_out}/selftest\;${PROJECT_SOURCE_DIR}/include\;${_hdr_dir}")
|
||||||
|
unset(ENGINE_GFX_SELFTEST_BUILT CACHE)
|
||||||
|
set(_st_exe "${_bin}/gfx_selftest_configure${CMAKE_EXECUTABLE_SUFFIX}")
|
||||||
|
try_compile(ENGINE_GFX_SELFTEST_BUILT "${_bin}/check_selftest"
|
||||||
|
SOURCES ${_st_srcs}
|
||||||
|
CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${_st_incs}"
|
||||||
|
COMPILE_DEFINITIONS ${_st_flags}
|
||||||
|
CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF
|
||||||
|
COPY_FILE "${_st_exe}"
|
||||||
|
OUTPUT_VARIABLE _st_log)
|
||||||
|
if(NOT ENGINE_GFX_SELFTEST_BUILT)
|
||||||
|
_engine_gfx_errors("${_st_log}" _err)
|
||||||
|
if(ENGINE_GFX_STRICT)
|
||||||
|
message(FATAL_ERROR "eng::gfx: its self-test does not build:\n ${_err}")
|
||||||
|
endif()
|
||||||
|
message(WARNING "eng::gfx: the self-test of the wrappers does not build, so it is not part of the tests:\n ${_err}")
|
||||||
|
elseif(CMAKE_CROSSCOMPILING)
|
||||||
|
set(_selftest ON)
|
||||||
|
message(STATUS "engine: eng::gfx self-test built (cross-compiling: not run)")
|
||||||
|
else()
|
||||||
|
execute_process(COMMAND "${_st_exe}" RESULT_VARIABLE _rr OUTPUT_VARIABLE _ro ERROR_VARIABLE _re TIMEOUT 300)
|
||||||
|
set(_selftest ON)
|
||||||
|
if(_rr EQUAL 0)
|
||||||
|
string(REGEX MATCH "gfx_selftest: ([0-9]+) checks" _m "${_ro}")
|
||||||
|
message(STATUS "engine: eng::gfx self-test passed (${CMAKE_MATCH_1} checks)")
|
||||||
|
string(APPEND _summary "; self-test ${CMAKE_MATCH_1} checks passed")
|
||||||
|
else()
|
||||||
|
string(REGEX MATCHALL "FAIL [^\n]*" _fails "${_ro}")
|
||||||
|
list(LENGTH _fails _nf)
|
||||||
|
if(_nf GREATER 10)
|
||||||
|
list(SUBLIST _fails 0 10 _fails)
|
||||||
|
endif()
|
||||||
|
string(REPLACE ";" "\n " _fails "${_fails}")
|
||||||
|
if(ENGINE_GFX_STRICT)
|
||||||
|
message(FATAL_ERROR "eng::gfx: the self-test of the wrappers FAILED:\n ${_fails}")
|
||||||
|
endif()
|
||||||
|
message(WARNING "eng::gfx: the self-test of the generated wrappers FAILED (${_rr}); the wrappers named below pass wrong values to raylib:\n ${_fails}\n ${_re}\n"
|
||||||
|
" eng::gfx is still built. The failing functions should not be used; see ${_out}/gfx_report.txt.")
|
||||||
|
string(APPEND _summary "; self-test FAILED")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(ENGINE_GFX_CACHE_KEY "${_key}" CACHE INTERNAL "")
|
||||||
|
set(ENGINE_GFX_CACHE_GROUPS "${_groups}" CACHE INTERNAL "")
|
||||||
|
set(ENGINE_GFX_CACHE_SELFTEST "${_selftest}" CACHE INTERNAL "")
|
||||||
|
set(ENGINE_GFX_CACHE_SUMMARY "${_summary}" CACHE INTERNAL "")
|
||||||
|
message(STATUS "engine: eng::gfx generated from raylib's headers (${_summary})")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ---- results for the caller -------------------------------------------------------------------------------------
|
||||||
|
set(_srcs)
|
||||||
|
foreach(_g IN LISTS _groups)
|
||||||
|
list(APPEND _srcs "${_out}/${_src_of_${_g}}")
|
||||||
|
endforeach()
|
||||||
|
set(_st_srcs)
|
||||||
|
if(_selftest)
|
||||||
|
set(_st_srcs ${_srcs} "${_out}/selftest/gfx_mock.cpp" "${_out}/selftest/gfx_selftest.cpp")
|
||||||
|
endif()
|
||||||
|
set(ENGINE_GFX_OK ON PARENT_SCOPE)
|
||||||
|
set(ENGINE_GFX_SOURCES ${_srcs} PARENT_SCOPE)
|
||||||
|
set(ENGINE_GFX_INCLUDE_DIR "${_out}/include" PARENT_SCOPE)
|
||||||
|
set(ENGINE_GFX_SELFTEST_INCLUDE_DIR "${_out}/selftest" PARENT_SCOPE)
|
||||||
|
set(ENGINE_GFX_RAYLIB_INCLUDE "${_hdr_dir}" PARENT_SCOPE)
|
||||||
|
set(ENGINE_GFX_DEFINITIONS ${_defs} PARENT_SCOPE)
|
||||||
|
set(ENGINE_GFX_SELFTEST_SOURCES ${_st_srcs} PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
+61
-1
@@ -2,7 +2,11 @@
|
|||||||
<!-- Demo level. Every shape carries an Inkscape "label" (inkscape:label) with the extrusion labels:
|
<!-- Demo level. Every shape carries an Inkscape "label" (inkscape:label) with the extrusion labels:
|
||||||
y=<base elevation> h=<top elevation> e=(fraction,ratio)... color=#rrggbb
|
y=<base elevation> h=<top elevation> e=(fraction,ratio)... color=#rrggbb
|
||||||
#group @id vpatch spatch only=(...) except=(...)
|
#group @id vpatch spatch only=(...) except=(...)
|
||||||
Open it in Inkscape and edit the labels in Object Properties (Ctrl+Shift+O). -->
|
Open it in Inkscape and edit the labels in Object Properties (Ctrl+Shift+O).
|
||||||
|
|
||||||
|
Besides the park, the level has a HOUSE (south of the start) and a walled GARDEN (south-east corner): the demo gives the
|
||||||
|
house a 90% dark ambient zone with a lamp in each room, and the garden - an outdoor area inside the outdoors - purple
|
||||||
|
ambient light. See "ambient zones" in main.cpp. -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
width="100mm" height="100mm" viewBox="0 0 100 100">
|
width="100mm" height="100mm" viewBox="0 0 100 100">
|
||||||
@@ -49,4 +53,60 @@
|
|||||||
<rect inkscape:label="vpatch h=3 color=#ff5a5a only=(@ramp)" x="80" y="10" width="4" height="16"
|
<rect inkscape:label="vpatch h=3 color=#ff5a5a only=(@ramp)" x="80" y="10" width="4" height="16"
|
||||||
style="fill:#ff0000;fill-opacity:0.3"/>
|
style="fill:#ff0000;fill-opacity:0.3"/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
<g inkscape:label="house walls #house y=0 h=2.6 color=#d9c9ad" inkscape:groupmode="layer" id="layer5">
|
||||||
|
<!-- A house south of the start: 0.3-thick walls, a door in the north wall, a window in the west wall and a doorway in the partition.
|
||||||
|
The whole house is one ambient zone in demo/main.cpp (90% dark) with a lamp in each room. -->
|
||||||
|
<rect inkscape:label="@house_north_a" x="38" y="76" width="4" height="1"/>
|
||||||
|
<rect inkscape:label="@house_north_b" x="46" y="76" width="14" height="1"/>
|
||||||
|
<rect inkscape:label="@house_south" x="38" y="91" width="22" height="1"/>
|
||||||
|
<rect inkscape:label="@house_west_a" x="38" y="77" width="1" height="6.333"/>
|
||||||
|
<rect inkscape:label="@house_west_b" x="38" y="87.333" width="1" height="3.667"/>
|
||||||
|
<rect inkscape:label="@house_east" x="59" y="77" width="1" height="14"/>
|
||||||
|
<rect inkscape:label="@house_part_a" x="50.333" y="77" width="0.667" height="4.667"/>
|
||||||
|
<rect inkscape:label="@house_part_b" x="50.333" y="85.667" width="0.667" height="5.333"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g inkscape:label="house lintels #house y=2.0 h=2.6 color=#d9c9ad" inkscape:groupmode="layer" id="layer6">
|
||||||
|
<!-- above the door, the doorway and the window -->
|
||||||
|
<rect inkscape:label="@house_lintel_door" x="42" y="76" width="4" height="1"/>
|
||||||
|
<rect inkscape:label="@house_lintel_inner" x="50.333" y="81.667" width="0.667" height="4"/>
|
||||||
|
<rect inkscape:label="@house_lintel_window" x="38" y="83.333" width="1" height="4"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g inkscape:label="house sill #house y=0 h=0.9 color=#d9c9ad" inkscape:groupmode="layer" id="layer7">
|
||||||
|
<!-- below the window -->
|
||||||
|
<rect inkscape:label="@house_sill" x="38" y="83.333" width="1" height="4"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g inkscape:label="house floor #house y=0 h=0.06 color=#a58459" inkscape:groupmode="layer" id="layer8">
|
||||||
|
<!-- wooden floor (a slab 0.06 high, so the doorstep is a small step) -->
|
||||||
|
<rect inkscape:label="@house_floor" x="39" y="77" width="20" height="14"/>
|
||||||
|
<rect inkscape:label="@house_threshold" x="42" y="76" width="4" height="1"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g inkscape:label="house roof #house y=2.6 h=4.2 e=(1,0) color=#8b4a3a" inkscape:groupmode="layer" id="layer9">
|
||||||
|
<!-- hip roof: the top ring collapses to a point (e=(1,0)); it overhangs the walls by 0.3 -->
|
||||||
|
<rect inkscape:label="@house_roof" x="37" y="75" width="24" height="18"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g inkscape:label="garden walls #garden y=0 h=1.0 color=#8a789e" inkscape:groupmode="layer" id="layer10">
|
||||||
|
<!-- A walled garden in the south-east corner, open to the sky, with a gate in its north wall: an outdoor area inside the
|
||||||
|
outdoors. In demo/main.cpp it has its own ambient zone: purple. -->
|
||||||
|
<rect inkscape:label="@garden_north_a" x="77" y="78" width="7" height="1"/>
|
||||||
|
<rect inkscape:label="@garden_north_b" x="89" y="78" width="4.5" height="1"/>
|
||||||
|
<rect inkscape:label="@garden_west" x="77" y="79" width="1" height="14.5"/>
|
||||||
|
<rect inkscape:label="@garden_east" x="92.5" y="79" width="1" height="14.5"/>
|
||||||
|
<rect inkscape:label="@garden_south" x="78" y="92.5" width="14.5" height="1"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g inkscape:label="garden floor #garden y=0 h=0.05 color=#6d5d86" inkscape:groupmode="layer" id="layer11">
|
||||||
|
<!-- paving -->
|
||||||
|
<rect inkscape:label="@garden_floor" x="78" y="79" width="14.5" height="13.5"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g inkscape:label="garden obelisk #garden y=0.05 h=2.6 e=(1,0.35) color=#b9a7d6" inkscape:groupmode="layer" id="layer12">
|
||||||
|
<!-- a tapering pillar in the middle (the top ring shrinks to 35%) -->
|
||||||
|
<rect inkscape:label="@garden_obelisk" x="88.667" y="80.667" width="3" height="3"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 6.8 KiB |
+792
-266
File diff suppressed because it is too large.
Load diff
@@ -33,7 +33,7 @@ struct RunConfig {
|
|||||||
// ENGINE_MAX_FRAMES=N and ENGINE_SCREENSHOT=file.png, no code needed):
|
// ENGINE_MAX_FRAMES=N and ENGINE_SCREENSHOT=file.png, no code needed):
|
||||||
float frameTime = 0; // > 0: use this dt for every frame instead of the measured one
|
float frameTime = 0; // > 0: use this dt for every frame instead of the measured one
|
||||||
int maxFrames = 0; // > 0: return after this many frames (implies frameTime = 1/60)
|
int maxFrames = 0; // > 0: return after this many frames (implies frameTime = 1/60)
|
||||||
std::string screenshot; // written after the last frame when maxFrames is set
|
std::string screenshot = {}; // written after the last frame when maxFrames is set
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns 0 on a clean exit, non-zero if the window could not be created.
|
// Returns 0 on a clean exit, non-zero if the window could not be created.
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// How a collider follows its entity's transform. Pure maths with no Jolt in sight, so the physics
|
||||||
|
// backend (physics.cpp) and the unit tests share exactly the same rules.
|
||||||
|
#pragma once
|
||||||
|
#include "engine/physics.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace eng::detail {
|
||||||
|
|
||||||
|
inline constexpr float kMinColliderSize = 0.005f; // nothing reaches the solver thinner than this
|
||||||
|
|
||||||
|
// Effective, world-space dimensions of a collider: its authored size multiplied by the entity's world
|
||||||
|
// scale (parents included), clamped so a degenerate scale never produces an invalid shape.
|
||||||
|
struct ColliderDims {
|
||||||
|
ShapeType type = ShapeType::Box;
|
||||||
|
Vec3 halfExtents{}; // Box
|
||||||
|
float radius = 0; // Sphere, Capsule (caps), Cylinder
|
||||||
|
float halfHeight = 0; // Capsule (cylindrical part only), Cylinder
|
||||||
|
Vec3 meshScale{1, 1, 1}; // ConvexHull / Mesh: signed per-axis scale (negative mirrors)
|
||||||
|
Vec3 offset{}; // shape centre relative to the body, already scaled
|
||||||
|
};
|
||||||
|
|
||||||
|
inline float clampedSigned(float v) { return v < 0 ? -std::max(-v, kMinColliderSize) : std::max(v, kMinColliderSize); }
|
||||||
|
|
||||||
|
// `dynamic` bodies cannot use triangle meshes (a convex hull of the same mesh is used instead).
|
||||||
|
inline ColliderDims colliderDims(const Collider& c, Vec3 worldScale, bool dynamic) {
|
||||||
|
const Vec3 as = abs3(worldScale);
|
||||||
|
const float hMax = std::max({as.x, as.y, as.z});
|
||||||
|
const float hXZ = std::max(as.x, as.z);
|
||||||
|
ColliderDims d;
|
||||||
|
d.type = (c.shape == ShapeType::Mesh && dynamic) ? ShapeType::ConvexHull : c.shape;
|
||||||
|
d.offset = c.offset * worldScale;
|
||||||
|
switch (d.type) {
|
||||||
|
case ShapeType::Box:
|
||||||
|
d.halfExtents = {std::max(kMinColliderSize, c.size.x * as.x * 0.5f), std::max(kMinColliderSize, c.size.y * as.y * 0.5f),
|
||||||
|
std::max(kMinColliderSize, c.size.z * as.z * 0.5f)};
|
||||||
|
break;
|
||||||
|
case ShapeType::Sphere:
|
||||||
|
d.radius = std::max(kMinColliderSize, c.radius * hMax);
|
||||||
|
break;
|
||||||
|
case ShapeType::Capsule:
|
||||||
|
d.radius = std::max(kMinColliderSize, c.radius * hXZ);
|
||||||
|
d.halfHeight = std::max(kMinColliderSize, 0.5f * c.height * as.y - d.radius);
|
||||||
|
break;
|
||||||
|
case ShapeType::Cylinder:
|
||||||
|
d.radius = std::max(kMinColliderSize, c.radius * hXZ);
|
||||||
|
d.halfHeight = std::max(kMinColliderSize, 0.5f * c.height * as.y);
|
||||||
|
break;
|
||||||
|
case ShapeType::ConvexHull:
|
||||||
|
case ShapeType::Mesh:
|
||||||
|
d.meshScale = {clampedSigned(worldScale.x), clampedSigned(worldScale.y), clampedSigned(worldScale.z)};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The upright capsule of a character controller follows the entity's scale the same way.
|
||||||
|
struct CapsuleDims { float radius, halfHeight; };
|
||||||
|
inline CapsuleDims characterCapsule(float radius, float height, Vec3 worldScale) {
|
||||||
|
const Vec3 as = abs3(worldScale);
|
||||||
|
const float r = std::max(0.05f, radius * std::max(as.x, as.z));
|
||||||
|
return {r, std::max(0.01f, 0.5f * height * as.y - r)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The scale a built shape depends on, for telling whether it needs rebuilding. Just the magnitude for shapes
|
||||||
|
// that cannot tell a mirror image from the original (a centred box, sphere, capsule or cylinder), so flipping
|
||||||
|
// such an entity with a negative scale costs nothing. The sign counts once something can land on the other
|
||||||
|
// side of the entity: an off-centre collider, a hull or a mesh.
|
||||||
|
inline Vec3 shapeScale(const Collider& c, Vec3 worldScale) {
|
||||||
|
const bool signMatters = c.shape == ShapeType::ConvexHull || c.shape == ShapeType::Mesh || c.offset.lengthSq() > 0.0f;
|
||||||
|
return signMatters ? worldScale : abs3(worldScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are all three axes scaled alike (a mirror does not count)? Only then does a scaled convex hull keep its
|
||||||
|
// proportions exactly: Jolt rounds a hull with a fixed convex radius, which a scale wrapper can only shrink in
|
||||||
|
// step with the smallest axis, so a stretched hull would come out smaller than its mesh. Stretched hulls are
|
||||||
|
// built from scaled points instead.
|
||||||
|
inline bool uniformScale(Vec3 s) {
|
||||||
|
const Vec3 a = abs3(s);
|
||||||
|
const float hi = std::max({a.x, a.y, a.z}), lo = std::min({a.x, a.y, a.z});
|
||||||
|
return hi - lo <= 1e-3f * hi;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing a dynamic body back to its entity. The body stands for the matrix T * Q * diag(bodyScale) (Q is its
|
||||||
|
// rotation, bodyScale the decomposition of the entity's world matrix, which carries a mirror on X), so the entity has
|
||||||
|
// to end up showing the same picture:
|
||||||
|
// parent * T(local position) * R * diag(local scale) == T * Q * diag(bodyScale).
|
||||||
|
// This returns the local rotation R that makes it so, whichever signs the entity's own scale has and whether or not a
|
||||||
|
// parent is mirrored; `parentInverse` is the inverse of the parent's world matrix. (Just undoing the parents' rotations
|
||||||
|
// is only right while nothing is mirrored: a negative scale on the entity or an ancestor would flip its picture, or
|
||||||
|
// turn it the wrong way, the moment physics takes over.) With a parent that is a rotation plus a uniform scale (mirror
|
||||||
|
// or not) this is exact; with a sheared parent it is the closest rotation. `keep` comes back if an axis degenerates.
|
||||||
|
inline Quat localRotationForBody(const Mat4& parentInverse, const Quat& bodyRotation, Vec3 bodyScale, Vec3 localScale, const Quat& keep) {
|
||||||
|
const float bs[3] = {bodyScale.x, bodyScale.y, bodyScale.z}, ls[3] = {localScale.x, localScale.y, localScale.z};
|
||||||
|
const Vec3 axis[3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
|
||||||
|
Vec3 col[3];
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
col[i] = parentInverse.transformDirection(bodyRotation * axis[i]);
|
||||||
|
const float len = col[i].length();
|
||||||
|
if (!(len > 1e-12f)) return keep;
|
||||||
|
const float sign = (bs[i] < 0 ? -1.0f : 1.0f) * (ls[i] < 0 ? -1.0f : 1.0f); // where the two scales' signs differ the axis flips
|
||||||
|
col[i] = col[i] * (sign / len);
|
||||||
|
}
|
||||||
|
if (dot(col[0], cross(col[1], col[2])) < 0) return keep; // not a rotation (a flipped or sheared hierarchy): leave it
|
||||||
|
return Quat::fromBasis(col[0], col[1], col[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has the world scale moved far enough from the one a shape was built with to be worth a new shape?
|
||||||
|
inline bool scaleChanged(Vec3 built, Vec3 now) {
|
||||||
|
auto differs = [](float a, float b) { return std::fabs(a - b) > 1e-4f * std::max(1.0f, std::max(std::fabs(a), std::fabs(b))); };
|
||||||
|
return differs(built.x, now.x) || differs(built.y, now.y) || differs(built.z, now.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything about a collider that shapes the solver body (not friction/trigger, which need rebuild()).
|
||||||
|
// A change in this signature reshapes the body in place of the old one: edit `size`, `radius`,
|
||||||
|
// `height`, `offset`, `shape` or `mesh` and the collider follows, no rebuild() call needed.
|
||||||
|
inline std::uint64_t colliderSignature(const Collider& c) {
|
||||||
|
const float f[8] = {c.size.x, c.size.y, c.size.z, c.radius, c.height, c.offset.x, c.offset.y, c.offset.z};
|
||||||
|
std::uint64_t h = 1469598103934665603ull;
|
||||||
|
auto mix = [&h](const void* p, std::size_t n) {
|
||||||
|
const auto* b = static_cast<const unsigned char*>(p);
|
||||||
|
for (std::size_t i = 0; i < n; ++i) { h ^= b[i]; h *= 1099511628211ull; }
|
||||||
|
};
|
||||||
|
const std::uint8_t t = static_cast<std::uint8_t>(c.shape);
|
||||||
|
mix(&t, 1);
|
||||||
|
mix(f, sizeof f);
|
||||||
|
const std::uint64_t meshId = c.mesh ? c.mesh->id() : 0;
|
||||||
|
mix(&meshId, sizeof meshId);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eng::detail
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
// Pure mesh generation for one extruded island: ear-clipping triangulation with hole bridging, loft
|
||||||
|
// levels, wall/cap emission and crease-aware smooth normals. Deliberately free of Clipper2 so it can be
|
||||||
|
// unit-tested on its own; extrude.cpp (which owns the polygon clipping) includes it.
|
||||||
|
#pragma once
|
||||||
|
#include "engine/model.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace eng::meshgen {
|
||||||
|
|
||||||
|
struct P2 {
|
||||||
|
double x = 0, y = 0;
|
||||||
|
};
|
||||||
|
inline P2 operator-(P2 a, P2 b) { return {a.x - b.x, a.y - b.y}; }
|
||||||
|
inline double cross2(P2 a, P2 b) { return a.x * b.y - a.y * b.x; }
|
||||||
|
inline double dot2(P2 a, P2 b) { return a.x * b.x + a.y * b.y; }
|
||||||
|
inline bool same(P2 a, P2 b) { return std::fabs(a.x - b.x) < 1e-9 && std::fabs(a.y - b.y) < 1e-9; }
|
||||||
|
|
||||||
|
// Shoelace area: > 0 for counter-clockwise rings in a y-up plane (what Clipper2 calls positive).
|
||||||
|
inline double signedArea(const std::vector<P2>& p) {
|
||||||
|
double a = 0;
|
||||||
|
for (std::size_t i = 0, n = p.size(); i < n; ++i) {
|
||||||
|
const P2& u = p[i];
|
||||||
|
const P2& v = p[(i + 1) % n];
|
||||||
|
a += u.x * v.y - v.x * u.y;
|
||||||
|
}
|
||||||
|
return a * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline P2 centroidOf(const std::vector<P2>& p) {
|
||||||
|
double a2 = 0, cx = 0, cy = 0;
|
||||||
|
for (std::size_t i = 0, n = p.size(); i < n; ++i) {
|
||||||
|
const P2& u = p[i];
|
||||||
|
const P2& v = p[(i + 1) % n];
|
||||||
|
const double c = u.x * v.y - v.x * u.y;
|
||||||
|
a2 += c;
|
||||||
|
cx += (u.x + v.x) * c;
|
||||||
|
cy += (u.y + v.y) * c;
|
||||||
|
}
|
||||||
|
if (std::fabs(a2) < 1e-18) {
|
||||||
|
P2 m;
|
||||||
|
for (const P2& q : p) { m.x += q.x; m.y += q.y; }
|
||||||
|
return {m.x / static_cast<double>(p.size()), m.y / static_cast<double>(p.size())};
|
||||||
|
}
|
||||||
|
return {cx / (3.0 * a2), cy / (3.0 * a2)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Triangulation: ear clipping with hole bridging.
|
||||||
|
// rings[0] is the outer ring (CCW), the rest are holes (CW), all stored in `pts`;
|
||||||
|
// ringStart has rings+1 entries. Returns index triples into `pts`. Collinear vertices are
|
||||||
|
// kept (as zero-area triangles) so per-vertex heights stay honoured on straight edges.
|
||||||
|
// ===========================================================================
|
||||||
|
inline std::vector<std::array<int, 3>> triangulate(const std::vector<P2>& pts, const std::vector<std::size_t>& ringStart,
|
||||||
|
double eps) {
|
||||||
|
struct Node { P2 p; int idx, prev, next; };
|
||||||
|
std::vector<Node> nd;
|
||||||
|
nd.reserve(pts.size() + 2 * ringStart.size() + 4);
|
||||||
|
|
||||||
|
auto addRing = [&](std::size_t r) {
|
||||||
|
int first = -1, last = -1;
|
||||||
|
for (std::size_t i = ringStart[r]; i < ringStart[r + 1]; ++i) {
|
||||||
|
const int id = static_cast<int>(nd.size());
|
||||||
|
nd.push_back({pts[i], static_cast<int>(i), last, -1});
|
||||||
|
if (last >= 0) nd[static_cast<std::size_t>(last)].next = id; else first = id;
|
||||||
|
last = id;
|
||||||
|
}
|
||||||
|
nd[static_cast<std::size_t>(last)].next = first;
|
||||||
|
nd[static_cast<std::size_t>(first)].prev = last;
|
||||||
|
return first;
|
||||||
|
};
|
||||||
|
auto N = [&](int id) -> Node& { return nd[static_cast<std::size_t>(id)]; };
|
||||||
|
|
||||||
|
const std::size_t rings = ringStart.size() - 1;
|
||||||
|
int outer = addRing(0);
|
||||||
|
|
||||||
|
std::vector<int> holes; // rightmost vertex of each hole
|
||||||
|
for (std::size_t r = 1; r < rings; ++r) {
|
||||||
|
const int first = addRing(r);
|
||||||
|
int best = first;
|
||||||
|
for (int id = N(first).next; id != first; id = N(id).next)
|
||||||
|
if (N(id).p.x > N(best).p.x) best = id;
|
||||||
|
holes.push_back(best);
|
||||||
|
}
|
||||||
|
std::sort(holes.begin(), holes.end(), [&](int a, int b) { return N(a).p.x > N(b).p.x; });
|
||||||
|
|
||||||
|
auto segHit = [&](P2 p1, P2 p2, P2 q1, P2 q2) {
|
||||||
|
const double d1 = cross2(p2 - p1, q1 - p1), d2 = cross2(p2 - p1, q2 - p1);
|
||||||
|
const double d3 = cross2(q2 - q1, p1 - q1), d4 = cross2(q2 - q1, p2 - q1);
|
||||||
|
if (((d1 > eps && d2 < -eps) || (d1 < -eps && d2 > eps)) &&
|
||||||
|
((d3 > eps && d4 < -eps) || (d3 < -eps && d4 > eps)))
|
||||||
|
return true;
|
||||||
|
auto onSeg = [&](P2 a, P2 b, P2 p) { return std::fabs(cross2(b - a, p - a)) <= eps && dot2(p - a, p - b) <= eps; };
|
||||||
|
return onSeg(p1, p2, q1) || onSeg(p1, p2, q2) || onSeg(q1, q2, p1) || onSeg(q1, q2, p2);
|
||||||
|
};
|
||||||
|
auto ringClear = [&](int start, P2 h, P2 v) { // segment h-v crosses no edge of the ring?
|
||||||
|
int e = start;
|
||||||
|
do {
|
||||||
|
const int f = N(e).next;
|
||||||
|
const P2 a = N(e).p, b = N(f).p;
|
||||||
|
if (!(same(a, h) || same(b, h) || same(a, v) || same(b, v)) && segHit(h, v, a, b)) return false;
|
||||||
|
e = f;
|
||||||
|
} while (e != start);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
auto ringContains = [&](int start, P2 pt) { // even-odd point in ring
|
||||||
|
bool in = false;
|
||||||
|
int i = start;
|
||||||
|
do {
|
||||||
|
const P2 a = N(i).p, b = N(N(i).next).p;
|
||||||
|
if ((a.y > pt.y) != (b.y > pt.y) && pt.x < a.x + (pt.y - a.y) * (b.x - a.x) / (b.y - a.y)) in = !in;
|
||||||
|
i = N(i).next;
|
||||||
|
} while (i != start);
|
||||||
|
return in;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (std::size_t hi = 0; hi < holes.size(); ++hi) {
|
||||||
|
const int H = holes[hi];
|
||||||
|
const P2 hp = N(H).p;
|
||||||
|
std::vector<std::pair<double, int>> cand;
|
||||||
|
int i = outer;
|
||||||
|
do {
|
||||||
|
const P2 d = N(i).p - hp;
|
||||||
|
cand.emplace_back(dot2(d, d), i);
|
||||||
|
i = N(i).next;
|
||||||
|
} while (i != outer);
|
||||||
|
std::sort(cand.begin(), cand.end());
|
||||||
|
|
||||||
|
int chosen = -1;
|
||||||
|
for (const auto& [dist, V] : cand) {
|
||||||
|
const P2 vp = N(V).p;
|
||||||
|
bool ok = ringClear(outer, hp, vp);
|
||||||
|
for (std::size_t hj = hi; ok && hj < holes.size(); ++hj) ok = ringClear(holes[hj], hp, vp);
|
||||||
|
if (ok) ok = ringContains(outer, {(hp.x + vp.x) * 0.5, (hp.y + vp.y) * 0.5});
|
||||||
|
if (ok) { chosen = V; break; }
|
||||||
|
}
|
||||||
|
if (chosen < 0) chosen = cand.front().second; // degenerate input: connect to the nearest anyway
|
||||||
|
|
||||||
|
const int V = chosen;
|
||||||
|
const int Vn = N(V).next, Hp = N(H).prev;
|
||||||
|
const int V2 = static_cast<int>(nd.size());
|
||||||
|
nd.push_back({N(V).p, N(V).idx, -1, -1});
|
||||||
|
const int H2 = static_cast<int>(nd.size());
|
||||||
|
nd.push_back({N(H).p, N(H).idx, -1, -1});
|
||||||
|
N(V).next = H; N(H).prev = V;
|
||||||
|
N(Hp).next = H2; N(H2).prev = Hp;
|
||||||
|
N(H2).next = V2; N(V2).prev = H2;
|
||||||
|
N(V2).next = Vn; N(Vn).prev = V2;
|
||||||
|
outer = V;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto isEar = [&](int a, int b, int c) {
|
||||||
|
const P2 pa = N(a).p, pb = N(b).p, pc = N(c).p;
|
||||||
|
const double cr = cross2(pb - pa, pc - pb);
|
||||||
|
if (cr < -eps) return false; // reflex
|
||||||
|
if (cr <= eps) return false; // straight angle: keep it as a real vertex of a later ear
|
||||||
|
for (int q = N(c).next; q != a; q = N(q).next) {
|
||||||
|
const P2 pq = N(q).p;
|
||||||
|
if (same(pq, pa) || same(pq, pb) || same(pq, pc)) continue;
|
||||||
|
if (cross2(pb - pa, pq - pa) >= -eps && cross2(pc - pb, pq - pb) >= -eps &&
|
||||||
|
cross2(pa - pc, pq - pc) >= -eps)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
int remaining = 0;
|
||||||
|
for (int i = outer, first = outer; ; ) { ++remaining; i = N(i).next; if (i == first) break; }
|
||||||
|
|
||||||
|
std::vector<std::array<int, 3>> tris;
|
||||||
|
tris.reserve(static_cast<std::size_t>(remaining));
|
||||||
|
auto removeNode = [&](int id) { N(N(id).prev).next = N(id).next; N(N(id).next).prev = N(id).prev; };
|
||||||
|
|
||||||
|
int cur = outer, stall = 0;
|
||||||
|
while (remaining > 3) {
|
||||||
|
const int p = N(cur).prev, n = N(cur).next;
|
||||||
|
if (isEar(p, cur, n)) {
|
||||||
|
tris.push_back({N(p).idx, N(cur).idx, N(n).idx});
|
||||||
|
removeNode(cur);
|
||||||
|
--remaining;
|
||||||
|
cur = n;
|
||||||
|
stall = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cur = n;
|
||||||
|
if (++stall > remaining) { // no ear anywhere: clip the most convex vertex
|
||||||
|
int best = cur, it = cur;
|
||||||
|
double bestCross = -1e300;
|
||||||
|
for (int k = 0; k < remaining; ++k, it = N(it).next) {
|
||||||
|
const double c = cross2(N(it).p - N(N(it).prev).p, N(N(it).next).p - N(it).p);
|
||||||
|
if (c > bestCross) { bestCross = c; best = it; }
|
||||||
|
}
|
||||||
|
const int bp = N(best).prev, bn = N(best).next;
|
||||||
|
tris.push_back({N(bp).idx, N(best).idx, N(bn).idx});
|
||||||
|
removeNode(best);
|
||||||
|
--remaining;
|
||||||
|
cur = bn;
|
||||||
|
stall = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (remaining == 3) tris.push_back({N(N(cur).prev).idx, N(cur).idx, N(N(cur).next).idx});
|
||||||
|
return tris;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Mesh generation for one island
|
||||||
|
// ===========================================================================
|
||||||
|
struct Level { float f, r; };
|
||||||
|
|
||||||
|
inline std::vector<Level> makeLevels(const std::vector<std::pair<float, float>>& e) {
|
||||||
|
std::vector<Level> levels;
|
||||||
|
for (const auto& [f, r] : e) levels.push_back({std::clamp(f, 0.0f, 1.0f), std::max(r, 0.0f)});
|
||||||
|
std::stable_sort(levels.begin(), levels.end(), [](const Level& a, const Level& b) { return a.f < b.f; });
|
||||||
|
if (levels.empty() || levels.front().f > 0) levels.insert(levels.begin(), Level{0.0f, 1.0f});
|
||||||
|
if (levels.back().f < 1) levels.push_back(Level{1.0f, 1.0f});
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IslandData {
|
||||||
|
std::vector<P2> pts;
|
||||||
|
std::vector<float> base, top;
|
||||||
|
std::vector<Rgba> color;
|
||||||
|
std::vector<std::size_t> ringStart;
|
||||||
|
P2 pivot;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Smooth shading.
|
||||||
|
//
|
||||||
|
// Every triangle is emitted with its flat face normal; this pass then re-computes the normal of each
|
||||||
|
// corner. Corners are welded by position, and a corner's normal becomes the angle-weighted average of
|
||||||
|
// the face normals around that position whose faces bend less than the crease angle away from the
|
||||||
|
// corner's own face. That one rule gives:
|
||||||
|
// * walls shaded smoothly around curves (circles, arcs, rounded shapes),
|
||||||
|
// * lofted bands (e=...) shaded smoothly *across* levels when the profile bends gently,
|
||||||
|
// * hard edges wherever the surface bends sharply (wall/cap edges, ledges, pyramid faces),
|
||||||
|
// * one consistent normal per position and surface (no seams, no averaging across a crease).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
inline void smoothCreaseNormals(MeshData& m, std::size_t first, float cosCrease) {
|
||||||
|
const std::size_t total = m.positions.size();
|
||||||
|
if (first >= total || (total - first) % 3 != 0) return;
|
||||||
|
const std::size_t tris = (total - first) / 3;
|
||||||
|
|
||||||
|
std::vector<Vec3> face(tris); // unit face normals (counter-clockwise = front)
|
||||||
|
std::vector<std::array<float, 3>> angle(tris); // interior angle at each corner
|
||||||
|
for (std::size_t t = 0; t < tris; ++t) {
|
||||||
|
const Vec3* p = &m.positions[first + 3 * t];
|
||||||
|
face[t] = cross(p[1] - p[0], p[2] - p[0]).normalized();
|
||||||
|
for (std::size_t k = 0; k < 3; ++k) {
|
||||||
|
const Vec3 e1 = (p[(k + 1) % 3] - p[k]).normalized();
|
||||||
|
const Vec3 e2 = (p[(k + 2) % 3] - p[k]).normalized();
|
||||||
|
angle[t][k] = std::acos(std::clamp(dot(e1, e2), -1.0f, 1.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Key {
|
||||||
|
std::int64_t x, y, z;
|
||||||
|
bool operator==(const Key&) const = default;
|
||||||
|
};
|
||||||
|
struct KeyHash {
|
||||||
|
std::size_t operator()(const Key& k) const noexcept {
|
||||||
|
return static_cast<std::size_t>(k.x) * 73856093u ^ static_cast<std::size_t>(k.y) * 19349663u ^
|
||||||
|
static_cast<std::size_t>(k.z) * 83492791u;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto quant = [](float v) { return static_cast<std::int64_t>(std::llround(static_cast<double>(v) * 1e5)); };
|
||||||
|
|
||||||
|
std::unordered_map<Key, std::vector<std::uint32_t>, KeyHash> groups; // corner ids (triangle * 3 + corner)
|
||||||
|
groups.reserve(tris * 2);
|
||||||
|
for (std::size_t c = 0; c < tris * 3; ++c) {
|
||||||
|
const Vec3& p = m.positions[first + c];
|
||||||
|
groups[Key{quant(p.x), quant(p.y), quant(p.z)}].push_back(static_cast<std::uint32_t>(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [key, ids] : groups) {
|
||||||
|
(void)key;
|
||||||
|
for (const std::uint32_t c : ids) {
|
||||||
|
const std::size_t tc = c / 3;
|
||||||
|
if (face[tc].lengthSq() < 0.5f) continue; // degenerate: keep what it has
|
||||||
|
Vec3 sum;
|
||||||
|
for (const std::uint32_t d : ids) {
|
||||||
|
const std::size_t td = d / 3;
|
||||||
|
if (td != tc && dot(face[tc], face[td]) < cosCrease) continue; // across a crease
|
||||||
|
sum += face[td] * angle[td][d % 3];
|
||||||
|
}
|
||||||
|
const Vec3 n = sum.normalized();
|
||||||
|
m.normals[first + c] = n.lengthSq() > 0.5f ? n : face[tc];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Emitter {
|
||||||
|
MeshData& out;
|
||||||
|
|
||||||
|
// Triangles carry their flat normal; smoothCreaseNormals() refines it afterwards.
|
||||||
|
void wallTri(Vec3 a, Vec3 b, Vec3 c, Rgba ca, Rgba cb, Rgba cc) const {
|
||||||
|
const Vec3 cr = cross(b - a, c - a);
|
||||||
|
if (cr.lengthSq() < 1e-16f) return; // degenerate
|
||||||
|
const Vec3 n = cr.normalized();
|
||||||
|
out.addTriangle(a, b, c, n, n, n, ca, cb, cc);
|
||||||
|
}
|
||||||
|
void capTri(Vec3 a, Vec3 b, Vec3 c, Rgba ca, Rgba cb, Rgba cc) const {
|
||||||
|
const Vec3 n = cross(b - a, c - a).normalized();
|
||||||
|
if (n.lengthSq() < 0.5f) return;
|
||||||
|
out.addTriangle(a, b, c, n, n, n, ca, cb, cc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void emitIsland(MeshData& out, const IslandData& d, const std::vector<Level>& levels, const ExtrudeOptions& opt, P2 origin) {
|
||||||
|
const Emitter em{out};
|
||||||
|
const std::size_t first = out.positions.size();
|
||||||
|
const float cosCrease = std::cos(std::max(opt.smoothAngleDeg, 0.0f) * kDegToRad);
|
||||||
|
const double s = opt.scale;
|
||||||
|
|
||||||
|
auto position = [&](std::size_t i, const Level& lv) {
|
||||||
|
const double x = d.pivot.x + lv.r * (d.pts[i].x - d.pivot.x);
|
||||||
|
const double y = d.pivot.y + lv.r * (d.pts[i].y - d.pivot.y);
|
||||||
|
return Vec3{static_cast<float>((x - origin.x) * s), d.base[i] + lv.f * (d.top[i] - d.base[i]),
|
||||||
|
static_cast<float>((y - origin.y) * s)};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- walls: one strip per contour edge between consecutive levels ----
|
||||||
|
for (std::size_t k = 0; k + 1 < levels.size(); ++k) {
|
||||||
|
for (std::size_t r = 0; r + 1 < d.ringStart.size(); ++r) {
|
||||||
|
const std::size_t b0 = d.ringStart[r], n = d.ringStart[r + 1] - b0;
|
||||||
|
if (n < 3) continue;
|
||||||
|
std::vector<Vec3> A(n), B(n);
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
A[i] = position(b0 + i, levels[k]);
|
||||||
|
B[i] = position(b0 + i, levels[k + 1]);
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
const std::size_t j = (i + 1) % n;
|
||||||
|
const Rgba ci = d.color[b0 + i], cj = d.color[b0 + j];
|
||||||
|
em.wallTri(A[i], B[j], A[j], ci, cj, cj);
|
||||||
|
em.wallTri(A[i], B[i], B[j], ci, ci, cj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- caps ----
|
||||||
|
const double eps = std::max(1e-12, 0.1 * std::pow(10.0, -2.0 * opt.precision));
|
||||||
|
const auto tris = triangulate(d.pts, d.ringStart, eps);
|
||||||
|
const Level& bottom = levels.front();
|
||||||
|
const Level& topLv = levels.back();
|
||||||
|
for (const auto& t : tris) {
|
||||||
|
const std::size_t a = static_cast<std::size_t>(t[0]), b = static_cast<std::size_t>(t[1]), c = static_cast<std::size_t>(t[2]);
|
||||||
|
const double area2 = cross2(d.pts[b] - d.pts[a], d.pts[c] - d.pts[a]);
|
||||||
|
const bool ccw = area2 > 0; // in svg space (y up); maps to -Y facing
|
||||||
|
// top (+Y): reverse the ccw order
|
||||||
|
{
|
||||||
|
const Vec3 pa = position(a, topLv), pb = position(b, topLv), pc = position(c, topLv);
|
||||||
|
if (ccw) em.capTri(pa, pc, pb, d.color[a], d.color[c], d.color[b]);
|
||||||
|
else em.capTri(pa, pb, pc, d.color[a], d.color[b], d.color[c]);
|
||||||
|
}
|
||||||
|
// bottom (-Y)
|
||||||
|
{
|
||||||
|
const Vec3 pa = position(a, bottom), pb = position(b, bottom), pc = position(c, bottom);
|
||||||
|
if (ccw) em.capTri(pa, pb, pc, d.color[a], d.color[b], d.color[c]);
|
||||||
|
else em.capTri(pa, pc, pb, d.color[a], d.color[c], d.color[b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
smoothCreaseNormals(out, first, cosCrease);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eng::meshgen
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// The CPU half of drawing a frame. Everything the GPU needs - which instances of which mesh go into which pass,
|
||||||
|
// every shader uniform, the shadow views and the ambient grid - is worked out here without touching OpenGL, so
|
||||||
|
// it is unit-testable, and the renderer (src/render.cpp) is left with nothing but upload-and-draw calls.
|
||||||
|
#pragma once
|
||||||
|
#include "engine/detail/scene.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace eng::detail {
|
||||||
|
|
||||||
|
// Per-object GPU data, the same for every pass (the shadow pass only reads the matrix).
|
||||||
|
struct GpuInstance {
|
||||||
|
float m[16]; // world matrix, column-major
|
||||||
|
std::uint8_t rgba[4]; // tint
|
||||||
|
std::uint8_t material[4]; // roughness, metallic, emission / 4, flags (kMat*)
|
||||||
|
};
|
||||||
|
static_assert(sizeof(GpuInstance) == 72, "the instance attribute layout (stride 72, colour at 64, material at 68) is hard-coded");
|
||||||
|
inline constexpr int kInstanceStride = 72;
|
||||||
|
inline constexpr int kInstanceColorOffset = 64;
|
||||||
|
inline constexpr int kInstanceMaterialOffset = 68;
|
||||||
|
|
||||||
|
// What the lit shader reads about the surface of one batch, in the types it declares them: worked out here (from a Material,
|
||||||
|
// clamped to sane ranges) so the renderer only uploads it, and so it can be tested without a GPU.
|
||||||
|
struct MaterialUniforms {
|
||||||
|
int mode = 0; // 0: no material: roughness, metallic and emission come per instance, no maps
|
||||||
|
// 1: a Material
|
||||||
|
int maps = 0; // Material::MapBit bits: which of `textures` are real
|
||||||
|
float color[4] = {1, 1, 1, 1}; // albedo (gamma-encoded, like the vertex colours)
|
||||||
|
float surface[4] = {1, 0.5f, 0, 0.85f}; // diffuse, specular level, metalness, roughness
|
||||||
|
float extra[4] = {1, 0, 0, 0}; // normal strength, emission, 1 = project the textures from the world / 0 = use the mesh's UVs, unused
|
||||||
|
float specColor[4] = {1, 1, 1, 1}; // specular colour (gamma-encoded)
|
||||||
|
float uv[4] = {1, 1, 0, 0}; // texture coordinate scale (xy) and offset (zw)
|
||||||
|
const eng::Texture* textures[Material::kMapCount] = {}; // albedo, diffuse, specular, metalness, roughness, normal (Material::MapBit order)
|
||||||
|
};
|
||||||
|
|
||||||
|
// The shader values of `material` (null: the plain look, mode 0) drawn on a mesh that has / has not texture coordinates.
|
||||||
|
[[nodiscard]] MaterialUniforms materialUniforms(const eng::Material* material, bool meshHasTexcoords);
|
||||||
|
|
||||||
|
// A run of instances (in FramePlan::instances) that share one mesh, one material and one handedness.
|
||||||
|
struct DrawBatch {
|
||||||
|
const eng::Mesh* mesh = nullptr;
|
||||||
|
bool mirrored = false; // negative determinant: winding flips, so the culled face must too
|
||||||
|
std::uint32_t first = 0;
|
||||||
|
std::uint32_t count = 0;
|
||||||
|
MaterialUniforms mat; // the camera's pass only (shadow batches only need the depth)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Values of the lit shader's uniforms, in the types and layout the shader declares them.
|
||||||
|
struct LitUniforms {
|
||||||
|
int lightCount = 0;
|
||||||
|
float lightPosType[kMaxLights][4] = {}; // sun: direction TO the light, w 0; point: position, w 1; spot: position, w 2
|
||||||
|
float lightColorRange[kMaxLights][4] = {}; // linear colour * intensity, range
|
||||||
|
float lightSpot[kMaxLights][4] = {}; // spot axis, cos of the outer half angle
|
||||||
|
float lightParams[kMaxLights][4] = {}; // 1 / (cos inner - cos outer), shadow slot (-1 none), bias multiplier
|
||||||
|
float camPos[3] = {};
|
||||||
|
Mat4 shadowMatrix[kMaxShadowMaps];
|
||||||
|
float shadowInfo[kMaxShadowMaps][4] = {}; // texel world size, 1 = radial (spot), depth range, 1 / resolution
|
||||||
|
float shadowParams[4] = {}; // strength, softness in texels, sun shadow distance
|
||||||
|
float ambSkyGlobal[3] = {}; // linear
|
||||||
|
float ambGroundGlobal[3] = {};
|
||||||
|
float ambVolOrigin[4] = {}; // grid origin, cell size
|
||||||
|
float ambVolDims[4] = {0, 0, 0, 1}; // cells x y z, tiles per atlas row; x == 0: no zones
|
||||||
|
float ambColorScale = 1;
|
||||||
|
float envParams[4] = {1, 2, 0, 0}; // exposure, tone mapping, fog density
|
||||||
|
float fogColor[3] = {}; // linear
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShadowPass {
|
||||||
|
int slot = 0; // 0..kMaxShadowMaps-1: which shadow map / sampler
|
||||||
|
int resolution = 0; // square map, texels per side
|
||||||
|
Mat4 viewProj;
|
||||||
|
bool radial = false; // spot: the map stores distance from `lightPos` divided by `range`
|
||||||
|
Vec3 lightPos{};
|
||||||
|
float range = 1;
|
||||||
|
std::vector<DrawBatch> batches; // the casters inside the light's view
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FramePlan {
|
||||||
|
bool hasCamera = false;
|
||||||
|
CameraData camera;
|
||||||
|
float aspect = 1;
|
||||||
|
Mat4 view, projection, viewProj;
|
||||||
|
|
||||||
|
std::vector<GpuInstance> instances; // every pass back to back: one upload per frame
|
||||||
|
std::vector<DrawBatch> main; // the camera's pass
|
||||||
|
std::vector<ShadowPass> shadows;
|
||||||
|
LitUniforms lit;
|
||||||
|
|
||||||
|
bool ambientChanged = false; // the ambient grid was re-baked: refresh the GPU textures
|
||||||
|
const AmbientGrid* ambient = nullptr; // owned by the planner; valid until the next plan()
|
||||||
|
|
||||||
|
std::vector<TextData> texts; // HUD text, in order
|
||||||
|
|
||||||
|
int meshesSeen = 0; // visible mesh renderers before culling
|
||||||
|
int culled = 0; // rejected by the camera frustum
|
||||||
|
int lightsShaded = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shadow-map side length: a power of two in [256, 4096] (rounding up).
|
||||||
|
[[nodiscard]] int shadowMapResolution(int requested);
|
||||||
|
|
||||||
|
class FramePlanner {
|
||||||
|
public:
|
||||||
|
FramePlanner();
|
||||||
|
~FramePlanner();
|
||||||
|
FramePlanner(const FramePlanner&) = delete;
|
||||||
|
FramePlanner& operator=(const FramePlanner&) = delete;
|
||||||
|
|
||||||
|
// Plans one frame of `world` for a viewport with the given aspect ratio. The returned plan (and the mesh
|
||||||
|
// pointers in it) stay valid until the next call; the world must not change in between.
|
||||||
|
[[nodiscard]] const FramePlan& plan(World& world, float aspect);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace eng::detail
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
// What the renderer needs from the ECS, gathered without touching the GPU (so all of it is unit-testable),
|
||||||
|
// plus the CPU side of lighting: light budgeting, shadow-map fitting and the ambient-zone bake.
|
||||||
|
#pragma once
|
||||||
|
#include "engine/graphics.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace eng::detail {
|
||||||
|
|
||||||
|
inline constexpr int kMaxLights = 16; // shaded per frame (shader array size)
|
||||||
|
inline constexpr int kMaxShadowMaps = 3; // shadow maps per frame (shader sampler count)
|
||||||
|
inline constexpr std::uint8_t kMatReceiveShadows = 1; // MeshInstance::flags
|
||||||
|
inline constexpr std::uint8_t kMatUnlit = 2;
|
||||||
|
|
||||||
|
// ---- frustum -------------------------------------------------------------------------------------
|
||||||
|
struct Frustum {
|
||||||
|
float planes[6][4] = {}; // normalised, pointing inwards
|
||||||
|
static Frustum fromViewProj(const Mat4& viewProj);
|
||||||
|
[[nodiscard]] bool sphereVisible(Vec3 center, float radius) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- what is in the scene ------------------------------------------------------------------------
|
||||||
|
struct MeshInstance {
|
||||||
|
const eng::Mesh* mesh = nullptr;
|
||||||
|
const eng::Material* material = nullptr; // null: the renderer's own roughness / metallic / emission below
|
||||||
|
Mat4 world;
|
||||||
|
Vec3 center; // world-space bounding sphere
|
||||||
|
float radius = 0;
|
||||||
|
Rgba tint{255, 255, 255, 255};
|
||||||
|
std::uint8_t roughness = 217, metallic = 0, emission = 0; // 0..255 (emission: 0..4)
|
||||||
|
std::uint8_t flags = kMatReceiveShadows;
|
||||||
|
bool castShadows = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LightData {
|
||||||
|
LightType type = LightType::Directional;
|
||||||
|
Vec3 position{}; // world; unused for directional lights
|
||||||
|
Vec3 direction{0, -1, 0}; // the way the light travels (unit)
|
||||||
|
Vec3 color{1, 1, 1};
|
||||||
|
float intensity = 1;
|
||||||
|
float range = 20;
|
||||||
|
float cosInner = 1, cosOuter = 0; // spot cone
|
||||||
|
bool castShadows = false;
|
||||||
|
float shadowBias = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TextData {
|
||||||
|
std::string value;
|
||||||
|
Vec2 position;
|
||||||
|
float size = 20;
|
||||||
|
Rgba color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CameraData {
|
||||||
|
Vec3 position{}, forward{0, 0, -1}, up{0, 1, 0};
|
||||||
|
float fovDeg = 60, nearPlane = 0.1f, farPlane = 1000, orthoSize = 10;
|
||||||
|
bool orthographic = false;
|
||||||
|
[[nodiscard]] Mat4 view() const { return Mat4::lookAt(position, position + forward, up); }
|
||||||
|
[[nodiscard]] Mat4 projection(float aspect) const {
|
||||||
|
if (!orthographic) return Mat4::perspective(fovDeg, aspect, nearPlane, farPlane);
|
||||||
|
const float top = 0.5f * orthoSize;
|
||||||
|
return Mat4::ortho(-top * aspect, top * aspect, -top, top, nearPlane, farPlane);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ZoneData {
|
||||||
|
ZoneShape shape = ZoneShape::Box;
|
||||||
|
Mat4 world, inverse; // entity -> world, and back
|
||||||
|
Vec3 size{};
|
||||||
|
float radius = 0, height = 0;
|
||||||
|
std::vector<Vec2> polygon;
|
||||||
|
MeshPtr mesh;
|
||||||
|
Vec3 sky{}, ground{};
|
||||||
|
float intensity = 1;
|
||||||
|
ZoneMode mode = ZoneMode::Replace;
|
||||||
|
float falloff = 0;
|
||||||
|
int priority = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SceneSnapshot {
|
||||||
|
std::vector<MeshInstance> meshes; // visible renderers (hidden/disabled ones are absent)
|
||||||
|
std::vector<LightData> lights; // visible lights; a default sun when the world has none at all
|
||||||
|
std::vector<ZoneData> zones; // enabled ambient zones
|
||||||
|
std::vector<TextData> texts; // visible HUD text, in hierarchy order
|
||||||
|
bool hasCamera = false;
|
||||||
|
CameraData camera;
|
||||||
|
Environment env; // the first enabled Environment, else the defaults
|
||||||
|
};
|
||||||
|
|
||||||
|
// Walks the hierarchy once, applying the visibility rules of Renderable (see graphics.hpp).
|
||||||
|
[[nodiscard]] SceneSnapshot gatherScene(World& world);
|
||||||
|
|
||||||
|
// ---- lights and shadows --------------------------------------------------------------------------
|
||||||
|
// The lights shaded this frame: suns first (strongest, at most 4), then the point/spot lights that matter
|
||||||
|
// most to the camera, up to kMaxLights. Lights with no brightness are dropped.
|
||||||
|
[[nodiscard]] std::vector<LightData> selectLights(const std::vector<LightData>& all, Vec3 cameraPosition);
|
||||||
|
|
||||||
|
// Shadow slot (0..kMaxShadowMaps-1) of every selected light, or -1: the first shadow-casting sun gets slot 0,
|
||||||
|
// then the shadow-casting spots in order, up to env.maxShadowLights. All -1 when env.shadows is off.
|
||||||
|
[[nodiscard]] std::vector<int> assignShadowSlots(const std::vector<LightData>& selected, const Environment& env);
|
||||||
|
|
||||||
|
struct ShadowView {
|
||||||
|
Mat4 viewProj;
|
||||||
|
Frustum frustum; // for culling shadow casters
|
||||||
|
bool perspective = false; // spot lights
|
||||||
|
float texelWorld = 0; // sun: world size of one texel. Spot: size at distance 1 (scale by distance).
|
||||||
|
float depthRange = 1; // sun: world length the depth range covers. Spot: the light's range.
|
||||||
|
};
|
||||||
|
|
||||||
|
// A square orthographic view along `lightDir` that covers the camera's view up to `shadowDistance`, with the
|
||||||
|
// depth range tightened to the casters in front of it. The window is snapped to whole texels so shadows do not
|
||||||
|
// shimmer while the camera moves or turns.
|
||||||
|
[[nodiscard]] ShadowView fitSunShadow(Vec3 lightDir, const CameraData& camera, float aspect, float shadowDistance, int resolution,
|
||||||
|
const std::vector<MeshInstance>& casters);
|
||||||
|
[[nodiscard]] ShadowView fitSpotShadow(const LightData& light, int resolution);
|
||||||
|
|
||||||
|
// ---- ambient zones -------------------------------------------------------------------------------
|
||||||
|
[[nodiscard]] bool zoneContains(const ZoneData& zone, Vec3 worldPoint); // meshes are voxelised on demand (see AmbientBaker)
|
||||||
|
|
||||||
|
// Colours are authored in the usual "screen" values (0..1) and lit in linear light, as pow(c, 2.2).
|
||||||
|
inline constexpr float kGamma = 2.2f;
|
||||||
|
[[nodiscard]] Vec3 toLinear(Vec3 c);
|
||||||
|
[[nodiscard]] Vec3 toGamma(Vec3 c);
|
||||||
|
// The 8-bit texel of a linear value in [0, 1]: exactly the byte toGamma() gives it (rounded, clamped), table-driven.
|
||||||
|
[[nodiscard]] std::uint8_t encodeLinearToByte(float linear);
|
||||||
|
|
||||||
|
// A box of grid cells, both ends included, in the grid's own cell coordinates (0 .. n - 1 on every axis).
|
||||||
|
struct CellBox {
|
||||||
|
int x0 = 0, y0 = 0, z0 = 0;
|
||||||
|
int x1 = -1, y1 = -1, z1 = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AmbientGrid {
|
||||||
|
bool valid = false; // false: no zones, shade with the Environment's ambient everywhere
|
||||||
|
Vec3 origin{}; // world position of the grid's minimum corner
|
||||||
|
float cell = 1; // world units per cell
|
||||||
|
int nx = 0, ny = 0, nz = 0;
|
||||||
|
float colorScale = 1; // linear light is stored as toGamma(value / colorScale): 8 bits are spent
|
||||||
|
// where the eye needs them, and zones can go above 1
|
||||||
|
std::vector<std::uint8_t> sky, ground; // RGBA8, index (y * nz + z) * nx + x
|
||||||
|
// The same data laid out for a 2D texture: one nx*nz tile per Y slice, tilesX tiles per row.
|
||||||
|
int tilesX = 1, tilesY = 1;
|
||||||
|
std::vector<std::uint8_t> atlasSky, atlasGround; // (nx * tilesX) x (nz * tilesY) RGBA8
|
||||||
|
// What the last update() rewrote, so the GPU copy can be refreshed without re-sending everything: either all of
|
||||||
|
// it (a new grid, a new layout or a new colour scale), or just the boxes of cells whose texels changed (a zone
|
||||||
|
// that walks with the player touches a few thousand cells, not the whole grid).
|
||||||
|
bool updateAll = false;
|
||||||
|
std::vector<CellBox> updated;
|
||||||
|
[[nodiscard]] int atlasWidth() const { return nx * tilesX; }
|
||||||
|
[[nodiscard]] int atlasHeight() const { return nz * tilesY; }
|
||||||
|
// Reference sampling of the baked field, in linear light: trilinear on the stored (encoded) texels and decoded
|
||||||
|
// afterwards, exactly what the pixel shader does on the atlas.
|
||||||
|
void sample(Vec3 worldPoint, Vec3& sky, Vec3& ground) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// What the last AmbientBaker::update() did. A bake costs what changed, not the size of the scene: moving one zone
|
||||||
|
// recomputes that zone's layer and rewrites the texels it touched, nothing else.
|
||||||
|
struct BakeStats {
|
||||||
|
int zones = 0; // usable zones blended
|
||||||
|
int layersComputed = 0; // zones whose reach (inside test + fade-out distances) had to be computed
|
||||||
|
int layersReused = 0; // zones that kept their cached reach
|
||||||
|
std::size_t cellsBlended = 0; // grid cells whose colour was worked out again (the part of the grid a changed zone can reach)
|
||||||
|
std::size_t cellsEncoded = 0; // grid cells whose texels (sky and ground) were rewritten
|
||||||
|
bool full = false; // every texel was rewritten: the first bake, a different grid, or a new colour scale
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bakes AmbientZone components into a grid and re-bakes only when their inputs change.
|
||||||
|
//
|
||||||
|
// The grid lies on a lattice fixed in the world (cell i covers [i * cell, (i + 1) * cell)) and every zone keeps its own
|
||||||
|
// cached layer (where it is and how its fade-out falls off), keyed by its shape and transform. A bake therefore
|
||||||
|
// * recomputes the layers of the zones that changed (an unchanged zone is a cache hit, even when the grid grows
|
||||||
|
// or shrinks around it),
|
||||||
|
// * works out the colour again only where a changed zone was or is (the cells its old and its new layer cover; every
|
||||||
|
// other cell keeps the colour of the previous bake, which is what a bake from scratch would give it too), and
|
||||||
|
// * rewrites only the texels whose colour came out different from the previous bake.
|
||||||
|
// So the cost of moving one zone follows the size of that zone, not the size of the grid.
|
||||||
|
// The grid is always exactly what a bake from scratch would give: its extent is a function of the zones alone (their
|
||||||
|
// bounds, rounded out to blocks of cells so that a zone that walks does not resize the grid on every step), and the
|
||||||
|
// colour scale is a function of the brightest cell alone (rounded up to a step of a ladder, so that a bright zone that
|
||||||
|
// overlaps another one as it moves does not re-encode every texel on every step).
|
||||||
|
class AmbientBaker {
|
||||||
|
public:
|
||||||
|
AmbientBaker();
|
||||||
|
~AmbientBaker();
|
||||||
|
// Returns true when the grid's texels (or its size) are not what they were, so a GPU copy must be refreshed. An
|
||||||
|
// input that changed without changing a single texel (a zone moving by less than it takes to flip one) returns false.
|
||||||
|
bool update(const std::vector<ZoneData>& zones, const Environment& env);
|
||||||
|
[[nodiscard]] const AmbientGrid& grid() const { return grid_; }
|
||||||
|
// Voxelisation of closed meshes is cached by mesh id; this is how many are held.
|
||||||
|
[[nodiscard]] std::size_t cachedMeshVolumes() const;
|
||||||
|
// Zone layers held for reuse: one per distinct zone (shape + transform) of the last bake, never more.
|
||||||
|
[[nodiscard]] std::size_t cachedLayers() const;
|
||||||
|
// What the most recent update() call did (all zero when it returned false: nothing had changed).
|
||||||
|
[[nodiscard]] const BakeStats& lastBake() const { return stats_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
AmbientGrid grid_;
|
||||||
|
std::uint64_t hash_ = 0;
|
||||||
|
BakeStats stats_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace eng::detail
|
||||||
@@ -103,6 +103,12 @@ public:
|
|||||||
void setEntityEnabled(bool enabled) const;
|
void setEntityEnabled(bool enabled) const;
|
||||||
[[nodiscard]] bool entityActive() const; // enabled and all ancestors enabled
|
[[nodiscard]] bool entityActive() const; // enabled and all ancestors enabled
|
||||||
|
|
||||||
|
// Visual only (scripts and physics keep running): hides/shows the entity's renderer components
|
||||||
|
// (MeshRenderer, Light, Text, ...) and, by default, every entity below it. A plain group entity gets a
|
||||||
|
// Visibility component to carry the switch. See Renderable in graphics.hpp for finer control.
|
||||||
|
void setVisible(bool visible, bool includeChildren = true) const;
|
||||||
|
[[nodiscard]] bool isVisible() const; // drawn right now: not hidden here nor by an ancestor
|
||||||
|
|
||||||
[[nodiscard]] std::string& name() const;
|
[[nodiscard]] std::string& name() const;
|
||||||
void setName(std::string_view name) const;
|
void setName(std::string_view name) const;
|
||||||
[[nodiscard]] TagType tag() const;
|
[[nodiscard]] TagType tag() const;
|
||||||
@@ -136,6 +142,8 @@ template <class T> concept HasStart = requires(T& t) { t.start(); };
|
|||||||
template <class T> concept HasUpdate = requires(T& t) { t.update(); } || requires(T& t, float dt) { t.update(dt); };
|
template <class T> concept HasUpdate = requires(T& t) { t.update(); } || requires(T& t, float dt) { t.update(dt); };
|
||||||
template <class T> concept HasFixedUpdate = requires(T& t) { t.fixedUpdate(); } || requires(T& t, float dt) { t.fixedUpdate(dt); };
|
template <class T> concept HasFixedUpdate = requires(T& t) { t.fixedUpdate(); } || requires(T& t, float dt) { t.fixedUpdate(dt); };
|
||||||
template <class T> concept HasLateUpdate = requires(T& t) { t.lateUpdate(); } || requires(T& t, float dt) { t.lateUpdate(dt); };
|
template <class T> concept HasLateUpdate = requires(T& t) { t.lateUpdate(); } || requires(T& t, float dt) { t.lateUpdate(dt); };
|
||||||
|
template <class T> concept HasDrawGizmos = requires(T& t) { t.drawGizmos(); }; // 3D immediate drawing, inside the camera's view
|
||||||
|
template <class T> concept HasDrawGui = requires(T& t) { t.drawGui(); }; // 2D immediate drawing, on top of everything
|
||||||
template <class T> concept HasDestroyed = requires(T& t) { t.destroyed(); } || detail::HookDestroyed<T>;
|
template <class T> concept HasDestroyed = requires(T& t) { t.destroyed(); } || detail::HookDestroyed<T>;
|
||||||
template <class T> concept HasCollisionEnter = requires(T& t, Entity o) { t.collisionEnter(o); };
|
template <class T> concept HasCollisionEnter = requires(T& t, Entity o) { t.collisionEnter(o); };
|
||||||
template <class T> concept HasCollisionExit = requires(T& t, Entity o) { t.collisionExit(o); };
|
template <class T> concept HasCollisionExit = requires(T& t, Entity o) { t.collisionExit(o); };
|
||||||
@@ -332,6 +340,11 @@ public:
|
|||||||
// fixed steps (fixedUpdate + physics), update(), lateUpdate(), then deferred destruction.
|
// fixed steps (fixedUpdate + physics), update(), lateUpdate(), then deferred destruction.
|
||||||
// run() calls this for you; call it yourself only for headless use (servers, tests).
|
// run() calls this for you; call it yourself only for headless use (servers, tests).
|
||||||
void update(float dt);
|
void update(float dt);
|
||||||
|
// Called by the renderer (run() does it for you): components with drawGizmos() draw 3D immediate-mode
|
||||||
|
// graphics inside the camera's view, components with drawGui() draw 2D on top of the finished frame.
|
||||||
|
// The immediate-mode drawing functions of gfx.hpp are valid only inside these two callbacks.
|
||||||
|
void drawGizmos();
|
||||||
|
void drawGui();
|
||||||
[[nodiscard]] float deltaTime() const noexcept { return dt_; }
|
[[nodiscard]] float deltaTime() const noexcept { return dt_; }
|
||||||
[[nodiscard]] float time() const noexcept { return time_; }
|
[[nodiscard]] float time() const noexcept { return time_; }
|
||||||
[[nodiscard]] std::uint64_t frame() const noexcept { return frame_; }
|
[[nodiscard]] std::uint64_t frame() const noexcept { return frame_; }
|
||||||
@@ -355,6 +368,8 @@ private:
|
|||||||
void (*update)(World&, float) = nullptr;
|
void (*update)(World&, float) = nullptr;
|
||||||
void (*fixedUpdate)(World&, float) = nullptr;
|
void (*fixedUpdate)(World&, float) = nullptr;
|
||||||
void (*lateUpdate)(World&, float) = nullptr;
|
void (*lateUpdate)(World&, float) = nullptr;
|
||||||
|
void (*drawGizmos)(World&) = nullptr;
|
||||||
|
void (*drawGui)(World&) = nullptr;
|
||||||
void (*destroyed)(Entity) = nullptr;
|
void (*destroyed)(Entity) = nullptr;
|
||||||
void (*collisionEnter)(Entity, Entity) = nullptr;
|
void (*collisionEnter)(Entity, Entity) = nullptr;
|
||||||
void (*collisionExit)(Entity, Entity) = nullptr;
|
void (*collisionExit)(Entity, Entity) = nullptr;
|
||||||
@@ -368,6 +383,7 @@ private:
|
|||||||
template <class T> void runUpdate(float dt);
|
template <class T> void runUpdate(float dt);
|
||||||
template <class T> void runFixedUpdate(float dt);
|
template <class T> void runFixedUpdate(float dt);
|
||||||
template <class T> void runLateUpdate(float dt);
|
template <class T> void runLateUpdate(float dt);
|
||||||
|
template <class T> void runDraw(bool gizmos);
|
||||||
template <class T> void snapshot(std::vector<entt::entity>& out);
|
template <class T> void snapshot(std::vector<entt::entity>& out);
|
||||||
|
|
||||||
template <class T> T& attachBuiltin(entt::entity e);
|
template <class T> T& attachBuiltin(entt::entity e);
|
||||||
@@ -470,6 +486,8 @@ template <class T> void World::registerType() {
|
|||||||
if constexpr (HasUpdate<T>) ops.update = [](World& w, float dt) { w.runUpdate<T>(dt); };
|
if constexpr (HasUpdate<T>) ops.update = [](World& w, float dt) { w.runUpdate<T>(dt); };
|
||||||
if constexpr (HasFixedUpdate<T>) ops.fixedUpdate = [](World& w, float dt) { w.runFixedUpdate<T>(dt); };
|
if constexpr (HasFixedUpdate<T>) ops.fixedUpdate = [](World& w, float dt) { w.runFixedUpdate<T>(dt); };
|
||||||
if constexpr (HasLateUpdate<T>) ops.lateUpdate = [](World& w, float dt) { w.runLateUpdate<T>(dt); };
|
if constexpr (HasLateUpdate<T>) ops.lateUpdate = [](World& w, float dt) { w.runLateUpdate<T>(dt); };
|
||||||
|
if constexpr (HasDrawGizmos<T>) ops.drawGizmos = [](World& w) { w.runDraw<T>(true); };
|
||||||
|
if constexpr (HasDrawGui<T>) ops.drawGui = [](World& w) { w.runDraw<T>(false); };
|
||||||
if constexpr (HasCollisionEnter<T>) ops.collisionEnter = [](Entity s, Entity o) {
|
if constexpr (HasCollisionEnter<T>) ops.collisionEnter = [](Entity s, Entity o) {
|
||||||
if (T* c = s.tryGet<T>()) if (c->componentActive()) c->collisionEnter(o); };
|
if (T* c = s.tryGet<T>()) if (c->componentActive()) c->collisionEnter(o); };
|
||||||
if constexpr (HasCollisionExit<T>) ops.collisionExit = [](Entity s, Entity o) {
|
if constexpr (HasCollisionExit<T>) ops.collisionExit = [](Entity s, Entity o) {
|
||||||
@@ -560,4 +578,23 @@ template <class T> void World::runLateUpdate(float dt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <class T> void World::runDraw(bool gizmos) {
|
||||||
|
if constexpr (std::is_base_of_v<Component, T> && (HasDrawGizmos<T> || HasDrawGui<T>)) {
|
||||||
|
std::vector<entt::entity> ids;
|
||||||
|
ids.swap(scratch_);
|
||||||
|
snapshot<T>(ids);
|
||||||
|
for (const entt::entity e : ids) {
|
||||||
|
T* c = reg_.try_get<T>(e);
|
||||||
|
if (!c) continue;
|
||||||
|
Component& base = *c;
|
||||||
|
if (!base.enabled_ || !isActive(e)) continue;
|
||||||
|
if constexpr (HasStart<T>) { if (!base.started_) continue; }
|
||||||
|
if constexpr (HasDrawGizmos<T>) { if (gizmos) c->drawGizmos(); }
|
||||||
|
if constexpr (HasDrawGui<T>) { if (!gizmos) c->drawGui(); }
|
||||||
|
}
|
||||||
|
ids.clear();
|
||||||
|
ids.swap(scratch_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace eng
|
} // namespace eng
|
||||||
@@ -4,8 +4,18 @@
|
|||||||
#include "math.hpp"
|
#include "math.hpp"
|
||||||
#include "ecs.hpp"
|
#include "ecs.hpp"
|
||||||
#include "input.hpp"
|
#include "input.hpp"
|
||||||
|
#include "texture.hpp"
|
||||||
|
#include "material.hpp"
|
||||||
#include "graphics.hpp"
|
#include "graphics.hpp"
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
#include "resources.hpp"
|
#include "resources.hpp"
|
||||||
#include "physics.hpp"
|
#include "physics.hpp"
|
||||||
#include "app.hpp"
|
#include "app.hpp"
|
||||||
|
|
||||||
|
// eng::gfx, the engine's equivalent of every raylib function (see gfx.hpp), when this build has it. It is left out
|
||||||
|
// when the file already included <raylib.h> before this header: raylib's macros (RED, PI, ...) cannot coexist with
|
||||||
|
// the declarations, and that code does not need it. Include <engine/gfx.hpp> yourself to get a clear error instead.
|
||||||
|
#if __has_include(<engine/gfx_api.hpp>) && !defined(RAYLIB_H) && !defined(RLGL_H) && !defined(RAYMATH_H)
|
||||||
|
#include "gfx.hpp"
|
||||||
|
#define ENGINE_HAS_GFX 1 // code that must also build without eng::gfx can test this
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// eng::gfx - the engine's equivalent of raylib's API.
|
||||||
|
//
|
||||||
|
// #include <engine/gfx.hpp>
|
||||||
|
//
|
||||||
|
// Every raylib function (raylib.h, and rlgl.h / raymath.h when present) is available as eng::gfx::<name> with the
|
||||||
|
// first letter lower-cased: DrawCircle -> gfx::drawCircle, LoadTexture -> gfx::loadTexture, rlBegin -> gfx::rlBegin,
|
||||||
|
// Vector3Add -> gfx::vector3Add. Constants and enumerators keep raylib's names (gfx::KEY_SPACE, gfx::FLAG_VSYNC_HINT),
|
||||||
|
// colours live in gfx::colors (gfx::colors::SkyBlue). The wrappers are generated from the raylib headers you build
|
||||||
|
// against (tools/gfx_gen, see cmake/gfx.cmake), so they always match that version; the build log and
|
||||||
|
// <build>/gfx_gen/out/gfx_report.txt say what was wrapped.
|
||||||
|
//
|
||||||
|
// Types: raylib's Vector2/Vector3/Quaternion/Color/BoundingBox ARE the engine's Vec2/Vec3/Quat/Rgba/Aabb, and a
|
||||||
|
// Matrix passed by value is a Mat4 (the wrappers convert, so there is nothing to do on your side). Everything
|
||||||
|
// else (Texture2D, Font, Model, Camera3D, Rectangle, Ray, ...) has raylib's own layout under raylib's own name.
|
||||||
|
// Overloads taking std::string exist next to every `const char*` parameter, gfx::textFormat() returns a std::string
|
||||||
|
// and gfx::traceLog() takes printf arguments.
|
||||||
|
//
|
||||||
|
// Where to call it:
|
||||||
|
// * Drawing: in a component's `void drawGizmos()` (3D, inside the camera's BeginMode3D) or `void drawGui()` (2D,
|
||||||
|
// on top of the frame). run() owns the window, the frame and the 3D camera: InitWindow, CloseWindow,
|
||||||
|
// BeginDrawing, EndDrawing, BeginMode3D, ClearBackground, SetTargetFPS ... exist here for completeness (their
|
||||||
|
// docs say so) but the engine calls them itself; use RunConfig for the window.
|
||||||
|
// * Anything that needs the window (loadTexture, loadFont, loadModel, loadShader, audio ...) must run after the
|
||||||
|
// window exists, i.e. from a component callback during run(), not before it.
|
||||||
|
// * Do not include <raylib.h>, <rlgl.h> or <raymath.h> before this header in the same file: they define macros
|
||||||
|
// (RED, PI, ...) that would corrupt the declarations; the headers stop you with an #error. Including them after
|
||||||
|
// is fine, and qualifying names (gfx::...) is always safe. Code that includes <engine/engine.hpp> and raylib.h
|
||||||
|
// together keeps working: the umbrella header then simply leaves eng::gfx out.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#if !__has_include(<engine/gfx_api.hpp>)
|
||||||
|
#error "eng::gfx is not part of this build: it is generated from raylib's headers by CMake (ENGINE_BUILD_GFX, see cmake/gfx.cmake). Link the `engine` target, and check the configure log for an 'eng::gfx:' warning."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <engine/ecs.hpp>
|
||||||
|
#include <engine/graphics.hpp>
|
||||||
|
#include <engine/math.hpp>
|
||||||
|
|
||||||
|
#include <engine/gfx_api.hpp>
|
||||||
|
#if __has_include(<engine/gfx_rlgl_api.hpp>)
|
||||||
|
#include <engine/gfx_rlgl_api.hpp>
|
||||||
|
#endif
|
||||||
|
#if __has_include(<engine/gfx_raymath_api.hpp>)
|
||||||
|
#include <engine/gfx_raymath_api.hpp>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace eng::gfx {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------
|
||||||
|
// Engine <-> raylib glue
|
||||||
|
// (Inside eng::gfx the names Camera and Transform are raylib's own types, so the engine's are written eng::...)
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// The raylib Camera3D for a camera entity: what BeginMode3D(...) / GetWorldToScreen(...) / GetScreenToWorldRay(...)
|
||||||
|
// take. Position and orientation come from the entity's WORLD transform (parents included, it looks down its -Z),
|
||||||
|
// fovy is the field of view in degrees (perspective) or the vertical extent in world units (orthographic), as in
|
||||||
|
// raylib. An entity without a eng::Camera component gets the Camera defaults.
|
||||||
|
[[nodiscard]] inline Camera3D toCamera3D(const eng::Transform& transform, const eng::Camera& camera) {
|
||||||
|
const Vec3 position = transform.worldPosition();
|
||||||
|
Camera3D out{};
|
||||||
|
out.position = position;
|
||||||
|
out.target = position + transform.forward();
|
||||||
|
out.up = transform.up();
|
||||||
|
out.fovy = camera.orthographic ? camera.orthoSize : camera.fovDeg;
|
||||||
|
out.projection = camera.orthographic ? static_cast<int>(CAMERA_ORTHOGRAPHIC) : static_cast<int>(CAMERA_PERSPECTIVE);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline Camera3D toCamera3D(eng::Entity cameraEntity) {
|
||||||
|
static const eng::Camera defaults{};
|
||||||
|
const eng::Camera* camera = cameraEntity.tryGet<eng::Camera>();
|
||||||
|
return toCamera3D(cameraEntity.transform(), camera ? *camera : defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where a world position lands on the window (pixels, origin top-left) as seen by `camera`.
|
||||||
|
// Uses the current window size; the result is only meaningful once the window exists (inside run()).
|
||||||
|
[[nodiscard]] inline Vec2 worldToScreen(eng::Entity camera, Vec3 worldPosition) {
|
||||||
|
return getWorldToScreenEx(worldPosition, toCamera3D(camera), getScreenWidth(), getScreenHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The world-space ray through a window position (e.g. gfx::getMousePosition()) as seen by `camera`.
|
||||||
|
[[nodiscard]] inline Ray screenRay(eng::Entity camera, Vec2 screenPosition) {
|
||||||
|
return getScreenToWorldRayEx(screenPosition, toCamera3D(camera), getScreenWidth(), getScreenHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eng::gfx
|
||||||
+227
-5
@@ -3,6 +3,7 @@
|
|||||||
// identical meshes are shared instead of duplicated.
|
// identical meshes are shared instead of duplicated.
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "ecs.hpp"
|
#include "ecs.hpp"
|
||||||
|
#include "material.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -16,20 +17,38 @@ struct MeshData {
|
|||||||
std::vector<Vec3> positions;
|
std::vector<Vec3> positions;
|
||||||
std::vector<Vec3> normals;
|
std::vector<Vec3> normals;
|
||||||
std::vector<Rgba> colors;
|
std::vector<Rgba> colors;
|
||||||
|
// Optional: one per vertex, or none at all (empty). (0, 0) is the top-left corner of a texture, v grows downwards.
|
||||||
|
// Without them a material maps its textures from the world instead (UvMode::Auto, see material.hpp).
|
||||||
|
std::vector<Vec2> texcoords;
|
||||||
|
|
||||||
[[nodiscard]] std::size_t vertexCount() const { return positions.size(); }
|
[[nodiscard]] std::size_t vertexCount() const { return positions.size(); }
|
||||||
[[nodiscard]] std::size_t triangleCount() const { return positions.size() / 3; }
|
[[nodiscard]] std::size_t triangleCount() const { return positions.size() / 3; }
|
||||||
[[nodiscard]] bool empty() const { return positions.empty(); }
|
[[nodiscard]] bool empty() const { return positions.empty(); }
|
||||||
|
[[nodiscard]] bool hasTexcoords() const { return !positions.empty() && texcoords.size() == positions.size(); }
|
||||||
|
|
||||||
void addTriangle(Vec3 a, Vec3 b, Vec3 c, Vec3 na, Vec3 nb, Vec3 nc, Rgba ca, Rgba cb, Rgba cc) {
|
void addTriangle(Vec3 a, Vec3 b, Vec3 c, Vec3 na, Vec3 nb, Vec3 nc, Rgba ca, Rgba cb, Rgba cc) {
|
||||||
positions.insert(positions.end(), {a, b, c});
|
positions.insert(positions.end(), {a, b, c});
|
||||||
normals.insert(normals.end(), {na, nb, nc});
|
normals.insert(normals.end(), {na, nb, nc});
|
||||||
colors.insert(colors.end(), {ca, cb, cc});
|
colors.insert(colors.end(), {ca, cb, cc});
|
||||||
|
if (!texcoords.empty()) texcoords.resize(positions.size()); // a mesh that has texture coordinates keeps one per vertex
|
||||||
|
}
|
||||||
|
void addTriangle(Vec3 a, Vec3 b, Vec3 c, Vec3 na, Vec3 nb, Vec3 nc, Rgba ca, Rgba cb, Rgba cc, Vec2 ta, Vec2 tb, Vec2 tc) {
|
||||||
|
texcoords.resize(positions.size()); // triangles added without them so far get (0, 0)
|
||||||
|
positions.insert(positions.end(), {a, b, c});
|
||||||
|
normals.insert(normals.end(), {na, nb, nc});
|
||||||
|
colors.insert(colors.end(), {ca, cb, cc});
|
||||||
|
texcoords.insert(texcoords.end(), {ta, tb, tc});
|
||||||
}
|
}
|
||||||
void append(const MeshData& o) {
|
void append(const MeshData& o) {
|
||||||
|
const std::size_t had = positions.size();
|
||||||
positions.insert(positions.end(), o.positions.begin(), o.positions.end());
|
positions.insert(positions.end(), o.positions.begin(), o.positions.end());
|
||||||
normals.insert(normals.end(), o.normals.begin(), o.normals.end());
|
normals.insert(normals.end(), o.normals.begin(), o.normals.end());
|
||||||
colors.insert(colors.end(), o.colors.begin(), o.colors.end());
|
colors.insert(colors.end(), o.colors.begin(), o.colors.end());
|
||||||
|
if (!texcoords.empty() || !o.texcoords.empty()) { // whichever side has them: the other one's vertices get (0, 0)
|
||||||
|
texcoords.resize(had);
|
||||||
|
texcoords.insert(texcoords.end(), o.texcoords.begin(), o.texcoords.end());
|
||||||
|
texcoords.resize(positions.size());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
[[nodiscard]] Aabb bounds() const {
|
[[nodiscard]] Aabb bounds() const {
|
||||||
Aabb b;
|
Aabb b;
|
||||||
@@ -66,10 +85,52 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ---- components ---------------------------------------------------------------------------
|
// ---- components ---------------------------------------------------------------------------
|
||||||
class MeshRenderer : public Component {
|
|
||||||
|
// Base of every component that draws something (or lights the scene).
|
||||||
|
//
|
||||||
|
// Hiding is purely visual: scripts keep updating, physics keeps simulating, colliders keep colliding
|
||||||
|
// (use Entity::setEntityEnabled() to switch an entity off completely). A hidden mesh also casts no
|
||||||
|
// shadow and a hidden light is off.
|
||||||
|
//
|
||||||
|
// visible this component draws nothing while false
|
||||||
|
// childrenVisible false hides every entity BELOW this one as well; this component itself is
|
||||||
|
// controlled by `visible`, so a parent can stay visible while its subtree is hidden
|
||||||
|
//
|
||||||
|
// Entity::setVisible(false) is the one-call version (hides the entity and everything below it); it works
|
||||||
|
// on plain group entities too, by giving them a Visibility component.
|
||||||
|
class Renderable : public Component {
|
||||||
|
public:
|
||||||
|
bool visible = true;
|
||||||
|
bool childrenVisible = true;
|
||||||
|
|
||||||
|
// Hides/shows this component and, by default, everything below its entity.
|
||||||
|
void setVisible(bool show, bool includeChildren = true) {
|
||||||
|
visible = show;
|
||||||
|
if (includeChildren) childrenVisible = show;
|
||||||
|
}
|
||||||
|
// `visible` and no ancestor hides its children.
|
||||||
|
[[nodiscard]] bool visibleInHierarchy() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draws nothing: gives a plain group entity the same visibility switches as a renderer, so it can hide
|
||||||
|
// its subtree (Entity::setVisible adds one automatically).
|
||||||
|
class Visibility : public Renderable {};
|
||||||
|
|
||||||
|
class MeshRenderer : public Renderable {
|
||||||
public:
|
public:
|
||||||
MeshPtr mesh;
|
MeshPtr mesh;
|
||||||
Rgba tint{255, 255, 255, 255}; // multiplied with the vertex colours
|
Rgba tint{255, 255, 255, 255}; // multiplied with the vertex colours (and with a material's albedo)
|
||||||
|
|
||||||
|
// Surface. The defaults give a matte, non-glowing material.
|
||||||
|
float roughness = 0.85f; // 0 = mirror-like highlights .. 1 = fully diffuse
|
||||||
|
float metallic = 0.0f; // 0 = plastic/stone/wood .. 1 = metal (the colour tints the reflection)
|
||||||
|
float emission = 0.0f; // self-lit: adds colour * emission regardless of lights (glowing: 1..3)
|
||||||
|
// A material (material.hpp) with textures and more properties. While it is set, it decides the surface: the three
|
||||||
|
// numbers above are not used (the tint still is). Share one material between as many renderers as you like.
|
||||||
|
MaterialPtr material;
|
||||||
|
bool castShadows = true;
|
||||||
|
bool receiveShadows = true;
|
||||||
|
bool unlit = false; // ignore all lighting and draw the plain colour
|
||||||
};
|
};
|
||||||
|
|
||||||
// run() renders with the first enabled Camera. It looks down the entity's -Z.
|
// run() renders with the first enabled Camera. It looks down the entity's -Z.
|
||||||
@@ -82,15 +143,167 @@ public:
|
|||||||
float orthoSize = 10.0f; // vertical extent in world units when orthographic
|
float orthoSize = 10.0f; // vertical extent in world units when orthographic
|
||||||
};
|
};
|
||||||
|
|
||||||
// Directional light shining along the entity's -Z (aim it with Transform::lookAt / rotateTo).
|
enum class LightType : std::uint8_t {
|
||||||
class Light : public Component {
|
Directional, // the sun: parallel rays along the entity's -Z, position ignored
|
||||||
|
Point, // radiates from the entity's position
|
||||||
|
Spot, // cone from the entity's position along its -Z
|
||||||
|
};
|
||||||
|
|
||||||
|
// A light source. Aim directional/spot lights with Transform::lookAt / rotateTo. A world without any
|
||||||
|
// Light gets a default sun. Every light is a Renderable: a hidden light is off.
|
||||||
|
//
|
||||||
|
// Brightness: for the sun, `intensity` 1 lights a surface facing it with its plain colour. Point and spot
|
||||||
|
// lights fall off with the inverse square of the distance (smoothly reaching zero at `range`), so give
|
||||||
|
// them more: `intensity` is the brightness one world unit away.
|
||||||
|
class Light : public Renderable {
|
||||||
public:
|
public:
|
||||||
|
LightType type = LightType::Directional;
|
||||||
Vec3 color{1.0f, 0.98f, 0.92f};
|
Vec3 color{1.0f, 0.98f, 0.92f};
|
||||||
float intensity = 1.0f;
|
float intensity = 1.0f;
|
||||||
|
|
||||||
|
float range = 20.0f; // Point/Spot: distance at which the light has faded to nothing
|
||||||
|
float spotAngleDeg = 50.0f; // Spot: full cone angle
|
||||||
|
float spotSoftness = 0.25f; // Spot: 0 = hard edge .. 1 = fades from the axis outwards
|
||||||
|
|
||||||
|
bool castShadows = true; // Directional and Spot lights (see Environment::maxShadowLights)
|
||||||
|
float shadowBias = 1.0f; // multiplier on the automatic depth bias: raise for acne, lower for floating shadows
|
||||||
|
|
||||||
|
static Light directional(Vec3 color = {1.0f, 0.98f, 0.92f}, float intensity = 1.0f) {
|
||||||
|
Light l;
|
||||||
|
l.color = color;
|
||||||
|
l.intensity = intensity;
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
static Light point(Vec3 color, float intensity = 30.0f, float range = 15.0f) {
|
||||||
|
Light l;
|
||||||
|
l.type = LightType::Point;
|
||||||
|
l.color = color;
|
||||||
|
l.intensity = intensity;
|
||||||
|
l.range = range;
|
||||||
|
l.castShadows = false;
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
static Light spot(Vec3 color, float intensity = 60.0f, float range = 25.0f, float angleDeg = 50.0f) {
|
||||||
|
Light l;
|
||||||
|
l.type = LightType::Spot;
|
||||||
|
l.color = color;
|
||||||
|
l.intensity = intensity;
|
||||||
|
l.range = range;
|
||||||
|
l.spotAngleDeg = angleDeg;
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ToneMapping : std::uint8_t { None, Reinhard, Aces };
|
||||||
|
|
||||||
|
// Scene-wide look: ambient light, exposure, fog and shadow quality. The first enabled Environment
|
||||||
|
// in the world is used; without one the defaults below apply.
|
||||||
|
class Environment : public Component {
|
||||||
|
public:
|
||||||
|
// Hemisphere ambient: `ambientSky` lights surfaces facing up, `ambientGround` surfaces facing down.
|
||||||
|
// AmbientZone components override this inside their region.
|
||||||
|
Vec3 ambientSky{0.42f, 0.47f, 0.56f};
|
||||||
|
Vec3 ambientGround{0.20f, 0.18f, 0.16f};
|
||||||
|
float ambientIntensity = 1.0f;
|
||||||
|
|
||||||
|
float exposure = 1.0f;
|
||||||
|
ToneMapping toneMapping = ToneMapping::Aces;
|
||||||
|
|
||||||
|
// Distance fog (exponential squared); 0 = off.
|
||||||
|
Vec3 fogColor{0.62f, 0.70f, 0.82f};
|
||||||
|
float fogDensity = 0.0f;
|
||||||
|
|
||||||
|
// Shadows
|
||||||
|
bool shadows = true;
|
||||||
|
int shadowResolution = 2048; // texels per side, rounded to a power of two in [256, 4096]
|
||||||
|
float shadowDistance = 60.0f; // how far from the camera the sun's shadow reaches
|
||||||
|
float shadowSoftness = 1.0f; // filter radius in texels (0 = hard edges)
|
||||||
|
float shadowStrength = 1.0f; // 0 = no visible shadow .. 1 = fully dark
|
||||||
|
int maxShadowLights = 3; // shadow maps per frame: the sun first, then the nearest shadowing spots
|
||||||
|
|
||||||
|
// Ambient zones are baked into a 3D grid this fine (world units per cell), coarsened automatically
|
||||||
|
// when the zones span more than `zoneMaxCells` cells.
|
||||||
|
float zoneCellSize = 0.5f;
|
||||||
|
int zoneMaxCells = 262144;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ZoneShape : std::uint8_t {
|
||||||
|
Box, // `size`, centred on the entity
|
||||||
|
Sphere, // `radius`, centred on the entity (an ellipsoid when the entity is scaled)
|
||||||
|
Capsule, // `radius` and `height` (end to end) along local Y, centred
|
||||||
|
Cylinder, // `radius` and `height` along local Y, centred
|
||||||
|
Polygon, // `polygon` on the local XZ plane, extruded from y = 0 up to `height`
|
||||||
|
Mesh, // any closed mesh in local space (e.g. an extruded SVG shape)
|
||||||
|
};
|
||||||
|
enum class ZoneMode : std::uint8_t {
|
||||||
|
Replace, // the zone's ambient takes the place of what is below it
|
||||||
|
Add, // adds its ambient to what is below it
|
||||||
|
Multiply, // tints/darkens what is below it (1 = unchanged)
|
||||||
|
};
|
||||||
|
|
||||||
|
// A region of space with its own ambient light.
|
||||||
|
//
|
||||||
|
// The shape lives in the entity's local space, so moving, rotating or scaling the entity moves, turns
|
||||||
|
// and resizes the zone. It can be a box, sphere, capsule, cylinder, any polygon or any closed mesh, and
|
||||||
|
// fades in over `falloff` world units outside its boundary (0 = hard edge). Overlapping zones are applied
|
||||||
|
// in `priority` order (the highest ends up on top); where no zone applies the Environment's ambient is used.
|
||||||
|
// Zones are baked into a grid (see Environment::zoneCellSize); the light is evaluated per pixel, so large
|
||||||
|
// meshes crossing a boundary shade correctly. Disable the component to switch a zone off. Moving, resizing or
|
||||||
|
// recolouring a zone re-bakes only what it touches (about a millisecond on a level of ~90,000 cells), so zones
|
||||||
|
// that follow a player or breathe are fine; a bigger grid (smaller zoneCellSize, larger world) costs more.
|
||||||
|
class AmbientZone : public Component {
|
||||||
|
public:
|
||||||
|
ZoneShape shape = ZoneShape::Box;
|
||||||
|
Vec3 size{4.0f, 3.0f, 4.0f};
|
||||||
|
float radius = 2.0f;
|
||||||
|
float height = 4.0f;
|
||||||
|
std::vector<Vec2> polygon;
|
||||||
|
MeshPtr mesh;
|
||||||
|
|
||||||
|
Vec3 sky{0.42f, 0.47f, 0.56f}; // ambient from above inside the zone
|
||||||
|
Vec3 ground{0.20f, 0.18f, 0.16f}; // ambient from below inside the zone
|
||||||
|
float intensity = 1.0f;
|
||||||
|
ZoneMode mode = ZoneMode::Replace;
|
||||||
|
float falloff = 1.0f; // world units over which the zone fades in outside its boundary
|
||||||
|
int priority = 0;
|
||||||
|
|
||||||
|
static AmbientZone box(Vec3 size, Vec3 sky, Vec3 ground) {
|
||||||
|
AmbientZone z;
|
||||||
|
z.shape = ZoneShape::Box;
|
||||||
|
z.size = size;
|
||||||
|
z.sky = sky;
|
||||||
|
z.ground = ground;
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
static AmbientZone sphere(float radius, Vec3 sky, Vec3 ground) {
|
||||||
|
AmbientZone z;
|
||||||
|
z.shape = ZoneShape::Sphere;
|
||||||
|
z.radius = radius;
|
||||||
|
z.sky = sky;
|
||||||
|
z.ground = ground;
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
static AmbientZone polygonal(std::vector<Vec2> outline, float height, Vec3 sky, Vec3 ground) {
|
||||||
|
AmbientZone z;
|
||||||
|
z.shape = ZoneShape::Polygon;
|
||||||
|
z.polygon = std::move(outline);
|
||||||
|
z.height = height;
|
||||||
|
z.sky = sky;
|
||||||
|
z.ground = ground;
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
static AmbientZone fromMesh(MeshPtr closedMesh, Vec3 sky, Vec3 ground) {
|
||||||
|
AmbientZone z;
|
||||||
|
z.shape = ZoneShape::Mesh;
|
||||||
|
z.mesh = std::move(closedMesh);
|
||||||
|
z.sky = sky;
|
||||||
|
z.ground = ground;
|
||||||
|
return z;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Screen-space text drawn on top of the 3D scene (HUDs, debug read-outs).
|
// Screen-space text drawn on top of the 3D scene (HUDs, debug read-outs).
|
||||||
class Text : public Component {
|
class Text : public Renderable {
|
||||||
public:
|
public:
|
||||||
std::string value;
|
std::string value;
|
||||||
Vec2 position{10, 10}; // pixels from the top-left corner
|
Vec2 position{10, 10}; // pixels from the top-left corner
|
||||||
@@ -98,4 +311,13 @@ public:
|
|||||||
Rgba color{20, 20, 30, 255};
|
Rgba color{20, 20, 30, 255};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calls f(Renderable&) for every renderer-type component on `e` (the renderer's visibility rules read
|
||||||
|
// these). Add new Renderable component types here.
|
||||||
|
template <class F> void forEachRenderable(Entity e, F&& f) {
|
||||||
|
if (MeshRenderer* c = e.tryGet<MeshRenderer>()) f(static_cast<Renderable&>(*c));
|
||||||
|
if (Light* c = e.tryGet<Light>()) f(static_cast<Renderable&>(*c));
|
||||||
|
if (Text* c = e.tryGet<Text>()) f(static_cast<Renderable&>(*c));
|
||||||
|
if (Visibility* c = e.tryGet<Visibility>()) f(static_cast<Renderable&>(*c));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace eng
|
} // namespace eng
|
||||||
@@ -36,9 +36,6 @@ public:
|
|||||||
[[nodiscard]] static Vec2 mouseDelta(); // pixels moved since last frame
|
[[nodiscard]] static Vec2 mouseDelta(); // pixels moved since last frame
|
||||||
[[nodiscard]] static float mouseWheel(); // notches scrolled this frame (+ = away from you)
|
[[nodiscard]] static float mouseWheel(); // notches scrolled this frame (+ = away from you)
|
||||||
|
|
||||||
static void enableCursor();
|
|
||||||
static void disableCursor();
|
|
||||||
|
|
||||||
[[nodiscard]] static Vec2 windowSize(); // framebuffer size in pixels (0,0 when headless)
|
[[nodiscard]] static Vec2 windowSize(); // framebuffer size in pixels (0,0 when headless)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Materials: how a surface looks under light. Every property is a number, a texture, or both (the texture's value is
|
||||||
|
// multiplied by the number, so a map keeps its range and the number turns it up or down).
|
||||||
|
//
|
||||||
|
// albedo the colour of the surface under plain white light albedo (colour) * albedoMap (colour, RGB)
|
||||||
|
// diffuse how much of the light it scatters in all directions diffuse * diffuseMap (R)
|
||||||
|
// specular how strongly a non-metal reflects light like glass specular, specularColor * specularMap (R)
|
||||||
|
// metalness 0 = plastic/stone/wood .. 1 = metal (reflects in its colour, no diffuse) metalness * metalnessMap (R)
|
||||||
|
// roughness 0 = mirror-like highlights .. 1 = fully matte roughness * roughnessMap (R)
|
||||||
|
// normals small bumps without geometry (tangent space, see below) normalMap, normalStrength
|
||||||
|
// emission self-lit: adds albedo * emission whatever the lights do emission
|
||||||
|
//
|
||||||
|
// Colour maps (albedoMap) hold gamma-encoded colours like every colour in the engine; data maps (all the others) hold the
|
||||||
|
// numbers as they are. A normal map stores the surface normal in tangent space as RGB = (x, y, z) * 0.5 + 0.5, with +x towards
|
||||||
|
// the right of the image, +y towards its TOP and +z out of the surface (a flat map is (128, 128, 255)).
|
||||||
|
//
|
||||||
|
// Where the texture coordinates come from: the mesh's own (boxes and spheres have them; so can your own meshes), or the world:
|
||||||
|
// UvMode::World projects the textures along the three axes by position, so ANY geometry takes them with no unwrapping and
|
||||||
|
// neighbouring pieces (the extruded SVG shapes of a level) line up seamlessly. See UvMode.
|
||||||
|
//
|
||||||
|
// Give a MeshRenderer a material with MeshRenderer::material. Materials are shared and meant to be edited: change a field
|
||||||
|
// and every renderer that uses the material follows from the next frame (a flickering lamp is one assignment per frame).
|
||||||
|
// A renderer without a material keeps its own roughness / metallic / emission and the plain look.
|
||||||
|
//
|
||||||
|
// Like meshes, objects with the same material and mesh draw in one instanced draw call.
|
||||||
|
#pragma once
|
||||||
|
#include "math.hpp"
|
||||||
|
#include "texture.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace eng {
|
||||||
|
|
||||||
|
enum class UvMode : std::uint8_t {
|
||||||
|
Auto, // the mesh's own texture coordinates if it has them, else World
|
||||||
|
Mesh, // the mesh's own texture coordinates (a mesh without them shows one texel of each texture)
|
||||||
|
World, // projected from the world along x, y and z (triplanar), blended where faces turn: no UVs needed
|
||||||
|
};
|
||||||
|
|
||||||
|
class Material;
|
||||||
|
using MaterialPtr = std::shared_ptr<Material>;
|
||||||
|
|
||||||
|
class Material {
|
||||||
|
public:
|
||||||
|
Material();
|
||||||
|
Material(const Material&) = delete; // a copy would share the id; clone() makes a proper new material
|
||||||
|
Material& operator=(const Material&) = delete;
|
||||||
|
static MaterialPtr create(); // a new material: white, matte, not shiny, no maps
|
||||||
|
[[nodiscard]] MaterialPtr clone() const; // a new material (and id) with the same numbers and the same textures
|
||||||
|
|
||||||
|
// ---- albedo ----
|
||||||
|
Vec3 albedo{1, 1, 1}; // multiplies the map, the mesh's vertex colours and MeshRenderer::tint
|
||||||
|
TexturePtr albedoMap; // its alpha multiplies the opacity of the tint
|
||||||
|
|
||||||
|
// ---- diffuse: the scattered light ----
|
||||||
|
float diffuse = 1.0f; // 1 as usual .. 0 none (a surface that only reflects)
|
||||||
|
TexturePtr diffuseMap; // R
|
||||||
|
|
||||||
|
// ---- specular: the reflection of non-metals ----
|
||||||
|
float specular = 0.5f; // 0 none .. 1 strong; 0.5 is the 4% reflectance of plastic and glass
|
||||||
|
Vec3 specularColor{1, 1, 1}; // tints it (metals reflect in their albedo instead)
|
||||||
|
TexturePtr specularMap; // R
|
||||||
|
|
||||||
|
// ---- metalness ----
|
||||||
|
float metalness = 0.0f; // 0 .. 1
|
||||||
|
TexturePtr metalnessMap; // R
|
||||||
|
|
||||||
|
// ---- roughness ----
|
||||||
|
float roughness = 0.85f; // 0 .. 1 (the same default as MeshRenderer)
|
||||||
|
TexturePtr roughnessMap; // R
|
||||||
|
|
||||||
|
// ---- normals ----
|
||||||
|
TexturePtr normalMap;
|
||||||
|
float normalStrength = 1.0f; // 0 flat .. 1 as authored .. more exaggerates
|
||||||
|
|
||||||
|
// ---- emission ----
|
||||||
|
float emission = 0.0f; // 0 .. 4: adds albedo * emission regardless of lights (glowing: 1..3)
|
||||||
|
|
||||||
|
// ---- texture coordinates ----
|
||||||
|
UvMode uv = UvMode::Auto;
|
||||||
|
Vec2 uvScale{1, 1}; // repeats per unit: one repeat per world unit for World and for the boxes and spheres of Resources
|
||||||
|
Vec2 uvOffset{0, 0}; // shifts the textures (in repeats)
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint64_t id() const noexcept { return id_; } // unique for the process lifetime
|
||||||
|
|
||||||
|
// Which maps are set, one bit each (the renderer's lit shader takes the same bits).
|
||||||
|
enum MapBit : unsigned { kAlbedoMap = 1, kDiffuseMap = 2, kSpecularMap = 4, kMetalnessMap = 8, kRoughnessMap = 16, kNormalMap = 32 };
|
||||||
|
inline static constexpr int kMapCount = 6;
|
||||||
|
[[nodiscard]] unsigned maps() const noexcept {
|
||||||
|
return (albedoMap ? kAlbedoMap : 0u) | (diffuseMap ? kDiffuseMap : 0u) | (specularMap ? kSpecularMap : 0u) |
|
||||||
|
(metalnessMap ? kMetalnessMap : 0u) | (roughnessMap ? kRoughnessMap : 0u) | (normalMap ? kNormalMap : 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::uint64_t id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace eng
|
||||||
@@ -227,6 +227,79 @@ struct Mat4 {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// General 4x4 inverse (identity when singular). Use inverseAffine() for transforms: it is cheaper.
|
||||||
|
Mat4 inverse() const {
|
||||||
|
const float* a = m;
|
||||||
|
float inv[16];
|
||||||
|
inv[0] = a[5] * a[10] * a[15] - a[5] * a[11] * a[14] - a[9] * a[6] * a[15] + a[9] * a[7] * a[14] + a[13] * a[6] * a[11] - a[13] * a[7] * a[10];
|
||||||
|
inv[4] = -a[4] * a[10] * a[15] + a[4] * a[11] * a[14] + a[8] * a[6] * a[15] - a[8] * a[7] * a[14] - a[12] * a[6] * a[11] + a[12] * a[7] * a[10];
|
||||||
|
inv[8] = a[4] * a[9] * a[15] - a[4] * a[11] * a[13] - a[8] * a[5] * a[15] + a[8] * a[7] * a[13] + a[12] * a[5] * a[11] - a[12] * a[7] * a[9];
|
||||||
|
inv[12] = -a[4] * a[9] * a[14] + a[4] * a[10] * a[13] + a[8] * a[5] * a[14] - a[8] * a[6] * a[13] - a[12] * a[5] * a[10] + a[12] * a[6] * a[9];
|
||||||
|
inv[1] = -a[1] * a[10] * a[15] + a[1] * a[11] * a[14] + a[9] * a[2] * a[15] - a[9] * a[3] * a[14] - a[13] * a[2] * a[11] + a[13] * a[3] * a[10];
|
||||||
|
inv[5] = a[0] * a[10] * a[15] - a[0] * a[11] * a[14] - a[8] * a[2] * a[15] + a[8] * a[3] * a[14] + a[12] * a[2] * a[11] - a[12] * a[3] * a[10];
|
||||||
|
inv[9] = -a[0] * a[9] * a[15] + a[0] * a[11] * a[13] + a[8] * a[1] * a[15] - a[8] * a[3] * a[13] - a[12] * a[1] * a[11] + a[12] * a[3] * a[9];
|
||||||
|
inv[13] = a[0] * a[9] * a[14] - a[0] * a[10] * a[13] - a[8] * a[1] * a[14] + a[8] * a[2] * a[13] + a[12] * a[1] * a[10] - a[12] * a[2] * a[9];
|
||||||
|
inv[2] = a[1] * a[6] * a[15] - a[1] * a[7] * a[14] - a[5] * a[2] * a[15] + a[5] * a[3] * a[14] + a[13] * a[2] * a[7] - a[13] * a[3] * a[6];
|
||||||
|
inv[6] = -a[0] * a[6] * a[15] + a[0] * a[7] * a[14] + a[4] * a[2] * a[15] - a[4] * a[3] * a[14] - a[12] * a[2] * a[7] + a[12] * a[3] * a[6];
|
||||||
|
inv[10] = a[0] * a[5] * a[15] - a[0] * a[7] * a[13] - a[4] * a[1] * a[15] + a[4] * a[3] * a[13] + a[12] * a[1] * a[7] - a[12] * a[3] * a[5];
|
||||||
|
inv[14] = -a[0] * a[5] * a[14] + a[0] * a[6] * a[13] + a[4] * a[1] * a[14] - a[4] * a[2] * a[13] - a[12] * a[1] * a[6] + a[12] * a[2] * a[5];
|
||||||
|
inv[3] = -a[1] * a[6] * a[11] + a[1] * a[7] * a[10] + a[5] * a[2] * a[11] - a[5] * a[3] * a[10] - a[9] * a[2] * a[7] + a[9] * a[3] * a[6];
|
||||||
|
inv[7] = a[0] * a[6] * a[11] - a[0] * a[7] * a[10] - a[4] * a[2] * a[11] + a[4] * a[3] * a[10] + a[8] * a[2] * a[7] - a[8] * a[3] * a[6];
|
||||||
|
inv[11] = -a[0] * a[5] * a[11] + a[0] * a[7] * a[9] + a[4] * a[1] * a[11] - a[4] * a[3] * a[9] - a[8] * a[1] * a[7] + a[8] * a[3] * a[5];
|
||||||
|
inv[15] = a[0] * a[5] * a[10] - a[0] * a[6] * a[9] - a[4] * a[1] * a[10] + a[4] * a[2] * a[9] + a[8] * a[1] * a[6] - a[8] * a[2] * a[5];
|
||||||
|
const float det = a[0] * inv[0] + a[1] * inv[4] + a[2] * inv[8] + a[3] * inv[12];
|
||||||
|
if (std::fabs(det) < 1e-30f) return {};
|
||||||
|
const float id = 1.0f / det;
|
||||||
|
Mat4 r;
|
||||||
|
for (int i = 0; i < 16; ++i) r.m[i] = inv[i] * id;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transforms a point by a projective matrix (perspective divide included).
|
||||||
|
Vec3 transformProjective(Vec3 p) const {
|
||||||
|
const float x = m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12];
|
||||||
|
const float y = m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13];
|
||||||
|
const float z = m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14];
|
||||||
|
const float w = m[3] * p.x + m[7] * p.y + m[11] * p.z + m[15];
|
||||||
|
const float iw = std::fabs(w) > 1e-20f ? 1.0f / w : 1.0f;
|
||||||
|
return {x * iw, y * iw, z * iw};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-handed view matrix: a camera at `eye` looks at `target` down its -Z axis.
|
||||||
|
static Mat4 lookAt(Vec3 eye, Vec3 target, Vec3 up) {
|
||||||
|
const Vec3 f = (target - eye).normalized();
|
||||||
|
Vec3 side = cross(f, up).normalized();
|
||||||
|
if (side.lengthSq() < 0.5f) side = cross(f, std::fabs(f.y) < 0.99f ? Vec3{0, 1, 0} : Vec3{1, 0, 0}).normalized(); // up parallel to the view
|
||||||
|
const Vec3 u = cross(side, f);
|
||||||
|
Mat4 r;
|
||||||
|
r.m[0] = side.x; r.m[4] = side.y; r.m[8] = side.z; r.m[12] = -dot(side, eye);
|
||||||
|
r.m[1] = u.x; r.m[5] = u.y; r.m[9] = u.z; r.m[13] = -dot(u, eye);
|
||||||
|
r.m[2] = -f.x; r.m[6] = -f.y; r.m[10] = -f.z; r.m[14] = dot(f, eye);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
// OpenGL clip space (depth -1..1), vertical field of view in degrees.
|
||||||
|
static Mat4 perspective(float fovyDeg, float aspect, float nearPlane, float farPlane) {
|
||||||
|
const float t = 1.0f / std::tan(fovyDeg * kDegToRad * 0.5f);
|
||||||
|
Mat4 r;
|
||||||
|
r.m[0] = t / aspect;
|
||||||
|
r.m[5] = t;
|
||||||
|
r.m[10] = (farPlane + nearPlane) / (nearPlane - farPlane);
|
||||||
|
r.m[11] = -1.0f;
|
||||||
|
r.m[14] = 2.0f * farPlane * nearPlane / (nearPlane - farPlane);
|
||||||
|
r.m[15] = 0.0f;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
static Mat4 ortho(float left, float right, float bottom, float top, float nearPlane, float farPlane) {
|
||||||
|
Mat4 r;
|
||||||
|
r.m[0] = 2.0f / (right - left);
|
||||||
|
r.m[5] = 2.0f / (top - bottom);
|
||||||
|
r.m[10] = -2.0f / (farPlane - nearPlane);
|
||||||
|
r.m[12] = -(right + left) / (right - left);
|
||||||
|
r.m[13] = -(top + bottom) / (top - bottom);
|
||||||
|
r.m[14] = -(farPlane + nearPlane) / (farPlane - nearPlane);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
// Split into translation / rotation / (lossy) scale.
|
// Split into translation / rotation / (lossy) scale.
|
||||||
void decompose(Vec3& t, Quat& q, Vec3& s) const {
|
void decompose(Vec3& t, Quat& q, Vec3& s) const {
|
||||||
t = translation();
|
t = translation();
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct ExtrudeOptions {
|
|||||||
float defaultBase = 0.0f; // used when a shape has no y label
|
float defaultBase = 0.0f; // used when a shape has no y label
|
||||||
float defaultTop = 1.0f; // used when a shape has no h label
|
float defaultTop = 1.0f; // used when a shape has no h label
|
||||||
Rgba fallbackColor{200, 200, 200, 255}; // when there is neither a color label nor a fill
|
Rgba fallbackColor{200, 200, 200, 255}; // when there is neither a color label nor a fill
|
||||||
float smoothAngleDeg = 40.0f; // wall normals are averaged across edges bending less than this
|
float smoothAngleDeg = 40.0f; // normals are averaged across edges bending less than this (walls, lofted bands, caps)
|
||||||
int precision = 4; // Clipper2 decimal places (in SVG user units)
|
int precision = 4; // Clipper2 decimal places (in SVG user units)
|
||||||
double curveTolerance = 0.05; // max error when flattening curves/arcs, SVG user units
|
double curveTolerance = 0.05; // max error when flattening curves/arcs, SVG user units
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,12 +5,33 @@
|
|||||||
// * An entity with a Collider gets a body. With a RigidBody it is Static / Kinematic /
|
// * An entity with a Collider gets a body. With a RigidBody it is Static / Kinematic /
|
||||||
// Dynamic as requested; without one it is a Static body (a level piece).
|
// Dynamic as requested; without one it is a Static body (a level piece).
|
||||||
// * Bodies are created lazily at the start of the next physics step, so component order
|
// * Bodies are created lazily at the start of the next physics step, so component order
|
||||||
// does not matter. Change a shape/setting at runtime, then call rebuild() on the component.
|
// does not matter. Surface and body settings (friction, restitution, trigger, mass, ...) are
|
||||||
|
// read when the body is created: change them at runtime, then call rebuild() on the component.
|
||||||
// * Every fixed step: entity transforms -> bodies (kinematic bodies are moved, static and
|
// * Every fixed step: entity transforms -> bodies (kinematic bodies are moved, static and
|
||||||
// dynamic bodies are teleported only if you moved the entity), simulate, then dynamic
|
// dynamic bodies are teleported only if you moved the entity: a change under 0.1 mm or 0.01
|
||||||
// bodies -> entity transforms.
|
// degrees is ignored, and slow drifts add up until they count), simulate, then dynamic bodies ->
|
||||||
// * Collider shapes are baked with the entity's world scale at creation time, and shared
|
// entity transforms. That write-back is position and rotation only: the scale you authored, and
|
||||||
// between bodies that ask for the same shape (clones, repeated props, shared meshes).
|
// the picture it makes, stay as they are, whichever signs the scales on the entity or its parents
|
||||||
|
// have. Characters only move.
|
||||||
|
// * Colliders follow their entity's transform. The shape is sized from the entity's WORLD scale
|
||||||
|
// (parents included) and its `offset` scales with it: scale the entity, or any ancestor, and the
|
||||||
|
// collider grows or shrinks with it. Editing size / radius / height / offset / mesh / shape also
|
||||||
|
// takes effect by itself on the next step.
|
||||||
|
// A box scales per axis, a sphere stays round (largest axis wins), a capsule or cylinder takes
|
||||||
|
// its width from the larger of X/Z and its length from Y, a hull or mesh scales per axis.
|
||||||
|
// A negative scale mirrors whatever can land on the other side of the entity (an off-centre
|
||||||
|
// collider, a hull, a mesh); a centred box, sphere, capsule or cylinder looks the same mirrored, so
|
||||||
|
// flipping those costs nothing.
|
||||||
|
// Static and kinematic bodies swap the shape in place. A dynamic body is rebuilt with its linear
|
||||||
|
// and angular velocity carried over; its contact events start over (collisionEnter fires again).
|
||||||
|
// Shapes are shared between bodies that ask for the same shape (clones, repeated props, shared
|
||||||
|
// meshes): a mesh, or a hull scaled alike on every axis, is a cheap wrapper around one shared
|
||||||
|
// shape, while a hull stretched unevenly gets its own, built from the scaled points so it keeps the
|
||||||
|
// proportions of its mesh.
|
||||||
|
// * A character's capsule follows the entity's scale the same way (radius from X/Z, height from Y)
|
||||||
|
// and keeps its fall speed when it is rebuilt.
|
||||||
|
// * Limit: a dynamic body under a parent that is both rotated and scaled unevenly (a sheared parent)
|
||||||
|
// cannot be shown exactly; it gets the closest rotation.
|
||||||
// * Contact events call collisionEnter/Exit(Entity other) and triggerEnter/Exit(Entity other)
|
// * Contact events call collisionEnter/Exit(Entity other) and triggerEnter/Exit(Entity other)
|
||||||
// on components of both entities (Collider::trigger = true makes a sensor volume).
|
// on components of both entities (Collider::trigger = true makes a sensor volume).
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -27,10 +48,10 @@ enum class BodyType : std::uint8_t { Static, Kinematic, Dynamic };
|
|||||||
class Collider : public Component {
|
class Collider : public Component {
|
||||||
public:
|
public:
|
||||||
ShapeType shape = ShapeType::Box;
|
ShapeType shape = ShapeType::Box;
|
||||||
Vec3 size{1, 1, 1}; // Box: full extents
|
Vec3 size{1, 1, 1}; // Box: full extents (before the entity's world scale)
|
||||||
float radius = 0.5f; // Sphere / Capsule / Cylinder
|
float radius = 0.5f; // Sphere / Capsule / Cylinder
|
||||||
float height = 1.0f; // Capsule (total height including caps) / Cylinder
|
float height = 1.0f; // Capsule (total height including caps) / Cylinder
|
||||||
Vec3 offset{0, 0, 0}; // shape centre relative to the entity origin
|
Vec3 offset{0, 0, 0}; // shape centre relative to the entity origin (scaled with the entity)
|
||||||
float friction = 0.5f;
|
float friction = 0.5f;
|
||||||
float restitution = 0.0f;
|
float restitution = 0.0f;
|
||||||
bool trigger = false; // sensor: reports overlaps, never pushes anything
|
bool trigger = false; // sensor: reports overlaps, never pushes anything
|
||||||
|
|||||||
@@ -20,13 +20,20 @@ class Resources {
|
|||||||
public:
|
public:
|
||||||
Resources() = delete;
|
Resources() = delete;
|
||||||
|
|
||||||
// Procedural primitives, centred on the origin, outward facing, vertex-coloured.
|
// Procedural primitives, centred on the origin, outward facing, vertex-coloured. They carry texture coordinates for
|
||||||
|
// materials: a box shows a texture once per world unit on every face (not stretched, however long it is; v grows
|
||||||
|
// downwards from the top edge of a face), a sphere wraps it once around and from pole to pole.
|
||||||
static MeshPtr box(Vec3 size = {1, 1, 1}, Rgba color = {255, 255, 255, 255});
|
static MeshPtr box(Vec3 size = {1, 1, 1}, Rgba color = {255, 255, 255, 255});
|
||||||
static MeshPtr sphere(float radius = 0.5f, int segments = 24, int rings = 14, Rgba color = {255, 255, 255, 255});
|
static MeshPtr sphere(float radius = 0.5f, int segments = 24, int rings = 14, Rgba color = {255, 255, 255, 255});
|
||||||
|
|
||||||
// Your own mesh, cached under `key`: `build` only runs if no live mesh has that key.
|
// Your own mesh, cached under `key`: `build` only runs if no live mesh has that key.
|
||||||
static MeshPtr mesh(const std::string& key, const std::function<MeshData()>& build);
|
static MeshPtr mesh(const std::string& key, const std::function<MeshData()>& build);
|
||||||
|
|
||||||
|
// An image file as a texture (see texture.hpp), shared between everything that asks for the same file with the same
|
||||||
|
// options. Throws std::runtime_error when the file cannot be read or no image decoder is installed (eng::run() installs one).
|
||||||
|
// The cache key includes the file's size and modification time, so edited files are reloaded.
|
||||||
|
static TexturePtr texture(const std::string& path, TextureOptions options = {});
|
||||||
|
|
||||||
// Labelled SVG drawing -> extruded model. Throws std::runtime_error on a missing/invalid file.
|
// Labelled SVG drawing -> extruded model. Throws std::runtime_error on a missing/invalid file.
|
||||||
// The cache key includes the file's size and modification time, so edited files are reloaded.
|
// The cache key includes the file's size and modification time, so edited files are reloaded.
|
||||||
static ModelPtr svg(const std::string& path, const ExtrudeOptions& options = {});
|
static ModelPtr svg(const std::string& path, const ExtrudeOptions& options = {});
|
||||||
@@ -36,7 +43,7 @@ public:
|
|||||||
static void collect();
|
static void collect();
|
||||||
|
|
||||||
struct Stats {
|
struct Stats {
|
||||||
std::size_t liveMeshes = 0, liveModels = 0; // currently shared resources
|
std::size_t liveMeshes = 0, liveModels = 0, liveTextures = 0; // currently shared resources
|
||||||
std::size_t hits = 0, misses = 0; // cache lookups since start
|
std::size_t hits = 0, misses = 0; // cache lookups since start
|
||||||
};
|
};
|
||||||
[[nodiscard]] static Stats stats();
|
[[nodiscard]] static Stats stats();
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// Images for materials (material.hpp). A Texture is immutable RGBA8 pixel data shared by everything that uses it, like
|
||||||
|
// a Mesh: the renderer keeps one GPU copy per Texture::id(), however many materials point at it. Nothing here depends on
|
||||||
|
// raylib/OpenGL, so textures can be built (and tested) headless.
|
||||||
|
//
|
||||||
|
// Image files come through Resources::texture(path). Decoding them is the window's job (eng::run() installs raylib's
|
||||||
|
// decoder: PNG, JPG, BMP, TGA, ...); a program without a window can install its own with setImageLoader().
|
||||||
|
//
|
||||||
|
// Conventions: pixel (0, 0) is the TOP-left corner of the image and texture coordinate v = 0 is its top edge (v grows
|
||||||
|
// downwards, as in raylib). Colour textures (albedo, specular colour) hold gamma-encoded values like every colour in the
|
||||||
|
// engine; data textures (normal, roughness, metalness, specular, diffuse) hold the numbers as they are.
|
||||||
|
#pragma once
|
||||||
|
#include "math.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace eng {
|
||||||
|
|
||||||
|
enum class TextureWrap : std::uint8_t {
|
||||||
|
Repeat, // tiles forever (the default)
|
||||||
|
Clamp, // the edge pixels continue
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TextureFilter : std::uint8_t {
|
||||||
|
Smooth, // mipmaps + trilinear (+ anisotropic where the GPU has it): no shimmer at a distance
|
||||||
|
Pixelated, // nearest pixel, no mipmaps: crisp pixel art
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TextureOptions {
|
||||||
|
TextureWrap wrap = TextureWrap::Repeat;
|
||||||
|
TextureFilter filter = TextureFilter::Smooth;
|
||||||
|
bool operator==(const TextureOptions&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Texture;
|
||||||
|
using TexturePtr = std::shared_ptr<const Texture>;
|
||||||
|
|
||||||
|
class Texture {
|
||||||
|
public:
|
||||||
|
// `rgba`: width * height * 4 bytes, top row first. Throws std::invalid_argument for a size that is not 1..16384 or a buffer
|
||||||
|
// of the wrong length.
|
||||||
|
Texture(int width, int height, std::vector<std::uint8_t> rgba, TextureOptions options);
|
||||||
|
static TexturePtr create(int width, int height, std::vector<std::uint8_t> rgba, TextureOptions options = {});
|
||||||
|
|
||||||
|
// ---- procedural textures (no files needed: placeholders, tests, patterns) ----
|
||||||
|
static TexturePtr solid(Rgba color, TextureOptions options = {}); // 1 x 1
|
||||||
|
// `cells` x `cells` squares alternating between two colours, `size` pixels wide.
|
||||||
|
static TexturePtr checker(int size = 64, int cells = 8, Rgba a = {230, 230, 230, 255}, Rgba b = {90, 90, 90, 255}, TextureOptions options = {});
|
||||||
|
// A tangent-space normal map from a height map (its red channel: white = high), with wrap-around so it tiles.
|
||||||
|
// `strength`: slope per unit of height difference between neighbouring pixels (about 1..8).
|
||||||
|
static TexturePtr normalMapFromHeight(const Texture& height, float strength = 3.0f);
|
||||||
|
|
||||||
|
[[nodiscard]] int width() const noexcept { return width_; }
|
||||||
|
[[nodiscard]] int height() const noexcept { return height_; }
|
||||||
|
[[nodiscard]] const std::vector<std::uint8_t>& pixels() const noexcept { return pixels_; }
|
||||||
|
[[nodiscard]] const TextureOptions& options() const noexcept { return options_; }
|
||||||
|
[[nodiscard]] std::uint64_t id() const noexcept { return id_; } // unique for the process lifetime
|
||||||
|
|
||||||
|
[[nodiscard]] Rgba texel(int x, int y) const; // wraps or clamps like the texture does
|
||||||
|
// Bilinear sample at texture coordinates `uv` (wrapping or clamping like the GPU does), channels 0..1. Handy for tests
|
||||||
|
// and CPU-side effects; the GPU does its own filtering (mipmaps, anisotropy).
|
||||||
|
struct Sample { float r, g, b, a; };
|
||||||
|
[[nodiscard]] Sample sample(Vec2 uv) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int width_, height_;
|
||||||
|
std::vector<std::uint8_t> pixels_;
|
||||||
|
TextureOptions options_;
|
||||||
|
std::uint64_t id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- decoding image files ------------------------------------------------------------------------
|
||||||
|
struct ImageData {
|
||||||
|
int width = 0, height = 0;
|
||||||
|
std::vector<std::uint8_t> rgba; // width * height * 4, top row first
|
||||||
|
};
|
||||||
|
// Reads and decodes the file at `path`; false when it cannot be read.
|
||||||
|
using ImageLoader = std::function<bool(const std::string& path, ImageData& out)>;
|
||||||
|
// eng::run() installs one that uses raylib. Replace it to read another way (or to run without a window).
|
||||||
|
void setImageLoader(ImageLoader loader);
|
||||||
|
[[nodiscard]] bool hasImageLoader();
|
||||||
|
// Decodes `path` with the installed loader. Throws std::runtime_error when there is none or the file cannot be read.
|
||||||
|
[[nodiscard]] ImageData loadImageFile(const std::string& path);
|
||||||
|
|
||||||
|
} // namespace eng
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Demo level. Every shape carries an Inkscape "label" (inkscape:label) with the extrusion labels:
|
|
||||||
y=<base elevation> h=<top elevation> e=(fraction,ratio)... color=#rrggbb
|
|
||||||
#group @id vpatch spatch only=(...) except=(...)
|
|
||||||
Open it in Inkscape and edit the labels in Object Properties (Ctrl+Shift+O). -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="100mm"
|
|
||||||
height="100mm"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
version="1.1"
|
|
||||||
id="svg10"
|
|
||||||
sodipodi:docname="level.svg"
|
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs10" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview10"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:zoom="1.4049375"
|
|
||||||
inkscape:cx="108.90164"
|
|
||||||
inkscape:cy="478.66898"
|
|
||||||
inkscape:window-width="1366"
|
|
||||||
inkscape:window-height="740"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="true">
|
|
||||||
<inkscape:grid
|
|
||||||
id="grid10"
|
|
||||||
units="mm"
|
|
||||||
originx="0"
|
|
||||||
originy="0"
|
|
||||||
spacingx="0.99999999"
|
|
||||||
spacingy="0.99999999"
|
|
||||||
empcolor="#0099e5"
|
|
||||||
empopacity="0.30196078"
|
|
||||||
color="#0099e5"
|
|
||||||
opacity="0.14901961"
|
|
||||||
empspacing="5"
|
|
||||||
enabled="true"
|
|
||||||
visible="true" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<g
|
|
||||||
inkscape:label="terrain"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1">
|
|
||||||
<!-- floor slab: 0.5 thick, top at y=0 -->
|
|
||||||
<rect
|
|
||||||
inkscape:label="y=-0.5 h=-.01 color=#5d7a5a @ground #floor"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="100"
|
|
||||||
height="178.56743"
|
|
||||||
id="rect1"
|
|
||||||
style="stroke-width:1.33629" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:label="walls #walls y=0 h=2.5 color=#9a9aa5"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer2">
|
|
||||||
<!-- the group label is inherited by the four walls below -->
|
|
||||||
<rect
|
|
||||||
inkscape:label="@north"
|
|
||||||
x="3"
|
|
||||||
y="3"
|
|
||||||
width="94"
|
|
||||||
height="3"
|
|
||||||
id="rect2"
|
|
||||||
style="fill:#483e37" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="@south"
|
|
||||||
x="3"
|
|
||||||
y="173.37502"
|
|
||||||
width="94"
|
|
||||||
height="3"
|
|
||||||
id="rect3"
|
|
||||||
style="fill:#483e37" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="@west"
|
|
||||||
x="3"
|
|
||||||
y="6"
|
|
||||||
width="3"
|
|
||||||
height="167.37502"
|
|
||||||
id="rect4"
|
|
||||||
style="fill:#483e37;stroke-width:1.37913" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="@east"
|
|
||||||
x="94"
|
|
||||||
y="6"
|
|
||||||
width="3"
|
|
||||||
height="167.37502"
|
|
||||||
id="rect5"
|
|
||||||
style="fill:#483e37;stroke-width:1.37913" />
|
|
||||||
<!-- spatch: patches every #walls shape it touches (here: only the east wall) -->
|
|
||||||
<rect
|
|
||||||
inkscape:label="spatch h=5 color=#c9a0a0 only=(#walls) except=(@north @south @west)"
|
|
||||||
x="92"
|
|
||||||
y="40"
|
|
||||||
width="8"
|
|
||||||
height="12"
|
|
||||||
style="fill:#ff00ff;fill-opacity:0.3"
|
|
||||||
id="rect6" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer5"
|
|
||||||
inkscape:label="house #house y=0 h=5">
|
|
||||||
<rect
|
|
||||||
inkscape:label="floor y=-.5 h=0"
|
|
||||||
x="15"
|
|
||||||
y="95"
|
|
||||||
width="70"
|
|
||||||
height="69.999977"
|
|
||||||
id="rect3-6-56-9-3-1-2"
|
|
||||||
style="display:inline;fill:#ac9d93;stroke-width:1.86418" />
|
|
||||||
<g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer6"
|
|
||||||
inkscape:label="wall"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164">
|
|
||||||
<rect
|
|
||||||
inkscape:label="b"
|
|
||||||
x="15"
|
|
||||||
y="162"
|
|
||||||
width="10"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_hi y=4.2 h=5"
|
|
||||||
x="25"
|
|
||||||
y="162"
|
|
||||||
width="50"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_low h=.8"
|
|
||||||
x="25"
|
|
||||||
y="162"
|
|
||||||
width="50"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-3"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="a"
|
|
||||||
x="75"
|
|
||||||
y="162"
|
|
||||||
width="9.999999"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer6-7"
|
|
||||||
inkscape:label="wall"
|
|
||||||
transform="rotate(-90,16.500001,163.5)"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164">
|
|
||||||
<rect
|
|
||||||
inkscape:label="b"
|
|
||||||
x="15"
|
|
||||||
y="162"
|
|
||||||
width="10"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-9"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_hi y=4.2 h=5"
|
|
||||||
x="25"
|
|
||||||
y="162"
|
|
||||||
width="50"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-2"
|
|
||||||
style="fill:#483e37;stroke-width:0.729325" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_low h=.8"
|
|
||||||
x="25"
|
|
||||||
y="162"
|
|
||||||
width="50"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-3-0"
|
|
||||||
style="fill:#483e37;stroke-width:0.729325" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="a"
|
|
||||||
x="75"
|
|
||||||
y="162"
|
|
||||||
width="9.999999"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-2"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer6-75"
|
|
||||||
inkscape:label="wall"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164"
|
|
||||||
transform="rotate(180,50,130)">
|
|
||||||
<rect
|
|
||||||
inkscape:label="b"
|
|
||||||
x="15"
|
|
||||||
y="162"
|
|
||||||
width="9.999999"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-92"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_hi y=4.2 h=5"
|
|
||||||
x="210"
|
|
||||||
y="-85"
|
|
||||||
width="12"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-3-0-5-3"
|
|
||||||
style="fill:#483e37;stroke-width:0.357295"
|
|
||||||
transform="rotate(180,123.5,40)" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_low h=.8"
|
|
||||||
x="-37"
|
|
||||||
y="-165"
|
|
||||||
width="12"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-3-0-5-3-6"
|
|
||||||
style="fill:#483e37;stroke-width:0.357295"
|
|
||||||
transform="scale(-1)" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="b"
|
|
||||||
x="37"
|
|
||||||
y="162"
|
|
||||||
width="9.999999"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-92-8"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_hi y=3 h=5"
|
|
||||||
x="47"
|
|
||||||
y="162"
|
|
||||||
width="6"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-28"
|
|
||||||
style="fill:#483e37;stroke-width:0.112987" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="a"
|
|
||||||
x="75"
|
|
||||||
y="162"
|
|
||||||
width="10"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-7"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_hi y=4.2 h=5"
|
|
||||||
x="172"
|
|
||||||
y="-85"
|
|
||||||
width="11.999999"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-3-0-5"
|
|
||||||
style="fill:#483e37;stroke-width:0.357295"
|
|
||||||
transform="rotate(180,123.5,40)" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_low h=.8"
|
|
||||||
x="-75"
|
|
||||||
y="-165"
|
|
||||||
width="11.999999"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-3-0-5-1"
|
|
||||||
style="fill:#483e37;stroke-width:0.357295"
|
|
||||||
transform="scale(-1)" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="a"
|
|
||||||
x="53"
|
|
||||||
y="162"
|
|
||||||
width="10"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-7-4"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer6-7-3"
|
|
||||||
inkscape:label="wall"
|
|
||||||
transform="rotate(90,83.499998,163.5)"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164">
|
|
||||||
<rect
|
|
||||||
inkscape:label="b"
|
|
||||||
x="15"
|
|
||||||
y="162"
|
|
||||||
width="10"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-9-6"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_hi y=4.2 h=5"
|
|
||||||
x="25"
|
|
||||||
y="162"
|
|
||||||
width="50"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-2-1"
|
|
||||||
style="fill:#483e37;stroke-width:0.729325" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="ab_low h=.8"
|
|
||||||
x="25"
|
|
||||||
y="162"
|
|
||||||
width="50"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-9-3-0-2"
|
|
||||||
style="fill:#483e37;stroke-width:0.729325" />
|
|
||||||
<rect
|
|
||||||
inkscape:label="a"
|
|
||||||
x="75"
|
|
||||||
y="162"
|
|
||||||
width="9.999999"
|
|
||||||
height="2.9999988"
|
|
||||||
id="rect3-6-56-2-9"
|
|
||||||
style="fill:#483e37;stroke-width:0.326164" />
|
|
||||||
</g>
|
|
||||||
<rect
|
|
||||||
inkscape:label="roof y=5 h=5.2"
|
|
||||||
x="15"
|
|
||||||
y="95"
|
|
||||||
width="70"
|
|
||||||
height="69.999977"
|
|
||||||
id="rect3-6-56-9-3-1"
|
|
||||||
style="display:inline;fill:#483e37;stroke-width:1.86418" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:label="props"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer3">
|
|
||||||
<!-- vase: at 30% of the height the outline shrinks to 75%, at 70% it grows to 125% -->
|
|
||||||
<circle
|
|
||||||
inkscape:label="y=0 h=7 e=(0.3,0.75)(0.7,1.25) color=#c9866b #decor @vase"
|
|
||||||
cx="28"
|
|
||||||
cy="28"
|
|
||||||
r="5"
|
|
||||||
id="circle6"
|
|
||||||
style="fill:#a02c2c" />
|
|
||||||
<!-- pyramid: the top ring collapses to a point (ratio 0) -->
|
|
||||||
<rect
|
|
||||||
inkscape:label="y=0 h=4 e=(1,0) color=#e0c060 #decor @pyramid"
|
|
||||||
x="66"
|
|
||||||
y="64"
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
id="rect7"
|
|
||||||
style="fill:#a02c2c" />
|
|
||||||
<!-- ziggurat: equal fractions make a horizontal ledge; the profile starts/ends at ratio 1 unless given -->
|
|
||||||
<rect
|
|
||||||
inkscape:label="y=0 h=3 e=(0.33,1)(0.33,0.75)(0.66,0.75)(0.66,0.5)(1,0.5) color=#8fb8d8 #decor @steps"
|
|
||||||
x="14"
|
|
||||||
y="66"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
id="rect8"
|
|
||||||
style="fill:#a02c2c" />
|
|
||||||
<!-- ring (evenodd hole) -->
|
|
||||||
<path
|
|
||||||
inkscape:label="y=0 h=1 color=#d8d8d8 @ring"
|
|
||||||
style="fill-rule:evenodd;fill:#a02c2c"
|
|
||||||
d="M45,45 h14 v14 h-14 Z M48,48 v8 h8 v-8 Z"
|
|
||||||
id="path8" />
|
|
||||||
<!-- a wall drawn as a stroked line: Clipper2 offsets it into a 2-unit-thick outline -->
|
|
||||||
<path
|
|
||||||
inkscape:label="y=0 h=1.6 color=#a58f7a #decor @fence"
|
|
||||||
d="M22,50 L40,50 L40,38"
|
|
||||||
style="fill:none;stroke:#a02c2c;stroke-width:2"
|
|
||||||
id="path9" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:label="ramp"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer4">
|
|
||||||
<rect
|
|
||||||
inkscape:label="y=0 h=0.3 color=#b58bd6 @ramp #decor"
|
|
||||||
x="56"
|
|
||||||
y="12"
|
|
||||||
width="26"
|
|
||||||
height="12"
|
|
||||||
id="rect9"
|
|
||||||
style="fill:#decd87" />
|
|
||||||
<!-- vpatch: patches only the vertices it covers (the two right-hand corners) of the ramp -->
|
|
||||||
<rect
|
|
||||||
inkscape:label="vpatch h=3 color=#ff5a5a only=(@ramp)"
|
|
||||||
x="80"
|
|
||||||
y="10"
|
|
||||||
width="4"
|
|
||||||
height="16"
|
|
||||||
style="fill:#ff0000;fill-opacity:0.3"
|
|
||||||
id="rect10" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
+956
@@ -0,0 +1,956 @@
|
|||||||
|
// Ambient zones: shape tests for any shape, and the bake of all zones into one 3D grid that the pixel shader
|
||||||
|
// samples (so a large mesh crossing a zone boundary is shaded correctly, per pixel).
|
||||||
|
#include "engine/detail/scene.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <bit>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <deque>
|
||||||
|
#include <limits>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace eng::detail {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr float kInf = std::numeric_limits<float>::infinity();
|
||||||
|
|
||||||
|
float smoothstep01(float x) {
|
||||||
|
x = std::clamp(x, 0.0f, 1.0f);
|
||||||
|
return x * x * (3.0f - 2.0f * x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Shapes
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
struct Box3 {
|
||||||
|
Vec3 lo{kInf, kInf, kInf}, hi{-kInf, -kInf, -kInf};
|
||||||
|
bool valid() const { return lo.x <= hi.x && lo.y <= hi.y && lo.z <= hi.z; }
|
||||||
|
void add(Vec3 p) {
|
||||||
|
lo = {std::min(lo.x, p.x), std::min(lo.y, p.y), std::min(lo.z, p.z)};
|
||||||
|
hi = {std::max(hi.x, p.x), std::max(hi.y, p.y), std::max(hi.z, p.z)};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool usable(const ZoneData& z) {
|
||||||
|
switch (z.shape) {
|
||||||
|
case ZoneShape::Box: return z.size.x > 0 && z.size.y > 0 && z.size.z > 0;
|
||||||
|
case ZoneShape::Sphere: return z.radius > 0;
|
||||||
|
case ZoneShape::Capsule:
|
||||||
|
case ZoneShape::Cylinder: return z.radius > 0 && z.height > 0;
|
||||||
|
case ZoneShape::Polygon: return z.polygon.size() >= 3 && z.height > 0;
|
||||||
|
case ZoneShape::Mesh: return z.mesh && !z.mesh->data().empty() && z.mesh->bounds().valid();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Box3 localBounds(const ZoneData& z) {
|
||||||
|
Box3 b;
|
||||||
|
switch (z.shape) {
|
||||||
|
case ZoneShape::Box: b.add(z.size * -0.5f); b.add(z.size * 0.5f); break;
|
||||||
|
case ZoneShape::Sphere: b.add({-z.radius, -z.radius, -z.radius}); b.add({z.radius, z.radius, z.radius}); break;
|
||||||
|
case ZoneShape::Capsule:
|
||||||
|
case ZoneShape::Cylinder: {
|
||||||
|
const float h = 0.5f * std::max(z.height, z.shape == ZoneShape::Capsule ? 2.0f * z.radius : 0.0f);
|
||||||
|
b.add({-z.radius, -h, -z.radius});
|
||||||
|
b.add({z.radius, h, z.radius});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ZoneShape::Polygon:
|
||||||
|
for (const Vec2& p : z.polygon) { b.add({p.x, 0, p.y}); b.add({p.x, z.height, p.y}); }
|
||||||
|
break;
|
||||||
|
case ZoneShape::Mesh:
|
||||||
|
if (z.mesh && z.mesh->bounds().valid()) { b.add(z.mesh->bounds().min); b.add(z.mesh->bounds().max); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
Box3 worldBounds(const ZoneData& z) {
|
||||||
|
const Box3 l = localBounds(z);
|
||||||
|
Box3 w;
|
||||||
|
if (!l.valid()) return w;
|
||||||
|
for (int i = 0; i < 8; ++i)
|
||||||
|
w.add(z.world.transformPoint({(i & 1) ? l.hi.x : l.lo.x, (i & 2) ? l.hi.y : l.lo.y, (i & 4) ? l.hi.z : l.lo.z}));
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool insidePolygon(const std::vector<Vec2>& poly, float x, float z) { // even-odd, either winding
|
||||||
|
bool in = false;
|
||||||
|
for (std::size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++) {
|
||||||
|
const Vec2 &a = poly[i], &b = poly[j];
|
||||||
|
if (((a.y > z) != (b.y > z)) && (x < (b.x - a.x) * (z - a.y) / (b.y - a.y) + a.x)) in = !in;
|
||||||
|
}
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crossings of the vertical line through (x, z) with a triangle soup, as Y values (callback form).
|
||||||
|
template <class F> void forEachTriangleXZ(const MeshData& md, F&& f) {
|
||||||
|
for (std::size_t i = 0; i + 2 < md.positions.size(); i += 3) f(md.positions[i], md.positions[i + 1], md.positions[i + 2]);
|
||||||
|
}
|
||||||
|
bool crossing(Vec3 a, Vec3 b, Vec3 c, float x, float z, float& y) {
|
||||||
|
auto side = [](Vec3 p, Vec3 q, float px, float pz) { return (q.x - p.x) * (pz - p.z) - (q.z - p.z) * (px - p.x); };
|
||||||
|
const float area = side(a, b, c.x, c.z);
|
||||||
|
if (std::fabs(area) < 1e-12f) return false; // vertical or degenerate in XZ
|
||||||
|
const float u = side(b, c, x, z) / area, v = side(c, a, x, z) / area, w = side(a, b, x, z) / area;
|
||||||
|
if (u < 0 || v < 0 || w < 0) return false;
|
||||||
|
y = u * a.y + v * b.y + w * c.y;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference inside test for a closed mesh: ray parity along +Y (jittered so it never runs along an edge).
|
||||||
|
bool insideMeshByRay(const MeshData& md, const Aabb& bounds, Vec3 p) {
|
||||||
|
const float eps = 1e-4f * std::max({bounds.size().x, bounds.size().y, bounds.size().z, 1e-3f});
|
||||||
|
const float x = p.x + 0.7313f * eps, z = p.z + 0.4129f * eps;
|
||||||
|
int hits = 0;
|
||||||
|
forEachTriangleXZ(md, [&](Vec3 a, Vec3 b, Vec3 c) {
|
||||||
|
float y;
|
||||||
|
if (crossing(a, b, c, x, z, y) && y > p.y) ++hits;
|
||||||
|
});
|
||||||
|
return (hits & 1) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool containsLocal(const ZoneData& z, Vec3 l) {
|
||||||
|
switch (z.shape) {
|
||||||
|
case ZoneShape::Box:
|
||||||
|
return std::fabs(l.x) <= 0.5f * z.size.x && std::fabs(l.y) <= 0.5f * z.size.y && std::fabs(l.z) <= 0.5f * z.size.z;
|
||||||
|
case ZoneShape::Sphere: return l.lengthSq() <= z.radius * z.radius;
|
||||||
|
case ZoneShape::Capsule: {
|
||||||
|
const float half = std::max(0.0f, 0.5f * z.height - z.radius);
|
||||||
|
const float cy = std::clamp(l.y, -half, half);
|
||||||
|
return l.x * l.x + (l.y - cy) * (l.y - cy) + l.z * l.z <= z.radius * z.radius;
|
||||||
|
}
|
||||||
|
case ZoneShape::Cylinder: return l.x * l.x + l.z * l.z <= z.radius * z.radius && std::fabs(l.y) <= 0.5f * z.height;
|
||||||
|
case ZoneShape::Polygon: return l.y >= 0 && l.y <= z.height && insidePolygon(z.polygon, l.x, l.z);
|
||||||
|
case ZoneShape::Mesh:
|
||||||
|
return z.mesh && z.mesh->bounds().valid() && l.x >= z.mesh->bounds().min.x && l.x <= z.mesh->bounds().max.x &&
|
||||||
|
l.y >= z.mesh->bounds().min.y && l.y <= z.mesh->bounds().max.y && l.z >= z.mesh->bounds().min.z &&
|
||||||
|
l.z <= z.mesh->bounds().max.z && insideMeshByRay(z.mesh->data(), z.mesh->bounds(), l);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Closed meshes, voxelised once (scanline: every XZ column is crossed with the triangles, the Y crossings
|
||||||
|
// are paired up, and the voxels between each pair are inside).
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
struct MeshVolume {
|
||||||
|
Vec3 origin{};
|
||||||
|
float vox = 1;
|
||||||
|
int nx = 0, ny = 0, nz = 0;
|
||||||
|
std::vector<std::uint8_t> inside; // (y * nz + z) * nx + x
|
||||||
|
|
||||||
|
bool contains(Vec3 l) const {
|
||||||
|
const int ix = static_cast<int>(std::floor((l.x - origin.x) / vox));
|
||||||
|
const int iy = static_cast<int>(std::floor((l.y - origin.y) / vox));
|
||||||
|
const int iz = static_cast<int>(std::floor((l.z - origin.z) / vox));
|
||||||
|
if (ix < 0 || iy < 0 || iz < 0 || ix >= nx || iy >= ny || iz >= nz) return false;
|
||||||
|
return inside[(static_cast<std::size_t>(iy) * static_cast<std::size_t>(nz) + static_cast<std::size_t>(iz)) * static_cast<std::size_t>(nx) +
|
||||||
|
static_cast<std::size_t>(ix)] != 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<MeshVolume> voxelise(const MeshData& md, const Aabb& bounds, float targetVox, std::size_t maxVoxels) {
|
||||||
|
auto vol = std::make_shared<MeshVolume>();
|
||||||
|
float vox = std::max(targetVox, 1e-4f);
|
||||||
|
const Vec3 ext = bounds.size();
|
||||||
|
auto dimsFor = [&](float v) {
|
||||||
|
return std::array<std::size_t, 3>{static_cast<std::size_t>(std::ceil(ext.x / v)) + 2, static_cast<std::size_t>(std::ceil(ext.y / v)) + 2,
|
||||||
|
static_cast<std::size_t>(std::ceil(ext.z / v)) + 2};
|
||||||
|
};
|
||||||
|
auto d = dimsFor(vox);
|
||||||
|
while (d[0] * d[1] * d[2] > maxVoxels) { vox *= 1.2f; d = dimsFor(vox); }
|
||||||
|
vol->vox = vox;
|
||||||
|
vol->nx = static_cast<int>(d[0]);
|
||||||
|
vol->ny = static_cast<int>(d[1]);
|
||||||
|
vol->nz = static_cast<int>(d[2]);
|
||||||
|
vol->origin = bounds.min - Vec3{vox, vox, vox} * 0.5f;
|
||||||
|
vol->inside.assign(d[0] * d[1] * d[2], 0);
|
||||||
|
|
||||||
|
const std::size_t columns = d[0] * d[2];
|
||||||
|
const float jx = 0.0173f * vox, jz = 0.0291f * vox; // keeps column centres off shared edges
|
||||||
|
auto colIndex = [&](int ix, int iz) { return static_cast<std::size_t>(iz) * d[0] + static_cast<std::size_t>(ix); };
|
||||||
|
|
||||||
|
// two passes over the triangles: count crossings per column, then fill (CSR layout, no per-column vectors)
|
||||||
|
auto traverse = [&](auto&& emit) {
|
||||||
|
forEachTriangleXZ(md, [&](Vec3 a, Vec3 b, Vec3 c) {
|
||||||
|
const float minx = std::min({a.x, b.x, c.x}), maxx = std::max({a.x, b.x, c.x});
|
||||||
|
const float minz = std::min({a.z, b.z, c.z}), maxz = std::max({a.z, b.z, c.z});
|
||||||
|
const int x0 = std::max(0, static_cast<int>(std::floor((minx - vol->origin.x) / vox - 0.5f)));
|
||||||
|
const int x1 = std::min(vol->nx - 1, static_cast<int>(std::ceil((maxx - vol->origin.x) / vox - 0.5f)));
|
||||||
|
const int z0 = std::max(0, static_cast<int>(std::floor((minz - vol->origin.z) / vox - 0.5f)));
|
||||||
|
const int z1 = std::min(vol->nz - 1, static_cast<int>(std::ceil((maxz - vol->origin.z) / vox - 0.5f)));
|
||||||
|
for (int iz = z0; iz <= z1; ++iz)
|
||||||
|
for (int ix = x0; ix <= x1; ++ix) {
|
||||||
|
const float px = vol->origin.x + (static_cast<float>(ix) + 0.5f) * vox + jx;
|
||||||
|
const float pz = vol->origin.z + (static_cast<float>(iz) + 0.5f) * vox + jz;
|
||||||
|
float y;
|
||||||
|
if (crossing(a, b, c, px, pz, y)) emit(colIndex(ix, iz), y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
std::vector<std::uint32_t> start(columns + 1, 0);
|
||||||
|
traverse([&](std::size_t col, float) { ++start[col + 1]; });
|
||||||
|
for (std::size_t i = 0; i < columns; ++i) start[i + 1] += start[i];
|
||||||
|
std::vector<float> ys(start.back());
|
||||||
|
std::vector<std::uint32_t> fillAt(start.begin(), start.end() - 1);
|
||||||
|
traverse([&](std::size_t col, float y) { ys[fillAt[col]++] = y; });
|
||||||
|
|
||||||
|
for (std::size_t col = 0; col < columns; ++col) {
|
||||||
|
float* b = ys.data() + start[col];
|
||||||
|
float* e = ys.data() + start[col + 1];
|
||||||
|
std::sort(b, e);
|
||||||
|
const std::size_t n = static_cast<std::size_t>(e - b) & ~static_cast<std::size_t>(1); // an odd leftover means a leaky mesh: ignore it
|
||||||
|
const int ix = static_cast<int>(col % d[0]), iz = static_cast<int>(col / d[0]);
|
||||||
|
for (std::size_t k = 0; k + 1 < n; k += 2) {
|
||||||
|
const int y0 = std::max(0, static_cast<int>(std::ceil((b[k] - vol->origin.y) / vox - 0.5f)));
|
||||||
|
const int y1 = std::min(vol->ny - 1, static_cast<int>(std::floor((b[k + 1] - vol->origin.y) / vox - 0.5f)));
|
||||||
|
for (int iy = y0; iy <= y1; ++iy)
|
||||||
|
vol->inside[(static_cast<std::size_t>(iy) * d[2] + static_cast<std::size_t>(iz)) * d[0] + static_cast<std::size_t>(ix)] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vol;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Hashing of everything a bake depends on
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
struct Hasher {
|
||||||
|
std::uint64_t h = 1469598103934665603ull;
|
||||||
|
void bytes(const void* p, std::size_t n) {
|
||||||
|
const auto* b = static_cast<const unsigned char*>(p);
|
||||||
|
for (std::size_t i = 0; i < n; ++i) { h ^= b[i]; h *= 1099511628211ull; }
|
||||||
|
}
|
||||||
|
template <class T> void add(const T& v) { bytes(&v, sizeof v); }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::uint64_t hashInputs(const std::vector<ZoneData>& zones, const Environment& env) {
|
||||||
|
Hasher h;
|
||||||
|
h.add(env.ambientSky); h.add(env.ambientGround); h.add(env.ambientIntensity);
|
||||||
|
h.add(env.zoneCellSize); h.add(env.zoneMaxCells);
|
||||||
|
h.add(zones.size());
|
||||||
|
for (const ZoneData& z : zones) {
|
||||||
|
h.bytes(z.world.m, sizeof z.world.m);
|
||||||
|
h.add(z.shape); h.add(z.size); h.add(z.radius); h.add(z.height);
|
||||||
|
for (const Vec2& p : z.polygon) h.add(p);
|
||||||
|
h.add(z.polygon.size());
|
||||||
|
h.add(static_cast<std::uint64_t>(z.mesh ? z.mesh->id() : 0));
|
||||||
|
h.add(z.sky); h.add(z.ground); h.add(z.intensity); h.add(z.mode); h.add(z.falloff); h.add(z.priority);
|
||||||
|
}
|
||||||
|
return h.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the layer of a zone (its reach: see AmbientBaker::Impl::Layer) depends on. Colours, mode and priority are
|
||||||
|
// not part of it, so recolouring or re-prioritising a zone keeps its layer.
|
||||||
|
std::uint64_t layerKey(const ZoneData& z, float cell) {
|
||||||
|
Hasher h;
|
||||||
|
h.bytes(z.world.m, sizeof z.world.m);
|
||||||
|
h.add(z.shape); h.add(z.size); h.add(z.radius); h.add(z.height);
|
||||||
|
for (const Vec2& p : z.polygon) h.add(p);
|
||||||
|
h.add(z.polygon.size());
|
||||||
|
h.add(static_cast<std::uint64_t>(z.mesh ? z.mesh->id() : 0));
|
||||||
|
h.add(z.falloff);
|
||||||
|
h.add(cell);
|
||||||
|
return h.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint8_t toByte(float v) { return static_cast<std::uint8_t>(std::lround(std::clamp(v, 0.0f, 1.0f) * 255.0f)); }
|
||||||
|
|
||||||
|
// Texel encoding. encodeLinearToByte(v) is, for every float v, exactly toByte(toGamma(v)): the gamma curve and the
|
||||||
|
// rounding the texels have always had, but without a pow() per texel (which was more than half the cost of a bake).
|
||||||
|
// The byte only changes at 255 thresholds. They are found once, by bisection on the real function, and a table over
|
||||||
|
// [0, 1] says which one to start from, so a lookup is a table read and (at most a few) comparisons.
|
||||||
|
struct GammaCodec {
|
||||||
|
static constexpr int kBins = 2048;
|
||||||
|
float threshold[256] = {}; // threshold[k]: the smallest v that encodes to k or more (k = 1..255)
|
||||||
|
std::uint8_t binStart[kBins + 1] = {}; // the byte of v = i / kBins
|
||||||
|
|
||||||
|
static std::uint8_t reference(float v) { return toByte(std::pow(std::max(v, 0.0f), 1.0f / kGamma)); }
|
||||||
|
|
||||||
|
GammaCodec() {
|
||||||
|
for (int k = 1; k <= 255; ++k) {
|
||||||
|
std::uint32_t lo = 0, hi = std::bit_cast<std::uint32_t>(1.0f); // floats >= 0 order like their bits: reference(lo) < k <= reference(hi)
|
||||||
|
while (hi - lo > 1) {
|
||||||
|
const std::uint32_t mid = lo + (hi - lo) / 2;
|
||||||
|
if (reference(std::bit_cast<float>(mid)) >= k) hi = mid; else lo = mid;
|
||||||
|
}
|
||||||
|
threshold[k] = std::bit_cast<float>(hi);
|
||||||
|
}
|
||||||
|
int b = 0;
|
||||||
|
for (int i = 0; i <= kBins; ++i) {
|
||||||
|
const float v = static_cast<float>(i) / static_cast<float>(kBins);
|
||||||
|
while (b < 255 && v >= threshold[b + 1]) ++b;
|
||||||
|
binStart[i] = static_cast<std::uint8_t>(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint8_t encode(float v) const {
|
||||||
|
if (!(v > 0.0f)) return 0;
|
||||||
|
if (v >= threshold[255]) return 255;
|
||||||
|
int b = binStart[static_cast<int>(v * static_cast<float>(kBins))];
|
||||||
|
while (b < 255 && v >= threshold[b + 1]) ++b;
|
||||||
|
return static_cast<std::uint8_t>(b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const GammaCodec& gammaCodec() {
|
||||||
|
static const GammaCodec codec;
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The largest component of any colour in a row of cells, at least 1. Independent accumulators per component keep it from
|
||||||
|
// being one long dependency chain.
|
||||||
|
float largestComponent(const Vec3* a, const Vec3* b, int n) {
|
||||||
|
static_assert(sizeof(Vec3) == 3 * sizeof(float));
|
||||||
|
float mx = 1.0f, my = 1.0f, mz = 1.0f;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
mx = a[i].x > mx ? a[i].x : mx;
|
||||||
|
my = a[i].y > my ? a[i].y : my;
|
||||||
|
mz = a[i].z > mz ? a[i].z : mz;
|
||||||
|
mx = b[i].x > mx ? b[i].x : mx;
|
||||||
|
my = b[i].y > my ? b[i].y : my;
|
||||||
|
mz = b[i].z > mz ? b[i].z : mz;
|
||||||
|
}
|
||||||
|
return std::max({mx, my, mz});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The colour scale of a grid: the brightest colour, rounded UP to a step of a fixed ladder (1, 1.25, 1.5, 1.75, 2, 2.5,
|
||||||
|
// 3, 3.5, 4, 5, ...: four steps per octave), and at least 1. Every texel is stored divided by it, so it is part of every
|
||||||
|
// texel: a scale that followed the brightest cell exactly would change (and re-encode and re-upload the whole grid)
|
||||||
|
// every time a bright zone and a moving one overlap by a little more or less. On the ladder it only changes when the
|
||||||
|
// brightest colour crosses a rung. It is a function of that colour alone, so a bake from scratch finds the same one.
|
||||||
|
float colorScaleFor(float top) {
|
||||||
|
if (!(top > 1.0f)) return 1.0f; // also NaN
|
||||||
|
if (!std::isfinite(top)) return top;
|
||||||
|
int e = 0;
|
||||||
|
const float m = std::frexp(top, &e); // top = m * 2^e with m in [0.5, 1)
|
||||||
|
return std::ldexp(std::ceil(m * 8.0f) / 8.0f, e); // exact: the multiplications and the division are by powers of two
|
||||||
|
}
|
||||||
|
|
||||||
|
// The grid's extent is rounded out to whole blocks of cells (Y in smaller ones: levels are wider than they are tall), so a
|
||||||
|
// zone that walks beyond the others does not resize the grid on every step, only when it crosses a block edge. The rounding
|
||||||
|
// needs room: a grid that is already close to the cell budget gets smaller blocks, and finally none (see update()).
|
||||||
|
struct Blocks { int xz, y; };
|
||||||
|
constexpr Blocks kBlocks[] = {{8, 4}, {4, 2}, {2, 1}, {1, 1}};
|
||||||
|
|
||||||
|
// A box of lattice cells, both ends included (empty when hi < lo on any axis).
|
||||||
|
struct Range {
|
||||||
|
int lo[3] = {0, 0, 0}, hi[3] = {-1, -1, -1};
|
||||||
|
[[nodiscard]] bool empty() const { return hi[0] < lo[0] || hi[1] < lo[1] || hi[2] < lo[2]; }
|
||||||
|
[[nodiscard]] std::size_t volume() const {
|
||||||
|
return empty() ? 0 : static_cast<std::size_t>(hi[0] - lo[0] + 1) * static_cast<std::size_t>(hi[1] - lo[1] + 1) * static_cast<std::size_t>(hi[2] - lo[2] + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Range intersect(const Range& a, const Range& b) {
|
||||||
|
Range r;
|
||||||
|
for (int i = 0; i < 3; ++i) { r.lo[i] = std::max(a.lo[i], b.lo[i]); r.hi[i] = std::min(a.hi[i], b.hi[i]); }
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
Range hull(const Range& a, const Range& b) {
|
||||||
|
Range r;
|
||||||
|
for (int i = 0; i < 3; ++i) { r.lo[i] = std::min(a.lo[i], b.lo[i]); r.hi[i] = std::max(a.hi[i], b.hi[i]); }
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything one zone contributes to a bake: where it is (its layer key), its colours, intensity and mode, and its place in
|
||||||
|
// the blending order (its priority and its rank among the zones of that priority). Two zones with the same key paint the
|
||||||
|
// same cells in the same colours at the same point of the blend, whichever bake they come from.
|
||||||
|
std::uint64_t zoneKey(const ZoneData& z, std::uint64_t layer, int tie) {
|
||||||
|
Hasher h;
|
||||||
|
h.add(layer);
|
||||||
|
h.add(z.sky); h.add(z.ground); h.add(z.intensity); h.add(z.mode); h.add(z.priority); h.add(tie);
|
||||||
|
return h.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y slices are laid out as a roughly square grid of tiles.
|
||||||
|
int atlasTilesX(int ny) { return std::max(1, static_cast<int>(std::ceil(std::sqrt(static_cast<double>(ny))))); }
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Vec3 toLinear(Vec3 c) { return {std::pow(std::max(c.x, 0.0f), kGamma), std::pow(std::max(c.y, 0.0f), kGamma), std::pow(std::max(c.z, 0.0f), kGamma)}; }
|
||||||
|
Vec3 toGamma(Vec3 c) {
|
||||||
|
const float g = 1.0f / kGamma;
|
||||||
|
return {std::pow(std::max(c.x, 0.0f), g), std::pow(std::max(c.y, 0.0f), g), std::pow(std::max(c.z, 0.0f), g)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint8_t encodeLinearToByte(float v) { return gammaCodec().encode(v); }
|
||||||
|
|
||||||
|
bool zoneContains(const ZoneData& zone, Vec3 worldPoint) {
|
||||||
|
return usable(zone) && containsLocal(zone, zone.inverse.transformPoint(worldPoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// AmbientGrid
|
||||||
|
// ===========================================================================
|
||||||
|
void AmbientGrid::sample(Vec3 p, Vec3& outSky, Vec3& outGround) const {
|
||||||
|
if (!valid) { outSky = outGround = {}; return; }
|
||||||
|
const float gx = (p.x - origin.x) / cell - 0.5f, gy = (p.y - origin.y) / cell - 0.5f, gz = (p.z - origin.z) / cell - 0.5f;
|
||||||
|
const int x0 = static_cast<int>(std::floor(gx)), y0 = static_cast<int>(std::floor(gy)), z0 = static_cast<int>(std::floor(gz));
|
||||||
|
const float tx = gx - static_cast<float>(x0), ty = gy - static_cast<float>(y0), tz = gz - static_cast<float>(z0);
|
||||||
|
auto at = [&](const std::vector<std::uint8_t>& v, int x, int y, int z) { // an encoded texel, 0..1
|
||||||
|
x = std::clamp(x, 0, nx - 1); y = std::clamp(y, 0, ny - 1); z = std::clamp(z, 0, nz - 1);
|
||||||
|
const std::size_t i = ((static_cast<std::size_t>(y) * static_cast<std::size_t>(nz) + static_cast<std::size_t>(z)) * static_cast<std::size_t>(nx) +
|
||||||
|
static_cast<std::size_t>(x)) * 4;
|
||||||
|
return Vec3{static_cast<float>(v[i]), static_cast<float>(v[i + 1]), static_cast<float>(v[i + 2])} * (1.0f / 255.0f);
|
||||||
|
};
|
||||||
|
auto tri = [&](const std::vector<std::uint8_t>& v) {
|
||||||
|
Vec3 r{};
|
||||||
|
for (int dz = 0; dz < 2; ++dz)
|
||||||
|
for (int dy = 0; dy < 2; ++dy)
|
||||||
|
for (int dx = 0; dx < 2; ++dx)
|
||||||
|
r += at(v, x0 + dx, y0 + dy, z0 + dz) * ((dx ? tx : 1 - tx) * (dy ? ty : 1 - ty) * (dz ? tz : 1 - tz));
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
outSky = toLinear(tri(sky)) * colorScale; // filter the encoded texels, then decode (as the GPU does)
|
||||||
|
outGround = toLinear(tri(ground)) * colorScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// AmbientBaker
|
||||||
|
// ===========================================================================
|
||||||
|
struct AmbientBaker::Impl {
|
||||||
|
// ---- voxelised closed meshes, cached by mesh id and voxel size ----
|
||||||
|
std::unordered_map<std::uint64_t, std::shared_ptr<MeshVolume>> volumes; // mesh id + voxel size bucket
|
||||||
|
std::unordered_set<std::uint64_t> usedThisBake;
|
||||||
|
|
||||||
|
// Voxel size for a mesh zone: half a bake cell in WORLD units, expressed in the mesh's own space, quantised
|
||||||
|
// to powers of sqrt(2) so small scale animations reuse the cached voxelisation.
|
||||||
|
static std::uint64_t volumeKey(const ZoneData& z, float bakeCell, int& bucket) {
|
||||||
|
const Vec3 c0{z.world.m[0], z.world.m[1], z.world.m[2]}, c1{z.world.m[4], z.world.m[5], z.world.m[6]},
|
||||||
|
c2{z.world.m[8], z.world.m[9], z.world.m[10]};
|
||||||
|
const float scale = std::max({c0.length(), c1.length(), c2.length(), 1e-4f});
|
||||||
|
bucket = static_cast<int>(std::lround(std::log2(std::max(0.5f * bakeCell / scale, 1e-4f)) * 2.0f));
|
||||||
|
return (static_cast<std::uint64_t>(z.mesh->id()) << 8) ^ static_cast<std::uint64_t>(static_cast<std::uint8_t>(bucket + 128));
|
||||||
|
}
|
||||||
|
const MeshVolume* volumeFor(const ZoneData& z, float bakeCell) {
|
||||||
|
int bucket = 0;
|
||||||
|
const std::uint64_t key = volumeKey(z, bakeCell, bucket);
|
||||||
|
usedThisBake.insert(key);
|
||||||
|
if (const auto it = volumes.find(key); it != volumes.end()) return it->second.get();
|
||||||
|
const float vox = std::exp2(static_cast<float>(bucket) * 0.5f);
|
||||||
|
return (volumes[key] = voxelise(z.mesh->data(), z.mesh->bounds(), vox, 4'000'000)).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- layers: where one zone is, and how far its fade-out reaches ----
|
||||||
|
// A layer holds, for every cell of the lattice around a zone, the zone's weight there: 1 inside, fading to 0 over
|
||||||
|
// the zone's falloff. It depends only on the zone's shape and transform (see layerKey), so it is computed once
|
||||||
|
// and reused until that zone changes: moving one zone does not recompute any other, and colours, mode and
|
||||||
|
// priority (which only decide how layers are blended) are free to change.
|
||||||
|
struct Layer {
|
||||||
|
int x0 = 0, y0 = 0, z0 = 0; // first cell, in lattice coordinates (cell i covers [i * cell, (i + 1) * cell))
|
||||||
|
int rx = 0, ry = 0, rz = 0; // size in cells
|
||||||
|
std::vector<float> w; // (y * rz + z) * rx + x
|
||||||
|
bool used = false;
|
||||||
|
};
|
||||||
|
static constexpr std::size_t kLayerCacheCells = 8'000'000; // 32 MB of weights; beyond that layers are rebuilt every bake
|
||||||
|
std::unordered_map<std::uint64_t, Layer> layers;
|
||||||
|
std::size_t layerCells = 0;
|
||||||
|
|
||||||
|
// ---- what the previous bake left behind, so the next one only touches what changed ----
|
||||||
|
struct Lattice {
|
||||||
|
int lo[3] = {}; // first cell of the grid, in lattice coordinates
|
||||||
|
int n[3] = {}; // cells per axis
|
||||||
|
float cell = 0;
|
||||||
|
bool operator==(const Lattice&) const = default;
|
||||||
|
};
|
||||||
|
Lattice lattice;
|
||||||
|
bool haveState = false; // the members below describe a finished bake
|
||||||
|
std::vector<Vec3> lin[2]; // the blended colour of every cell of `lattice`, linear light: [0] sky, [1] ground
|
||||||
|
std::vector<float> rowMax; // for every (y, z) row of the lattice: its largest colour component, at least 1
|
||||||
|
float scale = 1; // the colour scale the texels were encoded with
|
||||||
|
std::uint64_t baseKey = 0; // the Environment's own ambient (what a cell holds where no zone is)
|
||||||
|
std::vector<std::pair<std::uint64_t, Range>> placed; // the zones of the previous bake: their keys and the cells their layers cover
|
||||||
|
std::vector<Vec3> neu[2]; // scratch: the new colours of one box of cells
|
||||||
|
std::vector<Vec3> spare[2]; // the colours of the grid before the last one: its memory is reused by the next grid
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
volumes.clear();
|
||||||
|
layers.clear();
|
||||||
|
layerCells = 0;
|
||||||
|
for (auto* v : {&lin[0], &lin[1], &neu[0], &neu[1], &spare[0], &spare[1]}) std::vector<Vec3>().swap(*v);
|
||||||
|
std::vector<float>().swap(rowMax);
|
||||||
|
placed.clear();
|
||||||
|
lattice = {};
|
||||||
|
haveState = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void computeLayer(const ZoneData& z, float cell, Layer& L) {
|
||||||
|
const float fade = z.falloff > 0.0f ? z.falloff : 0.0f;
|
||||||
|
const Box3 wb = worldBounds(z);
|
||||||
|
const float reach = fade + cell;
|
||||||
|
auto cellOf = [&](float v) { return static_cast<int>(std::floor(v / cell)); };
|
||||||
|
L.x0 = cellOf(wb.lo.x - reach);
|
||||||
|
L.y0 = cellOf(wb.lo.y - reach);
|
||||||
|
L.z0 = cellOf(wb.lo.z - reach);
|
||||||
|
L.rx = cellOf(wb.hi.x + reach) - L.x0 + 1;
|
||||||
|
L.ry = cellOf(wb.hi.y + reach) - L.y0 + 1;
|
||||||
|
L.rz = cellOf(wb.hi.z + reach) - L.z0 + 1;
|
||||||
|
const std::size_t rx = static_cast<std::size_t>(L.rx), ry = static_cast<std::size_t>(L.ry), rz = static_cast<std::size_t>(L.rz);
|
||||||
|
const MeshVolume* vol = z.shape == ZoneShape::Mesh ? volumeFor(z, cell) : nullptr;
|
||||||
|
|
||||||
|
std::vector<float>& dist = L.w; // distances first, turned into weights at the end
|
||||||
|
dist.assign(rx * ry * rz, kInf);
|
||||||
|
auto ridx = [&](int x, int y, int zz) { return (static_cast<std::size_t>(y) * rz + static_cast<std::size_t>(zz)) * rx + static_cast<std::size_t>(x); };
|
||||||
|
for (int y = 0; y < L.ry; ++y)
|
||||||
|
for (int zz = 0; zz < L.rz; ++zz)
|
||||||
|
for (int x = 0; x < L.rx; ++x) {
|
||||||
|
const Vec3 p{(static_cast<float>(L.x0 + x) + 0.5f) * cell, (static_cast<float>(L.y0 + y) + 0.5f) * cell,
|
||||||
|
(static_cast<float>(L.z0 + zz) + 0.5f) * cell};
|
||||||
|
const Vec3 l = z.inverse.transformPoint(p);
|
||||||
|
if (vol ? vol->contains(l) : containsLocal(z, l)) dist[ridx(x, y, zz)] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fade > 0) { // two-pass 26-neighbour chamfer distance to the nearest inside cell
|
||||||
|
struct Off { int dx, dy, dz; std::ptrdiff_t step; float w; }; // the 13 neighbours a forward sweep has already visited
|
||||||
|
Off fwd[13];
|
||||||
|
int nf = 0;
|
||||||
|
for (int dy = -1; dy <= 1; ++dy)
|
||||||
|
for (int dz = -1; dz <= 1; ++dz)
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
if (dx == 0 && dy == 0 && dz == 0) continue;
|
||||||
|
if (dy < 0 || (dy == 0 && dz < 0) || (dy == 0 && dz == 0 && dx < 0))
|
||||||
|
fwd[nf++] = {dx, dy, dz, (static_cast<std::ptrdiff_t>(dy) * static_cast<std::ptrdiff_t>(rz) + dz) * static_cast<std::ptrdiff_t>(rx) + dx,
|
||||||
|
cell * std::sqrt(static_cast<float>(dx * dx + dy * dy + dz * dz))};
|
||||||
|
}
|
||||||
|
auto relaxChecked = [&](int x, int y, int zz, int sgn) { // a cell on the layer's surface: some neighbours do not exist
|
||||||
|
float best = dist[ridx(x, y, zz)];
|
||||||
|
for (const Off& o : fwd) {
|
||||||
|
const int nx = x + sgn * o.dx, ny = y + sgn * o.dy, nz = zz + sgn * o.dz;
|
||||||
|
if (nx < 0 || ny < 0 || nz < 0 || nx >= L.rx || ny >= L.ry || nz >= L.rz) continue;
|
||||||
|
best = std::min(best, dist[ridx(nx, ny, nz)] + o.w);
|
||||||
|
}
|
||||||
|
dist[ridx(x, y, zz)] = best;
|
||||||
|
};
|
||||||
|
// The sweeps are the cost of a layer (13 neighbours of every cell, twice): a cell with all of them (nearly every
|
||||||
|
// cell) reaches them by a fixed step in memory, without testing the edges.
|
||||||
|
auto sweep = [&](int sgn) {
|
||||||
|
const std::ptrdiff_t sg = sgn;
|
||||||
|
for (int yi = 0; yi < L.ry; ++yi) {
|
||||||
|
const int y = sgn > 0 ? yi : L.ry - 1 - yi;
|
||||||
|
for (int zi = 0; zi < L.rz; ++zi) {
|
||||||
|
const int zz = sgn > 0 ? zi : L.rz - 1 - zi;
|
||||||
|
const bool inner = y >= 1 && y <= L.ry - 2 && zz >= 1 && zz <= L.rz - 2;
|
||||||
|
float* row = dist.data() + (static_cast<std::size_t>(y) * rz + static_cast<std::size_t>(zz)) * rx;
|
||||||
|
for (int xi = 0; xi < L.rx; ++xi) {
|
||||||
|
const int x = sgn > 0 ? xi : L.rx - 1 - xi;
|
||||||
|
if (inner && x >= 1 && x <= L.rx - 2) {
|
||||||
|
float* c = row + x;
|
||||||
|
float best = *c;
|
||||||
|
for (const Off& o : fwd) best = std::min(best, c[sg * o.step] + o.w);
|
||||||
|
*c = best;
|
||||||
|
} else {
|
||||||
|
relaxChecked(x, y, zz, sgn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sweep(+1);
|
||||||
|
sweep(-1);
|
||||||
|
}
|
||||||
|
for (float& d : dist) d = d == 0 ? 1.0f : (fade > 0 ? 1.0f - smoothstep01(d / fade) : 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The layer of `z` (its key is `key`): the cached one, or a new one (kept in the cache while there is room, else in
|
||||||
|
// `overflow`, which holds the layers of this bake only). The pointer stays valid until the bake ends.
|
||||||
|
const Layer* acquire(const ZoneData& z, std::uint64_t key, float cell, std::deque<Layer>& overflow, BakeStats& stats) {
|
||||||
|
if (const auto it = layers.find(key); it != layers.end()) {
|
||||||
|
it->second.used = true;
|
||||||
|
if (z.shape == ZoneShape::Mesh && z.mesh) { // its voxels stay for the day the layer has to be rebuilt
|
||||||
|
int bucket = 0;
|
||||||
|
usedThisBake.insert(volumeKey(z, cell, bucket));
|
||||||
|
}
|
||||||
|
++stats.layersReused;
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
Layer& fresh = overflow.emplace_back();
|
||||||
|
computeLayer(z, cell, fresh);
|
||||||
|
++stats.layersComputed;
|
||||||
|
if (layerCells + fresh.w.size() > kLayerCacheCells) return &fresh;
|
||||||
|
layerCells += fresh.w.size();
|
||||||
|
Layer& stored = layers[key];
|
||||||
|
stored = std::move(fresh);
|
||||||
|
overflow.pop_back();
|
||||||
|
stored.used = true;
|
||||||
|
return &stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dropUnusedLayers() {
|
||||||
|
for (auto it = layers.begin(); it != layers.end();) {
|
||||||
|
if (it->second.used) { it->second.used = false; ++it; }
|
||||||
|
else { layerCells -= it->second.w.size(); it = layers.erase(it); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AmbientBaker::AmbientBaker() : impl_(std::make_unique<Impl>()) {}
|
||||||
|
AmbientBaker::~AmbientBaker() = default;
|
||||||
|
std::size_t AmbientBaker::cachedMeshVolumes() const { return impl_->volumes.size(); }
|
||||||
|
std::size_t AmbientBaker::cachedLayers() const { return impl_->layers.size(); }
|
||||||
|
|
||||||
|
bool AmbientBaker::update(const std::vector<ZoneData>& zonesIn, const Environment& env) {
|
||||||
|
stats_ = {};
|
||||||
|
const std::uint64_t h = hashInputs(zonesIn, env);
|
||||||
|
if (h == hash_) return false;
|
||||||
|
hash_ = h;
|
||||||
|
Impl& s = *impl_;
|
||||||
|
s.usedThisBake.clear();
|
||||||
|
|
||||||
|
// ---- the zones that count, in the order they are blended: by priority, then as given ----
|
||||||
|
struct Run {
|
||||||
|
const ZoneData* z = nullptr;
|
||||||
|
Box3 bounds;
|
||||||
|
int tie = 0; // rank among the zones of the same priority
|
||||||
|
std::uint64_t key = 0; // see zoneKey()
|
||||||
|
const Impl::Layer* layer = nullptr;
|
||||||
|
Vec3 sky{}, ground{}; // linear light times intensity
|
||||||
|
};
|
||||||
|
std::vector<Run> zones;
|
||||||
|
for (const ZoneData& z : zonesIn) {
|
||||||
|
if (!usable(z)) continue;
|
||||||
|
Run r;
|
||||||
|
r.z = &z;
|
||||||
|
r.bounds = worldBounds(z);
|
||||||
|
if (r.bounds.valid()) zones.push_back(std::move(r));
|
||||||
|
}
|
||||||
|
if (zones.empty()) { grid_ = {}; s.reset(); return true; }
|
||||||
|
std::stable_sort(zones.begin(), zones.end(), [](const Run& a, const Run& b) { return a.z->priority < b.z->priority; });
|
||||||
|
for (std::size_t i = 1; i < zones.size(); ++i)
|
||||||
|
zones[i].tie = zones[i - 1].z->priority == zones[i].z->priority ? zones[i - 1].tie + 1 : 0;
|
||||||
|
|
||||||
|
// ---- grid extent: every zone plus its fade-out, one cell of padding, rounded out to blocks of cells ----
|
||||||
|
Box3 all;
|
||||||
|
for (const Run& r : zones) {
|
||||||
|
const float fade = r.z->falloff > 0.0f ? r.z->falloff : 0.0f;
|
||||||
|
all.add(r.bounds.lo - Vec3{fade, fade, fade});
|
||||||
|
all.add(r.bounds.hi + Vec3{fade, fade, fade});
|
||||||
|
}
|
||||||
|
if (!all.valid() || !std::isfinite(all.lo.x + all.lo.y + all.lo.z + all.hi.x + all.hi.y + all.hi.z)) { grid_ = {}; s.reset(); return true; }
|
||||||
|
|
||||||
|
// The grid lies on a lattice fixed in the world (cell i covers [i * cell, (i + 1) * cell)), so a zone covers the
|
||||||
|
// same cells whatever the other zones do to the grid's extent, and its cached layer stays valid.
|
||||||
|
float cell = std::isfinite(env.zoneCellSize) ? std::max(env.zoneCellSize, 0.05f) : 0.5f; // NaN or infinity would never fit any budget
|
||||||
|
const std::size_t maxCells = static_cast<std::size_t>(std::clamp(env.zoneMaxCells, 1000, 4'194'304));
|
||||||
|
const float lo[3] = {all.lo.x, all.lo.y, all.lo.z}, hi[3] = {all.hi.x, all.hi.y, all.hi.z};
|
||||||
|
auto latticeFor = [&](float c, const Blocks& blocks) {
|
||||||
|
Impl::Lattice l;
|
||||||
|
l.cell = c;
|
||||||
|
for (int a = 0; a < 3; ++a) {
|
||||||
|
const double block = a == 1 ? blocks.y : blocks.xz; // out to whole blocks of cells
|
||||||
|
const double first = std::clamp(std::floor(static_cast<double>(lo[a]) / static_cast<double>(c)) - 1.0, -1.0e9, 1.0e9);
|
||||||
|
const double last = std::clamp(std::floor(static_cast<double>(hi[a]) / static_cast<double>(c)) + 1.0, -1.0e9, 1.0e9);
|
||||||
|
const double from = std::floor(first / block) * block;
|
||||||
|
const double to = std::ceil((last + 1.0) / block) * block - 1.0;
|
||||||
|
l.lo[a] = static_cast<int>(from);
|
||||||
|
l.n[a] = static_cast<int>(std::min(to - from + 1.0, 2.0e9));
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
};
|
||||||
|
// The grid is uploaded as a 2D atlas, so besides the cell budget it has to fit a texture every GPU supports.
|
||||||
|
constexpr double kMaxAtlasSide = 4096;
|
||||||
|
auto fits = [&](const Impl::Lattice& l) {
|
||||||
|
const double tx = atlasTilesX(l.n[1]), ty = std::ceil(static_cast<double>(l.n[1]) / tx);
|
||||||
|
return static_cast<double>(l.n[0]) * l.n[1] * l.n[2] <= static_cast<double>(maxCells) && l.n[0] * tx <= kMaxAtlasSide && l.n[2] * ty <= kMaxAtlasSide;
|
||||||
|
};
|
||||||
|
// The rounded-out grid has a little more room around the zones than they need, which a tight budget may not have:
|
||||||
|
// smaller blocks come next, down to the exact grid (what the grid always was), and only then are the cells coarsened.
|
||||||
|
// So the rounding never costs resolution.
|
||||||
|
Impl::Lattice lat;
|
||||||
|
for (bool found = false; !found;) {
|
||||||
|
for (const Blocks& blocks : kBlocks) {
|
||||||
|
lat = latticeFor(cell, blocks);
|
||||||
|
if ((found = fits(lat))) break;
|
||||||
|
}
|
||||||
|
if (!found) cell *= 1.1f;
|
||||||
|
}
|
||||||
|
const float farthest = std::max({std::fabs(lo[0]), std::fabs(lo[1]), std::fabs(lo[2]), std::fabs(hi[0]), std::fabs(hi[1]), std::fabs(hi[2])});
|
||||||
|
if (!std::isfinite(cell) || !(farthest / cell < 1.0e9f)) { grid_ = {}; s.reset(); return true; } // beyond what a float lattice can address
|
||||||
|
|
||||||
|
const int nx = lat.n[0], ny = lat.n[1], nz = lat.n[2];
|
||||||
|
const std::size_t total = static_cast<std::size_t>(nx) * static_cast<std::size_t>(ny) * static_cast<std::size_t>(nz);
|
||||||
|
auto cellIndex = [&](int x, int y, int z) { // grid coordinates (0 .. n - 1)
|
||||||
|
return (static_cast<std::size_t>(y) * static_cast<std::size_t>(nz) + static_cast<std::size_t>(z)) * static_cast<std::size_t>(nx) + static_cast<std::size_t>(x);
|
||||||
|
};
|
||||||
|
Range whole; // the grid, in lattice coordinates
|
||||||
|
for (int a = 0; a < 3; ++a) { whole.lo[a] = lat.lo[a]; whole.hi[a] = lat.lo[a] + lat.n[a] - 1; }
|
||||||
|
|
||||||
|
// ---- every zone's layer (cached: only a zone that changed costs anything here) ----
|
||||||
|
std::deque<Impl::Layer> overflow;
|
||||||
|
for (Run& r : zones) {
|
||||||
|
const ZoneData& z = *r.z;
|
||||||
|
const std::uint64_t layerId = layerKey(z, cell);
|
||||||
|
r.key = zoneKey(z, layerId, r.tie);
|
||||||
|
r.layer = s.acquire(z, layerId, cell, overflow, stats_);
|
||||||
|
r.sky = toLinear(z.sky) * z.intensity;
|
||||||
|
r.ground = toLinear(z.ground) * z.intensity;
|
||||||
|
++stats_.zones;
|
||||||
|
}
|
||||||
|
auto layerRange = [](const Impl::Layer& L) {
|
||||||
|
Range r;
|
||||||
|
r.lo[0] = L.x0; r.lo[1] = L.y0; r.lo[2] = L.z0;
|
||||||
|
r.hi[0] = L.x0 + L.rx - 1; r.hi[1] = L.y0 + L.ry - 1; r.hi[2] = L.z0 + L.rz - 1;
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- what is different from the previous bake ----
|
||||||
|
Hasher baseHash;
|
||||||
|
baseHash.add(env.ambientSky); baseHash.add(env.ambientGround); baseHash.add(env.ambientIntensity);
|
||||||
|
const std::uint64_t baseKey = baseHash.h;
|
||||||
|
const Vec3 baseSky = toLinear(env.ambientSky) * env.ambientIntensity, baseGround = toLinear(env.ambientGround) * env.ambientIntensity;
|
||||||
|
const bool layoutChanged = !s.haveState || !grid_.valid || !(s.lattice == lat); // another grid: the texels move
|
||||||
|
const bool everything = !s.haveState || s.baseKey != baseKey || s.lattice.cell != lat.cell;
|
||||||
|
|
||||||
|
// Dirty boxes: the cells whose colour has to be worked out again. A cell nobody changed has the colour it had (it is
|
||||||
|
// a function of the zones that cover it, and those are the same zones in the same order), so it keeps it.
|
||||||
|
std::vector<Range> dirty;
|
||||||
|
Range common; // the cells of this grid that the previous grid had
|
||||||
|
if (everything) {
|
||||||
|
dirty.push_back(whole);
|
||||||
|
} else {
|
||||||
|
std::unordered_multimap<std::uint64_t, Range> before(s.placed.begin(), s.placed.end());
|
||||||
|
for (const Run& r : zones) {
|
||||||
|
const auto same = before.find(r.key); // a zone with the key of one the previous bake had: unchanged
|
||||||
|
if (same != before.end()) before.erase(same);
|
||||||
|
else dirty.push_back(layerRange(*r.layer)); // new, or changed: where it is now ...
|
||||||
|
}
|
||||||
|
for (const auto& gone : before) dirty.push_back(gone.second); // ... and where it was (or where a zone that is gone was)
|
||||||
|
if (layoutChanged) { // cells the previous grid did not have
|
||||||
|
Range old;
|
||||||
|
for (int a = 0; a < 3; ++a) { old.lo[a] = s.lattice.lo[a]; old.hi[a] = old.lo[a] + s.lattice.n[a] - 1; }
|
||||||
|
common = intersect(whole, old);
|
||||||
|
if (common.empty()) {
|
||||||
|
dirty.push_back(whole);
|
||||||
|
} else {
|
||||||
|
Range rest = whole; // peel the new cells off axis by axis; what is left is `common`
|
||||||
|
for (int a = 0; a < 3; ++a) {
|
||||||
|
if (rest.lo[a] < common.lo[a]) { Range slab = rest; slab.hi[a] = common.lo[a] - 1; dirty.push_back(slab); rest.lo[a] = common.lo[a]; }
|
||||||
|
if (rest.hi[a] > common.hi[a]) { Range slab = rest; slab.lo[a] = common.hi[a] + 1; dirty.push_back(slab); rest.hi[a] = common.hi[a]; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Range& r : dirty) r = intersect(r, whole);
|
||||||
|
dirty.erase(std::remove_if(dirty.begin(), dirty.end(), [](const Range& r) { return r.empty(); }), dirty.end());
|
||||||
|
for (bool merged = true; merged;) { // overlapping boxes become one (a cell would be worked out twice)
|
||||||
|
merged = false;
|
||||||
|
for (std::size_t i = 0; i < dirty.size() && !merged; ++i)
|
||||||
|
for (std::size_t j = i + 1; j < dirty.size() && !merged; ++j)
|
||||||
|
if (!intersect(dirty[i], dirty[j]).empty()) {
|
||||||
|
dirty[i] = hull(dirty[i], dirty[j]);
|
||||||
|
dirty.erase(dirty.begin() + static_cast<std::ptrdiff_t>(j));
|
||||||
|
merged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the colours of the new grid: the cells of the previous one, carried over (the rest start as the plain ambient) ----
|
||||||
|
if (layoutChanged) {
|
||||||
|
std::vector<Vec3> oldSky = std::move(s.lin[0]), oldGround = std::move(s.lin[1]);
|
||||||
|
s.lin[0] = std::move(s.spare[0]); // memory of a grid two bakes ago: no new allocation (and page faults) unless this one is bigger
|
||||||
|
s.lin[1] = std::move(s.spare[1]);
|
||||||
|
s.lin[0].resize(total);
|
||||||
|
s.lin[1].resize(total);
|
||||||
|
const bool carry = !everything && !common.empty();
|
||||||
|
const Impl::Lattice& ol = s.lattice;
|
||||||
|
for (int y = 0; y < ny; ++y)
|
||||||
|
for (int zz = 0; zz < nz; ++zz) {
|
||||||
|
Vec3* skyRow = &s.lin[0][cellIndex(0, y, zz)];
|
||||||
|
Vec3* groundRow = &s.lin[1][cellIndex(0, y, zz)];
|
||||||
|
const int wy = lat.lo[1] + y, wz = lat.lo[2] + zz; // this row, in lattice coordinates
|
||||||
|
int from = 0, to = 0; // the cells of the row that carry over: [from, to)
|
||||||
|
if (carry && wy >= common.lo[1] && wy <= common.hi[1] && wz >= common.lo[2] && wz <= common.hi[2]) {
|
||||||
|
from = common.lo[0] - lat.lo[0];
|
||||||
|
to = common.hi[0] - lat.lo[0] + 1;
|
||||||
|
}
|
||||||
|
std::fill(skyRow, skyRow + from, baseSky);
|
||||||
|
std::fill(groundRow, groundRow + from, baseGround);
|
||||||
|
if (to > from) {
|
||||||
|
const std::size_t src = (static_cast<std::size_t>(wy - ol.lo[1]) * static_cast<std::size_t>(ol.n[2]) + static_cast<std::size_t>(wz - ol.lo[2])) *
|
||||||
|
static_cast<std::size_t>(ol.n[0]) + static_cast<std::size_t>(common.lo[0] - ol.lo[0]);
|
||||||
|
std::memcpy(skyRow + from, &oldSky[src], static_cast<std::size_t>(to - from) * sizeof(Vec3));
|
||||||
|
std::memcpy(groundRow + from, &oldGround[src], static_cast<std::size_t>(to - from) * sizeof(Vec3));
|
||||||
|
}
|
||||||
|
std::fill(skyRow + to, skyRow + nx, baseSky);
|
||||||
|
std::fill(groundRow + to, groundRow + nx, baseGround);
|
||||||
|
}
|
||||||
|
s.spare[0] = std::move(oldSky);
|
||||||
|
s.spare[1] = std::move(oldGround);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- work the dirty cells out again: base ambient, then every zone that reaches them, lowest priority first ----
|
||||||
|
const GammaCodec& codec = gammaCodec();
|
||||||
|
const bool inPlace = !layoutChanged; // texels of the existing grid are rewritten as colours change
|
||||||
|
auto encodeCell = [&](AmbientGrid& g, int x, int y, int zz, float sc) {
|
||||||
|
const std::size_t i = cellIndex(x, y, zz);
|
||||||
|
const std::size_t a = ((static_cast<std::size_t>(y / g.tilesX) * static_cast<std::size_t>(nz) + static_cast<std::size_t>(zz)) * static_cast<std::size_t>(g.atlasWidth()) +
|
||||||
|
static_cast<std::size_t>(y % g.tilesX) * static_cast<std::size_t>(nx) + static_cast<std::size_t>(x)) * 4;
|
||||||
|
const Vec3 cs = s.lin[0][i] / sc, cg = s.lin[1][i] / sc;
|
||||||
|
const std::uint8_t sb[4] = {codec.encode(cs.x), codec.encode(cs.y), codec.encode(cs.z), 255};
|
||||||
|
const std::uint8_t gb[4] = {codec.encode(cg.x), codec.encode(cg.y), codec.encode(cg.z), 255};
|
||||||
|
std::memcpy(&g.sky[i * 4], sb, 4);
|
||||||
|
std::memcpy(&g.ground[i * 4], gb, 4);
|
||||||
|
std::memcpy(&g.atlasSky[a], sb, 4);
|
||||||
|
std::memcpy(&g.atlasGround[a], gb, 4);
|
||||||
|
};
|
||||||
|
if (layoutChanged) s.rowMax.assign(static_cast<std::size_t>(ny) * static_cast<std::size_t>(nz), 1.0f);
|
||||||
|
std::size_t blended = 0, changed = 0;
|
||||||
|
std::vector<CellBox> updated;
|
||||||
|
for (const Range& D : dirty) {
|
||||||
|
const int dw = D.hi[0] - D.lo[0] + 1, dd = D.hi[2] - D.lo[2] + 1;
|
||||||
|
const std::size_t vol = D.volume();
|
||||||
|
s.neu[0].assign(vol, baseSky);
|
||||||
|
s.neu[1].assign(vol, baseGround);
|
||||||
|
for (const Run& r : zones) {
|
||||||
|
const Impl::Layer& L = *r.layer;
|
||||||
|
const Range R = intersect(layerRange(L), D);
|
||||||
|
if (R.empty()) continue;
|
||||||
|
for (int y = R.lo[1]; y <= R.hi[1]; ++y)
|
||||||
|
for (int zz = R.lo[2]; zz <= R.hi[2]; ++zz) {
|
||||||
|
const float* wrow = &L.w[(static_cast<std::size_t>(y - L.y0) * static_cast<std::size_t>(L.rz) + static_cast<std::size_t>(zz - L.z0)) * static_cast<std::size_t>(L.rx)];
|
||||||
|
const std::size_t o = (static_cast<std::size_t>(y - D.lo[1]) * static_cast<std::size_t>(dd) + static_cast<std::size_t>(zz - D.lo[2])) * static_cast<std::size_t>(dw);
|
||||||
|
Vec3* skyRow = &s.neu[0][o];
|
||||||
|
Vec3* groundRow = &s.neu[1][o];
|
||||||
|
for (int x = R.lo[0]; x <= R.hi[0]; ++x) {
|
||||||
|
const float wt = wrow[x - L.x0];
|
||||||
|
if (wt <= 0) continue;
|
||||||
|
Vec3& sk = skyRow[x - D.lo[0]];
|
||||||
|
Vec3& gr = groundRow[x - D.lo[0]];
|
||||||
|
switch (r.z->mode) {
|
||||||
|
case ZoneMode::Replace: sk = lerp(sk, r.sky, wt); gr = lerp(gr, r.ground, wt); break;
|
||||||
|
case ZoneMode::Add: sk += r.sky * wt; gr += r.ground * wt; break;
|
||||||
|
case ZoneMode::Multiply:
|
||||||
|
sk = sk * lerp(Vec3{1, 1, 1}, r.sky, wt);
|
||||||
|
gr = gr * lerp(Vec3{1, 1, 1}, r.ground, wt);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blended += vol;
|
||||||
|
|
||||||
|
// keep what came out, and remember which cells are not what they were
|
||||||
|
CellBox box;
|
||||||
|
bool any = false;
|
||||||
|
for (int y = D.lo[1]; y <= D.hi[1]; ++y)
|
||||||
|
for (int zz = D.lo[2]; zz <= D.hi[2]; ++zz) {
|
||||||
|
const std::size_t li = cellIndex(D.lo[0] - lat.lo[0], y - lat.lo[1], zz - lat.lo[2]);
|
||||||
|
const std::size_t ni = (static_cast<std::size_t>(y - D.lo[1]) * static_cast<std::size_t>(dd) + static_cast<std::size_t>(zz - D.lo[2])) * static_cast<std::size_t>(dw);
|
||||||
|
Vec3* oldSky = &s.lin[0][li];
|
||||||
|
Vec3* oldGround = &s.lin[1][li];
|
||||||
|
const Vec3* newSky = &s.neu[0][ni];
|
||||||
|
const Vec3* newGround = &s.neu[1][ni];
|
||||||
|
if (std::memcmp(oldSky, newSky, static_cast<std::size_t>(dw) * sizeof(Vec3)) == 0 &&
|
||||||
|
std::memcmp(oldGround, newGround, static_cast<std::size_t>(dw) * sizeof(Vec3)) == 0)
|
||||||
|
continue; // a whole row came out as before
|
||||||
|
bool rowChanged = false;
|
||||||
|
for (int x = 0; x < dw; ++x) {
|
||||||
|
if (oldSky[x] == newSky[x] && oldGround[x] == newGround[x]) continue;
|
||||||
|
oldSky[x] = newSky[x];
|
||||||
|
oldGround[x] = newGround[x];
|
||||||
|
rowChanged = true;
|
||||||
|
++changed;
|
||||||
|
const int gx = D.lo[0] - lat.lo[0] + x, gy = y - lat.lo[1], gz = zz - lat.lo[2];
|
||||||
|
if (!any) { box = {gx, gy, gz, gx, gy, gz}; any = true; }
|
||||||
|
else {
|
||||||
|
box.x0 = std::min(box.x0, gx); box.x1 = std::max(box.x1, gx);
|
||||||
|
box.y0 = std::min(box.y0, gy); box.y1 = std::max(box.y1, gy);
|
||||||
|
box.z0 = std::min(box.z0, gz); box.z1 = std::max(box.z1, gz);
|
||||||
|
}
|
||||||
|
if (inPlace) encodeCell(grid_, gx, gy, gz, s.scale); // right unless the colour scale changes: then everything is redone
|
||||||
|
}
|
||||||
|
if (rowChanged && inPlace) {
|
||||||
|
const std::size_t rowStart = cellIndex(0, y - lat.lo[1], zz - lat.lo[2]);
|
||||||
|
s.rowMax[static_cast<std::size_t>(y - lat.lo[1]) * static_cast<std::size_t>(nz) + static_cast<std::size_t>(zz - lat.lo[2])] =
|
||||||
|
largestComponent(&s.lin[0][rowStart], &s.lin[1][rowStart], nx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (any) updated.push_back(box);
|
||||||
|
}
|
||||||
|
if (layoutChanged)
|
||||||
|
for (int y = 0; y < ny; ++y)
|
||||||
|
for (int zz = 0; zz < nz; ++zz) {
|
||||||
|
const std::size_t rowStart = cellIndex(0, y, zz);
|
||||||
|
s.rowMax[static_cast<std::size_t>(y) * static_cast<std::size_t>(nz) + static_cast<std::size_t>(zz)] = largestComponent(&s.lin[0][rowStart], &s.lin[1][rowStart], nx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- store as RGBA8, gamma-encoded (colours above 1 are carried by colorScale) ----
|
||||||
|
float top = 1.0f;
|
||||||
|
for (const float m : s.rowMax) top = std::max(top, m);
|
||||||
|
const float scale = colorScaleFor(top);
|
||||||
|
const bool scaleChanged = !s.haveState || s.scale != scale;
|
||||||
|
const bool full = layoutChanged || scaleChanged; // a texel is a function of its colour and the scale: a new scale or layout rewrites them all
|
||||||
|
auto encodeAll = [&](AmbientGrid& g) {
|
||||||
|
const std::size_t atlasW = static_cast<std::size_t>(g.atlasWidth());
|
||||||
|
Vec3 lastSky{-1, -1, -1}, lastGround{-1, -1, -1}; // a run of equal colours (most of a level is the plain ambient) is encoded once
|
||||||
|
std::uint8_t sb[4] = {0, 0, 0, 255}, gb[4] = {0, 0, 0, 255};
|
||||||
|
bool haveLast = false;
|
||||||
|
for (int y = 0; y < ny; ++y) {
|
||||||
|
const std::size_t tileX = static_cast<std::size_t>(y % g.tilesX) * static_cast<std::size_t>(nx);
|
||||||
|
const std::size_t tileY = static_cast<std::size_t>(y / g.tilesX) * static_cast<std::size_t>(nz);
|
||||||
|
for (int zz = 0; zz < nz; ++zz) {
|
||||||
|
std::size_t i = cellIndex(0, y, zz);
|
||||||
|
std::size_t a = ((tileY + static_cast<std::size_t>(zz)) * atlasW + tileX) * 4;
|
||||||
|
for (int x = 0; x < nx; ++x, ++i, a += 4) {
|
||||||
|
const Vec3 sky = s.lin[0][i], ground = s.lin[1][i];
|
||||||
|
if (!haveLast || !(sky == lastSky && ground == lastGround)) {
|
||||||
|
const Vec3 cs = sky / scale, cg = ground / scale;
|
||||||
|
sb[0] = codec.encode(cs.x); sb[1] = codec.encode(cs.y); sb[2] = codec.encode(cs.z);
|
||||||
|
gb[0] = codec.encode(cg.x); gb[1] = codec.encode(cg.y); gb[2] = codec.encode(cg.z);
|
||||||
|
lastSky = sky;
|
||||||
|
lastGround = ground;
|
||||||
|
haveLast = true;
|
||||||
|
}
|
||||||
|
std::memcpy(&g.sky[i * 4], sb, 4);
|
||||||
|
std::memcpy(&g.ground[i * 4], gb, 4);
|
||||||
|
std::memcpy(&g.atlasSky[a], sb, 4);
|
||||||
|
std::memcpy(&g.atlasGround[a], gb, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (layoutChanged) {
|
||||||
|
AmbientGrid fresh;
|
||||||
|
fresh.sky = std::move(grid_.sky); // the arrays of the grid it replaces: same memory, no new allocation unless it is bigger
|
||||||
|
fresh.ground = std::move(grid_.ground);
|
||||||
|
fresh.atlasSky = std::move(grid_.atlasSky);
|
||||||
|
fresh.atlasGround = std::move(grid_.atlasGround);
|
||||||
|
fresh.valid = true;
|
||||||
|
fresh.cell = cell;
|
||||||
|
fresh.nx = nx;
|
||||||
|
fresh.ny = ny;
|
||||||
|
fresh.nz = nz;
|
||||||
|
fresh.origin = {static_cast<float>(lat.lo[0]) * cell, static_cast<float>(lat.lo[1]) * cell, static_cast<float>(lat.lo[2]) * cell};
|
||||||
|
fresh.colorScale = scale;
|
||||||
|
fresh.sky.assign(total * 4, 255);
|
||||||
|
fresh.ground.assign(total * 4, 255);
|
||||||
|
fresh.tilesX = atlasTilesX(ny); // the 2D atlas for the GPU: one nx*nz tile per Y slice
|
||||||
|
fresh.tilesY = (ny + fresh.tilesX - 1) / fresh.tilesX;
|
||||||
|
fresh.atlasSky.assign(static_cast<std::size_t>(fresh.atlasWidth()) * static_cast<std::size_t>(fresh.atlasHeight()) * 4, 255);
|
||||||
|
fresh.atlasGround.assign(fresh.atlasSky.size(), 255);
|
||||||
|
encodeAll(fresh);
|
||||||
|
grid_ = std::move(fresh);
|
||||||
|
} else if (scaleChanged) {
|
||||||
|
grid_.colorScale = scale;
|
||||||
|
encodeAll(grid_);
|
||||||
|
}
|
||||||
|
grid_.updateAll = full;
|
||||||
|
grid_.updated = full ? std::vector<CellBox>{} : std::move(updated);
|
||||||
|
|
||||||
|
s.lattice = lat;
|
||||||
|
s.scale = scale;
|
||||||
|
s.baseKey = baseKey;
|
||||||
|
s.haveState = true;
|
||||||
|
s.placed.clear();
|
||||||
|
for (const Run& r : zones) s.placed.emplace_back(r.key, layerRange(*r.layer));
|
||||||
|
s.dropUnusedLayers();
|
||||||
|
stats_.cellsBlended = blended;
|
||||||
|
stats_.cellsEncoded = full ? total : changed;
|
||||||
|
stats_.full = full;
|
||||||
|
|
||||||
|
// forget voxelised meshes no zone uses any more
|
||||||
|
for (auto it = s.volumes.begin(); it != s.volumes.end();)
|
||||||
|
it = s.usedThisBake.count(it->first) ? std::next(it) : s.volumes.erase(it);
|
||||||
|
return full || changed > 0; // a zone that moved less than it takes to flip a texel changes nothing to upload
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eng::detail
|
||||||
+21
@@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
#include "engine/detail/input_state.hpp"
|
#include "engine/detail/input_state.hpp"
|
||||||
#include "engine/graphics.hpp"
|
#include "engine/graphics.hpp"
|
||||||
|
#include "engine/texture.hpp"
|
||||||
#include "renderer.hpp"
|
#include "renderer.hpp"
|
||||||
|
|
||||||
#include <raylib.h>
|
#include <raylib.h>
|
||||||
#include <rlgl.h>
|
#include <rlgl.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace eng {
|
namespace eng {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -49,6 +52,24 @@ void pollInput() {
|
|||||||
|
|
||||||
void clearInput() { detail::inputState() = detail::InputState{}; }
|
void clearInput() { detail::inputState() = detail::InputState{}; }
|
||||||
|
|
||||||
|
// Decodes image files for Resources::texture() with raylib (PNG, JPG, BMP, TGA, ...). It needs no window, and is installed when
|
||||||
|
// the program starts - not in run() - because the scene (and its textures) is usually built before run() is called.
|
||||||
|
bool decodeWithRaylib(const std::string& path, ImageData& out) {
|
||||||
|
::Image img = LoadImage(path.c_str());
|
||||||
|
if (!IsImageValid(img)) return false;
|
||||||
|
ImageFormat(&img, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8);
|
||||||
|
const bool ok = img.data != nullptr && img.format == PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 && img.width > 0 && img.height > 0;
|
||||||
|
if (ok) {
|
||||||
|
out.width = img.width;
|
||||||
|
out.height = img.height;
|
||||||
|
const auto* px = static_cast<const std::uint8_t*>(img.data);
|
||||||
|
out.rgba.assign(px, px + static_cast<std::size_t>(img.width) * static_cast<std::size_t>(img.height) * 4);
|
||||||
|
}
|
||||||
|
UnloadImage(img);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
[[maybe_unused]] const bool kImageLoaderInstalled = (setImageLoader(decodeWithRaylib), true);
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int run(World& world, const RunConfig& cfg) {
|
int run(World& world, const RunConfig& cfg) {
|
||||||
|
|||||||
+16
@@ -407,4 +407,20 @@ void World::update(float dt) {
|
|||||||
flushDestroyed();
|
flushDestroyed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void World::drawGizmos() {
|
||||||
|
deferring_ = true;
|
||||||
|
for (std::size_t i = 0; i < ops_.size(); ++i)
|
||||||
|
if (auto fn = ops_[i].drawGizmos) fn(*this);
|
||||||
|
deferring_ = false;
|
||||||
|
flushDestroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void World::drawGui() {
|
||||||
|
deferring_ = true;
|
||||||
|
for (std::size_t i = 0; i < ops_.size(); ++i)
|
||||||
|
if (auto fn = ops_[i].drawGui) fn(*this);
|
||||||
|
deferring_ = false;
|
||||||
|
flushDestroyed();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace eng
|
} // namespace eng
|
||||||
+2
-299
@@ -1,4 +1,5 @@
|
|||||||
#include "engine/detail/extrude_impl.hpp"
|
#include "engine/detail/extrude_impl.hpp"
|
||||||
|
#include "engine/detail/extrude_mesh.hpp"
|
||||||
|
|
||||||
#include <clipper2/clipper.h>
|
#include <clipper2/clipper.h>
|
||||||
|
|
||||||
@@ -11,200 +12,7 @@ namespace eng {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
namespace C2 = Clipper2Lib;
|
namespace C2 = Clipper2Lib;
|
||||||
|
using namespace meshgen;
|
||||||
struct P2 {
|
|
||||||
double x = 0, y = 0;
|
|
||||||
};
|
|
||||||
inline P2 operator-(P2 a, P2 b) { return {a.x - b.x, a.y - b.y}; }
|
|
||||||
inline double cross2(P2 a, P2 b) { return a.x * b.y - a.y * b.x; }
|
|
||||||
inline double dot2(P2 a, P2 b) { return a.x * b.x + a.y * b.y; }
|
|
||||||
inline bool same(P2 a, P2 b) { return std::fabs(a.x - b.x) < 1e-9 && std::fabs(a.y - b.y) < 1e-9; }
|
|
||||||
|
|
||||||
// Shoelace area: > 0 for counter-clockwise rings in a y-up plane (what Clipper2 calls positive).
|
|
||||||
double signedArea(const std::vector<P2>& p) {
|
|
||||||
double a = 0;
|
|
||||||
for (std::size_t i = 0, n = p.size(); i < n; ++i) {
|
|
||||||
const P2& u = p[i];
|
|
||||||
const P2& v = p[(i + 1) % n];
|
|
||||||
a += u.x * v.y - v.x * u.y;
|
|
||||||
}
|
|
||||||
return a * 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
P2 centroidOf(const std::vector<P2>& p) {
|
|
||||||
double a2 = 0, cx = 0, cy = 0;
|
|
||||||
for (std::size_t i = 0, n = p.size(); i < n; ++i) {
|
|
||||||
const P2& u = p[i];
|
|
||||||
const P2& v = p[(i + 1) % n];
|
|
||||||
const double c = u.x * v.y - v.x * u.y;
|
|
||||||
a2 += c;
|
|
||||||
cx += (u.x + v.x) * c;
|
|
||||||
cy += (u.y + v.y) * c;
|
|
||||||
}
|
|
||||||
if (std::fabs(a2) < 1e-18) {
|
|
||||||
P2 m;
|
|
||||||
for (const P2& q : p) { m.x += q.x; m.y += q.y; }
|
|
||||||
return {m.x / static_cast<double>(p.size()), m.y / static_cast<double>(p.size())};
|
|
||||||
}
|
|
||||||
return {cx / (3.0 * a2), cy / (3.0 * a2)};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// Triangulation: ear clipping with hole bridging.
|
|
||||||
// rings[0] is the outer ring (CCW), the rest are holes (CW), all stored in `pts`;
|
|
||||||
// ringStart has rings+1 entries. Returns index triples into `pts`. Collinear vertices are
|
|
||||||
// kept (as zero-area triangles) so per-vertex heights stay honoured on straight edges.
|
|
||||||
// ===========================================================================
|
|
||||||
std::vector<std::array<int, 3>> triangulate(const std::vector<P2>& pts, const std::vector<std::size_t>& ringStart,
|
|
||||||
double eps) {
|
|
||||||
struct Node { P2 p; int idx, prev, next; };
|
|
||||||
std::vector<Node> nd;
|
|
||||||
nd.reserve(pts.size() + 2 * ringStart.size() + 4);
|
|
||||||
|
|
||||||
auto addRing = [&](std::size_t r) {
|
|
||||||
int first = -1, last = -1;
|
|
||||||
for (std::size_t i = ringStart[r]; i < ringStart[r + 1]; ++i) {
|
|
||||||
const int id = static_cast<int>(nd.size());
|
|
||||||
nd.push_back({pts[i], static_cast<int>(i), last, -1});
|
|
||||||
if (last >= 0) nd[static_cast<std::size_t>(last)].next = id; else first = id;
|
|
||||||
last = id;
|
|
||||||
}
|
|
||||||
nd[static_cast<std::size_t>(last)].next = first;
|
|
||||||
nd[static_cast<std::size_t>(first)].prev = last;
|
|
||||||
return first;
|
|
||||||
};
|
|
||||||
auto N = [&](int id) -> Node& { return nd[static_cast<std::size_t>(id)]; };
|
|
||||||
|
|
||||||
const std::size_t rings = ringStart.size() - 1;
|
|
||||||
int outer = addRing(0);
|
|
||||||
|
|
||||||
std::vector<int> holes; // rightmost vertex of each hole
|
|
||||||
for (std::size_t r = 1; r < rings; ++r) {
|
|
||||||
const int first = addRing(r);
|
|
||||||
int best = first;
|
|
||||||
for (int id = N(first).next; id != first; id = N(id).next)
|
|
||||||
if (N(id).p.x > N(best).p.x) best = id;
|
|
||||||
holes.push_back(best);
|
|
||||||
}
|
|
||||||
std::sort(holes.begin(), holes.end(), [&](int a, int b) { return N(a).p.x > N(b).p.x; });
|
|
||||||
|
|
||||||
auto segHit = [&](P2 p1, P2 p2, P2 q1, P2 q2) {
|
|
||||||
const double d1 = cross2(p2 - p1, q1 - p1), d2 = cross2(p2 - p1, q2 - p1);
|
|
||||||
const double d3 = cross2(q2 - q1, p1 - q1), d4 = cross2(q2 - q1, p2 - q1);
|
|
||||||
if (((d1 > eps && d2 < -eps) || (d1 < -eps && d2 > eps)) &&
|
|
||||||
((d3 > eps && d4 < -eps) || (d3 < -eps && d4 > eps)))
|
|
||||||
return true;
|
|
||||||
auto onSeg = [&](P2 a, P2 b, P2 p) { return std::fabs(cross2(b - a, p - a)) <= eps && dot2(p - a, p - b) <= eps; };
|
|
||||||
return onSeg(p1, p2, q1) || onSeg(p1, p2, q2) || onSeg(q1, q2, p1) || onSeg(q1, q2, p2);
|
|
||||||
};
|
|
||||||
auto ringClear = [&](int start, P2 h, P2 v) { // segment h-v crosses no edge of the ring?
|
|
||||||
int e = start;
|
|
||||||
do {
|
|
||||||
const int f = N(e).next;
|
|
||||||
const P2 a = N(e).p, b = N(f).p;
|
|
||||||
if (!(same(a, h) || same(b, h) || same(a, v) || same(b, v)) && segHit(h, v, a, b)) return false;
|
|
||||||
e = f;
|
|
||||||
} while (e != start);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
auto ringContains = [&](int start, P2 pt) { // even-odd point in ring
|
|
||||||
bool in = false;
|
|
||||||
int i = start;
|
|
||||||
do {
|
|
||||||
const P2 a = N(i).p, b = N(N(i).next).p;
|
|
||||||
if ((a.y > pt.y) != (b.y > pt.y) && pt.x < a.x + (pt.y - a.y) * (b.x - a.x) / (b.y - a.y)) in = !in;
|
|
||||||
i = N(i).next;
|
|
||||||
} while (i != start);
|
|
||||||
return in;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (std::size_t hi = 0; hi < holes.size(); ++hi) {
|
|
||||||
const int H = holes[hi];
|
|
||||||
const P2 hp = N(H).p;
|
|
||||||
std::vector<std::pair<double, int>> cand;
|
|
||||||
int i = outer;
|
|
||||||
do {
|
|
||||||
const P2 d = N(i).p - hp;
|
|
||||||
cand.emplace_back(dot2(d, d), i);
|
|
||||||
i = N(i).next;
|
|
||||||
} while (i != outer);
|
|
||||||
std::sort(cand.begin(), cand.end());
|
|
||||||
|
|
||||||
int chosen = -1;
|
|
||||||
for (const auto& [dist, V] : cand) {
|
|
||||||
const P2 vp = N(V).p;
|
|
||||||
bool ok = ringClear(outer, hp, vp);
|
|
||||||
for (std::size_t hj = hi; ok && hj < holes.size(); ++hj) ok = ringClear(holes[hj], hp, vp);
|
|
||||||
if (ok) ok = ringContains(outer, {(hp.x + vp.x) * 0.5, (hp.y + vp.y) * 0.5});
|
|
||||||
if (ok) { chosen = V; break; }
|
|
||||||
}
|
|
||||||
if (chosen < 0) chosen = cand.front().second; // degenerate input: connect to the nearest anyway
|
|
||||||
|
|
||||||
const int V = chosen;
|
|
||||||
const int Vn = N(V).next, Hp = N(H).prev;
|
|
||||||
const int V2 = static_cast<int>(nd.size());
|
|
||||||
nd.push_back({N(V).p, N(V).idx, -1, -1});
|
|
||||||
const int H2 = static_cast<int>(nd.size());
|
|
||||||
nd.push_back({N(H).p, N(H).idx, -1, -1});
|
|
||||||
N(V).next = H; N(H).prev = V;
|
|
||||||
N(Hp).next = H2; N(H2).prev = Hp;
|
|
||||||
N(H2).next = V2; N(V2).prev = H2;
|
|
||||||
N(V2).next = Vn; N(Vn).prev = V2;
|
|
||||||
outer = V;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto isEar = [&](int a, int b, int c) {
|
|
||||||
const P2 pa = N(a).p, pb = N(b).p, pc = N(c).p;
|
|
||||||
const double cr = cross2(pb - pa, pc - pb);
|
|
||||||
if (cr < -eps) return false; // reflex
|
|
||||||
if (cr <= eps) return false; // straight angle: keep it as a real vertex of a later ear
|
|
||||||
for (int q = N(c).next; q != a; q = N(q).next) {
|
|
||||||
const P2 pq = N(q).p;
|
|
||||||
if (same(pq, pa) || same(pq, pb) || same(pq, pc)) continue;
|
|
||||||
if (cross2(pb - pa, pq - pa) >= -eps && cross2(pc - pb, pq - pb) >= -eps &&
|
|
||||||
cross2(pa - pc, pq - pc) >= -eps)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
int remaining = 0;
|
|
||||||
for (int i = outer, first = outer; ; ) { ++remaining; i = N(i).next; if (i == first) break; }
|
|
||||||
|
|
||||||
std::vector<std::array<int, 3>> tris;
|
|
||||||
tris.reserve(static_cast<std::size_t>(remaining));
|
|
||||||
auto removeNode = [&](int id) { N(N(id).prev).next = N(id).next; N(N(id).next).prev = N(id).prev; };
|
|
||||||
|
|
||||||
int cur = outer, stall = 0;
|
|
||||||
while (remaining > 3) {
|
|
||||||
const int p = N(cur).prev, n = N(cur).next;
|
|
||||||
if (isEar(p, cur, n)) {
|
|
||||||
tris.push_back({N(p).idx, N(cur).idx, N(n).idx});
|
|
||||||
removeNode(cur);
|
|
||||||
--remaining;
|
|
||||||
cur = n;
|
|
||||||
stall = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cur = n;
|
|
||||||
if (++stall > remaining) { // no ear anywhere: clip the most convex vertex
|
|
||||||
int best = cur, it = cur;
|
|
||||||
double bestCross = -1e300;
|
|
||||||
for (int k = 0; k < remaining; ++k, it = N(it).next) {
|
|
||||||
const double c = cross2(N(it).p - N(N(it).prev).p, N(N(it).next).p - N(it).p);
|
|
||||||
if (c > bestCross) { bestCross = c; best = it; }
|
|
||||||
}
|
|
||||||
const int bp = N(best).prev, bn = N(best).next;
|
|
||||||
tris.push_back({N(bp).idx, N(best).idx, N(bn).idx});
|
|
||||||
removeNode(best);
|
|
||||||
--remaining;
|
|
||||||
cur = bn;
|
|
||||||
stall = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (remaining == 3) tris.push_back({N(N(cur).prev).idx, N(cur).idx, N(N(cur).next).idx});
|
|
||||||
return tris;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Regions (Clipper2): the normalised outline of one SVG shape.
|
// Regions (Clipper2): the normalised outline of one SVG shape.
|
||||||
@@ -349,111 +157,6 @@ bool patchAppliesTo(const SvgLabel& patch, const Item& target) {
|
|||||||
if (std::any_of(patch.except.begin(), patch.except.end(), hit)) return false;
|
if (std::any_of(patch.except.begin(), patch.except.end(), hit)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// Mesh generation for one island
|
|
||||||
// ===========================================================================
|
|
||||||
struct Level { float f, r; };
|
|
||||||
|
|
||||||
std::vector<Level> makeLevels(const std::vector<std::pair<float, float>>& e) {
|
|
||||||
std::vector<Level> levels;
|
|
||||||
for (const auto& [f, r] : e) levels.push_back({std::clamp(f, 0.0f, 1.0f), std::max(r, 0.0f)});
|
|
||||||
std::stable_sort(levels.begin(), levels.end(), [](const Level& a, const Level& b) { return a.f < b.f; });
|
|
||||||
if (levels.empty() || levels.front().f > 0) levels.insert(levels.begin(), Level{0.0f, 1.0f});
|
|
||||||
if (levels.back().f < 1) levels.push_back(Level{1.0f, 1.0f});
|
|
||||||
return levels;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct IslandData {
|
|
||||||
std::vector<P2> pts;
|
|
||||||
std::vector<float> base, top;
|
|
||||||
std::vector<Rgba> color;
|
|
||||||
std::vector<std::size_t> ringStart;
|
|
||||||
P2 pivot;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Emitter {
|
|
||||||
MeshData& out;
|
|
||||||
const ExtrudeOptions& opt;
|
|
||||||
P2 origin;
|
|
||||||
|
|
||||||
void tri(Vec3 a, Vec3 b, Vec3 c, Vec3 na, Vec3 nb, Vec3 nc, Rgba ca, Rgba cb, Rgba cc) const {
|
|
||||||
if (cross(b - a, c - a).lengthSq() < 1e-16f) return; // degenerate
|
|
||||||
out.addTriangle(a, b, c, na, nb, nc, ca, cb, cc);
|
|
||||||
}
|
|
||||||
void flatTri(Vec3 a, Vec3 b, Vec3 c, Rgba ca, Rgba cb, Rgba cc) const {
|
|
||||||
const Vec3 n = cross(b - a, c - a).normalized();
|
|
||||||
if (n.lengthSq() < 0.5f) return;
|
|
||||||
out.addTriangle(a, b, c, n, n, n, ca, cb, cc);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void emitIsland(MeshData& out, const IslandData& d, const std::vector<Level>& levels, const ExtrudeOptions& opt, P2 origin) {
|
|
||||||
const Emitter em{out, opt, origin};
|
|
||||||
const float cosCrease = std::cos(opt.smoothAngleDeg * kDegToRad);
|
|
||||||
const double s = opt.scale;
|
|
||||||
|
|
||||||
auto position = [&](std::size_t i, const Level& lv) {
|
|
||||||
const double x = d.pivot.x + lv.r * (d.pts[i].x - d.pivot.x);
|
|
||||||
const double y = d.pivot.y + lv.r * (d.pts[i].y - d.pivot.y);
|
|
||||||
return Vec3{static_cast<float>((x - origin.x) * s), d.base[i] + lv.f * (d.top[i] - d.base[i]),
|
|
||||||
static_cast<float>((y - origin.y) * s)};
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- walls: one strip per contour edge between consecutive levels ----
|
|
||||||
for (std::size_t k = 0; k + 1 < levels.size(); ++k) {
|
|
||||||
for (std::size_t r = 0; r + 1 < d.ringStart.size(); ++r) {
|
|
||||||
const std::size_t b0 = d.ringStart[r], n = d.ringStart[r + 1] - b0;
|
|
||||||
if (n < 3) continue;
|
|
||||||
std::vector<Vec3> A(n), B(n), nq(n);
|
|
||||||
for (std::size_t i = 0; i < n; ++i) {
|
|
||||||
A[i] = position(b0 + i, levels[k]);
|
|
||||||
B[i] = position(b0 + i, levels[k + 1]);
|
|
||||||
}
|
|
||||||
for (std::size_t i = 0; i < n; ++i) {
|
|
||||||
const std::size_t j = (i + 1) % n;
|
|
||||||
nq[i] = cross(B[j] - A[i], A[j] - B[i]).normalized();
|
|
||||||
}
|
|
||||||
auto smooth = [&](Vec3 own, Vec3 other) {
|
|
||||||
if (own.lengthSq() < 0.5f) return other;
|
|
||||||
if (other.lengthSq() > 0.5f && dot(own, other) > cosCrease) return (own + other).normalized();
|
|
||||||
return own;
|
|
||||||
};
|
|
||||||
for (std::size_t i = 0; i < n; ++i) {
|
|
||||||
const std::size_t j = (i + 1) % n, prev = (i + n - 1) % n, next = (j + 1) % n;
|
|
||||||
if (nq[i].lengthSq() < 0.5f) continue;
|
|
||||||
const Vec3 ni = smooth(nq[i], nq[prev]), nj = smooth(nq[i], nq[next]);
|
|
||||||
const Rgba ci = d.color[b0 + i], cj = d.color[b0 + j];
|
|
||||||
em.tri(A[i], B[j], A[j], ni, nj, nj, ci, cj, cj);
|
|
||||||
em.tri(A[i], B[i], B[j], ni, ni, nj, ci, ci, cj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- caps ----
|
|
||||||
const double eps = std::max(1e-12, 0.1 * std::pow(10.0, -2.0 * opt.precision));
|
|
||||||
const auto tris = triangulate(d.pts, d.ringStart, eps);
|
|
||||||
const Level& bottom = levels.front();
|
|
||||||
const Level& topLv = levels.back();
|
|
||||||
for (const auto& t : tris) {
|
|
||||||
const std::size_t a = static_cast<std::size_t>(t[0]), b = static_cast<std::size_t>(t[1]), c = static_cast<std::size_t>(t[2]);
|
|
||||||
const double area2 = cross2(d.pts[b] - d.pts[a], d.pts[c] - d.pts[a]);
|
|
||||||
const bool ccw = area2 > 0; // in svg space (y up); maps to -Y facing
|
|
||||||
// top (+Y): reverse the ccw order
|
|
||||||
{
|
|
||||||
const Vec3 pa = position(a, topLv), pb = position(b, topLv), pc = position(c, topLv);
|
|
||||||
if (ccw) em.flatTri(pa, pc, pb, d.color[a], d.color[c], d.color[b]);
|
|
||||||
else em.flatTri(pa, pb, pc, d.color[a], d.color[b], d.color[c]);
|
|
||||||
}
|
|
||||||
// bottom (-Y)
|
|
||||||
{
|
|
||||||
const Vec3 pa = position(a, bottom), pb = position(b, bottom), pc = position(c, bottom);
|
|
||||||
if (ccw) em.flatTri(pa, pb, pc, d.color[a], d.color[b], d.color[c]);
|
|
||||||
else em.flatTri(pa, pc, pb, d.color[a], d.color[c], d.color[b]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
+261
@@ -0,0 +1,261 @@
|
|||||||
|
#include "engine/detail/frame.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace eng::detail {
|
||||||
|
|
||||||
|
int shadowMapResolution(int requested) {
|
||||||
|
int r = 256;
|
||||||
|
while (r < requested && r < 4096) r *= 2;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialUniforms materialUniforms(const eng::Material* m, bool meshHasTexcoords) {
|
||||||
|
MaterialUniforms u;
|
||||||
|
if (!m) return u;
|
||||||
|
auto num = [](float v, float lo, float hi, float fallback) { return std::isfinite(v) ? std::clamp(v, lo, hi) : fallback; };
|
||||||
|
u.mode = 1;
|
||||||
|
u.maps = static_cast<int>(m->maps());
|
||||||
|
u.color[0] = num(m->albedo.x, 0.0f, 16.0f, 1.0f);
|
||||||
|
u.color[1] = num(m->albedo.y, 0.0f, 16.0f, 1.0f);
|
||||||
|
u.color[2] = num(m->albedo.z, 0.0f, 16.0f, 1.0f);
|
||||||
|
u.surface[0] = num(m->diffuse, 0.0f, 4.0f, 1.0f);
|
||||||
|
u.surface[1] = num(m->specular, 0.0f, 4.0f, 0.5f);
|
||||||
|
u.surface[2] = num(m->metalness, 0.0f, 1.0f, 0.0f);
|
||||||
|
u.surface[3] = num(m->roughness, 0.0f, 1.0f, 0.85f);
|
||||||
|
u.extra[0] = num(m->normalStrength, 0.0f, 8.0f, 1.0f);
|
||||||
|
u.extra[1] = num(m->emission, 0.0f, 16.0f, 0.0f);
|
||||||
|
const bool fromWorld = m->uv == UvMode::World || (m->uv == UvMode::Auto && !meshHasTexcoords);
|
||||||
|
u.extra[2] = fromWorld ? 1.0f : 0.0f;
|
||||||
|
u.specColor[0] = num(m->specularColor.x, 0.0f, 4.0f, 1.0f);
|
||||||
|
u.specColor[1] = num(m->specularColor.y, 0.0f, 4.0f, 1.0f);
|
||||||
|
u.specColor[2] = num(m->specularColor.z, 0.0f, 4.0f, 1.0f);
|
||||||
|
u.uv[0] = num(m->uvScale.x, -1.0e4f, 1.0e4f, 1.0f);
|
||||||
|
u.uv[1] = num(m->uvScale.y, -1.0e4f, 1.0e4f, 1.0f);
|
||||||
|
u.uv[2] = num(m->uvOffset.x, -1.0e4f, 1.0e4f, 0.0f);
|
||||||
|
u.uv[3] = num(m->uvOffset.y, -1.0e4f, 1.0e4f, 0.0f);
|
||||||
|
u.textures[0] = m->albedoMap.get();
|
||||||
|
u.textures[1] = m->diffuseMap.get();
|
||||||
|
u.textures[2] = m->specularMap.get();
|
||||||
|
u.textures[3] = m->metalnessMap.get();
|
||||||
|
u.textures[4] = m->roughnessMap.get();
|
||||||
|
u.textures[5] = m->normalMap.get();
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct BucketKey {
|
||||||
|
std::uint64_t mesh = 0, material = 0; // material 0: none
|
||||||
|
bool mirrored = false;
|
||||||
|
bool operator==(const BucketKey&) const = default;
|
||||||
|
};
|
||||||
|
struct BucketKeyHash {
|
||||||
|
std::size_t operator()(const BucketKey& k) const {
|
||||||
|
std::uint64_t h = k.mesh * 0x9E3779B97F4A7C15ull;
|
||||||
|
h ^= (k.material + 0x7F4A7C15F39CC060ull) * 0xC2B2AE3D27D4EB4Full + (h << 6) + (h >> 2);
|
||||||
|
h ^= k.mirrored ? 0x165667B19E3779F9ull : 0;
|
||||||
|
return static_cast<std::size_t>(h);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct Bucket { // every instance of one mesh, material and handedness in one pass
|
||||||
|
const eng::Mesh* mesh = nullptr;
|
||||||
|
const eng::Material* material = nullptr;
|
||||||
|
bool mirrored = false;
|
||||||
|
std::vector<GpuInstance> items;
|
||||||
|
std::uint64_t lastFrame = 0;
|
||||||
|
};
|
||||||
|
using Pool = std::unordered_map<BucketKey, Bucket, BucketKeyHash>;
|
||||||
|
|
||||||
|
bool isMirrored(const Mat4& w) {
|
||||||
|
const Vec3 c0{w.m[0], w.m[1], w.m[2]}, c1{w.m[4], w.m[5], w.m[6]}, c2{w.m[8], w.m[9], w.m[10]};
|
||||||
|
return dot(c0, cross(c1, c2)) < 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
GpuInstance pack(const MeshInstance& in) {
|
||||||
|
GpuInstance g;
|
||||||
|
std::memcpy(g.m, in.world.m, sizeof g.m);
|
||||||
|
g.rgba[0] = in.tint.r; g.rgba[1] = in.tint.g; g.rgba[2] = in.tint.b; g.rgba[3] = in.tint.a;
|
||||||
|
g.material[0] = in.roughness; g.material[1] = in.metallic; g.material[2] = in.emission; g.material[3] = in.flags;
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearPool(Pool& pool) {
|
||||||
|
for (auto& [key, b] : pool) b.items.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// `byMaterial`: the camera's pass splits batches by material (a draw call per mesh and material); the shadow passes only
|
||||||
|
// draw depth, so they ignore materials and keep one batch per mesh.
|
||||||
|
void route(Pool& pool, const MeshInstance& in, std::uint64_t frame, bool byMaterial) {
|
||||||
|
const bool mirrored = isMirrored(in.world);
|
||||||
|
const eng::Material* material = byMaterial ? in.material : nullptr;
|
||||||
|
Bucket& b = pool[BucketKey{in.mesh->id(), material ? material->id() : 0, mirrored}];
|
||||||
|
b.mesh = in.mesh;
|
||||||
|
b.material = material;
|
||||||
|
b.mirrored = mirrored;
|
||||||
|
b.lastFrame = frame;
|
||||||
|
b.items.push_back(pack(in));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appends the pool's batches to `all` in a stable order (mesh id, then material id, then handedness), so draws are reproducible.
|
||||||
|
void emit(Pool& pool, std::vector<GpuInstance>& all, std::vector<DrawBatch>& out, bool withMaterials) {
|
||||||
|
std::vector<Bucket*> used;
|
||||||
|
for (auto& [key, b] : pool)
|
||||||
|
if (!b.items.empty()) used.push_back(&b);
|
||||||
|
auto materialId = [](const Bucket* b) { return b->material ? b->material->id() : std::uint64_t{0}; };
|
||||||
|
std::sort(used.begin(), used.end(), [&](const Bucket* a, const Bucket* b) {
|
||||||
|
if (a->mesh->id() != b->mesh->id()) return a->mesh->id() < b->mesh->id();
|
||||||
|
if (materialId(a) != materialId(b)) return materialId(a) < materialId(b);
|
||||||
|
return a->mirrored < b->mirrored;
|
||||||
|
});
|
||||||
|
for (const Bucket* b : used) {
|
||||||
|
DrawBatch batch;
|
||||||
|
batch.mesh = b->mesh;
|
||||||
|
batch.mirrored = b->mirrored;
|
||||||
|
batch.first = static_cast<std::uint32_t>(all.size());
|
||||||
|
batch.count = static_cast<std::uint32_t>(b->items.size());
|
||||||
|
if (withMaterials) batch.mat = materialUniforms(b->material, b->mesh->data().hasTexcoords());
|
||||||
|
out.push_back(batch);
|
||||||
|
all.insert(all.end(), b->items.begin(), b->items.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setVec4(float (&dst)[4], float x, float y, float z, float w) {
|
||||||
|
dst[0] = x; dst[1] = y; dst[2] = z; dst[3] = w;
|
||||||
|
}
|
||||||
|
void setVec3(float (&dst)[3], Vec3 v) {
|
||||||
|
dst[0] = v.x; dst[1] = v.y; dst[2] = v.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct FramePlanner::Impl {
|
||||||
|
Pool pools[1 + kMaxShadowMaps]; // 0: the camera, 1..: the shadow maps
|
||||||
|
AmbientBaker baker;
|
||||||
|
FramePlan plan;
|
||||||
|
std::uint64_t frame = 0;
|
||||||
|
|
||||||
|
void prune() {
|
||||||
|
for (Pool& pool : pools)
|
||||||
|
for (auto it = pool.begin(); it != pool.end();) {
|
||||||
|
if (frame - it->second.lastFrame > 240) it = pool.erase(it); else ++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FramePlanner::FramePlanner() : impl_(std::make_unique<Impl>()) {}
|
||||||
|
FramePlanner::~FramePlanner() = default;
|
||||||
|
|
||||||
|
const FramePlan& FramePlanner::plan(World& world, float aspect) {
|
||||||
|
Impl& s = *impl_;
|
||||||
|
++s.frame;
|
||||||
|
if (s.frame % 120 == 0) s.prune();
|
||||||
|
|
||||||
|
FramePlan& p = s.plan;
|
||||||
|
std::vector<GpuInstance> keep = std::move(p.instances); // reuse the big allocation
|
||||||
|
p = FramePlan{};
|
||||||
|
keep.clear();
|
||||||
|
p.instances = std::move(keep);
|
||||||
|
p.aspect = aspect > 1e-3f ? aspect : 1.0f;
|
||||||
|
|
||||||
|
SceneSnapshot snap = gatherScene(world);
|
||||||
|
p.texts = std::move(snap.texts);
|
||||||
|
p.hasCamera = snap.hasCamera;
|
||||||
|
p.camera = snap.camera;
|
||||||
|
p.meshesSeen = static_cast<int>(snap.meshes.size());
|
||||||
|
const Environment& env = snap.env;
|
||||||
|
|
||||||
|
// The ambient grid only depends on the zones: bake it (or keep the cached bake) even without a camera.
|
||||||
|
p.ambientChanged = s.baker.update(snap.zones, env);
|
||||||
|
p.ambient = &s.baker.grid();
|
||||||
|
if (!snap.hasCamera) return p;
|
||||||
|
|
||||||
|
const CameraData& cam = snap.camera;
|
||||||
|
p.view = cam.view();
|
||||||
|
p.projection = cam.projection(p.aspect);
|
||||||
|
p.viewProj = p.projection * p.view;
|
||||||
|
const Frustum frustum = Frustum::fromViewProj(p.viewProj);
|
||||||
|
|
||||||
|
// ---- the camera's pass ----
|
||||||
|
Pool& mainPool = s.pools[0];
|
||||||
|
clearPool(mainPool);
|
||||||
|
for (const MeshInstance& m : snap.meshes) {
|
||||||
|
if (!frustum.sphereVisible(m.center, m.radius)) { ++p.culled; continue; }
|
||||||
|
route(mainPool, m, s.frame, true);
|
||||||
|
}
|
||||||
|
emit(mainPool, p.instances, p.main, true);
|
||||||
|
|
||||||
|
// ---- lights ----
|
||||||
|
LitUniforms& u = p.lit;
|
||||||
|
const std::vector<LightData> lights = selectLights(snap.lights, cam.position);
|
||||||
|
const std::vector<int> slots = assignShadowSlots(lights, env);
|
||||||
|
p.lightsShaded = static_cast<int>(lights.size());
|
||||||
|
u.lightCount = static_cast<int>(lights.size());
|
||||||
|
setVec3(u.camPos, cam.position);
|
||||||
|
for (std::size_t i = 0; i < lights.size(); ++i) {
|
||||||
|
const LightData& l = lights[i];
|
||||||
|
const Vec3 lin = toLinear(l.color) * l.intensity;
|
||||||
|
if (l.type == LightType::Directional)
|
||||||
|
setVec4(u.lightPosType[i], -l.direction.x, -l.direction.y, -l.direction.z, 0.0f);
|
||||||
|
else
|
||||||
|
setVec4(u.lightPosType[i], l.position.x, l.position.y, l.position.z, l.type == LightType::Point ? 1.0f : 2.0f);
|
||||||
|
setVec4(u.lightColorRange[i], lin.x, lin.y, lin.z, l.range);
|
||||||
|
setVec4(u.lightSpot[i], l.direction.x, l.direction.y, l.direction.z, l.cosOuter);
|
||||||
|
setVec4(u.lightParams[i], 1.0f / std::max(l.cosInner - l.cosOuter, 1e-4f), static_cast<float>(slots[i]), l.shadowBias, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- shadow maps ----
|
||||||
|
std::vector<MeshInstance> castersStorage;
|
||||||
|
const std::vector<MeshInstance>* casters = &snap.meshes;
|
||||||
|
if (!std::all_of(snap.meshes.begin(), snap.meshes.end(), [](const MeshInstance& m) { return m.castShadows; })) {
|
||||||
|
for (const MeshInstance& m : snap.meshes)
|
||||||
|
if (m.castShadows) castersStorage.push_back(m);
|
||||||
|
casters = &castersStorage;
|
||||||
|
}
|
||||||
|
const int sunRes = shadowMapResolution(env.shadowResolution);
|
||||||
|
for (std::size_t i = 0; i < lights.size(); ++i) {
|
||||||
|
const int slot = slots[i];
|
||||||
|
if (slot < 0) continue;
|
||||||
|
const LightData& l = lights[i];
|
||||||
|
const bool sun = l.type == LightType::Directional;
|
||||||
|
const int res = sun ? sunRes : std::max(256, sunRes / 2); // spots get half the resolution
|
||||||
|
const ShadowView view = sun ? fitSunShadow(l.direction, cam, p.aspect, env.shadowDistance, res, *casters)
|
||||||
|
: fitSpotShadow(l, res);
|
||||||
|
|
||||||
|
ShadowPass pass;
|
||||||
|
pass.slot = slot;
|
||||||
|
pass.resolution = res;
|
||||||
|
pass.viewProj = view.viewProj;
|
||||||
|
pass.radial = view.perspective;
|
||||||
|
pass.lightPos = l.position;
|
||||||
|
pass.range = std::max(l.range, 0.01f);
|
||||||
|
Pool& pool = s.pools[1 + slot];
|
||||||
|
clearPool(pool);
|
||||||
|
for (const MeshInstance& c : *casters)
|
||||||
|
if (view.frustum.sphereVisible(c.center, c.radius)) route(pool, c, s.frame, false);
|
||||||
|
emit(pool, p.instances, pass.batches, false);
|
||||||
|
p.shadows.push_back(std::move(pass));
|
||||||
|
|
||||||
|
u.shadowMatrix[slot] = view.viewProj;
|
||||||
|
setVec4(u.shadowInfo[slot], view.texelWorld, view.perspective ? 1.0f : 0.0f, view.depthRange, 1.0f / static_cast<float>(res));
|
||||||
|
}
|
||||||
|
setVec4(u.shadowParams, std::clamp(env.shadowStrength, 0.0f, 1.0f), std::max(env.shadowSoftness, 0.0f), std::max(env.shadowDistance, 1.0f), 0.0f);
|
||||||
|
|
||||||
|
// ---- ambient, tone mapping, fog ----
|
||||||
|
setVec3(u.ambSkyGlobal, toLinear(env.ambientSky) * env.ambientIntensity);
|
||||||
|
setVec3(u.ambGroundGlobal, toLinear(env.ambientGround) * env.ambientIntensity);
|
||||||
|
const AmbientGrid& g = s.baker.grid();
|
||||||
|
if (g.valid) {
|
||||||
|
setVec4(u.ambVolOrigin, g.origin.x, g.origin.y, g.origin.z, g.cell);
|
||||||
|
setVec4(u.ambVolDims, static_cast<float>(g.nx), static_cast<float>(g.ny), static_cast<float>(g.nz), static_cast<float>(g.tilesX));
|
||||||
|
u.ambColorScale = g.colorScale;
|
||||||
|
}
|
||||||
|
setVec4(u.envParams, std::max(env.exposure, 0.0f), static_cast<float>(static_cast<int>(env.toneMapping)), std::max(env.fogDensity, 0.0f), 0.0f);
|
||||||
|
setVec3(u.fogColor, toLinear(env.fogColor));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eng::detail
|
||||||
+1
-3
@@ -1,6 +1,6 @@
|
|||||||
#include "engine/detail/input_state.hpp"
|
#include "engine/detail/input_state.hpp"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <raylib.h>
|
|
||||||
|
|
||||||
namespace eng {
|
namespace eng {
|
||||||
namespace detail {
|
namespace detail {
|
||||||
@@ -32,8 +32,6 @@ bool Input::mouseReleased(MouseButton b) { return inRange(b) && detail::inputSta
|
|||||||
Vec2 Input::mousePosition() { return detail::inputState().mousePosition; }
|
Vec2 Input::mousePosition() { return detail::inputState().mousePosition; }
|
||||||
Vec2 Input::mouseDelta() { return detail::inputState().mouseDelta; }
|
Vec2 Input::mouseDelta() { return detail::inputState().mouseDelta; }
|
||||||
float Input::mouseWheel() { return detail::inputState().mouseWheel; }
|
float Input::mouseWheel() { return detail::inputState().mouseWheel; }
|
||||||
void Input::enableCursor() { EnableCursor(); }
|
|
||||||
void Input::disableCursor() { DisableCursor(); }
|
|
||||||
Vec2 Input::windowSize() { return detail::inputState().windowSize; }
|
Vec2 Input::windowSize() { return detail::inputState().windowSize; }
|
||||||
|
|
||||||
void quit() { detail::g_quit.store(true); }
|
void quit() { detail::g_quit.store(true); }
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#include "engine/material.hpp"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
namespace eng {
|
||||||
|
|
||||||
|
Material::Material() {
|
||||||
|
static std::atomic<std::uint64_t> counter{1};
|
||||||
|
id_ = counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialPtr Material::create() { return std::make_shared<Material>(); }
|
||||||
|
|
||||||
|
MaterialPtr Material::clone() const {
|
||||||
|
MaterialPtr m = std::make_shared<Material>();
|
||||||
|
m->albedo = albedo;
|
||||||
|
m->albedoMap = albedoMap;
|
||||||
|
m->diffuse = diffuse;
|
||||||
|
m->diffuseMap = diffuseMap;
|
||||||
|
m->specular = specular;
|
||||||
|
m->specularColor = specularColor;
|
||||||
|
m->specularMap = specularMap;
|
||||||
|
m->metalness = metalness;
|
||||||
|
m->metalnessMap = metalnessMap;
|
||||||
|
m->roughness = roughness;
|
||||||
|
m->roughnessMap = roughnessMap;
|
||||||
|
m->normalMap = normalMap;
|
||||||
|
m->normalStrength = normalStrength;
|
||||||
|
m->emission = emission;
|
||||||
|
m->uv = uv;
|
||||||
|
m->uvScale = uvScale;
|
||||||
|
m->uvOffset = uvOffset;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eng
|
||||||
+185
-80
@@ -17,6 +17,7 @@
|
|||||||
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
|
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
|
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/ScaledShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
||||||
#include <Jolt/Physics/Constraints/DistanceConstraint.h>
|
#include <Jolt/Physics/Constraints/DistanceConstraint.h>
|
||||||
#include <Jolt/Physics/Constraints/FixedConstraint.h>
|
#include <Jolt/Physics/Constraints/FixedConstraint.h>
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
#include <Jolt/RegisterTypes.h>
|
#include <Jolt/RegisterTypes.h>
|
||||||
|
|
||||||
#include "access.hpp"
|
#include "access.hpp"
|
||||||
|
#include "engine/detail/collider_math.hpp"
|
||||||
#include "engine/physics.hpp"
|
#include "engine/physics.hpp"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -157,18 +159,26 @@ struct BodyLink {
|
|||||||
bool added = false;
|
bool added = false;
|
||||||
Vec3 lastPos{};
|
Vec3 lastPos{};
|
||||||
Quat lastRot{};
|
Quat lastRot{};
|
||||||
|
Vec3 bodyScale{1, 1, 1}; // world scale the body's pose was taken with (decomposed: a mirror shows as x < 0)
|
||||||
|
Vec3 scale{1, 1, 1}; // detail::shapeScale() the shape was built for: a different one reshapes the body
|
||||||
|
std::uint64_t sig = 0; // detail::colliderSignature() of the collider it was built from
|
||||||
};
|
};
|
||||||
struct CharLink {
|
struct CharLink {
|
||||||
JPH::Ref<JPH::CharacterVirtual> ch;
|
JPH::Ref<JPH::CharacterVirtual> ch;
|
||||||
JPH::BodyID inner; // kinematic body inside the character: lets sensors/dynamics see it
|
JPH::BodyID inner; // kinematic body inside the character: lets sensors/dynamics see it
|
||||||
Vec3 lastPos{};
|
Vec3 lastPos{};
|
||||||
|
Vec3 scale{1, 1, 1}; // |world scale| the capsule was built for
|
||||||
};
|
};
|
||||||
struct JointLink {
|
struct JointLink {
|
||||||
JPH::Ref<JPH::TwoBodyConstraint> c;
|
JPH::Ref<JPH::TwoBodyConstraint> c;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Has an entity's pose left the one its body was last given? The turn is measured by the vector part of q * lq^-1, which is
|
||||||
|
// sin(angle / 2) and as exact as a float can be; "1 - |dot|" is not (rounding alone makes it exceed 1e-8 for identical
|
||||||
|
// rotations, which put dynamic bodies back in place, and woke them, on frames where nothing had moved).
|
||||||
bool moved(Vec3 p, Quat q, Vec3 lp, Quat lq) {
|
bool moved(Vec3 p, Quat q, Vec3 lp, Quat lq) {
|
||||||
return (p - lp).lengthSq() > 1e-8f || (1.0f - std::fabs(Quat::dot(q, lq))) > 1e-8f;
|
const Quat d = q * lq.conjugate();
|
||||||
|
return (p - lp).lengthSq() > 1e-8f || d.x * d.x + d.y * d.y + d.z * d.z > 1e-8f; // 0.1 mm, 0.011 degrees
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -220,97 +230,161 @@ struct Physics::Impl {
|
|||||||
|
|
||||||
// ---- shapes -----------------------------------------------------------
|
// ---- shapes -----------------------------------------------------------
|
||||||
// Shapes are immutable and reference counted, so bodies that ask for the same shape share one.
|
// Shapes are immutable and reference counted, so bodies that ask for the same shape share one.
|
||||||
|
// Boxes, spheres, capsules and cylinders are sized straight from the world scale (detail::colliderDims).
|
||||||
|
// Meshes and hulls are built once at unit scale and scaled with a cheap wrapper, so animating an
|
||||||
|
// entity's scale never rebuilds them.
|
||||||
std::unordered_map<std::string, JPH::ShapeRefC> shapeCache;
|
std::unordered_map<std::string, JPH::ShapeRefC> shapeCache;
|
||||||
std::size_t shapePruneAt = 256;
|
std::size_t shapePruneAt = 256;
|
||||||
|
|
||||||
JPH::ShapeRefC makeShape(const Collider& c, Vec3 worldScale, bool dynamic) {
|
template <class T> static void put(std::string& key, const T& v) { key.append(reinterpret_cast<const char*>(&v), sizeof v); }
|
||||||
const ShapeType type = (c.shape == ShapeType::Mesh && dynamic) ? ShapeType::ConvexHull : c.shape;
|
|
||||||
std::string key;
|
|
||||||
const std::uint8_t t = static_cast<std::uint8_t>(type);
|
|
||||||
key.append(reinterpret_cast<const char*>(&t), 1);
|
|
||||||
const float f[11] = {c.size.x, c.size.y, c.size.z, c.radius, c.height, c.offset.x, c.offset.y, c.offset.z,
|
|
||||||
worldScale.x, worldScale.y, worldScale.z};
|
|
||||||
key.append(reinterpret_cast<const char*>(f), sizeof f);
|
|
||||||
const std::uint64_t meshId = c.mesh ? c.mesh->id() : 0;
|
|
||||||
key.append(reinterpret_cast<const char*>(&meshId), sizeof meshId);
|
|
||||||
if (const auto it = shapeCache.find(key); it != shapeCache.end()) return it->second;
|
|
||||||
|
|
||||||
JPH::ShapeRefC shape = buildShape(c, worldScale, dynamic);
|
void remember(std::string key, const JPH::ShapeRefC& shape) {
|
||||||
if (!shape) return nullptr;
|
|
||||||
if (shapeCache.size() >= shapePruneAt) { // forget shapes no body uses any more
|
if (shapeCache.size() >= shapePruneAt) { // forget shapes no body uses any more
|
||||||
for (auto it = shapeCache.begin(); it != shapeCache.end();)
|
for (auto it = shapeCache.begin(); it != shapeCache.end();)
|
||||||
it = it->second->GetRefCount() == 1 ? shapeCache.erase(it) : std::next(it);
|
it = it->second->GetRefCount() == 1 ? shapeCache.erase(it) : std::next(it);
|
||||||
shapePruneAt = std::max<std::size_t>(256, shapeCache.size() * 2);
|
shapePruneAt = std::max<std::size_t>(256, shapeCache.size() * 2);
|
||||||
}
|
}
|
||||||
shapeCache.emplace(std::move(key), shape);
|
shapeCache.emplace(std::move(key), shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
JPH::ShapeRefC makeShape(const Collider& c, Vec3 worldScale, bool dynamic) {
|
||||||
|
const detail::ColliderDims d = detail::colliderDims(c, worldScale, dynamic);
|
||||||
|
std::string key;
|
||||||
|
put(key, static_cast<std::uint8_t>(d.type));
|
||||||
|
const float f[12] = {d.halfExtents.x, d.halfExtents.y, d.halfExtents.z, d.radius, d.halfHeight, d.meshScale.x,
|
||||||
|
d.meshScale.y, d.meshScale.z, d.offset.x, d.offset.y, d.offset.z, 0.0f};
|
||||||
|
put(key, f);
|
||||||
|
put(key, static_cast<std::uint64_t>(c.mesh ? c.mesh->id() : 0));
|
||||||
|
if (const auto it = shapeCache.find(key); it != shapeCache.end()) return it->second;
|
||||||
|
|
||||||
|
JPH::ShapeRefC shape = buildShape(c, d);
|
||||||
|
if (!shape) return nullptr;
|
||||||
|
remember(std::move(key), shape);
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
JPH::ShapeRefC buildShape(const Collider& c, Vec3 worldScale, bool dynamic) {
|
// A mesh or convex hull at unit scale, shared by every scaled variant of it.
|
||||||
const Vec3 as = abs3(worldScale);
|
JPH::ShapeRefC unitShape(const Collider& c, ShapeType type) {
|
||||||
const float hMax = std::max({as.x, as.y, as.z});
|
if (!c.mesh || c.mesh->data().empty()) {
|
||||||
const float hXZ = std::max(as.x, as.z);
|
std::fprintf(stderr, "eng::Physics: %s collider without a mesh\n", type == ShapeType::Mesh ? "Mesh" : "ConvexHull");
|
||||||
ShapeType type = c.shape;
|
return nullptr;
|
||||||
if (type == ShapeType::Mesh && dynamic) type = ShapeType::ConvexHull; // Jolt meshes cannot be dynamic
|
}
|
||||||
|
std::string key;
|
||||||
|
put(key, static_cast<std::uint8_t>(static_cast<unsigned>(type) | 0x80u)); // distinct from the scaled variants
|
||||||
|
put(key, static_cast<std::uint64_t>(c.mesh->id()));
|
||||||
|
if (const auto it = shapeCache.find(key); it != shapeCache.end()) return it->second;
|
||||||
|
|
||||||
|
const MeshData& md = c.mesh->data();
|
||||||
JPH::ShapeSettings::ShapeResult res;
|
JPH::ShapeSettings::ShapeResult res;
|
||||||
switch (type) {
|
if (type == ShapeType::ConvexHull) {
|
||||||
case ShapeType::Box: {
|
JPH::Array<JPH::Vec3> pts;
|
||||||
const Vec3 he = {std::max(0.005f, c.size.x * as.x * 0.5f), std::max(0.005f, c.size.y * as.y * 0.5f),
|
pts.reserve(md.positions.size());
|
||||||
std::max(0.005f, c.size.z * as.z * 0.5f)};
|
for (const Vec3& p : md.positions) pts.push_back(J(p));
|
||||||
res = JPH::BoxShapeSettings(J(he), std::min(JPH::cDefaultConvexRadius, std::min({he.x, he.y, he.z}))).Create();
|
res = JPH::ConvexHullShapeSettings(pts).Create();
|
||||||
break;
|
} else {
|
||||||
}
|
JPH::TriangleList tris;
|
||||||
case ShapeType::Sphere:
|
tris.reserve(md.triangleCount());
|
||||||
res = JPH::SphereShapeSettings(std::max(0.005f, c.radius * hMax)).Create();
|
for (std::size_t i = 0; i + 2 < md.positions.size(); i += 3)
|
||||||
break;
|
tris.push_back(JPH::Triangle(J(md.positions[i]), J(md.positions[i + 1]), J(md.positions[i + 2])));
|
||||||
case ShapeType::Capsule: {
|
res = JPH::MeshShapeSettings(tris).Create();
|
||||||
const float r = std::max(0.005f, c.radius * hXZ);
|
|
||||||
const float half = std::max(0.005f, 0.5f * c.height * as.y - r);
|
|
||||||
res = JPH::CapsuleShapeSettings(half, r).Create();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ShapeType::Cylinder: {
|
|
||||||
const float r = std::max(0.005f, c.radius * hXZ);
|
|
||||||
const float half = std::max(0.005f, 0.5f * c.height * as.y);
|
|
||||||
res = JPH::CylinderShapeSettings(half, r, std::min(JPH::cDefaultConvexRadius, std::min(half, r))).Create();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ShapeType::ConvexHull: {
|
|
||||||
if (!c.mesh || c.mesh->data().empty()) { std::fprintf(stderr, "eng::Physics: ConvexHull collider without a mesh\n"); return nullptr; }
|
|
||||||
JPH::Array<JPH::Vec3> pts;
|
|
||||||
pts.reserve(c.mesh->data().positions.size());
|
|
||||||
for (const Vec3& p : c.mesh->data().positions) pts.push_back(J(p * worldScale));
|
|
||||||
res = JPH::ConvexHullShapeSettings(pts).Create();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ShapeType::Mesh: {
|
|
||||||
if (!c.mesh || c.mesh->data().empty()) { std::fprintf(stderr, "eng::Physics: Mesh collider without a mesh\n"); return nullptr; }
|
|
||||||
JPH::TriangleList tris;
|
|
||||||
const MeshData& md = c.mesh->data();
|
|
||||||
tris.reserve(md.triangleCount());
|
|
||||||
for (std::size_t i = 0; i + 2 < md.positions.size(); i += 3) {
|
|
||||||
const Vec3 a = md.positions[i] * worldScale, b = md.positions[i + 1] * worldScale,
|
|
||||||
d = md.positions[i + 2] * worldScale;
|
|
||||||
tris.push_back(JPH::Triangle(J(a), J(b), J(d)));
|
|
||||||
}
|
|
||||||
res = JPH::MeshShapeSettings(tris).Create();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (res.HasError()) {
|
if (res.HasError()) {
|
||||||
std::fprintf(stderr, "eng::Physics: cannot create shape: %s\n", res.GetError().c_str());
|
std::fprintf(stderr, "eng::Physics: cannot create shape: %s\n", res.GetError().c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
JPH::ShapeRefC shape = res.Get();
|
JPH::ShapeRefC shape = res.Get();
|
||||||
if (c.offset.lengthSq() > 0) {
|
remember(std::move(key), shape);
|
||||||
JPH::ShapeSettings::ShapeResult r2 =
|
return shape;
|
||||||
JPH::RotatedTranslatedShapeSettings(J(c.offset * worldScale), JPH::Quat::sIdentity(), shape).Create();
|
}
|
||||||
|
|
||||||
|
// A convex hull scaled unevenly. Jolt rounds every hull with a convex radius that a scale wrapper can only
|
||||||
|
// shrink in step with the smallest axis, which would leave a stretched hull smaller than its mesh (by up to
|
||||||
|
// radius * (largest - smallest scale)). So the points are scaled first and the hull is built from them; the
|
||||||
|
// shape cache keeps one per scale. (Uniform scales and triangle meshes only wrap the shared unit shape.)
|
||||||
|
JPH::ShapeRefC stretchedHull(const Collider& c, Vec3 scale) {
|
||||||
|
if (!c.mesh || c.mesh->data().empty()) {
|
||||||
|
std::fprintf(stderr, "eng::Physics: ConvexHull collider without a mesh\n");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const MeshData& md = c.mesh->data();
|
||||||
|
JPH::Array<JPH::Vec3> pts;
|
||||||
|
pts.reserve(md.positions.size());
|
||||||
|
for (const Vec3& p : md.positions) pts.push_back(J(p * scale));
|
||||||
|
const JPH::ShapeSettings::ShapeResult res = JPH::ConvexHullShapeSettings(pts).Create();
|
||||||
|
if (res.HasError()) {
|
||||||
|
std::fprintf(stderr, "eng::Physics: cannot create shape: %s\n", res.GetError().c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return res.Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
JPH::ShapeRefC buildShape(const Collider& c, const detail::ColliderDims& d) {
|
||||||
|
JPH::ShapeSettings::ShapeResult res;
|
||||||
|
JPH::ShapeRefC shape;
|
||||||
|
switch (d.type) {
|
||||||
|
case ShapeType::Box:
|
||||||
|
res = JPH::BoxShapeSettings(J(d.halfExtents),
|
||||||
|
std::min(JPH::cDefaultConvexRadius, std::min({d.halfExtents.x, d.halfExtents.y, d.halfExtents.z})))
|
||||||
|
.Create();
|
||||||
|
break;
|
||||||
|
case ShapeType::Sphere:
|
||||||
|
res = JPH::SphereShapeSettings(d.radius).Create();
|
||||||
|
break;
|
||||||
|
case ShapeType::Capsule:
|
||||||
|
res = JPH::CapsuleShapeSettings(d.halfHeight, d.radius).Create();
|
||||||
|
break;
|
||||||
|
case ShapeType::Cylinder:
|
||||||
|
res = JPH::CylinderShapeSettings(d.halfHeight, d.radius, std::min(JPH::cDefaultConvexRadius, std::min(d.halfHeight, d.radius)))
|
||||||
|
.Create();
|
||||||
|
break;
|
||||||
|
case ShapeType::ConvexHull:
|
||||||
|
case ShapeType::Mesh: {
|
||||||
|
if (d.type == ShapeType::ConvexHull && !detail::uniformScale(d.meshScale)) {
|
||||||
|
shape = stretchedHull(c, d.meshScale);
|
||||||
|
if (!shape) return nullptr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
JPH::ShapeRefC unit = unitShape(c, d.type);
|
||||||
|
if (!unit) return nullptr;
|
||||||
|
if (d.meshScale.x == 1.0f && d.meshScale.y == 1.0f && d.meshScale.z == 1.0f) shape = unit;
|
||||||
|
else res = JPH::ScaledShapeSettings(unit, J(d.meshScale)).Create();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!shape) {
|
||||||
|
if (res.HasError()) {
|
||||||
|
std::fprintf(stderr, "eng::Physics: cannot create shape: %s\n", res.GetError().c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
shape = res.Get();
|
||||||
|
}
|
||||||
|
if (d.offset.lengthSq() > 0) {
|
||||||
|
JPH::ShapeSettings::ShapeResult r2 = JPH::RotatedTranslatedShapeSettings(J(d.offset), JPH::Quat::sIdentity(), shape).Create();
|
||||||
if (!r2.HasError()) shape = r2.Get();
|
if (!r2.HasError()) shape = r2.Get();
|
||||||
}
|
}
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A collider follows its entity: a new world scale, or edited size/radius/height/offset/mesh, gives the
|
||||||
|
// body a new shape. Static and kinematic bodies swap it in place (overlaps stay continuous, so a
|
||||||
|
// growing trigger volume just starts or stops touching things). A dynamic body is rebuilt with its
|
||||||
|
// velocity carried over, so its mass and inertia come out right for the new shape.
|
||||||
|
void reshape(entt::entity e, BodyLink*& link, const Collider& col, Vec3 worldScale) {
|
||||||
|
if (link->type == BodyType::Dynamic) {
|
||||||
|
const Vec3 v = fromJ(bi().GetLinearVelocity(link->id)), w = fromJ(bi().GetAngularVelocity(link->id));
|
||||||
|
destroyBody(e);
|
||||||
|
link = createBody(e);
|
||||||
|
if (!link) return;
|
||||||
|
bi().SetLinearVelocity(link->id, J(v));
|
||||||
|
bi().SetAngularVelocity(link->id, J(w));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link->scale = detail::shapeScale(col, worldScale); // also when the build fails: no retry (and no log spam) every frame
|
||||||
|
link->sig = detail::colliderSignature(col);
|
||||||
|
if (JPH::ShapeRefC shape = makeShape(col, worldScale, false))
|
||||||
|
bi().SetShape(link->id, shape, false, JPH::EActivation::DontActivate);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- bodies -----------------------------------------------------------
|
// ---- bodies -----------------------------------------------------------
|
||||||
void worldPose(entt::entity e, Vec3& p, Quat& q, Vec3& s) { reg.get<Transform>(e).worldMatrix().decompose(p, q, s); }
|
void worldPose(entt::entity e, Vec3& p, Quat& q, Vec3& s) { reg.get<Transform>(e).worldMatrix().decompose(p, q, s); }
|
||||||
|
|
||||||
@@ -356,6 +430,8 @@ struct Physics::Impl {
|
|||||||
link.type = type;
|
link.type = type;
|
||||||
link.lastPos = p;
|
link.lastPos = p;
|
||||||
link.lastRot = q;
|
link.lastRot = q;
|
||||||
|
link.scale = detail::shapeScale(col, s);
|
||||||
|
link.sig = detail::colliderSignature(col);
|
||||||
bodyToEntity[link.id.GetIndexAndSequenceNumber()] = e;
|
bodyToEntity[link.id.GetIndexAndSequenceNumber()] = e;
|
||||||
if (wantActive(e)) {
|
if (wantActive(e)) {
|
||||||
bi().AddBody(link.id, type == BodyType::Static ? JPH::EActivation::DontActivate : JPH::EActivation::Activate);
|
bi().AddBody(link.id, type == BodyType::Static ? JPH::EActivation::DontActivate : JPH::EActivation::Activate);
|
||||||
@@ -444,19 +520,34 @@ struct Physics::Impl {
|
|||||||
Vec3 p, s;
|
Vec3 p, s;
|
||||||
Quat q;
|
Quat q;
|
||||||
worldPose(e, p, q, s);
|
worldPose(e, p, q, s);
|
||||||
|
if (detail::scaleChanged(link->scale, detail::shapeScale(col, s)) || detail::colliderSignature(col) != link->sig) {
|
||||||
|
reshape(e, link, col, s);
|
||||||
|
if (!link) continue;
|
||||||
|
}
|
||||||
|
// lastPos / lastRot are the pose the body was last given: a drift too small to matter in one frame builds up
|
||||||
|
// until it does, instead of being forgotten every frame (a platform creeping at 5 mm/s must still arrive).
|
||||||
switch (link->type) {
|
switch (link->type) {
|
||||||
case BodyType::Kinematic: bi().MoveKinematic(link->id, JR(p), J(q), dt); break;
|
case BodyType::Kinematic:
|
||||||
|
bi().MoveKinematic(link->id, JR(p), J(q), dt);
|
||||||
|
link->lastPos = p;
|
||||||
|
link->lastRot = q;
|
||||||
|
break;
|
||||||
case BodyType::Static:
|
case BodyType::Static:
|
||||||
if (moved(p, q, link->lastPos, link->lastRot))
|
if (moved(p, q, link->lastPos, link->lastRot)) {
|
||||||
bi().SetPositionAndRotation(link->id, JR(p), J(q), JPH::EActivation::DontActivate);
|
bi().SetPositionAndRotation(link->id, JR(p), J(q), JPH::EActivation::DontActivate);
|
||||||
|
link->lastPos = p;
|
||||||
|
link->lastRot = q;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case BodyType::Dynamic:
|
case BodyType::Dynamic:
|
||||||
if (moved(p, q, link->lastPos, link->lastRot))
|
if (moved(p, q, link->lastPos, link->lastRot)) {
|
||||||
bi().SetPositionAndRotation(link->id, JR(p), J(q), JPH::EActivation::Activate);
|
bi().SetPositionAndRotation(link->id, JR(p), J(q), JPH::EActivation::Activate);
|
||||||
|
link->lastPos = p;
|
||||||
|
link->lastRot = q;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
link->lastPos = p;
|
link->bodyScale = s;
|
||||||
link->lastRot = q;
|
|
||||||
}
|
}
|
||||||
syncJoints();
|
syncJoints();
|
||||||
}
|
}
|
||||||
@@ -552,14 +643,21 @@ struct Physics::Impl {
|
|||||||
|
|
||||||
CharLink* link = nullptr;
|
CharLink* link = nullptr;
|
||||||
if (const auto it = chars.find(e); it != chars.end()) link = &it->second;
|
if (const auto it = chars.find(e); it != chars.end()) link = &it->second;
|
||||||
if (link && cc.dirty_) { destroyCharacter(e); link = nullptr; }
|
bool carryVelocity = false; // a new size must not stop a fall in mid-air
|
||||||
|
JPH::Vec3 carried = JPH::Vec3::sZero();
|
||||||
|
if (link && (cc.dirty_ || detail::scaleChanged(link->scale, abs3(s)))) {
|
||||||
|
if (!cc.dirty_) { carried = link->ch->GetLinearVelocity(); carryVelocity = true; }
|
||||||
|
destroyCharacter(e);
|
||||||
|
link = nullptr;
|
||||||
|
}
|
||||||
if (!link) {
|
if (!link) {
|
||||||
JPH::CharacterVirtualSettings cs;
|
JPH::CharacterVirtualSettings cs;
|
||||||
cs.mUp = JPH::Vec3::sAxisY();
|
cs.mUp = JPH::Vec3::sAxisY();
|
||||||
cs.mMaxSlopeAngle = cc.maxSlopeDeg * kDegToRad;
|
cs.mMaxSlopeAngle = cc.maxSlopeDeg * kDegToRad;
|
||||||
cs.mMass = cc.mass;
|
cs.mMass = cc.mass;
|
||||||
const float radius = std::max(0.05f, cc.radius);
|
const detail::CapsuleDims cap = detail::characterCapsule(cc.radius, cc.height, s); // follows the entity's scale
|
||||||
const float half = std::max(0.01f, 0.5f * cc.height - radius);
|
const float radius = cap.radius;
|
||||||
|
const float half = cap.halfHeight;
|
||||||
cs.mShape = JPH::RotatedTranslatedShapeSettings(JPH::Vec3(0, half + radius, 0), JPH::Quat::sIdentity(),
|
cs.mShape = JPH::RotatedTranslatedShapeSettings(JPH::Vec3(0, half + radius, 0), JPH::Quat::sIdentity(),
|
||||||
new JPH::CapsuleShape(half, radius))
|
new JPH::CapsuleShape(half, radius))
|
||||||
.Create().Get();
|
.Create().Get();
|
||||||
@@ -568,9 +666,11 @@ struct Physics::Impl {
|
|||||||
cs.mInnerBodyLayer = Layers::MOVING;
|
cs.mInnerBodyLayer = Layers::MOVING;
|
||||||
CharLink fresh;
|
CharLink fresh;
|
||||||
fresh.ch = new JPH::CharacterVirtual(&cs, JR(p), JPH::Quat::sIdentity(), static_cast<JPH::uint64>(entt::to_integral(e)), &system);
|
fresh.ch = new JPH::CharacterVirtual(&cs, JR(p), JPH::Quat::sIdentity(), static_cast<JPH::uint64>(entt::to_integral(e)), &system);
|
||||||
|
if (carryVelocity) fresh.ch->SetLinearVelocity(carried);
|
||||||
fresh.inner = fresh.ch->GetInnerBodyID();
|
fresh.inner = fresh.ch->GetInnerBodyID();
|
||||||
if (!fresh.inner.IsInvalid()) bodyToEntity[fresh.inner.GetIndexAndSequenceNumber()] = e;
|
if (!fresh.inner.IsInvalid()) bodyToEntity[fresh.inner.GetIndexAndSequenceNumber()] = e;
|
||||||
fresh.lastPos = p;
|
fresh.lastPos = p;
|
||||||
|
fresh.scale = abs3(s);
|
||||||
link = &(chars[e] = fresh);
|
link = &(chars[e] = fresh);
|
||||||
cc.dirty_ = false;
|
cc.dirty_ = false;
|
||||||
} else if ((p - link->lastPos).lengthSq() > 1e-8f) {
|
} else if ((p - link->lastPos).lengthSq() > 1e-8f) {
|
||||||
@@ -605,7 +705,8 @@ struct Physics::Impl {
|
|||||||
system.GetDefaultLayerFilter(Layers::MOVING), JPH::BodyFilter(), JPH::ShapeFilter(), temp);
|
system.GetDefaultLayerFilter(Layers::MOVING), JPH::BodyFilter(), JPH::ShapeFilter(), temp);
|
||||||
|
|
||||||
const Vec3 np = fromJ(ch.GetPosition());
|
const Vec3 np = fromJ(ch.GetPosition());
|
||||||
reg.get<Transform>(e).setWorldPose(np, q);
|
Transform& tr = reg.get<Transform>(e);
|
||||||
|
tr.setWorldPose(np, tr.worldRotation()); // controllers only move: the entity keeps its own rotation, mirrored or not
|
||||||
link->lastPos = np;
|
link->lastPos = np;
|
||||||
cc.grounded_ = ch.GetGroundState() == JPH::CharacterBase::EGroundState::OnGround;
|
cc.grounded_ = ch.GetGroundState() == JPH::CharacterBase::EGroundState::OnGround;
|
||||||
cc.velocity_ = fromJ(ch.GetLinearVelocity());
|
cc.velocity_ = fromJ(ch.GetLinearVelocity());
|
||||||
@@ -622,7 +723,11 @@ struct Physics::Impl {
|
|||||||
bi().GetPositionAndRotation(l.id, p, q);
|
bi().GetPositionAndRotation(l.id, p, q);
|
||||||
l.lastPos = fromJ(p);
|
l.lastPos = fromJ(p);
|
||||||
l.lastRot = fromJ(q);
|
l.lastRot = fromJ(q);
|
||||||
reg.get<Transform>(e).setWorldPose(l.lastPos, l.lastRot);
|
// The entity must show what the body stands for, mirrored scales (its own or a parent's) included.
|
||||||
|
Transform& tr = reg.get<Transform>(e);
|
||||||
|
const Mat4 parentInverse = tr.parentWorldMatrix().inverseAffine();
|
||||||
|
tr.position = parentInverse.transformPoint(l.lastPos);
|
||||||
|
tr.rotation = detail::localRotationForBody(parentInverse, l.lastRot, l.bodyScale, tr.scale, tr.rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,10 +842,10 @@ Collider Collider::triangleMesh(MeshPtr mesh) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace detail {
|
namespace detail {
|
||||||
void Hooks<Collider>::awake(Collider& c) { c.world().physics(); c.dirty_ = true; }
|
void Hooks<Collider>::awake(Collider& c) { static_cast<void>(c.world().physics()); c.dirty_ = true; }
|
||||||
void Hooks<RigidBody>::awake(RigidBody& c) { c.world().physics(); c.dirty_ = true; }
|
void Hooks<RigidBody>::awake(RigidBody& c) { static_cast<void>(c.world().physics()); c.dirty_ = true; }
|
||||||
void Hooks<CharacterController>::awake(CharacterController& c) { c.world().physics(); c.dirty_ = true; }
|
void Hooks<CharacterController>::awake(CharacterController& c) { static_cast<void>(c.world().physics()); c.dirty_ = true; }
|
||||||
void Hooks<Joint>::awake(Joint& c) { c.world().physics(); c.dirty_ = true; }
|
void Hooks<Joint>::awake(Joint& c) { static_cast<void>(c.world().physics()); c.dirty_ = true; }
|
||||||
|
|
||||||
void Hooks<Collider>::destroyed(Collider& c) {
|
void Hooks<Collider>::destroyed(Collider& c) {
|
||||||
if (Access::hasPhysics(c.world())) c.world().physics().releaseBody(c.entity());
|
if (Access::hasPhysics(c.world())) c.world().physics().releaseBody(c.entity());
|
||||||
|
|||||||
+458
-233
@@ -1,104 +1,58 @@
|
|||||||
|
// The GPU half of rendering: uploads what FramePlanner (frame.cpp) worked out and issues the draw calls.
|
||||||
|
// Nothing here decides anything about the scene - that is all in the (headless, tested) frame plan.
|
||||||
|
//
|
||||||
|
// A frame is: refresh the ambient-zone textures if they were re-baked, upload every instance of every pass
|
||||||
|
// in ONE buffer update, render each shadow map into its own render texture, then draw the camera's pass with
|
||||||
|
// the lit shader (PBR-style, up to 16 lights, shadow maps sampled with PCF, ambient zones sampled from a
|
||||||
|
// 3D grid stored in a 2D atlas), let components draw gizmos, and finish with the 2D overlay.
|
||||||
#include "renderer.hpp"
|
#include "renderer.hpp"
|
||||||
|
|
||||||
#include "engine/graphics.hpp"
|
#include "engine/detail/frame.hpp"
|
||||||
|
#include "shaders.hpp"
|
||||||
|
|
||||||
#include <raylib.h>
|
#include <raylib.h>
|
||||||
#include <rlgl.h>
|
#include <rlgl.h>
|
||||||
|
|
||||||
#if defined(__GNUC__)
|
|
||||||
#pragma GCC diagnostic push
|
|
||||||
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
|
|
||||||
#endif
|
|
||||||
#include <raymath.h> // MatrixMultiply (raylib's own header trips -Wextra)
|
|
||||||
#if defined(__GNUC__)
|
|
||||||
#pragma GCC diagnostic pop
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <type_traits>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
// raylib declares global types named Camera, Transform, Color, ... Inside namespace eng the
|
// raylib declares global types named Camera, Transform, Color, ... Inside namespace eng the
|
||||||
// unqualified names are the engine's own; raylib's are spelled with a leading "::" or by their
|
// unqualified names are the engine's own; raylib's are spelled with a leading "::" (::Mesh, ::Camera3D, ...).
|
||||||
// unique names (Camera3D, Mesh is eng::Mesh here, raylib's is ::Mesh).
|
|
||||||
|
|
||||||
namespace eng::detail {
|
namespace eng::detail {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Attribute locations: 0..5 are raylib's standard mesh layout (UploadMesh), instance data follows.
|
|
||||||
constexpr unsigned kLocInstanceTransform = 9; // mat4 -> 9..12
|
|
||||||
constexpr unsigned kLocInstanceColor = 13;
|
|
||||||
|
|
||||||
const char* kVertexShader = R"(#version 330
|
|
||||||
layout(location = 0) in vec3 vertexPosition;
|
|
||||||
layout(location = 2) in vec3 vertexNormal;
|
|
||||||
layout(location = 3) in vec4 vertexColor;
|
|
||||||
layout(location = 9) in mat4 instanceTransform;
|
|
||||||
layout(location = 13) in vec4 instanceColor;
|
|
||||||
uniform mat4 viewProj;
|
|
||||||
out vec3 fragNormal;
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main() {
|
|
||||||
vec3 c0 = instanceTransform[0].xyz, c1 = instanceTransform[1].xyz, c2 = instanceTransform[2].xyz;
|
|
||||||
vec3 k0 = cross(c1, c2);
|
|
||||||
mat3 normalMat = mat3(k0, cross(c2, c0), cross(c0, c1)); // cofactor matrix: det * inverse-transpose
|
|
||||||
if (dot(c0, k0) < 0.0) normalMat = -normalMat;
|
|
||||||
fragNormal = normalize(normalMat*vertexNormal);
|
|
||||||
fragColor = vertexColor*instanceColor;
|
|
||||||
gl_Position = viewProj*instanceTransform*vec4(vertexPosition, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* kFragmentShader = R"(#version 330
|
|
||||||
in vec3 fragNormal;
|
|
||||||
in vec4 fragColor;
|
|
||||||
uniform vec3 lightDir; // direction the light travels
|
|
||||||
uniform vec3 lightColor;
|
|
||||||
uniform vec3 ambient;
|
|
||||||
out vec4 finalColor;
|
|
||||||
void main() {
|
|
||||||
float ndl = max(dot(normalize(fragNormal), -lightDir), 0.0);
|
|
||||||
finalColor = vec4(fragColor.rgb*(ambient + lightColor*ndl), fragColor.a);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
struct Instance { // per-object GPU data: 68 bytes
|
|
||||||
float m[16];
|
|
||||||
std::uint8_t rgba[4];
|
|
||||||
};
|
|
||||||
static_assert(sizeof(Instance) == 68, "instance layout is hard-coded in the attribute setup");
|
|
||||||
|
|
||||||
void matrixToArray(const ::Matrix& m, float* a) {
|
|
||||||
a[0] = m.m0; a[1] = m.m1; a[2] = m.m2; a[3] = m.m3;
|
|
||||||
a[4] = m.m4; a[5] = m.m5; a[6] = m.m6; a[7] = m.m7;
|
|
||||||
a[8] = m.m8; a[9] = m.m9; a[10] = m.m10; a[11] = m.m11;
|
|
||||||
a[12] = m.m12; a[13] = m.m13; a[14] = m.m14; a[15] = m.m15;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gribb/Hartmann frustum planes of a column-major view-projection matrix (clip = M * p).
|
|
||||||
void extractFrustum(const float* a, float planes[6][4]) {
|
|
||||||
auto row = [&](int r, int c) { return a[c * 4 + r]; };
|
|
||||||
const int sign[6] = {+1, -1, +1, -1, +1, -1};
|
|
||||||
const int axis[6] = {0, 0, 1, 1, 2, 2};
|
|
||||||
for (int i = 0; i < 6; ++i) {
|
|
||||||
float* p = planes[i];
|
|
||||||
for (int c = 0; c < 4; ++c) p[c] = row(3, c) + static_cast<float>(sign[i]) * row(axis[i], c);
|
|
||||||
const float len = std::sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
|
|
||||||
if (len > 1e-12f) for (int c = 0; c < 4; ++c) p[c] /= len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool sphereVisible(const float planes[6][4], Vec3 c, float r) {
|
|
||||||
for (int i = 0; i < 6; ++i)
|
|
||||||
if (planes[i][0] * c.x + planes[i][1] * c.y + planes[i][2] * c.z + planes[i][3] < -r) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
::Vector3 toRaylib(Vec3 v) { return ::Vector3{v.x, v.y, v.z}; }
|
::Vector3 toRaylib(Vec3 v) { return ::Vector3{v.x, v.y, v.z}; }
|
||||||
|
|
||||||
|
// raylib's Matrix lists its fields row by row (m0 m4 m8 m12 / m1 ...); element m[i] of a column-major array is mi.
|
||||||
|
::Matrix toRaylib(const Mat4& a) {
|
||||||
|
::Matrix m;
|
||||||
|
m.m0 = a.m[0]; m.m1 = a.m[1]; m.m2 = a.m[2]; m.m3 = a.m[3];
|
||||||
|
m.m4 = a.m[4]; m.m5 = a.m[5]; m.m6 = a.m[6]; m.m7 = a.m[7];
|
||||||
|
m.m8 = a.m[8]; m.m9 = a.m[9]; m.m10 = a.m[10]; m.m11 = a.m[11];
|
||||||
|
m.m12 = a.m[12]; m.m13 = a.m[13]; m.m14 = a.m[14]; m.m15 = a.m[15];
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rlSetVertexAttribute() takes the byte offset as an int in raylib 6 and as a `const void*` before; accept either.
|
||||||
|
template <class Fn>
|
||||||
|
void setAttribute(Fn&& fn, unsigned index, int components, int type, bool normalized, int stride, int offset) {
|
||||||
|
if constexpr (std::is_invocable_v<Fn, unsigned, int, int, bool, int, int>)
|
||||||
|
fn(index, components, type, normalized, stride, offset);
|
||||||
|
else
|
||||||
|
fn(index, components, type, normalized, stride, reinterpret_cast<const void*>(static_cast<std::intptr_t>(offset)));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int kTextureSlotShadow = 0; // shadowMap0..2 use slots 0..2
|
||||||
|
constexpr int kTextureSlotAmbientSky = kMaxShadowMaps;
|
||||||
|
constexpr int kTextureSlotAmbientGround = kMaxShadowMaps + 1;
|
||||||
|
constexpr int kTextureSlotMaterial = kMaxShadowMaps + 2; // the material's maps (Material::MapBit order) follow the ambient grid's
|
||||||
|
constexpr int kLastTextureSlot = kTextureSlotMaterial + Material::kMapCount - 1;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
struct Renderer::Impl {
|
struct Renderer::Impl {
|
||||||
@@ -106,24 +60,58 @@ struct Renderer::Impl {
|
|||||||
::Mesh mesh{};
|
::Mesh mesh{};
|
||||||
std::uint64_t lastUsed = 0;
|
std::uint64_t lastUsed = 0;
|
||||||
};
|
};
|
||||||
struct Bucket { // every instance of one mesh, this frame
|
struct LitLocs {
|
||||||
const eng::Mesh* mesh = nullptr;
|
int viewProj = -1, camPos = -1, lightCount = -1;
|
||||||
std::vector<Instance> items;
|
int lightPosType = -1, lightColorRange = -1, lightSpot = -1, lightParams = -1;
|
||||||
std::uint64_t lastFrame = 0;
|
int shadowMap[kMaxShadowMaps], shadowMatrix[kMaxShadowMaps], shadowInfo[kMaxShadowMaps];
|
||||||
|
int shadowParams = -1;
|
||||||
|
int ambSkyGlobal = -1, ambGroundGlobal = -1, ambSkyTex = -1, ambGroundTex = -1;
|
||||||
|
int ambVolOrigin = -1, ambVolDims = -1, ambColorScale = -1;
|
||||||
|
int envParams = -1, fogColor = -1;
|
||||||
|
int matMode = -1, matMaps = -1, matColor = -1, matSurface = -1, matExtra = -1, matSpecColor = -1, matUv = -1;
|
||||||
|
int matMap[Material::kMapCount];
|
||||||
|
LitLocs() {
|
||||||
|
std::fill(std::begin(matMap), std::end(matMap), -1);
|
||||||
|
std::fill(std::begin(shadowMap), std::end(shadowMap), -1);
|
||||||
|
std::fill(std::begin(shadowMatrix), std::end(shadowMatrix), -1);
|
||||||
|
std::fill(std::begin(shadowInfo), std::end(shadowInfo), -1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct ShadowLocs {
|
||||||
|
int viewProj = -1, shadowCast = -1, lightPos = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
::Shader shader{};
|
::Shader lit{};
|
||||||
bool ready = false;
|
::Shader shadow{};
|
||||||
int locViewProj = -1, locLightDir = -1, locLightColor = -1, locAmbient = -1;
|
bool ready = false; // the lit shader works (nothing can be drawn without it)
|
||||||
|
bool shadowReady = false; // the depth shader works (without it there are simply no shadows)
|
||||||
|
LitLocs L;
|
||||||
|
ShadowLocs S;
|
||||||
|
|
||||||
|
FramePlanner planner;
|
||||||
std::unordered_map<std::uint64_t, Gpu> gpu;
|
std::unordered_map<std::uint64_t, Gpu> gpu;
|
||||||
std::unordered_map<std::uint64_t, Bucket> buckets;
|
|
||||||
std::vector<Instance> packed;
|
|
||||||
unsigned instanceVbo = 0;
|
unsigned instanceVbo = 0;
|
||||||
std::size_t instanceCapacity = 0; // in instances
|
std::size_t instanceCapacity = 0; // in instances
|
||||||
float planes[6][4] = {};
|
|
||||||
|
::RenderTexture2D shadowRt[kMaxShadowMaps] = {};
|
||||||
|
int shadowRes[kMaxShadowMaps] = {};
|
||||||
|
bool shadowActive[kMaxShadowMaps] = {}; // rendered this frame: the lit pass may sample it
|
||||||
|
std::uint64_t shadowLastUsed[kMaxShadowMaps] = {};
|
||||||
|
|
||||||
|
struct GpuTexture {
|
||||||
|
::Texture2D tex{};
|
||||||
|
std::uint64_t lastUsed = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<std::uint64_t, GpuTexture> textures; // by Texture::id()
|
||||||
|
|
||||||
|
::Texture2D ambSky{}, ambGround{};
|
||||||
|
int ambW = 0, ambH = 0;
|
||||||
|
std::vector<std::uint8_t> ambStage; // scratch for the rectangles of a partial ambient upload
|
||||||
|
|
||||||
std::uint64_t frame = 0;
|
std::uint64_t frame = 0;
|
||||||
RenderStats stats;
|
RenderStats stats;
|
||||||
|
|
||||||
|
// ---- meshes --------------------------------------------------------------------------------------------
|
||||||
// Unindexed mesh upload (no 16-bit index limit); raylib keeps the VAO/VBO ids in the ::Mesh.
|
// Unindexed mesh upload (no 16-bit index limit); raylib keeps the VAO/VBO ids in the ::Mesh.
|
||||||
static ::Mesh upload(const MeshData& d) {
|
static ::Mesh upload(const MeshData& d) {
|
||||||
::Mesh m{};
|
::Mesh m{};
|
||||||
@@ -135,8 +123,13 @@ struct Renderer::Impl {
|
|||||||
m.texcoords = static_cast<float*>(MemAlloc(static_cast<unsigned>(n) * 2 * sizeof(float)));
|
m.texcoords = static_cast<float*>(MemAlloc(static_cast<unsigned>(n) * 2 * sizeof(float)));
|
||||||
m.colors = static_cast<unsigned char*>(MemAlloc(static_cast<unsigned>(n) * 4));
|
m.colors = static_cast<unsigned char*>(MemAlloc(static_cast<unsigned>(n) * 4));
|
||||||
std::memset(m.texcoords, 0, static_cast<std::size_t>(n) * 2 * sizeof(float));
|
std::memset(m.texcoords, 0, static_cast<std::size_t>(n) * 2 * sizeof(float));
|
||||||
|
const bool uvs = d.hasTexcoords();
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
const auto k = static_cast<std::size_t>(i);
|
const auto k = static_cast<std::size_t>(i);
|
||||||
|
if (uvs) {
|
||||||
|
m.texcoords[i * 2 + 0] = d.texcoords[k].x;
|
||||||
|
m.texcoords[i * 2 + 1] = d.texcoords[k].y;
|
||||||
|
}
|
||||||
m.vertices[i * 3 + 0] = d.positions[k].x;
|
m.vertices[i * 3 + 0] = d.positions[k].x;
|
||||||
m.vertices[i * 3 + 1] = d.positions[k].y;
|
m.vertices[i * 3 + 1] = d.positions[k].y;
|
||||||
m.vertices[i * 3 + 2] = d.positions[k].z;
|
m.vertices[i * 3 + 2] = d.positions[k].z;
|
||||||
@@ -154,51 +147,332 @@ struct Renderer::Impl {
|
|||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ::Mesh& gpuMesh(const eng::Mesh& mesh) {
|
||||||
|
auto it = gpu.find(mesh.id());
|
||||||
|
if (it == gpu.end()) it = gpu.emplace(mesh.id(), Gpu{upload(mesh.data()), 0}).first;
|
||||||
|
it->second.lastUsed = frame;
|
||||||
|
return it->second.mesh;
|
||||||
|
}
|
||||||
|
|
||||||
void ensureInstanceCapacity(std::size_t needed) {
|
void ensureInstanceCapacity(std::size_t needed) {
|
||||||
if (needed <= instanceCapacity) return;
|
if (needed <= instanceCapacity) return;
|
||||||
if (instanceVbo) rlUnloadVertexBuffer(instanceVbo);
|
if (instanceVbo) rlUnloadVertexBuffer(instanceVbo);
|
||||||
instanceCapacity = std::max<std::size_t>({needed, instanceCapacity * 2, 1024});
|
instanceCapacity = std::max<std::size_t>({needed, instanceCapacity * 2, 1024});
|
||||||
instanceVbo = rlLoadVertexBuffer(nullptr, static_cast<int>(instanceCapacity * sizeof(Instance)), true);
|
instanceVbo = rlLoadVertexBuffer(nullptr, static_cast<int>(instanceCapacity * sizeof(GpuInstance)), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gathers every visible MeshRenderer below `e`, in one top-down pass (O(N), no matrix chains).
|
// All passes' instances go up in one go; each batch then points its attributes at its own slice.
|
||||||
void collect(Entity e, const Mat4& parentWorld) {
|
void uploadInstances(const FramePlan& plan) {
|
||||||
if (!e.entityActive()) return; // a disabled entity hides its whole subtree
|
if (plan.instances.empty()) return;
|
||||||
const Mat4 world = parentWorld * e.transform().localMatrix();
|
ensureInstanceCapacity(plan.instances.size());
|
||||||
|
rlUpdateVertexBuffer(instanceVbo, plan.instances.data(), static_cast<int>(plan.instances.size() * sizeof(GpuInstance)), 0);
|
||||||
|
}
|
||||||
|
|
||||||
const MeshRenderer* mr = e.tryGet<MeshRenderer>();
|
// One instanced draw call. `culling`: set the cull face from the batch's handedness (shadow maps draw both faces).
|
||||||
if (mr && mr->componentEnabled() && mr->mesh && !mr->mesh->data().empty()) {
|
// `withMaterial`: the lit pass also reads the per-instance colour and material; the depth pass leaves them off.
|
||||||
const eng::Mesh& mesh = *mr->mesh;
|
void drawBatch(const DrawBatch& b, bool culling, bool withMaterial) {
|
||||||
bool visible = true;
|
const ::Mesh& gm = gpuMesh(*b.mesh);
|
||||||
if (mesh.bounds().valid()) {
|
rlEnableVertexArray(gm.vaoId);
|
||||||
const Vec3 c = world.transformPoint(mesh.bounds().center());
|
rlEnableVertexBuffer(instanceVbo);
|
||||||
const Vec3 c0{world.m[0], world.m[1], world.m[2]}, c1{world.m[4], world.m[5], world.m[6]},
|
const int base = static_cast<int>(b.first) * kInstanceStride;
|
||||||
c2{world.m[8], world.m[9], world.m[10]};
|
for (unsigned i = 0; i < 4; ++i) {
|
||||||
const float scale = std::max({c0.length(), c1.length(), c2.length()});
|
rlEnableVertexAttribute(kLocInstanceTransform + i);
|
||||||
visible = sphereVisible(planes, c, 0.5f * mesh.bounds().size().length() * scale);
|
setAttribute(rlSetVertexAttribute, kLocInstanceTransform + i, 4, RL_FLOAT, false, kInstanceStride, base + static_cast<int>(i) * 16);
|
||||||
|
rlSetVertexAttributeDivisor(kLocInstanceTransform + i, 1);
|
||||||
|
}
|
||||||
|
if (withMaterial) {
|
||||||
|
rlEnableVertexAttribute(kLocInstanceColor);
|
||||||
|
setAttribute(rlSetVertexAttribute, kLocInstanceColor, 4, RL_UNSIGNED_BYTE, true, kInstanceStride, base + kInstanceColorOffset);
|
||||||
|
rlSetVertexAttributeDivisor(kLocInstanceColor, 1);
|
||||||
|
rlEnableVertexAttribute(kLocInstanceMaterial);
|
||||||
|
setAttribute(rlSetVertexAttribute, kLocInstanceMaterial, 4, RL_UNSIGNED_BYTE, true, kInstanceStride, base + kInstanceMaterialOffset);
|
||||||
|
rlSetVertexAttributeDivisor(kLocInstanceMaterial, 1);
|
||||||
|
} else {
|
||||||
|
rlDisableVertexAttribute(kLocInstanceColor); // the VAO is shared with the lit pass: nothing stale may stay enabled
|
||||||
|
rlDisableVertexAttribute(kLocInstanceMaterial);
|
||||||
|
}
|
||||||
|
rlDisableVertexBuffer();
|
||||||
|
if (culling) rlSetCullFace(b.mirrored ? RL_CULL_FACE_FRONT : RL_CULL_FACE_BACK); // mirroring flips the winding
|
||||||
|
rlDrawVertexArrayInstanced(0, gm.vertexCount, static_cast<int>(b.count));
|
||||||
|
rlDisableVertexArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- material textures ---------------------------------------------------------------------------------
|
||||||
|
// One GPU copy per Texture::id(), made the first time a batch needs it and dropped when nobody has drawn with it for a while.
|
||||||
|
// Smooth textures get mipmaps and anisotropic filtering (no shimmer on floors seen at a grazing angle).
|
||||||
|
unsigned gpuTexture(const eng::Texture& t, unsigned fallback) {
|
||||||
|
auto it = textures.find(t.id());
|
||||||
|
if (it == textures.end()) {
|
||||||
|
GpuTexture g;
|
||||||
|
::Image img{};
|
||||||
|
img.data = const_cast<std::uint8_t*>(t.pixels().data()); // read only: LoadTextureFromImage copies it
|
||||||
|
img.width = t.width();
|
||||||
|
img.height = t.height();
|
||||||
|
img.mipmaps = 1;
|
||||||
|
img.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
|
||||||
|
g.tex = LoadTextureFromImage(img);
|
||||||
|
if (g.tex.id != 0) {
|
||||||
|
if (t.options().filter == TextureFilter::Smooth) {
|
||||||
|
GenTextureMipmaps(&g.tex);
|
||||||
|
SetTextureFilter(g.tex, TEXTURE_FILTER_TRILINEAR);
|
||||||
|
SetTextureFilter(g.tex, TEXTURE_FILTER_ANISOTROPIC_8X);
|
||||||
|
} else {
|
||||||
|
SetTextureFilter(g.tex, TEXTURE_FILTER_POINT);
|
||||||
|
}
|
||||||
|
SetTextureWrap(g.tex, t.options().wrap == TextureWrap::Clamp ? TEXTURE_WRAP_CLAMP : TEXTURE_WRAP_REPEAT);
|
||||||
}
|
}
|
||||||
if (visible) {
|
it = textures.emplace(t.id(), g).first;
|
||||||
Bucket& b = buckets[mesh.id()];
|
}
|
||||||
b.mesh = &mesh;
|
it->second.lastUsed = frame;
|
||||||
b.lastFrame = frame;
|
return it->second.tex.id != 0 ? it->second.tex.id : fallback;
|
||||||
Instance inst;
|
}
|
||||||
std::memcpy(inst.m, world.m, sizeof inst.m);
|
|
||||||
inst.rgba[0] = mr->tint.r; inst.rgba[1] = mr->tint.g; inst.rgba[2] = mr->tint.b; inst.rgba[3] = mr->tint.a;
|
// The lit shader's material uniforms and the six map samplers for one batch. A map that is not set gets the 1x1 white
|
||||||
b.items.push_back(inst);
|
// texture (an unbound sampler is undefined behaviour on some drivers); the shader skips it by `maps` anyway.
|
||||||
} else {
|
void applyMaterial(const MaterialUniforms& m, unsigned fallback) {
|
||||||
++stats.culled;
|
rlSetUniform(L.matMode, &m.mode, RL_SHADER_UNIFORM_INT, 1);
|
||||||
|
rlSetUniform(L.matMaps, &m.maps, RL_SHADER_UNIFORM_INT, 1);
|
||||||
|
rlSetUniform(L.matColor, m.color, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
rlSetUniform(L.matSurface, m.surface, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
rlSetUniform(L.matExtra, m.extra, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
rlSetUniform(L.matSpecColor, m.specColor, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
rlSetUniform(L.matUv, m.uv, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
for (int k = 0; k < Material::kMapCount; ++k) {
|
||||||
|
const eng::Texture* t = ((m.maps >> k) & 1) != 0 ? m.textures[k] : nullptr;
|
||||||
|
bindTexture(kTextureSlotMaterial + k, t ? gpuTexture(*t, fallback) : fallback, L.matMap[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- ambient zones -------------------------------------------------------------------------------------
|
||||||
|
void releaseAmbient() {
|
||||||
|
if (ambSky.id) UnloadTexture(ambSky);
|
||||||
|
if (ambGround.id) UnloadTexture(ambGround);
|
||||||
|
ambSky = {};
|
||||||
|
ambGround = {};
|
||||||
|
ambW = ambH = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends just the texels the last bake rewrote: one rectangle per Y slice of every box of changed cells. A zone that
|
||||||
|
// walks with the player changes a few thousand texels per frame, where the whole atlas is about a megabyte, twice.
|
||||||
|
// Returns false when that is not worth it (a large part of the atlas, or too many pieces): send everything then.
|
||||||
|
bool updateAmbientPartly(const AmbientGrid& g) {
|
||||||
|
std::size_t texels = 0, pieces = 0;
|
||||||
|
for (const CellBox& b : g.updated) {
|
||||||
|
const std::size_t slices = static_cast<std::size_t>(b.y1 - b.y0 + 1);
|
||||||
|
texels += slices * static_cast<std::size_t>(b.x1 - b.x0 + 1) * static_cast<std::size_t>(b.z1 - b.z0 + 1);
|
||||||
|
pieces += slices;
|
||||||
|
}
|
||||||
|
const std::size_t atlasW = static_cast<std::size_t>(g.atlasWidth());
|
||||||
|
if (pieces > 96 || texels * 2 > atlasW * static_cast<std::size_t>(g.atlasHeight())) return false;
|
||||||
|
for (const CellBox& b : g.updated) {
|
||||||
|
const int w = b.x1 - b.x0 + 1, h = b.z1 - b.z0 + 1;
|
||||||
|
ambStage.resize(static_cast<std::size_t>(w) * static_cast<std::size_t>(h) * 4);
|
||||||
|
for (int y = b.y0; y <= b.y1; ++y) {
|
||||||
|
const int px = (y % g.tilesX) * g.nx + b.x0, py = (y / g.tilesX) * g.nz + b.z0; // this slice's tile in the atlas
|
||||||
|
for (int pass = 0; pass < 2; ++pass) {
|
||||||
|
const std::vector<std::uint8_t>& atlas = pass == 0 ? g.atlasSky : g.atlasGround;
|
||||||
|
for (int r = 0; r < h; ++r)
|
||||||
|
std::memcpy(&ambStage[static_cast<std::size_t>(r) * static_cast<std::size_t>(w) * 4],
|
||||||
|
&atlas[(static_cast<std::size_t>(py + r) * atlasW + static_cast<std::size_t>(px)) * 4], static_cast<std::size_t>(w) * 4);
|
||||||
|
UpdateTextureRec(pass == 0 ? ambSky : ambGround,
|
||||||
|
::Rectangle{static_cast<float>(px), static_cast<float>(py), static_cast<float>(w), static_cast<float>(h)}, ambStage.data());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Entity c = e.hierarchy().firstChild(); c.valid(); c = c.hierarchy().nextSibling()) collect(c, world);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void refreshAmbient(const FramePlan& plan) {
|
||||||
|
if (!plan.ambientChanged || !plan.ambient) return;
|
||||||
|
const AmbientGrid& g = *plan.ambient;
|
||||||
|
if (!g.valid) {
|
||||||
|
releaseAmbient();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int w = g.atlasWidth(), h = g.atlasHeight();
|
||||||
|
if (ambSky.id == 0 || ambGround.id == 0 || w != ambW || h != ambH) {
|
||||||
|
releaseAmbient();
|
||||||
|
auto make = [&](const std::vector<std::uint8_t>& px) {
|
||||||
|
::Image img{};
|
||||||
|
img.data = const_cast<std::uint8_t*>(px.data()); // LoadTextureFromImage only reads it
|
||||||
|
img.width = w;
|
||||||
|
img.height = h;
|
||||||
|
img.mipmaps = 1;
|
||||||
|
img.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
|
||||||
|
::Texture2D t = LoadTextureFromImage(img);
|
||||||
|
SetTextureFilter(t, TEXTURE_FILTER_BILINEAR); // hardware filtering inside a slice, the shader blends slices
|
||||||
|
SetTextureWrap(t, TEXTURE_WRAP_CLAMP);
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
ambSky = make(g.atlasSky);
|
||||||
|
ambGround = make(g.atlasGround);
|
||||||
|
ambW = w;
|
||||||
|
ambH = h;
|
||||||
|
} else if (g.updateAll || !updateAmbientPartly(g)) {
|
||||||
|
UpdateTexture(ambSky, g.atlasSky.data());
|
||||||
|
UpdateTexture(ambGround, g.atlasGround.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- shadow maps ---------------------------------------------------------------------------------------
|
||||||
|
void releaseShadow(int slot) {
|
||||||
|
if (shadowRt[slot].id) UnloadRenderTexture(shadowRt[slot]);
|
||||||
|
shadowRt[slot] = {};
|
||||||
|
shadowRes[slot] = 0;
|
||||||
|
shadowActive[slot] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packed depth lives in RGBA8 (24 bits across RGB), so the map must be sampled without filtering.
|
||||||
|
bool ensureShadowTarget(int slot, int resolution) {
|
||||||
|
if (shadowRt[slot].id != 0 && shadowRes[slot] == resolution) return true;
|
||||||
|
releaseShadow(slot);
|
||||||
|
::RenderTexture2D rt = LoadRenderTexture(resolution, resolution);
|
||||||
|
if (!IsRenderTextureValid(rt)) return false;
|
||||||
|
SetTextureFilter(rt.texture, TEXTURE_FILTER_POINT);
|
||||||
|
SetTextureWrap(rt.texture, TEXTURE_WRAP_CLAMP);
|
||||||
|
shadowRt[slot] = rt;
|
||||||
|
shadowRes[slot] = resolution;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawShadowPass(const ShadowPass& pass) {
|
||||||
|
if (pass.slot < 0 || pass.slot >= kMaxShadowMaps) return;
|
||||||
|
if (!ensureShadowTarget(pass.slot, pass.resolution)) return;
|
||||||
|
|
||||||
|
BeginTextureMode(shadowRt[pass.slot]);
|
||||||
|
ClearBackground(::Color{255, 255, 255, 255}); // depth 1: nothing in the way
|
||||||
|
rlEnableDepthTest();
|
||||||
|
rlDisableColorBlend();
|
||||||
|
rlDisableBackfaceCulling(); // both faces cast: no light leaks through thin walls
|
||||||
|
|
||||||
|
rlEnableShader(shadow.id);
|
||||||
|
rlSetUniformMatrix(S.viewProj, toRaylib(pass.viewProj));
|
||||||
|
const float cast[4] = {pass.radial ? 1.0f : 0.0f, pass.range, 0.0f, 0.0f};
|
||||||
|
rlSetUniform(S.shadowCast, cast, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
const float lightPos[3] = {pass.lightPos.x, pass.lightPos.y, pass.lightPos.z};
|
||||||
|
rlSetUniform(S.lightPos, lightPos, RL_SHADER_UNIFORM_VEC3, 1);
|
||||||
|
for (const DrawBatch& b : pass.batches) {
|
||||||
|
drawBatch(b, false, false);
|
||||||
|
++stats.shadowDrawCalls;
|
||||||
|
}
|
||||||
|
rlDisableShader();
|
||||||
|
|
||||||
|
rlEnableBackfaceCulling();
|
||||||
|
rlEnableColorBlend();
|
||||||
|
rlDisableDepthTest();
|
||||||
|
EndTextureMode();
|
||||||
|
|
||||||
|
shadowActive[pass.slot] = true;
|
||||||
|
shadowLastUsed[pass.slot] = frame;
|
||||||
|
++stats.shadowMaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the camera's pass ---------------------------------------------------------------------------------
|
||||||
|
void bindTexture(int slot, unsigned id, int samplerLoc) {
|
||||||
|
rlActiveTextureSlot(slot);
|
||||||
|
rlEnableTexture(id);
|
||||||
|
rlSetUniform(samplerLoc, &slot, RL_SHADER_UNIFORM_INT, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawLit(const FramePlan& plan) {
|
||||||
|
if (plan.main.empty()) return;
|
||||||
|
const LitUniforms& u = plan.lit;
|
||||||
|
|
||||||
|
rlEnableShader(lit.id);
|
||||||
|
rlSetUniformMatrix(L.viewProj, toRaylib(plan.viewProj));
|
||||||
|
rlSetUniform(L.camPos, u.camPos, RL_SHADER_UNIFORM_VEC3, 1);
|
||||||
|
rlSetUniform(L.lightCount, &u.lightCount, RL_SHADER_UNIFORM_INT, 1);
|
||||||
|
if (u.lightCount > 0) {
|
||||||
|
rlSetUniform(L.lightPosType, u.lightPosType, RL_SHADER_UNIFORM_VEC4, u.lightCount);
|
||||||
|
rlSetUniform(L.lightColorRange, u.lightColorRange, RL_SHADER_UNIFORM_VEC4, u.lightCount);
|
||||||
|
rlSetUniform(L.lightSpot, u.lightSpot, RL_SHADER_UNIFORM_VEC4, u.lightCount);
|
||||||
|
rlSetUniform(L.lightParams, u.lightParams, RL_SHADER_UNIFORM_VEC4, u.lightCount);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < kMaxShadowMaps; ++i) {
|
||||||
|
rlSetUniformMatrix(L.shadowMatrix[i], toRaylib(u.shadowMatrix[i]));
|
||||||
|
rlSetUniform(L.shadowInfo[i], u.shadowInfo[i], RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
}
|
||||||
|
rlSetUniform(L.shadowParams, u.shadowParams, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
rlSetUniform(L.ambSkyGlobal, u.ambSkyGlobal, RL_SHADER_UNIFORM_VEC3, 1);
|
||||||
|
rlSetUniform(L.ambGroundGlobal, u.ambGroundGlobal, RL_SHADER_UNIFORM_VEC3, 1);
|
||||||
|
rlSetUniform(L.ambVolOrigin, u.ambVolOrigin, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
rlSetUniform(L.ambVolDims, u.ambVolDims, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
rlSetUniform(L.ambColorScale, &u.ambColorScale, RL_SHADER_UNIFORM_FLOAT, 1);
|
||||||
|
rlSetUniform(L.envParams, u.envParams, RL_SHADER_UNIFORM_VEC4, 1);
|
||||||
|
rlSetUniform(L.fogColor, u.fogColor, RL_SHADER_UNIFORM_VEC3, 1);
|
||||||
|
|
||||||
|
// Every sampler gets a real texture (raylib's 1x1 white one when there is no shadow map / ambient grid):
|
||||||
|
// an unbound sampler is undefined behaviour on some drivers.
|
||||||
|
const unsigned fallback = rlGetTextureIdDefault();
|
||||||
|
for (int i = 0; i < kMaxShadowMaps; ++i)
|
||||||
|
bindTexture(kTextureSlotShadow + i, shadowActive[i] ? shadowRt[i].texture.id : fallback, L.shadowMap[i]);
|
||||||
|
bindTexture(kTextureSlotAmbientSky, ambSky.id ? ambSky.id : fallback, L.ambSkyTex);
|
||||||
|
bindTexture(kTextureSlotAmbientGround, ambGround.id ? ambGround.id : fallback, L.ambGroundTex);
|
||||||
|
|
||||||
|
for (const DrawBatch& b : plan.main) {
|
||||||
|
applyMaterial(b.mat, fallback);
|
||||||
|
drawBatch(b, true, true);
|
||||||
|
++stats.drawCalls;
|
||||||
|
++stats.meshes;
|
||||||
|
stats.instances += static_cast<int>(b.count);
|
||||||
|
stats.triangles += static_cast<long>(b.mesh->data().triangleCount()) * static_cast<long>(b.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int slot = kLastTextureSlot; slot >= 0; --slot) {
|
||||||
|
rlActiveTextureSlot(slot);
|
||||||
|
rlDisableTexture();
|
||||||
|
}
|
||||||
|
rlActiveTextureSlot(0);
|
||||||
|
rlSetCullFace(RL_CULL_FACE_BACK);
|
||||||
|
rlDisableShader();
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawScene(const FramePlan& plan, World& world) {
|
||||||
|
const CameraData& cam = plan.camera;
|
||||||
|
::Camera3D rc{};
|
||||||
|
rc.position = toRaylib(cam.position);
|
||||||
|
rc.target = toRaylib(cam.position + cam.forward);
|
||||||
|
rc.up = toRaylib(cam.up);
|
||||||
|
rc.fovy = cam.orthographic ? cam.orthoSize : cam.fovDeg;
|
||||||
|
rc.projection = cam.orthographic ? CAMERA_ORTHOGRAPHIC : CAMERA_PERSPECTIVE;
|
||||||
|
|
||||||
|
rlSetClipPlanes(cam.nearPlane, cam.farPlane);
|
||||||
|
BeginMode3D(rc);
|
||||||
|
drawLit(plan);
|
||||||
|
world.drawGizmos();
|
||||||
|
EndMode3D();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- housekeeping --------------------------------------------------------------------------------------
|
||||||
void release() {
|
void release() {
|
||||||
for (auto& [id, g] : gpu) UnloadMesh(g.mesh);
|
for (auto& [id, g] : gpu) UnloadMesh(g.mesh);
|
||||||
gpu.clear();
|
gpu.clear();
|
||||||
buckets.clear();
|
|
||||||
if (instanceVbo) rlUnloadVertexBuffer(instanceVbo);
|
if (instanceVbo) rlUnloadVertexBuffer(instanceVbo);
|
||||||
instanceVbo = 0;
|
instanceVbo = 0;
|
||||||
instanceCapacity = 0;
|
instanceCapacity = 0;
|
||||||
|
for (int i = 0; i < kMaxShadowMaps; ++i) releaseShadow(i);
|
||||||
|
releaseAmbient();
|
||||||
|
for (auto& [id, g] : textures)
|
||||||
|
if (g.tex.id != 0) UnloadTexture(g.tex);
|
||||||
|
textures.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void housekeeping() {
|
||||||
|
for (int i = 0; i < kMaxShadowMaps; ++i)
|
||||||
|
if (shadowRt[i].id != 0 && frame - shadowLastUsed[i] > 600) releaseShadow(i); // lights went away: give the memory back
|
||||||
|
if (frame % 120 != 0) return;
|
||||||
|
for (auto it = gpu.begin(); it != gpu.end();) { // meshes nobody has drawn for a while
|
||||||
|
if (frame - it->second.lastUsed > 240) { UnloadMesh(it->second.mesh); it = gpu.erase(it); }
|
||||||
|
else ++it;
|
||||||
|
}
|
||||||
|
for (auto it = textures.begin(); it != textures.end();) { // textures nobody has drawn with for a while
|
||||||
|
if (frame - it->second.lastUsed > 240) {
|
||||||
|
if (it->second.tex.id != 0) UnloadTexture(it->second.tex);
|
||||||
|
it = textures.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -208,21 +482,60 @@ const RenderStats& Renderer::stats() const { return impl_->stats; }
|
|||||||
|
|
||||||
bool Renderer::init() {
|
bool Renderer::init() {
|
||||||
Impl& r = *impl_;
|
Impl& r = *impl_;
|
||||||
r.shader = LoadShaderFromMemory(kVertexShader, kFragmentShader);
|
r.lit = LoadShaderFromMemory(kLitVertexShader, kLitFragmentShader);
|
||||||
r.ready = IsShaderValid(r.shader);
|
r.ready = IsShaderValid(r.lit);
|
||||||
if (!r.ready) return false;
|
if (!r.ready) return false;
|
||||||
r.locViewProj = GetShaderLocation(r.shader, "viewProj");
|
auto loc = [&](const char* name) { return GetShaderLocation(r.lit, name); };
|
||||||
r.locLightDir = GetShaderLocation(r.shader, "lightDir");
|
Impl::LitLocs& L = r.L;
|
||||||
r.locLightColor = GetShaderLocation(r.shader, "lightColor");
|
L.viewProj = loc("viewProj");
|
||||||
r.locAmbient = GetShaderLocation(r.shader, "ambient");
|
L.camPos = loc("camPos");
|
||||||
|
L.lightCount = loc("lightCount");
|
||||||
|
L.lightPosType = loc("lightPosType");
|
||||||
|
L.lightColorRange = loc("lightColorRange");
|
||||||
|
L.lightSpot = loc("lightSpot");
|
||||||
|
L.lightParams = loc("lightParams");
|
||||||
|
L.shadowMap[0] = loc("shadowMap0"); L.shadowMap[1] = loc("shadowMap1"); L.shadowMap[2] = loc("shadowMap2");
|
||||||
|
L.shadowMatrix[0] = loc("shadowMatrix0"); L.shadowMatrix[1] = loc("shadowMatrix1"); L.shadowMatrix[2] = loc("shadowMatrix2");
|
||||||
|
L.shadowInfo[0] = loc("shadowInfo0"); L.shadowInfo[1] = loc("shadowInfo1"); L.shadowInfo[2] = loc("shadowInfo2");
|
||||||
|
L.shadowParams = loc("shadowParams");
|
||||||
|
L.ambSkyGlobal = loc("ambSkyGlobal");
|
||||||
|
L.ambGroundGlobal = loc("ambGroundGlobal");
|
||||||
|
L.ambSkyTex = loc("ambSkyTex");
|
||||||
|
L.ambGroundTex = loc("ambGroundTex");
|
||||||
|
L.ambVolOrigin = loc("ambVolOrigin");
|
||||||
|
L.ambVolDims = loc("ambVolDims");
|
||||||
|
L.ambColorScale = loc("ambColorScale");
|
||||||
|
L.envParams = loc("envParams");
|
||||||
|
L.fogColor = loc("fogColor");
|
||||||
|
L.matMode = loc("matMode");
|
||||||
|
L.matMaps = loc("matMaps");
|
||||||
|
L.matColor = loc("matColor");
|
||||||
|
L.matSurface = loc("matSurface");
|
||||||
|
L.matExtra = loc("matExtra");
|
||||||
|
L.matSpecColor = loc("matSpecColor");
|
||||||
|
L.matUv = loc("matUv");
|
||||||
|
const char* const mapNames[Material::kMapCount] = {"matAlbedoMap", "matDiffuseMap", "matSpecularMap", "matMetalnessMap", "matRoughnessMap", "matNormalMap"};
|
||||||
|
for (int k = 0; k < Material::kMapCount; ++k) L.matMap[k] = loc(mapNames[k]);
|
||||||
|
|
||||||
|
r.shadow = LoadShaderFromMemory(kShadowVertexShader, kShadowFragmentShader);
|
||||||
|
r.shadowReady = IsShaderValid(r.shadow);
|
||||||
|
if (r.shadowReady) {
|
||||||
|
r.S.viewProj = GetShaderLocation(r.shadow, "viewProj");
|
||||||
|
r.S.shadowCast = GetShaderLocation(r.shadow, "shadowCast");
|
||||||
|
r.S.lightPos = GetShaderLocation(r.shadow, "lightPos");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::shutdown() {
|
void Renderer::shutdown() {
|
||||||
Impl& r = *impl_;
|
Impl& r = *impl_;
|
||||||
r.release();
|
r.release();
|
||||||
|
if (r.shadowReady) {
|
||||||
|
UnloadShader(r.shadow);
|
||||||
|
r.shadowReady = false;
|
||||||
|
}
|
||||||
if (r.ready) {
|
if (r.ready) {
|
||||||
UnloadShader(r.shader);
|
UnloadShader(r.lit);
|
||||||
r.ready = false;
|
r.ready = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,123 +545,35 @@ void Renderer::render(World& world, bool showStats) {
|
|||||||
r.stats = {};
|
r.stats = {};
|
||||||
if (!r.ready) return;
|
if (!r.ready) return;
|
||||||
++r.frame;
|
++r.frame;
|
||||||
Hierarchy& scene = world.hierarchy();
|
for (bool& active : r.shadowActive) active = false;
|
||||||
|
|
||||||
// ---- camera ----
|
const int screenW = GetScreenWidth(), screenH = GetScreenHeight();
|
||||||
Entity camEntity;
|
const FramePlan& plan = r.planner.plan(world, static_cast<float>(screenW) / static_cast<float>(std::max(screenH, 1)));
|
||||||
const Camera* cam = nullptr;
|
r.refreshAmbient(plan); // the bake is announced once: apply it even on a frame without a camera
|
||||||
for (const Entity e : scene.entitiesWith<Camera>()) {
|
r.stats.culled = plan.culled;
|
||||||
const Camera& c = e.get<Camera>();
|
r.stats.lights = plan.lightsShaded;
|
||||||
if (c.componentActive()) { cam = &c; camEntity = e; break; }
|
|
||||||
}
|
|
||||||
if (cam) {
|
|
||||||
const Transform& ct = camEntity.transform();
|
|
||||||
const Vec3 pos = ct.worldPosition();
|
|
||||||
::Camera3D rc{};
|
|
||||||
rc.position = toRaylib(pos);
|
|
||||||
rc.target = toRaylib(pos + ct.forward());
|
|
||||||
rc.up = toRaylib(ct.up());
|
|
||||||
rc.fovy = cam->orthographic ? cam->orthoSize : cam->fovDeg;
|
|
||||||
rc.projection = cam->orthographic ? CAMERA_ORTHOGRAPHIC : CAMERA_PERSPECTIVE;
|
|
||||||
|
|
||||||
// ---- light ----
|
if (plan.hasCamera) {
|
||||||
Vec3 lightDir = Vec3{-0.4f, -1.0f, -0.3f}.normalized();
|
r.uploadInstances(plan);
|
||||||
Vec3 lightColor{1.0f, 0.97f, 0.9f};
|
if (r.shadowReady)
|
||||||
for (const Entity e : scene.entitiesWith<Light>()) {
|
for (const ShadowPass& pass : plan.shadows) r.drawShadowPass(pass);
|
||||||
const Light& l = e.get<Light>();
|
r.drawScene(plan, world);
|
||||||
if (!l.componentActive()) continue;
|
|
||||||
lightDir = e.transform().forward();
|
|
||||||
lightColor = l.color * l.intensity;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const Vec3 ambient{0.35f, 0.37f, 0.42f};
|
|
||||||
SetShaderValue(r.shader, r.locLightDir, &lightDir, SHADER_UNIFORM_VEC3);
|
|
||||||
SetShaderValue(r.shader, r.locLightColor, &lightColor, SHADER_UNIFORM_VEC3);
|
|
||||||
SetShaderValue(r.shader, r.locAmbient, &ambient, SHADER_UNIFORM_VEC3);
|
|
||||||
|
|
||||||
rlSetClipPlanes(cam->nearPlane, cam->farPlane);
|
|
||||||
BeginMode3D(rc);
|
|
||||||
const ::Matrix viewProj = MatrixMultiply(rlGetMatrixModelview(), rlGetMatrixProjection());
|
|
||||||
float vp[16];
|
|
||||||
matrixToArray(viewProj, vp);
|
|
||||||
extractFrustum(vp, r.planes);
|
|
||||||
|
|
||||||
// ---- gather -> batches per mesh ----
|
|
||||||
for (auto& [id, b] : r.buckets) b.items.clear();
|
|
||||||
r.collect(scene.entity(), Mat4{});
|
|
||||||
|
|
||||||
// ---- pack all instances into one buffer, one upload ----
|
|
||||||
r.packed.clear();
|
|
||||||
struct Batch { const eng::Mesh* mesh; std::size_t first, count; };
|
|
||||||
std::vector<Batch> batches;
|
|
||||||
for (auto& [id, b] : r.buckets) {
|
|
||||||
if (b.items.empty()) continue;
|
|
||||||
batches.push_back({b.mesh, r.packed.size(), b.items.size()});
|
|
||||||
r.packed.insert(r.packed.end(), b.items.begin(), b.items.end());
|
|
||||||
}
|
|
||||||
if (!r.packed.empty()) {
|
|
||||||
r.ensureInstanceCapacity(r.packed.size());
|
|
||||||
rlUpdateVertexBuffer(r.instanceVbo, r.packed.data(), static_cast<int>(r.packed.size() * sizeof(Instance)), 0);
|
|
||||||
|
|
||||||
rlEnableShader(r.shader.id);
|
|
||||||
rlSetUniformMatrix(r.locViewProj, viewProj);
|
|
||||||
for (const Batch& batch : batches) {
|
|
||||||
auto it = r.gpu.find(batch.mesh->id());
|
|
||||||
if (it == r.gpu.end()) it = r.gpu.emplace(batch.mesh->id(), Impl::Gpu{Impl::upload(batch.mesh->data()), 0}).first;
|
|
||||||
it->second.lastUsed = r.frame;
|
|
||||||
const ::Mesh& gm = it->second.mesh;
|
|
||||||
|
|
||||||
rlEnableVertexArray(gm.vaoId);
|
|
||||||
rlEnableVertexBuffer(r.instanceVbo);
|
|
||||||
const int base = static_cast<int>(batch.first * sizeof(Instance));
|
|
||||||
for (unsigned i = 0; i < 4; ++i) {
|
|
||||||
rlEnableVertexAttribute(kLocInstanceTransform + i);
|
|
||||||
rlSetVertexAttribute(kLocInstanceTransform + i, 4, RL_FLOAT, false, sizeof(Instance), base + static_cast<int>(i) * 16);
|
|
||||||
rlSetVertexAttributeDivisor(kLocInstanceTransform + i, 1);
|
|
||||||
}
|
|
||||||
rlEnableVertexAttribute(kLocInstanceColor);
|
|
||||||
rlSetVertexAttribute(kLocInstanceColor, 4, RL_UNSIGNED_BYTE, true, sizeof(Instance), base + 64);
|
|
||||||
rlSetVertexAttributeDivisor(kLocInstanceColor, 1);
|
|
||||||
rlDisableVertexBuffer();
|
|
||||||
|
|
||||||
rlDrawVertexArrayInstanced(0, gm.vertexCount, static_cast<int>(batch.count));
|
|
||||||
rlDisableVertexArray();
|
|
||||||
|
|
||||||
++r.stats.drawCalls;
|
|
||||||
++r.stats.meshes;
|
|
||||||
r.stats.instances += static_cast<int>(batch.count);
|
|
||||||
r.stats.triangles += static_cast<long>(gm.triangleCount) * static_cast<long>(batch.count);
|
|
||||||
}
|
|
||||||
rlDisableShader();
|
|
||||||
}
|
|
||||||
EndMode3D();
|
|
||||||
|
|
||||||
// ---- housekeeping: free GPU meshes / buckets nobody used for a while ----
|
|
||||||
if (r.frame % 120 == 0) {
|
|
||||||
for (auto it = r.gpu.begin(); it != r.gpu.end();) {
|
|
||||||
if (r.frame - it->second.lastUsed > 240) { UnloadMesh(it->second.mesh); it = r.gpu.erase(it); }
|
|
||||||
else ++it;
|
|
||||||
}
|
|
||||||
for (auto it = r.buckets.begin(); it != r.buckets.end();) {
|
|
||||||
if (r.frame - it->second.lastFrame > 240) it = r.buckets.erase(it); else ++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 2D overlay ----
|
// ---- 2D overlay ----
|
||||||
for (const Entity e : scene.entitiesWith<Text>()) {
|
for (const TextData& t : plan.texts)
|
||||||
const Text& t = e.get<Text>();
|
|
||||||
if (!t.componentActive() || t.value.empty()) continue;
|
|
||||||
DrawText(t.value.c_str(), static_cast<int>(t.position.x), static_cast<int>(t.position.y), static_cast<int>(t.size),
|
DrawText(t.value.c_str(), static_cast<int>(t.position.x), static_cast<int>(t.position.y), static_cast<int>(t.size),
|
||||||
::Color{t.color.r, t.color.g, t.color.b, t.color.a});
|
::Color{t.color.r, t.color.g, t.color.b, t.color.a});
|
||||||
}
|
world.drawGui();
|
||||||
if (showStats) {
|
if (showStats) {
|
||||||
const int y = GetScreenHeight() - 28;
|
const int y = screenH - 28;
|
||||||
DrawRectangle(0, y - 6, 560, 34, ::Color{0, 0, 0, 140});
|
DrawRectangle(0, y - 6, 820, 34, ::Color{0, 0, 0, 140});
|
||||||
DrawText(TextFormat("%d fps draw calls %d instances %d culled %d tris %ld", GetFPS(), r.stats.drawCalls,
|
DrawText(TextFormat("%d fps draw calls %d (+%d shadow) instances %d culled %d tris %ld lights %d shadow maps %d",
|
||||||
r.stats.instances, r.stats.culled, r.stats.triangles),
|
GetFPS(), r.stats.drawCalls, r.stats.shadowDrawCalls, r.stats.instances, r.stats.culled, r.stats.triangles,
|
||||||
|
r.stats.lights, r.stats.shadowMaps),
|
||||||
8, y, 18, ::Color{255, 255, 255, 255});
|
8, y, 18, ::Color{255, 255, 255, 255});
|
||||||
}
|
}
|
||||||
|
r.housekeeping();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace eng::detail
|
} // namespace eng::detail
|
||||||
+7
-3
@@ -7,11 +7,14 @@
|
|||||||
namespace eng::detail {
|
namespace eng::detail {
|
||||||
|
|
||||||
struct RenderStats {
|
struct RenderStats {
|
||||||
int drawCalls = 0; // instanced draw calls issued (one per distinct mesh)
|
int drawCalls = 0; // instanced draw calls of the camera pass (one per distinct mesh and handedness)
|
||||||
int instances = 0; // objects drawn
|
int instances = 0; // objects drawn
|
||||||
int culled = 0; // objects rejected by frustum culling
|
int culled = 0; // objects rejected by frustum culling
|
||||||
int meshes = 0; // distinct meshes drawn
|
int meshes = 0; // distinct meshes drawn
|
||||||
long triangles = 0; // triangles submitted
|
long triangles = 0; // triangles submitted by the camera pass
|
||||||
|
int lights = 0; // lights shaded
|
||||||
|
int shadowMaps = 0; // shadow maps rendered
|
||||||
|
int shadowDrawCalls = 0; // draw calls spent on shadow maps
|
||||||
};
|
};
|
||||||
|
|
||||||
class Renderer {
|
class Renderer {
|
||||||
@@ -23,7 +26,8 @@ public:
|
|||||||
|
|
||||||
bool init(); // after the window exists
|
bool init(); // after the window exists
|
||||||
void shutdown(); // before it closes
|
void shutdown(); // before it closes
|
||||||
// 3D pass from the first active Camera (GPU-instanced, frustum culled), then Text overlays.
|
// Shadow maps, then the lit 3D pass from the first active Camera (GPU-instanced, frustum culled), the
|
||||||
|
// components' drawGizmos() inside that view, then HUD text, drawGui() and the optional stats overlay.
|
||||||
void render(World& world, bool showStats);
|
void render(World& world, bool showStats);
|
||||||
[[nodiscard]] const RenderStats& stats() const;
|
[[nodiscard]] const RenderStats& stats() const;
|
||||||
|
|
||||||
|
|||||||
+35
-8
@@ -43,18 +43,24 @@ namespace {
|
|||||||
// Generators
|
// Generators
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
MeshData generateBox(Vec3 size, Rgba color) {
|
MeshData generateBox(Vec3 size, Rgba color) {
|
||||||
struct Face { Vec3 n, u, v; };
|
struct Face { Vec3 n, u, v, up; }; // outward normal, two edge directions for the corners, and "up" in the texture
|
||||||
static const Face faces[6] = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}, {{-1, 0, 0}, {0, 0, 1}, {0, 1, 0}},
|
static const Face faces[6] = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 1, 0}}, {{-1, 0, 0}, {0, 0, 1}, {0, 1, 0}, {0, 1, 0}},
|
||||||
{{0, 1, 0}, {0, 0, 1}, {1, 0, 0}}, {{0, -1, 0}, {1, 0, 0}, {0, 0, 1}},
|
{{0, 1, 0}, {0, 0, 1}, {1, 0, 0}, {0, 0, -1}}, {{0, -1, 0}, {1, 0, 0}, {0, 0, 1}, {0, 0, 1}},
|
||||||
{{0, 0, 1}, {1, 0, 0}, {0, 1, 0}}, {{0, 0, -1}, {0, 1, 0}, {1, 0, 0}}};
|
{{0, 0, 1}, {1, 0, 0}, {0, 1, 0}, {0, 1, 0}}, {{0, 0, -1}, {0, 1, 0}, {1, 0, 0}, {0, 1, 0}}};
|
||||||
MeshData m;
|
MeshData m;
|
||||||
const Vec3 h = size * 0.5f;
|
const Vec3 h = size * 0.5f;
|
||||||
for (const Face& f : faces) {
|
for (const Face& f : faces) {
|
||||||
const Vec3 c{f.n.x * h.x, f.n.y * h.y, f.n.z * h.z};
|
const Vec3 c{f.n.x * h.x, f.n.y * h.y, f.n.z * h.z};
|
||||||
const Vec3 u{f.u.x * h.x, f.u.y * h.y, f.u.z * h.z}, v{f.v.x * h.x, f.v.y * h.y, f.v.z * h.z};
|
const Vec3 u{f.u.x * h.x, f.u.y * h.y, f.u.z * h.z}, v{f.v.x * h.x, f.v.y * h.y, f.v.z * h.z};
|
||||||
const Vec3 p0 = c - u - v, p1 = c + u - v, p2 = c + u + v, p3 = c - u + v;
|
const Vec3 p0 = c - u - v, p1 = c + u - v, p2 = c + u + v, p3 = c - u + v;
|
||||||
m.addTriangle(p0, p1, p2, f.n, f.n, f.n, color, color, color);
|
// Texture coordinates in world units, so a texture is not stretched however long the box is: seen from outside,
|
||||||
m.addTriangle(p0, p2, p3, f.n, f.n, f.n, color, color, color);
|
// u grows to the right and v downwards from the top edge of the face (the top face's "up" is -Z).
|
||||||
|
const Vec3 right = cross(f.up, f.n);
|
||||||
|
const float width = std::fabs(right.x) * size.x + std::fabs(right.y) * size.y + std::fabs(right.z) * size.z;
|
||||||
|
const float height = std::fabs(f.up.x) * size.x + std::fabs(f.up.y) * size.y + std::fabs(f.up.z) * size.z;
|
||||||
|
auto uv = [&](Vec3 p) { const Vec3 d = p - c; return Vec2{dot(d, right) + width * 0.5f, height * 0.5f - dot(d, f.up)}; };
|
||||||
|
m.addTriangle(p0, p1, p2, f.n, f.n, f.n, color, color, color, uv(p0), uv(p1), uv(p2));
|
||||||
|
m.addTriangle(p0, p2, p3, f.n, f.n, f.n, color, color, color, uv(p0), uv(p2), uv(p3));
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
@@ -68,12 +74,14 @@ MeshData generateSphere(float radius, int segments, int rings, Rgba color) {
|
|||||||
const float phi = 2 * kPi * static_cast<float>(j) / static_cast<float>(segments);
|
const float phi = 2 * kPi * static_cast<float>(j) / static_cast<float>(segments);
|
||||||
return Vec3{std::sin(theta) * std::cos(phi), std::cos(theta), std::sin(theta) * std::sin(phi)};
|
return Vec3{std::sin(theta) * std::cos(phi), std::cos(theta), std::sin(theta) * std::sin(phi)};
|
||||||
};
|
};
|
||||||
|
// The whole texture once around the sphere (u from the right as seen from outside) and from pole to pole (v = 0 at the top).
|
||||||
|
auto uv = [&](int i, int j) { return Vec2{1.0f - static_cast<float>(j) / static_cast<float>(segments), static_cast<float>(i) / static_cast<float>(rings)}; };
|
||||||
for (int i = 0; i < rings; ++i)
|
for (int i = 0; i < rings; ++i)
|
||||||
for (int j = 0; j < segments; ++j) {
|
for (int j = 0; j < segments; ++j) {
|
||||||
const Vec3 a = point(i, j), b = point(i + 1, j), c = point(i + 1, j + 1), d = point(i, j + 1);
|
const Vec3 a = point(i, j), b = point(i + 1, j), c = point(i + 1, j + 1), d = point(i, j + 1);
|
||||||
// a==d at the top pole and b==c at the bottom pole: skip the degenerate half of the quad
|
// a==d at the top pole and b==c at the bottom pole: skip the degenerate half of the quad
|
||||||
if (i < rings - 1) m.addTriangle(a * radius, c * radius, b * radius, a, c, b, color, color, color);
|
if (i < rings - 1) m.addTriangle(a * radius, c * radius, b * radius, a, c, b, color, color, color, uv(i, j), uv(i + 1, j + 1), uv(i + 1, j));
|
||||||
if (i > 0) m.addTriangle(a * radius, d * radius, c * radius, a, d, c, color, color, color);
|
if (i > 0) m.addTriangle(a * radius, d * radius, c * radius, a, d, c, color, color, color, uv(i, j), uv(i, j + 1), uv(i + 1, j + 1));
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
@@ -128,6 +136,7 @@ struct State {
|
|||||||
std::mutex mutex;
|
std::mutex mutex;
|
||||||
Cache<Mesh> meshes;
|
Cache<Mesh> meshes;
|
||||||
Cache<Model> models;
|
Cache<Model> models;
|
||||||
|
Cache<Texture> textures;
|
||||||
std::size_t hits = 0, misses = 0;
|
std::size_t hits = 0, misses = 0;
|
||||||
};
|
};
|
||||||
State& state() {
|
State& state() {
|
||||||
@@ -221,11 +230,28 @@ ModelPtr Resources::svgFromString(std::string_view xml, const ExtrudeOptions& op
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TexturePtr Resources::texture(const std::string& path, TextureOptions options) {
|
||||||
|
std::error_code ec;
|
||||||
|
const std::filesystem::path canonical = std::filesystem::weakly_canonical(path, ec);
|
||||||
|
Key k;
|
||||||
|
k.tag("texture").add(std::string_view(ec ? path : canonical.string()));
|
||||||
|
const auto size = std::filesystem::file_size(path, ec);
|
||||||
|
k.raw(ec ? std::uintmax_t{0} : size);
|
||||||
|
const auto stamp = std::filesystem::last_write_time(path, ec);
|
||||||
|
k.raw(ec ? std::int64_t{0} : static_cast<std::int64_t>(stamp.time_since_epoch().count()));
|
||||||
|
k.raw(static_cast<std::uint32_t>(options.wrap)).raw(static_cast<std::uint32_t>(options.filter));
|
||||||
|
return getOrCreate(&State::textures, k.s, [&] {
|
||||||
|
ImageData img = loadImageFile(path);
|
||||||
|
return Texture::create(img.width, img.height, std::move(img.rgba), options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void Resources::collect() {
|
void Resources::collect() {
|
||||||
State& st = state();
|
State& st = state();
|
||||||
std::lock_guard<std::mutex> lock(st.mutex);
|
std::lock_guard<std::mutex> lock(st.mutex);
|
||||||
st.meshes.prune();
|
st.meshes.prune();
|
||||||
st.models.prune();
|
st.models.prune();
|
||||||
|
st.textures.prune();
|
||||||
}
|
}
|
||||||
|
|
||||||
Resources::Stats Resources::stats() {
|
Resources::Stats Resources::stats() {
|
||||||
@@ -234,6 +260,7 @@ Resources::Stats Resources::stats() {
|
|||||||
Stats s;
|
Stats s;
|
||||||
s.liveMeshes = st.meshes.live();
|
s.liveMeshes = st.meshes.live();
|
||||||
s.liveModels = st.models.live();
|
s.liveModels = st.models.live();
|
||||||
|
s.liveTextures = st.textures.live();
|
||||||
s.hits = st.hits;
|
s.hits = st.hits;
|
||||||
s.misses = st.misses;
|
s.misses = st.misses;
|
||||||
return s;
|
return s;
|
||||||
|
|||||||
+276
@@ -0,0 +1,276 @@
|
|||||||
|
#include "engine/detail/scene.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace eng::detail {
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Frustum
|
||||||
|
// ===========================================================================
|
||||||
|
Frustum Frustum::fromViewProj(const Mat4& vp) {
|
||||||
|
Frustum f;
|
||||||
|
auto row = [&](int r, int c) { return vp.m[c * 4 + r]; };
|
||||||
|
const int sign[6] = {+1, -1, +1, -1, +1, -1};
|
||||||
|
const int axis[6] = {0, 0, 1, 1, 2, 2};
|
||||||
|
for (int i = 0; i < 6; ++i) {
|
||||||
|
float* p = f.planes[i];
|
||||||
|
for (int c = 0; c < 4; ++c) p[c] = row(3, c) + static_cast<float>(sign[i]) * row(axis[i], c);
|
||||||
|
const float len = std::sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
|
||||||
|
if (len > 1e-12f) for (int c = 0; c < 4; ++c) p[c] /= len;
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Frustum::sphereVisible(Vec3 c, float r) const {
|
||||||
|
for (int i = 0; i < 6; ++i)
|
||||||
|
if (planes[i][0] * c.x + planes[i][1] * c.y + planes[i][2] * c.z + planes[i][3] < -r) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Scene gathering
|
||||||
|
// ===========================================================================
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::uint8_t toByte(float v) { return static_cast<std::uint8_t>(std::lround(std::clamp(v, 0.0f, 1.0f) * 255.0f)); }
|
||||||
|
|
||||||
|
struct Gatherer {
|
||||||
|
SceneSnapshot& out;
|
||||||
|
|
||||||
|
void visit(Entity e, const Mat4& parentWorld, bool hiddenAbove) {
|
||||||
|
if (!e.entityActive()) return; // a disabled entity hides its whole subtree
|
||||||
|
const Mat4 world = parentWorld * e.transform().localMatrix();
|
||||||
|
|
||||||
|
if (!hiddenAbove) {
|
||||||
|
if (const MeshRenderer* mr = e.tryGet<MeshRenderer>(); mr && mr->componentEnabled() && mr->visible) addMesh(*mr, world);
|
||||||
|
if (const Light* l = e.tryGet<Light>(); l && l->componentEnabled() && l->visible) addLight(*l, world);
|
||||||
|
if (const Text* t = e.tryGet<Text>(); t && t->componentEnabled() && t->visible && !t->value.empty())
|
||||||
|
out.texts.push_back({t->value, t->position, t->size, t->color});
|
||||||
|
}
|
||||||
|
if (const AmbientZone* z = e.tryGet<AmbientZone>(); z && z->componentEnabled()) addZone(*z, world); // zones are not "drawn"
|
||||||
|
|
||||||
|
bool hideChildren = hiddenAbove;
|
||||||
|
forEachRenderable(e, [&](Renderable& r) { hideChildren = hideChildren || (r.componentEnabled() && !r.childrenVisible); });
|
||||||
|
for (Entity c = e.hierarchy().firstChild(); c.valid(); c = c.hierarchy().nextSibling()) visit(c, world, hideChildren);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addMesh(const MeshRenderer& mr, const Mat4& world) {
|
||||||
|
if (!mr.mesh || mr.mesh->data().empty()) return;
|
||||||
|
const eng::Mesh& mesh = *mr.mesh;
|
||||||
|
MeshInstance inst;
|
||||||
|
inst.mesh = &mesh;
|
||||||
|
inst.material = mr.material.get();
|
||||||
|
inst.world = world;
|
||||||
|
if (mesh.bounds().valid()) {
|
||||||
|
inst.center = world.transformPoint(mesh.bounds().center());
|
||||||
|
const Vec3 c0{world.m[0], world.m[1], world.m[2]}, c1{world.m[4], world.m[5], world.m[6]},
|
||||||
|
c2{world.m[8], world.m[9], world.m[10]};
|
||||||
|
inst.radius = 0.5f * mesh.bounds().size().length() * std::max({c0.length(), c1.length(), c2.length()});
|
||||||
|
} else {
|
||||||
|
inst.center = world.translation();
|
||||||
|
}
|
||||||
|
inst.tint = mr.tint;
|
||||||
|
inst.roughness = toByte(mr.roughness);
|
||||||
|
inst.metallic = toByte(mr.metallic);
|
||||||
|
inst.emission = toByte(mr.emission / 4.0f);
|
||||||
|
inst.flags = static_cast<std::uint8_t>((mr.receiveShadows ? kMatReceiveShadows : 0) | (mr.unlit ? kMatUnlit : 0));
|
||||||
|
inst.castShadows = mr.castShadows;
|
||||||
|
out.meshes.push_back(inst);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addLight(const Light& l, const Mat4& world) {
|
||||||
|
LightData d;
|
||||||
|
d.type = l.type;
|
||||||
|
d.position = world.translation();
|
||||||
|
d.direction = world.transformDirection({0, 0, -1}).normalized();
|
||||||
|
d.color = l.color;
|
||||||
|
d.intensity = l.intensity;
|
||||||
|
d.range = std::max(l.range, 0.01f);
|
||||||
|
const float outerHalf = 0.5f * std::clamp(l.spotAngleDeg, 1.0f, 179.0f);
|
||||||
|
const float innerHalf = outerHalf * (1.0f - std::clamp(l.spotSoftness, 0.0f, 1.0f));
|
||||||
|
d.cosOuter = std::cos(outerHalf * kDegToRad);
|
||||||
|
d.cosInner = std::cos(innerHalf * kDegToRad);
|
||||||
|
d.castShadows = l.castShadows && l.type != LightType::Point; // point lights are never shadowed
|
||||||
|
d.shadowBias = l.shadowBias;
|
||||||
|
out.lights.push_back(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addZone(const AmbientZone& z, const Mat4& world) {
|
||||||
|
ZoneData d;
|
||||||
|
d.shape = z.shape;
|
||||||
|
d.world = world;
|
||||||
|
d.inverse = world.inverseAffine();
|
||||||
|
d.size = z.size;
|
||||||
|
d.radius = z.radius;
|
||||||
|
d.height = z.height;
|
||||||
|
d.polygon = z.polygon;
|
||||||
|
d.mesh = z.mesh;
|
||||||
|
d.sky = z.sky;
|
||||||
|
d.ground = z.ground;
|
||||||
|
d.intensity = z.intensity;
|
||||||
|
d.mode = z.mode;
|
||||||
|
d.falloff = std::max(z.falloff, 0.0f);
|
||||||
|
d.priority = z.priority;
|
||||||
|
out.zones.push_back(std::move(d));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SceneSnapshot gatherScene(World& world) {
|
||||||
|
SceneSnapshot s;
|
||||||
|
Hierarchy& scene = world.hierarchy();
|
||||||
|
|
||||||
|
for (const Entity e : scene.entitiesWith<Camera>()) {
|
||||||
|
const Camera& c = e.get<Camera>();
|
||||||
|
if (!c.componentActive()) continue;
|
||||||
|
const Transform& t = e.transform();
|
||||||
|
s.hasCamera = true;
|
||||||
|
s.camera.position = t.worldPosition();
|
||||||
|
s.camera.forward = t.forward();
|
||||||
|
s.camera.up = t.up();
|
||||||
|
s.camera.fovDeg = c.fovDeg;
|
||||||
|
s.camera.nearPlane = c.nearPlane;
|
||||||
|
s.camera.farPlane = c.farPlane;
|
||||||
|
s.camera.orthographic = c.orthographic;
|
||||||
|
s.camera.orthoSize = c.orthoSize;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (const Entity e : scene.entitiesWith<Environment>()) {
|
||||||
|
const Environment& env = e.get<Environment>();
|
||||||
|
if (env.componentActive()) { s.env = env; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Gatherer g{s};
|
||||||
|
g.visit(scene.entity(), Mat4{}, false);
|
||||||
|
|
||||||
|
// A world without any Light component gets a default sun. (Lights that exist but are hidden or disabled
|
||||||
|
// mean "lights off", so they do not bring it back.)
|
||||||
|
bool anyLight = false;
|
||||||
|
for (const Entity e : scene.entitiesWith<Light>()) { (void)e; anyLight = true; break; }
|
||||||
|
if (!anyLight) {
|
||||||
|
LightData sun;
|
||||||
|
sun.direction = Vec3{-0.4f, -1.0f, -0.3f}.normalized();
|
||||||
|
sun.color = {1.0f, 0.97f, 0.9f};
|
||||||
|
sun.castShadows = true;
|
||||||
|
s.lights.push_back(sun);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Light budget and shadow slots
|
||||||
|
// ===========================================================================
|
||||||
|
std::vector<LightData> selectLights(const std::vector<LightData>& all, Vec3 cameraPosition) {
|
||||||
|
std::vector<LightData> suns, local;
|
||||||
|
for (const LightData& l : all) {
|
||||||
|
if (l.intensity <= 0 || std::max({l.color.x, l.color.y, l.color.z}) <= 0) continue;
|
||||||
|
(l.type == LightType::Directional ? suns : local).push_back(l);
|
||||||
|
}
|
||||||
|
auto brightness = [](const LightData& l) { return l.intensity * std::max({l.color.x, l.color.y, l.color.z}); };
|
||||||
|
std::stable_sort(suns.begin(), suns.end(), [&](const LightData& a, const LightData& b) { return brightness(a) > brightness(b); });
|
||||||
|
if (suns.size() > 4) suns.resize(4);
|
||||||
|
auto score = [&](const LightData& l) { return brightness(l) / (1.0f + (l.position - cameraPosition).lengthSq()); };
|
||||||
|
std::stable_sort(local.begin(), local.end(), [&](const LightData& a, const LightData& b) { return score(a) > score(b); });
|
||||||
|
|
||||||
|
std::vector<LightData> out = suns;
|
||||||
|
for (const LightData& l : local) {
|
||||||
|
if (static_cast<int>(out.size()) >= kMaxLights) break;
|
||||||
|
out.push_back(l);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> assignShadowSlots(const std::vector<LightData>& selected, const Environment& env) {
|
||||||
|
std::vector<int> slot(selected.size(), -1);
|
||||||
|
if (!env.shadows) return slot;
|
||||||
|
const int budget = std::clamp(env.maxShadowLights, 0, kMaxShadowMaps);
|
||||||
|
int next = 0;
|
||||||
|
for (std::size_t i = 0; i < selected.size() && next < budget; ++i) // the sun first ...
|
||||||
|
if (selected[i].castShadows && selected[i].type == LightType::Directional) { slot[i] = next++; break; }
|
||||||
|
for (std::size_t i = 0; i < selected.size() && next < budget; ++i) // ... then spots, in priority order
|
||||||
|
if (selected[i].castShadows && selected[i].type == LightType::Spot) slot[i] = next++;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Shadow-map fitting
|
||||||
|
// ===========================================================================
|
||||||
|
namespace {
|
||||||
|
Vec3 lightUp(Vec3 d) { return std::fabs(d.y) > 0.99f ? Vec3{0, 0, 1} : Vec3{0, 1, 0}; }
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ShadowView fitSunShadow(Vec3 lightDir, const CameraData& cam, float aspect, float shadowDistance, int resolution,
|
||||||
|
const std::vector<MeshInstance>& casters) {
|
||||||
|
const Vec3 d = lightDir.normalized();
|
||||||
|
const float nearD = std::max(cam.nearPlane, 0.01f);
|
||||||
|
const float farD = std::max(nearD + 0.1f, std::min(cam.farPlane, shadowDistance));
|
||||||
|
|
||||||
|
// The slice of the camera's view the shadow has to cover.
|
||||||
|
const Vec3 right = cross(cam.forward, cam.up).normalized();
|
||||||
|
Vec3 corners[8];
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
const float dist = i == 0 ? nearD : farD;
|
||||||
|
const float hh = cam.orthographic ? 0.5f * cam.orthoSize : dist * std::tan(cam.fovDeg * kDegToRad * 0.5f);
|
||||||
|
const float hw = hh * aspect;
|
||||||
|
const Vec3 c = cam.position + cam.forward * dist;
|
||||||
|
corners[i * 4 + 0] = c + right * hw + cam.up * hh;
|
||||||
|
corners[i * 4 + 1] = c - right * hw + cam.up * hh;
|
||||||
|
corners[i * 4 + 2] = c + right * hw - cam.up * hh;
|
||||||
|
corners[i * 4 + 3] = c - right * hw - cam.up * hh;
|
||||||
|
}
|
||||||
|
Vec3 centre{};
|
||||||
|
for (const Vec3& c : corners) centre += c;
|
||||||
|
centre = centre / 8.0f;
|
||||||
|
float radius = 1.0f;
|
||||||
|
for (const Vec3& c : corners) radius = std::max(radius, (c - centre).length()); // a sphere: the same size however the camera turns
|
||||||
|
|
||||||
|
// Snap the window to whole texels (in light space) so shadow edges do not crawl.
|
||||||
|
const Vec3 up = lightUp(d);
|
||||||
|
const Mat4 ref = Mat4::lookAt({0, 0, 0}, d, up);
|
||||||
|
const float texel = 2.0f * radius / static_cast<float>(std::max(resolution, 1));
|
||||||
|
Vec3 ls = ref.transformPoint(centre);
|
||||||
|
ls.x = std::floor(ls.x / texel) * texel;
|
||||||
|
ls.y = std::floor(ls.y / texel) * texel;
|
||||||
|
const Vec3 snapped = ref.inverseAffine().transformPoint(ls);
|
||||||
|
|
||||||
|
// Depth range: just the casters that can land in the window.
|
||||||
|
float tmin = -radius, tmax = radius;
|
||||||
|
bool any = false;
|
||||||
|
for (const MeshInstance& c : casters) {
|
||||||
|
const Vec3 rel = c.center - snapped;
|
||||||
|
const float t = dot(rel, d);
|
||||||
|
if ((rel - d * t).length() > radius * 1.5f + c.radius) continue;
|
||||||
|
const float lo = t - c.radius, hi = t + c.radius;
|
||||||
|
tmin = any ? std::min(tmin, lo) : lo;
|
||||||
|
tmax = any ? std::max(tmax, hi) : hi;
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
const Vec3 eye = snapped + d * (tmin - 1.0f);
|
||||||
|
const float depth = (tmax - tmin) + 2.0f;
|
||||||
|
|
||||||
|
ShadowView v;
|
||||||
|
v.viewProj = Mat4::ortho(-radius, radius, -radius, radius, 0.0f, depth) * Mat4::lookAt(eye, eye + d, up);
|
||||||
|
v.frustum = Frustum::fromViewProj(v.viewProj);
|
||||||
|
v.perspective = false;
|
||||||
|
v.texelWorld = texel;
|
||||||
|
v.depthRange = depth;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShadowView fitSpotShadow(const LightData& l, int resolution) {
|
||||||
|
const Vec3 d = l.direction.normalized();
|
||||||
|
const float halfOuterDeg = std::acos(std::clamp(l.cosOuter, -1.0f, 1.0f)) / kDegToRad;
|
||||||
|
const float fov = std::min(170.0f, 2.0f * halfOuterDeg + 4.0f);
|
||||||
|
const float nearP = std::max(0.05f, l.range * 0.01f);
|
||||||
|
ShadowView v;
|
||||||
|
v.viewProj = Mat4::perspective(fov, 1.0f, nearP, std::max(l.range, nearP + 0.1f)) * Mat4::lookAt(l.position, l.position + d, lightUp(d));
|
||||||
|
v.frustum = Frustum::fromViewProj(v.viewProj);
|
||||||
|
v.perspective = true;
|
||||||
|
v.texelWorld = 2.0f * std::tan(fov * kDegToRad * 0.5f) / static_cast<float>(std::max(resolution, 1));
|
||||||
|
v.depthRange = std::max(l.range, 0.01f);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eng::detail
|
||||||
+381
@@ -0,0 +1,381 @@
|
|||||||
|
// GLSL 330 core for the lit, instanced mesh pipeline and its shadow pass. Kept in plain raw strings; tools/gpu_check
|
||||||
|
// compiles them on a software OpenGL driver and compares what they draw with an independent CPU model of the lighting.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace eng::detail {
|
||||||
|
|
||||||
|
// Attribute locations: 0..5 are raylib's standard mesh layout (UploadMesh), instance data follows.
|
||||||
|
inline constexpr unsigned kLocInstanceTransform = 9; // mat4 -> 9..12
|
||||||
|
inline constexpr unsigned kLocInstanceColor = 13;
|
||||||
|
inline constexpr unsigned kLocInstanceMaterial = 14; // roughness, metallic, emission / 4, flags
|
||||||
|
|
||||||
|
inline const char* const kLitVertexShader = R"GLSL(#version 330
|
||||||
|
layout(location = 0) in vec3 vertexPosition;
|
||||||
|
layout(location = 1) in vec2 vertexTexCoord;
|
||||||
|
layout(location = 2) in vec3 vertexNormal;
|
||||||
|
layout(location = 3) in vec4 vertexColor;
|
||||||
|
layout(location = 9) in mat4 instanceTransform;
|
||||||
|
layout(location = 13) in vec4 instanceColor;
|
||||||
|
layout(location = 14) in vec4 instanceMaterial;
|
||||||
|
uniform mat4 viewProj;
|
||||||
|
out vec3 fragWorldPos;
|
||||||
|
out vec3 fragNormal;
|
||||||
|
out vec4 fragColor;
|
||||||
|
out vec2 fragUV;
|
||||||
|
flat out vec4 fragMaterial;
|
||||||
|
void main() {
|
||||||
|
vec3 c0 = instanceTransform[0].xyz;
|
||||||
|
vec3 c1 = instanceTransform[1].xyz;
|
||||||
|
vec3 c2 = instanceTransform[2].xyz;
|
||||||
|
vec3 k0 = cross(c1, c2);
|
||||||
|
mat3 normalMat = mat3(k0, cross(c2, c0), cross(c0, c1)); // cofactor matrix: det * inverse-transpose
|
||||||
|
if (dot(c0, k0) < 0.0) normalMat = -normalMat;
|
||||||
|
fragNormal = normalize(normalMat*vertexNormal);
|
||||||
|
vec4 world = instanceTransform*vec4(vertexPosition, 1.0);
|
||||||
|
fragWorldPos = world.xyz;
|
||||||
|
fragColor = vertexColor*instanceColor;
|
||||||
|
fragUV = vertexTexCoord;
|
||||||
|
fragMaterial = instanceMaterial;
|
||||||
|
gl_Position = viewProj*world;
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
inline const char* const kLitFragmentShader = R"GLSL(#version 330
|
||||||
|
#define MAX_LIGHTS 16
|
||||||
|
const float PI = 3.14159265359;
|
||||||
|
|
||||||
|
in vec3 fragWorldPos;
|
||||||
|
in vec3 fragNormal;
|
||||||
|
in vec4 fragColor;
|
||||||
|
in vec2 fragUV;
|
||||||
|
flat in vec4 fragMaterial;
|
||||||
|
|
||||||
|
uniform vec3 camPos;
|
||||||
|
uniform int lightCount;
|
||||||
|
uniform vec4 lightPosType[MAX_LIGHTS]; // xyz: position (point/spot) or direction TO the light (sun); w: 0 sun, 1 point, 2 spot
|
||||||
|
uniform vec4 lightColorRange[MAX_LIGHTS]; // rgb: linear colour * intensity; w: range
|
||||||
|
uniform vec4 lightSpot[MAX_LIGHTS]; // xyz: the way a spot points; w: cos of the outer half angle
|
||||||
|
uniform vec4 lightParams[MAX_LIGHTS]; // x: 1 / (cos inner - cos outer); y: shadow slot (-1 none); z: shadow bias multiplier
|
||||||
|
|
||||||
|
uniform sampler2D shadowMap0;
|
||||||
|
uniform sampler2D shadowMap1;
|
||||||
|
uniform sampler2D shadowMap2;
|
||||||
|
uniform mat4 shadowMatrix0;
|
||||||
|
uniform mat4 shadowMatrix1;
|
||||||
|
uniform mat4 shadowMatrix2;
|
||||||
|
uniform vec4 shadowInfo0; // x: texel size in world units (spot: at distance 1); y: 1 = spot; z: depth range; w: 1 / resolution
|
||||||
|
uniform vec4 shadowInfo1;
|
||||||
|
uniform vec4 shadowInfo2;
|
||||||
|
uniform vec4 shadowParams; // x: strength; y: softness (texels); z: sun shadow distance
|
||||||
|
|
||||||
|
uniform vec3 ambSkyGlobal; // linear
|
||||||
|
uniform vec3 ambGroundGlobal;
|
||||||
|
uniform sampler2D ambSkyTex; // baked ambient zones: one tile per Y slice, gamma-encoded
|
||||||
|
uniform sampler2D ambGroundTex;
|
||||||
|
uniform vec4 ambVolOrigin; // xyz: grid origin; w: cell size
|
||||||
|
uniform vec4 ambVolDims; // xyz: cells; w: tiles per atlas row (x < 0.5: no zones)
|
||||||
|
uniform float ambColorScale;
|
||||||
|
|
||||||
|
uniform vec4 envParams; // x: exposure; y: tone mapping (0 none, 1 Reinhard, 2 ACES); z: fog density
|
||||||
|
uniform vec3 fogColor; // linear
|
||||||
|
|
||||||
|
// ---- the material of this draw call (see material.hpp) ----
|
||||||
|
uniform int matMode; // 0: plain (roughness / metallic / emission per instance, no maps); 1: a Material
|
||||||
|
uniform int matMaps; // which maps are set: 1 albedo, 2 diffuse, 4 specular, 8 metalness, 16 roughness, 32 normal
|
||||||
|
uniform vec4 matColor; // rgb: albedo (gamma-encoded)
|
||||||
|
uniform vec4 matSurface; // x: diffuse; y: specular level; z: metalness; w: roughness
|
||||||
|
uniform vec4 matExtra; // x: normal strength; y: emission; z: 1 = project the textures from the world, 0 = mesh UVs
|
||||||
|
uniform vec4 matSpecColor; // rgb: specular colour (gamma-encoded)
|
||||||
|
uniform vec4 matUv; // xy: texture coordinate scale; zw: offset
|
||||||
|
uniform sampler2D matAlbedoMap;
|
||||||
|
uniform sampler2D matDiffuseMap;
|
||||||
|
uniform sampler2D matSpecularMap;
|
||||||
|
uniform sampler2D matMetalnessMap;
|
||||||
|
uniform sampler2D matRoughnessMap;
|
||||||
|
uniform sampler2D matNormalMap;
|
||||||
|
|
||||||
|
out vec4 finalColor;
|
||||||
|
|
||||||
|
vec3 toLinear(vec3 c) { return pow(c, vec3(2.2)); }
|
||||||
|
vec3 toSrgb(vec3 c) { return pow(c, vec3(1.0/2.2)); }
|
||||||
|
|
||||||
|
// ---- ambient (hemisphere, optionally overridden by baked zones) ----
|
||||||
|
vec2 tileUV(vec2 xz, float slice, vec3 dims, float tilesX) {
|
||||||
|
float row = floor(slice/tilesX);
|
||||||
|
float col = slice - row*tilesX;
|
||||||
|
vec2 atlas = vec2(dims.x*tilesX, dims.z*ceil(dims.y/tilesX));
|
||||||
|
return (vec2(col*dims.x, row*dims.z) + xz)/atlas;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ambientColors(vec3 wp, out vec3 skyC, out vec3 groundC) {
|
||||||
|
skyC = ambSkyGlobal;
|
||||||
|
groundC = ambGroundGlobal;
|
||||||
|
if (ambVolDims.x < 0.5) return;
|
||||||
|
vec3 dims = ambVolDims.xyz;
|
||||||
|
vec3 g = clamp((wp - ambVolOrigin.xyz)/ambVolOrigin.w - 0.5, vec3(0.0), dims - 1.0);
|
||||||
|
float y0 = floor(g.y);
|
||||||
|
float ty = g.y - y0;
|
||||||
|
float y1 = min(y0 + 1.0, dims.y - 1.0);
|
||||||
|
vec2 xz = g.xz + 0.5;
|
||||||
|
float tilesX = ambVolDims.w;
|
||||||
|
vec2 uv0 = tileUV(xz, y0, dims, tilesX);
|
||||||
|
vec2 uv1 = tileUV(xz, y1, dims, tilesX);
|
||||||
|
vec3 s = mix(textureLod(ambSkyTex, uv0, 0.0).rgb, textureLod(ambSkyTex, uv1, 0.0).rgb, ty);
|
||||||
|
vec3 gr = mix(textureLod(ambGroundTex, uv0, 0.0).rgb, textureLod(ambGroundTex, uv1, 0.0).rgb, ty);
|
||||||
|
skyC = pow(s, vec3(2.2))*ambColorScale;
|
||||||
|
groundC = pow(gr, vec3(2.2))*ambColorScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- shadows ----
|
||||||
|
mat4 shadowMatrixOf(int slot) {
|
||||||
|
if (slot == 0) return shadowMatrix0;
|
||||||
|
if (slot == 1) return shadowMatrix1;
|
||||||
|
return shadowMatrix2;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 shadowInfoOf(int slot) {
|
||||||
|
if (slot == 0) return shadowInfo0;
|
||||||
|
if (slot == 1) return shadowInfo1;
|
||||||
|
return shadowInfo2;
|
||||||
|
}
|
||||||
|
|
||||||
|
float shadowDepthAt(int slot, vec2 uv) {
|
||||||
|
vec3 enc;
|
||||||
|
if (slot == 0) enc = textureLod(shadowMap0, uv, 0.0).rgb;
|
||||||
|
else if (slot == 1) enc = textureLod(shadowMap1, uv, 0.0).rgb;
|
||||||
|
else enc = textureLod(shadowMap2, uv, 0.0).rgb;
|
||||||
|
return dot(enc, vec3(1.0, 1.0/255.0, 1.0/65025.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 = fully lit, 0 = fully shadowed
|
||||||
|
float shadowFactor(int slot, vec3 wp, vec3 N, float NdL, vec3 lightPos, float biasMul) {
|
||||||
|
mat4 M = shadowMatrixOf(slot);
|
||||||
|
vec4 info = shadowInfoOf(slot);
|
||||||
|
bool radial = info.y > 0.5;
|
||||||
|
float texel = radial ? info.x*length(wp - lightPos) : info.x;
|
||||||
|
vec3 pos = wp + N*(texel*(1.5 + 2.0*(1.0 - NdL))*biasMul); // normal offset: no acne, no light leaks
|
||||||
|
vec4 sc = M*vec4(pos, 1.0);
|
||||||
|
if (sc.w <= 0.0) return 1.0;
|
||||||
|
vec3 p = sc.xyz/sc.w*0.5 + 0.5;
|
||||||
|
if (p.x < 0.0 || p.x > 1.0 || p.y < 0.0 || p.y > 1.0 || p.z < 0.0 || p.z > 1.0) return 1.0;
|
||||||
|
float receiver = radial ? length(pos - lightPos)/info.z : p.z;
|
||||||
|
float bias = (texel + 0.004)*biasMul/info.z;
|
||||||
|
float noise = fract(52.9829189*fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
||||||
|
float rot = noise*6.2831853;
|
||||||
|
float radius = shadowParams.y*info.w;
|
||||||
|
float lit = 0.0;
|
||||||
|
for (int i = 0; i < 12; ++i) { // a rotated Vogel disk: smooth penumbrae with few taps
|
||||||
|
float r = sqrt((float(i) + 0.5)/12.0);
|
||||||
|
float a = float(i)*2.39996323 + rot;
|
||||||
|
vec2 o = vec2(cos(a), sin(a))*(r*radius);
|
||||||
|
float stored = shadowDepthAt(slot, p.xy + o);
|
||||||
|
lit += (receiver - bias <= stored) ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
return lit/12.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- material textures ----
|
||||||
|
// Texture coordinates: the mesh's own, or three projections from the world (triplanar) blended by how much the surface
|
||||||
|
// faces each axis. A projection looks along its axis at the surface and has u to the right and v DOWN the image (the top
|
||||||
|
// edge of an image is v = 0), whichever side the surface faces, so a texture is never mirrored.
|
||||||
|
vec2 gUvMesh;
|
||||||
|
vec2 gUvX; // the surfaces that face +-x: u along -z (+z when facing -x), v = -y
|
||||||
|
vec2 gUvY; // +-y: u = x, v = z (-z when facing down)
|
||||||
|
vec2 gUvZ; // +-z: u = x (-x when facing -z), v = -y
|
||||||
|
vec3 gTri; // blend weights of the three projections
|
||||||
|
vec3 gSign; // which way the surface faces on each axis, +1 or -1
|
||||||
|
|
||||||
|
void materialUv(vec3 wp, vec3 N) {
|
||||||
|
gUvMesh = fragUV*matUv.xy + matUv.zw;
|
||||||
|
gSign = vec3(N.x >= 0.0 ? 1.0 : -1.0, N.y >= 0.0 ? 1.0 : -1.0, N.z >= 0.0 ? 1.0 : -1.0);
|
||||||
|
vec3 w = pow(abs(N), vec3(4.0));
|
||||||
|
gTri = w/(w.x + w.y + w.z);
|
||||||
|
gUvX = vec2(-wp.z*gSign.x, -wp.y)*matUv.xy + matUv.zw;
|
||||||
|
gUvY = vec2(wp.x, wp.z*gSign.y)*matUv.xy + matUv.zw;
|
||||||
|
gUvZ = vec2(wp.x*gSign.z, -wp.y)*matUv.xy + matUv.zw;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 materialSample(sampler2D t) {
|
||||||
|
if (matExtra.z < 0.5) return texture(t, gUvMesh);
|
||||||
|
return texture(t, gUvX)*gTri.x + texture(t, gUvY)*gTri.y + texture(t, gUvZ)*gTri.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tilts the normal by the normal map. A map normal (x, y, z) leans the surface by the slope (x, y)/z towards the image's
|
||||||
|
// right (x) and its top (y); the strength scales the slope. A flat map ((0, 0, 1)) leaves the normal alone, and on a
|
||||||
|
// surface that faces an axis the result is exactly the tangent-space normal of the map.
|
||||||
|
vec3 materialNormal(vec3 N, vec3 wp) {
|
||||||
|
if (matExtra.z < 0.5) { // the mesh's UVs: the frame comes from the screen-space derivatives
|
||||||
|
vec3 dp1 = dFdx(wp);
|
||||||
|
vec3 dp2 = dFdy(wp);
|
||||||
|
vec2 duv1 = dFdx(gUvMesh);
|
||||||
|
vec2 duv2 = dFdy(gUvMesh);
|
||||||
|
vec3 dp2perp = cross(dp2, N);
|
||||||
|
vec3 dp1perp = cross(N, dp1);
|
||||||
|
vec3 T = dp2perp*duv1.x + dp1perp*duv2.x; // towards increasing u
|
||||||
|
vec3 Bd = dp2perp*duv1.y + dp1perp*duv2.y; // towards increasing v: DOWN the image
|
||||||
|
float m = max(dot(T, T), dot(Bd, Bd));
|
||||||
|
if (m < 1e-20) return N;
|
||||||
|
vec3 tn = texture(matNormalMap, gUvMesh).xyz*2.0 - 1.0;
|
||||||
|
vec2 slope = tn.xy/max(tn.z, 0.05)*matExtra.x;
|
||||||
|
return normalize(N + (slope.x*T - slope.y*Bd)*inversesqrt(m));
|
||||||
|
}
|
||||||
|
vec3 tx = texture(matNormalMap, gUvX).xyz*2.0 - 1.0;
|
||||||
|
vec3 ty = texture(matNormalMap, gUvY).xyz*2.0 - 1.0;
|
||||||
|
vec3 tz = texture(matNormalMap, gUvZ).xyz*2.0 - 1.0;
|
||||||
|
vec2 sx = tx.xy/max(tx.z, 0.05)*matExtra.x;
|
||||||
|
vec2 sy = ty.xy/max(ty.z, 0.05)*matExtra.x;
|
||||||
|
vec2 sz = tz.xy/max(tz.z, 0.05)*matExtra.x;
|
||||||
|
vec3 dX = sx.x*vec3(0.0, 0.0, -gSign.x) + sx.y*vec3(0.0, 1.0, 0.0);
|
||||||
|
vec3 dY = sy.x*vec3(1.0, 0.0, 0.0) + sy.y*vec3(0.0, 0.0, -gSign.y);
|
||||||
|
vec3 dZ = sz.x*vec3(gSign.z, 0.0, 0.0) + sz.y*vec3(0.0, 1.0, 0.0);
|
||||||
|
return normalize(N + dX*gTri.x + dY*gTri.y + dZ*gTri.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int flags = int(fragMaterial.w*255.0 + 0.5);
|
||||||
|
vec3 Ng = normalize(fragNormal); // the geometry's normal (shadows bias along it)
|
||||||
|
vec3 N = Ng; // the shading normal (a normal map tilts it)
|
||||||
|
vec3 baseG = fragColor.rgb; // gamma-encoded colour: vertex colours * tint * material
|
||||||
|
float opacity = fragColor.a;
|
||||||
|
float roughnessIn = fragMaterial.x;
|
||||||
|
float metallic = fragMaterial.y;
|
||||||
|
float emission = fragMaterial.z*4.0;
|
||||||
|
float diffuseK = 1.0;
|
||||||
|
float specLevel = 0.5;
|
||||||
|
vec3 specTint = vec3(1.0);
|
||||||
|
if (matMode == 1) {
|
||||||
|
materialUv(fragWorldPos, Ng);
|
||||||
|
baseG *= matColor.rgb;
|
||||||
|
if ((matMaps & 1) != 0) {
|
||||||
|
vec4 t = materialSample(matAlbedoMap);
|
||||||
|
baseG *= t.rgb;
|
||||||
|
opacity *= t.a;
|
||||||
|
}
|
||||||
|
diffuseK = matSurface.x;
|
||||||
|
specLevel = matSurface.y;
|
||||||
|
metallic = matSurface.z;
|
||||||
|
roughnessIn = matSurface.w;
|
||||||
|
emission = matExtra.y;
|
||||||
|
specTint = toLinear(matSpecColor.rgb);
|
||||||
|
if ((matMaps & 2) != 0) diffuseK *= materialSample(matDiffuseMap).r;
|
||||||
|
if ((matMaps & 4) != 0) specLevel *= materialSample(matSpecularMap).r;
|
||||||
|
if ((matMaps & 8) != 0) metallic *= materialSample(matMetalnessMap).r;
|
||||||
|
if ((matMaps & 16) != 0) roughnessIn *= materialSample(matRoughnessMap).r;
|
||||||
|
if ((matMaps & 32) != 0) N = materialNormal(Ng, fragWorldPos);
|
||||||
|
}
|
||||||
|
if ((flags & 2) != 0) { // unlit: the plain colour
|
||||||
|
finalColor = vec4(baseG, opacity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool receive = (flags & 1) != 0;
|
||||||
|
|
||||||
|
vec3 toCam = camPos - fragWorldPos;
|
||||||
|
float camDist = length(toCam);
|
||||||
|
vec3 V = toCam/max(camDist, 0.0001);
|
||||||
|
vec3 albedo = toLinear(baseG);
|
||||||
|
float roughness = clamp(roughnessIn, 0.05, 1.0);
|
||||||
|
vec3 F0 = mix(vec3(0.08)*specLevel*specTint, albedo, metallic); // 0.08 * 0.5 = the 4% of plastic and glass
|
||||||
|
vec3 diffuseColor = albedo*(1.0 - metallic)*diffuseK;
|
||||||
|
float NdV = max(dot(N, V), 0.0001);
|
||||||
|
float a2 = roughness*roughness*roughness*roughness;
|
||||||
|
float k = (roughness + 1.0)*(roughness + 1.0)/8.0;
|
||||||
|
|
||||||
|
vec3 Lo = vec3(0.0);
|
||||||
|
for (int i = 0; i < lightCount; ++i) {
|
||||||
|
vec4 pt = lightPosType[i];
|
||||||
|
vec4 cr = lightColorRange[i];
|
||||||
|
vec4 sp = lightSpot[i];
|
||||||
|
vec4 lp = lightParams[i];
|
||||||
|
int type = int(pt.w + 0.5);
|
||||||
|
vec3 L;
|
||||||
|
float atten = 1.0;
|
||||||
|
if (type == 0) {
|
||||||
|
L = pt.xyz;
|
||||||
|
} else {
|
||||||
|
vec3 d = pt.xyz - fragWorldPos;
|
||||||
|
float dist = length(d);
|
||||||
|
L = d/max(dist, 0.0001);
|
||||||
|
float x = dist/cr.w;
|
||||||
|
float window = clamp(1.0 - x*x*x*x, 0.0, 1.0); // reaches exactly zero at the range
|
||||||
|
atten = window*window/max(dist*dist, 1.0); // inverse square, intensity = brightness at 1 unit
|
||||||
|
if (type == 2) {
|
||||||
|
float cd = dot(-L, sp.xyz);
|
||||||
|
float spot = clamp((cd - sp.w)*lp.x, 0.0, 1.0);
|
||||||
|
atten *= spot*spot*(3.0 - 2.0*spot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float NdL = max(dot(N, L), 0.0);
|
||||||
|
if (atten <= 0.0 || NdL <= 0.0) continue;
|
||||||
|
|
||||||
|
float lit = 1.0;
|
||||||
|
int slot = int(floor(lp.y + 0.5));
|
||||||
|
if (slot >= 0 && receive) {
|
||||||
|
lit = shadowFactor(slot, fragWorldPos, Ng, max(dot(Ng, L), 0.0), pt.xyz, lp.z);
|
||||||
|
if (type == 0) lit = mix(1.0, lit, 1.0 - smoothstep(0.85, 1.0, camDist/shadowParams.z));
|
||||||
|
lit = mix(1.0, lit, shadowParams.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 H = normalize(V + L);
|
||||||
|
float NdH = max(dot(N, H), 0.0);
|
||||||
|
float VdH = max(dot(V, H), 0.0);
|
||||||
|
float dd = NdH*NdH*(a2 - 1.0) + 1.0;
|
||||||
|
float D = a2/(PI*dd*dd); // GGX
|
||||||
|
float G = (NdL/(NdL*(1.0 - k) + k))*(NdV/(NdV*(1.0 - k) + k)); // Smith / Schlick-GGX
|
||||||
|
vec3 F = F0 + (1.0 - F0)*pow(1.0 - VdH, 5.0); // Schlick Fresnel
|
||||||
|
vec3 specular = D*G*F/max(4.0*NdV*NdL, 0.0001);
|
||||||
|
vec3 radiance = cr.rgb*(atten*lit*PI); // intensity 1 shows a surface facing it in its plain colour
|
||||||
|
Lo += ((1.0 - F)*diffuseColor/PI + specular)*radiance*NdL;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 skyA;
|
||||||
|
vec3 groundA;
|
||||||
|
ambientColors(fragWorldPos, skyA, groundA);
|
||||||
|
vec3 color = Lo + mix(groundA, skyA, N.y*0.5 + 0.5)*diffuseColor;
|
||||||
|
vec3 R = reflect(-V, N);
|
||||||
|
vec3 Fr = F0 + (max(vec3(1.0 - roughness), F0) - F0)*pow(1.0 - NdV, 5.0);
|
||||||
|
color += mix(groundA, skyA, R.y*0.5 + 0.5)*Fr*(1.0 - 0.7*roughness);
|
||||||
|
color += albedo*emission;
|
||||||
|
|
||||||
|
if (envParams.z > 0.0) { // exponential-squared fog
|
||||||
|
float f = 1.0 - exp(-pow(envParams.z*camDist, 2.0));
|
||||||
|
color = mix(color, fogColor, f);
|
||||||
|
}
|
||||||
|
color *= envParams.x;
|
||||||
|
int mode = int(envParams.y + 0.5);
|
||||||
|
if (mode == 1) color = color/(color + 1.0);
|
||||||
|
else if (mode == 2) color = clamp((color*(2.51*color + 0.03))/(color*(2.43*color + 0.59) + 0.14), 0.0, 1.0);
|
||||||
|
else color = clamp(color, 0.0, 1.0);
|
||||||
|
finalColor = vec4(toSrgb(color), opacity);
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
inline const char* const kShadowVertexShader = R"GLSL(#version 330
|
||||||
|
layout(location = 0) in vec3 vertexPosition;
|
||||||
|
layout(location = 9) in mat4 instanceTransform;
|
||||||
|
uniform mat4 viewProj;
|
||||||
|
out vec3 fragWorld;
|
||||||
|
void main() {
|
||||||
|
vec4 world = instanceTransform*vec4(vertexPosition, 1.0);
|
||||||
|
fragWorld = world.xyz;
|
||||||
|
gl_Position = viewProj*world;
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
inline const char* const kShadowFragmentShader = R"GLSL(#version 330
|
||||||
|
in vec3 fragWorld;
|
||||||
|
uniform vec4 shadowCast; // x: 0 = depth along the light, 1 = distance from the light; y: range
|
||||||
|
uniform vec3 lightPos;
|
||||||
|
out vec4 finalColor;
|
||||||
|
vec3 packDepth(float d) { // 24 bits in RGB (alpha stays 1, so blending cannot touch it)
|
||||||
|
d = clamp(d, 0.0, 0.99999);
|
||||||
|
vec3 enc = fract(vec3(1.0, 255.0, 65025.0)*d);
|
||||||
|
enc -= enc.yzz*vec3(1.0/255.0, 1.0/255.0, 0.0);
|
||||||
|
return enc;
|
||||||
|
}
|
||||||
|
void main() {
|
||||||
|
float d = shadowCast.x < 0.5 ? gl_FragCoord.z : length(fragWorld - lightPos)/shadowCast.y;
|
||||||
|
finalColor = vec4(packDepth(d), 1.0);
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
} // namespace eng::detail
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
#include "engine/texture.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cmath>
|
||||||
|
#include <mutex>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace eng {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int kMaxSide = 16384;
|
||||||
|
|
||||||
|
std::mutex& loaderMutex() { static std::mutex m; return m; }
|
||||||
|
ImageLoader& loaderSlot() { static ImageLoader l; return l; }
|
||||||
|
|
||||||
|
int wrapIndex(int i, int n, TextureWrap w) {
|
||||||
|
if (w == TextureWrap::Clamp) return std::clamp(i, 0, n - 1);
|
||||||
|
i %= n;
|
||||||
|
return i < 0 ? i + n : i;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Texture::Texture(int width, int height, std::vector<std::uint8_t> rgba, TextureOptions options)
|
||||||
|
: width_(width), height_(height), pixels_(std::move(rgba)), options_(options) {
|
||||||
|
if (width < 1 || height < 1 || width > kMaxSide || height > kMaxSide)
|
||||||
|
throw std::invalid_argument("Texture: the size must be 1.." + std::to_string(kMaxSide) + " pixels on each side");
|
||||||
|
if (pixels_.size() != static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4)
|
||||||
|
throw std::invalid_argument("Texture: expected width * height * 4 bytes of RGBA8 pixels");
|
||||||
|
static std::atomic<std::uint64_t> counter{1};
|
||||||
|
id_ = counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
TexturePtr Texture::create(int width, int height, std::vector<std::uint8_t> rgba, TextureOptions options) {
|
||||||
|
return std::make_shared<const Texture>(width, height, std::move(rgba), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
TexturePtr Texture::solid(Rgba color, TextureOptions options) {
|
||||||
|
return create(1, 1, {color.r, color.g, color.b, color.a}, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
TexturePtr Texture::checker(int size, int cells, Rgba a, Rgba b, TextureOptions options) {
|
||||||
|
size = std::clamp(size, 1, 4096);
|
||||||
|
cells = std::clamp(cells, 1, size);
|
||||||
|
std::vector<std::uint8_t> px(static_cast<std::size_t>(size) * static_cast<std::size_t>(size) * 4);
|
||||||
|
for (int y = 0; y < size; ++y)
|
||||||
|
for (int x = 0; x < size; ++x) {
|
||||||
|
const bool odd = ((x * cells / size) + (y * cells / size)) % 2 != 0;
|
||||||
|
const Rgba c = odd ? b : a;
|
||||||
|
std::uint8_t* p = &px[(static_cast<std::size_t>(y) * static_cast<std::size_t>(size) + static_cast<std::size_t>(x)) * 4];
|
||||||
|
p[0] = c.r; p[1] = c.g; p[2] = c.b; p[3] = c.a;
|
||||||
|
}
|
||||||
|
return create(size, size, std::move(px), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
TexturePtr Texture::normalMapFromHeight(const Texture& height, float strength) {
|
||||||
|
const int w = height.width(), h = height.height();
|
||||||
|
if (!std::isfinite(strength)) strength = 3.0f;
|
||||||
|
auto high = [&](int x, int y) {
|
||||||
|
x = wrapIndex(x, w, TextureWrap::Repeat);
|
||||||
|
y = wrapIndex(y, h, TextureWrap::Repeat);
|
||||||
|
return static_cast<float>(height.pixels()[(static_cast<std::size_t>(y) * static_cast<std::size_t>(w) + static_cast<std::size_t>(x)) * 4]) / 255.0f;
|
||||||
|
};
|
||||||
|
std::vector<std::uint8_t> px(static_cast<std::size_t>(w) * static_cast<std::size_t>(h) * 4);
|
||||||
|
for (int y = 0; y < h; ++y)
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
// slope towards +u (to the right) and towards the top of the image (y - 1): the normal leans away from rising ground
|
||||||
|
const float dx = (high(x + 1, y) - high(x - 1, y)) * 0.5f * strength;
|
||||||
|
const float dy = (high(x, y - 1) - high(x, y + 1)) * 0.5f * strength;
|
||||||
|
const Vec3 n = Vec3{-dx, -dy, 1.0f}.normalized();
|
||||||
|
std::uint8_t* p = &px[(static_cast<std::size_t>(y) * static_cast<std::size_t>(w) + static_cast<std::size_t>(x)) * 4];
|
||||||
|
p[0] = static_cast<std::uint8_t>(std::lround((n.x * 0.5f + 0.5f) * 255.0f));
|
||||||
|
p[1] = static_cast<std::uint8_t>(std::lround((n.y * 0.5f + 0.5f) * 255.0f));
|
||||||
|
p[2] = static_cast<std::uint8_t>(std::lround((n.z * 0.5f + 0.5f) * 255.0f));
|
||||||
|
p[3] = 255;
|
||||||
|
}
|
||||||
|
return create(w, h, std::move(px), TextureOptions{height.options().wrap, height.options().filter});
|
||||||
|
}
|
||||||
|
|
||||||
|
Rgba Texture::texel(int x, int y) const {
|
||||||
|
x = wrapIndex(x, width_, options_.wrap);
|
||||||
|
y = wrapIndex(y, height_, options_.wrap);
|
||||||
|
const std::uint8_t* p = &pixels_[(static_cast<std::size_t>(y) * static_cast<std::size_t>(width_) + static_cast<std::size_t>(x)) * 4];
|
||||||
|
return Rgba{p[0], p[1], p[2], p[3]};
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture::Sample Texture::sample(Vec2 uv) const {
|
||||||
|
// texel centres at (i + 0.5) / n, like the GPU
|
||||||
|
const float fx = uv.x * static_cast<float>(width_) - 0.5f, fy = uv.y * static_cast<float>(height_) - 0.5f;
|
||||||
|
const float x0 = std::floor(fx), y0 = std::floor(fy);
|
||||||
|
const float tx = fx - x0, ty = fy - y0;
|
||||||
|
const int ix = static_cast<int>(x0), iy = static_cast<int>(y0);
|
||||||
|
const Rgba c00 = texel(ix, iy), c10 = texel(ix + 1, iy), c01 = texel(ix, iy + 1), c11 = texel(ix + 1, iy + 1);
|
||||||
|
auto mix = [&](std::uint8_t a, std::uint8_t b, std::uint8_t c, std::uint8_t d) {
|
||||||
|
const float top = static_cast<float>(a) * (1 - tx) + static_cast<float>(b) * tx;
|
||||||
|
const float bottom = static_cast<float>(c) * (1 - tx) + static_cast<float>(d) * tx;
|
||||||
|
return (top * (1 - ty) + bottom * ty) / 255.0f;
|
||||||
|
};
|
||||||
|
return {mix(c00.r, c10.r, c01.r, c11.r), mix(c00.g, c10.g, c01.g, c11.g), mix(c00.b, c10.b, c01.b, c11.b), mix(c00.a, c10.a, c01.a, c11.a)};
|
||||||
|
}
|
||||||
|
|
||||||
|
void setImageLoader(ImageLoader loader) {
|
||||||
|
std::lock_guard<std::mutex> lock(loaderMutex());
|
||||||
|
loaderSlot() = std::move(loader);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasImageLoader() {
|
||||||
|
std::lock_guard<std::mutex> lock(loaderMutex());
|
||||||
|
return static_cast<bool>(loaderSlot());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageData loadImageFile(const std::string& path) {
|
||||||
|
ImageLoader loader;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(loaderMutex());
|
||||||
|
loader = loaderSlot();
|
||||||
|
}
|
||||||
|
if (!loader) throw std::runtime_error("no image decoder is installed (eng::run() installs one; see setImageLoader), cannot read " + path);
|
||||||
|
ImageData img;
|
||||||
|
if (!loader(path, img)) throw std::runtime_error("cannot read the image " + path);
|
||||||
|
if (img.width < 1 || img.height < 1 || img.rgba.size() != static_cast<std::size_t>(img.width) * static_cast<std::size_t>(img.height) * 4)
|
||||||
|
throw std::runtime_error("the decoder returned an invalid image for " + path);
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eng
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#include "engine/graphics.hpp"
|
||||||
|
|
||||||
|
namespace eng {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Does any ancestor of `e` hide the entities below it? Only enabled components count.
|
||||||
|
bool hiddenByAncestor(Entity e) {
|
||||||
|
for (Entity p = e.parent(); p.valid(); p = p.parent()) {
|
||||||
|
bool hides = false;
|
||||||
|
forEachRenderable(p, [&](Renderable& r) { hides = hides || (r.componentEnabled() && !r.childrenVisible); });
|
||||||
|
if (hides) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool Renderable::visibleInHierarchy() const { return visible && !hiddenByAncestor(entity()); }
|
||||||
|
|
||||||
|
void Entity::setVisible(bool visible, bool includeChildren) const {
|
||||||
|
bool any = false;
|
||||||
|
forEachRenderable(*this, [&](Renderable& r) {
|
||||||
|
any = true;
|
||||||
|
r.setVisible(visible, includeChildren);
|
||||||
|
});
|
||||||
|
if (!any && !visible) add<Visibility>().setVisible(false, includeChildren); // a plain group carries the switch itself
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Entity::isVisible() const {
|
||||||
|
if (!valid() || !entityActive() || hiddenByAncestor(*this)) return false;
|
||||||
|
bool any = false, shown = false;
|
||||||
|
forEachRenderable(*this, [&](Renderable& r) {
|
||||||
|
any = true;
|
||||||
|
shown = shown || (r.visible && r.componentEnabled());
|
||||||
|
});
|
||||||
|
return any ? shown : true; // an entity without renderers is "visible" unless an ancestor hides it
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eng
|
||||||
+2394
-1
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,539 @@
|
|||||||
|
// gfx_gen, part 3: the wrapper plan and the code for eng::gfx.
|
||||||
|
//
|
||||||
|
// For every raylib function we can express in the engine's terms we emit
|
||||||
|
// RET name(PARAMS) in engine/gfx_api.hpp (declaration, with raylib's own doc comment)
|
||||||
|
// RET name(PARAMS) {...} in gfx_core.cpp (a thin cast-and-forward body)
|
||||||
|
// Names: first letter lower-cased (DrawCircle -> drawCircle; rlgl's rl* names are kept).
|
||||||
|
// Types: Vector2/Vector3/Quaternion/Color/BoundingBox ARE the engine's Vec2/Vec3/Quat/Rgba/Aabb (same layout,
|
||||||
|
// asserted at compile time); a by-value Matrix is the engine's Mat4 (converted element by element); every other
|
||||||
|
// raylib struct is mirrored field for field in eng::gfx and passed through unchanged (also asserted).
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "model.hpp"
|
||||||
|
|
||||||
|
namespace gen {
|
||||||
|
|
||||||
|
constexpr int kGroupRaylib = 0, kGroupRlgl = 1, kGroupRaymath = 2;
|
||||||
|
inline const char* groupName(int g) { return g == kGroupRaylib ? "raylib.h" : g == kGroupRlgl ? "rlgl.h" : "raymath.h"; }
|
||||||
|
inline const char* groupFile(int g) { return g == kGroupRaylib ? "gfx_core.cpp" : g == kGroupRlgl ? "gfx_rlgl.cpp" : "gfx_raymath.cpp"; }
|
||||||
|
inline const char* groupHeader(int g) { return g == kGroupRaylib ? "gfx_api.hpp" : g == kGroupRlgl ? "gfx_rlgl_api.hpp" : "gfx_raymath_api.hpp"; }
|
||||||
|
|
||||||
|
struct Wrapped {
|
||||||
|
const FuncDef* f = nullptr;
|
||||||
|
std::string name; // eng::gfx name
|
||||||
|
std::vector<Res> p; // resolved parameter types
|
||||||
|
std::vector<std::string> pn; // parameter names
|
||||||
|
Res ret;
|
||||||
|
bool special = false; // printf-style: hand-written (traceLog, textFormat)
|
||||||
|
bool strOverload = false; // has a const char* parameter -> also a std::string overload
|
||||||
|
bool engineOwned = false;
|
||||||
|
};
|
||||||
|
struct Skipped {
|
||||||
|
const FuncDef* f = nullptr;
|
||||||
|
std::string why;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::string lowerFirst(std::string s) {
|
||||||
|
if (!s.empty()) s[0] = static_cast<char>(std::tolower(static_cast<unsigned char>(s[0])));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const std::set<std::string>& cppKeywords() {
|
||||||
|
static const std::set<std::string> s = {
|
||||||
|
"alignas", "alignof", "and", "asm", "auto", "bitand", "bitor", "bool", "break", "case", "catch", "char", "class", "compl", "concept",
|
||||||
|
"const", "consteval", "constexpr", "constinit", "continue", "decltype", "default", "delete", "do", "double", "else", "enum",
|
||||||
|
"explicit", "export", "extern", "false", "float", "for", "friend", "goto", "if", "inline", "int", "long", "mutable", "namespace",
|
||||||
|
"new", "noexcept", "not", "nullptr", "operator", "or", "private", "protected", "public", "register", "requires", "return", "short",
|
||||||
|
"signed", "sizeof", "static", "struct", "switch", "template", "this", "throw", "true", "try", "typedef", "typeid", "typename",
|
||||||
|
"union", "unsigned", "using", "virtual", "void", "volatile", "while", "xor",
|
||||||
|
// names that platform headers turn into macros
|
||||||
|
"min", "max", "near", "far", "small", "interface", "IN", "OUT", "ERROR", "DELETE", "NO_ERROR"};
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Plan {
|
||||||
|
public:
|
||||||
|
Plan(ModelData& m, Types& ty, Preprocessor& pp, std::set<int> groups) : m_(m), ty_(ty), pp_(pp), groups_(std::move(groups)) { build(); }
|
||||||
|
|
||||||
|
ModelData& m_;
|
||||||
|
Types& ty_;
|
||||||
|
Preprocessor& pp_;
|
||||||
|
std::set<int> groups_;
|
||||||
|
std::vector<Wrapped> wrapped;
|
||||||
|
std::vector<Skipped> skipped;
|
||||||
|
std::set<std::string> usedNames; // everything declared in eng::gfx
|
||||||
|
std::set<std::string> emittedStructs, emittedEnums, emittedCallbacks; // what actually gets declared
|
||||||
|
|
||||||
|
bool enabled(int g) const { return groups_.count(g) != 0; }
|
||||||
|
|
||||||
|
// ---- type strings --------------------------------------------------------------------------------------
|
||||||
|
static std::string extName(const std::string& n) { return n == "va_list" ? "std::va_list" : "std::" + n; }
|
||||||
|
|
||||||
|
bool isMat4(const Res& r, bool inStruct) const { return !inStruct && ty_.matrixMapped && r.cat == Cat::Struct && r.t.base == "Matrix" && r.t.ptr == 0; }
|
||||||
|
|
||||||
|
std::string gfxBase(const Res& r, bool inStruct) const {
|
||||||
|
switch (r.cat) {
|
||||||
|
case Cat::Void: return "void";
|
||||||
|
case Cat::Builtin: return r.t.base;
|
||||||
|
case Cat::ExtBuiltin: return extName(r.t.base);
|
||||||
|
case Cat::Mapped: return ty_.mapped.at(r.t.base);
|
||||||
|
case Cat::Struct:
|
||||||
|
if (isMat4(r, inStruct)) return "Mat4";
|
||||||
|
return r.t.base;
|
||||||
|
case Cat::Opaque: case Cat::Callback: return r.t.base;
|
||||||
|
case Cat::Enum: return (r.t.ptr == 0 && !inStruct) ? "int" : r.t.base;
|
||||||
|
case Cat::Unknown: break;
|
||||||
|
}
|
||||||
|
return "/*?*/";
|
||||||
|
}
|
||||||
|
std::string gfxType(const Res& r, bool inStruct = false) const {
|
||||||
|
std::string s;
|
||||||
|
if (r.t.ptr > 0 && r.t.constBase) s = "const ";
|
||||||
|
s += gfxBase(r, inStruct);
|
||||||
|
s += std::string(static_cast<size_t>(r.t.ptr), '*');
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
std::string rlBase(const Res& r) const {
|
||||||
|
switch (r.cat) {
|
||||||
|
case Cat::Void: return "void";
|
||||||
|
case Cat::Builtin: return r.t.base;
|
||||||
|
case Cat::ExtBuiltin: return extName(r.t.base);
|
||||||
|
case Cat::Mapped: case Cat::Struct: case Cat::Opaque: case Cat::Callback: case Cat::Enum: return "::" + r.t.base;
|
||||||
|
case Cat::Unknown: break;
|
||||||
|
}
|
||||||
|
return "/*?*/";
|
||||||
|
}
|
||||||
|
std::string rlType(const Res& r) const {
|
||||||
|
std::string s;
|
||||||
|
if (r.t.ptr > 0 && r.t.constBase) s = "const ";
|
||||||
|
s += rlBase(r);
|
||||||
|
s += std::string(static_cast<size_t>(r.t.ptr), '*');
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// gfx value -> what the raylib function is called with
|
||||||
|
std::string toRl(const Res& r, const std::string& e) const {
|
||||||
|
if (r.t.ptr > 0) {
|
||||||
|
if (gfxType(r) == rlType(r)) return e;
|
||||||
|
return "reinterpret_cast<" + rlType(r) + ">(" + e + ")";
|
||||||
|
}
|
||||||
|
switch (r.cat) {
|
||||||
|
case Cat::Builtin: case Cat::ExtBuiltin: return e;
|
||||||
|
case Cat::Mapped: return "std::bit_cast<" + rlType(r) + ">(" + e + ")";
|
||||||
|
case Cat::Struct:
|
||||||
|
if (isMat4(r, false)) return "std::bit_cast<::Matrix>(toMatrix(" + e + "))";
|
||||||
|
return "std::bit_cast<" + rlType(r) + ">(" + e + ")";
|
||||||
|
case Cat::Enum: return "static_cast<" + rlType(r) + ">(" + e + ")";
|
||||||
|
case Cat::Callback: return "reinterpret_cast<" + rlType(r) + ">(" + e + ")";
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
// what the raylib function returned -> gfx value
|
||||||
|
std::string fromRl(const Res& r, const std::string& e) const {
|
||||||
|
if (r.t.ptr > 0) {
|
||||||
|
if (gfxType(r) == rlType(r)) return e;
|
||||||
|
return "reinterpret_cast<" + gfxType(r) + ">(" + e + ")";
|
||||||
|
}
|
||||||
|
switch (r.cat) {
|
||||||
|
case Cat::Builtin: case Cat::ExtBuiltin: return e;
|
||||||
|
case Cat::Mapped: return "std::bit_cast<" + gfxType(r) + ">(" + e + ")";
|
||||||
|
case Cat::Struct:
|
||||||
|
if (isMat4(r, false)) return "toMat4(std::bit_cast<Matrix>(" + e + "))";
|
||||||
|
return "std::bit_cast<" + gfxType(r) + ">(" + e + ")";
|
||||||
|
case Cat::Enum: return "static_cast<int>(" + e + ")";
|
||||||
|
case Cat::Callback: return "reinterpret_cast<" + gfxType(r) + ">(" + e + ")";
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isCString(const Res& r) { return r.cat == Cat::Builtin && r.t.base == "char" && r.t.ptr == 1 && r.t.constBase; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void build() {
|
||||||
|
for (const StructDef& s : m_.structs) if (enabled(s.group) && !s.opaque && s.ok && !ty_.mapped.count(s.name)) emittedStructs.insert(s.name);
|
||||||
|
for (const StructDef& s : m_.structs) if (enabled(s.group) && s.opaque) emittedStructs.insert(s.name);
|
||||||
|
for (const EnumDef& e : m_.enums) if (enabled(e.group) && e.ok && !e.name.empty()) emittedEnums.insert(e.name);
|
||||||
|
for (const CallbackDef& c : m_.callbacks) if (enabled(c.group) && c.ok && ty_.callbackSupported(c)) emittedCallbacks.insert(c.name);
|
||||||
|
|
||||||
|
for (const StructDef& s : m_.structs) if (emittedStructs.count(s.name)) usedNames.insert(s.name);
|
||||||
|
for (const std::string& n : emittedEnums) usedNames.insert(n);
|
||||||
|
for (const std::string& n : emittedCallbacks) usedNames.insert(n);
|
||||||
|
for (const auto& kv : ty_.mapped) usedNames.insert(kv.first);
|
||||||
|
for (const std::string& a : m_.aliasOrder) if (enabled(m_.aliasGroup[a])) usedNames.insert(a);
|
||||||
|
for (const EnumDef& e : m_.enums) if (enabled(e.group) && e.ok) for (const EnumItem& i : e.items) usedNames.insert(i.name);
|
||||||
|
usedNames.insert("Mat4"); usedNames.insert("toMat4"); usedNames.insert("toMatrix"); usedNames.insert("colors");
|
||||||
|
|
||||||
|
for (const FuncDef& f : m_.funcs) if (enabled(f.group)) funcNames_.insert(lowerFirst(f.name));
|
||||||
|
static const std::set<std::string> owned = {"InitWindow", "CloseWindow", "WindowShouldClose", "BeginDrawing", "EndDrawing", "SwapScreenBuffer",
|
||||||
|
"PollInputEvents", "SetTargetFPS", "SetConfigFlags", "BeginMode3D", "EndMode3D", "ClearBackground"};
|
||||||
|
|
||||||
|
for (const FuncDef& f : m_.funcs) {
|
||||||
|
if (!enabled(f.group)) continue;
|
||||||
|
Wrapped w;
|
||||||
|
w.f = &f;
|
||||||
|
w.engineOwned = owned.count(f.name) != 0 && f.group == kGroupRaylib;
|
||||||
|
if (f.variadic) {
|
||||||
|
if (f.name == "TraceLog" || f.name == "TextFormat") {
|
||||||
|
w.special = true;
|
||||||
|
w.name = lowerFirst(f.name);
|
||||||
|
if (usedNames.count(w.name)) { skipped.push_back({&f, "name collision"}); continue; }
|
||||||
|
usedNames.insert(w.name);
|
||||||
|
wrapped.push_back(w);
|
||||||
|
} else {
|
||||||
|
skipped.push_back({&f, "variadic"});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (f.retUnsupported) { skipped.push_back({&f, f.retWhy}); continue; }
|
||||||
|
std::string why;
|
||||||
|
w.ret = ty_.classify(f.ret);
|
||||||
|
if (!w.ret.ok()) why = "return type: " + w.ret.why;
|
||||||
|
if (why.empty() && w.ret.cat == Cat::Opaque) why = "returns an opaque type by value";
|
||||||
|
std::set<std::string> used;
|
||||||
|
for (size_t i = 0; why.empty() && i < f.params.size(); ++i) {
|
||||||
|
const Param& p = f.params[i];
|
||||||
|
if (p.unsupported) { why = "parameter " + std::to_string(i + 1) + ": " + p.why; break; }
|
||||||
|
Res r = ty_.classify(p.type);
|
||||||
|
if (!r.ok()) { why = "parameter " + std::to_string(i + 1) + ": " + r.why; break; }
|
||||||
|
if (r.cat == Cat::Void) { why = "void parameter"; break; }
|
||||||
|
if (r.cat == Cat::ExtBuiltin && r.t.base == "va_list") { why = "va_list parameter"; break; }
|
||||||
|
w.p.push_back(r);
|
||||||
|
w.pn.push_back(paramName(p.name, i, used));
|
||||||
|
if (isCString(r)) w.strOverload = true;
|
||||||
|
}
|
||||||
|
if (!why.empty()) { skipped.push_back({&f, why}); continue; }
|
||||||
|
w.name = lowerFirst(f.name);
|
||||||
|
if (cppKeywords().count(w.name)) w.name += "_";
|
||||||
|
if (usedNames.count(w.name)) { skipped.push_back({&f, "name collision with " + w.name}); continue; }
|
||||||
|
usedNames.insert(w.name);
|
||||||
|
wrapped.push_back(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> funcNames_; // parameter names must not hide any wrapper (the std::string overloads call them)
|
||||||
|
|
||||||
|
std::string paramName(const std::string& raw, size_t idx, std::set<std::string>& used) const {
|
||||||
|
std::string n = raw.empty() ? "a" + std::to_string(idx) : raw;
|
||||||
|
if (cppKeywords().count(n) || pp_.macros.count(n) || usedNames.count(n) || funcNames_.count(n)) n += "_";
|
||||||
|
while (used.count(n)) n += "_";
|
||||||
|
used.insert(n);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Emission
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
inline std::string cppFloatLiteral(double v, bool f32) {
|
||||||
|
char buf[64];
|
||||||
|
std::snprintf(buf, sizeof buf, f32 ? "%.9g" : "%.17g", v);
|
||||||
|
std::string s = buf;
|
||||||
|
if (s.find_first_of(".eEn") == std::string::npos) s += ".0"; // keep it a floating literal
|
||||||
|
if (f32) s += "f";
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string colorConstName(const std::string& macro) {
|
||||||
|
std::string low;
|
||||||
|
for (char c : macro) low += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
for (const char* pre : {"light", "dark", "sky", "ray"}) {
|
||||||
|
const std::string p = pre;
|
||||||
|
if (low.size() > p.size() && low.compare(0, p.size(), p) == 0) {
|
||||||
|
std::string a = p, b = low.substr(p.size());
|
||||||
|
a[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(a[0])));
|
||||||
|
b[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(b[0])));
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
low[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(low[0])));
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApiWriter {
|
||||||
|
public:
|
||||||
|
explicit ApiWriter(const Plan& p) : p_(p), m_(p.m_), ty_(p.ty_) {}
|
||||||
|
|
||||||
|
// ---- the public header of one group ---------------------------------------------------------------------
|
||||||
|
std::string header(int g, const std::string& version) const {
|
||||||
|
std::ostringstream o;
|
||||||
|
o << "// GENERATED by tools/gfx_gen from raylib " << version << " (" << groupName(g) << "). DO NOT EDIT.\n"
|
||||||
|
<< "//\n"
|
||||||
|
<< "// eng::gfx is the engine's equivalent of raylib's API: every function is here under its raylib name with the\n"
|
||||||
|
<< "// first letter lower-cased (DrawCircle -> gfx::drawCircle). Vector2/Vector3/Quaternion/Color/BoundingBox are the\n"
|
||||||
|
<< "// engine's own Vec2/Vec3/Quat/Rgba/Aabb, a by-value Matrix is Mat4, everything else keeps raylib's layout.\n"
|
||||||
|
<< "// raylib itself is never included by game code: do not include <raylib.h> in a file that includes this one.\n"
|
||||||
|
<< "#pragma once\n";
|
||||||
|
const char* guard = g == kGroupRaylib ? "RAYLIB_H" : g == kGroupRlgl ? "RLGL_H" : "RAYMATH_H";
|
||||||
|
o << "#if defined(" << guard << ")\n"
|
||||||
|
<< "#error \"engine/" << groupHeader(g) << " wraps raylib for you: include only the engine headers in this file (qualify names as gfx::...)\"\n"
|
||||||
|
<< "#endif\n\n";
|
||||||
|
o << "#include <engine/math.hpp>\n";
|
||||||
|
if (g != kGroupRaylib) o << "#include <engine/gfx_api.hpp>\n";
|
||||||
|
o << "\n#include <cstddef>\n#include <cstdarg>\n#include <cstdint>\n#include <string>\n\n";
|
||||||
|
if (g == kGroupRaylib)
|
||||||
|
o << "#if defined(__GNUC__) || defined(__clang__)\n#define ENG_GFX_PRINTF(fmtIndex, firstArg) __attribute__((format(printf, fmtIndex, firstArg)))\n"
|
||||||
|
<< "#else\n#define ENG_GFX_PRINTF(fmtIndex, firstArg)\n#endif\n\n";
|
||||||
|
o << "namespace eng::gfx {\n\n";
|
||||||
|
|
||||||
|
if (g == kGroupRaylib) {
|
||||||
|
o << "// raylib's own names for the types the engine already has\n";
|
||||||
|
for (const auto& kv : ty_.mapped) o << "using " << kv.first << " = " << kv.second << ";\n";
|
||||||
|
o << "\n";
|
||||||
|
}
|
||||||
|
opaqueTypes(o, g);
|
||||||
|
enums(o, g);
|
||||||
|
constants(o, g);
|
||||||
|
structs(o, g);
|
||||||
|
if (g == kGroupRaylib) matrixHelpers(o);
|
||||||
|
aliases(o, g);
|
||||||
|
callbacks(o, g);
|
||||||
|
if (g == kGroupRaylib) colors(o);
|
||||||
|
functions(o, g);
|
||||||
|
o << "} // namespace eng::gfx\n";
|
||||||
|
return o.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the cpp of one group ------------------------------------------------------------------------------
|
||||||
|
std::string source(int g, const std::string& version) const {
|
||||||
|
std::ostringstream o;
|
||||||
|
o << "// GENERATED by tools/gfx_gen from raylib " << version << " (" << groupName(g) << "). DO NOT EDIT.\n"
|
||||||
|
<< "// The engine side first: the raylib headers define macros (RED, WHITE, PI, ...) that must not leak into eng::gfx.\n"
|
||||||
|
<< "#include <engine/gfx_api.hpp>\n";
|
||||||
|
if (g != kGroupRaylib) o << "#include <engine/" << groupHeader(g) << ">\n";
|
||||||
|
o << "\n#include <bit>\n#include <cstdarg>\n#include <cstddef>\n#include <cstdio>\n#include <string>\n#include <type_traits>\n\n#include <raylib.h>\n";
|
||||||
|
if (g == kGroupRlgl) o << "#include <rlgl.h>\n";
|
||||||
|
if (g == kGroupRaymath) o << "#include <raymath.h>\n";
|
||||||
|
o << "\n#define GFX_LAYOUT(G, R) static_assert(sizeof(G) == sizeof(R) && alignof(G) == alignof(R) && std::is_trivially_copyable_v<G> && \\\n"
|
||||||
|
<< " std::is_trivially_copyable_v<R>, \"eng::gfx::\" #G \" does not have raylib's layout of \" #R)\n"
|
||||||
|
<< "#define GFX_FIELD(G, R, f) static_assert(offsetof(G, f) == offsetof(R, f) && sizeof(G::f) == sizeof(R::f), \\\n"
|
||||||
|
<< " \"eng::gfx::\" #G \"::\" #f \" is not where raylib's \" #R \" has it\")\n"
|
||||||
|
<< "// fields of plain C types (int, float*, unsigned char[4], ...) must be exactly the type raylib declares: this is what\n"
|
||||||
|
<< "// catches a header read with different #defines (e.g. rlVertexBuffer::indices is unsigned short* on GLES2).\n"
|
||||||
|
<< "#define GFX_FIELD_TYPE(G, R, f) static_assert(std::is_same_v<decltype(G::f), decltype(R::f)>, \\\n"
|
||||||
|
<< " \"eng::gfx::\" #G \"::\" #f \" does not have the type raylib's \" #R \" gives it\")\n\n";
|
||||||
|
o << "namespace eng::gfx {\nnamespace {\n\n";
|
||||||
|
layoutChecks(o, g);
|
||||||
|
o << "} // namespace\n\n";
|
||||||
|
for (const Wrapped& w : p_.wrapped) {
|
||||||
|
if (w.f->group != g) continue;
|
||||||
|
if (w.special) { special(o, w); continue; }
|
||||||
|
definition(o, w);
|
||||||
|
}
|
||||||
|
o << "} // namespace eng::gfx\n";
|
||||||
|
return o.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Plan& p_;
|
||||||
|
const ModelData& m_;
|
||||||
|
const Types& ty_;
|
||||||
|
|
||||||
|
bool mine(int group, int g) const { return group == g; }
|
||||||
|
|
||||||
|
void opaqueTypes(std::ostream& o, int g) const {
|
||||||
|
bool any = false;
|
||||||
|
for (const StructDef& s : m_.structs)
|
||||||
|
if (s.group == g && s.opaque && p_.emittedStructs.count(s.name)) { o << "struct " << s.name << ";\n"; any = true; }
|
||||||
|
if (any) o << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void enums(std::ostream& o, int g) const {
|
||||||
|
std::set<std::string> seen;
|
||||||
|
for (const EnumDef& e : m_.enums) {
|
||||||
|
if (e.group != g || !e.ok || e.items.empty()) continue;
|
||||||
|
o << "enum " << (e.name.empty() ? "" : e.name + " ") << ": int {\n";
|
||||||
|
for (const EnumItem& i : e.items) o << " " << i.name << " = " << i.value << ",\n";
|
||||||
|
o << "};\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void constants(std::ostream& o, int g) const {
|
||||||
|
bool any = false;
|
||||||
|
for (const ConstDef& c : m_.consts) {
|
||||||
|
if (c.group != g || p_.usedNames.count(c.name)) continue;
|
||||||
|
if (!c.str.empty()) o << "inline constexpr const char* " << c.name << " = " << c.str << ";\n";
|
||||||
|
else if (c.value.isFloat) o << "inline constexpr " << (c.isFloat32 ? "float " : "double ") << c.name << " = " << cppFloatLiteral(c.value.d, c.isFloat32) << ";\n";
|
||||||
|
else if (c.value.i > 2147483647LL || c.value.i < -2147483648LL) o << "inline constexpr long long " << c.name << " = " << c.value.i << "LL;\n";
|
||||||
|
else o << "inline constexpr int " << c.name << " = " << c.value.i << ";\n";
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (any) o << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void structs(std::ostream& o, int g) const {
|
||||||
|
for (const StructDef& s : m_.structs) {
|
||||||
|
if (s.group != g || s.opaque || !s.ok || ty_.mapped.count(s.name)) continue;
|
||||||
|
o << "struct " << s.name << " {\n";
|
||||||
|
for (const Field& f : s.fields) {
|
||||||
|
const Res r = ty_.classify(f.type);
|
||||||
|
o << " " << p_.gfxType(r, true) << " " << f.name;
|
||||||
|
for (long long d : f.dims) o << "[" << d << "]";
|
||||||
|
o << ";\n";
|
||||||
|
}
|
||||||
|
o << "};\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void matrixHelpers(std::ostream& o) const {
|
||||||
|
if (!ty_.matrixMapped) return;
|
||||||
|
o << "// raylib's Matrix lists its fields row by row (m0, m4, m8, m12, m1, ...); the engine's Mat4 stores element i in m[i].\n"
|
||||||
|
<< "// Both mean the same matrix when element i is m<i>; these two functions convert by that rule.\n"
|
||||||
|
<< "inline Mat4 toMat4(const Matrix& r) {\n Mat4 m;\n";
|
||||||
|
for (size_t i = 0; i < ty_.matrixIndex.size(); ++i) {
|
||||||
|
const std::string f = m_.structs[m_.structIdx.at("Matrix")].fields[i].name;
|
||||||
|
o << " m.m[" << ty_.matrixIndex[i] << "] = r." << f << ";\n";
|
||||||
|
}
|
||||||
|
o << " return m;\n}\ninline Matrix toMatrix(const Mat4& m) {\n Matrix r{};\n";
|
||||||
|
for (size_t i = 0; i < ty_.matrixIndex.size(); ++i) {
|
||||||
|
const std::string f = m_.structs[m_.structIdx.at("Matrix")].fields[i].name;
|
||||||
|
o << " r." << f << " = m.m[" << ty_.matrixIndex[i] << "];\n";
|
||||||
|
}
|
||||||
|
o << " return r;\n}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void aliases(std::ostream& o, int g) const {
|
||||||
|
bool any = false;
|
||||||
|
for (const std::string& a : m_.aliasOrder) {
|
||||||
|
if (m_.aliasGroup.at(a) != g || ty_.mapped.count(a)) continue;
|
||||||
|
const Res r = ty_.classify(Type{a, 0, false});
|
||||||
|
if (!r.ok() || r.cat == Cat::Void) continue;
|
||||||
|
if (r.cat == Cat::Callback) continue; // emitted with the callbacks
|
||||||
|
o << "using " << a << " = " << p_.gfxType(r, true) << ";\n";
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (any) o << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void callbacks(std::ostream& o, int g) const {
|
||||||
|
bool any = false;
|
||||||
|
for (const CallbackDef& c : m_.callbacks) {
|
||||||
|
if (c.group != g || !p_.emittedCallbacks.count(c.name)) continue;
|
||||||
|
o << "using " << c.name << " = " << p_.gfxType(ty_.classify(c.ret), true) << " (*)(";
|
||||||
|
for (size_t i = 0; i < c.params.size(); ++i) o << (i ? ", " : "") << p_.gfxType(ty_.classify(c.params[i].type), true);
|
||||||
|
o << ");\n";
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (any) o << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void colors(std::ostream& o) const {
|
||||||
|
if (m_.colors.empty() || !ty_.mapped.count("Color")) return;
|
||||||
|
o << "// raylib's colour constants (RED, RAYWHITE, ...), CamelCase because the originals are macros.\nnamespace colors {\n";
|
||||||
|
for (const ColorDef& c : m_.colors)
|
||||||
|
o << "inline constexpr Rgba " << colorConstName(c.name) << "{" << c.r << ", " << c.g << ", " << c.b << ", " << c.a << "};\n";
|
||||||
|
o << "} // namespace colors\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void functions(std::ostream& o, int g) const {
|
||||||
|
o << "// ---- " << groupName(g) << " functions " << std::string(60, '-') << "\n\n";
|
||||||
|
for (const Wrapped& w : p_.wrapped) {
|
||||||
|
if (w.f->group != g) continue;
|
||||||
|
std::string doc = w.f->doc;
|
||||||
|
if (w.engineOwned) doc += (doc.empty() ? "" : " ") + std::string("[the engine calls this itself in run(); using it from game code interferes with the frame loop]");
|
||||||
|
if (!doc.empty()) o << "/// " << doc << "\n";
|
||||||
|
if (w.special) {
|
||||||
|
if (w.f->name == "TraceLog") o << "void traceLog(int logLevel, const char* text, ...) ENG_GFX_PRINTF(2, 3); // printf-style, like raylib's TraceLog\n";
|
||||||
|
else o << "std::string textFormat(const char* text, ...) ENG_GFX_PRINTF(1, 2); // printf-style; returns a std::string instead of raylib's shared static buffer\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
o << signature(w, false) << ";\n";
|
||||||
|
}
|
||||||
|
o << "\n";
|
||||||
|
// std::string overloads
|
||||||
|
bool any = false;
|
||||||
|
for (const Wrapped& w : p_.wrapped) {
|
||||||
|
if (w.f->group != g || !w.strOverload) continue;
|
||||||
|
if (!any) { o << "// ---- overloads taking std::string wherever raylib takes a const char* ----\n"; any = true; }
|
||||||
|
o << "inline " << signature(w, true) << " {\n " << (w.ret.cat == Cat::Void ? "" : "return ") << "::eng::gfx::" << w.name << "(";
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) o << (i ? ", " : "") << w.pn[i] << (Plan::isCString(w.p[i]) ? ".c_str()" : "");
|
||||||
|
o << ");\n}\n";
|
||||||
|
}
|
||||||
|
if (any) o << "\n";
|
||||||
|
// #define A B function aliases
|
||||||
|
bool anyAlias = false;
|
||||||
|
for (const MacroAlias& a : m_.macroAliases) {
|
||||||
|
if (a.group != g) continue;
|
||||||
|
const Wrapped* target = nullptr;
|
||||||
|
for (const Wrapped& w : p_.wrapped) if (w.f->name == a.target && !w.special) target = &w;
|
||||||
|
if (!target) continue;
|
||||||
|
const std::string name = lowerFirst(a.name);
|
||||||
|
if (p_.usedNames.count(name)) continue;
|
||||||
|
if (!anyAlias) { o << "// ---- raylib's compatibility #defines ----\n"; anyAlias = true; }
|
||||||
|
o << "template <class... A> inline decltype(auto) " << name << "(A&&... a) { return " << target->name << "(static_cast<A&&>(a)...); } // " << a.name << " -> " << a.target << "\n";
|
||||||
|
}
|
||||||
|
if (anyAlias) o << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string signature(const Wrapped& w, bool stringForm) const {
|
||||||
|
std::string s = p_.gfxType(w.ret) + " " + w.name + "(";
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) {
|
||||||
|
s += i ? ", " : "";
|
||||||
|
s += (stringForm && Plan::isCString(w.p[i])) ? "const std::string&" : p_.gfxType(w.p[i]);
|
||||||
|
s += " " + w.pn[i];
|
||||||
|
}
|
||||||
|
return s + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
void definition(std::ostream& o, const Wrapped& w) const {
|
||||||
|
o << signature(w, false) << " {\n ";
|
||||||
|
std::string call = "::" + w.f->name + "(";
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) call += (i ? ", " : "") + p_.toRl(w.p[i], w.pn[i]);
|
||||||
|
call += ")";
|
||||||
|
if (w.ret.cat == Cat::Void) o << call << ";\n";
|
||||||
|
else o << "return " << p_.fromRl(w.ret, call) << ";\n";
|
||||||
|
o << "}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void special(std::ostream& o, const Wrapped& w) const {
|
||||||
|
const char* fmtBody =
|
||||||
|
" std::va_list args;\n"
|
||||||
|
" va_start(args, text);\n"
|
||||||
|
" std::va_list copy;\n"
|
||||||
|
" va_copy(copy, args);\n"
|
||||||
|
" const int n = std::vsnprintf(nullptr, 0, text, copy);\n"
|
||||||
|
" va_end(copy);\n"
|
||||||
|
" std::string s;\n"
|
||||||
|
" if (n > 0) {\n"
|
||||||
|
" s.resize(static_cast<size_t>(n) + 1);\n"
|
||||||
|
" std::vsnprintf(s.data(), s.size(), text, args);\n"
|
||||||
|
" s.resize(static_cast<size_t>(n));\n"
|
||||||
|
" }\n"
|
||||||
|
" va_end(args);\n";
|
||||||
|
if (w.f->name == "TraceLog") {
|
||||||
|
o << "void traceLog(int logLevel, const char* text, ...) {\n" << fmtBody << " ::TraceLog(logLevel, \"%s\", s.c_str());\n}\n\n";
|
||||||
|
} else {
|
||||||
|
o << "std::string textFormat(const char* text, ...) {\n" << fmtBody << " return s;\n}\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void fieldChecks(std::ostream& o, const std::string& gfxName, const std::string& rlName, const Field& f) const {
|
||||||
|
o << "GFX_FIELD(" << gfxName << ", ::" << rlName << ", " << f.name << ");\n";
|
||||||
|
const Res r = ty_.classify(f.type);
|
||||||
|
if (r.cat == Cat::Builtin || r.cat == Cat::ExtBuiltin) o << "GFX_FIELD_TYPE(" << gfxName << ", ::" << rlName << ", " << f.name << ");\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void layoutChecks(std::ostream& o, int g) const {
|
||||||
|
if (g == kGroupRaylib)
|
||||||
|
for (const auto& kv : ty_.mapped) {
|
||||||
|
const StructDef* s = nullptr;
|
||||||
|
const std::string src = kv.first == "Quaternion" ? "Vector4" : kv.first;
|
||||||
|
const auto it = m_.structIdx.find(src);
|
||||||
|
if (it != m_.structIdx.end()) s = &m_.structs[it->second];
|
||||||
|
if (!s) continue;
|
||||||
|
o << "GFX_LAYOUT(" << kv.second << ", ::" << kv.first << ");\n";
|
||||||
|
for (const Field& f : s->fields) fieldChecks(o, kv.second, kv.first, f);
|
||||||
|
}
|
||||||
|
for (const StructDef& s : m_.structs) {
|
||||||
|
if (s.group != g || s.opaque || !s.ok || ty_.mapped.count(s.name)) continue;
|
||||||
|
o << "GFX_LAYOUT(" << s.name << ", ::" << s.name << ");\n";
|
||||||
|
for (const Field& f : s.fields) fieldChecks(o, s.name, s.name, f);
|
||||||
|
}
|
||||||
|
for (const EnumDef& e : m_.enums)
|
||||||
|
if (e.group == g && e.ok && !e.name.empty() && !e.items.empty()) o << "static_assert(sizeof(" << e.name << ") == sizeof(::" << e.name << "), \"enum " << e.name << " must have raylib's size\");\n";
|
||||||
|
o << "\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace gen
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
// gfx_gen, part 4: the self-test of the generated wrappers.
|
||||||
|
//
|
||||||
|
// The wrappers are mechanical, so the test is mechanical too. gfx_mock.cpp defines every raylib / rlgl function the
|
||||||
|
// wrappers call (with raylib's own signatures, taken from the real headers, so a mismatch is a compile error): the
|
||||||
|
// mock records a fingerprint of each argument exactly as raylib would have received it, and returns a sentinel.
|
||||||
|
// gfx_selftest.cpp calls every wrapper with distinctive values (every parameter, every struct field, every matrix
|
||||||
|
// element gets its own number) and checks that raylib saw exactly those values, in order, and that the sentinel
|
||||||
|
// comes back intact. raymath functions are real inline code, so there the wrapper's result is compared with a
|
||||||
|
// direct call of the raymath function ("oracle").
|
||||||
|
// A swapped parameter, a wrong cast or a transposed matrix cannot pass.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "emit_api.hpp"
|
||||||
|
|
||||||
|
namespace gen {
|
||||||
|
|
||||||
|
class TestWriter {
|
||||||
|
public:
|
||||||
|
explicit TestWriter(const Plan& p) : p_(p), m_(p.m_), ty_(p.ty_) {}
|
||||||
|
|
||||||
|
static std::string common() {
|
||||||
|
return R"(// GENERATED by tools/gfx_gen. DO NOT EDIT.
|
||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <deque>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace gfxtest {
|
||||||
|
|
||||||
|
struct Hash {
|
||||||
|
std::uint64_t v = 1469598103934665603ull;
|
||||||
|
void add(std::uint64_t x) {
|
||||||
|
for (int i = 0; i < 8; ++i) { v ^= (x >> (8 * i)) & 0xffu; v *= 1099511628211ull; }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void fp(Hash& h, bool x) { h.add(x ? 1u : 0u); }
|
||||||
|
template <class T, std::enable_if_t<std::is_integral_v<T> && !std::is_same_v<T, bool>, int> = 0>
|
||||||
|
inline void fp(Hash& h, T x) { h.add(static_cast<std::uint64_t>(static_cast<std::int64_t>(x))); }
|
||||||
|
template <class T, std::enable_if_t<std::is_enum_v<T>, int> = 0>
|
||||||
|
inline void fp(Hash& h, T x) { h.add(static_cast<std::uint64_t>(static_cast<std::int64_t>(x))); }
|
||||||
|
inline void fp(Hash& h, float x) { std::uint32_t b; std::memcpy(&b, &x, 4); h.add(b); }
|
||||||
|
inline void fp(Hash& h, double x) { std::uint64_t b; std::memcpy(&b, &x, 8); h.add(b); }
|
||||||
|
template <class T> inline void fpPtr(Hash& h, const T* p) { h.add(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(p))); }
|
||||||
|
inline void fpStr(Hash& h, const char* s) {
|
||||||
|
if (!s) { h.add(0); return; }
|
||||||
|
h.add(1);
|
||||||
|
for (; *s; ++s) h.add(static_cast<unsigned char>(*s));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T> inline std::uint64_t hv(const T& x) { Hash h; fp(h, x); return h.v; }
|
||||||
|
template <class T> inline std::uint64_t hvPtr(const T* p) { Hash h; fpPtr(h, p); return h.v; }
|
||||||
|
inline std::uint64_t hvStr(const char* s) { Hash h; fpStr(h, s); return h.v; }
|
||||||
|
template <class F> inline std::uint64_t hvFn(F f) { Hash h; h.add(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(f))); return h.v; }
|
||||||
|
|
||||||
|
// The compiler must not know the sentinel values. raymath's sinf/cosf/tan on a value it knows would be folded at
|
||||||
|
// compile time with the compiler's own rounding, which can differ in the last bit from the C library at run time;
|
||||||
|
// then a correct wrapper would be reported as "differs from calling raymath directly".
|
||||||
|
template <class T> inline T opaque(T v) {
|
||||||
|
static_assert(std::is_trivially_copyable_v<T>, "opaque() copies bytes");
|
||||||
|
unsigned char raw[sizeof(T)];
|
||||||
|
std::memcpy(raw, &v, sizeof(T));
|
||||||
|
const volatile unsigned char* p = raw;
|
||||||
|
unsigned char out[sizeof(T)];
|
||||||
|
for (std::size_t i = 0; i < sizeof(T); ++i) out[i] = p[i];
|
||||||
|
std::memcpy(&v, out, sizeof(T));
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distinctive values. The same salt gives the same value in the engine's types and in raylib's.
|
||||||
|
template <class T> inline T makeScalar(int salt) {
|
||||||
|
if constexpr (std::is_same_v<T, bool>) return opaque<bool>((salt % 3) != 0);
|
||||||
|
else if constexpr (std::is_floating_point_v<T>) return opaque<T>(static_cast<T>(static_cast<double>(salt) * 0.37 + 0.5));
|
||||||
|
else if constexpr (std::is_enum_v<T>) return opaque<T>(static_cast<T>(0));
|
||||||
|
else if constexpr (sizeof(T) == 1) return opaque<T>(static_cast<T>(salt % 120 + 1));
|
||||||
|
else return opaque<T>(static_cast<T>(salt % 30011 + 1));
|
||||||
|
}
|
||||||
|
template <class P> inline P makePtr(int salt) { return reinterpret_cast<P>(static_cast<std::uintptr_t>(0x100000 + static_cast<std::uintptr_t>(salt) * 64)); }
|
||||||
|
inline const char* makeStr(int salt) {
|
||||||
|
static std::deque<std::string> store;
|
||||||
|
store.push_back("str" + std::to_string(salt));
|
||||||
|
return store.back().c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace gfxtest
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string iface() {
|
||||||
|
return R"(// GENERATED by tools/gfx_gen. DO NOT EDIT.
|
||||||
|
#pragma once
|
||||||
|
#include "gfx_selftest_common.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <initializer_list>
|
||||||
|
|
||||||
|
namespace gfxtest {
|
||||||
|
|
||||||
|
struct Call {
|
||||||
|
const char* name = "";
|
||||||
|
std::vector<std::uint64_t> args;
|
||||||
|
std::uint64_t ret = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// implemented by gfx_mock.cpp
|
||||||
|
void reset();
|
||||||
|
const std::vector<Call>& calls();
|
||||||
|
int oracleCount();
|
||||||
|
const char* oracleName(int i);
|
||||||
|
// Runs raymath function i directly on the sentinel arguments; hashes = arguments, result, out-parameters.
|
||||||
|
bool oracleRun(int i, std::vector<std::uint64_t>& hashes);
|
||||||
|
|
||||||
|
struct Ctx {
|
||||||
|
int checks = 0, failures = 0;
|
||||||
|
void fail(const char* what, const std::string& why) {
|
||||||
|
++failures;
|
||||||
|
std::printf("FAIL %s: %s\n", what, why.c_str());
|
||||||
|
}
|
||||||
|
void expectCall(const char* name, std::initializer_list<std::uint64_t> args, bool hasRet, std::uint64_t ret) {
|
||||||
|
++checks;
|
||||||
|
const std::vector<Call>& c = calls();
|
||||||
|
if (c.size() != 1) { fail(name, "expected exactly one raylib call, saw " + std::to_string(c.size())); return; }
|
||||||
|
if (std::strcmp(c[0].name, name) != 0) { fail(name, std::string("called ") + c[0].name + " instead"); return; }
|
||||||
|
if (c[0].args.size() != args.size()) { fail(name, "wrong number of arguments"); return; }
|
||||||
|
size_t i = 0;
|
||||||
|
for (std::uint64_t a : args) {
|
||||||
|
if (c[0].args[i] != a) { fail(name, "argument " + std::to_string(i + 1) + " reached raylib changed or in the wrong place"); return; }
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (hasRet && c[0].ret != ret) fail(name, "the value raylib returned came back changed");
|
||||||
|
}
|
||||||
|
void expectEqual(const char* name, const std::vector<std::uint64_t>& got, const std::vector<std::uint64_t>& want) {
|
||||||
|
++checks;
|
||||||
|
if (got.size() != want.size()) { fail(name, "result count differs from the direct call"); return; }
|
||||||
|
for (size_t i = 0; i < got.size(); ++i)
|
||||||
|
if (got[i] != want[i]) { fail(name, "value " + std::to_string(i + 1) + " differs from calling raymath directly"); return; }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace gfxtest
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- gfx_mock.cpp ------------------------------------------------------------------------------------------
|
||||||
|
std::string mock(const std::string& version) const {
|
||||||
|
std::ostringstream o;
|
||||||
|
o << "// GENERATED by tools/gfx_gen from raylib " << version << ". DO NOT EDIT.\n"
|
||||||
|
<< "// A recording stand-in for raylib: see the header comment of gfx_gen's emit_test.hpp.\n"
|
||||||
|
<< "#include \"gfx_selftest_iface.hpp\"\n\n#include <cstdarg>\n#include <cstdint>\n\n#include <raylib.h>\n";
|
||||||
|
if (p_.enabled(kGroupRlgl)) o << "#include <rlgl.h>\n";
|
||||||
|
if (p_.enabled(kGroupRaymath)) o << "#include <raymath.h>\n";
|
||||||
|
o << "\nnamespace gfxtest {\nnamespace {\nstd::vector<Call> g_calls;\n}\n\n";
|
||||||
|
o << "void reset() { g_calls.clear(); }\nconst std::vector<Call>& calls() { return g_calls; }\n";
|
||||||
|
o << "inline Call& begin(const char* name) {\n g_calls.emplace_back();\n g_calls.back().name = name;\n return g_calls.back();\n}\n\n";
|
||||||
|
structFns(o, true);
|
||||||
|
o << "} // namespace gfxtest\n\n";
|
||||||
|
|
||||||
|
// the raylib / rlgl functions, with raylib's signatures
|
||||||
|
for (const Wrapped& w : p_.wrapped) {
|
||||||
|
if (w.f->group == kGroupRaymath) continue;
|
||||||
|
if (w.special) {
|
||||||
|
if (w.f->name == "TraceLog")
|
||||||
|
o << "void TraceLog(int a0, const char* a1, ...) {\n gfxtest::Call& c = gfxtest::begin(\"TraceLog\");\n"
|
||||||
|
<< " va_list args;\n va_start(args, a1);\n const char* msg = va_arg(args, const char*); // the wrapper always passes \"%s\", message\n"
|
||||||
|
<< " va_end(args);\n c.args = {gfxtest::hv(a0), gfxtest::hvStr(a1), gfxtest::hvStr(msg)};\n}\n\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mockFunction(o, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
// raymath oracles
|
||||||
|
o << "namespace gfxtest {\n";
|
||||||
|
std::vector<const Wrapped*> ray = raymathList();
|
||||||
|
o << "int oracleCount() { return " << ray.size() << "; }\n";
|
||||||
|
o << "const char* oracleName(int i) {\n static const char* names[] = {";
|
||||||
|
for (const Wrapped* w : ray) o << "\"" << w->f->name << "\", ";
|
||||||
|
o << "\"\"};\n return names[i];\n}\n";
|
||||||
|
o << "bool oracleRun(int i, std::vector<std::uint64_t>& hashes) {\n hashes.clear();\n switch (i) {\n";
|
||||||
|
for (size_t k = 0; k < ray.size(); ++k) {
|
||||||
|
o << " case " << k << ": {\n";
|
||||||
|
if (!oracleTestable(*ray[k])) { o << " return false;\n }\n"; continue; }
|
||||||
|
oracleBody(o, *ray[k], k, true);
|
||||||
|
o << " return true;\n }\n";
|
||||||
|
}
|
||||||
|
o << " default: break;\n }\n return false;\n}\n} // namespace gfxtest\n";
|
||||||
|
return o.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- gfx_selftest.cpp --------------------------------------------------------------------------------------
|
||||||
|
std::string test(const std::string& version) const {
|
||||||
|
std::ostringstream o;
|
||||||
|
o << "// GENERATED by tools/gfx_gen from raylib " << version << ". DO NOT EDIT.\n"
|
||||||
|
<< "#include <engine/gfx_api.hpp>\n";
|
||||||
|
if (p_.enabled(kGroupRlgl)) o << "#include <engine/gfx_rlgl_api.hpp>\n";
|
||||||
|
if (p_.enabled(kGroupRaymath)) o << "#include <engine/gfx_raymath_api.hpp>\n";
|
||||||
|
o << "#include \"gfx_selftest_iface.hpp\"\n\nnamespace gfx = eng::gfx;\nusing namespace eng::gfx;\nusing eng::Mat4;\nusing eng::Vec2;\nusing eng::Vec3;\nusing eng::Quat;\nusing eng::Rgba;\nusing eng::Aabb;\n\n";
|
||||||
|
o << "namespace gfxtest {\n";
|
||||||
|
structFns(o, false);
|
||||||
|
// callback stand-ins
|
||||||
|
for (const std::string& n : p_.emittedCallbacks) {
|
||||||
|
const CallbackDef& c = m_.callbacks[m_.callbackIdx.at(n)];
|
||||||
|
o << "[[maybe_unused]] static " << p_.gfxType(ty_.classify(c.ret), true) << " cb_" << n << "(";
|
||||||
|
for (size_t i = 0; i < c.params.size(); ++i) o << (i ? ", " : "") << p_.gfxType(ty_.classify(c.params[i].type), true);
|
||||||
|
o << ") { ";
|
||||||
|
const Res r = ty_.classify(c.ret);
|
||||||
|
if (r.cat != Cat::Void) o << "return " << (r.t.ptr ? "nullptr" : "{}") << "; ";
|
||||||
|
o << "}\n";
|
||||||
|
}
|
||||||
|
o << "} // namespace gfxtest\n\n";
|
||||||
|
|
||||||
|
size_t idx = 0;
|
||||||
|
std::vector<std::string> fnNames;
|
||||||
|
for (const Wrapped& w : p_.wrapped) {
|
||||||
|
if (w.f->group == kGroupRaymath || w.special) continue;
|
||||||
|
const std::string fn = "t" + std::to_string(idx++);
|
||||||
|
fnNames.push_back(fn);
|
||||||
|
testFunction(o, w, fn);
|
||||||
|
}
|
||||||
|
std::vector<const Wrapped*> ray = raymathList();
|
||||||
|
for (size_t k = 0; k < ray.size(); ++k) {
|
||||||
|
const std::string fn = "r" + std::to_string(k);
|
||||||
|
fnNames.push_back(fn);
|
||||||
|
o << "static void " << fn << "(gfxtest::Ctx& c) { // " << ray[k]->f->name << " -> gfx::" << ray[k]->name << "\n";
|
||||||
|
if (!oracleTestable(*ray[k])) { o << " (void)c;\n}\n\n"; continue; }
|
||||||
|
o << " std::vector<std::uint64_t> got, want;\n";
|
||||||
|
o << " if (!gfxtest::oracleRun(" << k << ", want)) return;\n";
|
||||||
|
o << " if (std::string(gfxtest::oracleName(" << k << ")) != \"" << ray[k]->f->name << "\") { c.fail(\"" << ray[k]->f->name << "\", \"oracle table out of step\"); return; }\n";
|
||||||
|
oracleBody(o, *ray[k], k, false);
|
||||||
|
o << " c.expectEqual(\"" << ray[k]->f->name << "\", got, want);\n}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
o << "int main() {\n gfxtest::Ctx c;\n";
|
||||||
|
for (const std::string& f : fnNames) o << " " << f << "(c);\n";
|
||||||
|
o << R"( // printf-style functions
|
||||||
|
{
|
||||||
|
const std::string s = gfx::textFormat("%d-%s-%.2f", 7, "q", 1.5);
|
||||||
|
++c.checks;
|
||||||
|
if (s != "7-q-1.50") c.fail("textFormat", "got '" + s + "'");
|
||||||
|
std::string big(3000, 'x');
|
||||||
|
const std::string s2 = gfx::textFormat("<%s>", big.c_str());
|
||||||
|
++c.checks;
|
||||||
|
if (s2 != "<" + big + ">") c.fail("textFormat", "long strings must not be cut off");
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
if (hasSpecial("TraceLog"))
|
||||||
|
o << R"( {
|
||||||
|
gfxtest::reset();
|
||||||
|
gfx::traceLog(3, "x=%d %s", 5, "abc");
|
||||||
|
c.expectCall("TraceLog", {gfxtest::hv(3), gfxtest::hvStr("%s"), gfxtest::hvStr("x=5 abc")}, false, 0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
o << " std::printf(\"gfx_selftest: %d checks, %d failed\\n\", c.checks, c.failures);\n return c.failures == 0 ? 0 : 1;\n}\n";
|
||||||
|
return o.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t raymathCount() const { return raymathList().size(); }
|
||||||
|
size_t mockedCount() const {
|
||||||
|
size_t n = 0;
|
||||||
|
for (const Wrapped& w : p_.wrapped) if (w.f->group != kGroupRaymath && !w.special) ++n;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
size_t oracleTestableCount() const {
|
||||||
|
size_t n = 0;
|
||||||
|
for (const Wrapped* w : raymathList()) if (oracleTestable(*w)) ++n;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Plan& p_;
|
||||||
|
const ModelData& m_;
|
||||||
|
const Types& ty_;
|
||||||
|
|
||||||
|
bool hasSpecial(const std::string& rlName) const {
|
||||||
|
for (const Wrapped& w : p_.wrapped) if (w.special && w.f->name == rlName) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const Wrapped*> raymathList() const {
|
||||||
|
std::vector<const Wrapped*> v;
|
||||||
|
for (const Wrapped& w : p_.wrapped) if (w.f->group == kGroupRaymath && !w.special) v.push_back(&w);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- per-struct fingerprint / sentinel functions ----------------------------------------------------------
|
||||||
|
struct StructFn {
|
||||||
|
std::string tag; // name used in mk_<tag>
|
||||||
|
std::string type; // how this TU spells the type
|
||||||
|
const StructDef* def;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<StructFn> structList(bool rl) const {
|
||||||
|
std::vector<StructFn> v;
|
||||||
|
for (const StructDef& s : m_.structs) {
|
||||||
|
if (s.opaque || !s.ok || !p_.enabled(s.group)) continue;
|
||||||
|
if (rl) v.push_back({s.name, "::" + s.name, &s});
|
||||||
|
else if (ty_.mapped.count(s.name)) v.push_back({s.name, ty_.mapped.at(s.name), &s});
|
||||||
|
else v.push_back({s.name, s.name, &s});
|
||||||
|
// Quaternion is raylib's alias of Vector4 and the engine's Quat: right behind it, before anything uses it
|
||||||
|
if (!rl && s.name == "Vector4" && ty_.mapped.count("Quaternion")) v.push_back({"Quaternion", "Quat", &s});
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string tagOf(const Res& r, bool rl) {
|
||||||
|
if (rl && r.t.base == "Quaternion") return "Vector4";
|
||||||
|
return r.t.base;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnumDef* enumOf(const std::string& name) const {
|
||||||
|
for (const EnumDef& e : m_.enums) if (e.name == name && e.ok && !e.items.empty()) return &e;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the type of a field/param as this TU spells it
|
||||||
|
std::string typeStr(const Res& r, bool rl, bool inStruct) const { return rl ? p_.rlType(r) : p_.gfxType(r, inStruct); }
|
||||||
|
|
||||||
|
std::string enumValue(const Res& r, int pick) const {
|
||||||
|
const EnumDef* e = enumOf(r.t.base);
|
||||||
|
return e ? std::to_string(e->items[static_cast<size_t>(pick) % e->items.size()].value) : "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// expression producing a sentinel value of type r
|
||||||
|
std::string makeExpr(const Res& r, const std::string& salt, bool rl, bool inStruct, int pick) const {
|
||||||
|
const std::string ts = typeStr(r, rl, inStruct);
|
||||||
|
if (r.t.ptr > 0) {
|
||||||
|
if (Plan::isCString(r)) return "gfxtest::makeStr(" + salt + ")";
|
||||||
|
return "gfxtest::makePtr<" + ts + ">(" + salt + ")";
|
||||||
|
}
|
||||||
|
switch (r.cat) {
|
||||||
|
case Cat::Builtin: case Cat::ExtBuiltin: return "gfxtest::makeScalar<" + ts + ">(" + salt + ")";
|
||||||
|
case Cat::Enum: {
|
||||||
|
if (!rl && !inStruct) return "int(" + enumValue(r, pick) + ")";
|
||||||
|
return "static_cast<" + ts + ">(" + enumValue(r, pick) + ")";
|
||||||
|
}
|
||||||
|
case Cat::Mapped: return "gfxtest::mk_" + tagOf(r, rl) + "(" + salt + ")";
|
||||||
|
case Cat::Struct:
|
||||||
|
if (!rl && p_.isMat4(r, inStruct)) return "gfxtest::mk_Mat4(" + salt + ")";
|
||||||
|
return "gfxtest::mk_" + tagOf(r, rl) + "(" + salt + ")";
|
||||||
|
case Cat::Callback: return rl ? "static_cast<" + ts + ">(nullptr)" : "&gfxtest::cb_" + r.t.base;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string hashExpr(const Res& r, const std::string& e) const {
|
||||||
|
if (r.t.ptr > 0) return Plan::isCString(r) ? "gfxtest::hvStr(" + e + ")" : "gfxtest::hvPtr(" + e + ")";
|
||||||
|
if (r.cat == Cat::Callback) return "gfxtest::hvFn(" + e + ")";
|
||||||
|
return "gfxtest::hv(" + e + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
void structFns(std::ostream& o, bool rl) const {
|
||||||
|
// fingerprints and sentinels of every struct (a struct's fields are always declared before it)
|
||||||
|
for (const StructFn& sf : structList(rl)) {
|
||||||
|
const StructDef& s = *sf.def;
|
||||||
|
const bool isMatrix = s.name == "Matrix" && ty_.matrixMapped;
|
||||||
|
o << "inline void fp(Hash& h, const " << sf.type << "& v) {\n";
|
||||||
|
if (s.fields.empty()) o << " (void)h; (void)v;\n";
|
||||||
|
for (const Field& f : s.fields) {
|
||||||
|
const Res rf = ty_.classify(f.type);
|
||||||
|
std::string expr = "v." + f.name;
|
||||||
|
std::string close;
|
||||||
|
for (size_t d = 0; d < f.dims.size(); ++d) {
|
||||||
|
o << std::string(4 + 4 * d, ' ') << "for (const auto& x" << d << " : " << expr << ") {\n";
|
||||||
|
expr = "x" + std::to_string(d);
|
||||||
|
close = std::string(4 + 4 * d, ' ') + "}\n" + close;
|
||||||
|
}
|
||||||
|
const std::string ind(4 + 4 * f.dims.size(), ' ');
|
||||||
|
if (rf.t.ptr > 0) o << ind << "fpPtr(h, " << expr << ");\n";
|
||||||
|
else o << ind << "fp(h, " << expr << ");\n";
|
||||||
|
o << close;
|
||||||
|
}
|
||||||
|
o << "}\n";
|
||||||
|
o << "inline " << sf.type << " mk_" << sf.tag << "(int salt) {\n " << sf.type << " r{};\n";
|
||||||
|
if (s.fields.empty()) o << " (void)salt;\n";
|
||||||
|
int j = 0;
|
||||||
|
for (size_t fi = 0; fi < s.fields.size(); ++fi) {
|
||||||
|
const Field& f = s.fields[fi];
|
||||||
|
const Res rf = ty_.classify(f.type);
|
||||||
|
const int jj = isMatrix ? ty_.matrixIndex[fi] : j;
|
||||||
|
std::string lhs = "r." + f.name;
|
||||||
|
std::string saltE = "(salt * 16 + " + std::to_string(jj) + ")";
|
||||||
|
std::string close;
|
||||||
|
for (size_t d = 0; d < f.dims.size(); ++d) {
|
||||||
|
o << std::string(4 + 4 * d, ' ') << "for (std::size_t i" << d << " = 0; i" << d << " < " << f.dims[d] << "; ++i" << d << ") {\n";
|
||||||
|
lhs += "[i" + std::to_string(d) + "]";
|
||||||
|
saltE = "(" + saltE + " + static_cast<int>(i" + std::to_string(d) + ") * 3)";
|
||||||
|
close = std::string(4 + 4 * d, ' ') + "}\n" + close;
|
||||||
|
}
|
||||||
|
const std::string ind(4 + 4 * f.dims.size(), ' ');
|
||||||
|
o << ind << lhs << " = " << makeExpr(rf, saltE + " % 100003", rl, true, jj) << ";\n" << close;
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
o << " return r;\n}\n";
|
||||||
|
}
|
||||||
|
if (!rl && ty_.matrixMapped) {
|
||||||
|
o << "inline void fp(Hash& h, const eng::Mat4& v) {\n";
|
||||||
|
for (int idx : ty_.matrixIndex) o << " fp(h, v.m[" << idx << "]);\n";
|
||||||
|
o << "}\ninline eng::Mat4 mk_Mat4(int salt) {\n eng::Mat4 r{};\n for (int i = 0; i < 16; ++i) r.m[i] = makeScalar<float>((salt * 16 + i) % 100003);\n return r;\n}\n";
|
||||||
|
}
|
||||||
|
o << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- mock of one raylib/rlgl function --------------------------------------------------------------------
|
||||||
|
int saltOf(const Wrapped& w) const { return static_cast<int>(&w - p_.wrapped.data()) * 100 + 100; }
|
||||||
|
|
||||||
|
void mockFunction(std::ostream& o, const Wrapped& w) const {
|
||||||
|
const int base = saltOf(w);
|
||||||
|
o << p_.rlType(w.ret) << " " << w.f->name << "(";
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) o << (i ? ", " : "") << p_.rlType(w.p[i]) << " a" << i;
|
||||||
|
const bool needsCall = !w.p.empty() || w.ret.cat != Cat::Void;
|
||||||
|
o << ") {\n " << (needsCall ? "gfxtest::Call& c = " : "") << "gfxtest::begin(\"" << w.f->name << "\");\n";
|
||||||
|
if (!w.p.empty()) {
|
||||||
|
o << " c.args = {";
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) o << (i ? ", " : "") << hashExpr(w.p[i], "a" + std::to_string(i));
|
||||||
|
o << "};\n";
|
||||||
|
}
|
||||||
|
if (w.ret.cat != Cat::Void) {
|
||||||
|
const std::string salt = std::to_string(base + 99);
|
||||||
|
const std::string rt = p_.rlType(w.ret);
|
||||||
|
if (Plan::isCString(w.ret)) o << " const char* r = gfxtest::makeStr(" << salt << ");\n";
|
||||||
|
else o << " " << rt << " r = " << makeExpr(w.ret, salt, true, false, 1) << ";\n";
|
||||||
|
o << " c.ret = " << hashExpr(w.ret, "r") << ";\n return r;\n";
|
||||||
|
}
|
||||||
|
o << "}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the test of one wrapper -----------------------------------------------------------------------------
|
||||||
|
void testFunction(std::ostream& o, const Wrapped& w, const std::string& fn) const {
|
||||||
|
const int base = saltOf(w);
|
||||||
|
o << "static void " << fn << "(gfxtest::Ctx& c) { // " << w.f->name << " -> gfx::" << w.name << "\n gfxtest::reset();\n";
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) {
|
||||||
|
const std::string salt = std::to_string(base + static_cast<int>(i) * 10);
|
||||||
|
o << " " << p_.gfxType(w.p[i]) << " a" << i << " = " << makeExpr(w.p[i], salt, false, false, static_cast<int>(i)) << ";\n";
|
||||||
|
}
|
||||||
|
auto call = [&](bool strings) {
|
||||||
|
std::string s = "gfx::" + w.name + "(";
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) s += (i ? ", " : "") + (strings && Plan::isCString(w.p[i]) ? "s" + std::to_string(i) : "a" + std::to_string(i));
|
||||||
|
return s + ")";
|
||||||
|
};
|
||||||
|
auto expect = [&](const std::string& resultVar) {
|
||||||
|
std::string e = " c.expectCall(\"" + w.f->name + "\", {";
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) e += (i ? ", " : "") + hashExpr(w.p[i], "a" + std::to_string(i));
|
||||||
|
e += "}, ";
|
||||||
|
e += w.ret.cat == Cat::Void ? "false, 0" : "true, " + hashExpr(w.ret, resultVar);
|
||||||
|
return e + ");\n";
|
||||||
|
};
|
||||||
|
if (w.ret.cat == Cat::Void) o << " " << call(false) << ";\n";
|
||||||
|
else o << " const auto r = " << call(false) << ";\n";
|
||||||
|
o << expect("r");
|
||||||
|
if (w.strOverload) {
|
||||||
|
o << " gfxtest::reset();\n";
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) if (Plan::isCString(w.p[i])) o << " const std::string s" << i << " = a" << i << ";\n";
|
||||||
|
if (w.ret.cat == Cat::Void) o << " " << call(true) << ";\n";
|
||||||
|
else o << " const auto r2 = " << call(true) << ";\n";
|
||||||
|
o << expect("r2");
|
||||||
|
}
|
||||||
|
o << "}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- raymath ---------------------------------------------------------------------------------------------
|
||||||
|
bool oracleTestable(const Wrapped& w) const {
|
||||||
|
for (const Res& r : w.p) {
|
||||||
|
if (r.t.ptr > 1) return false;
|
||||||
|
if (r.t.ptr == 1) {
|
||||||
|
if (r.cat == Cat::Opaque || (r.cat == Cat::Builtin && r.t.base == "void") || r.cat == Cat::Callback) return false;
|
||||||
|
if (Plan::isCString(r)) return false;
|
||||||
|
}
|
||||||
|
if (r.cat == Cat::Callback) return false;
|
||||||
|
}
|
||||||
|
if (w.ret.t.ptr > 0 || w.ret.cat == Cat::Callback) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declares the arguments, calls (the raymath function directly when `rl`, else the wrapper) and appends
|
||||||
|
// [arguments, result, out-parameters] fingerprints to `hashes` (rl) / `got` (wrapper).
|
||||||
|
void oracleBody(std::ostream& o, const Wrapped& w, size_t k, bool rl) const {
|
||||||
|
const std::string ind = rl ? " " : " ";
|
||||||
|
const std::string vec = rl ? "hashes" : "got";
|
||||||
|
const int base = 900000 + static_cast<int>(k) * 100;
|
||||||
|
std::vector<std::string> argExpr;
|
||||||
|
std::vector<std::string> outs;
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) {
|
||||||
|
const Res& r = w.p[i];
|
||||||
|
const std::string salt = std::to_string(base + static_cast<int>(i) * 10);
|
||||||
|
if (r.t.ptr == 1) {
|
||||||
|
Res pointee = r;
|
||||||
|
pointee.t.ptr = 0;
|
||||||
|
// storage behind a pointer keeps raylib's layout (a Matrix stays the mirror struct, never Mat4)
|
||||||
|
o << ind << typeStr(pointee, rl, true) << " p" << i << " = " << makeExpr(pointee, salt, rl, true, static_cast<int>(i)) << ";\n";
|
||||||
|
argExpr.push_back("&p" + std::to_string(i));
|
||||||
|
outs.push_back("p" + std::to_string(i));
|
||||||
|
} else {
|
||||||
|
o << ind << typeStr(r, rl, false) << " a" << i << " = " << makeExpr(r, salt, rl, false, static_cast<int>(i)) << ";\n";
|
||||||
|
argExpr.push_back("a" + std::to_string(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < w.p.size(); ++i) {
|
||||||
|
const Res& r = w.p[i];
|
||||||
|
if (r.t.ptr == 1) o << ind << vec << ".push_back(gfxtest::hv(p" << i << "));\n";
|
||||||
|
else o << ind << vec << ".push_back(" << hashExpr(r, "a" + std::to_string(i)) << ");\n";
|
||||||
|
}
|
||||||
|
std::string call = rl ? "::" + w.f->name + "(" : "gfx::" + w.name + "(";
|
||||||
|
for (size_t i = 0; i < argExpr.size(); ++i) call += (i ? ", " : "") + argExpr[i];
|
||||||
|
call += ")";
|
||||||
|
if (w.ret.cat == Cat::Void) o << ind << call << ";\n";
|
||||||
|
else {
|
||||||
|
o << ind << "const auto res = " << call << ";\n";
|
||||||
|
o << ind << vec << ".push_back(" << hashExpr(w.ret, "res") << ");\n";
|
||||||
|
}
|
||||||
|
for (const std::string& out : outs) o << ind << vec << ".push_back(gfxtest::hv(" << out << "));\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace gen
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
// gfx_gen: writes eng::gfx, the engine's equivalent of every raylib function, from raylib's own headers.
|
||||||
|
//
|
||||||
|
// gfx_gen --raylib <raylib.h> [--rlgl <rlgl.h>] [--raymath <raymath.h>] --out <dir> [-DNAME[=VALUE]]... [--no-selftest]
|
||||||
|
//
|
||||||
|
// Output (all under <dir>):
|
||||||
|
// include/engine/gfx_api.hpp, gfx_rlgl_api.hpp, gfx_raymath_api.hpp the public headers (one per input header)
|
||||||
|
// gfx_core.cpp, gfx_rlgl.cpp, gfx_raymath.cpp the wrappers (compile against raylib)
|
||||||
|
// selftest/* mock raylib + test of every wrapper
|
||||||
|
// gfx_report.txt what was wrapped, what was skipped and why
|
||||||
|
//
|
||||||
|
// The generator reads the headers of the raylib you build against, so the wrappers always match that version.
|
||||||
|
// What it cannot express safely (varargs other than TraceLog/TextFormat, function-pointer parameters, ...) is
|
||||||
|
// skipped and listed in the report; nothing is guessed.
|
||||||
|
#include "emit_test.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace gen;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
struct Options {
|
||||||
|
std::string raylib, rlgl, raymath, out;
|
||||||
|
std::vector<std::string> defines;
|
||||||
|
bool selftest = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Options parseArgs(int argc, char** argv) {
|
||||||
|
Options o;
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
const std::string a = argv[i];
|
||||||
|
auto next = [&]() -> std::string {
|
||||||
|
if (i + 1 >= argc) die("missing value after " + a);
|
||||||
|
return argv[++i];
|
||||||
|
};
|
||||||
|
if (a == "--raylib") o.raylib = next();
|
||||||
|
else if (a == "--rlgl") o.rlgl = next();
|
||||||
|
else if (a == "--raymath") o.raymath = next();
|
||||||
|
else if (a == "--out") o.out = next();
|
||||||
|
else if (a == "--no-selftest") o.selftest = false;
|
||||||
|
else if (a == "-D") o.defines.push_back(next());
|
||||||
|
else if (a.rfind("-D", 0) == 0) o.defines.push_back(a.substr(2));
|
||||||
|
else die("unknown argument " + a);
|
||||||
|
}
|
||||||
|
if (o.raylib.empty() || o.out.empty()) die("usage: gfx_gen --raylib raylib.h [--rlgl rlgl.h] [--raymath raymath.h] --out dir [-DNAME[=VALUE]]... [--no-selftest]");
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeIfChanged(const fs::path& p, const std::string& content) {
|
||||||
|
fs::create_directories(p.parent_path());
|
||||||
|
{
|
||||||
|
std::ifstream in(p, std::ios::binary);
|
||||||
|
if (in) {
|
||||||
|
std::ostringstream cur;
|
||||||
|
cur << in.rdbuf();
|
||||||
|
if (cur.str() == content) return; // untouched: no needless rebuilds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::ofstream out(p, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!out) die("cannot write " + p.string());
|
||||||
|
out << content;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool f32Literal(const Preprocessor& pp, const std::string& body, int depth = 0) {
|
||||||
|
if (depth > 8) return false;
|
||||||
|
for (const Tok& t : tokenize(body, 0)) {
|
||||||
|
if (t.kind == TK::Number && t.text.size() > 1 && (t.text.back() == 'f' || t.text.back() == 'F') &&
|
||||||
|
(t.text.find('.') != std::string::npos || t.text.find('e') != std::string::npos) && t.text.compare(0, 2, "0x") != 0)
|
||||||
|
return true;
|
||||||
|
if (t.kind == TK::Ident) {
|
||||||
|
const auto it = pp.macros.find(t.text);
|
||||||
|
if (it != pp.macros.end() && !it->second.fnLike && f32Literal(pp, it->second.body, depth + 1)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #define NAME body -> constant / colour / alias
|
||||||
|
void processDefines(ModelData& m, const Preprocessor& pp, int group, size_t from) {
|
||||||
|
for (size_t i = from; i < pp.defined.size(); ++i) {
|
||||||
|
const std::string& name = pp.defined[i].first;
|
||||||
|
const std::string& body = pp.defined[i].second;
|
||||||
|
if (body.empty() || name.empty() || name[0] == '_' || name == "RLAPI" || name == "RMAPI") continue;
|
||||||
|
const std::vector<Tok> toks = tokenize(body, 0);
|
||||||
|
if (toks.empty()) continue;
|
||||||
|
|
||||||
|
// CLITERAL(Color){ r, g, b, a }
|
||||||
|
if (toks.size() >= 3 && toks[0].text == "CLITERAL") {
|
||||||
|
std::vector<long long> v;
|
||||||
|
for (const Tok& t : toks) {
|
||||||
|
Val x;
|
||||||
|
if (t.kind == TK::Number && parseNumberLiteral(t.text, x) && !x.isFloat) v.push_back(x.i);
|
||||||
|
}
|
||||||
|
if (v.size() == 4) {
|
||||||
|
ColorDef c;
|
||||||
|
c.name = name;
|
||||||
|
c.r = static_cast<int>(v[0]); c.g = static_cast<int>(v[1]); c.b = static_cast<int>(v[2]); c.a = static_cast<int>(v[3]);
|
||||||
|
c.group = group;
|
||||||
|
m.colors.push_back(c);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (toks.size() == 1 && toks[0].kind == TK::String) {
|
||||||
|
ConstDef c;
|
||||||
|
c.name = name;
|
||||||
|
c.str = toks[0].text;
|
||||||
|
c.group = group;
|
||||||
|
m.consts.push_back(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (toks.size() == 1 && toks[0].kind == TK::Ident) {
|
||||||
|
const auto e = m.enumerators.find(toks[0].text);
|
||||||
|
if (e != m.enumerators.end()) {
|
||||||
|
ConstDef c;
|
||||||
|
c.name = name;
|
||||||
|
c.value = Val::ofInt(e->second);
|
||||||
|
c.group = group;
|
||||||
|
m.consts.push_back(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool isFunc = false;
|
||||||
|
for (const FuncDef& f : m.funcs) if (f.name == toks[0].text) isFunc = true;
|
||||||
|
if (isFunc) {
|
||||||
|
m.macroAliases.push_back({name, toks[0].text, group});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Val v;
|
||||||
|
if (pp.macroValue(name, v)) {
|
||||||
|
ConstDef c;
|
||||||
|
c.name = name;
|
||||||
|
c.value = v;
|
||||||
|
c.isFloat32 = v.isFloat && f32Literal(pp, body);
|
||||||
|
c.group = group;
|
||||||
|
m.consts.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string unquote(std::string s) {
|
||||||
|
if (s.size() >= 2 && s.front() == '"' && s.back() == '"') s = s.substr(1, s.size() - 2);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
const Options opt = parseArgs(argc, argv);
|
||||||
|
|
||||||
|
Preprocessor pp;
|
||||||
|
for (const std::string& d : opt.defines) pp.predefine(d);
|
||||||
|
ModelData m;
|
||||||
|
|
||||||
|
struct Input { int group; std::string path; };
|
||||||
|
std::vector<Input> inputs = {{kGroupRaylib, opt.raylib}};
|
||||||
|
if (!opt.rlgl.empty()) inputs.push_back({kGroupRlgl, opt.rlgl});
|
||||||
|
if (!opt.raymath.empty()) inputs.push_back({kGroupRaymath, opt.raymath});
|
||||||
|
|
||||||
|
std::set<int> groups;
|
||||||
|
for (const Input& in : inputs) {
|
||||||
|
const size_t before = pp.defined.size();
|
||||||
|
const Unit u = pp.run(in.path);
|
||||||
|
Parser(m, pp, in.group).parse(u);
|
||||||
|
processDefines(m, pp, in.group, before);
|
||||||
|
groups.insert(in.group);
|
||||||
|
}
|
||||||
|
// #define A B where B turned out to be a function declared later in the same header set is handled in
|
||||||
|
// processDefines for already-known functions; nothing else to resolve.
|
||||||
|
|
||||||
|
std::string version = "?";
|
||||||
|
if (const auto it = pp.macros.find("RAYLIB_VERSION"); it != pp.macros.end()) version = unquote(it->second.body);
|
||||||
|
|
||||||
|
Types ty(m);
|
||||||
|
Plan plan(m, ty, pp, groups);
|
||||||
|
ApiWriter api(plan);
|
||||||
|
TestWriter test(plan);
|
||||||
|
|
||||||
|
const fs::path out = opt.out;
|
||||||
|
for (int g : {kGroupRaylib, kGroupRlgl, kGroupRaymath}) {
|
||||||
|
const fs::path hdr = out / "include" / "engine" / groupHeader(g);
|
||||||
|
const fs::path src = out / groupFile(g);
|
||||||
|
if (groups.count(g)) {
|
||||||
|
writeIfChanged(hdr, api.header(g, version));
|
||||||
|
writeIfChanged(src, api.source(g, version));
|
||||||
|
} else {
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(hdr, ec);
|
||||||
|
fs::remove(src, ec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (opt.selftest) {
|
||||||
|
writeIfChanged(out / "selftest" / "gfx_selftest_common.hpp", TestWriter::common());
|
||||||
|
writeIfChanged(out / "selftest" / "gfx_selftest_iface.hpp", TestWriter::iface());
|
||||||
|
writeIfChanged(out / "selftest" / "gfx_mock.cpp", test.mock(version));
|
||||||
|
writeIfChanged(out / "selftest" / "gfx_selftest.cpp", test.test(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- report ----------------------------------------------------------------------------------------------
|
||||||
|
std::ostringstream rep;
|
||||||
|
rep << "gfx_gen report: raylib " << version << "\n\n";
|
||||||
|
for (int g : {kGroupRaylib, kGroupRlgl, kGroupRaymath}) {
|
||||||
|
if (!groups.count(g)) continue;
|
||||||
|
int declared = 0, wrapped = 0, skipped = 0;
|
||||||
|
for (const FuncDef& f : m.funcs) if (f.group == g) ++declared;
|
||||||
|
for (const Wrapped& w : plan.wrapped) if (w.f->group == g) ++wrapped;
|
||||||
|
for (const Skipped& s : plan.skipped) if (s.f->group == g) ++skipped;
|
||||||
|
rep << "GFX_SUMMARY " << (g == kGroupRaylib ? "raylib" : g == kGroupRlgl ? "rlgl" : "raymath") << " declared=" << declared << " wrapped=" << wrapped << " skipped=" << skipped << "\n";
|
||||||
|
}
|
||||||
|
rep << "GFX_SUMMARY structs=" << plan.emittedStructs.size() << " enums=" << plan.emittedEnums.size() << " callbacks=" << plan.emittedCallbacks.size()
|
||||||
|
<< " constants=" << m.consts.size() << " colors=" << m.colors.size() << " mat4=" << (ty.matrixMapped ? 1 : 0) << "\n";
|
||||||
|
rep << "GFX_SUMMARY mapped:";
|
||||||
|
for (const auto& kv : ty.mapped) rep << " " << kv.first << "->" << kv.second;
|
||||||
|
rep << "\n";
|
||||||
|
if (opt.selftest)
|
||||||
|
rep << "GFX_SUMMARY selftest mocked=" << test.mockedCount() << " raymath_oracles=" << test.oracleTestableCount() << "/" << test.raymathCount() << "\n";
|
||||||
|
rep << "\nSkipped functions (not wrapped; use raylib directly for these):\n";
|
||||||
|
for (const Skipped& s : plan.skipped) rep << " " << groupName(s.f->group) << ": " << s.f->name << " - " << s.why << "\n";
|
||||||
|
rep << "\nStructs that could not be mirrored:\n";
|
||||||
|
for (const StructDef& s : m.structs) if (!s.opaque && !s.ok) rep << " " << s.name << " - " << s.why << "\n";
|
||||||
|
rep << "\nEnums that could not be mirrored:\n";
|
||||||
|
for (const EnumDef& e : m.enums) if (!e.ok) rep << " " << (e.name.empty() ? "(anonymous)" : e.name) << " - " << e.why << "\n";
|
||||||
|
rep << "\nCallbacks that could not be mirrored:\n";
|
||||||
|
for (const CallbackDef& c : m.callbacks) if (!c.ok || !ty.callbackSupported(c)) rep << " " << c.name << " - " << (c.why.empty() ? "uses types other than plain C types" : c.why) << "\n";
|
||||||
|
if (!pp.notes.empty()) {
|
||||||
|
rep << "\nPreprocessor notes:\n";
|
||||||
|
for (const std::string& n : pp.notes) rep << " " << n << "\n";
|
||||||
|
}
|
||||||
|
writeIfChanged(out / "gfx_report.txt", rep.str());
|
||||||
|
|
||||||
|
// a short console summary
|
||||||
|
std::fputs(rep.str().substr(0, rep.str().find("\nSkipped functions")).c_str(), stdout);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
// gfx_gen, part 1: reading C headers.
|
||||||
|
//
|
||||||
|
// A small C preprocessor (conditionals, #define bookkeeping, comment stripping), a tokenizer and a
|
||||||
|
// constant-expression evaluator: exactly as much C as raylib.h, rlgl.h and raymath.h need, evaluated the way a
|
||||||
|
// C++ compiler would see them (so `#if defined(X)` blocks inside struct bodies resolve instead of being
|
||||||
|
// parsed twice). Nothing here knows about raylib; see model.hpp for the declaration parser.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace gen {
|
||||||
|
|
||||||
|
[[noreturn]] inline void die(const std::string& msg) {
|
||||||
|
std::fprintf(stderr, "gfx_gen: error: %s\n", msg.c_str());
|
||||||
|
std::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string trim(const std::string& s) {
|
||||||
|
size_t a = 0, b = s.size();
|
||||||
|
while (a < b && std::isspace(static_cast<unsigned char>(s[a]))) ++a;
|
||||||
|
while (b > a && std::isspace(static_cast<unsigned char>(s[b - 1]))) --b;
|
||||||
|
return s.substr(a, b - a);
|
||||||
|
}
|
||||||
|
inline bool isIdentStart(char c) { return std::isalpha(static_cast<unsigned char>(c)) || c == '_'; }
|
||||||
|
inline bool isIdentChar(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
|
||||||
|
|
||||||
|
template <class... A> std::string fmt(const char* f, A... a) {
|
||||||
|
char buf[1024];
|
||||||
|
std::snprintf(buf, sizeof buf, f, a...);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Tokens
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
enum class TK { Ident, Number, String, Punct };
|
||||||
|
struct Tok {
|
||||||
|
TK kind = TK::Punct;
|
||||||
|
std::string text;
|
||||||
|
int line = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::vector<Tok> tokenize(const std::string& s, int line) {
|
||||||
|
std::vector<Tok> out;
|
||||||
|
size_t i = 0;
|
||||||
|
const size_t n = s.size();
|
||||||
|
while (i < n) {
|
||||||
|
const char c = s[i];
|
||||||
|
if (std::isspace(static_cast<unsigned char>(c))) { ++i; continue; }
|
||||||
|
if (isIdentStart(c)) {
|
||||||
|
size_t j = i + 1;
|
||||||
|
while (j < n && isIdentChar(s[j])) ++j;
|
||||||
|
out.push_back({TK::Ident, s.substr(i, j - i), line});
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c)) || (c == '.' && i + 1 < n && std::isdigit(static_cast<unsigned char>(s[i + 1])))) {
|
||||||
|
const bool hex = c == '0' && i + 1 < n && (s[i + 1] == 'x' || s[i + 1] == 'X');
|
||||||
|
size_t j = i + 1;
|
||||||
|
while (j < n) {
|
||||||
|
const char d = s[j];
|
||||||
|
if (isIdentChar(d) || d == '.') ++j;
|
||||||
|
else if (!hex && (d == '+' || d == '-') && (s[j - 1] == 'e' || s[j - 1] == 'E')) ++j;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
out.push_back({TK::Number, s.substr(i, j - i), line});
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '"' || c == '\'') {
|
||||||
|
size_t j = i + 1;
|
||||||
|
while (j < n && s[j] != c) j += (s[j] == '\\' && j + 1 < n) ? 2 : 1;
|
||||||
|
j = std::min(n, j + 1);
|
||||||
|
out.push_back({c == '"' ? TK::String : TK::Number, s.substr(i, j - i), line});
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (s.compare(i, 3, "...") == 0) { out.push_back({TK::Punct, "...", line}); i += 3; continue; }
|
||||||
|
static const char* two[] = {"<<", ">>", "<=", ">=", "==", "!=", "&&", "||", "::", "->"};
|
||||||
|
bool matched = false;
|
||||||
|
for (const char* t : two)
|
||||||
|
if (s.compare(i, 2, t) == 0) { out.push_back({TK::Punct, t, line}); i += 2; matched = true; break; }
|
||||||
|
if (matched) continue;
|
||||||
|
out.push_back({TK::Punct, std::string(1, c), line});
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Constant expressions (#if conditions, enum values, array sizes, numeric #defines)
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
struct Val {
|
||||||
|
bool isFloat = false;
|
||||||
|
long long i = 0;
|
||||||
|
double d = 0;
|
||||||
|
double asDouble() const { return isFloat ? d : static_cast<double>(i); }
|
||||||
|
long long asInt() const { return isFloat ? static_cast<long long>(d) : i; }
|
||||||
|
static Val ofInt(long long v) { Val r; r.i = v; return r; }
|
||||||
|
static Val ofFloat(double v) { Val r; r.isFloat = true; r.d = v; return r; }
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool parseNumberLiteral(const std::string& text, Val& out) {
|
||||||
|
if (text.empty()) return false;
|
||||||
|
if (text[0] == '\'') { // character literal
|
||||||
|
if (text.size() >= 3 && text[1] != '\\') { out = Val::ofInt(static_cast<unsigned char>(text[1])); return true; }
|
||||||
|
if (text.size() >= 4 && text[1] == '\\') {
|
||||||
|
const char e = text[2];
|
||||||
|
out = Val::ofInt(e == 'n' ? 10 : e == 't' ? 9 : e == 'r' ? 13 : e == '0' ? 0 : e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string t = text;
|
||||||
|
const bool hex = t.size() > 1 && t[0] == '0' && (t[1] == 'x' || t[1] == 'X');
|
||||||
|
if (!hex) {
|
||||||
|
const bool floaty = t.find('.') != std::string::npos || t.find('e') != std::string::npos || t.find('E') != std::string::npos;
|
||||||
|
if (floaty) {
|
||||||
|
while (!t.empty() && (t.back() == 'f' || t.back() == 'F' || t.back() == 'l' || t.back() == 'L')) t.pop_back();
|
||||||
|
char* end = nullptr;
|
||||||
|
const double v = std::strtod(t.c_str(), &end);
|
||||||
|
if (!end || *end) return false;
|
||||||
|
out = Val::ofFloat(v);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!t.empty() && (t.back() == 'u' || t.back() == 'U' || t.back() == 'l' || t.back() == 'L')) t.pop_back();
|
||||||
|
if (t.empty()) return false;
|
||||||
|
char* end = nullptr;
|
||||||
|
const unsigned long long v = std::strtoull(t.c_str(), &end, hex ? 16 : (t.size() > 1 && t[0] == '0' ? 8 : 10));
|
||||||
|
if (!end || *end) return false;
|
||||||
|
out = Val::ofInt(static_cast<long long>(v));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluates a token range. `lookup` resolves identifiers (macros, enumerators); `isDefined` answers defined(X).
|
||||||
|
// In `lenient` mode (the preprocessor) unknown identifiers and function-like calls are 0, as in a real #if.
|
||||||
|
class Eval {
|
||||||
|
public:
|
||||||
|
using Lookup = std::function<bool(const std::string&, Val&)>;
|
||||||
|
using Defined = std::function<bool(const std::string&)>;
|
||||||
|
|
||||||
|
Eval(const std::vector<Tok>& t, Lookup lookup, Defined isDefined = {}, bool lenient = false)
|
||||||
|
: t_(t), lookup_(std::move(lookup)), defined_(std::move(isDefined)), lenient_(lenient) {}
|
||||||
|
|
||||||
|
bool run(Val& out) {
|
||||||
|
pos_ = 0;
|
||||||
|
if (!ternary(out)) return false;
|
||||||
|
return pos_ == t_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::vector<Tok>& t_;
|
||||||
|
Lookup lookup_;
|
||||||
|
Defined defined_;
|
||||||
|
bool lenient_;
|
||||||
|
size_t pos_ = 0;
|
||||||
|
|
||||||
|
bool peekIs(const char* s) const { return pos_ < t_.size() && t_[pos_].kind == TK::Punct && t_[pos_].text == s; }
|
||||||
|
|
||||||
|
static bool truthy(const Val& v) { return v.isFloat ? v.d != 0 : v.i != 0; }
|
||||||
|
|
||||||
|
bool ternary(Val& out) {
|
||||||
|
if (!binary(1, out)) return false;
|
||||||
|
if (peekIs("?")) {
|
||||||
|
++pos_;
|
||||||
|
Val a, b;
|
||||||
|
if (!ternary(a)) return false;
|
||||||
|
if (!peekIs(":")) return false;
|
||||||
|
++pos_;
|
||||||
|
if (!ternary(b)) return false;
|
||||||
|
out = truthy(out) ? a : b;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int precedence(const std::string& op) {
|
||||||
|
if (op == "||") return 1;
|
||||||
|
if (op == "&&") return 2;
|
||||||
|
if (op == "|") return 3;
|
||||||
|
if (op == "^") return 4;
|
||||||
|
if (op == "&") return 5;
|
||||||
|
if (op == "==" || op == "!=") return 6;
|
||||||
|
if (op == "<" || op == "<=" || op == ">" || op == ">=") return 7;
|
||||||
|
if (op == "<<" || op == ">>") return 8;
|
||||||
|
if (op == "+" || op == "-") return 9;
|
||||||
|
if (op == "*" || op == "/" || op == "%") return 10;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool binary(int minPrec, Val& lhs) {
|
||||||
|
if (!unary(lhs)) return false;
|
||||||
|
for (;;) {
|
||||||
|
if (pos_ >= t_.size() || t_[pos_].kind != TK::Punct) return true;
|
||||||
|
const std::string op = t_[pos_].text;
|
||||||
|
const int p = precedence(op);
|
||||||
|
if (p == 0 || p < minPrec) return true;
|
||||||
|
++pos_;
|
||||||
|
Val rhs;
|
||||||
|
if (!binary(p + 1, rhs)) return false;
|
||||||
|
if (!apply(op, lhs, rhs)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool apply(const std::string& op, Val& a, const Val& b) {
|
||||||
|
const bool fl = a.isFloat || b.isFloat;
|
||||||
|
if (op == "||") { a = Val::ofInt(truthy(a) || truthy(b)); return true; }
|
||||||
|
if (op == "&&") { a = Val::ofInt(truthy(a) && truthy(b)); return true; }
|
||||||
|
if (op == "==" || op == "!=" || op == "<" || op == "<=" || op == ">" || op == ">=") {
|
||||||
|
const double x = a.asDouble(), y = b.asDouble();
|
||||||
|
const bool r = op == "==" ? x == y : op == "!=" ? x != y : op == "<" ? x < y : op == "<=" ? x <= y : op == ">" ? x > y : x >= y;
|
||||||
|
a = Val::ofInt(r);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (op == "+" || op == "-" || op == "*" || op == "/") {
|
||||||
|
if (fl) {
|
||||||
|
const double x = a.asDouble(), y = b.asDouble();
|
||||||
|
if (op == "/" && y == 0) return false;
|
||||||
|
a = Val::ofFloat(op == "+" ? x + y : op == "-" ? x - y : op == "*" ? x * y : x / y);
|
||||||
|
} else {
|
||||||
|
if (op == "/" && b.i == 0) return false;
|
||||||
|
a = Val::ofInt(op == "+" ? a.i + b.i : op == "-" ? a.i - b.i : op == "*" ? a.i * b.i : a.i / b.i);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (fl) return false; // the remaining operators are integer-only
|
||||||
|
if (op == "%") { if (b.i == 0) return false; a = Val::ofInt(a.i % b.i); return true; }
|
||||||
|
if (op == "|") { a = Val::ofInt(a.i | b.i); return true; }
|
||||||
|
if (op == "&") { a = Val::ofInt(a.i & b.i); return true; }
|
||||||
|
if (op == "^") { a = Val::ofInt(a.i ^ b.i); return true; }
|
||||||
|
if (op == "<<") { a = Val::ofInt(a.i << b.i); return true; }
|
||||||
|
if (op == ">>") { a = Val::ofInt(a.i >> b.i); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unary(Val& out) {
|
||||||
|
if (peekIs("!")) { ++pos_; if (!unary(out)) return false; out = Val::ofInt(!truthy(out)); return true; }
|
||||||
|
if (peekIs("-")) { ++pos_; if (!unary(out)) return false; out = out.isFloat ? Val::ofFloat(-out.d) : Val::ofInt(-out.i); return true; }
|
||||||
|
if (peekIs("+")) { ++pos_; return unary(out); }
|
||||||
|
if (peekIs("~")) { ++pos_; if (!unary(out) || out.isFloat) return false; out = Val::ofInt(~out.i); return true; }
|
||||||
|
return primary(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool primary(Val& out) {
|
||||||
|
if (pos_ >= t_.size()) return false;
|
||||||
|
const Tok& k = t_[pos_];
|
||||||
|
if (k.kind == TK::Punct && k.text == "(") {
|
||||||
|
++pos_;
|
||||||
|
if (!ternary(out) || !peekIs(")")) return false;
|
||||||
|
++pos_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (k.kind == TK::Number) {
|
||||||
|
++pos_;
|
||||||
|
return parseNumberLiteral(k.text, out);
|
||||||
|
}
|
||||||
|
if (k.kind != TK::Ident) return false;
|
||||||
|
++pos_;
|
||||||
|
if (k.text == "defined") {
|
||||||
|
bool paren = peekIs("(");
|
||||||
|
if (paren) ++pos_;
|
||||||
|
if (pos_ >= t_.size() || t_[pos_].kind != TK::Ident) return false;
|
||||||
|
const std::string name = t_[pos_++].text;
|
||||||
|
if (paren) {
|
||||||
|
if (!peekIs(")")) return false;
|
||||||
|
++pos_;
|
||||||
|
}
|
||||||
|
out = Val::ofInt(defined_ ? defined_(name) : false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (k.text == "true") { out = Val::ofInt(1); return true; }
|
||||||
|
if (k.text == "false") { out = Val::ofInt(0); return true; }
|
||||||
|
if (peekIs("(") && lenient_) { // function-like macro or __has_include(...): unknown, so 0
|
||||||
|
int depth = 0;
|
||||||
|
while (pos_ < t_.size()) {
|
||||||
|
if (peekIs("(")) ++depth;
|
||||||
|
else if (peekIs(")") && --depth == 0) { ++pos_; break; }
|
||||||
|
++pos_;
|
||||||
|
}
|
||||||
|
out = Val::ofInt(0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (lookup_ && lookup_(k.text, out)) return true;
|
||||||
|
if (lenient_) { out = Val::ofInt(0); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Preprocessor
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
struct Macro {
|
||||||
|
bool fnLike = false;
|
||||||
|
std::string body;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Unit {
|
||||||
|
std::string path;
|
||||||
|
std::vector<Tok> toks; // tokens of the ACTIVE regions, directives removed
|
||||||
|
std::map<int, std::string> docs; // line -> trailing // comment on that line
|
||||||
|
};
|
||||||
|
|
||||||
|
class Preprocessor {
|
||||||
|
public:
|
||||||
|
std::map<std::string, Macro> macros;
|
||||||
|
std::vector<std::pair<std::string, std::string>> defined; // every active object-like #define, in order
|
||||||
|
std::vector<std::string> notes;
|
||||||
|
|
||||||
|
Preprocessor() {
|
||||||
|
define("__cplusplus", "202002L"); // we want the C++ view of the headers: that is how they get compiled
|
||||||
|
define("__STDC__", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
void define(const std::string& name, const std::string& body) { macros[name] = {false, body}; }
|
||||||
|
|
||||||
|
// "NAME" or "NAME=VALUE" (command line -D)
|
||||||
|
void predefine(const std::string& spec) {
|
||||||
|
const size_t eq = spec.find('=');
|
||||||
|
if (eq == std::string::npos) define(spec, "1");
|
||||||
|
else define(spec.substr(0, eq), spec.substr(eq + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Unit run(const std::string& path) {
|
||||||
|
Unit u;
|
||||||
|
u.path = path;
|
||||||
|
std::ifstream in(path);
|
||||||
|
if (!in) die("cannot read " + path);
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
for (std::string l; std::getline(in, l);) {
|
||||||
|
if (!l.empty() && l.back() == '\r') l.pop_back();
|
||||||
|
lines.push_back(l);
|
||||||
|
}
|
||||||
|
struct Cond { bool parentActive, taken, active; };
|
||||||
|
std::vector<Cond> stack;
|
||||||
|
bool active = true, inBlock = false;
|
||||||
|
|
||||||
|
for (size_t ln = 0; ln < lines.size(); ++ln) {
|
||||||
|
const int lineNo = static_cast<int>(ln) + 1;
|
||||||
|
std::string raw = lines[ln];
|
||||||
|
while (!raw.empty() && raw.back() == '\\' && ln + 1 < lines.size()) { // line continuation
|
||||||
|
raw.pop_back();
|
||||||
|
raw += ' ' + lines[++ln];
|
||||||
|
}
|
||||||
|
std::string comment;
|
||||||
|
const std::string code = trim(stripComments(raw, inBlock, &comment));
|
||||||
|
if (!comment.empty()) u.docs[lineNo] = comment;
|
||||||
|
if (code.empty()) continue;
|
||||||
|
|
||||||
|
if (code[0] != '#') {
|
||||||
|
if (active) for (Tok& t : tokenize(code, lineNo)) u.toks.push_back(std::move(t));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size_t p = 1;
|
||||||
|
while (p < code.size() && std::isspace(static_cast<unsigned char>(code[p]))) ++p;
|
||||||
|
size_t q = p;
|
||||||
|
while (q < code.size() && isIdentChar(code[q])) ++q;
|
||||||
|
const std::string dir = code.substr(p, q - p);
|
||||||
|
const std::string rest = trim(code.substr(q));
|
||||||
|
|
||||||
|
if (dir == "if") {
|
||||||
|
const bool c = active && evalCondition(rest, path, lineNo);
|
||||||
|
stack.push_back({active, c, c});
|
||||||
|
active = c;
|
||||||
|
} else if (dir == "ifdef" || dir == "ifndef") {
|
||||||
|
const std::string name = firstIdent(rest);
|
||||||
|
const bool d = macros.count(name) != 0;
|
||||||
|
const bool c = active && (dir == "ifdef" ? d : !d);
|
||||||
|
stack.push_back({active, c, c});
|
||||||
|
active = c;
|
||||||
|
} else if (dir == "elif") {
|
||||||
|
if (stack.empty()) continue;
|
||||||
|
Cond& top = stack.back();
|
||||||
|
if (!top.parentActive || top.taken) top.active = false;
|
||||||
|
else { const bool c = evalCondition(rest, path, lineNo); top.active = c; top.taken = c; }
|
||||||
|
active = top.active;
|
||||||
|
} else if (dir == "else") {
|
||||||
|
if (stack.empty()) continue;
|
||||||
|
Cond& top = stack.back();
|
||||||
|
top.active = top.parentActive && !top.taken;
|
||||||
|
top.taken = true;
|
||||||
|
active = top.active;
|
||||||
|
} else if (dir == "endif") {
|
||||||
|
if (stack.empty()) continue;
|
||||||
|
active = stack.back().parentActive;
|
||||||
|
stack.pop_back();
|
||||||
|
} else if (active && dir == "define") {
|
||||||
|
size_t a = 0;
|
||||||
|
while (a < rest.size() && isIdentChar(rest[a])) ++a;
|
||||||
|
const std::string name = rest.substr(0, a);
|
||||||
|
if (name.empty()) continue;
|
||||||
|
if (a < rest.size() && rest[a] == '(') { // function-like: remember it exists, ignore the body
|
||||||
|
macros[name] = {true, ""};
|
||||||
|
} else {
|
||||||
|
const std::string body = trim(rest.substr(a));
|
||||||
|
macros[name] = {false, body};
|
||||||
|
defined.emplace_back(name, body);
|
||||||
|
}
|
||||||
|
} else if (active && dir == "undef") {
|
||||||
|
macros.erase(firstIdent(rest));
|
||||||
|
}
|
||||||
|
// #include, #pragma, #error, #warning, #line: nothing to do
|
||||||
|
}
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value of an object-like macro (recursively expanded), if it is a constant expression.
|
||||||
|
bool macroValue(const std::string& name, Val& out, int depth = 0) const {
|
||||||
|
const auto it = macros.find(name);
|
||||||
|
if (it == macros.end() || it->second.fnLike || depth > 16) return false;
|
||||||
|
const std::vector<Tok> toks = tokenize(it->second.body, 0);
|
||||||
|
if (toks.empty()) return false;
|
||||||
|
Eval ev(toks, [&](const std::string& id, Val& v) { return macroValue(id, v, depth + 1); },
|
||||||
|
[&](const std::string& id) { return macros.count(id) != 0; }, false);
|
||||||
|
return ev.run(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string firstIdent(const std::string& s) {
|
||||||
|
size_t a = 0;
|
||||||
|
while (a < s.size() && !isIdentStart(s[a])) ++a;
|
||||||
|
size_t b = a;
|
||||||
|
while (b < s.size() && isIdentChar(s[b])) ++b;
|
||||||
|
return s.substr(a, b - a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes comments (keeping string literals intact); `lineComment` receives the text of a trailing // comment.
|
||||||
|
static std::string stripComments(const std::string& raw, bool& inBlock, std::string* lineComment) {
|
||||||
|
std::string out;
|
||||||
|
bool inStr = false;
|
||||||
|
char quote = 0;
|
||||||
|
for (size_t i = 0; i < raw.size(); ++i) {
|
||||||
|
const char c = raw[i];
|
||||||
|
if (inBlock) {
|
||||||
|
if (c == '*' && i + 1 < raw.size() && raw[i + 1] == '/') { inBlock = false; ++i; out += ' '; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inStr) {
|
||||||
|
out += c;
|
||||||
|
if (c == '\\' && i + 1 < raw.size()) out += raw[++i];
|
||||||
|
else if (c == quote) inStr = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '"' || c == '\'') { inStr = true; quote = c; out += c; continue; }
|
||||||
|
if (c == '/' && i + 1 < raw.size() && raw[i + 1] == '/') {
|
||||||
|
if (lineComment) *lineComment = trim(raw.substr(i + 2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (c == '/' && i + 1 < raw.size() && raw[i + 1] == '*') { inBlock = true; ++i; continue; }
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool evalCondition(const std::string& expr, const std::string& path, int line) {
|
||||||
|
const std::vector<Tok> toks = tokenize(expr, line);
|
||||||
|
Eval ev(toks, [&](const std::string& id, Val& v) {
|
||||||
|
if (!macroValue(id, v)) v = Val::ofInt(0);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[&](const std::string& id) { return macros.count(id) != 0; }, true);
|
||||||
|
Val v;
|
||||||
|
if (!ev.run(v)) {
|
||||||
|
notes.push_back(fmt("%s:%d: could not evaluate '#if %s'; treated as false", path.c_str(), line, expr.c_str()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return v.isFloat ? v.d != 0 : v.i != 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace gen
|
||||||
@@ -0,0 +1,693 @@
|
|||||||
|
// gfx_gen, part 2: the declarations found in the headers, and the rules for what can be wrapped.
|
||||||
|
//
|
||||||
|
// Parser: typedef struct / enum / alias / callback, RLAPI / RMAPI functions, numeric and colour #defines.
|
||||||
|
// Types: alias resolution, the mapping of raylib's Vector2/Vector3/Quaternion/Color/BoundingBox onto the
|
||||||
|
// engine's own Vec2/Vec3/Quat/Rgba/Aabb, and the verdict (wrappable or not, and why) for every
|
||||||
|
// struct and function. Whatever cannot be wrapped safely is skipped and listed in the report.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "lex.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace gen {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Data model
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
struct Type {
|
||||||
|
std::string base; // builtin spelled canonically ("unsigned int"), or a typedef / struct / enum name
|
||||||
|
int ptr = 0;
|
||||||
|
bool constBase = false; // `const` applies to the pointee (or to the value when ptr == 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Field {
|
||||||
|
Type type;
|
||||||
|
std::string name;
|
||||||
|
std::vector<long long> dims; // array extents, empty = plain field
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StructDef {
|
||||||
|
std::string name;
|
||||||
|
std::vector<Field> fields;
|
||||||
|
bool opaque = false; // declared, never defined (rAudioBuffer): usable behind a pointer only
|
||||||
|
int group = 0;
|
||||||
|
bool ok = true; // parsed completely
|
||||||
|
std::string why;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EnumItem { std::string name; long long value = 0; };
|
||||||
|
struct EnumDef {
|
||||||
|
std::string name; // may be empty (anonymous)
|
||||||
|
std::vector<EnumItem> items;
|
||||||
|
int group = 0;
|
||||||
|
bool ok = true;
|
||||||
|
std::string why;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Param {
|
||||||
|
Type type;
|
||||||
|
std::string name;
|
||||||
|
bool unsupported = false;
|
||||||
|
std::string why;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CallbackDef {
|
||||||
|
std::string name;
|
||||||
|
Type ret;
|
||||||
|
std::vector<Param> params;
|
||||||
|
int group = 0;
|
||||||
|
bool ok = true;
|
||||||
|
std::string why;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FuncDef {
|
||||||
|
std::string name;
|
||||||
|
Type ret;
|
||||||
|
std::vector<Param> params;
|
||||||
|
bool variadic = false;
|
||||||
|
bool retUnsupported = false;
|
||||||
|
std::string retWhy;
|
||||||
|
bool hasBody = false; // RMAPI (raymath): the declaration is followed by a body
|
||||||
|
int group = 0;
|
||||||
|
std::string doc;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ConstDef {
|
||||||
|
std::string name;
|
||||||
|
Val value;
|
||||||
|
bool isFloat32 = false; // the literal had an f suffix
|
||||||
|
std::string str; // set for string constants
|
||||||
|
int group = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ColorDef { std::string name; int r = 0, g = 0, b = 0, a = 255; int group = 0; };
|
||||||
|
struct MacroAlias { std::string name, target; int group = 0; };
|
||||||
|
|
||||||
|
struct ModelData {
|
||||||
|
std::vector<StructDef> structs;
|
||||||
|
std::map<std::string, size_t> structIdx;
|
||||||
|
std::vector<EnumDef> enums;
|
||||||
|
std::map<std::string, long long> enumerators;
|
||||||
|
std::map<std::string, std::string> enumOf; // enumerator -> enum type name (possibly empty)
|
||||||
|
std::map<std::string, Type> aliases; // typedef NAME -> target
|
||||||
|
std::vector<std::string> aliasOrder;
|
||||||
|
std::map<std::string, int> aliasGroup;
|
||||||
|
std::vector<CallbackDef> callbacks;
|
||||||
|
std::map<std::string, size_t> callbackIdx;
|
||||||
|
std::vector<FuncDef> funcs;
|
||||||
|
std::vector<ConstDef> consts;
|
||||||
|
std::vector<ColorDef> colors;
|
||||||
|
std::vector<MacroAlias> macroAliases;
|
||||||
|
std::vector<std::string> notes;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool isBuiltinWord(const std::string& w) {
|
||||||
|
static const std::set<std::string> s = {"unsigned", "signed", "short", "long", "int", "char", "float", "double", "void", "_Bool", "bool"};
|
||||||
|
return s.count(w) != 0;
|
||||||
|
}
|
||||||
|
inline bool isQualifier(const std::string& w) {
|
||||||
|
static const std::set<std::string> s = {"const", "volatile", "restrict", "__restrict", "register", "static", "extern", "inline", "__inline", "__inline__"};
|
||||||
|
return s.count(w) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::optional<std::string> canonicalBuiltin(const std::vector<std::string>& words) {
|
||||||
|
int nLong = 0, nShort = 0;
|
||||||
|
bool uns = false, sig = false;
|
||||||
|
std::string core;
|
||||||
|
for (const std::string& w : words) {
|
||||||
|
if (w == "unsigned") uns = true;
|
||||||
|
else if (w == "signed") sig = true;
|
||||||
|
else if (w == "long") ++nLong;
|
||||||
|
else if (w == "short") ++nShort;
|
||||||
|
else if (w == "int" || w == "char" || w == "float" || w == "double" || w == "void" || w == "_Bool" || w == "bool") {
|
||||||
|
if (!core.empty()) return std::nullopt;
|
||||||
|
core = w;
|
||||||
|
} else return std::nullopt;
|
||||||
|
}
|
||||||
|
if (core == "float" || core == "double" || core == "void") return nLong || nShort || uns || sig ? std::nullopt : std::optional<std::string>(core);
|
||||||
|
if (core == "_Bool" || core == "bool") return std::string("bool");
|
||||||
|
if (core == "char") return uns ? "unsigned char" : sig ? "signed char" : "char";
|
||||||
|
if (nShort) return uns ? "unsigned short" : "short";
|
||||||
|
if (nLong == 1) return uns ? "unsigned long" : "long";
|
||||||
|
if (nLong == 2) return uns ? "unsigned long long" : "long long";
|
||||||
|
if (nLong > 2) return std::nullopt;
|
||||||
|
return uns ? "unsigned int" : "int";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Parser
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
class Parser {
|
||||||
|
public:
|
||||||
|
Parser(ModelData& m, Preprocessor& pp, int group) : m_(m), pp_(pp), group_(group) {}
|
||||||
|
|
||||||
|
void parse(const Unit& u) {
|
||||||
|
t_ = &u.toks;
|
||||||
|
docs_ = &u.docs;
|
||||||
|
const std::vector<Tok>& t = *t_;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < t.size()) {
|
||||||
|
const Tok& k = t[i];
|
||||||
|
if (k.kind == TK::Ident && k.text == "extern" && i + 2 < t.size() && t[i + 1].kind == TK::String && t[i + 2].text == "{") { i += 3; continue; }
|
||||||
|
if (k.text == "}" || k.text == ";") { ++i; continue; }
|
||||||
|
if (k.kind == TK::Ident && k.text == "typedef") { i = typedefStmt(i); continue; }
|
||||||
|
if (k.kind == TK::Ident && (k.text == "RLAPI" || k.text == "RMAPI")) { i = functionStmt(i); continue; }
|
||||||
|
i = skipStmt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
ModelData& m_;
|
||||||
|
Preprocessor& pp_;
|
||||||
|
int group_;
|
||||||
|
const std::vector<Tok>* t_ = nullptr;
|
||||||
|
const std::map<int, std::string>* docs_ = nullptr;
|
||||||
|
|
||||||
|
struct Decl {
|
||||||
|
std::vector<std::string> words;
|
||||||
|
int ptr = 0;
|
||||||
|
bool constBase = false;
|
||||||
|
std::vector<long long> dims;
|
||||||
|
bool hasArray = false, hasParen = false, ok = true;
|
||||||
|
std::string why;
|
||||||
|
};
|
||||||
|
|
||||||
|
size_t matchBrace(size_t j) const {
|
||||||
|
const std::vector<Tok>& t = *t_;
|
||||||
|
int depth = 0;
|
||||||
|
for (size_t k = j; k < t.size(); ++k) {
|
||||||
|
if (t[k].text == "{") ++depth;
|
||||||
|
else if (t[k].text == "}" && --depth == 0) return k;
|
||||||
|
}
|
||||||
|
return t.size() - 1;
|
||||||
|
}
|
||||||
|
size_t matchParen(size_t j) const {
|
||||||
|
const std::vector<Tok>& t = *t_;
|
||||||
|
int depth = 0;
|
||||||
|
for (size_t k = j; k < t.size(); ++k) {
|
||||||
|
if (t[k].text == "(") ++depth;
|
||||||
|
else if (t[k].text == ")" && --depth == 0) return k;
|
||||||
|
}
|
||||||
|
return t.size() - 1;
|
||||||
|
}
|
||||||
|
size_t skipStmt(size_t i) const {
|
||||||
|
const std::vector<Tok>& t = *t_;
|
||||||
|
int depth = 0;
|
||||||
|
for (size_t j = i; j < t.size(); ++j) {
|
||||||
|
if (t[j].text == "{") ++depth;
|
||||||
|
else if (t[j].text == "}") {
|
||||||
|
if (depth == 0) return j + 1;
|
||||||
|
if (--depth == 0) {
|
||||||
|
++j;
|
||||||
|
if (j < t.size() && t[j].text == ";") ++j;
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
} else if (t[j].text == ";" && depth == 0) return j + 1;
|
||||||
|
}
|
||||||
|
return t.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool evalConst(const std::vector<Tok>& toks, Val& out) const {
|
||||||
|
Eval ev(toks,
|
||||||
|
[this](const std::string& id, Val& v) {
|
||||||
|
const auto it = m_.enumerators.find(id);
|
||||||
|
if (it != m_.enumerators.end()) { v = Val::ofInt(it->second); return true; }
|
||||||
|
return pp_.macroValue(id, v);
|
||||||
|
},
|
||||||
|
[this](const std::string& id) { return pp_.macros.count(id) != 0; }, false);
|
||||||
|
return ev.run(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits t[from, to) at top-level commas.
|
||||||
|
std::vector<std::pair<size_t, size_t>> splitCommas(const std::vector<Tok>& t, size_t from, size_t to) const {
|
||||||
|
std::vector<std::pair<size_t, size_t>> parts;
|
||||||
|
int depth = 0;
|
||||||
|
size_t start = from;
|
||||||
|
for (size_t k = from; k < to; ++k) {
|
||||||
|
const std::string& s = t[k].text;
|
||||||
|
if (s == "(" || s == "[" || s == "{") ++depth;
|
||||||
|
else if (s == ")" || s == "]" || s == "}") --depth;
|
||||||
|
else if (s == "," && depth == 0) { parts.emplace_back(start, k); start = k + 1; }
|
||||||
|
}
|
||||||
|
parts.emplace_back(start, to);
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
Decl scanDecl(const std::vector<Tok>& t, size_t from, size_t to) const {
|
||||||
|
Decl d;
|
||||||
|
for (size_t k = from; k < to; ++k) {
|
||||||
|
const Tok& tk = t[k];
|
||||||
|
if (tk.kind == TK::Ident) {
|
||||||
|
if (tk.text == "const") { if (d.ptr == 0) d.constBase = true; }
|
||||||
|
else if (isQualifier(tk.text) || tk.text == "struct" || tk.text == "enum" || tk.text == "union") {}
|
||||||
|
else d.words.push_back(tk.text);
|
||||||
|
} else if (tk.text == "*") {
|
||||||
|
++d.ptr;
|
||||||
|
} else if (tk.text == "[") {
|
||||||
|
size_t e = k + 1;
|
||||||
|
int depth = 1;
|
||||||
|
while (e < to && depth > 0) {
|
||||||
|
if (t[e].text == "[") ++depth;
|
||||||
|
else if (t[e].text == "]") --depth;
|
||||||
|
if (depth > 0) ++e;
|
||||||
|
}
|
||||||
|
d.hasArray = true;
|
||||||
|
const std::vector<Tok> expr(t.begin() + static_cast<long>(k) + 1, t.begin() + static_cast<long>(e));
|
||||||
|
Val v;
|
||||||
|
if (expr.empty()) d.dims.push_back(0);
|
||||||
|
else if (evalConst(expr, v) && !v.isFloat && v.i >= 0) d.dims.push_back(v.i);
|
||||||
|
else { d.ok = false; d.why = "array size is not a constant"; }
|
||||||
|
k = e;
|
||||||
|
} else if (tk.text == "(" || tk.text == ")") {
|
||||||
|
d.hasParen = true;
|
||||||
|
} else {
|
||||||
|
d.ok = false;
|
||||||
|
d.why = "unexpected '" + tk.text + "'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declarator that carries a name (fields, parameters, typedef aliases) -> type + name. A parameter may be unnamed.
|
||||||
|
static bool typeAndName(const Decl& d, bool expectName, Type& type, std::string& name) {
|
||||||
|
std::vector<std::string> w = d.words;
|
||||||
|
name.clear();
|
||||||
|
if (expectName) {
|
||||||
|
if (w.size() >= 2 && !isBuiltinWord(w.back())) { name = w.back(); w.pop_back(); }
|
||||||
|
}
|
||||||
|
if (w.empty()) return false;
|
||||||
|
bool anyBuiltin = false, anyOther = false;
|
||||||
|
for (const std::string& x : w) (isBuiltinWord(x) ? anyBuiltin : anyOther) = true;
|
||||||
|
if (anyBuiltin && !anyOther) {
|
||||||
|
const auto c = canonicalBuiltin(w);
|
||||||
|
if (!c) return false;
|
||||||
|
type.base = *c;
|
||||||
|
} else if (!anyBuiltin && w.size() == 1) {
|
||||||
|
type.base = w[0];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
type.ptr = d.ptr;
|
||||||
|
type.constBase = d.constBase;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// typedef ... ;
|
||||||
|
size_t typedefStmt(size_t i) {
|
||||||
|
const std::vector<Tok>& t = *t_;
|
||||||
|
const size_t n = t.size();
|
||||||
|
size_t j = i + 1;
|
||||||
|
if (j < n && t[j].kind == TK::Ident && (t[j].text == "struct" || t[j].text == "enum")) {
|
||||||
|
const bool isEnum = t[j].text == "enum";
|
||||||
|
size_t k = j + 1;
|
||||||
|
std::string tag;
|
||||||
|
if (k < n && t[k].kind == TK::Ident) { tag = t[k].text; ++k; }
|
||||||
|
if (k < n && t[k].text == "{") {
|
||||||
|
const size_t close = matchBrace(k);
|
||||||
|
const std::vector<Tok> body(t.begin() + static_cast<long>(k) + 1, t.begin() + static_cast<long>(close));
|
||||||
|
size_t e = close + 1;
|
||||||
|
std::vector<std::string> names;
|
||||||
|
while (e < n && t[e].text != ";") {
|
||||||
|
if (t[e].kind == TK::Ident) names.push_back(t[e].text);
|
||||||
|
++e;
|
||||||
|
}
|
||||||
|
const std::string name = names.empty() ? tag : names[0];
|
||||||
|
if (isEnum) addEnum(name, body);
|
||||||
|
else addStruct(name, body);
|
||||||
|
for (size_t x = 1; x < names.size(); ++x) addAlias(names[x], Type{name, 0, false});
|
||||||
|
return e < n ? e + 1 : e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size_t end = j;
|
||||||
|
int depth = 0;
|
||||||
|
while (end < n && !(t[end].text == ";" && depth == 0)) {
|
||||||
|
if (t[end].text == "(") ++depth;
|
||||||
|
else if (t[end].text == ")") --depth;
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
const size_t next = end < n ? end + 1 : end;
|
||||||
|
// function pointer: typedef RET (*NAME)(PARAMS);
|
||||||
|
size_t open = j;
|
||||||
|
while (open < end && t[open].text != "(") ++open;
|
||||||
|
if (open < end) {
|
||||||
|
if (open + 3 < end && t[open + 1].text == "*" && t[open + 2].kind == TK::Ident && t[open + 3].text == ")") addCallback(i, j, open, end);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
const Decl d = scanDecl(t, j, end);
|
||||||
|
Type type;
|
||||||
|
std::string name;
|
||||||
|
if (!d.ok || d.hasArray || !typeAndName(d, true, type, name) || name.empty()) return next;
|
||||||
|
if (type.base == "bool" && name == "bool") return next;
|
||||||
|
addAlias(name, type);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addAlias(const std::string& name, const Type& type) {
|
||||||
|
if (name == type.base && type.ptr == 0) { // typedef struct X X; -> an opaque struct
|
||||||
|
if (!m_.structIdx.count(name)) {
|
||||||
|
StructDef s;
|
||||||
|
s.name = name;
|
||||||
|
s.opaque = true;
|
||||||
|
s.group = group_;
|
||||||
|
m_.structIdx[name] = m_.structs.size();
|
||||||
|
m_.structs.push_back(s);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_.aliases.count(name)) return;
|
||||||
|
if (type.ptr == 0 && !isBuiltinWord(type.base) && !m_.structIdx.count(type.base) && !m_.aliases.count(type.base) &&
|
||||||
|
!m_.callbackIdx.count(type.base) && !isExternalName(type.base)) {
|
||||||
|
// typedef struct Tag Name; for a tag that was never defined: opaque
|
||||||
|
StructDef s;
|
||||||
|
s.name = type.base;
|
||||||
|
s.opaque = true;
|
||||||
|
s.group = group_;
|
||||||
|
m_.structIdx[s.name] = m_.structs.size();
|
||||||
|
m_.structs.push_back(s);
|
||||||
|
}
|
||||||
|
m_.aliases[name] = type;
|
||||||
|
m_.aliasOrder.push_back(name);
|
||||||
|
m_.aliasGroup[name] = group_;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
static bool isExternalName(const std::string& n) {
|
||||||
|
static const std::set<std::string> s = {"size_t", "ptrdiff_t", "intptr_t", "uintptr_t", "int8_t", "int16_t", "int32_t", "int64_t",
|
||||||
|
"uint8_t", "uint16_t", "uint32_t", "uint64_t", "va_list"};
|
||||||
|
return s.count(n) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void addStruct(const std::string& name, const std::vector<Tok>& body) {
|
||||||
|
if (m_.structIdx.count(name) && !m_.structs[m_.structIdx[name]].opaque) return; // first definition wins
|
||||||
|
StructDef s;
|
||||||
|
s.name = name;
|
||||||
|
s.group = group_;
|
||||||
|
size_t start = 0;
|
||||||
|
int depth = 0;
|
||||||
|
for (size_t k = 0; k <= body.size(); ++k) {
|
||||||
|
const bool end = k == body.size();
|
||||||
|
if (end || (body[k].text == ";" && depth == 0)) {
|
||||||
|
if (k > start) fieldStmt(s, body, start, k);
|
||||||
|
start = k + 1;
|
||||||
|
} else if (body[k].text == "{" || body[k].text == "(" || body[k].text == "[") ++depth;
|
||||||
|
else if (body[k].text == "}" || body[k].text == ")" || body[k].text == "]") --depth;
|
||||||
|
}
|
||||||
|
const auto it = m_.structIdx.find(name);
|
||||||
|
if (it != m_.structIdx.end()) m_.structs[it->second] = s; // replaces the opaque forward declaration
|
||||||
|
else { m_.structIdx[name] = m_.structs.size(); m_.structs.push_back(s); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void fieldStmt(StructDef& s, const std::vector<Tok>& b, size_t from, size_t to) {
|
||||||
|
const auto parts = splitCommas(b, from, to);
|
||||||
|
Type base;
|
||||||
|
for (size_t p = 0; p < parts.size(); ++p) {
|
||||||
|
const Decl d = scanDecl(b, parts[p].first, parts[p].second);
|
||||||
|
if (!d.ok || d.hasParen) { s.ok = false; s.why = d.hasParen ? "function pointer or nested aggregate field" : d.why; return; }
|
||||||
|
Field f;
|
||||||
|
if (p == 0) {
|
||||||
|
std::string name;
|
||||||
|
if (!typeAndName(d, true, base, name) || name.empty()) { s.ok = false; s.why = "cannot read a field declaration"; return; }
|
||||||
|
f.type = base;
|
||||||
|
f.name = name;
|
||||||
|
} else {
|
||||||
|
if (d.words.size() != 1) { s.ok = false; s.why = "cannot read a field declaration"; return; }
|
||||||
|
f.type = base;
|
||||||
|
f.type.ptr = d.ptr;
|
||||||
|
f.name = d.words[0];
|
||||||
|
}
|
||||||
|
f.dims = d.dims;
|
||||||
|
s.fields.push_back(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addEnum(const std::string& name, const std::vector<Tok>& body) {
|
||||||
|
EnumDef e;
|
||||||
|
e.name = name;
|
||||||
|
e.group = group_;
|
||||||
|
if (name == "bool") return;
|
||||||
|
long long next = 0;
|
||||||
|
for (const auto& part : splitCommas(body, 0, body.size())) {
|
||||||
|
if (part.first >= part.second) continue; // trailing comma
|
||||||
|
if (body[part.first].kind != TK::Ident) { e.ok = false; e.why = "unexpected token in enum"; break; }
|
||||||
|
EnumItem item;
|
||||||
|
item.name = body[part.first].text;
|
||||||
|
if (part.first + 1 < part.second) {
|
||||||
|
if (body[part.first + 1].text != "=") { e.ok = false; e.why = "unexpected token in enum"; break; }
|
||||||
|
const std::vector<Tok> expr(body.begin() + static_cast<long>(part.first) + 2, body.begin() + static_cast<long>(part.second));
|
||||||
|
Val v;
|
||||||
|
if (!evalConst(expr, v) || v.isFloat) { e.ok = false; e.why = "value of " + item.name + " is not an integer constant"; break; }
|
||||||
|
next = v.i;
|
||||||
|
}
|
||||||
|
item.value = next++;
|
||||||
|
m_.enumerators[item.name] = item.value;
|
||||||
|
m_.enumOf[item.name] = name;
|
||||||
|
e.items.push_back(item);
|
||||||
|
}
|
||||||
|
m_.enums.push_back(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// typedef RET (*NAME)(PARAMS); tokens: i = typedef, j = first token after it, open = the '(' before '*'
|
||||||
|
void addCallback(size_t /*i*/, size_t j, size_t open, size_t end) {
|
||||||
|
const std::vector<Tok>& t = *t_;
|
||||||
|
CallbackDef c;
|
||||||
|
c.name = t[open + 2].text;
|
||||||
|
c.group = group_;
|
||||||
|
const Decl rd = scanDecl(t, j, open);
|
||||||
|
std::string unused;
|
||||||
|
if (!rd.ok || rd.hasParen || rd.hasArray || !typeAndName(rd, false, c.ret, unused)) { c.ok = false; c.why = "unreadable return type"; }
|
||||||
|
size_t po = open + 4;
|
||||||
|
if (po < end && t[po].text == "(") {
|
||||||
|
const size_t pc = matchParen(po);
|
||||||
|
parseParams(po + 1, pc, c.params, nullptr);
|
||||||
|
for (const Param& p : c.params) if (p.unsupported) { c.ok = false; c.why = p.why; }
|
||||||
|
} else {
|
||||||
|
c.ok = false;
|
||||||
|
c.why = "unreadable parameter list";
|
||||||
|
}
|
||||||
|
if (m_.callbackIdx.count(c.name)) return;
|
||||||
|
m_.callbackIdx[c.name] = m_.callbacks.size();
|
||||||
|
m_.callbacks.push_back(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseParams(size_t from, size_t to, std::vector<Param>& out, bool* variadic) {
|
||||||
|
const std::vector<Tok>& t = *t_;
|
||||||
|
if (from >= to) return;
|
||||||
|
if (to == from + 1 && t[from].text == "void") return;
|
||||||
|
for (const auto& part : splitCommas(t, from, to)) {
|
||||||
|
Param p;
|
||||||
|
if (part.second == part.first + 1 && t[part.first].text == "...") {
|
||||||
|
if (variadic) *variadic = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Decl d = scanDecl(t, part.first, part.second);
|
||||||
|
if (!d.ok || d.hasParen || d.hasArray) {
|
||||||
|
p.unsupported = true;
|
||||||
|
p.why = d.hasParen ? "function-pointer parameter" : d.hasArray ? "array parameter" : d.why;
|
||||||
|
} else if (!typeAndName(d, true, p.type, p.name)) {
|
||||||
|
p.unsupported = true;
|
||||||
|
p.why = "unreadable parameter";
|
||||||
|
}
|
||||||
|
out.push_back(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t functionStmt(size_t i) {
|
||||||
|
const std::vector<Tok>& t = *t_;
|
||||||
|
const size_t n = t.size();
|
||||||
|
const bool body = t[i].text == "RMAPI";
|
||||||
|
size_t open = i + 1;
|
||||||
|
while (open < n && t[open].text != "(" && t[open].text != ";") ++open;
|
||||||
|
if (open >= n || t[open].text != "(" || open == i + 1 || t[open - 1].kind != TK::Ident) return skipStmt(i);
|
||||||
|
FuncDef f;
|
||||||
|
f.name = t[open - 1].text;
|
||||||
|
f.group = group_;
|
||||||
|
f.hasBody = body;
|
||||||
|
const Decl rd = scanDecl(t, i + 1, open - 1);
|
||||||
|
std::string unused;
|
||||||
|
if (!rd.ok || rd.hasParen || rd.hasArray || !typeAndName(rd, false, f.ret, unused)) {
|
||||||
|
f.retUnsupported = true;
|
||||||
|
f.retWhy = "unreadable return type";
|
||||||
|
}
|
||||||
|
const size_t close = matchParen(open);
|
||||||
|
parseParams(open + 1, close, f.params, &f.variadic);
|
||||||
|
size_t after = close + 1;
|
||||||
|
if (body) {
|
||||||
|
while (after < n && t[after].text != "{" && t[after].text != ";") ++after;
|
||||||
|
if (after < n && t[after].text == "{") after = matchBrace(after) + 1;
|
||||||
|
else if (after < n) ++after;
|
||||||
|
} else {
|
||||||
|
if (after < n && t[after].text == ";") ++after;
|
||||||
|
}
|
||||||
|
const size_t last = std::min(close + 1, n - 1);
|
||||||
|
for (size_t probe : {last, close}) {
|
||||||
|
const auto it = docs_->find(t[probe].line);
|
||||||
|
if (it != docs_->end()) { f.doc = it->second; break; }
|
||||||
|
}
|
||||||
|
m_.funcs.push_back(f);
|
||||||
|
return after;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
// Types: what is wrappable
|
||||||
|
// ---------------------------------------------------------------------------------------------------------
|
||||||
|
enum class Cat { Void, Builtin, ExtBuiltin, Mapped, Struct, Opaque, Enum, Callback, Unknown };
|
||||||
|
|
||||||
|
struct Res {
|
||||||
|
Type t; // alias resolved
|
||||||
|
Cat cat = Cat::Unknown;
|
||||||
|
std::string why;
|
||||||
|
bool ok() const { return cat != Cat::Unknown; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class Types {
|
||||||
|
public:
|
||||||
|
explicit Types(ModelData& m) : m_(m) { decideMappings(); decideStructs(); }
|
||||||
|
|
||||||
|
// raylib name -> engine name for the types that share the engine's layout
|
||||||
|
std::map<std::string, std::string> mapped;
|
||||||
|
bool matrixMapped = false; // by-value Matrix <-> engine Mat4
|
||||||
|
std::vector<int> matrixIndex; // field i of struct Matrix is element matrixIndex[i] of a Mat4
|
||||||
|
|
||||||
|
Res classify(Type t) const {
|
||||||
|
Res r;
|
||||||
|
for (int guard = 0; guard < 16; ++guard) {
|
||||||
|
if (mapped.count(t.base)) break;
|
||||||
|
const auto it = m_.aliases.find(t.base);
|
||||||
|
if (it == m_.aliases.end()) break;
|
||||||
|
t.base = it->second.base;
|
||||||
|
t.ptr += it->second.ptr;
|
||||||
|
t.constBase = t.constBase || it->second.constBase;
|
||||||
|
}
|
||||||
|
r.t = t;
|
||||||
|
const std::string& b = t.base;
|
||||||
|
if (b == "void") { r.cat = t.ptr == 0 ? Cat::Void : Cat::Builtin; return r; }
|
||||||
|
if (isBuiltin(b)) { r.cat = Cat::Builtin; return r; }
|
||||||
|
if (Parser::isExternalName(b)) { r.cat = Cat::ExtBuiltin; return r; }
|
||||||
|
if (mapped.count(b)) { r.cat = Cat::Mapped; return r; }
|
||||||
|
if (m_.callbackIdx.count(b)) {
|
||||||
|
const CallbackDef& c = m_.callbacks[m_.callbackIdx.at(b)];
|
||||||
|
if (t.ptr != 0) r.why = "pointer to function-pointer typedef " + b;
|
||||||
|
else if (!c.ok) r.why = "callback " + b + ": " + c.why;
|
||||||
|
else if (!callbackSupported(c)) r.why = "callback " + b + " uses types other than plain C types";
|
||||||
|
else r.cat = Cat::Callback;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
const auto s = m_.structIdx.find(b);
|
||||||
|
if (s != m_.structIdx.end()) {
|
||||||
|
const StructDef& sd = m_.structs[s->second];
|
||||||
|
if (sd.opaque) {
|
||||||
|
if (t.ptr == 0) r.why = "opaque type " + b + " by value";
|
||||||
|
else r.cat = Cat::Opaque;
|
||||||
|
} else if (!sd.ok) r.why = "struct " + b + ": " + sd.why;
|
||||||
|
else r.cat = Cat::Struct;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
for (const EnumDef& e : m_.enums)
|
||||||
|
if (e.name == b && !b.empty()) {
|
||||||
|
if (e.ok) r.cat = Cat::Enum;
|
||||||
|
else r.why = "enum " + b + ": " + e.why;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
r.why = "unknown type " + b;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A struct is mirrored iff every field is a type we know. (Computed once, in declaration order.)
|
||||||
|
bool structOk(const std::string& name) const {
|
||||||
|
const auto it = m_.structIdx.find(name);
|
||||||
|
return it != m_.structIdx.end() && !m_.structs[it->second].opaque && m_.structs[it->second].ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isBuiltin(const std::string& b) {
|
||||||
|
static const std::set<std::string> s = {"char", "signed char", "unsigned char", "short", "unsigned short", "int", "unsigned int", "long",
|
||||||
|
"unsigned long", "long long", "unsigned long long", "float", "double", "bool"};
|
||||||
|
return s.count(b) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool callbackSupported(const CallbackDef& c) const {
|
||||||
|
auto plain = [&](const Type& t0) {
|
||||||
|
const Res r = classify(t0);
|
||||||
|
return r.cat == Cat::Void || r.cat == Cat::Builtin || r.cat == Cat::ExtBuiltin;
|
||||||
|
};
|
||||||
|
if (!plain(c.ret)) return false;
|
||||||
|
for (const Param& p : c.params) if (p.unsupported || !plain(p.type)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
ModelData& m_;
|
||||||
|
|
||||||
|
const StructDef* find(const std::string& n) const {
|
||||||
|
const auto it = m_.structIdx.find(n);
|
||||||
|
return it == m_.structIdx.end() ? nullptr : &m_.structs[it->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shape(const std::string& structName, const std::vector<std::pair<std::string, std::string>>& fields) const {
|
||||||
|
const StructDef* s = find(structName);
|
||||||
|
if (!s || s->opaque || !s->ok || s->fields.size() != fields.size()) return false;
|
||||||
|
for (size_t i = 0; i < fields.size(); ++i) {
|
||||||
|
const Field& f = s->fields[i];
|
||||||
|
if (!f.dims.empty() || f.type.ptr != 0 || f.name != fields[i].second) return false;
|
||||||
|
std::string base = f.type.base;
|
||||||
|
const auto a = m_.aliases.find(base);
|
||||||
|
if (a != m_.aliases.end() && a->second.ptr == 0) base = a->second.base;
|
||||||
|
if (base != fields[i].first) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void decideMappings() {
|
||||||
|
if (shape("Vector2", {{"float", "x"}, {"float", "y"}})) mapped["Vector2"] = "Vec2";
|
||||||
|
if (shape("Vector3", {{"float", "x"}, {"float", "y"}, {"float", "z"}})) mapped["Vector3"] = "Vec3";
|
||||||
|
if (shape("Vector4", {{"float", "x"}, {"float", "y"}, {"float", "z"}, {"float", "w"}})) {
|
||||||
|
const auto q = m_.aliases.find("Quaternion");
|
||||||
|
if (q != m_.aliases.end() && q->second.base == "Vector4" && q->second.ptr == 0) mapped["Quaternion"] = "Quat";
|
||||||
|
}
|
||||||
|
if (shape("Color", {{"unsigned char", "r"}, {"unsigned char", "g"}, {"unsigned char", "b"}, {"unsigned char", "a"}})) mapped["Color"] = "Rgba";
|
||||||
|
if (mapped.count("Vector3") && shape("BoundingBox", {{"Vector3", "min"}, {"Vector3", "max"}})) mapped["BoundingBox"] = "Aabb";
|
||||||
|
|
||||||
|
if (const StructDef* s = find("Matrix"); s && !s->opaque && s->ok && s->fields.size() == 16) {
|
||||||
|
std::vector<int> idx;
|
||||||
|
std::set<int> seen;
|
||||||
|
bool good = true;
|
||||||
|
for (const Field& f : s->fields) {
|
||||||
|
if (f.type.base != "float" || f.type.ptr != 0 || !f.dims.empty() || f.name.size() < 2 || f.name[0] != 'm') { good = false; break; }
|
||||||
|
char* end = nullptr;
|
||||||
|
const long v = std::strtol(f.name.c_str() + 1, &end, 10);
|
||||||
|
if (!end || *end || v < 0 || v > 15 || !seen.insert(static_cast<int>(v)).second) { good = false; break; }
|
||||||
|
idx.push_back(static_cast<int>(v));
|
||||||
|
}
|
||||||
|
if (good) { matrixMapped = true; matrixIndex = idx; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void decideStructs() {
|
||||||
|
for (StructDef& s : m_.structs) {
|
||||||
|
if (s.opaque || !s.ok) continue;
|
||||||
|
for (const Field& f : s.fields) {
|
||||||
|
const Res r = classify(f.type);
|
||||||
|
bool good = false;
|
||||||
|
switch (r.cat) {
|
||||||
|
case Cat::Builtin: case Cat::Mapped: case Cat::Struct: case Cat::Enum: good = true; break;
|
||||||
|
case Cat::ExtBuiltin: good = f.type.base != "va_list"; break;
|
||||||
|
case Cat::Opaque: good = r.t.ptr > 0; break;
|
||||||
|
case Cat::Void: good = false; break;
|
||||||
|
case Cat::Callback: good = false; break;
|
||||||
|
case Cat::Unknown: break;
|
||||||
|
}
|
||||||
|
if (r.cat == Cat::Builtin && r.t.base == "void" && r.t.ptr == 0) good = false;
|
||||||
|
if (!good) {
|
||||||
|
s.ok = false;
|
||||||
|
s.why = "field " + f.name + ": " + (r.why.empty() ? "type not supported" : r.why);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace gen
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// The slice of desktop OpenGL 3.3 core the shim needs, declared by hand (no GL headers on this machine) and loaded
|
||||||
|
// through eglGetProcAddress. Not a general-purpose loader.
|
||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
typedef unsigned int GLenum;
|
||||||
|
typedef unsigned char GLboolean;
|
||||||
|
typedef unsigned int GLbitfield;
|
||||||
|
typedef int GLint;
|
||||||
|
typedef int GLsizei;
|
||||||
|
typedef unsigned char GLubyte;
|
||||||
|
typedef unsigned int GLuint;
|
||||||
|
typedef float GLfloat;
|
||||||
|
typedef double GLdouble;
|
||||||
|
typedef char GLchar;
|
||||||
|
typedef std::ptrdiff_t GLsizeiptr;
|
||||||
|
typedef std::ptrdiff_t GLintptr;
|
||||||
|
|
||||||
|
constexpr GLenum GL_NO_ERROR = 0;
|
||||||
|
constexpr GLenum GL_FALSE = 0, GL_TRUE = 1;
|
||||||
|
constexpr GLenum GL_DEPTH_TEST = 0x0B71, GL_BLEND = 0x0BE2, GL_CULL_FACE = 0x0B44, GL_LEQUAL = 0x0203;
|
||||||
|
constexpr GLenum GL_SRC_ALPHA = 0x0302, GL_ONE_MINUS_SRC_ALPHA = 0x0303, GL_BACK = 0x0405, GL_FRONT = 0x0404, GL_CCW = 0x0901;
|
||||||
|
constexpr GLbitfield GL_COLOR_BUFFER_BIT = 0x4000, GL_DEPTH_BUFFER_BIT = 0x0100, GL_STENCIL_BUFFER_BIT = 0x0400;
|
||||||
|
constexpr GLenum GL_TEXTURE_2D = 0x0DE1, GL_TEXTURE0 = 0x84C0, GL_RGBA = 0x1908, GL_RGBA8 = 0x8058;
|
||||||
|
constexpr GLenum GL_UNSIGNED_BYTE = 0x1401, GL_FLOAT = 0x1406;
|
||||||
|
constexpr GLenum GL_TEXTURE_MIN_FILTER = 0x2801, GL_TEXTURE_MAG_FILTER = 0x2800, GL_TEXTURE_WRAP_S = 0x2802, GL_TEXTURE_WRAP_T = 0x2803;
|
||||||
|
constexpr GLenum GL_NEAREST = 0x2600, GL_LINEAR = 0x2601, GL_REPEAT = 0x2901, GL_CLAMP_TO_EDGE = 0x812F, GL_LINEAR_MIPMAP_LINEAR = 0x2703;
|
||||||
|
constexpr GLenum GL_UNPACK_ALIGNMENT = 0x0CF5, GL_PACK_ALIGNMENT = 0x0D05;
|
||||||
|
constexpr GLenum GL_FRAMEBUFFER = 0x8D40, GL_RENDERBUFFER = 0x8D41, GL_COLOR_ATTACHMENT0 = 0x8CE0, GL_DEPTH_ATTACHMENT = 0x8D00;
|
||||||
|
constexpr GLenum GL_DEPTH_COMPONENT24 = 0x81A6, GL_FRAMEBUFFER_COMPLETE = 0x8CD5;
|
||||||
|
constexpr GLenum GL_VERTEX_SHADER = 0x8B31, GL_FRAGMENT_SHADER = 0x8B30;
|
||||||
|
constexpr GLenum GL_COMPILE_STATUS = 0x8B81, GL_LINK_STATUS = 0x8B82, GL_INFO_LOG_LENGTH = 0x8B84;
|
||||||
|
constexpr GLenum GL_ARRAY_BUFFER = 0x8892, GL_STATIC_DRAW = 0x88E4, GL_DYNAMIC_DRAW = 0x88E8, GL_TRIANGLES = 0x0004;
|
||||||
|
constexpr GLenum GL_VERSION = 0x1F02, GL_RENDERER = 0x1F01, GL_SHADING_LANGUAGE_VERSION = 0x8B8C;
|
||||||
|
constexpr GLenum GL_MAX_TEXTURE_SIZE = 0x0D33, GL_MAX_VERTEX_ATTRIBS = 0x8869, GL_MAX_TEXTURE_IMAGE_UNITS = 0x8872;
|
||||||
|
constexpr GLenum GL_DEBUG_OUTPUT = 0x92E0, GL_DEBUG_OUTPUT_SYNCHRONOUS = 0x8242;
|
||||||
|
constexpr GLenum GL_DEBUG_SEVERITY_HIGH = 0x9146, GL_DEBUG_SEVERITY_MEDIUM = 0x9147, GL_DEBUG_SEVERITY_LOW = 0x9148, GL_DEBUG_SEVERITY_NOTIFICATION = 0x826B;
|
||||||
|
constexpr GLenum GL_DEPTH_FUNC = 0x0B74;
|
||||||
|
constexpr GLenum GL_CURRENT_PROGRAM = 0x8B8D, GL_FRAMEBUFFER_BINDING = 0x8CA6, GL_VERTEX_ARRAY_BINDING = 0x85B5;
|
||||||
|
constexpr GLenum GL_ACTIVE_TEXTURE = 0x84E0, GL_TEXTURE_BINDING_2D = 0x8069, GL_CULL_FACE_MODE = 0x0B45;
|
||||||
|
|
||||||
|
typedef void (*GLDEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam);
|
||||||
|
|
||||||
|
// X(return type, name, parameter list)
|
||||||
|
#define GL_FUNCTIONS(X) \
|
||||||
|
X(const GLubyte*, glGetString, (GLenum)) \
|
||||||
|
X(GLenum, glGetError, (void)) \
|
||||||
|
X(void, glGetIntegerv, (GLenum, GLint*)) \
|
||||||
|
X(void, glEnable, (GLenum)) \
|
||||||
|
X(void, glDisable, (GLenum)) \
|
||||||
|
X(GLboolean, glIsEnabled, (GLenum)) \
|
||||||
|
X(void, glDepthFunc, (GLenum)) \
|
||||||
|
X(void, glBlendFunc, (GLenum, GLenum)) \
|
||||||
|
X(void, glCullFace, (GLenum)) \
|
||||||
|
X(void, glFrontFace, (GLenum)) \
|
||||||
|
X(void, glClearColor, (GLfloat, GLfloat, GLfloat, GLfloat)) \
|
||||||
|
X(void, glClearDepth, (GLdouble)) \
|
||||||
|
X(void, glClear, (GLbitfield)) \
|
||||||
|
X(void, glViewport, (GLint, GLint, GLsizei, GLsizei)) \
|
||||||
|
X(void, glFinish, (void)) \
|
||||||
|
X(void, glPixelStorei, (GLenum, GLint)) \
|
||||||
|
X(void, glReadPixels, (GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*)) \
|
||||||
|
X(void, glGenTextures, (GLsizei, GLuint*)) \
|
||||||
|
X(void, glDeleteTextures, (GLsizei, const GLuint*)) \
|
||||||
|
X(void, glBindTexture, (GLenum, GLuint)) \
|
||||||
|
X(void, glActiveTexture, (GLenum)) \
|
||||||
|
X(void, glTexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*)) \
|
||||||
|
X(void, glTexSubImage2D, (GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*)) \
|
||||||
|
X(void, glTexParameteri, (GLenum, GLenum, GLint)) \
|
||||||
|
X(void, glGenerateMipmap, (GLenum)) \
|
||||||
|
X(void, glGenFramebuffers, (GLsizei, GLuint*)) \
|
||||||
|
X(void, glDeleteFramebuffers, (GLsizei, const GLuint*)) \
|
||||||
|
X(void, glBindFramebuffer, (GLenum, GLuint)) \
|
||||||
|
X(void, glFramebufferTexture2D, (GLenum, GLenum, GLenum, GLuint, GLint)) \
|
||||||
|
X(void, glFramebufferRenderbuffer, (GLenum, GLenum, GLenum, GLuint)) \
|
||||||
|
X(GLenum, glCheckFramebufferStatus, (GLenum)) \
|
||||||
|
X(void, glGenRenderbuffers, (GLsizei, GLuint*)) \
|
||||||
|
X(void, glDeleteRenderbuffers, (GLsizei, const GLuint*)) \
|
||||||
|
X(void, glBindRenderbuffer, (GLenum, GLuint)) \
|
||||||
|
X(void, glRenderbufferStorage, (GLenum, GLenum, GLsizei, GLsizei)) \
|
||||||
|
X(GLuint, glCreateShader, (GLenum)) \
|
||||||
|
X(void, glShaderSource, (GLuint, GLsizei, const GLchar* const*, const GLint*)) \
|
||||||
|
X(void, glCompileShader, (GLuint)) \
|
||||||
|
X(void, glGetShaderiv, (GLuint, GLenum, GLint*)) \
|
||||||
|
X(void, glGetShaderInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*)) \
|
||||||
|
X(void, glDeleteShader, (GLuint)) \
|
||||||
|
X(GLuint, glCreateProgram, (void)) \
|
||||||
|
X(void, glAttachShader, (GLuint, GLuint)) \
|
||||||
|
X(void, glDetachShader, (GLuint, GLuint)) \
|
||||||
|
X(void, glBindAttribLocation, (GLuint, GLuint, const GLchar*)) \
|
||||||
|
X(void, glLinkProgram, (GLuint)) \
|
||||||
|
X(void, glGetProgramiv, (GLuint, GLenum, GLint*)) \
|
||||||
|
X(void, glGetProgramInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*)) \
|
||||||
|
X(void, glDeleteProgram, (GLuint)) \
|
||||||
|
X(void, glUseProgram, (GLuint)) \
|
||||||
|
X(GLint, glGetUniformLocation, (GLuint, const GLchar*)) \
|
||||||
|
X(void, glUniform1fv, (GLint, GLsizei, const GLfloat*)) \
|
||||||
|
X(void, glUniform2fv, (GLint, GLsizei, const GLfloat*)) \
|
||||||
|
X(void, glUniform3fv, (GLint, GLsizei, const GLfloat*)) \
|
||||||
|
X(void, glUniform4fv, (GLint, GLsizei, const GLfloat*)) \
|
||||||
|
X(void, glUniform1iv, (GLint, GLsizei, const GLint*)) \
|
||||||
|
X(void, glUniform2iv, (GLint, GLsizei, const GLint*)) \
|
||||||
|
X(void, glUniform3iv, (GLint, GLsizei, const GLint*)) \
|
||||||
|
X(void, glUniform4iv, (GLint, GLsizei, const GLint*)) \
|
||||||
|
X(void, glUniformMatrix4fv, (GLint, GLsizei, GLboolean, const GLfloat*)) \
|
||||||
|
X(void, glGenVertexArrays, (GLsizei, GLuint*)) \
|
||||||
|
X(void, glBindVertexArray, (GLuint)) \
|
||||||
|
X(void, glDeleteVertexArrays, (GLsizei, const GLuint*)) \
|
||||||
|
X(void, glGenBuffers, (GLsizei, GLuint*)) \
|
||||||
|
X(void, glBindBuffer, (GLenum, GLuint)) \
|
||||||
|
X(void, glBufferData, (GLenum, GLsizeiptr, const void*, GLenum)) \
|
||||||
|
X(void, glBufferSubData, (GLenum, GLintptr, GLsizeiptr, const void*)) \
|
||||||
|
X(void, glDeleteBuffers, (GLsizei, const GLuint*)) \
|
||||||
|
X(void, glVertexAttribPointer, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void*)) \
|
||||||
|
X(void, glEnableVertexAttribArray, (GLuint)) \
|
||||||
|
X(void, glDisableVertexAttribArray, (GLuint)) \
|
||||||
|
X(void, glVertexAttribDivisor, (GLuint, GLuint)) \
|
||||||
|
X(void, glDrawArrays, (GLenum, GLint, GLsizei)) \
|
||||||
|
X(void, glDrawArraysInstanced, (GLenum, GLint, GLsizei, GLsizei)) \
|
||||||
|
X(void, glDebugMessageCallback, (GLDEBUGPROC, const void*))
|
||||||
|
|
||||||
|
namespace gl {
|
||||||
|
#define X(ret, name, args) extern ret(*name) args;
|
||||||
|
GL_FUNCTIONS(X)
|
||||||
|
#undef X
|
||||||
|
} // namespace gl
|
||||||
@@ -0,0 +1,738 @@
|
|||||||
|
// VERIFICATION HARNESS ONLY. A software-OpenGL implementation of the slice of raylib/rlgl that the engine's renderer
|
||||||
|
// (src/render.cpp) and run loop (src/app.cpp) call, so both can be compiled UNMODIFIED against the stand-in headers and
|
||||||
|
// run on Mesa llvmpipe (surfaceless EGL, desktop GL 3.3 core). Every function follows what raylib 5.5/6.0's own source
|
||||||
|
// does at the GL level (the state it sets, the textures it binds, the order it does it in) - that is what makes the
|
||||||
|
// frames it draws meaningful. Everything else raylib offers is simply not here: the linker tells us if the engine
|
||||||
|
// starts depending on more.
|
||||||
|
#include "gl_min.h"
|
||||||
|
#include "raylib.h"
|
||||||
|
#include "rlgl.h"
|
||||||
|
#include "shim_api.h"
|
||||||
|
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <png.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace gl {
|
||||||
|
#define X(ret, name, args) ret(*name) args = nullptr;
|
||||||
|
GL_FUNCTIONS(X)
|
||||||
|
#undef X
|
||||||
|
} // namespace gl
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ---- EGL (surfaceless Mesa) ------------------------------------------------------------------------------------------
|
||||||
|
typedef void* EGLDisplay;
|
||||||
|
typedef void* EGLConfig;
|
||||||
|
typedef void* EGLContext;
|
||||||
|
typedef void* EGLSurface;
|
||||||
|
typedef int EGLint;
|
||||||
|
typedef unsigned int EGLBoolean;
|
||||||
|
typedef unsigned int EGLenum;
|
||||||
|
constexpr EGLint EGL_NONE = 0x3038, EGL_SURFACE_TYPE = 0x3033, EGL_RENDERABLE_TYPE = 0x3040, EGL_OPENGL_BIT = 0x0008;
|
||||||
|
constexpr EGLenum EGL_OPENGL_API = 0x30A2, EGL_PLATFORM_SURFACELESS_MESA = 0x31DD;
|
||||||
|
constexpr EGLint EGL_CONTEXT_MAJOR_VERSION = 0x3098, EGL_CONTEXT_MINOR_VERSION = 0x30FB;
|
||||||
|
constexpr EGLint EGL_CONTEXT_OPENGL_PROFILE_MASK = 0x30FD, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT = 1;
|
||||||
|
constexpr EGLint EGL_CONTEXT_OPENGL_DEBUG = 0x31B0, EGL_TRUE = 1;
|
||||||
|
|
||||||
|
// ---- shared state ------------------------------------------------------------------------------------------------------
|
||||||
|
shim::Report gReport;
|
||||||
|
shim::Frame gScreenshot;
|
||||||
|
|
||||||
|
struct Egl {
|
||||||
|
void* lib = nullptr;
|
||||||
|
void* (*getProc)(const char*) = nullptr;
|
||||||
|
EGLDisplay dpy = nullptr;
|
||||||
|
EGLContext ctx = nullptr;
|
||||||
|
bool ok = false;
|
||||||
|
} gEgl;
|
||||||
|
|
||||||
|
struct Window {
|
||||||
|
bool ready = false;
|
||||||
|
int width = 0, height = 0;
|
||||||
|
unsigned screenFbo = 0, screenColor = 0, screenDepth = 0;
|
||||||
|
unsigned defaultTexture = 0, defaultShader = 0;
|
||||||
|
unsigned currentFbo = 0; // what BeginTextureMode/EndTextureMode toggle
|
||||||
|
double cullNear = 0.05, cullFar = 4000.0;
|
||||||
|
bool in3D = false;
|
||||||
|
unsigned configFlags = 0;
|
||||||
|
double time = 0;
|
||||||
|
} gWin;
|
||||||
|
|
||||||
|
constexpr int kMaxShaderLocations = 32;
|
||||||
|
|
||||||
|
void note(const std::string& s) {
|
||||||
|
gReport.log.push_back(s);
|
||||||
|
std::fprintf(stderr, "[shim] %s\n", s.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkGl(const char* where) {
|
||||||
|
if (!gl::glGetError) return;
|
||||||
|
for (int n = 0; n < 8; ++n) {
|
||||||
|
const GLenum e = gl::glGetError();
|
||||||
|
if (e == GL_NO_ERROR) break;
|
||||||
|
++gReport.glErrors;
|
||||||
|
char buf[160];
|
||||||
|
std::snprintf(buf, sizeof buf, "GL error 0x%04x after %s", e, where);
|
||||||
|
note(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void debugCallback(GLenum, GLenum type, GLuint, GLenum severity, GLsizei, const GLchar* message, const void*) {
|
||||||
|
if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return;
|
||||||
|
const bool isError = type == 0x824C /*ERROR*/ || type == 0x824E /*UNDEFINED_BEHAVIOR*/;
|
||||||
|
if (isError) ++gReport.glErrors;
|
||||||
|
char buf[640];
|
||||||
|
std::snprintf(buf, sizeof buf, "GL %s: %s", isError ? "ERROR" : "message", message);
|
||||||
|
note(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadGl() {
|
||||||
|
if (gEgl.ok) return true;
|
||||||
|
gEgl.lib = dlopen("libEGL.so.1", RTLD_NOW | RTLD_GLOBAL);
|
||||||
|
if (!gEgl.lib) {
|
||||||
|
note("cannot load libEGL.so.1");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
gEgl.getProc = reinterpret_cast<void* (*)(const char*)>(dlsym(gEgl.lib, "eglGetProcAddress"));
|
||||||
|
auto getPlatformDisplay = reinterpret_cast<EGLDisplay (*)(EGLenum, void*, const EGLint*)>(gEgl.getProc("eglGetPlatformDisplay"));
|
||||||
|
auto eglInitialize = reinterpret_cast<EGLBoolean (*)(EGLDisplay, EGLint*, EGLint*)>(dlsym(gEgl.lib, "eglInitialize"));
|
||||||
|
auto eglBindAPI = reinterpret_cast<EGLBoolean (*)(EGLenum)>(dlsym(gEgl.lib, "eglBindAPI"));
|
||||||
|
auto eglChooseConfig = reinterpret_cast<EGLBoolean (*)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*)>(dlsym(gEgl.lib, "eglChooseConfig"));
|
||||||
|
auto eglCreateContext = reinterpret_cast<EGLContext (*)(EGLDisplay, EGLConfig, EGLContext, const EGLint*)>(dlsym(gEgl.lib, "eglCreateContext"));
|
||||||
|
auto eglMakeCurrent = reinterpret_cast<EGLBoolean (*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext)>(dlsym(gEgl.lib, "eglMakeCurrent"));
|
||||||
|
gEgl.dpy = getPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, nullptr, nullptr);
|
||||||
|
EGLint maj = 0, min = 0;
|
||||||
|
if (!gEgl.dpy || !eglInitialize(gEgl.dpy, &maj, &min) || !eglBindAPI(EGL_OPENGL_API)) {
|
||||||
|
note("EGL display/initialise failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const EGLint cfgAttr[] = {EGL_SURFACE_TYPE, 0, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE};
|
||||||
|
EGLConfig cfg = nullptr;
|
||||||
|
EGLint n = 0;
|
||||||
|
if (!eglChooseConfig(gEgl.dpy, cfgAttr, &cfg, 1, &n) || n == 0) {
|
||||||
|
note("EGL: no config");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const EGLint ctxAttr[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3,
|
||||||
|
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
||||||
|
EGL_CONTEXT_OPENGL_DEBUG, EGL_TRUE, EGL_NONE};
|
||||||
|
gEgl.ctx = eglCreateContext(gEgl.dpy, cfg, nullptr, ctxAttr);
|
||||||
|
if (!gEgl.ctx || !eglMakeCurrent(gEgl.dpy, nullptr, nullptr, gEgl.ctx)) {
|
||||||
|
note("EGL: no core 3.3 context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#define X(ret, name, args) \
|
||||||
|
gl::name = reinterpret_cast<ret(*) args>(gEgl.getProc(#name)); \
|
||||||
|
if (!gl::name) { \
|
||||||
|
note(std::string("missing GL function ") + #name); \
|
||||||
|
return false; \
|
||||||
|
}
|
||||||
|
GL_FUNCTIONS(X)
|
||||||
|
#undef X
|
||||||
|
gReport.glVersion = reinterpret_cast<const char*>(gl::glGetString(GL_VERSION));
|
||||||
|
gReport.glRenderer = reinterpret_cast<const char*>(gl::glGetString(GL_RENDERER));
|
||||||
|
gReport.glsl = reinterpret_cast<const char*>(gl::glGetString(GL_SHADING_LANGUAGE_VERSION));
|
||||||
|
gl::glEnable(GL_DEBUG_OUTPUT);
|
||||||
|
gl::glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
|
||||||
|
gl::glDebugMessageCallback(debugCallback, nullptr);
|
||||||
|
gEgl.ok = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned loadTextureRgba8(const void* data, int w, int h) {
|
||||||
|
GLuint id = 0;
|
||||||
|
gl::glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
gl::glGenTextures(1, &id);
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, id);
|
||||||
|
gl::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
|
||||||
|
gl::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // raylib's defaults for a new texture
|
||||||
|
gl::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||||
|
gl::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
gl::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned compileStage(const char* code, GLenum type) {
|
||||||
|
const GLuint s = gl::glCreateShader(type);
|
||||||
|
gl::glShaderSource(s, 1, &code, nullptr);
|
||||||
|
gl::glCompileShader(s);
|
||||||
|
GLint ok = 0;
|
||||||
|
gl::glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
||||||
|
if (ok == GL_FALSE) {
|
||||||
|
++gReport.shaderFailures;
|
||||||
|
GLint len = 0;
|
||||||
|
gl::glGetShaderiv(s, GL_INFO_LOG_LENGTH, &len);
|
||||||
|
std::string log(static_cast<std::size_t>(len > 0 ? len : 1), '\0');
|
||||||
|
gl::glGetShaderInfoLog(s, len, nullptr, log.data());
|
||||||
|
note(std::string("SHADER: failed to compile ") + (type == GL_VERTEX_SHADER ? "vertex" : "fragment") + " shader:\n" + log);
|
||||||
|
gl::glDeleteShader(s);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned linkProgram(unsigned vs, unsigned fs) {
|
||||||
|
const GLuint p = gl::glCreateProgram();
|
||||||
|
gl::glAttachShader(p, vs);
|
||||||
|
gl::glAttachShader(p, fs);
|
||||||
|
// raylib binds its default attribute names before linking (explicit layout(location) in a shader wins).
|
||||||
|
gl::glBindAttribLocation(p, 0, "vertexPosition");
|
||||||
|
gl::glBindAttribLocation(p, 1, "vertexTexCoord");
|
||||||
|
gl::glBindAttribLocation(p, 2, "vertexNormal");
|
||||||
|
gl::glBindAttribLocation(p, 3, "vertexColor");
|
||||||
|
gl::glBindAttribLocation(p, 4, "vertexTangent");
|
||||||
|
gl::glBindAttribLocation(p, 5, "vertexTexCoord2");
|
||||||
|
gl::glLinkProgram(p);
|
||||||
|
GLint ok = 0;
|
||||||
|
gl::glGetProgramiv(p, GL_LINK_STATUS, &ok);
|
||||||
|
if (ok == GL_FALSE) {
|
||||||
|
++gReport.shaderFailures;
|
||||||
|
GLint len = 0;
|
||||||
|
gl::glGetProgramiv(p, GL_INFO_LOG_LENGTH, &len);
|
||||||
|
std::string log(static_cast<std::size_t>(len > 0 ? len : 1), '\0');
|
||||||
|
gl::glGetProgramInfoLog(p, len, nullptr, log.data());
|
||||||
|
note("SHADER: failed to link program:\n" + log);
|
||||||
|
gl::glDeleteProgram(p);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupScreen(int w, int h) {
|
||||||
|
gWin.width = w;
|
||||||
|
gWin.height = h;
|
||||||
|
gl::glGenFramebuffers(1, &gWin.screenFbo);
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, gWin.screenFbo);
|
||||||
|
gWin.screenColor = loadTextureRgba8(nullptr, w, h);
|
||||||
|
gl::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gWin.screenColor, 0);
|
||||||
|
gl::glGenRenderbuffers(1, &gWin.screenDepth);
|
||||||
|
gl::glBindRenderbuffer(GL_RENDERBUFFER, gWin.screenDepth);
|
||||||
|
gl::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, w, h);
|
||||||
|
gl::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, gWin.screenDepth);
|
||||||
|
if (gl::glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) note("screen framebuffer incomplete");
|
||||||
|
gWin.currentFbo = gWin.screenFbo;
|
||||||
|
gl::glViewport(0, 0, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rlglInit()'s default state.
|
||||||
|
void defaultState() {
|
||||||
|
gl::glDepthFunc(GL_LEQUAL);
|
||||||
|
gl::glDisable(GL_DEPTH_TEST);
|
||||||
|
gl::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
gl::glEnable(GL_BLEND);
|
||||||
|
gl::glCullFace(GL_BACK);
|
||||||
|
gl::glFrontFace(GL_CCW);
|
||||||
|
gl::glEnable(GL_CULL_FACE);
|
||||||
|
gl::glClearColor(0, 0, 0, 1);
|
||||||
|
gl::glClearDepth(1.0);
|
||||||
|
gl::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
shim::Frame readFramebuffer(unsigned fbo, int w, int h) {
|
||||||
|
shim::Frame f;
|
||||||
|
f.width = w;
|
||||||
|
f.height = h;
|
||||||
|
std::vector<std::uint8_t> raw(static_cast<std::size_t>(w) * static_cast<std::size_t>(h) * 4);
|
||||||
|
GLint prev = 0;
|
||||||
|
gl::glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev);
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
gl::glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
gl::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, raw.data());
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prev));
|
||||||
|
f.rgba.resize(raw.size());
|
||||||
|
for (int y = 0; y < h; ++y) { // flip: glReadPixels starts at the bottom row
|
||||||
|
const std::uint8_t* src = &raw[static_cast<std::size_t>(h - 1 - y) * static_cast<std::size_t>(w) * 4];
|
||||||
|
std::uint8_t* dst = &f.rgba[static_cast<std::size_t>(y) * static_cast<std::size_t>(w) * 4];
|
||||||
|
std::memcpy(dst, src, static_cast<std::size_t>(w) * 4);
|
||||||
|
for (int x = 0; x < w; ++x) dst[x * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ============================================================================================================================
|
||||||
|
// shim_api.h
|
||||||
|
// ============================================================================================================================
|
||||||
|
namespace shim {
|
||||||
|
Report& report() { return gReport; }
|
||||||
|
void resetCounters() {
|
||||||
|
gReport.drawCalls = 0;
|
||||||
|
gReport.instancesDrawn = 0;
|
||||||
|
gReport.texts.clear();
|
||||||
|
}
|
||||||
|
const Frame& lastScreenshot() { return gScreenshot; }
|
||||||
|
Frame readScreen() { return readFramebuffer(gWin.screenFbo, gWin.width, gWin.height); }
|
||||||
|
bool writePng(const std::string& path, const Frame& f) {
|
||||||
|
if (!f.valid()) return false;
|
||||||
|
png_image img{};
|
||||||
|
img.version = PNG_IMAGE_VERSION;
|
||||||
|
img.width = static_cast<png_uint_32>(f.width);
|
||||||
|
img.height = static_cast<png_uint_32>(f.height);
|
||||||
|
img.format = PNG_FORMAT_RGBA;
|
||||||
|
return png_image_write_to_file(&img, path.c_str(), 0, f.rgba.data(), 0, nullptr) != 0;
|
||||||
|
}
|
||||||
|
} // namespace shim
|
||||||
|
|
||||||
|
// ============================================================================================================================
|
||||||
|
// raylib core
|
||||||
|
// ============================================================================================================================
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
void SetConfigFlags(unsigned int flags) { gWin.configFlags = flags; }
|
||||||
|
|
||||||
|
void InitWindow(int width, int height, const char*) {
|
||||||
|
if (!loadGl()) return;
|
||||||
|
setupScreen(width, height);
|
||||||
|
defaultState();
|
||||||
|
const unsigned char white[4] = {255, 255, 255, 255};
|
||||||
|
gWin.defaultTexture = loadTextureRgba8(white, 1, 1);
|
||||||
|
gWin.defaultShader = 1; // only ever compared against, never used by the renderer
|
||||||
|
gWin.cullNear = 0.05;
|
||||||
|
gWin.cullFar = 4000.0;
|
||||||
|
gWin.time = 0;
|
||||||
|
gWin.ready = true;
|
||||||
|
gReport.framesPresented = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsWindowReady(void) { return gWin.ready; }
|
||||||
|
|
||||||
|
void CloseWindow(void) {
|
||||||
|
if (!gWin.ready) return;
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
gl::glDeleteFramebuffers(1, &gWin.screenFbo);
|
||||||
|
gl::glDeleteRenderbuffers(1, &gWin.screenDepth);
|
||||||
|
gl::glDeleteTextures(1, &gWin.screenColor);
|
||||||
|
gl::glDeleteTextures(1, &gWin.defaultTexture);
|
||||||
|
checkGl("CloseWindow");
|
||||||
|
gWin.ready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetTargetFPS(int) {}
|
||||||
|
void SetExitKey(int) {}
|
||||||
|
bool WindowShouldClose(void) { return false; }
|
||||||
|
int GetScreenWidth(void) { return gWin.width; }
|
||||||
|
int GetScreenHeight(void) { return gWin.height; }
|
||||||
|
float GetFrameTime(void) { return 1.0f / 60.0f; }
|
||||||
|
double GetTime(void) { return gWin.time; }
|
||||||
|
int GetFPS(void) { return 60; }
|
||||||
|
|
||||||
|
void BeginDrawing(void) {
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, gWin.screenFbo);
|
||||||
|
gl::glViewport(0, 0, gWin.width, gWin.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndDrawing(void) {
|
||||||
|
gl::glFinish();
|
||||||
|
checkGl("EndDrawing");
|
||||||
|
++gReport.framesPresented;
|
||||||
|
gWin.time += 1.0 / 60.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBackground(Color c) {
|
||||||
|
gl::glClearColor(static_cast<float>(c.r) / 255.0f, static_cast<float>(c.g) / 255.0f, static_cast<float>(c.b) / 255.0f,
|
||||||
|
static_cast<float>(c.a) / 255.0f);
|
||||||
|
gl::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeginMode3D(Camera3D) {
|
||||||
|
gWin.in3D = true;
|
||||||
|
gl::glEnable(GL_DEPTH_TEST); // rlEnableDepthTest() at the end of BeginMode3D
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndMode3D(void) {
|
||||||
|
gWin.in3D = false;
|
||||||
|
gl::glDisable(GL_DEPTH_TEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeginTextureMode(RenderTexture2D target) {
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, target.id);
|
||||||
|
gl::glViewport(0, 0, target.texture.width, target.texture.height);
|
||||||
|
gWin.currentFbo = target.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndTextureMode(void) {
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, gWin.screenFbo);
|
||||||
|
gl::glViewport(0, 0, gWin.width, gWin.height);
|
||||||
|
gWin.currentFbo = gWin.screenFbo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- shaders ------------------------------------------------------------------------------------------------------------
|
||||||
|
Shader LoadShaderFromMemory(const char* vsCode, const char* fsCode) {
|
||||||
|
Shader shader{};
|
||||||
|
const unsigned vs = compileStage(vsCode, GL_VERTEX_SHADER);
|
||||||
|
const unsigned fs = compileStage(fsCode, GL_FRAGMENT_SHADER);
|
||||||
|
unsigned program = 0;
|
||||||
|
if (vs && fs) program = linkProgram(vs, fs);
|
||||||
|
if (vs) gl::glDeleteShader(vs);
|
||||||
|
if (fs) gl::glDeleteShader(fs);
|
||||||
|
shader.id = program;
|
||||||
|
if (program > 0) {
|
||||||
|
shader.locs = static_cast<int*>(std::calloc(kMaxShaderLocations, sizeof(int)));
|
||||||
|
for (int i = 0; i < kMaxShaderLocations; ++i) shader.locs[i] = -1;
|
||||||
|
}
|
||||||
|
checkGl("LoadShaderFromMemory");
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsShaderValid(Shader shader) { return shader.id > 0 && shader.locs != nullptr; }
|
||||||
|
|
||||||
|
int GetShaderLocation(Shader shader, const char* uniformName) { return gl::glGetUniformLocation(shader.id, uniformName); }
|
||||||
|
|
||||||
|
void UnloadShader(Shader shader) {
|
||||||
|
if (shader.id != gWin.defaultShader) {
|
||||||
|
gl::glDeleteProgram(shader.id);
|
||||||
|
std::free(shader.locs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- textures / render textures -----------------------------------------------------------------------------------------
|
||||||
|
Texture2D LoadTextureFromImage(Image image) {
|
||||||
|
Texture2D t{};
|
||||||
|
if (image.data == nullptr || image.format != PIXELFORMAT_UNCOMPRESSED_R8G8B8A8) {
|
||||||
|
note("LoadTextureFromImage: only RGBA8 images are supported by the shim");
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
t.id = loadTextureRgba8(image.data, image.width, image.height);
|
||||||
|
t.width = image.width;
|
||||||
|
t.height = image.height;
|
||||||
|
t.mipmaps = 1;
|
||||||
|
t.format = image.format;
|
||||||
|
checkGl("LoadTextureFromImage");
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnloadTexture(Texture2D texture) {
|
||||||
|
if (texture.id > 0) gl::glDeleteTextures(1, &texture.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateTexture(Texture2D texture, const void* pixels) {
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, texture.id);
|
||||||
|
gl::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, texture.width, texture.height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
checkGl("UpdateTexture");
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateTextureRec(Texture2D texture, Rectangle rec, const void* pixels) {
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, texture.id);
|
||||||
|
gl::glTexSubImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(rec.x), static_cast<GLint>(rec.y), static_cast<GLsizei>(rec.width), static_cast<GLsizei>(rec.height),
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
checkGl("UpdateTextureRec");
|
||||||
|
}
|
||||||
|
|
||||||
|
// raylib's SetTextureFilter: point and bilinear set both filters; trilinear needs mipmaps (it falls back to bilinear without);
|
||||||
|
// the anisotropic modes only set the anisotropy, which this shim does not model.
|
||||||
|
void SetTextureFilter(Texture2D texture, int filter) {
|
||||||
|
if (filter == TEXTURE_FILTER_POINT) {
|
||||||
|
rlTextureParameters(texture.id, RL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
rlTextureParameters(texture.id, RL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
} else if (filter == TEXTURE_FILTER_BILINEAR || (filter == TEXTURE_FILTER_TRILINEAR && texture.mipmaps <= 1)) {
|
||||||
|
rlTextureParameters(texture.id, RL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
rlTextureParameters(texture.id, RL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
} else if (filter == TEXTURE_FILTER_TRILINEAR) {
|
||||||
|
rlTextureParameters(texture.id, RL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||||
|
rlTextureParameters(texture.id, RL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GenTextureMipmaps(Texture2D* texture) {
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, texture->id);
|
||||||
|
gl::glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
int levels = 1;
|
||||||
|
for (int side = std::max(texture->width, texture->height); side > 1; side >>= 1) ++levels;
|
||||||
|
texture->mipmaps = levels;
|
||||||
|
checkGl("GenTextureMipmaps");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image files: PNG only (libpng's simplified API), always decoded to RGBA8. Enough to test the engine's whole
|
||||||
|
// path from a file on disk to a pixel on screen; the real raylib reads many more formats.
|
||||||
|
Image LoadImage(const char* fileName) {
|
||||||
|
Image img{};
|
||||||
|
png_image png;
|
||||||
|
std::memset(&png, 0, sizeof png);
|
||||||
|
png.version = PNG_IMAGE_VERSION;
|
||||||
|
if (!png_image_begin_read_from_file(&png, fileName)) return img;
|
||||||
|
png.format = PNG_FORMAT_RGBA;
|
||||||
|
void* data = std::malloc(PNG_IMAGE_SIZE(png));
|
||||||
|
if (data == nullptr || !png_image_finish_read(&png, nullptr, data, 0, nullptr)) {
|
||||||
|
std::free(data);
|
||||||
|
png_image_free(&png);
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
img.data = data;
|
||||||
|
img.width = static_cast<int>(png.width);
|
||||||
|
img.height = static_cast<int>(png.height);
|
||||||
|
img.mipmaps = 1;
|
||||||
|
img.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
bool IsImageValid(Image image) { return image.data != nullptr && image.width > 0 && image.height > 0; }
|
||||||
|
void ImageFormat(Image*, int) {} // LoadImage above already returns RGBA8
|
||||||
|
|
||||||
|
void SetTextureWrap(Texture2D texture, int wrap) {
|
||||||
|
const GLint w = wrap == TEXTURE_WRAP_CLAMP ? GL_CLAMP_TO_EDGE : GL_REPEAT;
|
||||||
|
rlTextureParameters(texture.id, RL_TEXTURE_WRAP_S, w);
|
||||||
|
rlTextureParameters(texture.id, RL_TEXTURE_WRAP_T, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderTexture2D LoadRenderTexture(int width, int height) {
|
||||||
|
RenderTexture2D target{};
|
||||||
|
GLuint fbo = 0;
|
||||||
|
gl::glGenFramebuffers(1, &fbo);
|
||||||
|
target.id = fbo;
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
target.texture.id = loadTextureRgba8(nullptr, width, height);
|
||||||
|
target.texture.width = width;
|
||||||
|
target.texture.height = height;
|
||||||
|
target.texture.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
|
||||||
|
target.texture.mipmaps = 1;
|
||||||
|
GLuint depth = 0;
|
||||||
|
gl::glGenRenderbuffers(1, &depth);
|
||||||
|
gl::glBindRenderbuffer(GL_RENDERBUFFER, depth);
|
||||||
|
gl::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
|
||||||
|
target.depth.id = depth;
|
||||||
|
target.depth.width = width;
|
||||||
|
target.depth.height = height;
|
||||||
|
target.depth.mipmaps = 1;
|
||||||
|
gl::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture.id, 0);
|
||||||
|
gl::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth);
|
||||||
|
if (gl::glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
note("render texture framebuffer incomplete");
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, gWin.currentFbo);
|
||||||
|
return RenderTexture2D{};
|
||||||
|
}
|
||||||
|
gl::glBindFramebuffer(GL_FRAMEBUFFER, gWin.currentFbo);
|
||||||
|
checkGl("LoadRenderTexture");
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsRenderTextureValid(RenderTexture2D target) { return target.id > 0 && target.texture.id > 0 && target.depth.id > 0; }
|
||||||
|
|
||||||
|
void UnloadRenderTexture(RenderTexture2D target) {
|
||||||
|
if (target.id == 0) return;
|
||||||
|
gl::glDeleteTextures(1, &target.texture.id);
|
||||||
|
gl::glDeleteRenderbuffers(1, &target.depth.id);
|
||||||
|
gl::glDeleteFramebuffers(1, &target.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- meshes ---------------------------------------------------------------------------------------------------------------
|
||||||
|
void* MemAlloc(unsigned int size) { return std::calloc(1, size); }
|
||||||
|
void MemFree(void* ptr) { std::free(ptr); }
|
||||||
|
|
||||||
|
void UploadMesh(Mesh* mesh, bool dynamic) {
|
||||||
|
if (mesh->vaoId > 0) return;
|
||||||
|
mesh->vboId = static_cast<unsigned int*>(std::calloc(9, sizeof(unsigned int)));
|
||||||
|
GLuint vao = 0;
|
||||||
|
gl::glGenVertexArrays(1, &vao);
|
||||||
|
mesh->vaoId = vao;
|
||||||
|
gl::glBindVertexArray(vao);
|
||||||
|
const unsigned n = static_cast<unsigned>(mesh->vertexCount);
|
||||||
|
mesh->vboId[0] = rlLoadVertexBuffer(mesh->vertices, static_cast<int>(n * 3 * sizeof(float)), dynamic);
|
||||||
|
rlSetVertexAttribute(0, 3, RL_FLOAT, false, 0, 0);
|
||||||
|
rlEnableVertexAttribute(0);
|
||||||
|
mesh->vboId[1] = rlLoadVertexBuffer(mesh->texcoords, static_cast<int>(n * 2 * sizeof(float)), dynamic);
|
||||||
|
rlSetVertexAttribute(1, 2, RL_FLOAT, false, 0, 0);
|
||||||
|
rlEnableVertexAttribute(1);
|
||||||
|
if (mesh->normals) {
|
||||||
|
mesh->vboId[2] = rlLoadVertexBuffer(mesh->normals, static_cast<int>(n * 3 * sizeof(float)), dynamic);
|
||||||
|
rlSetVertexAttribute(2, 3, RL_FLOAT, false, 0, 0);
|
||||||
|
rlEnableVertexAttribute(2);
|
||||||
|
}
|
||||||
|
if (mesh->colors) {
|
||||||
|
mesh->vboId[3] = rlLoadVertexBuffer(mesh->colors, static_cast<int>(n * 4), dynamic);
|
||||||
|
rlSetVertexAttribute(3, 4, RL_UNSIGNED_BYTE, true, 0, 0);
|
||||||
|
rlEnableVertexAttribute(3);
|
||||||
|
}
|
||||||
|
gl::glBindVertexArray(0);
|
||||||
|
checkGl("UploadMesh");
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnloadMesh(Mesh mesh) {
|
||||||
|
if (mesh.vaoId > 0) {
|
||||||
|
GLuint vao = mesh.vaoId;
|
||||||
|
gl::glDeleteVertexArrays(1, &vao);
|
||||||
|
}
|
||||||
|
if (mesh.vboId) {
|
||||||
|
for (int i = 0; i < 9; ++i)
|
||||||
|
if (mesh.vboId[i]) {
|
||||||
|
GLuint b = mesh.vboId[i];
|
||||||
|
gl::glDeleteBuffers(1, &b);
|
||||||
|
}
|
||||||
|
std::free(mesh.vboId);
|
||||||
|
}
|
||||||
|
std::free(mesh.vertices);
|
||||||
|
std::free(mesh.texcoords);
|
||||||
|
std::free(mesh.normals);
|
||||||
|
std::free(mesh.colors);
|
||||||
|
std::free(mesh.indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 2D drawing (recorded, not drawn) -----------------------------------------------------------------------------------
|
||||||
|
void DrawText(const char* text, int, int, int, Color) { gReport.texts.emplace_back(text); }
|
||||||
|
void DrawRectangle(int, int, int, int, Color) {}
|
||||||
|
|
||||||
|
const char* TextFormat(const char* text, ...) {
|
||||||
|
static char buffers[4][1024];
|
||||||
|
static int index = 0;
|
||||||
|
char* cur = buffers[index];
|
||||||
|
index = (index + 1) % 4;
|
||||||
|
va_list args;
|
||||||
|
va_start(args, text);
|
||||||
|
std::vsnprintf(cur, 1024, text, args);
|
||||||
|
va_end(args);
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TraceLog(int, const char* text, ...) {
|
||||||
|
char buf[512];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, text);
|
||||||
|
std::vsnprintf(buf, sizeof buf, text, args);
|
||||||
|
va_end(args);
|
||||||
|
note(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- images ------------------------------------------------------------------------------------------------------------
|
||||||
|
Image LoadImageFromScreen(void) {
|
||||||
|
gScreenshot = readFramebuffer(gWin.screenFbo, gWin.width, gWin.height);
|
||||||
|
Image img{};
|
||||||
|
const std::size_t bytes = gScreenshot.rgba.size();
|
||||||
|
img.data = std::malloc(bytes);
|
||||||
|
std::memcpy(img.data, gScreenshot.rgba.data(), bytes);
|
||||||
|
img.width = gScreenshot.width;
|
||||||
|
img.height = gScreenshot.height;
|
||||||
|
img.mipmaps = 1;
|
||||||
|
img.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8;
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExportImage(Image image, const char* fileName) {
|
||||||
|
if (image.format != PIXELFORMAT_UNCOMPRESSED_R8G8B8A8) return false;
|
||||||
|
shim::Frame f;
|
||||||
|
f.width = image.width;
|
||||||
|
f.height = image.height;
|
||||||
|
f.rgba.assign(static_cast<const std::uint8_t*>(image.data), static_cast<const std::uint8_t*>(image.data) + static_cast<std::size_t>(image.width) * static_cast<std::size_t>(image.height) * 4);
|
||||||
|
return shim::writePng(fileName, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnloadImage(Image image) { std::free(image.data); }
|
||||||
|
|
||||||
|
// ---- input: nothing is ever pressed -----------------------------------------------------------------------------------
|
||||||
|
bool IsKeyDown(int) { return false; }
|
||||||
|
bool IsKeyPressed(int) { return false; }
|
||||||
|
bool IsKeyReleased(int) { return false; }
|
||||||
|
bool IsMouseButtonDown(int) { return false; }
|
||||||
|
bool IsMouseButtonPressed(int) { return false; }
|
||||||
|
bool IsMouseButtonReleased(int) { return false; }
|
||||||
|
Vector2 GetMousePosition(void) { return Vector2{0, 0}; }
|
||||||
|
Vector2 GetMouseDelta(void) { return Vector2{0, 0}; }
|
||||||
|
float GetMouseWheelMove(void) { return 0; }
|
||||||
|
|
||||||
|
// ============================================================================================================================
|
||||||
|
// rlgl
|
||||||
|
// ============================================================================================================================
|
||||||
|
void rlSetClipPlanes(double nearPlane, double farPlane) {
|
||||||
|
gWin.cullNear = nearPlane;
|
||||||
|
gWin.cullFar = farPlane;
|
||||||
|
}
|
||||||
|
double rlGetCullDistanceNear(void) { return gWin.cullNear; }
|
||||||
|
double rlGetCullDistanceFar(void) { return gWin.cullFar; }
|
||||||
|
|
||||||
|
unsigned int rlLoadVertexBuffer(const void* buffer, int size, bool dynamic) {
|
||||||
|
GLuint id = 0;
|
||||||
|
gl::glGenBuffers(1, &id);
|
||||||
|
gl::glBindBuffer(GL_ARRAY_BUFFER, id);
|
||||||
|
gl::glBufferData(GL_ARRAY_BUFFER, size, buffer, dynamic ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
void rlUnloadVertexBuffer(unsigned int vboId) { gl::glDeleteBuffers(1, &vboId); }
|
||||||
|
void rlUpdateVertexBuffer(unsigned int bufferId, const void* data, int dataSize, int offset) {
|
||||||
|
gl::glBindBuffer(GL_ARRAY_BUFFER, bufferId);
|
||||||
|
gl::glBufferSubData(GL_ARRAY_BUFFER, offset, dataSize, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rlEnableShader(unsigned int id) { gl::glUseProgram(id); }
|
||||||
|
void rlDisableShader(void) { gl::glUseProgram(0); }
|
||||||
|
|
||||||
|
void rlSetUniform(int locIndex, const void* value, int uniformType, int count) {
|
||||||
|
switch (uniformType) {
|
||||||
|
case RL_SHADER_UNIFORM_FLOAT: gl::glUniform1fv(locIndex, count, static_cast<const float*>(value)); break;
|
||||||
|
case RL_SHADER_UNIFORM_VEC2: gl::glUniform2fv(locIndex, count, static_cast<const float*>(value)); break;
|
||||||
|
case RL_SHADER_UNIFORM_VEC3: gl::glUniform3fv(locIndex, count, static_cast<const float*>(value)); break;
|
||||||
|
case RL_SHADER_UNIFORM_VEC4: gl::glUniform4fv(locIndex, count, static_cast<const float*>(value)); break;
|
||||||
|
case RL_SHADER_UNIFORM_INT: gl::glUniform1iv(locIndex, count, static_cast<const int*>(value)); break;
|
||||||
|
case RL_SHADER_UNIFORM_IVEC2: gl::glUniform2iv(locIndex, count, static_cast<const int*>(value)); break;
|
||||||
|
case RL_SHADER_UNIFORM_IVEC3: gl::glUniform3iv(locIndex, count, static_cast<const int*>(value)); break;
|
||||||
|
case RL_SHADER_UNIFORM_IVEC4: gl::glUniform4iv(locIndex, count, static_cast<const int*>(value)); break;
|
||||||
|
case RL_SHADER_UNIFORM_SAMPLER2D: gl::glUniform1iv(locIndex, count, static_cast<const int*>(value)); break;
|
||||||
|
default: note("SHADER: failed to set uniform value, data type not recognized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void rlSetUniformMatrix(int locIndex, Matrix mat) {
|
||||||
|
const float m[16] = {mat.m0, mat.m1, mat.m2, mat.m3, mat.m4, mat.m5, mat.m6, mat.m7,
|
||||||
|
mat.m8, mat.m9, mat.m10, mat.m11, mat.m12, mat.m13, mat.m14, mat.m15};
|
||||||
|
gl::glUniformMatrix4fv(locIndex, 1, GL_FALSE, m);
|
||||||
|
}
|
||||||
|
void rlSetUniformSampler(int locIndex, unsigned int) { (void)locIndex; }
|
||||||
|
int rlGetLocationUniform(unsigned int id, const char* name) { return gl::glGetUniformLocation(id, name); }
|
||||||
|
int rlGetLocationAttrib(unsigned int, const char*) { return -1; }
|
||||||
|
|
||||||
|
bool rlEnableVertexArray(unsigned int vaoId) {
|
||||||
|
gl::glBindVertexArray(vaoId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void rlDisableVertexArray(void) { gl::glBindVertexArray(0); }
|
||||||
|
void rlEnableVertexBuffer(unsigned int id) { gl::glBindBuffer(GL_ARRAY_BUFFER, id); }
|
||||||
|
void rlDisableVertexBuffer(void) { gl::glBindBuffer(GL_ARRAY_BUFFER, 0); }
|
||||||
|
void rlEnableVertexAttribute(unsigned int index) { gl::glEnableVertexAttribArray(index); }
|
||||||
|
void rlDisableVertexAttribute(unsigned int index) { gl::glDisableVertexAttribArray(index); }
|
||||||
|
void rlSetVertexAttribute(unsigned int index, int compSize, int type, bool normalized, int stride, int offset) {
|
||||||
|
gl::glVertexAttribPointer(index, compSize, static_cast<GLenum>(type), normalized ? GL_TRUE : GL_FALSE, stride,
|
||||||
|
reinterpret_cast<const void*>(static_cast<std::intptr_t>(offset)));
|
||||||
|
}
|
||||||
|
void rlSetVertexAttributeDivisor(unsigned int index, int divisor) { gl::glVertexAttribDivisor(index, static_cast<GLuint>(divisor)); }
|
||||||
|
|
||||||
|
void rlDrawVertexArray(int offset, int count) { gl::glDrawArrays(GL_TRIANGLES, offset, count); }
|
||||||
|
void rlDrawVertexArrayInstanced(int, int count, int instances) {
|
||||||
|
gl::glDrawArraysInstanced(GL_TRIANGLES, 0, count, instances); // raylib ignores `offset` here (known quirk)
|
||||||
|
++gReport.drawCalls;
|
||||||
|
gReport.instancesDrawn += instances;
|
||||||
|
checkGl("glDrawArraysInstanced");
|
||||||
|
}
|
||||||
|
void rlDrawRenderBatchActive(void) {}
|
||||||
|
|
||||||
|
void rlActiveTextureSlot(int slot) { gl::glActiveTexture(GL_TEXTURE0 + static_cast<GLenum>(slot)); }
|
||||||
|
void rlEnableTexture(unsigned int id) { gl::glBindTexture(GL_TEXTURE_2D, id); }
|
||||||
|
void rlDisableTexture(void) { gl::glBindTexture(GL_TEXTURE_2D, 0); }
|
||||||
|
void rlTextureParameters(unsigned int id, int param, int value) {
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, id);
|
||||||
|
gl::glTexParameteri(GL_TEXTURE_2D, static_cast<GLenum>(param), value);
|
||||||
|
gl::glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
unsigned int rlGetTextureIdDefault(void) { return gWin.defaultTexture; }
|
||||||
|
unsigned int rlGetShaderIdDefault(void) { return gWin.defaultShader; }
|
||||||
|
|
||||||
|
void rlSetCullFace(int mode) { gl::glCullFace(mode == RL_CULL_FACE_BACK ? GL_BACK : GL_FRONT); }
|
||||||
|
void rlEnableBackfaceCulling(void) { gl::glEnable(GL_CULL_FACE); }
|
||||||
|
void rlDisableBackfaceCulling(void) { gl::glDisable(GL_CULL_FACE); }
|
||||||
|
void rlEnableDepthTest(void) { gl::glEnable(GL_DEPTH_TEST); }
|
||||||
|
void rlDisableDepthTest(void) { gl::glDisable(GL_DEPTH_TEST); }
|
||||||
|
void rlEnableColorBlend(void) { gl::glEnable(GL_BLEND); }
|
||||||
|
void rlDisableColorBlend(void) { gl::glDisable(GL_BLEND); }
|
||||||
|
void rlEnableFramebuffer(unsigned int id) { gl::glBindFramebuffer(GL_FRAMEBUFFER, id); }
|
||||||
|
void rlDisableFramebuffer(void) { gl::glBindFramebuffer(GL_FRAMEBUFFER, gWin.screenFbo); }
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,34 @@
|
|||||||
|
// What the GL shim lets a test inspect. Not part of raylib: only the verification harness includes this.
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace shim {
|
||||||
|
|
||||||
|
struct Frame {
|
||||||
|
int width = 0, height = 0;
|
||||||
|
std::vector<std::uint8_t> rgba; // top row first, alpha forced to 255 (like LoadImageFromScreen)
|
||||||
|
|
||||||
|
[[nodiscard]] bool valid() const { return width > 0 && height > 0 && rgba.size() == std::size_t(width) * std::size_t(height) * 4; }
|
||||||
|
[[nodiscard]] const std::uint8_t* px(int x, int y) const { return &rgba[(std::size_t(y) * std::size_t(width) + std::size_t(x)) * 4]; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Report {
|
||||||
|
std::string glVersion, glRenderer, glsl;
|
||||||
|
int glErrors = 0; // GL errors and error-type debug messages seen so far
|
||||||
|
int shaderFailures = 0; // shader stages/programs that failed to compile or link
|
||||||
|
int framesPresented = 0; // EndDrawing calls
|
||||||
|
long drawCalls = 0; // glDrawArraysInstanced calls since the last reset
|
||||||
|
long instancesDrawn = 0; // sum of their instance counts
|
||||||
|
std::vector<std::string> log; // shader logs, GL debug messages, raylib TraceLog lines
|
||||||
|
std::vector<std::string> texts; // every DrawText string, in order
|
||||||
|
};
|
||||||
|
|
||||||
|
Report& report();
|
||||||
|
void resetCounters(); // zero drawCalls/instancesDrawn/texts (not the error totals)
|
||||||
|
const Frame& lastScreenshot(); // what LoadImageFromScreen returned last
|
||||||
|
Frame readScreen(); // the current contents of the window's framebuffer
|
||||||
|
bool writePng(const std::string& path, const Frame& f);
|
||||||
|
|
||||||
|
} // namespace shim
|
||||||
Reference in new issue
Block a user