Optimize Three.js Depth Precision Dynamically

In 3D graphics, Z-fighting and depth precision artifacts often occur when a camera’s near and far clipping planes are set too far apart. This article explains how to dynamically calculate and adjust a Three.js camera’s near and far planes based on the bounding box of the active scene, ensuring optimal depth buffer precision and eliminating visual rendering glitches.

Why Depth Precision Matters

The depth buffer (or Z-buffer) in WebGL does not distribute precision linearly. Most of the precision is concentrated close to the near clipping plane. If your near plane is set too close to zero (e.g., 0.0001) or your far plane is set unnecessarily far away (e.g., 100000), the GPU loses the ability to distinguish between surfaces that are close together, resulting in flickering textures known as Z-fighting.

By dynamically matching the camera’s near and far planes to the tightest bounding box of the visible scene, you maximize the efficiency of the depth buffer.

Step-by-Step Implementation

To dynamically fit the clipping planes, you must calculate the bounding box of your scene in world space, project its corners into the camera’s local space, and extract the minimum and maximum depth values.

1. Compute the Scene’s Bounding Box

First, calculate the bounding box of all objects in the scene using THREE.Box3.

const boundingBox = new THREE.Box3().setFromObject(scene);

2. Transform Bounding Box Corners to Camera Space

Because the camera can be positioned and rotated arbitrarily, you cannot simply use the world-space coordinates of the bounding box. Instead, transform the eight corners of the bounding box into the camera’s coordinate system (view space).

In Three.js, the camera looks down its local negative Z-axis. Therefore, the distance (depth) of any point from the camera is equivalent to -z in camera space.

3. Implement the Adjustment Function

Here is a complete, reusable function to calculate the optimum clipping planes and update the projection matrix:

function adjustCameraPlanes(camera, scene) {
  const boundingBox = new THREE.Box3().setFromObject(scene);

  // If the scene is empty, revert to default values
  if (boundingBox.isEmpty()) {
    camera.near = 0.1;
    camera.far = 1000;
    camera.updateProjectionMatrix();
    return;
  }

  // Define the 8 corners of the bounding box
  const min = boundingBox.min;
  const max = boundingBox.max;
  const corners = [
    new THREE.Vector3(min.x, min.y, min.z),
    new THREE.Vector3(min.x, min.y, max.z),
    new THREE.Vector3(min.x, max.y, min.z),
    new THREE.Vector3(min.x, max.y, max.z),
    new THREE.Vector3(max.x, min.y, min.z),
    new THREE.Vector3(max.x, min.y, max.z),
    new THREE.Vector3(max.x, max.y, min.z),
    new THREE.Vector3(max.x, max.y, max.z)
  ];

  let minDepth = Infinity;
  let maxDepth = -Infinity;

  // Get the camera's inverse world matrix to transform points to camera space
  const viewMatrix = camera.matrixWorldInverse;

  corners.forEach(corner => {
    // Transform corner to camera space
    corner.applyMatrix4(viewMatrix);

    // In Three.js, active objects have negative Z values in camera space.
    // We negate Z to work with positive depth values.
    const depth = -corner.z;

    if (depth < minDepth) minDepth = depth;
    if (depth > maxDepth) maxDepth = depth;
  });

  // Apply a small padding to prevent clipping at the exact boundaries
  const padding = 0.1;
  
  // Ensure the near plane is never zero or negative
  camera.near = Math.max(0.1, minDepth - padding);
  camera.far = maxDepth + padding;

  // Apply the changes
  camera.updateProjectionMatrix();
}

4. Integration into the Render Loop

Call this function inside your animation render loop or trigger it whenever objects in your scene move, get added, or when the camera moves.

function animate() {
  requestAnimationFrame(animate);

  // Update controls if you are using OrbitControls
  controls.update();

  // Dynamically adjust planes before rendering
  adjustCameraPlanes(camera, scene);

  renderer.render(scene, camera);
}

Considerations