Repository navigation
feat(collision): add generator export dialogs and visible voxel preview - #3793
Conversation
fa7d02f to
a9203cd
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved export concurrency, sparse-padding handling, and saved-shape inconsistencies can produce incorrect geometry.
3 open issues
What changed in this PR
Adds optional triangle surfaces and export controls on top of the shared collision engine and authoring/Walk UI. Surface coarsening leaves full-resolution Walk occupancy unchanged.
Changes:
- Adds Faces/Smooth surface extraction, coplanar simplification, normals, and independent surface nodes.
- Adds asynchronous SuperSplat voxel and GLB export with progress, cancellation, and overwrite confirmation.
- Extends regression coverage, documentation, and UI keys across ten locales.
| File | Description |
|---|---|
| tests/test_voxelize_cli.cpp | Tests surface normals and cancellation. |
| tests/test_collision.cpp | Tests topology, seams, simplification, and coarsening. |
| tests/test_collision_scene.cpp | Extends archive coverage to extracted surfaces. |
| tests/test_collision_export.cpp | Tests GLB transforms, winding, cancellation, and paths. |
| tests/python/test_file_menu_recent.py | Updates expected export menu entries. |
| src/visualizer/gui/utils/splat_export_request.hpp | Declares collision export requests. |
| src/visualizer/gui/utils/splat_export_request.cpp | Stores and consumes export requests. |
| src/visualizer/gui/rmlui/resources/collision.rml | Adds surface generation controls. |
| src/visualizer/gui/rmlui/resources/collision_export.theme.rcss | Defines export panel theme colors. |
| src/visualizer/gui/rmlui/resources/collision_export.rml | Defines export actions and progress UI. |
| src/visualizer/gui/rmlui/resources/collision_export.rcss | Styles the export panel. |
| src/visualizer/gui/rmlui/elements/scene_graph_element.cpp | Adds context-menu export access. |
| src/visualizer/gui/resources/locales/zh.json | Adds English fallback surface/export strings. |
| src/visualizer/gui/resources/locales/pl.json | Adds English fallback surface/export strings. |
| src/visualizer/gui/resources/locales/nl.json | Adds English fallback surface/export strings. |
| src/visualizer/gui/resources/locales/ko.json | Adds English fallback surface/export strings. |
| src/visualizer/gui/resources/locales/ja.json | Adds English fallback surface/export strings. |
| src/visualizer/gui/resources/locales/it.json | Adds English fallback surface/export strings. |
| src/visualizer/gui/resources/locales/fr.json | Adds English fallback surface/export strings. |
| src/visualizer/gui/resources/locales/es.json | Adds English fallback surface/export strings. |
| src/visualizer/gui/resources/locales/en.json | Adds surface/export labels and help. |
| src/visualizer/gui/resources/locales/de.json | Adds English fallback surface/export strings. |
| src/visualizer/gui/gui_manager.cpp | Registers the floating export panel. |
| src/visualizer/gui/collision_panel.hpp | Declares surface jobs and result state. |
| src/visualizer/gui/collision_panel.cpp | Builds and adopts independent surface nodes. |
| src/visualizer/gui/collision_job_status.hpp | Adds coarsening advice for memory failures. |
| src/visualizer/gui/collision_export_panel.hpp | Declares asynchronous export panel state. |
| src/visualizer/gui/collision_export_panel.cpp | Implements export selection, confirmation, and jobs. |
| src/visualizer/CMakeLists.txt | Includes the export panel in the build. |
| src/python/stubs/lichtfeld/ui/__init__.pyi | Documents the collision export binding. |
| src/python/lfs/py_ui_theme.cpp | Exposes export opening to Python. |
| src/python/lfs_plugins/file_menu.py | Adds the collision export operator. |
| src/io/include/io/collision_export.hpp | Declares GLB surface export. |
| src/io/collision_export.cpp | Writes transformed GLB geometry transactionally. |
| src/core/include/core/collision.hpp | Exposes meshing and coarsening APIs. |
| src/core/include/core/collision_source.hpp | Declares native mesh construction. |
| src/core/collision.cpp | Implements extraction, simplification, and coarsening. |
| src/core/collision_source.cpp | Constructs mesh tensors and vertex normals. |
| docs/docs/features/collision.md | Documents surfaces, exports, and limits. |
Review effort: Balanced
e9bd2a5 to
c9429b6
Compare
c9429b6 to
4c0c3cb
Compare
4c0c3cb to
63387e4
Compare
MrNeRF
left a comment
There was a problem hiding this comment.
Built it and used it:
- Surfaces: they build fast. On a 5M-splat scene, Smooth 4× gives 545k triangles in 0.28 s, watertight with consistent winding; Faces 1× gives 7.4M triangles in 1.6 s.
- Export: the voxel pair is 2.3 MB, Unicode paths work, and the GLB bakes the node transform exactly.
Same size note as on #3789. 1.6k more lines for surfaces and export is a lot; the whole feature should land at around 2–3k lines.
- Every GLB is written in PlayCanvas coordinates (collision_export.cpp:268 bakes a 180° rotation about Z), behind a generic "Export surface (.glb)" button. Importing the GLB back into LichtFeld Studio puts it in the wrong place relative to the splats.
- "Export…" shows up on every mesh node.
- The condition is
node->collision || MESH(scene_graph_element.cpp:3125). A plain imported mesh with no collision gets the item, and it opens "Export collision" with GLB enabled. - File > Export > Collision… accepts plain meshes too.
- That makes it a hidden general-purpose mesh exporter.
- The condition is
- Overwriting asks twice. The file dialog already asks to replace the file. Then the panel asks again with "For SuperSplat, both the JSON and binary companion will be replaced", also for a .glb (collision_export_panel.cpp:122–133).
- The export panel can export the wrong node.
- The target is only read in
prepare()(:77). - Repro: select a surface, open File > Export > Collision… 0.3 s later, click GLB 0.8 s after that. It exported the previous node (same md5, 132.8 MB) under the new name.
- The panel never shows which node it will export.
- The target is only read in
- Duplication.
- The GLB writer is hand-rolled next to the existing GLB container code in io/formats/spz.cpp.
- The occupancy is serialized again for every node that carries it. One surface doubles the scene-graph chapter, from 0.6 to 1.2 MiB on a 137k scene.
Smaller things:
- "Export for SuperSplat" and the "SuperSplat Studio → Assets → Collision" upload help are product instructions in the locales.
- A Faces 1× surface on a 5M scene takes 334 MB of VRAM and doubles memory use while building. It's worth a warning, or a default other than 1×.
- Export has no Python or MCP path;
lf.ui.open_collision_exportonly opens the panel.
63387e4 to
d44a10c
Compare
d44a10c to
6e2ba8b
Compare
6e2ba8b to
30a5ce4
Compare
30a5ce4 to
cebb7a4
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 and retested. The GLB path is gone, and Export now only shows for colliders. The overwrite prompt appears once, and the export target is latched and shown in the popup, so my repro now exports the right node.
- The UI now redraws ten times a second while idle.
rendering_panel.py:310switches the Rendering panel from dirty updates toupdate_policy = "interval"at 100 ms. The collision popup does the same (panels.py:95), and itson_updatealways returns True.- Idle on the default Rendering tab: 49 presents in 5 s and 47% of a core. On the Training tab: 0 presents and 0.5%. With the popup open: 53%.
- Can these go back to dirty-driven updates?
- The Walk fields in Rendering > Collision overwrite the real settings.
- The panel keeps its own defaults (
collision_rendering.py:17,:60) and never reads the controller's settings. - Repro: set Walk to head height 0.5 from Python, then edit Step height in the panel. The stale 1.7 is committed, and the eye jumps from 0.55 to 1.75 while walking.
- Could the panel read the current settings? That also needs a way to read them back, which Python can't do yet.
- The panel keeps its own defaults (
Smaller things:
- The Collision section shows expanded in an empty scene, at the bottom of the Rendering panel.
- When only the
.binof a pair exists, the prompt shows the name and two paths with no explanation. - Size: the stack is ~4.8k lines; the target for the whole feature is ~2–3k. Here, the tests (
test_collision_controls.py167 lines, the 68-line menu-bar test for one Walk case) and the per-instance state rework inCollisionRenderingare where it can shrink.
cebb7a4 to
af6784e
Compare
af6784e to
07ba211
Compare
MrNeRF
left a comment
There was a problem hiding this comment.
The round-2 points are fixed:
- Idle redraw: the app idles at 0 frames and 0.4% CPU with a collider in the Rendering tab or the collision popup open. In round 2 it was ~10 presents/s at ~50% CPU.
- Walk settings: setting head height 0.5 from Python shows 0.5 in the panel, and editing Step height to 0.2 keeps the other values.
- Empty scene: the Collision section is hidden.
- Tests: visualizer 329/329, and the stack's Python tests pass.
One thing I'd like changed first: a fine collider makes the preview expensive.
Measured with the splat hidden, the camera moving every frame, a 1240×950 viewport and an RTX 4080:
| Collider | Frame interval | GPU use |
|---|---|---|
| none | baseline | 9% |
| 0.05 | +0.1 ms | |
| 0.0125 (7.08M records) | +11–12 ms | 26–32% |
| 0.005 | +14 ms | 29% |
These are frame intervals on Xvfb, not GPU timestamps, but GPU use triples with them. A collider is visible at 40% right after generation, so most users will pay this.
- The octree goes through
createStagingBuffer(VMA_MEMORY_USAGE_AUTO_PREFER_HOST) and is read as a storage buffer (vulkan_mesh_pass.cpp:2601, 2088). - The fragment shader walks it from the root at every step.
- I didn't prove it, but host-visible memory is the likely cause. A device-local copy is the first thing I'd try, and a GPU timestamp before and after would show it.
Smaller:
- Untranslated text:
scrub_fields.py:434shows "Expected a finite number" untranslated.- The stages "Collision ready", "Publishing voxel metadata" and "Preparing collision" aren't in the stage map, so they show the generic Running label.
- Formatting: clang-format 21 wants changes in tests/collision_fixture.hpp:18 and in the include order of test_native_save_dialog.cpp and test_orthographic_zoom.cpp.
- Tests:
test_collision_controls.py:120–145checks RML markup and that keys exist in 10 locales, whichcheck_locale_completenessalready covers.- The collision draw-item test sits in test_orthographic_zoom.cpp and the Walk dispatch test in test_menu_bar_title.cpp. Their own files would be easier to find.
- Docs:
features/collision.mdnarrates internals (pipeline reuse, failure logging, rollback). About 20 lines would cover what users need. - Structure:
CollisionRenderingkeepsexport_mode,export_targetandpopup_handleas class-level statics, which act as globals.save_json_file_dialogreturns a string or a tuple depending on a flag.
07ba211 to
d969286
Compare
d969286 to
e651e89
Compare


Tools > Collision, directly below Splat Simplify, opens a small standard floating generator for one selected loaded SPLAT. Localized tooltips explain cell size, Auto, occupancy threshold and cleanup/area filtering. Auto hides the manual value, shows the actual resolved size after preparation and invalidates stale results. Rendering > Collision contains only preview opacity and live Walk settings; both camera toolbars retain Walk with loaded-collider eligibility.
File > Export > Collision and collider context-menu export share a captured-target popup with bottom Export/Cancel actions. Export opens the established LFS save-dialog API, including its native backend. Picker cancellation writes nothing; a BIN-only existing pair gets one localized replacement question with both paths. Progress, errors and active-job cancellation remain observable. Panels use dirty/event-driven updates, and edited Walk fields merge against fresh canonical controller settings.
Every visible collision node renders independently of selection using cached immutable occupancy. The independently implemented SuperSplat-style overlay finds the first occupied boundary, or an orange empty-cell exit when the camera is inside solid space. Gray faces have axis shading and stronger dark edges. Default 40% face/80% edge opacity matches the upstream appearance; 0% is invisible and 100% fully opaque. Pipeline construction reuses the existing wireframe setup while retaining separate collision overlay depth/blending and unchanged mesh behavior. Perspective, orthographic and panorama transforms remain supported; lazy preview failure does not disable other rendering.
Runtime validation on the rebuilt tree: selected/unselected matched-camera captures are pixel-identical; 0/.4/1 opacity captures differ as expected; the settled open popup presents zero frames over five idle seconds. An actual GUI step edit preserved external height/radius/speed settings. Existing large-project validation saved/reopened 12 shared colliders as one COLV payload and contained a failed save without stopping rendering. Independent math/format, archive/lifecycle, native dispatch and Python control regressions are retained with shared fixtures rather than duplicate scaffolding. The exact top-head full Release build passed; 326 focused visualizer tests passed with one CUDA-dependent skip, alongside 7 CPU/Walk, 5 CUDA, 7 CLI/export, 7 file-dialog and 2 lifecycle tests. Clean generated stubs match committed files; the user's workspace stubs and voxel outputs were not used or changed.
The complete stack adds exactly 5,500 physical text lines against master
8ba8e91416ed59d1a6497a393e1725dfa849e92d: 3,655 production/tooling, 1,379 tests, 300 locales, 76 generated stubs, 59 docs and 31 build lines. The walking PNG is 704 bytes. No optional surfaces/GLB or processing subsystem has been restored. Full Python validation after the upstream STBN fix: 3,989 passed, 50 skipped and 482 deselected. I/O discipline, CUDA logging and error-debt gates pass with the existing baseline unchanged. New platform CI remains authoritative for Windows.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.