Command Palette

Search for a command to run...

Hectal
Course outline
Topic 2.7Beginner ~22 min · step 7 of 7

Build: Placing the Frame Tubes

In one line

Write tubeBetween(from, to, radius, name), a function that puts a cylinder exactly between two points, and use it to place the Lumen bike's down tube and top tube. The showroom gets a real frame, and the numbers come from vectors, not from guessing.

0/7 · 0%

Before you start

You will learn

  • Write a function that puts a tube between two points
  • Use vectors, quaternions and the scene graph together
  • Add a new file to the project and import it

You write code in

In your lumen-3d project: add src/objects/bike.js, replace src/main.js (index.html does not change), then run npm run dev

Files you touch

  • src/objects/bike.js
  • src/main.js

Think of it like this

A builder joining two posts with a beam does not guess angles. They measure from one post to the other, cut the beam to that length, and fix it halfway between the posts, pointing from one to the other. tubeBetween does exactly that: measure, cut, centre, aim.

Words you'll meet

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

tubeBetween
The course's helper: tubeBetween(from, to, radius, name) returns a cylinder mesh whose two ends are exactly the points from and to.
setFromUnitVectors
quaternion.setFromUnitVectors(a, b) sets the quaternion to the shortest turn that takes the unit vector a onto the unit vector b.
ES module
A JavaScript file that exports functions and imports others. src/objects/bike.js exports createBike and main.js imports it.
support check
supportsWebGL2() from Phase 1: it asks the browser for a WebGL 2 context and shows the still image if there is none.

Step by step

01bike.js: the points and the material

POINTS holds the four frame points as Vector3 objects, in metres. The FRAME material is Lumen orange with a little metal in it. Both live at the top of the file so every tube shares them: one material means one shader and no repeated work.

02tubeBetween: measure, cut, centre, aim

along = to.clone().sub(from) is the arrow from one end to the other (clone, so to is not changed). along.length() is the height of the cylinder. The position is the average of the ends. quaternion.setFromUnitVectors(UP, along.normalize()) turns a y-up cylinder to point along the arrow.

Notice the order: the length is read before normalize() changes along. That is the in-place trap of Topic 2.2.

03createBike

A Group named "bike" gets two tubes. The down tube joins the bracket and the head tube's bottom (radius 0.022). The top tube joins the seat cluster and the head tube's top (radius 0.018). Later phases add more add(...) lines in this same function.

04main.js: import, add, turn together

import { createBike } from "./objects/bike.js"; brings the function in. The scene gets the stand and the bike, and the loop turns both by the same angle, so the bike seems to stand on the stand. supportsWebGL2() runs first, as in Phase 1.

Try it yourself

  1. 1

    Reverse the ends

    In the example, swap the two arguments of the down tube: tubeBetween(POINTS.headBottom, POINTS.bottomBracket, 0.022, "down-tube"). The console prints the same two lines (centre 0.235, 0.500, 0.000, length 0.6172), because the middle and the length do not care which end is which. The tube now points the other way, which only matters later, when something is attached to one end.

  2. 2

    Fatten the top tube

    Change the top tube's radius from 0.018 to 0.05. It becomes a visibly thick beam, centred at the same point. A bigger radius never changes the length or position.

Code & diagrams

Build: the frame tubes (the same bike.js and main.js) New tab

This is src/objects/bike.js and src/main.js from the files below, joined into one example (local imports cannot run in the preview). The two console lines are the tubes' real centres and lengths. Open the scene tree: the bike group holds down-tube and top-tube.

idle

Console

Run the scene to see its console.

Expected console output

down-tube at 0.235, 0.500, 0.000, length 0.6172
top-tube at 0.170, 0.830, 0.000, length 0.5404

Your project files

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

src/objects/bike.jsjs

The whole file after this topic. If yours differs, you can copy this one and carry on.

import * as THREE from "three";

// Points of the Lumen frame, in metres (side view in the x-y plane, z across the bike, +x is the front).
export const POINTS = {
  bottomBracket: new THREE.Vector3(0, 0.3, 0),
  seatCluster: new THREE.Vector3(-0.1, 0.82, 0),
  headTop: new THREE.Vector3(0.44, 0.84, 0),
  headBottom: new THREE.Vector3(0.47, 0.7, 0),
};

const FRAME = new THREE.MeshStandardMaterial({ color: "#ff6b2c", metalness: 0.4, roughness: 0.35 });
const UP = new THREE.Vector3(0, 1, 0); // a CylinderGeometry stands along y

/** A tube (a thin cylinder) whose two ends are exactly `from` and `to`. */
export function tubeBetween(from, to, radius, name) {
  const along = to.clone().sub(from); // end minus start: the arrow from one end to the other
  const length = along.length();
  const tube = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, length, 16), FRAME);
  tube.name = name;
  tube.position.copy(from).add(to).multiplyScalar(0.5); // halfway between the ends
  tube.quaternion.setFromUnitVectors(UP, along.normalize()); // turn "up" onto the tube's direction
  return tube;
}

/** The Lumen bike so far: the down tube and the top tube. Wheels at y = 0, facing +x. */
export function createBike() {
  const bike = new THREE.Group();
  bike.name = "bike";
  bike.add(tubeBetween(POINTS.bottomBracket, POINTS.headBottom, 0.022, "down-tube"));
  bike.add(tubeBetween(POINTS.seatCluster, POINTS.headTop, 0.018, "top-tube"));
  return bike;
}

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

Pass arrays instead of vectors

In the example's createBike, change the down tube to tubeBetween([0, 0.3, 0], [0.47, 0.7, 0], 0.022, "down-tube").

terminal
$ Run
── what you'll see ──
Uncaught TypeError: to.clone is not a function

Break #2

Both ends the same

Change the down tube to tubeBetween(POINTS.bottomBracket, POINTS.bottomBracket, 0.022, "down-tube").

terminal
$ Run
── what you'll see ──
down-tube at 0.000, 0.300, 0.000, length 0.0000
top-tube at 0.170, 0.830, 0.000, length 0.5404

Myth vs fact

Myth

The tube needs its own rotation angle, worked out by hand.

Fact

Never. Give the function the two end points and it works out the position, the length and the direction itself. Later tubes (seat tube, stays, fork) are one line each.

Pro corner

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

  • ▸

    Each tube is its own mesh and draw call (Phase 17 counts them, and Phase 4 merges what never moves). With a shared material and 16 radial segments each tube is 64 triangles: 32 for the side (16 rectangles, 2 triangles each) plus 16 for each cap.

  • ▸

    setFromUnitVectors copes with two vectors that point exactly opposite ways by picking an axis at right angles to them, so a tube pointing exactly down (0, −1, 0) is still valid. A zero vector is not a direction at all: it gives no turn and no error, which is why the guard above matters.

Remember this

  1. 1

    The problem this solves: in Phase 1 the down tube was placed by hand, with a position and rotation.z = -0.8657 that you copied. A whole frame would need an angle worked out for every tube. With tubeBetween you give the two end points from the bike drawing and the function works out the rest. The showroom stops being a stand and starts being a bike.

  2. 2

    The four steps. 1. Subtract the start from the end: the arrow along (Topic 2.2). 2. Its length is the cylinder's height. 3. Put the cylinder halfway: (from + to) / 2. 4. A CylinderGeometry stands along y, so turn "up" onto the arrow's direction with quaternion.setFromUnitVectors(up, direction) (a quaternion, Topic 2.6). Every step is something this phase taught.

  3. 3

    The numbers. The bike is measured in metres, side view in the x-y plane, front at +x. The bottom bracket is (0, 0.3, 0), the bottom of the head tube is (0.47, 0.7, 0), the seat cluster is (−0.1, 0.82, 0) and the top of the head tube is (0.44, 0.84, 0). The down tube goes from the bracket to the bottom of the head tube: 0.6172 m long, centred at (0.235, 0.5, 0), exactly the numbers Phase 1 gave you. The top tube goes from the seat cluster to the top of the head tube: 0.5404 m long, centred at (0.17, 0.83, 0).

  4. 4

    **createBike() returns a group.** The function builds a THREE.Group named "bike" and adds the two tubes to it, named "down-tube" and "top-tube". Later phases add more parts to the same group, and exercises and tests find parts by these names. main.js adds the bike to the scene and turns it together with the stand.

  5. 5

    What the showroom can do now that it could not before: show a real frame shape, with tubes that meet at the points of the bike drawing. What is still missing: the seat tube (Phase 3), the rear triangle (Phase 4), the fork, wheels and everything else. The part this phase adds is the top tube; the down tube is rebuilt by tubeBetween in the same place. index.html, the #fallback markup and the support check stay exactly as Phase 1 left them: main.js only gains the import and the bike, and the stand keeps its small orange rim mark from Phase 0 so you can see it turn.

Explain it without notes

01

Walk through how tubeBetween turns two points into a positioned, aimed cylinder. Which Phase 2 idea does each step use?

02

What does createBike() add to the scene, and why is it a Group?

Practice

01

Use tubeBetween to add the seat tube from the bottom bracket (0, 0.3, 0) to the seat cluster (−0.1, 0.82, 0), radius 0.02, named "seat-tube". Predict its length.

02

Add a guard to tubeBetween that throws a clear error when the two points are closer than a micrometre.

Done when you can

  • Explain how a tube between two points gets its position, length and direction

  • Add a new tube to the bike with one line

  • Say what src/objects/bike.js exports and what main.js does with it

  • Say what the showroom can do now and which phase adds the next frame tube