The goal was smooth, organic terrain that could be reshaped at runtime, rather than blocky Minecraft-style voxels. Marching cubes ties together noise generation, mesh extraction and signed distance fields into one interactive system, implemented first as a naive CPU version and then as a parallel GPU compute shader.
What are marching cubes?
Marching cubes[1] is a computer-graphics algorithm that extracts a polygonal mesh of an iso-surface from a three-dimensional grid of scalar values (a discrete scalar field, or voxel grid). You can picture the grid as a lattice of vertices, each holding a value, where the space enclosed by every group of eight neighbouring vertices forms a single voxel.
The core algorithm
For each voxel, the algorithm looks at the value stored in its eight corners. A corner whose value is above a chosen threshold (the isovalue) is considered "active". The surface inside the voxel is then built from triangles selected from a precomputed table of 256 configurations. With eight corners there are 2^8 = 256 possible inside/outside combinations, all of which reduce to 15 base cases and their rotations.
The 15 base cases: every one of the 256 corner configurations reduces to one of these, plus its rotations and reflections.
Edge interpolation
Each vertex of the final mesh sits on an edge of a voxel. Accuracy improves dramatically by sliding that vertex along its edge according to the two corner values it connects. If an edge joins a corner of value 3 and a corner of value 1, the vertex lands closer to the "3" corner rather than at the midpoint, which turns a stair-stepped surface into a smooth one.
Where it is used
The technique is a staple of medical visualization (reconstructing surfaces from CT and MRI scans) and of organic 3D modelling such as metaballs and other iso-surfaces. In games, marching cubes (or a variant) powers procedural and fully destructible terrain, as seen in titles like Astroneer[2] and Deep Rock Galactic[3].
Implementing it in Unity
A naive CPU prototype
I started simple: subdividing space into voxels and running the algorithm inside a C# MonoBehaviour, with every operation handled on the CPU. It was slow, but it validated the approach and gave me a reference implementation to optimise against.
Chunking
Next I introduced chunking. Instead of one giant grid for the whole world, space is split into a grid of chunks, each its own grid of voxels. When a value changes, only the affected chunk is re-meshed rather than the entire world. This cuts computation time in most situations and allows a world of effectively infinite size.
Moving to a compute shader
Because the algorithm works per voxel, it is embarrassingly parallel. I rewrote the core in an HLSL compute shader so the CPU could delegate the work to the GPU, computing the triangles of thousands of voxels simultaneously. This brought another large drop in computation time.
Removing duplicate vertices
Computing each voxel independently duplicates the vertices shared along voxel edges. I fixed this by giving every vertex an ID (two integers identifying the edge it lives on) and keeping a single vertex per edge. As a side effect, the mesh now renders with smooth shading. The result is a fully dynamic, fully destructible mesh.Editing a smooth, fully destructible marching-cubes mesh in real time.
Procedural terrain with Perlin noise
Cave generation
With a working mesher in hand, I began feeding it procedural data. Perlin noise[4] is a procedural texture with a pseudo-random look, but unlike pure randomness, nearby samples never vary wildly, so its detail stays coherent in scale. To carve caves, I assign each voxel a density from 3D Perlin noise. Since Unity[5] only exposes 2D Perlin noise, I derive a 3D value by averaging six 2D samples:
// Sample 3D Perlin noise from Unity's 2D function
static float Perlin3D(Vector3 pos, Vector3 scale)
{
pos = Vector3.Scale(pos, scale);
float ab = Mathf.PerlinNoise(pos.x, pos.y);
float bc = Mathf.PerlinNoise(pos.y, pos.z);
float ac = Mathf.PerlinNoise(pos.x, pos.z);
float ba = Mathf.PerlinNoise(pos.y, pos.x);
float cb = Mathf.PerlinNoise(pos.z, pos.y);
float ca = Mathf.PerlinNoise(pos.z, pos.x);
return (ab + bc + ac + ba + cb + ca) / 6f;
}
Deriving 3D Perlin noise from Unity’s 2D function.
A scale parameter controls the overall shape and frequency of the caves.
Texturing with triplanar mapping
Marching-cubes geometry has no natural UV coordinates. The mesh is rebuilt every time the terrain changes, and its triangles line up with no fixed texture layout, so conventional UV mapping simply does not apply. The fix is triplanar mapping: instead of relying on UVs, the shader samples the texture three times, once projected along each world axis (X, Y and Z), then blends the three samples using the surface normal as the weight so the projection most aligned with the surface dominates. The result is seamless, stretch-free texturing on arbitrary geometry, with no UVs to author or maintain.This suits dynamic editing well. Because the texturing is derived purely from world position and surface normal, it needs nothing precomputed and nothing stored on the mesh: the moment a cave is carved or a chunk is re-meshed, the freshly exposed surface is textured correctly on the next frame, with no UV-regeneration step to slow the edit down.Procedural caves, textured with triplanar mapping, generated by sampling 3D Perlin noise per voxel.
Surfaces with octaved noise
A single Perlin noise only gave me caves, not a believable surface. The fix was octaves: summing several Perlin noises while doubling the frequency and halving the amplitude at each step.
// Layer Perlin noises with rising frequency and falling amplitude
private static float OctavedPerlin(Vector2 pos, int octaves)
{
float res = 0;
for (int i = 0; i < octaves; i++)
{
float octaveMag = Mathf.Pow(2, i * 2);
res += Mathf.PerlinNoise(pos.x * octaveMag, pos.y * octaveMag) / octaveMag;
}
return res;
}
Layering Perlin noise octaves for natural surfaces.
I use this to define a surface height for each (x, z) coordinate, with y as elevation. Everything below that height is "ground" (filled with Perlin-driven density), everything above is "air". The outcome is terrain with a coherent interior and a noisy, natural-looking surface.
Single Perlin noise
Octaved Perlin noise
Surface height from a single Perlin noise (left) versus several octaves summed (right): octaves add the detail across scales that makes the terrain believable.
Water and biomes
Because the data structure is just a grid of scalars, extending it is easy. I added a water level so anything between the ground and that level counts as "water", and a per-voxel type value to distinguish water, grass and rock, colouring triangles accordingly. Two extra fields per (x, z), temperature and humidity, then drive biome selection, letting the world shift between climates.The full procedural terrain: an octaved surface with caves, water and biomes, all editable at runtime.
Further experiments
Converting meshes to volume data
Sometimes you want to start from a conventional mesh, for example to make a building fully destructible. Authoring volume data for such a model by hand is painful, so I wrote a tool that voxelizes an existing mesh. It computes the mesh bounding box, subdivides it into a 2^depth grid of voxels via an octree depth, and tests each voxel centre for containment: a ray is cast in a random direction and, using Möller-Trumbore[6] intersection, the number of triangle hits is counted. An even count means the voxel is outside the mesh; an odd count means it is inside.The approach has clear drawbacks. It is slow, since every voxel tests against every triangle, and it returns boolean occupancy rather than scalar density, so it loses the smooth interpolation that marching cubes depends on. I plan to rework it to test triangles against voxel edges instead, yielding a scalar "fill" value per voxel.
SDFs and constructive solid geometry
A signed distance field (SDF) defines a volume by returning the orthogonal distance from a point to the surface, with the sign indicating whether the point is inside or outside. The sphere is the canonical example:
// Signed distance to a sphere
float sdSphere(float3 eye, float3 pos, float radius)
{
return distance(eye, pos) - radius;
}
A signed distance field for a sphere.
I had used SDFs before with ray marching, but that lived purely as a post-process on top of the rendered scene, with no way to attach physics or collisions that worked with Unity. To get there I needed real geometry, which is exactly where marching cubes comes back in.Meshing an SDF is simple: discretize the volume’s bounding box into voxels and evaluate the SDF at each voxel centre to get its density. The interpolation works cleanly, and combining SDFs with min/max operations gives constructive solid geometry: adding and subtracting shapes from the world, then re-meshing only the affected chunks.Constructive solid geometry: adding and subtracting SDF shapes, re-meshed live with marching cubes.
Ideas for the future
Dynamic voxel size
Today every voxel is the same size and every chunk holds the same count. Giving chunks a dynamic voxel count would act as a level-of-detail system, reducing both the number of voxels to compute and the complexity of meshes for chunks that are distant or occluded.
Cellular automata
Because the voxel grid carries a natural notion of neighbourhood and connected space, it is a great substrate for simulation. I would like to add gravity via a flow-fill algorithm, or a cellular automaton that models, for instance, the spread of fire across the terrain.