Repository navigation
feat(collision): persist collision nodes and add Walk APIs - #3791
Conversation
45da8f2 to
a536b3b
Compare
a536b3b to
63b4c08
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
3 open issues
What changed in this PR
Adds native collision authoring and a collision-aware Walk camera, including UI panels/controls, persistence in .licht, rendering integration for voxel previews, and expanded test coverage for the new behavior.
Changes:
- Introduces a native Collision panel for authoring voxel colliders from visible splats with async progress/cancel/error reporting.
- Adds Walk camera navigation mode gated by loaded collision, plus rendering hooks to preview voxel first-hit faces/edges.
- Persists collision payloads/display state in scene archives and undo/redo, and adds C++/Python tests/contracts.
| File | Description |
|---|---|
| tools/generate_collision_icon.py | Adds a small script to generate/paste the collision icon into the scene atlas. |
| tests/test_viewport_depth_layout.cpp | Adds regression test for text input while another panel captures the pointer. |
| tests/test_collision_scene.cpp | Adds CPU tests for collision persistence, undo/redo, preview state, and Walk behavior. |
| tests/python/test_toolbar_hooks.py | Updates toolbar expectations to include the new Walk camera mode button. |
| tests/python/test_collision_panel_contract.py | Verifies Collision panel RML IDs/sections remain consistent with code expectations. |
| tests/python/test_collision_controls.py | Adds behavior tests for Rendering > Collision controls and safety/error handling. |
| tests/CMakeLists.txt | Registers the new collision scene test binary and ensures needed deps are linked for format tests. |
| src/visualizer/rendering/viewport_request_builder.cpp | Plumbs collision preview data/owners into Vulkan draw items and disables shadows for voxel box preview. |
| src/visualizer/rendering/rendering_manager_events.cpp | Treats DISPLAY_CHANGED as overlay/mesh dirty for redraw. |
| src/visualizer/preferences.cpp | Allows persisted camera mode preference value "walk". |
| src/visualizer/operation/undo_entry.hpp | Extends scene graph snapshots to include collision payload and display state. |
| src/visualizer/operation/undo_entry.cpp | Captures/restores collision payload/display in undo/redo and estimates snapshot memory. |
| src/visualizer/input/input_controller.hpp | Adds Walk camera mode API and stores walk state/settings/error in controller. |
| src/visualizer/input/input_controller.cpp | Implements Walk mode gating, collision query building, movement solver integration, and disables zoom/pan/pivot while walking. |
| src/visualizer/gui/rmlui/rml_theme.cpp | Adds atlas sprite mapping for collision icon. |
| src/visualizer/gui/rmlui/resources/viewport_overlay.rcss | Minor style spacing adjustment. |
| src/visualizer/gui/rmlui/resources/rendering.rml | Adds Rendering > Collision sections for Walk and voxel preview scrubs. |
| src/visualizer/gui/rmlui/resources/rendering.rcss | Adds styling for read-only collision controls. |
| src/visualizer/gui/rmlui/resources/menubar.rml | Adds disabled state binding for toolbar buttons. |
| src/visualizer/gui/rmlui/resources/menubar.rcss | Styles disabled toolbar buttons. |
| src/visualizer/gui/rmlui/resources/collision.theme.rcss | Adds theme overrides for the new Collision panel UI elements. |
| src/visualizer/gui/rmlui/resources/collision.rml | Adds the new native Collision floating window markup. |
| src/visualizer/gui/rmlui/resources/collision.rcss | Adds layout/styling rules for the Collision panel contents. |
| src/visualizer/gui/rmlui/elements/scene_graph_element.hpp | Extends node snapshot data to flag collision nodes for icon changes. |
| src/visualizer/gui/rmlui/elements/scene_graph_element.cpp | Uses collision icon/label for voxel-collider nodes in the scene graph UI and drag ghost. |
| src/visualizer/gui/rml_viewport_overlay.cpp | Ensures tooltip clearing doesn’t stomp a focused text field receiving typing. |
| src/visualizer/gui/rml_menu_bar.hpp | Extends toolbar button model with an enabled flag and adds tooltip slot for Walk. |
| src/visualizer/gui/rml_menu_bar.cpp | Disables Walk camera button when collision is unavailable and blocks disabled clicks. |
| src/visualizer/gui/resources/locales/*.json | Adds locale keys/strings for collision authoring and Walk UI and tooltips. |
| src/visualizer/gui/gui_manager.cpp | Registers the new native Collision panel in the GUI manager. |
| src/visualizer/gui/collision_panel.hpp | Declares CollisionPanel with async worker, progress/cancel, and control parsing. |
| src/visualizer/gui/collision_panel.cpp | Implements collision authoring pipeline: snapshot visible splats, voxelize, create/update collider node, and UI wiring. |
| src/visualizer/gui/collision_job_status.hpp | Adds helper to categorize collision failures with actionable advice. |
| src/visualizer/gui/assets/icon/src/walk.svg | Adds Walk icon source asset. |
| src/visualizer/gui/assets/icon/scene/LICENSE | Documents collision icon provenance and atlas composition. |
| src/visualizer/gui/assets/icon/LICENSE | Notes Walk icon as original artwork on the Tabler grid. |
| src/visualizer/CMakeLists.txt | Builds collision_panel.cpp as part of the visualizer library. |
| src/python/stubs/lichtfeld/__init__.pyi | Updates Python API stubs to include walk mode and collision controls functions. |
| src/python/lfs_plugins/tools_menu.py | Adds Tools > Collision menu item to open the native collision panel. |
| src/python/lfs_plugins/toolbar.py | Adds Walk to camera mode specs and disables it when collision is unavailable. |
| src/python/lfs_plugins/scrub_fields.py | Adds set_enabled() and blocks scrub interaction when disabled. |
| src/python/lfs_plugins/rendering_panel.py | Wires Rendering > Collision controls into the rendering panel model/update cycle. |
| src/python/lfs_plugins/preferences_panel.py | Adds Walk to navigation mode preferences options. |
| src/python/lfs_plugins/collision_rendering.py | Implements Rendering > Collision control bindings and safe live edits via Python API. |
| src/python/lfs/module.cpp | Exposes walk/collision control APIs to Python and updates camera mode parsing/errors. |
| src/io/project/scene_chapter_adapter.cpp | Persists collision payload/display in staged scene node descriptors. |
| src/io/project/project_chapters.cpp | Serializes/deserializes collision payload/display in SCNG nodes section with validation. |
| src/io/include/io/project_chapters.hpp | Adds collision fields to SceneNodeRecord and custom equality logic for shared_ptr payloads. |
| src/core/scene.cpp | Adds DISPLAY_CHANGED mutation handling, includes collision fields in visible mesh snapshot, adds graph_epoch, and adds snapshotVisibleSplatGeometry(). |
| src/core/include/core/scene.hpp | Extends scene/node/visible mesh structures to carry collision payload/display and adds snapshotVisibleSplatGeometry()/graphEpoch(). |
| docs/docs/features/collision.md | Adds user documentation for collision authoring, preview, Walk, saving/export, and limits. |
Review effort: Lite
63b4c08 to
c194e86
Compare
5bdc4ef to
ef91255
Compare
ef91255 to
4ff5b50
Compare
MrNeRF
left a comment
There was a problem hiding this comment.
Built the stack at #3793's head and tried it on three scenes, including a small walk course I made with a floor, a step, a wall and a low slab.
- Walk: it holds eye height, steps up and down smoothly, stops at the wall with the right clearance and is blocked under the slab. Walking has no measurable per-frame cost.
- .licht: saving and reopening restores the collider.
- Tests: native and Python tests all pass.
Size is the same issue as on #3789. This layer adds 3.4k lines, while the whole feature should come in at around 2–3k.
- Typing into a collision panel scrub breaks the panel.
- collision_panel.cpp:219 commits on every
changeevent, and text inputs fire one per keystroke. Typing 0.005 commits "0" ("Voxel size must be finite and positive"). beginEditthen throws at :197 and leaves the edit state set. From then on both scrubs ignore clicks, and Generate fails with "Invalid number: voxel-size" until restart.- These ~150 lines (:135–291) duplicate the Python
ScrubFieldControllerthis PR already uses under Rendering > Collision. That one commits on Enter.
- collision_panel.cpp:219 commits on every
- Escape is hardcoded. In Walk,
handleKeyreturns early onKEY_ESCAPE(input_controller.cpp:1978), before GUI focus is checked. Pressing Escape while editing Head height cancels the edit and also leaves Walk. It needs to be a key map action with a default binding, a locale entry and key map tests. - Generate can't be undone.
- The panel adds or replaces the mesh and collision data directly on the scene (collision_panel.cpp:570, 589–616), outside the undo history.
- After Generate, undo removed an unrelated transform and the collider stayed.
- Regenerating overwrites the previous occupancy for good.
- Walk falls forever when there is nothing below.
- From the home view of a 137k scene, the camera fell to y=−576 in about 10 s and kept accelerating, rendering every frame until Escape.
- On a 5M scene that isn't aligned to gravity, 15 of 20 start points fell.
- Entering Walk should be refused here, or there should be a floor.
- Older builds open a project with a collider as a giant opaque box, shown as "Collision (8V / 12F)".
- The cause: the collision rides on a MESH node with an 8-vertex stand-in mesh, and the format version doesn't change.
- Saving the project in the older build drops the collision keys. The PR build then reopens it with no Walk and the stand-in box left over.
- A real node type would avoid both, and would also stop MCP reporting
gaussian_count8 for the collider.
- Stale-result rejection discards valid results.
- It is keyed on
renderGeneration(collision_panel.cpp:541), which also changes on display, visibility and transform edits. - Changing an existing collider's preview opacity during a 2.4 s generation threw the new result away.
- The UI only says "discarded".
- It is keyed on
- An error on every startup after using Walk. With "remember camera navigation" on,
InputController::initialize(:577) restores Walk before a scene exists. Every startup then logs "Cannot enter walk mode" and shows a toast. - Localization and brand names.
- All 88 new keys are English copies in the other nine locales.
- About 50 user-visible strings are C++ literals that never reach the locale files: failure titles and advice in collision_job_status.hpp, progress stages, the Walk toast and export errors.
- "SuperSplat" appears in UI text, including step-by-step instructions for that product ("SuperSplat Studio → Assets → Collision"). Naming the file format is fine; instructions for a third-party product are not.
Smaller things:
- The panel is its own thread/progress/cancel/error implementation (801 lines) instead of
AsyncTaskManager. As a result there is no runtime job, and MCPruntime_job_*can't see the work. Python can switch to Walk and read the controls, but nothing can generate a collider from Python or MCP. - The preview cost about 5.8 ms per frame in my run, where the docs say 0.7–0.9 ms.
- Clicking Generate on a 5M scene freezes the UI for one 202 ms frame, because the splat snapshot (collision_panel.cpp:454) runs on the GUI thread.
- tools/generate_collision_icon.py is a second icon generator. An SVG under icon/src/scene/ goes through the existing exporter. walk.svg also uses
#fffinstead ofcurrentColor. - The Collision section sits above Quality & Performance in the Rendering panel, even in an empty scene.
- This PR already has five
surface_readychecks and "collision surface" text for a state only #3793 creates.
4ff5b50 to
4687fd5
Compare
4687fd5 to
9a42100
Compare
f66c37a to
2865138
Compare
|
@MrNeRF The collision stack has been rebuilt and is ready for review now. The PR descriptions include the changes, validation results, and current line counts. Thanks! |
MrNeRF
left a comment
There was a problem hiding this comment.
Rebuilt the stack and went through my last review again in the GUI. Almost everything is fixed:
- Fields: scrub fields commit on Enter.
- Escape: it's a real
EXIT_WALKaction, with profile v37, the history row and all locales. - Generate: it's undoable.
- Walk entry: entering Walk over empty space is refused.
- Node type and startup: the collider is its own node type, and startup no longer errors after using Walk.
- Stale results: they're no longer thrown away on display changes.
- Strings: locales are translated and the product instructions are gone.
- Background job: generation and export run as an
AsyncTaskManagerjob that MCP can wait on. - APIs: Python and MCP can generate and export.
- Preview: it costs under ~1 ms per frame.
- A large collider makes the project unsaveable and stops the renderer.
- Repro: one 5M-scene collider at 0.0125 (7.08M records), or 12 duplicates of a 0.05 collider.
project_save_asthen fails with "Scene graph exceeds the project JSON chapter size supported by the reader" (project_chapters.cpp:2862).- The exception reaches the frame loop: "Frame failed" nine times, then the "Renderer Error … stopped" dialog.
- The occupancy is stored as hex in the scene-graph JSON, 8 bytes per word, written once for every node that carries it. That's 5.7 MiB per collider on a 5M scene at 0.05.
- Could it go into a binary payload chapter, once per volume?
- A collider generated in one project lands in the next.
- Start generation on the 5M scene, then open another project. The job finishes and adds the old scene's collider to the new project as "Collision_2", with the old scene's extents.
- The result check (
async_task_manager.cpp:3832/3844) only comparesgallery_scene_epoch_, which opening a project doesn't change.
KeymapTest.EveryDefaultKeyDispatchesAcrossSavedLayoutsfails (test_keymap_layout_contract.cpp:126). In the new Walk tool mode, the global Cancel Polygon binding on Escape dispatchesEXIT_WALKon every layout. The default-contract test was updated; this one wasn't. CI doesn't runlichtfeld_tests, so it shows green.
Smaller things:
- Walk ends at any drop larger than the step height. It switches to Orbit and logs an error (
collision_walk.cpp:245); if W is still held, the camera then flies off in Orbit. On a 5M scene that isn't gravity-aligned, 15 of 16 walks ended within 0.2–2.3 s. On an aligned course it works well. - The collider is a root node, not a child of its splat. Moving the splat during or after generation leaves the collider behind, and moving it mid-generation isn't detected.
- Generating on a 5M scene stalls the UI. Five frames of ~720 ms each (about 1.4 fps for 3 s), and VRAM peaks 1.5 GiB higher than before. The GPU work comes from #3789's path.
- Older builds refuse the whole project ("unsupported scene node type"), splats included, and the minimum reader version still says 1.0.0. The same trade-off was accepted for crop polygons, but users get no hint that a newer version is needed.
- About 12 English literals still reach toasts and the panel, e.g. "Cannot enter Walk: No Walk support below the camera".
- Python duplicates: the functions are registered twice, on
lfand onlf.scene.
2865138 to
f9acb03
Compare
f9acb03 to
43f1ddb
Compare
MrNeRF
left a comment
There was a problem hiding this comment.
Checked it in the app against my round-2 findings:
- Large colliders save:
- One 7.08M-record collider with a 1.1 GB splat saves in 4.9 s.
- Twelve copies add only 12.7 KB, since they share one COLV payload. That project reopens in 0.6 s and finishes loading at 3.5 s.
- A failed save shows a toast, and the renderer keeps running.
- A master build opening such a project asks for a newer version.
- No collider in the next project: I started a 0.005 generation and opened another project. The job ended as cancelled, and only the new project's nodes were there.
- Walk: Escape leaves Walk mode through the
EXIT_WALKkey map action, andKeymapTest.EveryDefaultKeyDispatchesAcrossSavedLayoutspasses. Walking off a ledge above the step height slides down instead of leaving Walk. - Smaller fixes:
- The collider is a child of its splat.
- The UI stall during generation is down from ~720 ms frames to ≤177 ms.
lf.*andlf.scene.*are the same objects.
- Python and MCP:
generate_collision,export_collision,cancel_collision,get_collision_state,set_walk_settings,scene_generate_collision/scene_export_collisionand thecollision.mainjob all work.
Approving. Follow-ups:
- Opening another project doesn't stop a running generation. It finishes (about 14 s more GPU work and up to 2.7 GB in my run) and is then discarded (async_task_manager.cpp:3866). Cancelling the collision job from the project lifecycle would avoid that.
- Errors are localized by matching their English text, and the mapping exists twice (input_controller.cpp:66–75 and collision_rendering.py:21–45). Generation and export errors (record limit, "Voxel export requires uniform scale…") still reach the popup in English. Having the native code report keys would fix both.
collision_serialization.hppbelongs with the project chapters: it lives in core but onlyproject_document.cppuses it, and it hand-rolls little-endian put/get. Inio/projectit could usechapter_binary_utils.hpp'sread_u32/write_u32/read_f32(about −35 lines).- Leftovers:
collision_byte_budgetand thebyte_budgetparameters ofencodeCollision/decodeCollisiononly serve tests (project_document.cpp:2104, 2213–2217; collision_serialization.hpp:16–20, 49–50).captureCollisionSourcesalways returns exactly one source (scene.cpp:4348), and thepreparekernel'soffsetis always 0.- The
lfaliases at module.cpp:3372–3373 are also listed twice in the stubs.
- Formatting:
- clang-format 21 wants a change at py_scene.cpp:1541.
- project_document.cpp:6360 re-indents a master line this PR doesn't otherwise touch.
- Memory, from reading:
captureCollisionSourcesclones the whole combined model before slicing it, so GPU memory scales with every splat in the scene, not just the target.
4325445 to
dbb68dc
Compare


Add real independent
COLLISIONnodes, asynchronous generation/export APIs and safe Walk navigation. Collision geometry is immutable, has real bounds and no mesh/Gaussian proxy, and supports undo/redo, duplication, clipboard, transforms and persistence.Projects store one binary COLV payload per distinct volume, with UUID references from scene nodes. Duplicates share validated payloads, deferred opens/Save As avoid decoding unused volumes, and hydration reuses them. Training snapshots and recovery preserve the same payloads. The existing capability mechanism requires
collision-volumes-v1; older readers get the newer-reader hint. Save failures settle at the save-operation boundary instead of escaping into the frame loop. Unreleased inline-hex collision payloads are rejected explicitly.Generation requires one selected loaded SPLAT, including hidden sources. The owned geometry/world-transform capture is checked against document replacement, scene lifecycle and source identity/content before adoption. Open/reopen/new/clear and source changes cannot install stale work. The result is parented to its source with an inverse captured-world local transform, preserving initial alignment while following later parent edits.
Walk owns canonical tested feet independently of the smoothed camera. Sweeps retain wall/ceiling clearance, minimum-step lifting and bounded support behavior; unsupported ledges stop motion. Failed entry leaves the prior camera/mode intact and produces one useful error rather than stopping the renderer. Hidden loaded colliders remain eligible. Live settings are read from the controller;
lfandlf.scenealias the same callable APIs. Walk's solver and independent regressions live in this consuming layer.Validation before the upstream-only rebase includes exact-head full Release build, core/CUDA/export suites, real project open/reopen/clear/new during pending jobs, real binary save/reopen/deferred Save As, shared-volume deduplication, capability/corruption/read-budget/save-failure cases, source snapshot lifetime, repeated transformed grounding, steps/ceilings/ledges, scoped undo and keymap dispatch. The exact scene-head focused visualizer suite passed 324 tests with one CUDA-dependent skip; generated stubs match independently generated clean layer builds.
Master through this layer adds 4,179 physical lines; the complete stack adds exactly 5,500 against
8ba8e91416ed59d1a6497a393e1725dfa849e92d, all-inclusive. The top PR supplies the generator/export dialogs, toolbars, reactive settings UI and voxel preview.Synced onto upstream
8ba8e9141(#3820), preserving its STBN source/include fix exactly. Each layer differs from its previously validated head only by those two upstream files; collision code and the combined 5,500-line count are unchanged. The new top head passed the full all-core Release build, native suites and generated-stub check; source gates and the full Python suite pass after the upstream fix.