Disclaimer: This is a vibe-coding project. I used Claude Code for the vast majority of the implementation (especially the wgpu rendering pipeline and noise-based terrain generation). My primary focus was exploring the architecture around chunk management, spatial indexing, and meshing. đ
Live Demo ¡ GitHub Repository
The Software Stack
With WebGPU maturing and WebAssembly integration in Rust becoming remarkably smooth, I wanted to see how far browser-based 3D engines could be pushed:
Blocks and Chunks
At the core of any voxel engine lies a hierarchy: individual voxels aggregate into manageable spatial volumes called Chunks.
A chunk is represented as a contiguous 3D array of Blocks. Storing millions of empty air voxels naively would quickly exhaust memory, especially within browser constraints. To mitigate this, chunks consisting solely of air are treated as uniform singletons and avoid individual heap allocations:
| |
View Distance & The Single-Thread Bottleneck
In a traditional native engine, chunk generation and meshing are dispatched to background thread pools (e.g., via rayon). In a standard browser WASM environment, however, we are bound to the main UI and animation thread.
If a frame takes longer than 16.6 ms we fall under the holy 60 FPS mark. Because generating 3D Perlin/Simplex noise and extracting surface polygons (meshing) is computationally heavy, we cannot generate all chunks within the player’s view distance in a single frame.
Instead, we need two things:
- Prioritization: Chunks closest to the player must generate first.
- Time-slicing (Amortization): Bound the number of chunks and meshes generated per frame cycle.
1. Precomputed Spherical Offsets
Calculating Euclidean distances on every frame for dozens of surrounding chunk coordinates is wasteful. Since the relative geometric layout never changes, we can precompute the offset coordinates relative to $(0, 0, 0)$ once at startup and sort them spherically:
| |
2. The Frame Update Loop
In each render tick, the engine slides its active chunk window based on the player’s movement, and then consumes a strictly defined compute budget of chunk and mesh builds:
| |
Chunk Management: 3D Toroidal Buffering
Rather than reallocating an entire 3D grid whenever the player crosses a chunk boundary, the engine uses a 3D Toroidal Ring Buffer (Sliding Window).
Because coordinates wrap around via modulo operations across all three axes, moving through the world requires zero data reallocations. When the player moves along an axis (say, $+X$), only the 2D plane on the opposite, trailing boundary ($-X$) is invalidated and marked for regeneration with the newly entered terrain.
| |
Prioritized Chunk & Mesh Generation
Generating a chunk is a two-step pipeline:
- Voxel Population: The 3D density noise generates raw voxel data.
- Mesh Extraction: Once a chunk and all its direct neighbors are populated, faces obscured by adjacent solid blocks are culled, and GPU vertex buffers are created and uploaded.
Notice how whenever a non-empty chunk finishes loading, it flags its immediate neighbors (unset_mesh()): a border chunk cannot accurately cull its outer faces until adjacent blocks are known!
| |
What’s Next?
While the engine runs at a solid 60 FPS under normal navigation, there are several exciting paths for future improvements:
- Greedy Meshing: Combining adjacent coplanar quad faces into single larger quads to drastically reduce vertex and index buffer sizes.
- Web Workers via SharedArrayBuffer: Offloading noise sampling to background Web Workers using Rust’s
wasm-bindgen-rayonto make chunk generation truly asynchronous without stuttering the render thread. - Persistent Browser Storage: Currently, all edits vanish on page refresh. The browser provides powerful mechanisms to persist modified worlds:
- OPFS (Origin Private File System): A modern, sandboxed filesystem API that allows fast, synchronous binary I/O from within Web Workers. It would enable Minecraft-style binary region files (
.mca) running purely on the client. - IndexedDB: An alternative key-value store to save compressed diffs of player-modified chunks without touching the pristine procedurally generated baseline.
- OPFS (Origin Private File System): A modern, sandboxed filesystem API that allows fast, synchronous binary I/O from within Web Workers. It would enable Minecraft-style binary region files (
- Floating Origin (Camera-Relative Rendering): While chunk coordinates can logically span a 32-bit integer space (billions of blocks), GPU rendering relies on 32-bit floating-point numbers (
f32). At extreme coordinate distances, IEEE 754 floats lose precision, manifesting as severe vertex jitter, camera stutter, and depth-fighting artifacts (the classic “Far Lands” effect).
An architectural fix is a floating origin: instead of translating the camera through an ever-expanding global space, the camera is pinned statically at $(0, 0, 0)$. Chunks maintain their true logical world coordinates for generation and storage, but are indexed and pushed to the GPU transformed into local camera space.
