Skip to content

Building an open world in the browser, part 14: The world comes alive

By Oleg Sidorkin, CTO and Co-Founder of Cinevva

New here? Use the series guide. It explains what a spike is and links all parts.

After part 13 we had sculpting. Raise terrain, carve caves, smooth cliffs, all in real time, seams intact. But the world looked like a tech demo. Debug-colored flat shading, wireframe overlays, grey geometry with LOD colors to tell chunks apart. You could edit it. You couldn't feel it.

Three spikes changed that. Not the architecture. The exact same pipeline, buffers, and seam stitching from Part 13. We just added the layers that make terrain feel like a place: surfaces that respond to the shape, life growing on those surfaces, and a body that walks on them.

The difference between "technically working" and "I want to stay here" turned out to be surprisingly small.

Sculpt a cliff and watch it turn to rock

Spike 28 asked a narrow question: can a 4-layer material with triplanar mapping run on compute-generated terrain without killing frame budget? The answer was yes, but the interesting part was what happened next.

Open Spike 28 in a new tab ↗ · View source

Four procedural textures (grass, rock, sand, snow) generated from FBM noise at startup. No external files, no asset pipeline, just math and a DataTexture. The material weights come from the surface itself: slope and altitude. Flat areas below the treeline get grass. Steep faces get rock. Low ground gets sand. High peaks get snow. Weights are normalized per-fragment so they always sum to 1. Triplanar mapping handles UV projection for MC chunks where triangles have no meaningful UV coordinates.

The moment that sold it: raise the terrain with the brush to create a steep cliff, and rock texture appears on the new face in the same frame. Flatten it back down, and grass reclaims the surface. The material doesn't know about the brush. It just reads world position and surface normal, the same data the geometry was built from. The sculpting-to-visual feedback loop is instant and unscripted.

Spike 8 tested terrain material cost a month and a half ago with a static mesh on WebGL. Spike 28 proves it works on dynamic compute-generated terrain with the full sculpting pipeline underneath. We'd budgeted for this, but seeing it hold at 60fps with everything running was still a relief.

80,000 grass clumps and a cave ceiling

Spike 29 is where the gamedev instincts took over.

Open Spike 29 in a new tab ↗ · View source

We wanted Breath of the Wild grass. Not the poly count, the feel. Grass that covers the right places, moves with the wind, and makes you want to run through it.

Each grass clump is three intersecting quads at 60-degree angles with four vertical segments for bending. It's a cross shape that looks volumetric from any angle without billboard tricks. One InstancedMesh per chunk, 80,000 clumps across the world, roughly 2.4 million grass vertices total.

Placement is the part that matters. The CPU walks a jittered grid across each chunk and evaluates the same slope/altitude logic the GPU material shader uses. Where the material system says "grass," grass grows. Where rock or sand dominates, density drops to zero. The falloff is smooth because the underlying smoothstep weights produce continuous gradients at biome edges. You don't notice a boundary because there isn't one.

Then we did something I wasn't sure would work. Grass on SDF surfaces. The scatter function walks each column of the SDF volume and finds zero-crossings between adjacent voxels. Where the SDF crosses from negative to positive, there's a surface. Estimate the slope from the SDF gradient at that point. If it's gentle enough, place grass.

Carve a cave with the SDF brush in Spike 27. Come back in Spike 29 and there's grass growing on top of the cave ceiling. The scatter code doesn't know what a cave is. It just sees a surface with the right slope at the right altitude. That's the kind of emergent behavior that makes open-world systems satisfying to build.

Wind is a sine wave in the TSL vertex shader, modulated by the blade's V coordinate so tips sway and roots stay planted. Toggle V to cycle between gentle, strong, and off. The wind runs on all 80K clumps simultaneously with zero CPU cost because it's entirely in the vertex shader.

Spike 7 tested 50K grass instances and we were nervous about hitting the limit. Spike 29 runs 80K on top of compute terrain, seam stitching, multi-material texturing, and brushes. Batching into fewer InstancedMesh draws still matters more than reducing per-blade vertex count. The lesson from Spike 7 held up.

One thing we punted: grass doesn't update when you sculpt under it. The instance matrices are set at scatter time. Sculpt a hill into a valley and the grass floats in midair until you toggle G to re-scatter. Good enough for a spike. Production will need dirty-chunk re-scatter.

The first footstep

Spike 30 is the one I keep going back to.

Open Spike 30 in a new tab ↗ · View source

No physics library. Custom capsule at 120Hz fixed timestep. The production codebase uses Rapier in a web worker (Spike 2 proved the latency is fine), but this spike needed to prove the collision queries themselves. Can a character walk across heightmap terrain, step onto sculpted SDF terrain, and not fall through?

The core is a function called terrainQuery(x, y, z). It checks whether the position falls inside an MC-locked chunk. If so, it trilinearly interpolates the CPU SDF mirror and returns the gradient as a surface normal. Otherwise, heightmap lookup with central-difference normal. The character doesn't know which terrain system it's standing on. It just asks for the ground and gets an answer.

SDF collision was the part I expected to be hard. Seven probe points around the capsule (bottom, center, top, four cardinal offsets). Wherever the SDF value is less than the capsule radius, the gradient gives the push-out direction. Velocity is projected to cancel the component going into the surface. It's not a physics engine. It's geometry queries and simple response. But it handles caves, overhangs, and sculpted tunnels with zero special-case code per shape. You walk into a cave you carved ten seconds ago and the capsule follows the ceiling contour exactly.

The movement model started practical and got fun. Walk, run, sprint, jump, slope sliding above 45 degrees. Then I added a BotW paraglider and the spike turned into something I didn't want to close.

Press Space while airborne. Gravity drops from -30 to -4. Fall speed caps at -3. A delta-wing mesh unfolds from the capsule with scale interpolation. Bank left and the wing tilts. The capsule's facing auto-aligns toward the velocity vector so you're always looking where you're going. Let go of Space to drop. Hit the ground and you're running again.

The camera pulls back into third-person (toggle P). It follows behind the player with yaw smoothing. A ray-march from player to camera tests terrain at 20 steps. Fly into a cave and the camera arm shortens smoothly instead of clipping through rock. Emerge on the other side and it extends back out.

Here's the moment that made the spike worth it: sculpt a tall cliff with the heightmap brush. Switch to the third-person camera. Run to the edge. Jump. Deploy the glider. Bank left over the terrain you just sculpted, with the grass you grew in Spike 29 waving below, the rock texture from Spike 28 on the cliff face, Transvoxel seams holding at every chunk boundary. Land on the other side. Everything working together in one browser tab.

Sculpting under your own feet

One thing I wasn't sure about: what happens when you sculpt the terrain the character is standing on? The heightmap changes propagate through terrainQuery() instantly because it reads the CPU buffer directly. SDF changes propagate through the CPU SDF mirror. The capsule resolves penetration on the next physics tick, which at 120Hz is within 8ms.

It just works. Raise the ground under the player and they rise with it. Carve the ground away and they fall. No special handling. The physics runs fast enough that single-frame terrain changes never produce large penetrations. This was a happy accident of choosing 120Hz for the timestep. We picked it for smooth movement, and it made terrain editing safe for free.

30 spikes later

We started this series with a flat heightmap and 500 cubes. Now we have sculpted terrain with volumetric caves, Transvoxel seam stitching, multi-material texturing that responds to the surface shape, 80,000 grass clumps swaying in the wind, and a character that walks, sprints, jumps, and glides across all of it.

None of this is in production yet. The world/client/ codebase still runs WebGL with simple heightmap chunks. Everything from these 30 spikes lives in standalone HTML pages. The integration work is next: migrating to WebGPURenderer, wiring the hybrid HM/MC policy into the chunk manager, connecting the brush and material systems to multiplayer.

But the rendering system isn't the risk anymore. The open questions are about data flow: edit persistence, network sync of brush strokes, collaborative sculpting. The stuff that happens between players, not between triangles.

If you've followed this series from Part 1, thanks for sticking with the messy parts. If you just found this, go back to Part 1. The wrong turns are where the lessons live.

Technology referenced in this chapter

TSL (Three Shading Language). Three.js's node-based shader system for the WebGPU renderer. Materials are composed from nodes (positionWorld, normalWorld, smoothstep, triplanarTexture) using function composition in JavaScript. The shader graph compiles to WGSL at runtime. TSL replaces raw GLSL ShaderMaterial for WebGPU targets and provides interop between standard Three.js material features (lights, shadows, fog) and custom per-fragment logic.

Triplanar mapping. A texture projection technique that samples a texture three times (XY, XZ, YZ planes) and blends based on surface normal direction. This eliminates UV stretching on arbitrary mesh geometry, which is critical for marching cubes output where triangles have no meaningful UV coordinates. TSL provides triplanarTexture() as a built-in node.

Instanced vegetation with cross-quad blades. Each grass clump is 3 intersecting quads at 60-degree angles, creating a volumetric appearance from any view direction. 4 vertical segments per quad allow smooth bending for wind animation. The entire field is rendered as a single InstancedMesh per chunk. Capacity is over-allocated by 25% so new instances can fill reserved slots when terrain is edited without reallocating the GPU buffer. See our landscape generation guide on vegetation.

Capsule vs SDF collision. Character collision against volumetric terrain without a physics engine. The capsule is probed at multiple points against the SDF. Where the field value is less than the capsule radius, the gradient provides the outward normal and the difference gives the penetration depth. This handles caves, overhangs, and tunnels with no shape-specific code. See SDF terrain collisions.

Fixed-timestep character controller. Physics steps at 120Hz regardless of frame rate, accumulating real time and consuming it in fixed-size steps. A max substep count prevents spiral-of-death on slow frames. Ground snapping keeps the capsule attached during slope traversal. The fixed timestep ensures deterministic behavior for future multiplayer replay.


Part 14 of 14. Previous: Part 13 - Terrain sculpting and the death of the math function Series guide: /blog/2026-02-25-open-world-browser-series-guide