Skip to content

Building an open world in the browser, part 5: Budgeting the pretty stuff

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.

This was the chapter where visual ambition met arithmetic.

We split rendering cost into separate spikes because bundled results are hard to diagnose. If you turn everything on at once, you only learn that the frame is slow. You don't learn which feature ate the budget.

Spike 7 targeted vegetation density and animation cost. The approach was runtime scattering from 32x32 density maps per terrain chunk, feeding large InstancedMesh sets. Each grass blade and shrub cluster got vertex-shader wind driven by a scrolling noise texture. The key number we watched wasn't triangle count but draw call overhead and vertex throughput on mid-range GPUs. We found that batching instances into fewer meshes mattered more than reducing per-blade polygon count.

Open Spike 7 in a new tab ↗ · View source

Spike 8 pushed terrain material complexity. Multi-layer blending weighted by slope angle and altitude, optional triplanar projection for cliff faces, and per-layer normal maps. The shader was doing slope-based splatting with four texture layers, each needing a diffuse and normal sample. That's eight texture fetches per fragment before you add any lighting. We profiled on integrated Intel GPUs specifically to find the floor. The takeaway was that triplanar projection on vertical surfaces was worth the cost, but adding a fifth splat layer wasn't.

Open Spike 8 in a new tab ↗ · View source

Spike 9 focused on cascaded shadow map cost under realistic terrain and object load. CSM with three cascades was baseline. We tested with low-angle sun positions specifically because that's where cascade pressure gets worst. The far cascade covers a huge frustum slice, and shadow map resolution per texel drops fast. We measured the GPU time difference between two and four cascades, then between 1024 and 2048 shadow map resolution. The result was that three cascades at 1024 gave us acceptable contact shadows near the camera without exceeding 2ms of GPU time on our target hardware.

Open Spike 9 in a new tab ↗ · View source

The hard part in this phase was product discipline. Some effects looked excellent and still had to be constrained because they consumed too much of the frame budget relative to their visual impact.

Our rule became simple. A feature moves forward only if it can explain its cost with measured frame-time data.

That sounds obvious. It's not common in fast prototype cycles where everyone is excited about the next visual win. Keeping this rule early made architecture decisions around clipmaps and volumetric zones much cleaner later, because we already knew the per-feature cost of everything competing for the same 16ms.

In part 6 we hit the first major terrain architecture pivot with geometry clipmaps.

Technology referenced in this chapter

InstancedMesh and GPU vegetation. Three.js's InstancedMesh renders N copies of the same geometry with one draw call. For vegetation, a density map (32x32 per chunk) drives runtime scattering of grass blades and shrub clusters into instance buffers. Wind animation runs in the vertex shader using a scrolling noise texture. At scale, WebGPU's ComputeInstanceCulling eliminates off-screen and distant instances before rasterization, and IndirectBatchedMesh packs multiple vegetation types into a single buffer drawn with multi-draw indirect. See our landscape guide on GPU vegetation culling.

Triplanar mapping. Standard UV-mapped textures stretch on steep slopes because UV coordinates compress. Triplanar mapping projects textures along all three axes (X, Y, Z) and blends based on the surface normal. Cliff faces get the X or Z projection (no stretching), flat ground gets the Y projection. The blending is smooth and automatic with no UV unwrapping required. For PBR terrain, the same blending weights apply to albedo, normal, roughness, and ambient occlusion channels. See triplanar mapping details.

Slope and altitude-based material splatting. Instead of hand-painted splat maps, materials are assigned procedurally in the fragment shader based on terrain properties. Flat ground at low altitude gets grass, steep slopes get rock, high altitude gets snow (only on surfaces flat enough for accumulation), near sea level gets sand. The transitions use smoothstep for smooth blending. In our implementation, each terrain chunk evaluates four texture layers with diffuse and normal samples per layer, resulting in eight texture fetches per fragment before lighting. See slope and altitude material assignment.

Cascaded Shadow Maps (CSM). CSM splits the camera's view frustum into 3-4 distance ranges (cascades). Each cascade renders a shadow map from the sun's perspective at a resolution matched to its distance. Close cascades get high-resolution shadows (detailed contact shadows under trees and buildings), far cascades get lower resolution (broad mountain shadows). The terrain shader samples all cascades and selects the appropriate one per fragment. Performance cost: 3-4 cascades at 1024x1024 add ~0.5-1 ms for shadow map rendering plus ~0.2-0.3 ms for sampling. See shadows for terrain.


Part 5 of 12.
Previous: Part 4 - Streaming before fancy terrain
Next: Part 6 - Clipmaps changed the plot
Series guide: /blog/2026-02-25-open-world-browser-series-guide