Course outline
Build: The Fork and Showroom Lighting
In one line
The showroom gets a key light that casts a real shadow, a soft fill, a tone-mapping setting that keeps Lumen orange true, and its sixth bike part, the fork. The bike now looks like painted metal and sits on its stand instead of floating.
Before you start
You will learn
- Light the showroom with a key and a fill
- Make a shadow with all the switches on
- Add the fork to the bike
You write code in
In your lumen-3d project: replace the four files, then run npm run dev
Files you touch
- src/world/lights.js
- src/world/renderer.js
- src/objects/showroom.js
- src/objects/bike.js
Think of it like this
A shop window at night looks dead until someone switches on the display lamps and aims them. The goods were always there. Light is what makes them look worth buying.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- key light
- The main light, the one that makes the shadow. Here it is the directional light named
key-light. - fill light
- A soft light from all around that keeps the shadow side from going black. Here it is the hemisphere light.
- head tube
- The short, wide tube at the front of the frame in which the fork's steering tube turns.
- fork
- The two legs that hold the front wheel's axle, joined to the head tube at the top.
Step by step
01lights.js: two lights
The fill is a hemisphere light (white sky, blue-grey ground, intensity 1.2). The key is a directional light at (2, 4, 3), intensity 2.4, which casts the shadow. Its shadow camera is a 3 m box, near 1 and far 10: the light is about 5.4 m from the origin, so the bike sits well inside the box.
02renderer.js: shadows and tone
Two new lines: renderer.shadowMap.enabled = true and renderer.toneMapping = THREE.NeutralToneMapping. The shadow switch must be set before the first frame (Topic 5.5).
03showroom.js and bike.js: who casts, who receives
The stand receives (receiveShadow). Every mesh of the bike casts: createBike() ends with bike.traverse(...) setting castShadow. The stand does not cast, so it saves 1 draw call in the shadow pass.
04bike.js: the fork
createFork() builds the head tube from the head-tube top (0.44, 0.84) to its bottom (0.47, 0.70), radius 0.026, and a pair of legs from the head-tube bottom to the front axle (0.58, 0.35), radius 0.014, using tubePair from Phase 4. The group is named fork.
05Run it
The live example is the project's files joined together. The first frame prints the bike's parts, the fork's parts, the triangle count, the draw calls and the shadow map size.
Try it yourself
- 1
Switch the shadow off
In the example set
key.castShadow = falseinlights.js. The first frame prints12 (shadow pass included)instead of22: no shadow pass, so 10 draw calls fewer. The shadow on the stand is gone. The last line then throwsUncaught TypeError: Cannot read properties of null (reading 'width'): with no shadow passkey.shadow.mapwas never created. - 2
Look at the fork
Open the scene tree and expand
bikethenfork: you seehead-tubeandfork-legs, which holdsfork-legs-leftandfork-legs-right. Three meshes make the fork, andcountTrianglescounts 64 triangles for each (a 16-sided cylinder): 192.
Code & diagrams
The exact code of the four project files below (import and export words removed: local imports cannot run in the preview), plus a few lines of scene, camera and turntable. In the scene tree the bike now holds a `fork` group.
Console
Run the scene to see its console.Expected console output
bike parts: down-tube, top-tube, seat-tube, chain-stays, seat-stays, fork
fork parts: head-tube, fork-legs
bike: 640 triangles
draw calls in the first frame: 22 (shadow pass included)
shadow map: 1024 x 1024Your project files
Your lumen-3d project after this topic. Click a file to see it in full.
The whole file after this topic. If yours differs, you can copy this one and carry on.
import * as THREE from "three";
// Two lights. The fill (a hemisphere light) keeps the shadow side from going black.
// The key (a directional light) is the "sun" of the showroom: it is the only light that casts a shadow.
export function createLights(scene) {
scene.add(new THREE.HemisphereLight("#ffffff", "#334455", 1.2));
const key = new THREE.DirectionalLight("#ffffff", 2.4);
key.name = "key-light";
key.position.set(2, 4, 3);
key.castShadow = true;
key.shadow.mapSize.set(1024, 1024); // one 1024 x 1024 depth picture, 4 MiB
// The shadow camera is a box around the light's view. It must hold the stand and the bike (about 3 m across), and nothing more.
Object.assign(key.shadow.camera, { left: -1.5, right: 1.5, top: 1.5, bottom: -1.5, near: 1, far: 10 });
key.shadow.camera.updateProjectionMatrix();
key.shadow.bias = -0.0005; // a little less strict than the depth picture, so a surface does not shadow itself
key.shadow.normalBias = 0.02; // and its depth is read a little way out along the surface's normal
scene.add(key);
return key;
}
Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A shadow that never appears
In renderer.js delete the line renderer.shadowMap.enabled = true;, keep everything else.
Myth vs fact
Myth
The shadow is a property of the object.
Fact
It is the result of three things working together: a light that casts, a mesh that casts, a surface that receives. If one is missing you get nothing.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Lumen's key light updates its shadow every frame, because the turntable moves the bike. If the bike stood still,
key.shadow.autoUpdate = falseand onekey.shadow.needsUpdate = truewould remove the 10 shadow draw calls from every later frame. - ▸
The fork is two groups of tubes today. Phase 10 replaces the shapes with a loaded model, and the names
fork,down-tubeand the rest stay, so tests and the scene tree keep working.
Remember this
- 1
The problem this solves: the story's complaint was a cardboard cut-out. After this build the bike has a key light that makes a shadow on the stand, a fill that keeps the shadow from going black, and a tone curve that keeps the orange honest. It also has its fork, so the frame is complete at the front.
- 2
What it adds.
src/world/lights.jsnow holds the two lights: aHemisphereLightfill and aDirectionalLightkey namedkey-light, with its shadow settings (1024 × 1024, a 3 m box,bias-0.0005,normalBias0.02).src/world/renderer.jsturns onshadowMapand setsNeutralToneMapping.src/objects/showroom.jssetsstand.receiveShadow = true.src/objects/bike.jsgainscreateFork()(a head tube and a pair of fork legs, in a group namedfork) and makes every bike mesh cast a shadow. Two tidy-ups: the frame's key points become top-level constants (the fork needsFRONT_AXLE) andcreateBike()stops printing triangle counts (Phase 4's console lines);countTrianglesstays exported and the example prints the total. The light intensities move from Phase 3's 1.5 and 2 to 1.2 and 2.4. - 3
The bike material. The bike's
paintis aMeshStandardMaterial. Its metalness drops from 0.4 to 0.1 and roughness rises to 0.4: until Phase 6 adds an environment, a strongly metallic frame would look dark (Topic 5.3). - 4
How it plugs in.
createLights(scene)keeps its Phase 3 signature, somain.jsdoes not change. We start from Phase 4's showroom and change only these four files. - 5
What the showroom can do now that it could not before: light the bike with a key and a fill, cast a shadow of the whole bike on the stand, keep the orange true under bright light and show the fork. The bike has 640 triangles and the first frame costs 22 draw calls: 12 to draw the scene plus 10 for the shadow map.
- 6
What is still missing. The wheels (Phase 6 front, Phase 7 rear). An environment image, so metal can reflect (Phase 6). Camera controls (Phase 7). The shadow falls on the stand only, because there is no floor yet: the stand is the whole ground.
Explain it without notes
List every change this build makes and the file it is in.
Why does the first frame print 22 draw calls when the bike has 10 meshes and the stand has 2?
Practice
Make the stand cast a shadow too, and predict the new draw-call count.
Move the key light to (-2, 4, 3). Which line changes, and does the shadow camera box still cover the bike?
Done when you can
Your showroom has a fill and a key light, and the stand shows the bike's shadow
renderer.shadowMap.enabledis true and tone mapping is Neutralbikeholds aforkwithhead-tubeandfork-legsYou can say what every changed file adds