Wing Mirrors Vibrating 31 km From the Origin
A studio is porting its racing engine to an 80 km × 80 km open world. World-space positions are stored and uploaded as 32-bit floats; the CPU camera transform is computed in double precision, but the view matrix is cast to float before upload. Testers report three symptoms. At the Salt Flats, 31 km from the map origin, a parked car's wing mirrors vibrate and wheel contact points pop by a few millimetres as the camera creeps forward at 0.5 m/s. Shadow-map cascades near that region show acne that vanishes at the origin. Replayed physics traces are bit-identical on the same build. The depth buffer is 24-bit with near 0.3 m and far 8,000 m. The team has one week of engineering time and cannot change the world size, the save-format version, or the network protocol's absolute-coordinate fields in this milestone. A junior engineer proposes converting all vertex positions and matrices to doubles.
1. Quantify the float32 spacing at 31 km and explain why visible vibration appears in rendered geometry even though the physics trace is deterministic.
Read the worked answer
Spacing. 31,000 lies in [2^14, 2^15) = [16,384, 32,768), so float32 spacing is 2^(14−23) = 2^−9 ≈ 1.95 mm. Every world-space vertex can only occupy positions on that ~2 mm grid.
Why it shows up as motion. Physics integrates on the CPU, so its trace never quantises; rendering does. Three amplifiers:
- Derived frames. Normals and tangents are rebuilt from differences of positions. When the true edge is shorter than the quantum, two endpoints round to the same or adjacent grid points and the reconstructed normal swings by a large angle.
- Cancellation in the model-view matrix. A translation near 31,000 minus a camera translation near 31,000 leaves only low-order bits - exactly the quantised ones - so the residual is noisy.
- Shadow projection. With n = 0.3 and f = 8,000, 24-bit fixed depth gives δz ≈ z²(f−n)/(f·n·2^24). At z = 3 km that is ≈ 1.8 m, far coarser than the geometry, which explains the acne.
At 0.5 m/s and 60 Hz the camera advances ~8 mm per frame, roughly four grid steps, so the result pops rather than drifts - matching the "vibration" report.
2. Design a floating-origin remedy under the one-week constraint. Specify the rebase threshold, what must be rebased, and how you avoid a frame hitch.
Read the worked answer
Threshold. Rebase whenever |camera| exceeds 2 km. At 2,048 the spacing is 2^(11−23) = 0.24 mm, roughly an order of magnitude below anything the artists can see and 8× finer than today. Rebase in fixed 2 km slabs so the operation is repeatable and testable.
What must move (subtract the slab offset from every world-space quantity):
- static mesh and instanced-batch transforms; road and spline meshes
- physics bodies, broadphase, and any cached contact points
- world-space particle systems and decals
- navmesh queries and path targets
- audio listener and occlusion sources
- light probes, cascaded shadow centres, reflection probes
- save/network: store the slab index plus a local float offset; the absolute double stays authoritative on the CPU
Avoiding a hitch. A full rebase of a dense level can cost tens of milliseconds. Amortise: rebase at level-stream boundaries or when the camera is inside a tunnel/loading gate; otherwise process objects in batches (e.g. 500 per frame over four frames), rebasing the camera first, so anything not yet rebased is still correct relative to its un-rebased transform.
The alternative - camera-relative rendering, subtracting the double camera origin per frame - avoids moving static data but forces per-frame re-upload of every transform; keep it only for the few dynamic objects that need it.
3. The junior engineer asks why not simply make every position and matrix a double. Give the technical and project-specific reasons, and state what precision policy you would adopt.
Read the worked answer
Technical objections.
- Memory and bandwidth double for vertex buffers, skinned poses, and per-instance data - on a bandwidth-bound title this is a direct frame-time cost, and it does not remove the jitter because shaders still evaluate in fp32 unless you emulate double on the GPU at large cost.
- It does not touch the depth buffer. The 1.8 m shadow/depth resolution at 3 km is a 24-bit fixed-point problem in the projection, not a world-coordinate problem.
- It collides with the milestone's constraints: bumping the save format and the network's absolute-coordinate fields is explicitly out of scope, and a double-based asset pipeline would force both.
Policy I would adopt. Separate the two roles of precision:
- CPU-authoritative state (camera, physics, save, network, rebase bookkeeping) stays in double.
- GPU-facing data stays float32 but is expressed relative to a nearby origin - the floating origin for statics, camera-relative for the handful of dynamic objects.
- Depth is handled separately: raise near to 0.3 (already), add a tighter far pass for distant terrain, and schedule reversed-Z float depth as its own work item.
This keeps the change to transform plumbing and a rebase system, which fits one week, and leaves the depth fix as an independently testable milestone.