[GRAPHICS]// 2025-12-10// 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.

LANG // JAVASCRIPTHidden-face culling logic in voxel chunk builder.
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).

CORE TAKEAWAY

Face culling reduces voxel vertex count by up to 85%, transforming an unplayable slide-show into a consistent 60 FPS experience.