Skip to content

About

A Minecraft-inspired voxel world in the browser: infinite procedural terrain, trees, caves, day/night, break and place blocks. Three.js + TypeScript, zero art assets, playable in one click.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

minecraft-clone

An infinite voxel world in your browser tab. No install, no assets, no server — every block, texture, and cloud is computed from one seed.

CI Deploy TypeScript strict License: MIT

▶ Play it now: vincent-p-essy.github.io/minecraft-clone

Gameplay

Exploring the generated voxel terrain

Block interaction with persistent world edits

Captured from the running production build with seed 12345, after moving through the terrain and breaking and placing blocks.

A Minecraft-inspired voxel game built from scratch with Three.js and TypeScript: procedurally generated infinite terrain with forests, deserts, snowy peaks and cave systems, a full day/night cycle, and free block building — with your edits persisted across reloads.

Desktop Touch
Move / jump / sprint WASD · Space · Shift left-thumb joystick (push to edge to sprint)
Swim Space in water (hop out at the surface) jump button in water
Look mouse (pointer lock — click to start) drag the right half of the screen
Break / place a block left click (hold to repeat) / right click tap to break · place button
Pick a block 1-9 or scroll wheel tap a hotbar slot
Share your world add ?seed=<number> to the URL same

It plays on your phone. On a touch device the game grows a floating joystick, a look surface, and jump/place buttons — no keyboard needed:

The game on a phone in portrait, with a floating movement joystick and jump/place buttons

If your browser refuses pointer lock (some extensions and embedded contexts block it silently), the desktop game switches to drag-to-look on its own. And if your machine has no WebGL at all, the game doesn't apologize — it switches to its own CPU raycaster and keeps playing:

The same bay rendered by the CPU raycaster: chunky pixels, clouds, an island with a tree

Same world, same physics, same controls — zero GPU. This mode exists because a real player hit exactly that.

What's inside

flowchart LR
    S[("seed")] --> T["terrain.ts<br/>fBm heightmap · biomes<br/>3D-noise caves · trees"]
    T --> W["Web Worker<br/>chunk generation"]
    W -->|transferable buffers| ST["streamer.ts<br/>load/unload around player"]
    E[("localStorage<br/>edit overlay")] --> ST
    ST --> M["Web Worker<br/>greedy meshing · per-vertex AO"]
    M -->|transferable geometry| R["Three.js scene<br/>texture-array shader<br/>day/night sky · clouds"]
    P["player<br/>swept AABB physics<br/>DDA voxel raycast"] --> ST
    P --> R
Loading
  • Terrain is a pure function of (seed, world position). Height, biome, cave carving, and even tree placement are all recomputable by any chunk independently — a canopy that leans across a chunk border gets identical blocks stamped from both sides without any chunk-to-chunk communication. That one design decision is what makes the world seamless and the generator trivially parallelizable.
  • Generation and meshing run in Web Workers. Terrain generation returns block buffers as transferables; a second worker holds a resident mirror of the loaded chunks and does the greedy meshing off the main thread, streaming finished geometry back (also transferable, zero copy). The main thread never blocks on a remesh, so the frame rate holds steady while you explore or dig. Results are generation-tagged, so a mesh that finishes after its chunk was re-edited or unloaded is dropped, not shown stale. Without workers it falls back to synchronous meshing.
  • The mesher greedy-meshes with real ambient occlusion. Per-vertex AO (the classic "0fps" formulation, with each quad's diagonal flipped toward the brighter vertex pair to kill the anisotropic dark-streak artifact), and then coplanar faces that share a tile and the exact same 4-corner AO pattern merge into one big quad — ~25% fewer triangles over real terrain, and far fewer on flat expanses. Because merged quads span many blocks, chunks render from a WebGL2 texture array through a custom shader that repeats the tile with fract() (an atlas would bleed into its neighbors), keeping one draw call per chunk and the crisp per-block grid.
  • Zero art assets. The entire texture atlas (grass, sand, wood rings, bark, water...) is drawn onto a small canvas at startup with a seeded PRNG. The hotbar icons are cropped from the same canvas.
  • Physics never touches the renderer. Movement is a pure per-axis swept AABB resolver over a (x, y, z) => solid? closure; block targeting is an Amanatides-Woo DDA raycast that visits every voxel boundary (a thin wall can't be skipped the way fixed-step sampling would). Both are unit-tested against synthetic terrain with no DOM or WebGL anywhere in sight.
  • Edits survive. Broken and placed blocks are stored as a sparse overlay in localStorage, keyed by seed, and re-applied over freshly generated chunks — an explored world costs kilobytes, not megabytes.
  • Two renderers, one game. The WebGL path draws meshed chunks with baked AO; the CPU path marches one ray per pixel straight through the block data (no meshes at all) with the same face shading, fog, water tint, clouds and day/night palette. Everything else — world, streaming, physics, interaction — is shared and renderer-blind.
  • It runs fast on weak hardware, and stays fast. The CPU raycaster only traces a real ray for one pixel in four: each traced ray records a compact hit signature, and the pixels between them interpolate wherever their neighbors agree (flat surfaces, sky, open water) and re-trace only across silhouettes and edges — a quarter of the ray budget for a near-identical image, so it holds a high internal resolution. Block reads in the hot loop go through a flat chunk lookup table, not a hash map. Both renderers watch their own frame time and trade resolution for smoothness: the CPU path scales its internal buffer (260–720px), the WebGL path steps its device-pixel-ratio down a ladder — so a phone GPU and a desktop GPU both converge on a fluid frame rate instead of a fixed quality that's wrong for one of them. The triangle savings from greedy meshing are spent on more draw distance, not just more frames; index buffers use 16-bit indices wherever a chunk fits under 65536 vertices (nearly always, after greedy merging), halving their upload.
  • Input-agnostic core. Keyboard, mouse, drag-look, and the on-screen touch controls all feed the same controller and interaction through a tiny surface (setExternalInput, lookBy, breakTargetedBlock). Adding the entire mobile control scheme touched no game logic.

Development

npm ci
npm run dev          # local dev server
npm run test         # 216 offline tests (world, mesher, physics, raycast, streaming, sky, input)
npm run lint && npm run typecheck
npm run build        # production bundle

Everything interesting is testable without a browser: terrain determinism and seam consistency, cave/ocean-floor invariants, mesher face counts and AO levels and triangle winding, collision resolution, raycast face detection, streaming load/unload/persistence round trips, day/night continuity. CI runs the full gate on every push.

On top of that, scripts/visual-check.mjs boots the built game in a headless Chromium (SwiftShader software WebGL — works on GPU-less machines and CI) and plays it: waits for the world to mesh, verifies the player spawned on solid ground, engages pointer lock, places a block on a neighboring column it picks by inspecting the world, breaks the block underfoot, walks — then opens a second page with requestPointerLock sabotaged to prove the drag-look fallback engages and stays fully playable (move, look, tap-break), a third page with getContext('webgl') sabotaged to prove the CPU raycaster boots, renders real terrain, moves and breaks, and a fourth emulating a phone with a touchscreen — proving the game detects touch, shows its on-screen controls, and is fully playable with a joystick drag and taps. 23 end-to-end checks with screenshots. It exists because unit tests can't see: the one real rendering bug this project shipped (an atlas UV/flipY mismatch that made grass sample an empty atlas region and leaves sample sand) was invisible to every unit test and obvious in the harness's first screenshot.

npm run build && npx vite preview --port 4173 &
node scripts/visual-check.mjs                 # PASS/FAIL per check + screenshots
node scripts/screenshot.mjs                   # regenerates docs/screenshot.png

Design decisions

  • Chunk-independent generation over chunk-to-chunk communication. The usual alternative — generate a chunk, then patch neighbors when a tree overhangs — needs ordering, patch queues, and produces seams when it's wrong. Making every feature a pure function of world position deletes the whole problem class, at the cost of some redundant computation at borders (cheap, and it runs in a worker anyway).
  • Meshing on the main thread, generation off it. Meshing needs neighbor-chunk reads and produces GPU-bound buffers; shipping all that state to a worker costs more than the ~1-2ms a chunk mesh takes. The expensive, state-free part (noise sampling) is what moves.
  • Reject-by-default physics steps. The collision resolver assumes small per-frame displacements and clamps them axis-by-axis; velocities zero out on contact. Terminal velocity plus the per-axis sweep keeps even long falls from tunneling through a floor.
  • localStorage over a save-file format. The overlay-over-regeneration model means persistence is a dictionary, not a world serializer.

Limits

  • No lighting propagation (torches, sunlight flood fill) — AO plus directional shading only. It's the single feature that would most change the look of caves, and the natural next step.
  • The CPU renderer trades some fidelity for universality: procedural texels instead of the full atlas and ~50 blocks of view distance. It still carries per-pixel ambient occlusion, sun-tracked face lighting, a sun disc, night stars, soft clouds, and the block-outline highlight. Its edge-adaptive sampling assumes neighboring pixels usually agree; that holds for blocky terrain but would smear a scene full of fine per-pixel detail (which this one never has).
  • Water supports swimming (buoyancy, swim up, hop onto the shore) but doesn't flow — breaking a dam doesn't flood anything.
  • No mobs, crafting, or inventory beyond the hotbar — the scope is the world itself: generate, explore, dig, build.
  • Chunk meshes rebuild whole-chunk on edit (a greedy remesh, ~1ms) rather than patching just the touched faces — fine in practice, and the greedy pass keeps the rebuilt buffer small.

License

MIT — © Vincent Plessy

About

A Minecraft-inspired voxel world in the browser: infinite procedural terrain, trees, caves, day/night, break and place blocks. Three.js + TypeScript, zero art assets, playable in one click.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages