TL;DR - This post and demo show how to do truly massive infinite terrain in Three.js without the usual LOD stitching nightmare. The terrain is a pure math function evaluated per-vertex on the GPU and per-sample on the CPU. Geometry is one bike-following fan whose vertex density is the LOD. No chunks, no workers, no seams, no popping. Precision stays clean at any distance (demo spawns at 1 AU). Single HTML file.
After I posted Deathchase 3D, a couple of friends told me the same thing:
“Dude. This would be incredible with actual undulating terrain.”
They weren’t wrong. But one of my favourite tricks in Deathchase 3D was the flat disc whose UV matrix was rewritten every frame so the world appeared to rotate around a stationary bike. That worked because the ground was flat. The moment you want real hills, you inherit every classic infinite-terrain problem.
So I started Eternal-Ride (live on GitHub, CodePen, source on GitHub). The demo spawns you at 1 AU (149,597,870,700 m) from origin to prove a point: that you can drive in any direction for as long as you want, with no seams, no popping, no warm-up.
The whole thing is built around one idea I think is criminally underused.
The classic LOD problem (and why I didn’t want to solve it)
The standard recipe (chunks, workers, distance-based resolution, seam stitching) is notoriously fiddly. Seams appear because you’re gluing together two different sampled versions of the same continuous surface.
The idea: mathematically-defined terrain.
The terrain is a math function, h = f(x, z). That’s it. No heightmap, no chunks, no cache.
function sampleTerrain(x, z) {
return A.amp * periodicPerlin(x * A.invScale, z * A.invScale, A.period)
+ B.amp * periodicPerlin(x * B.invScale, z * B.invScale, B.period);
}
The exact same function runs on the GPU (per vertex) and on the CPU (for collisions, bullets, etc.). They never disagree.
One bike-following fan (built-in LOD)
Instead of chunks, the visible world is one fan-shaped wedge centered on the bike:
- Polar layout (rings × sectors)
- Power curve makes near geometry dense, distant geometry sparse → natural LOD
- Constant angular density so every triangle subtends roughly the same screen angle regardless of aspect ratio
- Fan center offset ~1 m behind the bike to hide the inner pole
The fan is parented to a group that rotates and translates with the bike; same trick as Deathchase 3D, but now over a heightfield.
Why the GPU doesn’t tear at 1 AU
This is the part I’m proudest of, and the reason the demo starts at 1 AU.
A naive noise shader falls apart at large world coordinates. Float32 only has ~7 significant digits, so at x = 1.2e7 it already loses meter-level precision. Adjacent vertices round to the same value, noise returns the same height, and triangles degenerate.
The fix is a noise function that is exactly periodic in input space:
- Wrap the bike’s world origin modulo the period on the CPU in float64 (exact at any distance).
- Pass the small wrapped value to the GPU as a uniform.
- The vertex shader only adds the local fan offset (≤ 300 m), which stays float32-clean.
The periods are chosen so the tiling is invisible (P × scale ≫ fan radius). JS and GLSL implementations are kept bit-for-bit identical, so collisions and rendering always agree perfectly.
Lighting: don’t reinvent it, patch it
The fan uses a stock MeshStandardMaterial. onBeforeCompile injects exactly two things:
- Vertex displacement at #include <begin_vertex>
- Per-pixel analytical normal at #include <normal_fragment_begin> (critical for distant terrain quality)
A per-vertex normal would smear hills near the horizon into a flat blur. Per-pixel keeps real lighting detail on the slopes.
A nod to Mirko Kunze’s cheapwater
The water is built on Mirko Kunze’s cheapwater – same object-space normal map, same onBeforeCompile trick of summing rotated, time-scrolled copies on top of the geometric normal. I’m a fan, and I’ve used it before.
For infinite terrain a few things had to change:
- World-locked UVs using the same CPU-wrap trick as the terrain, so ripples stay pinned to the world instead of scrolling/rotating with the bike.
- Coarser-per-layer normal overlays (each layer tiles 2ⁱ times less often) to avoid the classic “second water” horizon seam.
- Live sky-tinted envMap — a tiny 16×16 cube painted from the same sky colors the skybox uses, updated only when the sky actually changes.
What you get from this
- Constant memory footprint (one geometry)
- No seams or popping
- Perfect CPU/GPU parity
- Strong performance even on phones
As usual, I kept the hot path allocation-free. The result is excellent frame times across devices.
Why I think this technique is underused
You trade authored heightmaps and non-periodic noise (like simplex). In return you get infinite, seamless, precision-clean terrain with guaranteed sync between rendering and collisions. Perfect for procedural cruising experiences.
The full source is a single HTML file with detailed comments.
Hope this technique sees more use.
