Endless Procedural Terrain in Three.js · Part 3 of 3
THREE.JS TERRAIN LOD
AND TERRAIN SHADER
Endless terrain is only useful if it runs. Part 3 adds LOD rings, seam-hiding skirts, a height and slope terrain shader and worker-built chunks, then compares it with how Super Soldier Battle does it today.
By Shane Brumback · · Final part of 3
Endless Procedural Terrain in Three.js — The Series
- Part 1: infinite chunk streaming (with the Part 1 tutorial: infinite chunks and a WASD character)
- Part 2: three.js terrain heightmap from noise (with the Part 2 tutorial: noise terrain code)
- Part 3 (you are here): terrain LOD and terrain shader — the final part. Get the code in the Three.js terrain LOD tutorial (Part 3 code).
Three.js terrain LOD is what turns an endless world from a tech demo into something a game can afford. This is Part 3, the final part, of my series Endless Procedural Terrain in Three.js. Part 1 made the ground stream forever in chunks. Part 2 shaped it into ridged mountains, valleys and flat arenas. Today I make it fast and give it a proper ground shader, all aimed at my browser shooter Super Soldier Battle.
Why Does Endless Terrain Need LOD?
In Part 2 every chunk had 32 by 32 quads, 2,048 triangles, even a mountain 400 units away. LOD (level of detail) fixes that with rings around the player:
| Ring (chunks from the player) | Quads per side | Triangles per chunk |
|---|---|---|
| 0 to 1 (LOD0) | 32 | 2,048 |
| 2 (LOD1) | 16 | 512 |
| 3 and beyond (LOD2) | 8 | 128 |
Each step down cuts triangles by four. In my test build that doubled the view distance from 4 chunks to 8: 214 chunks loaded instead of Part 2's 63, for about 67,000 terrain triangles with skirts, roughly half of Part 2's 129,000.
How Do You Fix Three.js Terrain LOD Seams?
A full-detail chunk has a vertex every 2 units along its edge. Its coarse neighbour has one every 8. On a slope, the fine edge bends where the coarse edge is straight, and you get thin cracks with sky showing through. Two common fixes:
- Skirts. Every chunk gets a strip of triangles hanging down from its edges, so a crack shows skirt instead of sky. It needs no knowledge of the neighbours.
- Edge stitching. Snap the fine chunk's extra edge vertices onto the coarse edge line. It's cleaner, but chunks must rebuild whenever a neighbour changes LOD.
I use skirts because chunks change LOD constantly as you walk. There's also a quieter seam: lighting. Normals computed per chunk jump at the borders, so I compute them from the height function with one fixed step, and neighbours match exactly.
What Does a Three.js Terrain Shader Add?
A terrain shader decides what the ground is made of at every pixel. Engines often use a splat map, an image storing how much sand, grass or rock goes where. Endless terrain has no image, so I derive the weights from the world:
- Sand near the valley floor, where Part 2 carves the riverbeds.
- Two-tone grass on flat ground, broken up with a little noise.
- Rock wherever the surface tilts past about 30 degrees.
- Snow high up, but only where it's flat enough to settle.
In Three.js, onBeforeCompile injects a few lines into MeshStandardMaterial, so you keep its lighting, shadows and fog.
Why Build Chunks in a Web Worker?
Part 2's noise costs about 21 noise calls per height sample, and smooth normals need four more samples per vertex. That's about 2.7 ms per full-detail chunk in my tests, and a few of those in one frame eat a big slice of a 16 ms frame budget.
Web Workers fix it. The same height code runs in background threads and hands back transferable arrays, so nothing is copied. A chunk keeps its old LOD on screen until the new one arrives, so you never see a hole.
How Do You Keep Physics Right With LOD?
If your character raycasts the visible mesh, LOD changes the ground under its feet. So never read collision from the render mesh. My character samples the full-detail height function directly, whatever LOD is on screen, and the same samples give the slope, which stops it walking straight up cliffs.
How Do You Put Three.js Grass on Terrain?
My endless procedural grass field recycles blades around the player on flat ground. On terrain, each blade that moves to new ground asks the height function how high it is and sits there. Blades on steep rock or valley sand hide, and the patch stays inside the full-detail ring so blades sit on the exact triangles you see.
How Super Soldier Battle Handles This Today
I read the current game code. Here's what it really does:
- No terrain LOD yet. The battlefield is a hand-built island in 16 pieces, each frustum-culled, sharing one material. Ground height comes from raycasts onto a separate invisible collision mesh with a BVH, not the visible pieces.
- LOD where it matters most. The pine forest is instanced with three detail levels and a hysteresis gap so trees don't flicker, and only the nearest level casts shadows. Distant enemy soldiers swap to lighter models past 60 units.
- A slope and height aware ground shader. The terrain shader blends a grass tint into the ground from 80 to 200 units away, only on flat-enough ground above the waterline and never on beaches, so the far field matches the real blades.
- Endless grass that follows you. 180,000 instanced blades live within 200 units of the player. Once you move 6 units, blades outside the circle are recycled, up to 2,000 a frame. Each one raycasts down onto the collision mesh and skips slopes, water, beaches, buildings and craters.
- A performance ladder. When the frame rate drops below 80, it trims clouds and pixel ratio first. Grass and shadows are never switched off.
So the game already uses LOD for props and characters, plus slope-aware grass. It has no terrain LOD, seams or worker-built ground, because the island is a fixed model.
What This Could Mean for Super Soldier Battle
Procedural endless terrain is planned, not shipped. With this series as the base, it could:
- Load new seeded maps with distant mountains at a fraction of the triangle cost.
- Build ground in workers, so the frame rate holds while the world streams in.
- Swap the grass raycast for a direct height lookup when 2,000 blades move in one frame.
Get the Code: The Part 3 Tutorial
The premium Three.js terrain LOD tutorial builds straight on the Part 2 noise terrain code: LOD rings, skirts, the terrain shader, worker chunks and slope-aware physics, with debug keys to see the LOD rings and toggle the skirts. New to the series? Start with the Part 1 chunk tutorial. For a cute break, the rigged capybara walks a grassy meadow.
Frequently Asked Questions
What is terrain LOD in Three.js?
Terrain LOD (level of detail) means far chunks use coarser meshes than near ones. A common setup is rings around the player: full detail in the nearest ring, half the vertices per side in the next, a quarter beyond that. Each step down cuts triangles by four, so you can see much farther for the same cost.
How do you fix seams between terrain LOD levels?
Use skirts or edge stitching. A fine chunk has extra edge vertices that a coarse neighbour doesn't, so tiny cracks open on slopes. A skirt is a strip of triangles hanging down from every chunk edge that fills the gap. Stitching instead snaps the fine edge vertices onto the coarse edge line.
How do you texture terrain by height and slope in Three.js?
Inject a few lines into a MeshStandardMaterial with onBeforeCompile. Pass the world position and world normal to the fragment shader, then blend colors or textures: sand near the valley floor, grass on flat ground, rock where the normal tilts past a threshold, snow on high flat ground. A little noise keeps the bands from looking like stripes.
Should terrain chunks be generated in a Web Worker?
Yes, once the height math gets heavy. Noise terrain can cost thousands of noise calls per chunk, and doing that on the main thread causes hitches while you walk. A worker can return heights and normals as transferable Float32Arrays, and the main thread only copies them into a pooled geometry.
How do you put grass on hilly terrain in Three.js?
Give every grass blade the ground height at its position. With an InstancedMesh, when a blade moves to new ground around the player, sample the same height function the terrain uses and set the blade's matrix there. Skip blades on steep slopes and in sand, and keep the grass inside the full-detail LOD ring.
Does Super Soldier Battle use procedural terrain LOD?
Not yet. The live game uses a hand-built island in 16 frustum-culled pieces with no terrain LOD. It already uses LOD for pine trees and distant soldiers, a slope and height aware ground shader, and 180,000 grass blades that follow the player. Procedural endless terrain is planned.
Play Super Soldier Battle
That wraps up the series. Play Super Soldier Battle free in your browser, run through those 180,000 blades of grass, and picture the same fights on mountains that go on forever.
Building a Three.js game and need terrain, rendering or performance help? I'm available for freelance and contract work: contact me.
Play Super Soldier Battle free →