Command Palette

Search for a command to run...

Hectal
Course outline
Topic 5.7Intermediate ~23 min · step 7 of 7

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.

0/7 · 0%

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

    Switch the shadow off

    In the example set key.castShadow = false in lights.js. The first frame prints 12 (shadow pass included) instead of 22: no shadow pass, so 10 draw calls fewer. The shadow on the stand is gone. The last line then throws Uncaught TypeError: Cannot read properties of null (reading 'width'): with no shadow pass key.shadow.map was never created.

  2. 2

    Look at the fork

    Open the scene tree and expand bike then fork: you see head-tube and fork-legs, which holds fork-legs-left and fork-legs-right. Three meshes make the fork, and countTriangles counts 64 triangles for each (a 16-sided cylinder): 192.

Code & diagrams

Build: the lit showroom with its fork (the real project files, joined) New tab

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.

idle

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 1024

Your project files

Your lumen-3d project after this topic. Click a file to see it in full.

src/world/lights.jsjs

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.

terminal
$ Run
── what you'll see ──
draw calls in the first frame: 12 (shadow pass included)
[error] Uncaught TypeError: Cannot read properties of null (reading 'width')
No shadow map is drawn: 12 calls, not 22, and the stand has no shadow. The only error is the example's own last line reading the missing map; three.js itself says nothing, so in your own project you get no message at all.

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 = false and one key.shadow.needsUpdate = true would 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-tube and the rest stay, so tests and the scene tree keep working.

Remember this

  1. 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. 2

    What it adds. src/world/lights.js now holds the two lights: a HemisphereLight fill and a DirectionalLight key named key-light, with its shadow settings (1024 × 1024, a 3 m box, bias -0.0005, normalBias 0.02). src/world/renderer.js turns on shadowMap and sets NeutralToneMapping. src/objects/showroom.js sets stand.receiveShadow = true. src/objects/bike.js gains createFork() (a head tube and a pair of fork legs, in a group named fork) and makes every bike mesh cast a shadow. Two tidy-ups: the frame's key points become top-level constants (the fork needs FRONT_AXLE) and createBike() stops printing triangle counts (Phase 4's console lines); countTriangles stays 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. 3

    The bike material. The bike's paint is a MeshStandardMaterial. 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. 4

    How it plugs in. createLights(scene) keeps its Phase 3 signature, so main.js does not change. We start from Phase 4's showroom and change only these four files.

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

01

List every change this build makes and the file it is in.

02

Why does the first frame print 22 draw calls when the bike has 10 meshes and the stand has 2?

Practice

01

Make the stand cast a shadow too, and predict the new draw-call count.

02

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.enabled is true and tone mapping is Neutral

  • bike holds a fork with head-tube and fork-legs

  • You can say what every changed file adds