Shadows are Framebuffers and Shadows's shadow map, switched on in four places: renderer.shadowMap.enabled, the light's castShadow, and each mesh's castShadow and receiveShadow. light.shadow holds the map size, the shadow camera (keep it tight for resolution) and the bias and normalBias that cure acne (Acne and Bias). enableShadows() in bookshelf.js sets all of these for the stage's sun:
<script src="three-map.js"></script>
<script type="module">
import * as THREE from 'three';
import * as BN from './bookshelf.js';
const stage = BN.createStage({ width: 640, height: 190 });
const wall = new THREE.Mesh(new THREE.PlaneGeometry(20, 8),
new THREE.MeshStandardMaterial({ color: '#fff3e0' }));
wall.position.z = -0.45;
stage.scene.add(BN.createShelf(), wall);
BN.enableShadows(stage); // shadowMap on, sun.castShadow, mapSize 1024, normalBias
BN.renderFrames(stage.renderer, stage.scene, stage.camera);
</script>
shadowMap.type defaults to PCFShadowMap, which is soft since r182; in r186 WebGLRenderer replaces the old PCFSoftShadowMap with it and logs a warning. VSMShadowMap blurs more, BasicShadowMap is hard-edged. A static scene can set shadowMap.autoUpdate = false and request needsUpdate only when something moves. For soft ambient light that varies across a room, a LightProbe stores irradiance as nine spherical-harmonic coefficients, which LightProbeGenerator.fromCubeRenderTarget() bakes from a cube camera's render.
<!doctype html>
<style>
body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
canvas { display: block; max-width: 100%; }
.ui { padding: 6px 8px; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div class="ui">normalBias <input type="range" id="bias" min="0" max="0.05" step="0.005" value="0.02">
mapSize <select id="size"><option>256</option><option>512</option><option selected>1024</option><option>2048</option></select>
<label><input type="checkbox" id="helper"> shadow camera</label></div>
<script type="module">
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 280);
renderer.shadowMap.enabled = true; // 1. the renderer
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 600 / 280, 0.1, 50);
camera.position.set(-0.2, 0.4, 3.4);
camera.lookAt(0, -0.1, 0);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 0.8));
const sun = new THREE.DirectionalLight('#ffffff', 3);
sun.position.set(-3, 1.6, 1.6);
sun.castShadow = true; // 2. the light
// Keep the orthographic shadow camera tight around the shelf for resolution
Object.assign(sun.shadow.camera, { left: -1.8, right: 1.8, top: 1.2, bottom: -1.2, near: 1, far: 8 });
scene.add(sun);
const helper = new THREE.CameraHelper(sun.shadow.camera);
helper.visible = false;
scene.add(helper);
const wall = new THREE.Mesh(new THREE.PlaneGeometry(8, 4), new THREE.MeshStandardMaterial({ color: '#fff3e0' }));
wall.position.z = -0.22;
wall.receiveShadow = true; // 4. receivers
scene.add(wall);
const plank = new THREE.Mesh(new THREE.BoxGeometry(3.2, 0.06, 0.4), new THREE.MeshStandardMaterial({ color: '#8d6e63' }));
plank.position.set(0, -0.52, 0);
plank.castShadow = plank.receiveShadow = true;
scene.add(plank);
['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'].forEach((color, i) => {
const book = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.98, 0.12), new THREE.MeshStandardMaterial({ color }));
book.position.set((i - 2.5) * 0.5, -0.0, 0);
book.rotation.y = i === 2 ? -0.6 : 0;
book.castShadow = book.receiveShadow = true; // 3. casters (and self-shadowing)
scene.add(book);
});
const $ = (id) => document.getElementById(id);
function update() {
sun.shadow.normalBias = +$('bias').value; // 0 shows shadow acne on lit faces
const size = +$('size').value;
if (sun.shadow.mapSize.x !== size) {
sun.shadow.mapSize.set(size, size);
sun.shadow.map?.dispose(); // a new size needs a new map
sun.shadow.map = null;
}
helper.visible = $('helper').checked;
helper.update();
renderer.render(scene, camera);
}
renderer.setAnimationLoop(update); // cheap scene: redraw every frame
</script>