Adding Shadows

Crossy Road with React Three Fiber ● Chapter 3

In this chapter, we add shadows, define a shadow camera, and ensure that the objects in the scene cast and receive shadows.

Directional Light with 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.

src/components/DirectionalLight.tsx
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}
/>
);
}
src/components/DirectionalLight.jsx
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.

src/components/Scene.tsx
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>
);
};
src/components/Scene.jsx
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>
);
};

Set castShadow and receiveShadow properties on the Meshes

To 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.

src/components/Grass.tsx
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>
);
}
src/components/Grass.jsx
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>
);
}
src/components/Road.tsx
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>
);
}
src/components/Road.jsx
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.

src/components/Player.tsx
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>
);
}
src/components/Player.jsx
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>
);
}
src/components/Tree.tsx
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>
);
}
src/components/Tree.jsx
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>
);
}
src/components/Car.tsx
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>
);
}
src/components/Car.jsx
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>
);
}
src/components/Truck.tsx
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>
);
}
src/components/Truck.jsx
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>
);
}
Previous:
Rendering the Map
Next:
Animating the Vehicles