Building an open world in the browser, part 8: Integration without losing our baseline
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.
Integration is where projects get messy. You have working pieces in isolation. You connect them and suddenly every bug looks like it could be anywhere.
Spikes 13 and 14 were our answer to that trap. Spike 13 established a clean Three.js WebGPU baseline. Just a renderer, a scene, a camera, and a simple mesh. No terrain, no compute, no effects. We confirmed that Three.js's WebGPU backend initialized correctly, that the render loop was stable, and that TSL (Three.js Shading Language) node materials worked as expected. Only after that checkpoint passed did we start adding layers.
Open Spike 13 in a new tab ↗ · View source
Spike 14 was incremental hardening. We added one capability at a time: first camera controls, then lighting, then the compute-generated mesh from the marching cubes pipeline, then buffer plumbing to feed GPU output directly into Three.js geometry attributes. After each addition, we verified that the previous layer still behaved correctly.
Open Spike 14 in a new tab ↗ · View source
That sounds slow. It was slow for exactly one day, and it saved us multiple days soon after when seam logic and policy switching got complicated.
The specific category of bug that justified this discipline was hairline artifacts. Thin slivers that looked like geometry corruption but were actually stale data. The compute shader would write N vertices into a buffer, but the draw call would still be configured to render N+M vertices from the previous frame. Those extra vertices contained garbage from the old dispatch. The visual result was flickering razor-thin triangles that appeared and vanished unpredictably.
You don't beat that class of bug with intuition. You beat it with controlled deltas where you know exactly what changed between the last working state and the current broken state.
The WebGPU integration also taught us about buffer lifecycle. GPU buffers in WebGPU are immutable once mapped for a specific usage. If you need to resize a vertex buffer because the marching cubes output grew, you have to create a new buffer and update the binding. There's no realloc. Getting that lifecycle right, destroying old buffers without racing against in-flight GPU work, required explicit fence management that doesn't exist in WebGL.
In part 9 we move into Transvoxel seam work. That chapter starts with a scaffold on purpose. By this point we had fully internalized the lesson that rushing integration produces mysteries, and controlled setup produces debuggable problems.
Technology referenced in this chapter
WebGPU. The successor to WebGL, providing low-level GPU access in the browser with compute shaders and indirect rendering. WebGPU's two critical features for open worlds: compute shaders enable GPU-side terrain generation, foliage placement, and culling; indirect rendering lets the GPU decide what to draw based on compute output, eliminating CPU bottlenecks in dense scenes. Available in Chrome, Edge, and Firefox on desktop. See WebGPU as a performance unlock.
Three.js Shading Language (TSL). Three.js's node-based shader system that replaces raw GLSL/WGSL with composable JavaScript expressions. TSL nodes like texture(), positionWorld, smoothstep(), and fog() build a shader graph at runtime that compiles to the appropriate backend (WebGL GLSL or WebGPU WGSL). TSL makes it possible to write material logic once and target both renderers. The node graph is evaluated per-frame, so dynamic uniforms and conditional branching work naturally.
GPU buffer lifecycle in WebGPU. WebGPU buffers are created with specific usage flags (VERTEX, STORAGE, COPY_DST, etc.) and can't be resized after creation. If a marching cubes dispatch produces more vertices than the buffer can hold, you must create a new buffer, update the binding, and destroy the old one. Destroying a buffer that's still referenced by an in-flight GPU command causes errors. Explicit fence management (via device.queue.onSubmittedWorkDone()) ensures the old buffer isn't destroyed until the GPU finishes using it. This lifecycle discipline doesn't exist in WebGL, where the driver manages memory implicitly.
Incremental hardening. A process discipline for integration: establish a known-good baseline, add one capability at a time, and verify the previous layer still works after each addition. This approach is slower for one day and saves days during later debugging because each regression can be traced to a specific, controlled change. The baseline-then-increment pattern is common in AAA open world development where systems are integrated in a specific order to manage risk.
Part 8 of 12.
Previous: Part 7 - Marching cubes and the first real caves
Next: Part 9 - Transvoxel started with a scaffold
Series guide: /blog/2026-02-25-open-world-browser-series-guide