fix: improve splat precision#666
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses Gaussian splat jitter at globe (ECEF) scale by introducing an RTC-style floating-origin approach for SparkJS splat rendering, and updates examples/docs accordingly.
Changes:
- Add a shared, camera-snapped floating-origin controller that recenters splat meshes and shifts the camera passed into Spark to keep float32 shader math numerically stable.
- Expose a new
splat.originCellSizeconfiguration option (shared per transparent scene) and document its tradeoffs. - Update the splat example to use
setCamera/flyToframing and add a “far” sample for dynamic-origin testing.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| web/navara_three/example/pages/splat/run.ts | Adjusts example camera framing and adds per-sample “Fly to” controls and a far-jitter test sample. |
| web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts | Integrates the shared dynamic-origin controller + Spark origin patch; adds originCellSize plumbing. |
| web/navara_three_default_descs/src/meshes/SplatMeshDesc/splatOriginController.ts | Implements the grid-snapped origin and mesh recentering logic. |
| web/navara_three_default_descs/src/meshes/SplatMeshDesc/splatOriginController.test.ts | Adds unit tests for snapping and recenter math/lifecycle. |
| web/navara_three_default_descs/src/meshes/SplatMeshDesc/sparkOriginPatch.ts | Patches SparkRenderer to shift the camera by -origin and invoke the origin updater each frame. |
| docs/src/content/docs/three_default_descs/Mesh Desc/splat-mesh-desc.md | Documents originCellSize and adds guidance about large scale instability. |
| docs/src/content/docs/ja/three_default_descs/Mesh Desc/splat-mesh-desc.md | Japanese documentation for originCellSize + large scale note. |
Comments suppressed due to low confidence (2)
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:80
- originCellSize is documented as “first splat wins” for the shared renderer, but when a renderer already exists this code silently ignores the requested cellSize. This can be very confusing when multiple splats are created with different originCellSize values.
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:249 - The dynamic-origin controller stores an “original ECEF matrix” at onCreate(), but if the descriptor’s spatial config (matrixWorld/position/rotation/scale) is later updated, the controller will keep using the stale stored matrix when it recenters on cell changes. That can revert/ignore transform updates.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:80
- When an existing shared SparkRenderer is reused,
opts.cellSize(fromsplat.originCellSize) is ignored because the controller is already created. Since this is now part of the public config surface, consider warning when a differentoriginCellSizeis requested but cannot take effect (similar to the existinglodmismatch warning) so users can diagnose why their setting is ignored.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (4)
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:180
splat.originCellSizeis passed straight intoSplatOriginControllerascellSizewithout validation. If a caller provides0, a negative number, or a non-finite value,update()will produce NaNs/Infinities and recentering will break. Please validate and fall back to the default (and warn) before constructing the controller.
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:101originCellSizeis documented as “first splat wins” because the SparkRenderer is shared per transparent scene, but the code currently has no way to detect/report mismatches when later splats request a different cell size. Storing the chosen cell size inrenderer.userDataenables mismatch warnings in the existing-renderer path.
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:80- When a shared SparkRenderer already exists, a different
originCellSizerequested by later splats is silently ignored (since the controller is already constructed). This can be very confusing given the neworiginCellSizeoption; please warn similarly to the existinglodmismatch warning.
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:239 - The controller stores a single “original ECEF matrix” per mesh at creation time, but
MeshDescsupports runtime updates tomatrixWorld/position/rotation/scale. After such an update, the next origin snap will recenter from the stale matrix and effectively undo the update. The controller’s stored matrix needs to be refreshed when spatial config changes occur.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:65
acquireSparkRendereracceptscellSize, but when a shared renderer already exists the incomingcellSizeis silently ignored (the first-created controller’s cell size effectively wins). SinceoriginCellSizeis a per-mesh config field, consider warning when a later splat requests a different cell size for the same transparent scene, similar to the existinglodmismatch warning, to avoid confusing no-op configuration.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (3)
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:215
mesh.matrixWorldmay not yet reflect the initialposition/rotation/scale(e.g. when placing a splat via the documentedpositionAPI). Cloning it here can register an identity/stale ECEF matrix, causing incorrect recentering once the RTC origin updates.
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:271- RTC recentering disables
matrixAutoUpdate/matrixWorldAutoUpdateinSplatOriginController, so subsequenthandle.update({ position/rotation/scale })calls (the documented usage) will not updatemesh.matrixWorldunless it is explicitly recomputed and re-registered. Additionally, the current guard(this.matrixWorld || this.matrix)prevents refreshing the cached ECEF matrix for position-only splats, so they can snap back to the original placement on the next origin change.
web/navara_three_default_descs/src/meshes/SplatMeshDesc/index.ts:80 - When a shared SparkRenderer already exists,
opts.cellSize(fromsplat.originCellSize) is silently ignored even though the docs state the first splat's value wins. This can surprise callers who set differentoriginCellSizevalues on later splats; consider warning on mismatch (similar to the existing LoD warning) so the behavior is explicit.
Overview
Fixed the precision issue by applying RTC method.
What I've done
What I haven't done
How I tested
Which point I want you to review particularly
Screenshots
Before
Screen.Recording.2026-07-03.at.17.36.36.mov
After
Screen.Recording.2026-07-03.at.17.34.38.mov
Checklist