Up until now, our 3D scenes might feel a little flat, even with lighting. The objects cast no shadows, making it hard to perceive their depth and relationship to the environment. Shadows are a fundamental aspect of how we perceive the real world, and their absence in a 3D scene can make it look artificial. In this section, we'll introduce the concept of shadows in Three.js and learn how to implement them to add a crucial layer of realism to your creations.
The core idea behind shadows in 3D rendering is that certain objects block light, preventing it from reaching other surfaces. Three.js simulates this by using a technique called shadow mapping. Essentially, the light source renders the scene from its own perspective, creating a depth map. This depth map, known as a shadow map, is then used to determine which parts of the scene are in shadow when rendered from the camera's perspective.
To enable shadows in your Three.js scene, you need to make a few key configurations. First, you must enable shadows on the renderer. Then, you need to tell each light that casts shadows to do so, and importantly, mark each object that should receive and cast shadows.
const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;Not all lights can cast shadows. Typically, you'll want to use DirectionalLight or SpotLight for shadow casting. Point lights can also cast shadows, but they require a bit more setup. When you create a shadow-casting light, you need to enable its shadow properties.
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
directionalLight.castShadow = true;For an object to participate in the shadow casting process, it needs to be marked as such. This involves setting two properties: castShadow to true for objects that will block light, and receiveShadow to true for objects that will have shadows drawn on them.
const cube = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial({ color: 0x00ff00 }));
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);const ground = new THREE.Mesh(new THREE.PlaneGeometry(10, 10), new THREE.MeshStandardMaterial({ color: 0xcccccc }));
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);When setting up shadow casting lights, especially DirectionalLight, it's often beneficial to adjust the shadow's camera's frustum. This frustum defines the area from which the shadow map is rendered. A well-adjusted frustum ensures that all relevant objects are captured, leading to sharper and more accurate shadows. You can configure properties like shadow.camera.left, shadow.camera.right, shadow.camera.top, shadow.camera.bottom, shadow.camera.near, and shadow.camera.far.
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
directionalLight.shadow.camera.near = 0.1;
directionalLight.shadow.camera.far = 20;The quality of shadows is determined by the resolution of the shadow map. A higher resolution shadow map results in sharper shadows but can also increase rendering performance demands. You can adjust this using the shadow.mapSize property.
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;By combining these steps, you can transform your scenes from flat representations to more visually convincing and immersive 3D experiences, all thanks to the introduction of realistic shadows. The interplay of light and shadow is a powerful tool for guiding the viewer's eye and conveying spatial relationships.