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.
To enable shadows in the scene, first, we have to enable shadow maps in the renderer.
import * as THREE from "three";
export function Renderer() { const canvas = document.querySelector("canvas.game"); if (!canvas) throw new Error("Canvas not found");
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, canvas: canvas, }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true;
return renderer;}
import * as THREE from "three";
export function Renderer() { const canvas = document.querySelector("canvas.game"); if (!canvas) throw new Error("Canvas not found");
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, canvas: canvas, }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true;
return renderer;}
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 file.
First, we need to set the light’s castShadow
property to enable shadow casting. We also need to set the shadow.mapSize.width
and height
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.
We need to define a camera frustum. In the case of directional light, the shadow camera is an orthographic camera, and its frustum is a box. Shadows are only calculated within this box.
import * as THREE from "three";
export function DirectionalLight() { const dirLight = new THREE.DirectionalLight(); dirLight.position.set(-100, -100, 200); dirLight.up.set(0, 0, 1); dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048; dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.up.set(0, 0, 1); dirLight.shadow.camera.left = -400; dirLight.shadow.camera.right = 400; dirLight.shadow.camera.top = 400; dirLight.shadow.camera.bottom = -400; dirLight.shadow.camera.near = 50; dirLight.shadow.camera.far = 400;
return dirLight;}
import * as THREE from "three";
export function DirectionalLight() { const dirLight = new THREE.DirectionalLight(); dirLight.position.set(-100, -100, 200); dirLight.up.set(0, 0, 1); dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048; dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.up.set(0, 0, 1); dirLight.shadow.camera.left = -400; dirLight.shadow.camera.right = 400; dirLight.shadow.camera.top = 400; dirLight.shadow.camera.bottom = -400; dirLight.shadow.camera.near = 50; dirLight.shadow.camera.far = 400;
return dirLight;}
In the demo below the green dot still represents our main camera that points towards the player.
We define the shadow camera’s frustum like when we set up our main camera. We define a box by the distance from the position of the light to the box’s near, far, left, right, top, and bottom sides.
Drag the demo below 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. All other light rays are parallel with this vector.
The bigger gray box around the scene represents the shadow camera frustum. The near, far, left, right, top, and bottom values represent how far each side of the frustum box is from the camera position along the different axes from the camera’s perspective.
The red rectangle shows what part of the ground is part of the shadow camera frustum. As the red rectangle covers more than the green rectangle representing the visible area, every object in the scene should be able to cast and receive shadows.
Now that we have defined the directional light in its own file, let’s replace the original directional light in the main file with a call to this new function.
import * as THREE from "three";import { Renderer } from "./components/Renderer";import { Camera } from "./components/Camera";import { DirectionalLight } from "./components/DirectionalLight";import { player } from "./components/Player";import { map, initializeMap } from "./components/Map";import "./style.css";
const scene = new THREE.Scene();scene.add(player);scene.add(map);
const ambientLight = new THREE.AmbientLight();scene.add(ambientLight);
const dirLight = DirectionalLight();scene.add(dirLight);
const camera = Camera();scene.add(camera);
initializeGame();
function initializeGame() { initializeMap();}
const renderer = Renderer();renderer.render(scene, camera);
import * as THREE from "three";import { Renderer } from "./components/Renderer";import { Camera } from "./components/Camera";import { DirectionalLight } from "./components/DirectionalLight";import { player } from "./components/Player";import { map, initializeMap } from "./components/Map";import "./style.css";
const scene = new THREE.Scene();scene.add(player);scene.add(map);
const ambientLight = new THREE.AmbientLight();scene.add(ambientLight);
const dirLight = DirectionalLight();scene.add(dirLight);
const camera = Camera();scene.add(camera);
initializeGame();
function initializeGame() { initializeMap();}
const renderer = Renderer();renderer.render(scene, camera);
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.
import * as THREE from "three";import { tilesPerRow, tileSize } from "../constants";
export function Grass(rowIndex: number) { const grass = new THREE.Group(); grass.position.y = rowIndex * tileSize;
const foundation = new THREE.Mesh( new THREE.BoxGeometry(tilesPerRow * tileSize, tileSize, 3), new THREE.MeshLambertMaterial({ color: 0xbaf455 }) ); foundation.position.z = 1.5; foundation.receiveShadow = true; grass.add(foundation);
return grass;}
import * as THREE from "three";import { tilesPerRow, tileSize } from "../constants";
export function Grass(rowIndex) { const grass = new THREE.Group(); grass.position.y = rowIndex * tileSize;
const foundation = new THREE.Mesh( new THREE.BoxGeometry(tilesPerRow * tileSize, tileSize, 3), new THREE.MeshLambertMaterial({ color: 0xbaf455 }) ); foundation.position.z = 1.5; foundation.receiveShadow = true; grass.add(foundation);
return grass;}
import * as THREE from "three";import { tilesPerRow, tileSize } from "../constants";
export function Road(rowIndex: number) { const road = new THREE.Group(); road.position.y = rowIndex * tileSize;
const foundation = new THREE.Mesh( new THREE.PlaneGeometry(tilesPerRow * tileSize, tileSize), new THREE.MeshLambertMaterial({ color: 0x454a59 }) ); foundation.receiveShadow = true; road.add(foundation);
return road;}
import * as THREE from "three";import { tilesPerRow, tileSize } from "../constants";
export function Road(rowIndex) { const road = new THREE.Group(); road.position.y = rowIndex * tileSize;
const foundation = new THREE.Mesh( new THREE.PlaneGeometry(tilesPerRow * tileSize, tileSize), new THREE.MeshLambertMaterial({ color: 0x454a59 }) ); foundation.receiveShadow = true; road.add(foundation);
return road;}
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 * as THREE from "three";
export const player = Player();
function Player() { const body = new THREE.Mesh( new THREE.BoxGeometry(15, 15, 20), new THREE.MeshLambertMaterial({ color: "white", flatShading: true, }) ); body.position.z = 10; body.castShadow = true; body.receiveShadow = true;
return body;}
import * as THREE from "three";
export const player = Player();
function Player() { const body = new THREE.Mesh( new THREE.BoxGeometry(15, 15, 20), new THREE.MeshLambertMaterial({ color: "white", flatShading: true, }) ); body.position.z = 10; body.castShadow = true; body.receiveShadow = true;
return body;}
import * as THREE from "three";import { tileSize } from "../constants";
export function Tree(tileIndex: number, height: number) { const tree = new THREE.Group(); tree.position.x = tileIndex * tileSize;
9 collapsed lines
const trunk = new THREE.Mesh( new THREE.BoxGeometry(15, 15, 20), new THREE.MeshLambertMaterial({ color: 0x4d2926, flatShading: true, }) ); trunk.position.z = 10; tree.add(trunk);
const crown = new THREE.Mesh( new THREE.BoxGeometry(30, 30, height), new THREE.MeshLambertMaterial({ color: 0x7aa21d, flatShading: true, }) ); crown.position.z = height / 2 + 20; crown.castShadow = true; crown.receiveShadow = true; tree.add(crown);
return tree;}
import * as THREE from "three";import { tileSize } from "../constants";
export function Tree(tileIndex, height) { const tree = new THREE.Group(); tree.position.x = tileIndex * tileSize;
9 collapsed lines
const trunk = new THREE.Mesh( new THREE.BoxGeometry(15, 15, 20), new THREE.MeshLambertMaterial({ color: 0x4d2926, flatShading: true, }) ); trunk.position.z = 10; tree.add(trunk);
const crown = new THREE.Mesh( new THREE.BoxGeometry(30, 30, height), new THREE.MeshLambertMaterial({ color: 0x7aa21d, flatShading: true, }) ); crown.position.z = height / 2 + 20; crown.castShadow = true; crown.receiveShadow = true; tree.add(crown);
return tree;}
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Car( initialTileIndex: number, direction: boolean, color: THREE.ColorRepresentation) { const car = new THREE.Group(); car.position.x = initialTileIndex * tileSize; if (!direction) car.rotation.z = Math.PI;
const main = new THREE.Mesh( new THREE.BoxGeometry(60, 30, 15), new THREE.MeshLambertMaterial({ color, flatShading: true }) ); main.position.z = 12; main.castShadow = true; main.receiveShadow = true; car.add(main);
const cabin = new THREE.Mesh( new THREE.BoxGeometry(33, 24, 12), new THREE.MeshLambertMaterial({ color: "white", flatShading: true, }) ); cabin.position.x = -6; cabin.position.z = 25.5; cabin.castShadow = true; cabin.receiveShadow = true; car.add(cabin);
const frontWheel = Wheel(18); car.add(frontWheel);
const backWheel = Wheel(-18); car.add(backWheel);
return car;}
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Car(initialTileIndex, direction, color) { const car = new THREE.Group(); car.position.x = initialTileIndex * tileSize; if (!direction) car.rotation.z = Math.PI;
const main = new THREE.Mesh( new THREE.BoxGeometry(60, 30, 15), new THREE.MeshLambertMaterial({ color, flatShading: true }) ); main.position.z = 12; main.castShadow = true; main.receiveShadow = true; car.add(main);
const cabin = new THREE.Mesh( new THREE.BoxGeometry(33, 24, 12), new THREE.MeshLambertMaterial({ color: "white", flatShading: true, }) ); cabin.position.x = -6; cabin.position.z = 25.5; cabin.castShadow = true; cabin.receiveShadow = true; car.add(cabin);
const frontWheel = Wheel(18); car.add(frontWheel);
const backWheel = Wheel(-18); car.add(backWheel);
return car;}
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Truck( initialTileIndex: number, direction: boolean, color: THREE.ColorRepresentation) { const truck = new THREE.Group(); truck.position.x = initialTileIndex * tileSize; if (!direction) truck.rotation.z = Math.PI;
const cargo = new THREE.Mesh( new THREE.BoxGeometry(70, 35, 35), new THREE.MeshLambertMaterial({ color: 0xb4c6fc, flatShading: true, }) ); cargo.position.x = -15; cargo.position.z = 25; cargo.castShadow = true; cargo.receiveShadow = true; truck.add(cargo);
const cabin = new THREE.Mesh( new THREE.BoxGeometry(30, 30, 30), new THREE.MeshLambertMaterial({ color, flatShading: true }) ); cabin.position.x = 35; cabin.position.z = 20; cabin.castShadow = true; cabin.receiveShadow = true; truck.add(cabin);
const frontWheel = Wheel(37); truck.add(frontWheel);
const middleWheel = Wheel(5); truck.add(middleWheel);
const backWheel = Wheel(-35); truck.add(backWheel);
return truck;}
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Truck(initialTileIndex, direction, color) { const truck = new THREE.Group(); truck.position.x = initialTileIndex * tileSize; if (!direction) truck.rotation.z = Math.PI;
const cargo = new THREE.Mesh( new THREE.BoxGeometry(70, 35, 35), new THREE.MeshLambertMaterial({ color: 0xb4c6fc, flatShading: true, }) ); cargo.position.x = -15; cargo.position.z = 25; cargo.castShadow = true; cargo.receiveShadow = true; truck.add(cargo);
const cabin = new THREE.Mesh( new THREE.BoxGeometry(30, 30, 30), new THREE.MeshLambertMaterial({ color, flatShading: true }) ); cabin.position.x = 35; cabin.position.z = 20; cabin.castShadow = true; cabin.receiveShadow = true; truck.add(cabin);
const frontWheel = Wheel(37); truck.add(frontWheel);
const middleWheel = Wheel(5); truck.add(middleWheel);
const backWheel = Wheel(-35); truck.add(backWheel);
return truck;}