In this chapter, we add shadows, define a shadow camera, and ensure that the objects in the scene cast and receive shadows.
In this chapter, we add shadows, define a shadow camera, and ensure that the objects in the scene cast and receive shadows.
Let’s update our directional light to cast shadows. We need to set up a few more properties to define how. As our directional light has more properties now, we move it in its own component.
First, we need to set the light’s castShadow
property to enable shadow casting. We also need to set the shadow-mapSize
to set the resolution of the shadow map. Higher values result in sharper shadows.
Then, we need to define a shadow camera. A shadow camera renders the scene from the perspective of the light to calculate shadows.
To define a shadow camera, we need to define the camera frustum. The frustum sets the area of the scene in which the shadows are calculated. In the case of directional light, the frustum is a box. Shadows are only calculated within this box.
export function DirectionalLight() { return ( <directionalLight position={[-100, -100, 200]} up={[0, 0, 1]} castShadow shadow-mapSize={[2048, 2048]} shadow-camera-left={-400} shadow-camera-right={400} shadow-camera-top={400} shadow-camera-bottom={-400} shadow-camera-near={50} shadow-camera-far={400} /> );}
export function DirectionalLight() { return ( <directionalLight position={[-100, -100, 200]} up={[0, 0, 1]} castShadow shadow-mapSize={[2048, 2048]} shadow-camera-left={-400} shadow-camera-right={400} shadow-camera-top={400} shadow-camera-bottom={-400} shadow-camera-near={50} shadow-camera-far={400} /> );}
Let’s take a look at this demo. You can drag it to see how it works. Here, the red dot represents the position of the directional light, and the line going to the player represents its direction.
The gray box around the scene represents the camera frustum. The red rectangle shows its intersection with the ground. Everything inside this box can cast and receive shadows.
To define the frustum for a directional light, we need to set how far each side of the box is from the camera position along the different axes from the camera’s perspective. We set the box’s left, right, top, botton, near, and far sides.
Now that we have defined the directional light in its own component let’s replace the original directional light in the Scene
component with this new component.
To enable shadows in the scene, we must also set the shadows
property of the Canvas
component.
import { Canvas } from "@react-three/fiber";import { DirectionalLight } from "./DirectionalLight";
type Props = { children: React.ReactNode;};
export const Scene = ({ children }: Props) => { return ( <Canvas orthographic={true} shadows={true} camera={{ up: [0, 0, 1], position: [300, -300, 300], }} > <ambientLight /> <directionalLight position={[-100, -100, 200]} /> <DirectionalLight /> {children} </Canvas> );};
import { Canvas } from "@react-three/fiber";import { DirectionalLight } from "./DirectionalLight";
export const Scene = ({ children }) => { return ( <Canvas orthographic={true} shadows={true} camera={{ up: [0, 0, 1], position: [300, -300, 300], }} > <ambientLight /> <directionalLight position={[-100, -100, 200]} /> <DirectionalLight /> {children} </Canvas> );};
castShadow
and receiveShadow
properties on the MeshesTo enable shadows on a mesh, you need to set the castShadow
and receiveShadow
properties. The castShadow
property tells the mesh to cast shadows, while the receiveShadow
property tells the mesh to receive shadows.
In the Grass
and Road
components, we need to set the receiveShadow
property on the mesh to allow them to receive shadows.
6 collapsed lines
import { tilesPerRow, tileSize } from "../constants";
type Props = { rowIndex: number; children?: React.ReactNode;};
export function Grass({ rowIndex, children }: Props) { return ( <group position-y={rowIndex * tileSize}> <mesh receiveShadow> <boxGeometry args={[tilesPerRow * tileSize, tileSize, 3]} /> <meshLambertMaterial color={0xbaf455} flatShading /> </mesh> {children} </group> );}
import { tilesPerRow, tileSize } from "../constants";
export function Grass({ rowIndex, children }) { return ( <group position-y={rowIndex * tileSize}> <mesh receiveShadow> <boxGeometry args={[tilesPerRow * tileSize, tileSize, 3]} /> <meshLambertMaterial color={0xbaf455} flatShading /> </mesh> {children} </group> );}
6 collapsed lines
import { tilesPerRow, tileSize } from "../constants";
type Props = { rowIndex: number; children?: React.ReactNode;};
export function Road({ rowIndex, children }: Props) { return ( <group position-y={rowIndex * tileSize}> <mesh receiveShadow> <planeGeometry args={[tilesPerRow * tileSize, tileSize]} /> <meshLambertMaterial color={0x454a59} flatShading /> </mesh> {children} </group> );}
import { tilesPerRow, tileSize } from "../constants";
export function Road({ rowIndex, children }) { return ( <group position-y={rowIndex * tileSize}> <mesh receiveShadow> <planeGeometry args={[tilesPerRow * tileSize, tileSize]} /> <meshLambertMaterial color={0x454a59} flatShading /> </mesh> {children} </group> );}
Then, for the Player
, Tree
, Car
, and Truck
components, we need to set both the castShadow
and receiveShadow
properties on the meshes to allow them to cast and receive shadows.
import { Bounds } from "@react-three/drei";
export function Player() { return ( <Bounds fit clip observe margin={10}> <mesh position={[0, 0, 10]} castShadow receiveShadow> <boxGeometry args={[15, 15, 20]} /> <meshLambertMaterial color={0xffffff} flatShading /> </mesh> </Bounds> );}
import { Bounds } from "@react-three/drei";
export function Player() { return ( <Bounds fit clip observe margin={10}> <mesh position={[0, 0, 10]} castShadow receiveShadow> <boxGeometry args={[15, 15, 20]} /> <meshLambertMaterial color={0xffffff} flatShading /> </mesh> </Bounds> );}
6 collapsed lines
import { tileSize } from "../constants";
type Props = { tileIndex: number; height: number;};
export function Tree({ tileIndex, height }: Props) { return ( <group position-x={tileIndex * tileSize}> <mesh position-z={height / 2 + 20} castShadow receiveShadow> <boxGeometry args={[30, 30, height]} /> <meshLambertMaterial color={0x7aa21d} flatShading /> </mesh> <mesh position-z={10} castShadow receiveShadow> <boxGeometry args={[15, 15, 20]} /> <meshLambertMaterial color={0x4d2926} flatShading /> </mesh> </group> );}
import { tileSize } from "../constants";
export function Tree({ tileIndex, height }) { return ( <group position-x={tileIndex * tileSize}> <mesh position-z={height / 2 + 20} castShadow receiveShadow> <boxGeometry args={[30, 30, height]} /> <meshLambertMaterial color={0x7aa21d} flatShading /> </mesh> <mesh position-z={10} castShadow receiveShadow> <boxGeometry args={[15, 15, 20]} /> <meshLambertMaterial color={0x4d2926} flatShading /> </mesh> </group> );}
11 collapsed lines
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
type Props = { rowIndex: number; initialTileIndex: number; direction: boolean; speed: number; color: THREE.ColorRepresentation;};
export function Car({ rowIndex, initialTileIndex, direction, speed, color,}: Props) { return ( <group position-x={initialTileIndex * tileSize} rotation-z={direction ? 0 : Math.PI} > <mesh position={[0, 0, 12]} castShadow receiveShadow> <boxGeometry args={[60, 30, 15]} /> <meshLambertMaterial color={color} flatShading /> </mesh> <mesh position={[-6, 0, 25.5]} castShadow receiveShadow> <boxGeometry args={[33, 24, 12]} /> <meshLambertMaterial color={0xffffff} flatShading /> </mesh> <Wheel x={-18} /> <Wheel x={18} /> </group> );}
import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Car({ rowIndex, initialTileIndex, direction, speed, color,}) { return ( <group position-x={initialTileIndex * tileSize} rotation-z={direction ? 0 : Math.PI} > <mesh position={[0, 0, 12]} castShadow receiveShadow> <boxGeometry args={[60, 30, 15]} /> <meshLambertMaterial color={color} flatShading /> </mesh> <mesh position={[-6, 0, 25.5]} castShadow receiveShadow> <boxGeometry args={[33, 24, 12]} /> <meshLambertMaterial color={0xffffff} flatShading /> </mesh> <Wheel x={-18} /> <Wheel x={18} /> </group> );}
11 collapsed lines
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
type Props = { rowIndex: number; initialTileIndex: number; direction: boolean; speed: number; color: THREE.ColorRepresentation;};
export function Truck({ rowIndex, initialTileIndex, direction, speed, color,}: Props) { return ( <group position-x={initialTileIndex * tileSize} rotation-z={direction ? 0 : Math.PI} > <mesh position={[-15, 0, 25]} castShadow receiveShadow> <boxGeometry args={[70, 35, 35]} /> <meshLambertMaterial color={0xb4c6fc} flatShading /> </mesh> <mesh position={[35, 0, 20]} castShadow receiveShadow> <boxGeometry args={[30, 30, 30]} /> <meshLambertMaterial color={color} flatShading /> </mesh> <Wheel x={-35} /> <Wheel x={5} /> <Wheel x={37} /> </group> );}
import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Truck({ rowIndex, initialTileIndex, direction, speed, color,}) { return ( <group position-x={initialTileIndex * tileSize} rotation-z={direction ? 0 : Math.PI} > <mesh position={[-15, 0, 25]} castShadow receiveShadow> <boxGeometry args={[70, 35, 35]} /> <meshLambertMaterial color={0xb4c6fc} flatShading /> </mesh> <mesh position={[35, 0, 20]} castShadow receiveShadow> <boxGeometry args={[30, 30, 30]} /> <meshLambertMaterial color={color} flatShading /> </mesh> <Wheel x={-35} /> <Wheel x={5} /> <Wheel x={37} /> </group> );}