Course outline
Build: The Display Stand
In one line
Replace the cube with the showroom's first part: a round display stand that turns slowly under the lights, in a page set up for the whole course.
Before you start
You will learn
- Set up the page the whole course builds on
- Place a stand whose top is the floor (y = 0) in metres
- Light a Standard material and cap the pixel ratio
You write code in
In your lumen-3d project: replace index.html and src/main.js, then run npm run dev
Files you touch
- index.html
- src/main.js
Think of it like this
A shop window is dressed in order. First the platform the product will stand on, then the lights, then the product. If the platform is wobbly or badly lit, nothing put on it later will look good. The stand is our platform.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- scene graph
- The tree of objects in a scene: each object can have children that move, turn and scale with it.
- pixel ratio
- How many real screen pixels make one CSS pixel. 1 on older screens, 2 on most phones and laptops, 3 on some phones.
- CylinderGeometry
- A three.js shape: a cylinder with a top radius, a bottom radius, a height and a number of segments around it.
- HemisphereLight
- A soft light that colours surfaces facing up with one colour (the sky) and those facing down with another (the ground).
- DirectionalLight
- Light that comes from one direction with parallel rays, like the sun; its position sets where it shines from.
Step by step
01The page: index.html
The page holds one empty <div id="app"> that covers the whole window (position: fixed; inset: 0), a dark background so the first frame doesn't flash white, and one script: /src/main.js as a module. canvas { display: block; } removes the small gap under the canvas that browsers add to inline elements.
02The camera looks down at the stage
The camera stands 4 m in front of the stand at eye height, 1.6 m, and looks at a point 0.3 m above the floor: camera.position.set(0, 1.6, 4) then camera.lookAt(0, 0.3, 0). The 45° field of view is the angle the camera sees from the top of the picture to the bottom. A moderate angle like this looks natural; a very wide one (say 100°) stretches the edges of the picture, and a very narrow one flattens depth. Phase 7 explains why.
03The stand and its mark
new THREE.CylinderGeometry(1, 1, 0.06, 64) is a flat disc 2 m across and 6 cm tall, with 64 segments so its edge looks round. It is named "stand" (names make objects easy to find in the scene tree and in tests) and lowered by half its height so its top is at y = 0.
The orange mark is a small box 0.9 m from the centre, sitting on the top surface. As a child of the stand, its position is measured from the stand's centre, not from the world's.
04Two lights
A HemisphereLight gives soft light from above (white) and below (a dark blue-grey), so no surface is pitch black. A DirectionalLight from up and to the right (2, 4, 3) gives the stand a lit side and a darker side, which is what makes it look solid.
05The loop and the resize
The loop sets stand.rotation.y = time / 4000: one full turn (2π radians) takes about 25 seconds, slow enough to look calm. When the window size changes, the camera's aspect ratio is updated, updateProjectionMatrix() makes the camera use it, and the canvas is resized.
Try it yourself
- 1
Make it yours
Replace your
index.htmlandsrc/main.jswith the two files below, save, and look at the dev server's page. You should see a light grey stand turning slowly with its orange mark. Resize the window: the stand stays centred and sharp. - 2
Check the floor
In the live example, add
console.log(new THREE.Box3().setFromObject(stand).max.y);just before step 7. It prints0.019999999776482584: the box includes the mark, whose top is 2 cm above the floor. Comment outstand.add(mark);and run again: it prints-6.705522526129215e-10.That second number is 0, written the way computers store decimals: −0.00000000067. The stand's corner points are stored as 32-bit floating-point numbers (that is what the GPU reads), and 0.03 can't be stored exactly in 32 bits, so the top comes out a hair off 0. Tiny errors like this are normal. Never compare positions with
===; check that they are close (the exercises use a tolerance of 0.001).
Code & diagrams
This is the exact src/main.js below, with the page's <div id="app">. Open the scene tree: the mark is inside the stand.
Console
Run the scene to see its console.Your 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.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lumen Bikes · 3D showroom</title>
<style>
html, body { margin: 0; height: 100%; background: #0b1118; }
#app { position: fixed; inset: 0; }
canvas { display: block; }
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
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
Size the canvas from a div with no height
In the live example's CSS, change #app { position: fixed; inset: 0; } to #app { }. To see why, add this last line to the JavaScript: console.log('canvas size:', renderer.domElement.width, 'x', renderer.domElement.height, '| aspect:', camera.aspect);
Myth vs fact
Myth
three.js works in pixels, like CSS.
Fact
three.js has no units at all. You decide what 1 means; this course uses metres. Pixels only appear at the very end, when the renderer draws into the canvas.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The stand's top is at y = 0 because geometry is built around its own centre: a cylinder of height 0.06 runs from −0.03 to +0.03, so moving it down by 0.03 puts its top at 0. Phase 4 shows how to shift a geometry itself (
geometry.translate) so the object's own origin sits on its base. - ▸
renderer.setAnimationLoopkeeps drawing even when nothing changes. For a showroom that is still most of the time, rendering only when something moves saves a lot of battery; Phase 3 shows render on demand.
Remember this
- 1
The problem this solves: the cube proved the tools work. Now the project needs its real starting point: a page with a full-screen 3D area, a camera that looks down slightly at the stage like a shopper would, light that a Standard material can use, and a stand whose top is exactly at height 0, so every part you add later sits on the floor without guessing.
- 2
Units: one unit is one metre. three.js has no built-in unit; you choose. This course uses metres, because real bikes are measured in them (the LB-01's wheels are 0.66 m across). The stand is 2 m wide (radius 1) and 6 cm tall. Its centre is at y = −0.03, so its top surface is at y = 0, the showroom floor.
- 3
Size from the element, not the window. The canvas goes inside
<div id="app">, and the code readsapp.clientWidthandapp.clientHeight. Today the div fills the window; in later phases a panel takes part of the screen, and the canvas must fit its own box. Phase 3 replaces the windowresizelistener with aResizeObserver, which notices when the box itself changes size. - 4
The pixel ratio cap.
setPixelRatio(Math.min(window.devicePixelRatio, 2))draws at the screen's real sharpness (2 on most phones and laptops) but never more than 2. A phone with a ratio of 3 would otherwise draw 9 times the pixels of ratio 1 for very little visible gain. Phase 17 measures what this saves. - 5
A child that turns with its parent. The small orange mark is added to the stand (
stand.add(mark)), not to the scene. When the stand turns, the mark turns with it, around the stand's centre. That is the scene graph, the idea that objects can carry other objects, and Phase 3 builds the whole bike on it.
Explain it without notes
Why is the stand moved down by 0.03 instead of left at y = 0?
Why does the orange mark turn with the stand without any code of its own?
Practice
Make the stand turn twice as fast and give it 6 segments instead of 64. What does it look like now?
Add a second mark on the opposite side of the rim, in Lumen's blue #2a9df4.
Done when you can
Your lumen-3d project shows the turning stand from your own dev server
You can explain the camera position, the stand's y = −0.03 and the pixel-ratio cap
You can say why the mark is a child of the stand
Your files match the two files of this topic