Course outline
Build: The Front Wheel
In one line
Add the front wheel to the bike: a tyre with a real tread (a colour map and a normal map, tiled 30 times round), a rim, a hub and spokes. Then give the showroom a studio environment so the metal shines.
Before you start
You will learn
- Give the tyre a tread from two texture files
- Light the metal with an HDR environment that has a fallback
- Add the front wheel to the bike
You write code in
In your lumen-3d project: copy the three asset files, replace or add the three code files, then run npm run dev
Files you touch
- public/data/3d/textures/rubber_tiles_diff_1k.jpg
- public/data/3d/textures/rubber_tiles_nor_gl_1k.jpg
- public/data/3d/hdr/studio_small_08_1k.hdr
- src/world/scene.js
- src/objects/bike.js
- src/main.js
Think of it like this
A bike shop fitting the first wheel to a bare frame. Until now the frame stood on the stand like a drawing. With one wheel in, it looks like a bike, and the shop lights finally have something to shine on.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- tread
- The pattern of blocks and grooves on a tyre's surface. Here it is a texture, not geometry.
- hub and spokes
- The hub is the wheel's centre on the axle. The spokes are thin rods from it to the rim.
- axle
- The line the wheel turns around. For the front wheel: (0.58, 0.35, 0), along z.
- ready promise
- A promise stored on the bike that resolves when its textures have loaded.
Step by step
01Copy the assets
Download the three files listed in the start strip from this site (for example https://hectal.in/data/3d/textures/rubber_tiles_diff_1k.jpg; the other two are …/textures/rubber_tiles_nor_gl_1k.jpg and …/hdr/studio_small_08_1k.hdr) into public/data/3d/ of your project, keeping the folder names. All three are from Poly Haven and are CC0 (public domain): no credit is required, though crediting is polite. Vite serves public/ at the site root, so the paths in the code work unchanged.
02Replace scene.js
createScene() is as before. loadEnvironment() is new: try the HDR, fall back to the built-in room, free the generator, set scene.environment.
03Replace bike.js
The file keeps the tubes and adds the tyre material, createWheel and the front wheel. Open the scene tree: bike now has front-wheel with a tyre, a rim, a hub and four spoke boxes.
04Update main.js
One new line after createLights(scene): loadEnvironment(scene, renderer);. It is not awaited: the stand starts at once and the reflections arrive a moment later.
05Run it
npm run dev. First the wheel is plain; a moment later the tread appears. In the live example the status bar shows the texture count: the lookup table, two tread textures and the environment.
Try it yourself
- 1
Stronger tread
In
createTyreMaterialaddmaterial.normalScale.set(2, 2);aftermaterial.normalMap = normal;. The grooves between the tread blocks look deeper. Above about 3 they look fake. - 2
Dim the studio
Set
scene.environmentIntensity = 0.05inloadEnvironment. In a screenshot the stand drops from bright to grey, the frame darkens a little, and the rim and spokes lose their shine and look like dull dark metal. The environment adds light to every Standard material, and metal depends on it most.
Code & diagrams
Local imports cannot run in the preview, so the changed files (scene.js, bike.js and a shortened main.js) are inline, each under a comment with its path; the renderer, camera, lights and stand are the Phase 3 versions shortened. Your project's lights.js has Phase 5's shadows, so your draw-call count is higher.
Console
Run the scene to see its console.Expected console output
front wheel at: 0.58, 0.35, 0 | tyre map repeat: 30, 2
colour map: srgb | normal map: ""
environment set: true | textures in GPU memory: 4
draw calls: 18 | triangles: 5104Your 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";
import { HDRLoader } from "three/addons/loaders/HDRLoader.js";
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
const HDR_URL = "/data/3d/hdr/studio_small_08_1k.hdr";
export function createScene() {
const scene = new THREE.Scene();
scene.background = new THREE.Color("#0b1118");
return scene;
}
// Light and reflections from an image. If the file cannot load, the built-in room is used, so metal never goes black.
export async function loadEnvironment(scene, renderer) {
const pmrem = new THREE.PMREMGenerator(renderer);
let environment;
try {
const hdr = await new HDRLoader().loadAsync(HDR_URL);
hdr.mapping = THREE.EquirectangularReflectionMapping;
environment = pmrem.fromEquirectangular(hdr).texture;
hdr.dispose(); // the 1024 x 512 original is not needed once prepared
} catch (error) {
console.warn("HDR environment failed, using RoomEnvironment:", error.message);
environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
}
pmrem.dispose(); // frees the generator's work buffers, not the environment it made
scene.environment = environment;
scene.environmentIntensity = 0.4; // the lights are still on: the image adds to them
return environment;
}
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 wrong texture path
In bike.js change TREAD to "/data/3d/textures/rubber_tiles_diff_2k.jpg" (no such file).
Myth vs fact
Myth
The tread is modelled in the geometry.
Fact
The tyre is a smooth torus. The tread is a picture lit as if it were bumpy; the outline stays a perfect ring.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
bike.userData.readyis a design choice: the creator stays synchronous, so the scene can start at once, and still tells the caller when the extras are in. Phase 10 turns it into a visible loading state; Phase 18 shows the poster until it resolves.
Remember this
- 1
What this phase solves (Ama's problem). The showroom's tubes were right but flat. Ama wanted rubber with a tread, and metal that shines. Lena's phone needed a known memory cost. Today all three are answered by two textures, one HDR file and a front wheel.
- 2
What it adds. In
src/objects/bike.js:createTyreMaterial()(a Standard material with the rubber colour and normal maps, tiled 30 × 2),createWheel(name, x, tyreMaterial)(tyre, rim, hub, eight spokes) and the front wheel, a group named"front-wheel"at the front axle (0.58, 0.35, 0), whose tyre mesh is"front-wheel-tyre". Insrc/world/scene.js:loadEnvironment(scene, renderer), which prepares the studio HDR withPMREMGeneratorand falls back toRoomEnvironment.src/main.jscalls it. - 3
How it plugs in. This build assumes the files from Phases 4 and 5: the chain stays and seat stays are pairs of tubes at z = ±0.055, the fork is a group named
fork(ahead-tubeand thefork-legspair down to the front axle), andsrc/world/lights.jshas the shadow-casting key light. The file keeps Phase 5'scountTriangles()and the loop that makes every mesh cast a shadow, so the new wheel casts one too. One change from Phase 5: the frame paint goes back tometalness: 0.4, because there is an environment to reflect now.lights.jsand the renderer are untouched. The environment adds to the lights, soenvironmentIntensityis 0.4; with shadows on, lower it further if the floor looks washed out. - 4
Loading and failure.
createBike()returns at once with plain dark rubber; the textures arrive a moment later and the tyre changes to tread (bike.userData.readyis a promise for that moment, used by the loading state in Phase 10). If a texture fails, a console warning appears and the tyre stays plain rubber. If the HDR fails, the built-in room is used. Memory: two 1024 textures are 10.7 MiB with mipmaps, and the environment adds a prepared map. - 5
What the showroom can do now: it has a wheel with a visible tread and shining spokes; chrome parts reflect a studio; a failed asset does not break it. Still missing: the rear wheel (Phase 7), controls to look around (Phase 7), picking (Phase 8), a visible loading state (Phase 10) and compressed textures (Topic 10.5).
Explain it without notes
Why does createBike() not await the textures?
Why does the build use a try / catch with RoomEnvironment instead of letting a failed HDR stop the app?
Practice
Add the rear wheel to createBike() using the existing helpers. Which line changes, and what name does it get?
How would you share one tyre material between two wheels, and what is the saving?
Done when you can
Add textured, lit parts to the bike and name them
Load assets without blocking the start, and fail softly
Set an environment with a fallback and balance its intensity
Say what this phase added and what the next phases still need