Skip to content

World: large-coordinate support — camera-relative rendering / floating-origin to kill far-from-origin jitter #429

Closed

Description

@drsnuggles8

Problem

All transforms are single-precision glm::vec3 (Scene/Components.h TransformComponent). There is no double-precision path and no floating-origin / origin-rebasing. Far from the origin you get vertex jitter, shadow swim, Z-fighting on distant geometry, and physics instability.

Why it matters (Tier 1)

A Far Cry 2-scale (~50 km²) map visibly degrades toward the edges. This is hard to retrofit late, so the strategy should be chosen early.

Proposed approach

  • Camera-relative rendering (recommended for OpenGL/PC): keep world positions in f32 but subtract the camera origin before upload so the GPU always works near 0. Cheapest, no ECS change to f64. Usually combined with:
  • Floating origin / origin rebasing: periodically re-base the world (shift all entities + physics bodies) when the player moves far from origin. Integrates naturally with the existing Scene/Streaming/SceneStreamer region model (rebase on region transition).
  • Double-precision (dvec3) transforms are the heavyweight fallback — likely unnecessary for PC/FC scale if camera-relative + rebasing is done well.

Acceptance criteria

  • No visible vertex jitter or shadow swim at the far edge of a 50 km² test map.
  • Physics stable far from origin.
  • Streaming + rebasing interoperate without hitches.

Tier 1 — architectural. From the open-world FPS gap analysis.

Score

capability: 8
craft: 5
stability: 5
decay: 5
effort: 8
confidence: 0.8
learning: 3
fun: 3
kano: table-stakes
blocked_by: []
blocks: []

Rated per issue-scoring · score = confidence × (capability + craft + stability + decay) / effort, derived by the picker.

Activity

  1. coderabbitai commented on Jun 28, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #400 - fix(net,task): isfinite-guard untrusted float ingestion (network snap… [merged]


    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. drsnuggles8 commented on Jul 7, 2026

    @drsnuggles8
    OwnerAuthor

    First slice shipped: camera-relative rendering

    Landed the camera-relative rendering technique from the proposed approach (branch feature/camera-relative-rendering). Positions stay f32 world-space in the ECS — nothing about TransformComponent or the data format changes. Only the upload path changes: a per-frame render origin is subtracted from every world position/matrix before it reaches the GPU, so the GPU always works with coordinates near 0 and the catastrophic cancellation that causes vertex jitter / shadow swim far from origin disappears.

    This issue stays open — it's the umbrella for large-coordinate support. The second independent technique, floating-origin / origin rebasing (periodically re-basing the world as the player travels far, integrating with SceneStreamer), is a separate follow-up slice and is not done here. Double-precision (dvec3) transforms remain explicitly out of scope.

    Design

    • Render origin = camera position snapped to a coarse grid (1024 units). Two payoffs: (1) within the first grid cell the origin is exactly (0,0,0), so the whole feature is a byte-identical no-op near origin — every existing scene and the full test suite are unaffected; (2) the origin only changes on cell crossings, so relative coordinates are frame-stable (no per-frame micro-jitter, CSM texel snapping stays aligned).
    • One small tested abstraction (Renderer/CameraRelative.h): ComputeRenderOrigin + MakeModelRelative / MakeViewRelative / MakeViewProjectionRelative / MakePositionRelative.
    • Lighting/fog/shadow math is unchanged because it's all differences (camPos − worldPos, lightPos − worldPos) — subtracting a common origin from both operands is invariant.

    Sites migrated (the full audit list + the done/remaining split lives in docs/agent-rules/camera-relative-rendering.md)

    • Camera UBO — every upload path (primary, the shared UploadCameraUBO, the terrain/voxel inline uploads now routed through it, and the planar-reflection mirror camera). InverseViewProjection inverts the relative VP.
    • Per-object transforms — shifted at the InstanceBuffer choke points, covering main/depth/quad/terrain/voxel/decal-cube meshes, the instanced loop, all shadow-caster paths, and the GPU frustum culler + legacy occlusion-culler proxies.
    • Shadows — CSM/spot/point sampling matrices + point positions (ShadowMap::UploadUBO) and the matching render-pass matrices + casters, shifted by the same origin so sampled and rendered depth stay consistent.
    • Lights — MultiLightUBO + the Forward+ point/spot/sphere-area SSBOs.
    • Difference-based peripherals — decals, light-probe volumes, fog volumes, underwater fog. Motion vectors preserved (prev-VP made relative to the current origin, matching prev-model).

    Verification

    • Contract tests (CameraRelativeTest.cpp) pin the grid-snap, the NDC-invariance identity, fine-detail preservation at 45 km (the naive world path corrupts nearby-vertex separation; the relative path preserves it), and motion-vector consistency.
    • Visual before/after (CameraRelativeVisualEvidenceTest.cpp): the same sphere-on-plane-with-shadow scene at 45 km renders identically to the near-origin ground truth with the fix ON (RMSE 2.7) and visibly degraded with it OFF (RMSE 6.9). PNGs inspected directly.
    • Full suite 4204/4204 pass (no near-origin regression). Real terrain+foliage editor scene renders clean at origin, log error-free.
    • Debug/bisect lever: Renderer3D::SetCameraRelativeEnabled(false) reverts to exact pre-change behaviour.

    Remaining (follow-up slices, documented in the agent-rules doc)

    • Absolute-world pattern shaders — terrain triplanar tiling / brush / snow-clipmap, water wave-phase, procedural noise, foliage wind. Their geometry already renders stable, but sampling an absolute world position for a pattern (not a difference) offsets far from origin; the fix is a u_RenderOrigin uniform + an + origin add-back per shader. Many are auto-saved by the 1024-multiple snap (e.g. the infinite grid renders clean far from origin already).
    • 2D sprites (Renderer2D) — detail is lost at CPU bake time (transform * local at 45 km), so it needs its own snapped origin applied to the VP + baked vertices.
    • Floating-origin rebasing — the umbrella's other half.
  3. drsnuggles8 commented on Jul 7, 2026

    @drsnuggles8
    OwnerAuthor

    Follow-up: absolute-world pattern shaders now covered too

    Extended this slice to close the "category-2" gap flagged in the first comment — the shaders that use an absolute world position for a pattern (not a lighting difference), which the plain origin-shift would offset far from origin.

    Mechanism

    Added u_RenderOrigin as the trailing member of the shared CameraMatrices UBO (binding 0, now 288 B — append-only, so shaders that stop earlier are unaffected; the cross-shader std140 layout check still passes). Most pattern shaders declare their own inline camera block, so each got the field added.

    Two coordinate spaces (the clean split)

    • Geometry pattern shaders receive a render-relative interpolated v_WorldPos, so they rebuild absWorldPos = v_WorldPos + u_RenderOrigin for the pattern (never interpolate the absolute position — a 45 km varying re-introduces jitter). Migrated: terrain (5 shaders — triplanar tiling, snow clipmap, brush, snow height), water (Gerstner phase / FFT / foam / normal-map / noise across VS/TCS/TES/FS), foliage (world-anchored wind), infinite grid (world-origin axes + lines).
    • Depth-reconstruction passes (fog, decals, underwater caustics, motion blur) instead invert the world view-projection: since inverse(VP_world) · (VP_rel · worldPos_rel) == worldPos_absolute, they reconstruct absolute world for free — no per-fragment add-back, and their world-space UBO inputs (WorldToLocal, decal inverse-transform) stay world-space. The only add-back is lifting the relative camera-UBO position for difference terms (volumetric fog).

    The full enumerated audit + the "which space is this consumer in" rule are in docs/agent-rules/camera-relative-rendering.md.

    Verification

    Still open (documented as follow-ups; #429 stays open)

    • Terrain geometry far from origin — the terrain quadtree LOD tessellation factors (CPU-side, TerrainUBO::TessFactors, from camera↔patch distance) lose precision at ~45 km, so terrain patches stop drawing. This is independent of the now-fixed terrain pattern shaders and of camera-relative rendering — a separate large-coordinate slice for the terrain LOD system. (Foliage / sphere / plane / grid all draw fine at 45 km; terrain is the outlier.)
    • 2D sprites (Renderer2D) — a clean coordinate shift (not a pattern shader): the CPU-baked world vertices lose detail at bake time, so it needs its own snapped origin on the VP + baked vertices.
    • Floating-origin / origin rebasing — the umbrella's other half.
  4. drsnuggles8 commented on Jul 7, 2026

    @drsnuggles8
    OwnerAuthor

    Camera-relative rendering shipped in #587 (first slice of this umbrella).

    Every world-space GPU upload is now rendered relative to a per-frame render origin (camera position snapped to a 1024 grid), so the GPU works near 0 far from the world origin — vertex jitter and shadow swim are gone. World positions stay f32 in the ECS; the shift is upload-only, and within the first grid cell the origin is exactly (0,0,0) so it's a byte-identical no-op near origin.

    Covers the full audit: core matrices/camera/transforms, shadows, multi-light + Forward+ SSBOs, GPU frustum/occlusion cull, light probes; the absolute-world pattern shaders (terrain triplanar/snow, water Gerstner/FFT/foam, foliage wind, infinite grid); and the depth-reconstruction passes (decals, volumetric fog, underwater, motion blur, deferred lighting). A /code-review pass on the initial slice caught and fixed several far-origin-only regressions (deferred lighting space mismatch, foliage identity-model displacement, occlusion-cull VP mismatch, shadow-pass origin, fog light-shaft shadows). Full suite 4206 pass; near-origin render byte-identical. Design + audit list in docs/agent-rules/camera-relative-rendering.md.

    Keeping this issue OPEN — remaining large-coordinate work:

    • Terrain geometry far from origin — the terrain quadtree LOD (TessFactors, CPU camera↔patch distance) loses precision at ~45 km so patches degenerate/cull; independent of the now-fixed terrain pattern shaders. (Foliage / sphere / plane / grid all render fine at 45 km.)
    • 2D sprites (Renderer2D) — done in feat(renderer2d): camera-relative CPU-bake coordinate shift for 2D sprites (#429) #601 (branch feature/large-coord-2d-sprites-429). Unlike the GPU-upload sites above, Renderer2D bakes world vertices on the CPU at Draw* time, so detail was lost at bake time. Fix: Renderer2D keeps its own grid-snapped render origin (per BeginScene, from the camera world position), bakes every vertex relative to it (MakeModelRelative for the transform-matrix Draw*, MakePositionRelative for the explicit-position line/polygon/quad-vertex paths) and uploads a relative VP. No shader change (pure coordinate shift). Own debug lever Renderer2D::SetCameraRelativeEnabled. Pinned by Renderer2DCameraRelativeTest (CPU) + Renderer2DCameraRelativeVisualEvidenceTest (far-origin ON reproduces the origin frame pixel-for-pixel, RMSE 0.000; OFF degrades, RMSE 15.3). UIRenderer / ParticleRenderer inherit the shift for free (they draw through Renderer2D).
    • Floating-origin / origin rebasing — re-base the world as the player travels far, integrating with SceneStreamer.
    • (nice-to-have) dedicated far-origin deferred-lighting visual-evidence test; minor non-blocking cleanup noted in the PR (alloc churn at origin, redundant RenderOrigin storage, shared camera-block duplication across inline shader blocks).
  5. added 2 commits that reference this issue on Jul 9, 2026
  6. drsnuggles8 commented on Jul 9, 2026

    @drsnuggles8
    OwnerAuthor

    Advanced by #587 (merged 2026-07-09); worktree removed. This delivered the first camera-relative rendering slice (per-frame grid-snapped render origin; families A/B/B′ pattern + depth-reconstruction shaders shifted). Umbrella stays open — remaining follow-ups: terrain geometry precision far from origin (quadtree LOD), 2D sprites (Renderer2D) coordinate shift, and floating-origin / origin rebasing.

  7. drsnuggles8 commented on Jul 9, 2026

    @drsnuggles8
    OwnerAuthor

    Appears resolved by #587 (merged 2026-07-07): camera-relative rendering + absolute-world pattern shaders. The core floating-origin/camera-relative path shipped there (OloEngine/src/OloEngine/Renderer/CameraRelative.h now exists) plus the two-phase GPU-cull transform shift + unified CommandDispatch render-origin source in commit f7584a7. Flagging for closure — verify the far-from-origin jitter is gone in the editor if any residual scope remains.

  8. drsnuggles8 commented on Jul 9, 2026

    @drsnuggles8
    OwnerAuthor

    Correction to my earlier comment: this should stay open. #587 shipped only the camera-relative render path (3D geometry uploaded relative to the camera origin). The umbrella still has open follow-ups:

    • Terrain geometry precision far from origin (quadtree LOD).
    • 2D sprite (Renderer2D) coordinate shift.
    • Floating-origin / origin rebasing (periodic world re-base incl. physics bodies, interoperating with SceneStreamer region transitions) — plus the acceptance criteria "physics stable far from origin" and "streaming + rebasing interoperate without hitches."

    So #587 closes the first technique, not the issue. Disregard the "flagging for closure" line above.

  9. added a commit that references this issue on Jul 9, 2026
  10. drsnuggles8 commented on Jul 9, 2026

    @drsnuggles8
    OwnerAuthor

    Advanced by #601 (merged 2026-07-09); worktree removed. This delivered the 2D sprites (Renderer2D) coordinate shift follow-up: per-BeginScene grid-snapped render origin, relative camera UBO + CPU-baked vertices (no shader change), debug lever Renderer2D::SetCameraRelativeEnabled(false), L1 + L8 visual-evidence tests (far_on == near_ref at RMSE 0.000). Umbrella stays open — remaining follow-ups: terrain geometry precision far from origin (quadtree LOD) and floating-origin / origin rebasing.

  11. drsnuggles8 commented on Jul 10, 2026

    @drsnuggles8
    OwnerAuthor

    Advanced by #605 (feature/terrain-camera-relative-429). This delivers the terrain geometry LOD / cull far from origin follow-up.

    Root cause (two bugs at once). The terrain quadtree keeps node bounds in terrain-local coordinates (minX·worldSize, no entity transform baked in) and the entity transform places them in the world at draw time — but LOD selection and chunk frustum culling were fed the world camera / view-projection / frustum. Far from origin that failed two ways: (1) it ignored the terrain's world transform, testing the world camera against local bounds, and (2) the world camera↔node subtraction cancelled catastrophically in f32. Symptom: patches degenerating to the coarsest LOD or frustum-culling entirely at ~45 km (TerrainGenerationEvidenceTest even documents the caveat: "a translated transform would cull the terrain out of view"). Note this is not a family-A/B shader issue — the rendered vertices were already correct (terrain transform goes through UploadModelInstance's relative shift; the pattern shaders add u_RenderOrigin back). Only the CPU LOD/cull inputs were in the wrong space.

    Fix. Run terrain LOD/cull in terrain-local space, evaluated through the grid-snapped render origin so the large-coordinate subtraction stays precise:

    • Renderer/CameraRelative.h: new MakeObjectLocalViewProjection / MakeObjectLocalCameraPos helpers (compose the existing relative-VP / relative-model helpers so the origin cancels analytically, not numerically).
    • Scene.cpp (ProcessScene3DSharedLogic, runtime and editor path): a per-terrain-entity MakeTerrainLocalCullInputs feeds both SelectVisibleChunks (tessellated quadtree) and GetVisibleChunks (non-tess cull). Same IsCameraRelativeEnabled() lever as the rest of World: large-coordinate support — camera-relative rendering / floating-origin to kill far-from-origin jitter #429; a byte-identical no-op near origin. Also corrects a latent bug for any off-origin terrain (e.g. ScriptedTerrainDemo's terrain at x=±150), not just at 45 km.

    Tests (55 terrain + camera-relative tests green). TerrainCameraRelativeLODTest.cpp (L1, CPU): local camera/VP reproduce the near-origin reference at 45 km; the quadtree screen-space-error decision is reproduced (same LOD) while the naive world eval collapses it; GPU-gated end-to-end TerrainQuadtree::SelectLOD picks the identical node set near vs far (diverges under the old naive path). TerrainCameraRelativeVisualEvidenceTest.cpp (L8): real tessellated editor pipeline near-origin vs +45 km — far terrain renders identically to origin (RMSE ~5 vs a culled ≈40+), PNGs inspected.

    Umbrella stays OPEN. Remaining follow-ups: terrain streaming far from origin (TerrainStreamer builds tiles in tile-local coords but renders with the entity transform only, and never calls SelectVisibleChunks per tile — entangled with the streaming/floating-origin work) and floating-origin / origin rebasing (re-base the world as the player travels far, SceneStreamer interop).

  12. added a commit that references this issue on Jul 10, 2026
  13. drsnuggles8 commented on Jul 10, 2026

    @drsnuggles8
    OwnerAuthor

    Floating-origin / origin-rebasing — first slice shipped in #612

    The third proposed approach on this umbrella (floating-origin / rebasing) now has a first shippable mechanism. The other two-thirds — camera-relative rendering — were already merged (#587 3D, #601 2D sprites, #605 terrain LOD/cull).

    Done (#612)

    • Scene::RebaseOrigin(shift) shifts the whole world back toward the origin: root entity transforms (children telescope), Jolt rigid bodies + static terrain + character controllers (JoltScene::ShiftOrigin, velocities/rotations/sleep preserved), and Box2D bodies — atomically on the game thread, then re-propagates world matrices and accumulates the offset (absolute = rebased + GetWorldOrigin()).
    • Scene::MaybeRebaseOrigin — grid-snapped auto-trigger in UpdateStreaming (physics idle, never races the parallel gameplay scheduler), gated by opt-in WorldOriginSettings (default off → every existing scene byte-identical).
    • Composes with camera-relative rendering as an orthogonal layer: rebasing moves stored state, camera-relative is a fixed per-frame GPU transform on top; after a rebase the render origin snaps back to (0,0,0).
    • Verified: unit contract math, Functional physics (velocity preserved across a rebase; resting body stays on a shifted static floor), and an L8 visual-evidence test proving no visible pop across a 45 km→origin rebase (before/after frames RMSE ~1.3).

    Remaining (tracked in #613)

    • Rebase the world-space state left outside the first slice: cloth soft-bodies, vehicle/constraint fixed-world anchors, NavMesh, TerrainStreamer per-tile WorldOrigin.
    • Full 50 km² end-to-end scale validation (no jitter/shadow-swim, physics stable across many rebases, streaming interoperates) + auto-trigger tuning under real traversal.

    Leaving this umbrella open until #613 lands.

  14. added 2 commits that reference this issue on Jul 10, 2026
  15. drsnuggles8 commented on Jul 18, 2026

    @drsnuggles8
    OwnerAuthor

    Umbrella complete. The three-part strategy from this issue's proposed approach shipped:

    1. Camera-relative rendering — shipped earlier (OloEngine/src/OloEngine/Renderer/CameraRelative.h): world positions stay f32 but the camera origin is subtracted before GPU upload, so the GPU always works near 0.
    2. Floating origin / origin rebasing — Scene::RebaseOrigin / WorldOriginSettings shipped in feat(scene): floating-origin / origin-rebasing (#429) #612 (first slice: root transforms, Jolt rigid bodies + static terrain + character controllers, Box2D), completed in Floating-origin: rebase cloth/constraints/navmesh/terrain-streamer + 50 km² scale validation (#613) #663 (cloth, world-anchored constraints, navmesh, terrain-streamer + 50 km² scale validation, closing Floating-origin: rebase cloth/vehicle/navmesh/terrain-streamer + 50 km² scale validation (#429 follow-up) #613).
    3. Double precision (dvec3) — deliberately not pursued; camera-relative + rebasing cover PC/Far-Cry scale without an ECS-wide f64 migration, exactly as this issue anticipated.

    Acceptance criteria met:

    • No visible vertex jitter / shadow swim at the far edge of a 50 km² map — camera-relative rendering + rebasing; the visual-evidence test shows RMSE 1.6 before/after a rebase at 50 km (no visible pop).
    • Physics stable far from origin — rebasing keeps stored coordinates small; Functional physics tests pass across single and many sequential rebases (velocity/rest/anchor continuity).
    • Streaming + rebasing interoperate without hitches — the terrain streamer is now entity-relative (rebase-free), and RebaseOrigin runs in UpdateStreaming on the game thread with physics idle, before the streamer update.

    Closing as completed (tracked continuation #613 also closed; delivery in #612 + #663).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requestperformancePerformance / data-oriented optimization

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions