vidar.games

Marching cubes terraforming

2026-11-10 ยท Discuss on X

A glowing brush digs a cave into a hillside, then builds an arch out of thin air. Underneath: byte voxels and marching cubes, re-meshed one 16-voxel chunk at a time.

The problem

Most terrain is a height field: one height per column. It can raise a hill or dig a pit, but it cannot hold anything with space underneath. A cave or an arch needs at least two surfaces in the same column, and a height field only has one.

How it works

The world is a 3D grid of bytes. Each one stores how solid that point is, and the surface sits where the value crosses 0.5. Terrain starts from layered noise: value = (y - height) x 0.5, clamped to 0..1.
Marching cubes walks every cell of eight voxels. Each corner below the threshold sets one bit of an 8-bit index. A 256-entry table says which of the 12 cell edges the surface crosses, and a vertex lands on each crossed edge by linear interpolation. A second table joins those vertices into up to five triangles.
The brush is the repo's deformer: every voxel within 3 units changes by 0.1 divided by its distance, every frame. Digging raises the values and building lowers them. Any chunk whose voxels changed is flagged and fully re-meshed.

What I hit

Chunks share their border voxels: each one stores 17 per side for 16 cells. An edit near a chunk corner therefore flags up to 8 chunks. While digging, 1 or 2 chunks were rebuilt per frame. While building the arch, which crosses three chunk borders, it went up to 8.
Normals are one per triangle, so the surface comes out faceted. That is how the source does it.

My build

Ported the voxel generation, the marching cubes loop with its lookup tables, the deformer and the chunk flagging to JavaScript, and drew the result with three.js.
65 x 33 x 65 byte voxels in 4 x 2 x 4 chunks of 16 cubed. The starting terrain has 14,218 triangles. After the cave and the arch it has 16,142.
Over the 24 s clip, chunks were rebuilt 1,308 times in total.
For the left half of the split I wrote a plain height field that moves each column's top to the edge of the brush sphere. That is about the closest a single height per column can get.

Trade-offs

Bytes keep memory small, at the cost of only 256 density steps per voxel. Re-meshing a whole chunk for a one-voxel change wastes work. Flat normals look faceted. The triplanar shader here blends three flat colours by the surface normal. It does not use textures.

When to use it

Diggable or buildable terrain, caves, overhangs and editor sculpting. For a big open landscape with no overhangs, a height field is lighter and still simpler.

New breakdowns go out on X first: @vidar_0208. If this one helped, you can buy me a coffee.

Source and credits

Technique source: Marching-Cubes-Terrain by Eldemarkki, https://github.com/Eldemarkki/Marching-Cubes-Terrain, MIT license, commit 97ae4d62f942c124145e02b2be8154e55d1826d7. Ported to JavaScript from its ProceduralTerrainVoxelDataCalculationJob (byte density from octave noise), MarchingCubesJob, MarchingCubesFunctions and MarchingCubesLookupTables (cube index, edge interpolation, triangle table, flat normal per triangle, degenerate triangles skipped), TerrainDeformer (EditTerrain: range 3, speed 0.1 divided by distance per frame), the PerChunkStore and ChunkUpdater chunk flagging with 16 cubed chunks, and the Triplanar Shader with the colours from its Triplanar Shading material.

Noise: 3D simplex noise after the Ashima Arts and Stefan Gustavson GLSL version (MIT), the one the source's noise call is based on, rewritten in JavaScript.

Render framework: three.js by mrdoob and contributors, https://github.com/mrdoob/three.js, MIT license, r187. Used for drawing, lighting and shadows. It is not the source of the terrain technique.

My own code: the scripted brush path, the height field used for the comparison, the chunk debug view, camera moves, labels and video. No external models or assets.