Shadow mapping is the most common and versatile technique for rendering realistic shadows in 3D graphics, including Three.js. It works by simulating how light behaves: what parts of the scene are occluded from the light source. Let's break down how it functions and the key considerations for achieving convincing results.
The core idea behind shadow mapping is to render the scene from the perspective of the light source. This creates a 'depth map' where each pixel stores the distance from the light to the nearest object. When rendering the scene from the camera's perspective, for each pixel, we compare its depth to the depth stored in the shadow map. If the pixel's depth is greater than the stored depth, it means that pixel is in shadow.
graph TD
A[Light Source] --> B{Render Scene from Light's POV};
B --> C[Shadow Map (Depth Texture)];
D[Camera] --> E{Render Scene from Camera's POV};
E --> F[Fragment Shader];
F -- Compare Fragment Depth --> C;
C -- Depth Check --> F;
F -- Determine Shadow or Not --> G[Final Pixel Color];
In Three.js, this process involves several steps. First, you need a light that can cast shadows, typically a DirectionalLight or SpotLight. You then need to enable shadow casting on that light and on the objects that should cast and receive shadows. Finally, you enable shadow mapping on the WebGLRenderer.
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(10, 10, 10);
light.castShadow = true;
scene.add(light);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);
renderer.shadowMap.enabled = true;A crucial parameter is the shadow map's resolution. A higher resolution means a more detailed shadow map, leading to sharper shadows. However, this comes at the cost of increased memory usage and processing time. Too low a resolution can result in 'aliasing' or 'jagged' shadows.
Another common issue is 'peter panning', where shadows appear to 'float' slightly above the surface they should be attached to. This happens when the shadow map's bias is not set correctly. You can adjust the shadow.bias property on the light to mitigate this.
light.shadow.bias = -0.005;For a more realistic look, especially with directional lights, you'll want to configure the shadow camera's frustum. This defines the area from which the shadow map is generated. If the frustum is too small, objects outside it won't cast shadows. If it's too large, the resolution for the visible area decreases, leading to blocky shadows. Three.js offers automatic shadow camera setup, but manual control can be beneficial.
Techniques like Percentage-Closer Filtering (PCF) are employed to smooth out the jagged edges of shadow maps, making them appear softer and more natural. Three.js supports PCF out of the box, often controlled by the renderer.shadowMap.type property. Different PCF types offer varying levels of blur and performance trade-offs.
renderer.shadowMap.type = THREE.PCFSoftShadowMap;Consider the 'cascaded shadow maps' (CSM) technique for large scenes. This involves using multiple shadow maps with different resolutions and frustums, each covering a different distance from the camera. This provides high-resolution shadows up close and lower-resolution shadows in the distance, optimizing performance and shadow quality.
Finally, remember that shadow rendering is computationally expensive. Be mindful of how many objects cast and receive shadows, and optimize your scene geometry and shadow map resolution accordingly to maintain a smooth frame rate.