tront.xyz / floatingorigin
A 1996 flight-sim trick, ported to a JavaScript / Three.js sandbox built like an infinite procedural IKEA. The engine quietly teleports the world back toward (0, 0, 0) as you traverse; you stay forever in the small, precise center of float-space.
Every 3D engine you'll touch stores positions as 32-bit floats. They get roughly 7 digits of precision. That's plenty when your coordinates fit in a phone booth; it breaks when your coordinates fit a small country.
X = 50,000, the smallest distinguishable step is around 0.004 units. Your character animations look slightly twitchy; vertices on distant geometry have started shimmering.X = 1,000,000, the step is 0.06 units. That's bigger than a footstep. The camera shakes; physics rattles; collision normals flicker; light probes pop.X = 16,777,216 (2¹⁴, the largest integer a float can still represent exactly) you've lost integer precision.This is the wall every open-world game eventually runs into. Unreal solved it with a "world origin rebasing" system in 4.4; Star Citizen went full 64-bit; Minecraft straight-up freezes if you push past the Far Lands. Indie open-worlds usually pretend they'll never hit it and then they do.
You lie to the engine. Keep the player physically near (0, 0, 0) at all times. When they drift past a threshold (say 300 units), teleport them back to zero, and shift the entire world the same amount in the same direction. From inside the engine, nothing changed: same relative positions, same view, same physics. But your camera coordinates stayed small and precise.
From the player's POV the world looks seamless; nothing snaps, nothing reloads. The only thing that moved is a counter you keep in software.
You'll need both, and you'll need to know which one any given value lives in.
The bridge is a running tally of every shift you've ever applied; call it globalOffset. The player's absolute world position is always engineCamera + globalOffset. JavaScript makes this easy because every Number is already a double; in C# you use double explicitly.
Per frame, after you've applied input to engineCam and worked out absPos = engineCam + globalOffset, recenter only if the engine camera has drifted past the threshold:
pseudocodeif length(engineCam.xz) > THRESHOLD:
shift = (engineCam.x, 0, engineCam.z)
globalOffset += shift
engineCam -= shift
for chunk in chunks: chunk.position -= shift
Order matters: derive absPos from the still-drifted camera before you recenter, or you'll lose track of which chunk you're standing on. Place a chunk back into engine space by subtracting the offset (engineX = chunkWorldX - globalOffset.x), and hash the chunk's world coords (never the engine coords) with a fixed seed to make the procgen deterministic across machines.
Press H inside the demo for the full pseudocode walkthrough with setup, per-frame, chunk placement, and deterministic generation, in tabbed form.
Big enough that you don't shift every frame, small enough that floats stay precise.
300–500. Tighter because GLSL highp on phones is often only mediump in practice.1000–2000 is comfortable; precision is fine up to roughly 10,000 before you start to see it.A common first instinct is to parent every chunk to one big worldRoot transform and translate that root instead of the player. It seems cleaner: one transform to move, no per-chunk bookkeeping. It also breaks for the exact reason as before.
Float precision lives at the final composed transform, not just at the player. Once your worldRoot sits at (-4,000,000, 0, 0), child translations like 0.001 round down to zero. The whole world starts twitching on chunk boundaries the same way the player would have.
You have to shift each chunk individually (or shift a small pool of nearby roots that themselves don't drift far). That's what the demo does, that's what real implementations do.
(0, 0, 0). As you walk, it stays put, because it does. When the floating-origin event fires, the pillar appears to teleport toward you, but actually you teleported to it. Everything else shifts to compensate, so the world looks seamless. Hold SHIFT to traverse fast and trigger shifts in seconds.(0, 0, 0); when you drift past the threshold ring, watch the world snap toward you as the shift counter ticks. WASD + mouse, SHIFT for 14x boost, SPACE / CTRL for altitude, H for the in-demo tabbed reference.Opens in a new tab. Pointer-lock + the HUD panels (telemetry, threshold ring, chunk minimap, controls) want the whole viewport; iframing them down to 16:9 crowds the layout.
FloatingOrigin.csAttach to a manager GameObject. player is whatever you consider the center of the world (usually the main camera root or character controller). Threshold of 1000 is a typical Unity choice; the float-stable range is generous before precision visibly suffers.
C#using UnityEngine;
using UnityEngine.SceneManagement;
public class FloatingOrigin : MonoBehaviour
{
public Transform player;
public float threshold = 1000f;
// Doubles survive way past Unity float range.
public double absoluteX, absoluteZ;
// Cache these at chunk load; FindObjectsOfType in LateUpdate would tank frame time.
[SerializeField] Rigidbody[] trackedBodies;
[SerializeField] ParticleSystem[] trackedParticles;
void LateUpdate()
{
Vector3 p = player.position;
if (p.x * p.x + p.z * p.z < threshold * threshold) return;
Vector3 shift = new Vector3(p.x, 0f, p.z);
absoluteX += shift.x;
absoluteZ += shift.z;
// 1. Recenter every root transform.
foreach (GameObject root in SceneManager.GetActiveScene().GetRootGameObjects())
root.transform.position -= shift;
// 2. Nudge active rigidbodies so interpolation doesn't snap.
foreach (Rigidbody rb in trackedBodies)
if (rb && !rb.isKinematic) rb.position -= shift;
// 3. Translate every live particle in world-simulated systems.
foreach (ParticleSystem ps in trackedParticles)
{
if (!ps || ps.main.simulationSpace != ParticleSystemSimulationSpace.World) continue;
var buf = new ParticleSystem.Particle[ps.particleCount];
int n = ps.GetParticles(buf);
for (int i = 0; i < n; i++) buf[i].position -= shift;
ps.SetParticles(buf, n);
}
}
}
C#// Anywhere in your code:
Vector3 engine = player.position;
double worldX = origin.absoluteX + engine.x;
double worldZ = origin.absoluteZ + engine.z;
// Feed worldX/worldZ to chunk generation, save files, network sync, etc.
int chunkX = (int)System.Math.Floor(worldX / CHUNK_SIZE);
int chunkZ = (int)System.Math.Floor(worldZ / CHUNK_SIZE);
The 30-line implementation above gets the camera + static geometry working. Real games have a lot more state living in world space that also has to shift, or it'll snap and pop and break your suspension of disbelief.
Set position directly on shift. Don't use MovePosition; that interpolates over the next physics step and you'll see a one-frame snap. For active simulations, also kill any cached predicted positions in your physics layer.
Only world-simulated particles need translation. Local-space particles move with their transform automatically. The above code does the right check; if you wire it up by hand, mind the simulationSpace.
Their internal points live in world space and won't move with the transform. Either Clear() them on shift (acceptable for short-lived trails like bullets), or iterate GetPositions(), offset every vertex, and SetPositions() back (necessary for long persistent trails like skid marks).
3D positional audio on transforms shifts cleanly with the rest of the scene. For streaming sources tied to the player (footsteps, suit comms, music), parent them to the player so they never drift to begin with.
Bake per chunk with NavMeshSurface. A monolithic baked navmesh tied to world coordinates won't survive shifts; it'll either crash the agent or send them through walls.
Each client maintains its own engine-space origin. Never sync engine-space positions over the wire. Sync the absolute world position (a double pair), then convert to local engine space on receive. If you sync engine-space, two players in the same room can disagree about who's at (50, 0, 0) by 800,000 units.
FindObjectsOfType in LateUpdate would tank your frame time the moment you have more than a few hundred Rigidbodies. Cache the rigidbodies and particle systems when chunks spawn; drop them when chunks despawn. The shift itself is cheap once the lists are pre-built.The floating-origin trick goes back at least to FlightGear in the late 90s; flight sims hit the precision wall before anyone else because their playable area was the actual surface of the Earth. Unity's manual still calls it "floating origin"; Unreal ships a built-in "World Origin Rebasing" since 4.4. The Three.js / IKEA implementation behind the demo is mine.
H for the in-demo modal.If you ship something built on this, drop a note in the Discord.