From 5f61ee37505e87daaa1dc5a2ff055139ce487faf Mon Sep 17 00:00:00 2001 From: Emil <65846814+emil28092005@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:53:56 +0300 Subject: [PATCH] Document and demonstrate prepared mesh LOD visibility --- cmake/GpuVisibility.cmake | 6 ++ docs/manual/editor/visibility-lod.md | 68 ++++++++++++++++++++++ examples/renderer/p2_visibility.cpp | 87 ++++++++++++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 162 insertions(+) create mode 100644 docs/manual/editor/visibility-lod.md create mode 100644 examples/renderer/p2_visibility.cpp diff --git a/cmake/GpuVisibility.cmake b/cmake/GpuVisibility.cmake index 16c3536..fe0d68b 100644 --- a/cmake/GpuVisibility.cmake +++ b/cmake/GpuVisibility.cmake @@ -1,3 +1,9 @@ +if(TARGET faset_render) + add_executable(faset_p2_visibility_example + "${PROJECT_SOURCE_DIR}/examples/renderer/p2_visibility.cpp") + target_link_libraries(faset_p2_visibility_example PRIVATE faset_render) +endif() + if(TARGET faset_render AND BUILD_TESTING) add_executable(faset_render_gpu_visibility_tests "${PROJECT_SOURCE_DIR}/tests/render_gpu_visibility_tests.cpp") diff --git a/docs/manual/editor/visibility-lod.md b/docs/manual/editor/visibility-lod.md new file mode 100644 index 0000000..24b0630 --- /dev/null +++ b/docs/manual/editor/visibility-lod.md @@ -0,0 +1,68 @@ +# GPU visibility and prepared mesh LOD + +Faset can draw opaque static triangle meshes through GPU frustum culling and, +optionally, two-pass HZB occlusion. The original direct renderer remains available +for comparison. In the Editor, build with `FASET_DEBUG_IMGUI=ON`, press **F12**, +and choose **Direct**, **GPU frustum**, or **GPU occlusion** in Diagnostics. **Path: +active** confirms that the selected GPU path actually ran. See +[developer diagnostics](diagnostics.md) for the HZB preview and counters. + +The current GPU path groups instances with the same mesh and texture into fixed +indirect draw bins. A previous-frame HZB can defer an object, but the current-frame +post pass checks it again before the final image. Sprites, editor UI, transparent +meshes, and shadow casters keep their existing ordered or shadow paths. This option +does not require ray tracing or mesh shaders. + +## Supply prepared LODs from C++ + +`DrawItem::mesh` is LOD 0. Set `lod_meshes[0]` to a prepared LOD 1 mesh, +`lod_meshes[1]` to LOD 2, and so on. These are actual `Mesh` objects with a +complete triangle list; Faset does not simplify a source mesh automatically. +Keep a stable `instance_key` across frames. Give different scene objects different +keys, and retain the same shared mesh objects instead of rebuilding them every +frame. For a manually constructed `Snapshot`, supply both `projection` and +`view_projection`; set `view_id` for each camera and `camera_cut` when switching +shots. The Player's scene extraction fills the camera fields and instance keys. + +```cpp +faset::render::DrawItem draw; +draw.mesh = prepared_high_detail; +draw.lod_meshes = {prepared_medium_detail, prepared_low_detail}; +draw.instance_key = "scene/object-id/primitive-id"; +draw.model = faset::render::transform({0, 0, 0}); +snapshot.draws.push_back(std::move(draw)); +``` + +LOD selection uses projected screen size. The first transition is 192 pixels; +each additional level halves that threshold. A 12% hysteresis band avoids +switching back and forth near a boundary. Missing levels fall back to an available +mesh. Changing level invalidates that instance's temporal occlusion state while +preserving its logical key. `FrameStats::lod_counts` reports selected levels 0–3 +(higher levels are included in the final bucket). + +For a runnable C++ example, build `faset_p2_visibility_example` and capture the +same scene in any mode: + +```sh +cmake --build --preset linux-debug --target faset_p2_visibility_example +build/linux-debug/faset_p2_visibility_example occlusion /tmp/p2-visibility.ppm +build/linux-debug/faset_p2_visibility_example direct /tmp/p2-direct.ppm +``` + +The example creates a fine cube and a prepared coarse tetrahedron in +[`examples/renderer/p2_visibility.cpp`](https://github.com/emil28092005/Faset_Engine/blob/main/examples/renderer/p2_visibility.cpp). +Imported GLB geometry can be supplied as prepared levels by C++ integration, +but automatic LOD generation and assigning a GLB's alternate meshes as LODs in +the Inspector are not implemented yet. + +## Measure before choosing a mode + +The GPU route saves per-instance CPU vertex transformation and direct draw +submission, but its compute passes and indirect rendering have a cost. Keep the +same scene, resolution and camera path when comparing modes. Hide Diagnostics for +an ordinary Player measurement: opening it enables GPU counter readback, while +**Show HZB** adds a separate image copy. The renderer currently captures and +waits for every frame even without this overlay. Read +[profiling and measurements](profiling.md) and the +[P2 acceptance study](https://github.com/emil28092005/Faset_Engine/blob/main/docs/studies/19-p2-gpu-visibility-acceptance.md) +for the measured scope and limitations. diff --git a/examples/renderer/p2_visibility.cpp b/examples/renderer/p2_visibility.cpp new file mode 100644 index 0000000..71997f2 --- /dev/null +++ b/examples/renderer/p2_visibility.cpp @@ -0,0 +1,87 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace faset::render; + +namespace { +std::shared_ptr coarse_mesh() { + auto mesh = std::make_shared(); + mesh->vertices = { + Vertex{{-1, -1, -1}, {-1, -1, -1}}, + Vertex{{1, -1, -1}, {1, -1, -1}}, + Vertex{{0, 1, 0}, {0, 1, 0}}, + Vertex{{0, -1, 1}, {0, -1, 1}}, + }; + mesh->indices = {0, 2, 1, 0, 1, 3, 1, 2, 3, 2, 0, 3}; + return mesh; +} + +VisibilityMode mode_from_argument(std::string_view argument) { + if (argument == "direct") + return VisibilityMode::Direct; + if (argument == "frustum") + return VisibilityMode::GpuFrustum; + if (argument == "occlusion") + return VisibilityMode::GpuOcclusion; + throw std::invalid_argument("mode must be direct, frustum, or occlusion"); +} +} // namespace + +int main(int argc, char** argv) { + try { + if (argc > 3) + throw std::invalid_argument("usage: faset_p2_visibility_example " + "[direct|frustum|occlusion] [capture.ppm]"); + RendererConfig config; + config.width = 640; + config.height = 360; + config.headless = true; + config.visibility_mode = argc > 1 ? mode_from_argument(argv[1]) + : VisibilityMode::GpuOcclusion; + config.visibility_diagnostics = true; + Renderer renderer(config); + + Snapshot frame; + frame.view_id = "p2-lod-example-camera"; + frame.eye = {0, 2, 12}; + frame.projection = perspective(.9f, float(config.width) / config.height, .1f, 100); + frame.view_projection = multiply(frame.projection, + look_at(frame.eye, {0, 0, 0})); + const auto prepared_coarse = coarse_mesh(); + for (int i = 0; i < 9; ++i) { + DrawItem item; + item.mesh = cube_mesh(); + item.lod_meshes = {prepared_coarse}; + item.instance_key = "example/cube/" + std::to_string(i); + item.model = transform({float(i - 4) * 1.8f, 0, i % 2 ? -5.f : 0.f}); + item.color = {0.28f + .07f * i, .6f, .8f, 1}; + item.cast_shadow = false; + frame.draws.push_back(std::move(item)); + } + + // The second and later frames have compatible HZB history for this view. + for (int i = 0; i < 3; ++i) + renderer.render(frame); + const auto& stats = renderer.stats(); + renderer.capture(argc > 2 ? std::filesystem::path(argv[2]) + : std::filesystem::path("p2-visibility.ppm")); + std::cout << "GPU path: " << (stats.gpu_visibility_active ? "active" : "unavailable") + << ", bins: " << stats.gpu_bins + << ", visible: " << stats.gpu_visible_instances + << ", LOD0: " << stats.lod_counts[0] + << ", LOD1: " << stats.lod_counts[1] + << ", validation errors: " << stats.validation_errors << '\n'; + return stats.validation_errors ? 1 : 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/mkdocs.yml b/mkdocs.yml index c0ef3c3..3622a6d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,7 @@ nav: - Editor workspace: editor/workspace.md - Scene templates: editor/templates.md - Assets and Blender: editor/assets.md + - GPU visibility and mesh LOD: editor/visibility-lod.md - Build, Play, and export: editor/export.md - Profiling and measurements: editor/profiling.md - Optional developer diagnostics: editor/diagnostics.md