Skip to content
← All notes

Graphics//5 min read

Optimizing 3D Voxel Meshing in WebGL & Three.js

Techniques for rendering procedural voxel worlds at 60 FPS in browsers: chunking, face culling, raycasting, and memory pooling.

#Three.js#WebGL#Graphics#Optimization#Perlin Noise

The Draw Call Bottleneck

In Three.js, instantiating a separate Mesh for every block in a 16x16x64 chunk produces over 16,000 individual draw calls per chunk. Even with GPU instancing, invisible internal faces between touching blocks waste precious rasterizer bandwidth.

1. Hidden Face Culling During Mesh Generation

Before adding a quad to a chunk's geometry buffer, inspect the voxel grid to see if an adjacent block already occupies the neighboring coordinate. If neighbor block is opaque, skip generating the face entirely.

javascript
function buildChunkGeometry(chunk, world) {
  const positions = [];
  const normals = [];
  const uvs = [];

  for (let x = 0; x < 16; x++) {
    for (let y = 0; y < 64; y++) {
      for (let z = 0; z < 16; z++) {
        const block = chunk.getBlock(x, y, z);
        if (!block.isSolid) continue;

        // Check 6 cardinal neighbors
        for (const face of FACES) {
          const neighbor = world.getBlock(x + face.dir[0], y + face.dir[1], z + face.dir[2]);
          if (!neighbor || !neighbor.isSolid) {
            // Neighbor is transparent or air — emit 2 triangles for this face
            emitFaceVertices(positions, normals, uvs, face, x, y, z, block.type);
          }
        }
      }
    }
  }
  return new BufferGeometry(/* ... */);
}

2. Procedural Heightmaps with Multi-Octave Noise

Terrain elevation is evaluated by combining high-frequency low-amplitude noise (surface roughness) with low-frequency high-amplitude noise (hills and continental forms).