Skip to content

feat(collision): persist collision nodes and add Walk APIs - #3791

Merged
MrNeRF merged 1 commit into
feat/voxelize-clifrom
feat/collision-authoring-walk
Oct 26, 2026
Merged

MrNeRF merged 1 commit into
feat/voxelize-clifrom
feat/collision-authoring-walk

Conversation

@shadygm

@shadygm shadygm commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Add real independent COLLISION nodes, 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; lf and lf.scene alias 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.

@shadygm
shadygm added this pull request to stack #3792 October 24, 2026 21:49
@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch from 45da8f2 to a536b3b Compare October 24, 2026 22:02
Copilot AI balanced review requested due to automatic review settings October 24, 2026 22:02
@shadygm shadygm changed the title Add native collision authoring and Walk camera Add collision authoring UI and Walk controls Oct 24, 2026
@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch from a536b3b to 63b4c08 Compare October 24, 2026 22:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/visualizer/gui/collision_panel.cpp Outdated
Comment thread src/visualizer/gui/collision_panel.cpp Outdated
Comment thread src/visualizer/gui/collision_panel.cpp Outdated
@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch from 63b4c08 to c194e86 Compare October 24, 2026 22:42
@shadygm
shadygm marked this pull request as ready for review October 24, 2026 22:50
@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch 2 times, most recently from 5bdc4ef to ef91255 Compare October 25, 2026 10:43
@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch from ef91255 to 4ff5b50 Compare October 25, 2026 12:40

@MrNeRF MrNeRF left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. Typing into a collision panel scrub breaks the panel.
    • collision_panel.cpp:219 commits on every change event, and text inputs fire one per keystroke. Typing 0.005 commits "0" ("Voxel size must be finite and positive").
    • beginEdit then 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 ScrubFieldController this PR already uses under Rendering > Collision. That one commits on Enter.
  2. Escape is hardcoded. In Walk, handleKey returns early on KEY_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.
  3. 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.
  4. 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.
  5. 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_count 8 for the collider.
  6. 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".
  7. 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.
  8. 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 MCP runtime_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 #fff instead of currentColor.
  • The Collision section sits above Quality & Performance in the Rendering panel, even in an empty scene.
  • This PR already has five surface_ready checks and "collision surface" text for a state only #3793 creates.

@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch from 4ff5b50 to 4687fd5 Compare October 25, 2026 15:33
@shadygm shadygm changed the title Add collision authoring UI and Walk controls feat(collision): persist collision nodes and add Walk APIs Oct 25, 2026
@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch from 4687fd5 to 9a42100 Compare October 25, 2026 17:08
@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch 2 times, most recently from f66c37a to 2865138 Compare October 25, 2026 18:31
@shadygm

shadygm commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

@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 MrNeRF left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_WALK action, 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 AsyncTaskManager job that MCP can wait on.
  • APIs: Python and MCP can generate and export.
  • Preview: it costs under ~1 ms per frame.
  1. 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_as then 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?
  2. 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 compares gallery_scene_epoch_, which opening a project doesn't change.
  3. KeymapTest.EveryDefaultKeyDispatchesAcrossSavedLayouts fails (test_keymap_layout_contract.cpp:126). In the new Walk tool mode, the global Cancel Polygon binding on Escape dispatches EXIT_WALK on every layout. The default-contract test was updated; this one wasn't. CI doesn't run lichtfeld_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 lf and on lf.scene.

@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch from 2865138 to f9acb03 Compare October 25, 2026 21:08
@shadygm
shadygm force-pushed the feat/collision-authoring-walk branch from f9acb03 to 43f1ddb Compare October 25, 2026 22:11

@MrNeRF MrNeRF left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_WALK key map action, and KeymapTest.EveryDefaultKeyDispatchesAcrossSavedLayouts passes. 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.* and lf.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_collision and the collision.main job 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.hpp belongs with the project chapters: it lives in core but only project_document.cpp uses it, and it hand-rolls little-endian put/get. In io/project it could use chapter_binary_utils.hpp's read_u32/write_u32/read_f32 (about −35 lines).
  • Leftovers:
    • collision_byte_budget and the byte_budget parameters of encodeCollision/decodeCollision only serve tests (project_document.cpp:2104, 2213–2217; collision_serialization.hpp:16–20, 49–50).
    • captureCollisionSources always returns exactly one source (scene.cpp:4348), and the prepare kernel's offset is always 0.
    • The lf aliases 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: captureCollisionSources clones the whole combined model before slicing it, so GPU memory scales with every splat in the scene, not just the target.

@MrNeRF
MrNeRF force-pushed the feat/collision-authoring-walk branch from 4325445 to dbb68dc Compare October 26, 2026 04:25
@MrNeRF
MrNeRF merged commit 7d3c678 into master Oct 26, 2026
An error occurred while trying to automatically change base from feat/voxelize-cli to master October 26, 2026 04:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants