Custom Shadow Materials

Using customDepthMaterial and customDistanceMaterial for fine-grained shadow control

When alphaTest isn't enough

In the previous article, we saw how alphaTest on a standard material lets the shadow pass discard transparent fragments. That works well for simple cases, but sometimes the built-in depth material doesn't pick up your alpha map correctly, or you need different shadow behavior than what the visual material provides.

For these cases, Three.js lets you assign a custom depth material to any mesh. This material is used only during the shadow map render pass. Your visual material stays the same. You're telling Three.js: "when you render this object's shadow, use this material instead of the default depth material."

customDepthMaterial

You can assign a customDepthMaterial to your mesh to replace the default depth material that Three.js uses during the shadow pass for directional lights and spot lights. You create a MeshDepthMaterial, give it the same alpha map and alpha test, and assign it to the mesh.

The demo above shows two planes with a radial gradient alpha map. The left plane uses the default depth material, and its shadow is a solid rectangle. The right plane has a customDepthMaterial configured with the same alpha map and alphaTest, producing a circular shadow that matches the visible shape of the plane.

typescript
// Create a radial gradient alpha texture
const alphaTexture = createRadialGradientTexture();

// The visual material
const material = new THREE.MeshStandardMaterial({
  color: 0x6688cc,
  alphaMap: alphaTexture,
  transparent: true,
  alphaTest: 0.5,
  side: THREE.DoubleSide
});

// Custom depth material for the shadow pass
const customDepth = new THREE.MeshDepthMaterial({
  depthPacking: THREE.RGBADepthPacking,
  alphaMap: alphaTexture,
  alphaTest: 0.5
});

const plane = new THREE.Mesh(planeGeom, material);
plane.castShadow = true;
plane.customDepthMaterial = customDepth;

The key detail is depthPacking: THREE.RGBADepthPacking. Three.js shadow maps encode depth across the RGBA channels for precision. If you omit this, the shadow will render but the depth values will be wrong, producing artifacts or missing shadows entirely.

Point lights need customDistanceMaterial

Here's a gotcha that catches people: customDepthMaterial only works for directional lights and spot lights. Point lights use a completely different shadow rendering pipeline based on cube maps, and they need customDistanceMaterial instead.

If you set customDepthMaterial on a mesh but your shadows come from a point light, the custom material will be silently ignored. No warning, no error. Your shadows will just look wrong and you'll spend an hour wondering why. Ask me how I know!

The demo above shows a point light casting shadows through two planes with a checkerboard alpha map. The left plane only has customDepthMaterial set, so the point light ignores it and casts a solid shadow. The right plane has customDistanceMaterial set, and its shadow correctly shows the cutout pattern.

typescript
// For DirectionalLight and SpotLight shadows:
mesh.customDepthMaterial = new THREE.MeshDepthMaterial({
  depthPacking: THREE.RGBADepthPacking,
  alphaMap: alphaTexture,
  alphaTest: 0.5
});

// For PointLight shadows:
mesh.customDistanceMaterial = new THREE.MeshDistanceMaterial({
  alphaMap: alphaTexture,
  alphaTest: 0.5
});

// If your scene uses BOTH directional/spot AND point lights
// with shadows, you need to set BOTH properties.
// Missing one will cause incorrect shadows from that
// light type with zero warnings.

When to use custom shadow materials

In many cases, simply setting alphaTest on the visual material is enough. The built-in depth material will inherit the alpha test and alpha map automatically. You need custom shadow materials when:

  • The automatic inheritance doesn't work. Some material configurations or custom shader materials don't propagate their alpha settings to the shadow pass.
  • You want different shadow behavior than the visual material. For example, a material that appears fully opaque visually but casts a patterned shadow (like a gobo light effect).
  • You're using point lights. The customDistanceMaterial is the only way to get alpha-tested shadows from point lights with custom alpha maps.
  • You're using ShaderMaterial or RawShaderMaterial. Custom shader materials don't automatically generate depth/distance variants. You must provide them manually.

The next article covers the other side of the shadow limitation: color. Shadow maps can't carry color information, but there are practical workarounds for producing colored and baked shadows.

←   Transparency in Shadows