Command Palette

Search for a command to run...

Hectal
Course outline
Topic 4.2Beginner ~23 min · step 2 of 7

Build the Bike from Shapes

In one line

A bicycle is tubes, rings and a few blobs. You build the whole Lumen frame from cylinders, tori and a capsule, aim each tube between two points, group paired parts, and count what each part costs.

0/7 · 0%

Think of it like this

A child builds a toy bike from a construction kit: straws for the frame, two rings for wheels, a bead for the seat. Nothing is carved. Every part is a simple shape, put in the right place at the right angle. Our bike is the same, with numbers in place of glue.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

unit vector
An arrow of length exactly 1. vector.normalize() makes one. Many rotation functions need it.
quaternion
A way to store a rotation as four numbers. setFromUnitVectors(a, b) makes the rotation that turns arrow a onto arrow b.
lookAt
Turns an object so its +z axis points at a point you give. Cameras are the exception: they look down −z.
Group
An object with no shape of its own that holds other objects, so they move, turn and are named together.

Step by step

01Name the key points

The frame is described by six points in metres, the numbers from the bike's drawing: the bottom bracket (where the pedals turn), the two axles, the seat cluster and the top and bottom of the head tube. Naming them in one object P means a number is typed once and read everywhere.

02Write tubeBetween once

tubeBetween(from, to, radius, name) does the three jobs: length from the distance, position halfway, turn from setFromUnitVectors. The direction is measured first (to − from), its length used, and only then normalised. Every tube on the bike is one call.

03Pairs in a loop

for (const z of [-0.055, 0.055]) runs twice. P.bottomBracket.clone().setZ(z) copies the point (never change P itself) and moves the copy to one side. The stay is then a tube between two copied points.

04Read the bill

traverse visits every object in the tree. The script adds up each mesh's triangles by name and prints them: the tubes are 64 each, the wheels 768 each. The renderer's own line then confirms 14 draw calls: 13 bike parts and the stand.

Try it yourself

  1. 1

    Make the wheels cheap

    In the bike example change TorusGeometry(0.33, 0.022, 8, 48) to TorusGeometry(0.33, 0.022, 4, 24). The status bar now says 1480 triangles in the same 14 draw calls: each wheel drops from 768 to 192 triangles, and the tyres look slightly angular close up.

  2. 2

    Forget the pair

    Change all four setZ(z) calls in the stays loop to setZ(0). The frame looks the same, because each pair now sits exactly on top of itself. The console still says 14 draw calls and 2632 triangles: the counters cannot see an overlap.

Code & diagrams

The Lumen bike from simple shapes New tab

Open the scene tree: the bike is a Group holding named meshes. The status bar shows 14 draw calls, one per mesh. This is the geometry only; paint and the finished parts arrive in later phases.

idle

Console

Run the scene to see its console.

Expected console output

down tube       64 triangles
top tube        64 triangles
seat tube       64 triangles
fork            64 triangles
chain stay     128 triangles
seat stay      128 triangles
rear wheel     768 triangles
front wheel    768 triangles
seat post       64 triangles
saddle         216 triangles
handlebar       48 triangles
drawn: 14 draw calls, 2632 triangles
Two ways to aim a tube between two points New tab

Orange: quaternion. Blue (see-through, fatter): lookAt on a geometry laid along z. They end up in the same place, so the orange tube sits inside the blue one.

idle

Console

Run the scene to see its console.

Expected console output

bottom bracket to head tube
  quaternion: (0.00, 0.30, 0.00) -> (0.47, 0.70, 0.00)
  lookAt:     (0.00, 0.30, 0.00) -> (0.47, 0.70, 0.00)
straight up
  quaternion: (0.00, 0.00, 0.00) -> (0.00, 1.00, 0.00)
  lookAt:     (0.00, 0.00, 0.00) -> (0.00, 1.00, 0.00)
straight down
  quaternion: (0.00, 1.00, 0.00) -> (0.00, 0.00, 0.00)
  lookAt:     (0.00, 1.00, 0.00) -> (0.00, 0.00, 0.00)
sideways, along z
  quaternion: (0.00, 0.50, -0.50) -> (0.00, 0.50, 0.50)
  lookAt:     (0.00, 0.50, -0.50) -> (0.00, 0.50, 0.50)
same point: length 0 - quaternion 0, 0, 0, 1

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

Skip normalize()

In the aiming example change to.clone().sub(from).normalize() to to.clone().sub(from).

terminal
$ Run
── what you'll see ──
bottom bracket to head tube
quaternion: (0.05, 0.25, 0.00) -> (0.42, 0.75, 0.00)
lookAt: (0.00, 0.30, 0.00) -> (0.47, 0.70, 0.00)
straight up
quaternion: (0.00, 0.00, 0.00) -> (0.00, 1.00, 0.00)
...
Only the first case (the slanted one) changes. The first line is wrong; the other three look right.

Myth vs fact

Myth

A tube needs a special tube geometry.

Fact

A cylinder is a tube. TubeGeometry follows a curve, which this bike does not need yet; straight parts are cylinders, placed with two points.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    A zero-length tube (the same point twice) produces a cylinder of height 0 and a rotation that is the identity (0, 0, 0, 1): nothing throws, nothing shows. If a part vanishes, print from.distanceTo(to) for it.

  • ▸

    Paired parts could share one geometry: both stays of a pair have the same length and radius, so new CylinderGeometry once and two Mesh objects would upload it once. Here each has its own, for clarity.

Remember this

  1. 1

    The problem this solves: the shapes from Topic 4.1 all sit at the origin, pointing up or along an axis. A bike frame needs a tube that runs from one exact point to another at a slant. The skill of this topic is placing a simple shape so its two ends land exactly on two points.

  2. 2

    Tubes from cylinders. A CylinderGeometry(r, r, length, 16) is centred on its own origin and runs along its y axis. To join point a to point b: make the length a.distanceTo(b), put the cylinder halfway ((a + b) / 2), and turn it so its y axis points along b − a. quaternion.setFromUnitVectors(from, to) builds exactly that turn. It needs both vectors to be unit vectors (length 1), so call normalize() on the direction first. (Recall: Phase 2's tubeBetween.)

  3. 3

    **lookAt is the other way.** object.lookAt(point) turns an object so its +z axis points at the point (a camera points the other way, down −z). A cylinder lies along y, so you first turn the geometry (geometry.rotateX(Math.PI / 2)) to lay it along z. The second example proves both ways land the ends on the same points.

  4. 4

    Wheels from tori. A TorusGeometry(0.33, 0.022, 8, 48) is a ring of radius 0.33 (the wheel) whose tube is 0.022 thick (the tyre), 8 segments round the tube and 48 round the ring. It stands upright in the x-y plane, so its axle runs along z: the wheel needs no turning. Placed at an axle point, its lowest point is 0.35 − 0.33 − 0.022 ≈ 0, on the stand's top.

  5. 5

    Count the cost. Every tube costs 64 triangles at 16 sides. A tyre costs 768. The saddle capsule costs 216. The two wheels are more than half of all the triangles in the scene. When it needs to be cheaper, look at the wheels first, and the status bar shows what each change saves.

Explain it without notes

01

How do you place a cylinder so its two ends are exactly on two points?

02

Why are P.bottomBracket.clone().setZ(z) and not P.bottomBracket.setZ(z) used for the stays?

Practice

01

Add a kickstand tube from the bottom bracket to (−0.2, 0, 0) with radius 0.012 and print how many triangles it adds.

02

Make tubeBetween refuse a zero-length tube: it should throw a helpful error naming the tube.

Done when you can

  • Place a cylinder between two points with setFromUnitVectors and check its ends

  • Say why normalize() is needed and what goes wrong without it

  • Build a pair of parts in a named Group