Command Palette

Search for a command to run...

Hectal
Course outline
Topic 6.7Intermediate ~24 min · step 7 of 7

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.

0/7 · 0%

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

    Stronger tread

    In createTyreMaterial add material.normalScale.set(2, 2); after material.normalMap = normal;. The grooves between the tread blocks look deeper. Above about 3 they look fake.

  2. 2

    Dim the studio

    Set scene.environmentIntensity = 0.05 in loadEnvironment. 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

Build: the showroom after Phase 6 (changed files in one example) New tab

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.

idle

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: 5104

Your project files

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

src/world/scene.jsjs

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

terminal
$ npm run dev (browser console)
── what you'll see ──
Tread textures failed, keeping plain rubber: error
The tyre stays dark grey, with no tread and no crash in your project. The browser's network panel shows the 404 for that file. (In the inline example on this page, the log lines at the end read `tyre.material.map.repeat` and then throw a TypeError, because there is no map. That is the example's own check, not the showroom.)

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.ready is 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. 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. 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". In src/world/scene.js: loadEnvironment(scene, renderer), which prepares the studio HDR with PMREMGenerator and falls back to RoomEnvironment. src/main.js calls it.

  3. 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 (a head-tube and the fork-legs pair down to the front axle), and src/world/lights.js has the shadow-casting key light. The file keeps Phase 5's countTriangles() 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 to metalness: 0.4, because there is an environment to reflect now. lights.js and the renderer are untouched. The environment adds to the lights, so environmentIntensity is 0.4; with shadows on, lower it further if the floor looks washed out.

  4. 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.ready is 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. 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

01

Why does createBike() not await the textures?

02

Why does the build use a try / catch with RoomEnvironment instead of letting a failed HDR stop the app?

Practice

01

Add the rear wheel to createBike() using the existing helpers. Which line changes, and what name does it get?

02

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