Skip to content

feat(collision): add generator export dialogs and visible voxel preview - #3793

Merged
MrNeRF merged 1 commit into
feat/collision-authoring-walkfrom
feat/collision-surfaces-export
Oct 26, 2026
Merged

MrNeRF merged 1 commit into
feat/collision-authoring-walkfrom
feat/collision-surfaces-export

Conversation

@shadygm

@shadygm shadygm commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

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.

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.

🟡 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

Comment thread src/visualizer/gui/collision_export_panel.cpp Outdated
Comment thread src/core/collision.cpp Outdated
Comment thread src/visualizer/gui/collision_panel.cpp Outdated
@shadygm
shadygm force-pushed the feat/collision-surfaces-export branch 2 times, most recently from e9bd2a5 to c9429b6 Compare October 25, 2026 00:38
@shadygm
shadygm force-pushed the feat/collision-surfaces-export branch from c9429b6 to 4c0c3cb Compare October 25, 2026 10:43
@shadygm
shadygm force-pushed the feat/collision-surfaces-export branch from 4c0c3cb to 63387e4 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 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.

  1. 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.
  2. "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.
  3. 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).
  4. 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.
  5. 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_export only opens the panel.

@shadygm
shadygm force-pushed the feat/collision-surfaces-export branch from 63387e4 to d44a10c Compare October 25, 2026 15:33
@shadygm shadygm changed the title Add collision surface generation and export UI feat(collision): add voxel preview and Rendering controls Oct 25, 2026
@shadygm
shadygm force-pushed the feat/collision-surfaces-export branch from d44a10c to 6e2ba8b Compare October 25, 2026 17:08
@shadygm shadygm changed the title feat(collision): add voxel preview and Rendering controls feat(collision): add generator export dialogs and visible voxel preview Oct 25, 2026
@shadygm
shadygm force-pushed the feat/collision-surfaces-export branch from 6e2ba8b to 30a5ce4 Compare October 25, 2026 17:47
@shadygm
shadygm force-pushed the feat/collision-surfaces-export branch from 30a5ce4 to cebb7a4 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 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.

  1. The UI now redraws ten times a second while idle.
    • rendering_panel.py:310 switches the Rendering panel from dirty updates to update_policy = "interval" at 100 ms. The collision popup does the same (panels.py:95), and its on_update always 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?
  2. 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.

Smaller things:

  • The Collision section shows expanded in an empty scene, at the bottom of the Rendering panel.
  • When only the .bin of 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.py 167 lines, the 68-line menu-bar test for one Walk case) and the per-instance state rework in CollisionRendering are where it can shrink.

@shadygm
shadygm force-pushed the feat/collision-surfaces-export branch from cebb7a4 to af6784e Compare October 25, 2026 21:08
@shadygm
shadygm force-pushed the feat/collision-surfaces-export branch from af6784e to 07ba211 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.

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:434 shows "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–145 checks RML markup and that keys exist in 10 locales, which check_locale_completeness already 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.md narrates internals (pipeline reuse, failure logging, rollback). About 20 lines would cover what users need.
  • Structure:
    • CollisionRendering keeps export_mode, export_target and popup_handle as class-level statics, which act as globals.
    • save_json_file_dialog returns a string or a tuple depending on a flag.

@MrNeRF
MrNeRF force-pushed the feat/collision-surfaces-export branch from d969286 to e651e89 Compare October 26, 2026 04:33
@MrNeRF
MrNeRF merged commit 83247a5 into master Oct 26, 2026
5 of 7 checks passed
An error occurred while trying to automatically change base from feat/collision-authoring-walk to master October 26, 2026 04:33
@MrNeRF
MrNeRF deleted the feat/collision-surfaces-export branch October 27, 2026 16:44
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