Course outline
Build: The Chain Stays and Seat Stays
In one line
The bike gets the rear triangle of its frame: a pair of chain stays and a pair of seat stays, built from cylinders in bike.js, and the showroom now reports how many triangles the bike costs.
Before you start
You will learn
- Build the rear triangle from two pairs of cylinders
- Group paired parts under one name
- Report the bike's triangle count from code
You write code in
In your lumen-3d project: replace src/objects/bike.js, then run npm run dev
Files you touch
- src/objects/bike.js
Think of it like this
A frame builder joins the front triangle first, then adds the rear triangle: two thin tubes from the bottom bracket to the back axle, and two from the seat to the back axle. Until they are on, the back wheel has nothing to hang from. We add them in the same order.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- chain stay
- One of two thin tubes running from the bottom bracket (where the pedals turn) back to the rear axle, one on each side of the rear wheel.
- seat stay
- One of two thin tubes running from near the top of the seat tube down to the rear axle.
- rear triangle
- The back part of the frame: seat tube, chain stays and seat stays together.
- turntable
- A group that holds the stand and the bike, so turning it turns both.
Step by step
01Key points as constants
bike.js starts with STAY_Z = 0.055, and createBike() begins with the five key points as Vector3 values. The numbers are in metres and come from the bike's drawing. Every tube is placed from these points.
02tubePair makes a left and a right
tubePair creates a Group, loops over ["left", -STAY_Z] and ["right", STAY_Z], copies both end points with clone().setZ(z), and adds a named tube per side. The group is named chain-stays or seat-stays, so bike.getObjectByName("chain-stays") finds the pair.
03countTriangles walks the tree
object.traverse visits the object and everything inside it. For each mesh it adds index.count / 3, or position.count / 3 when there is no index. It works on a single tube, a pair, or the whole bike.
04createBike reports
At the end of createBike, a loop prints each part's triangles and then the total. The live example below runs the same file inside Phase 3's showroom.
Try it yourself
- 1
Make it yours
Replace your
src/objects/bike.jswith the file below and look at the dev server's page. You should see the orange frame with its rear triangle turning on the stand. Open the browser console: you should see five part lines andbike: 448 triangles. - 2
Halve the sides
In the live example change
CylinderGeometry(radius, radius, length, 16)to use8sides. The console now reports 224 triangles for the bike (32 per tube, 64 per pair) and the status bar shows 8 draw calls: halving the sides halves the triangles and does not change the draw calls.
Code & diagrams
This is src/objects/bike.js below, inlined into one example with Phase 3's showroom (the world modules are shrunk to a few lines because local imports cannot run here). Open the scene tree: turntable > stand, bike > five parts. The status bar shows 8 draw calls (the stand and seven tubes).
Console
Run the scene to see its console.Expected console output
down-tube 64 triangles
top-tube 64 triangles
seat-tube 64 triangles
chain-stays 128 triangles
seat-stays 128 triangles
bike: 448 trianglesYour 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";
const frameMaterial = new THREE.MeshStandardMaterial({ color: "#ff6b2c", metalness: 0.4, roughness: 0.35 });
const UP = new THREE.Vector3(0, 1, 0);
const STAY_Z = 0.055; // the stays come in pairs, one each side of the middle
// A cylinder between two points (from Phase 2).
export function tubeBetween(from, to, radius, name) {
const direction = new THREE.Vector3().subVectors(to, from);
const length = direction.length();
const tube = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, length, 16), frameMaterial);
tube.name = name;
tube.position.copy(from).add(to).multiplyScalar(0.5); // halfway between the two ends
tube.quaternion.setFromUnitVectors(UP, direction.normalize()); // turn the cylinder's axis onto the direction
return tube;
}
// A left and a right tube in one group, so a part with two sides is one named thing.
function tubePair(from, to, radius, name) {
const pair = new THREE.Group();
pair.name = name;
for (const [side, z] of [["left", -STAY_Z], ["right", STAY_Z]]) {
pair.add(tubeBetween(from.clone().setZ(z), to.clone().setZ(z), radius, name + "-" + side));
}
return pair;
}
// How many triangles an object (and everything inside it) sends to the GPU.
export function countTriangles(object) {
let triangles = 0;
object.traverse((o) => {
if (o.isMesh) triangles += o.geometry.index ? o.geometry.index.count / 3 : o.geometry.attributes.position.count / 3;
});
return triangles;
}
export function createBike() {
const BB = new THREE.Vector3(0, 0.3, 0); // bottom bracket
const rearAxle = new THREE.Vector3(-0.55, 0.35, 0);
const seatCluster = new THREE.Vector3(-0.1, 0.82, 0);
const headTop = new THREE.Vector3(0.44, 0.84, 0);
const headBottom = new THREE.Vector3(0.47, 0.7, 0);
const bike = new THREE.Group();
bike.name = "bike";
bike.add(
tubeBetween(BB, headBottom, 0.022, "down-tube"),
tubeBetween(seatCluster, headTop, 0.018, "top-tube"),
tubeBetween(BB, seatCluster, 0.02, "seat-tube"),
tubePair(BB, rearAxle, 0.013, "chain-stays"), // new in Phase 4
tubePair(seatCluster, rearAxle, 0.012, "seat-stays"), // new in Phase 4
);
for (const part of bike.children) console.log(part.name.padEnd(12), countTriangles(part), "triangles");
console.log("bike:", countTriangles(bike), "triangles");
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
A stay to the wrong point
In the live example change the seat stays' second point from rearAxle to headBottom: tubePair(seatCluster, headBottom, 0.012, "seat-stays").
Myth vs fact
Myth
A bike with a few hundred triangles is certainly fast enough.
Fact
Probably, but cost depends on triangles, draw calls, materials and pixels together. Phase 17 measures the whole scene on a real phone.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Each tube makes its own
CylinderGeometry, so the frame uses 7 geometries and 7 draw calls. The two stays of a pair are the same length and could share one geometry. Merged (Topic 4.6) or instanced, the frame could be one or two draw calls. They stay separate so later phases can name and test each one.
Remember this
- 1
The problem this solves: Kofi's story question was "how many triangles is the bike, and will Lena's phone cope?" This build adds the missing rear frame and answers it with code:
countTriangles(object)adds up the triangles of every mesh inside a group, andcreateBike()prints the total. A budget you can print is a budget you can test later (Phase 17). - 2
What it adds.
src/objects/bike.jsgains two groups,chain-staysandseat-stays, each holding a left and a right cylinder built with the sametubeBetweenas the front triangle. A helpertubePair(from, to, radius, name)runs the loop overz = ±0.055. The bike now holds five named parts: down tube, top tube, seat tube, chain stays and seat stays. - 3
How it plugs in. Phase 3's
main.jsalready callscreateBike()and turns it with the stand on a turntable, so this phase changes one file,src/objects/bike.js. The stays appear by themselves, andcreateBike()prints each part's triangles and the total to the console. - 4
What the showroom can do now that it could not before. It has a complete frame: front triangle and rear triangle, every tube at the right place and size, grouped under names a test can find. It can say how much geometry it holds: 448 triangles for the frame, 7 draw calls. A cylinder costs 64 triangles at 16 sides whatever its length, so the frame is cheap.
- 5
What is still missing. The frame is only geometry with a flat paint colour; real materials and lights come in Phase 5, together with the fork. The wheels are Phase 6 (front) and Phase 7 (rear), the handlebar Phase 8. Nothing can be clicked or loaded yet.
Explain it without notes
Why is tubePair a function that returns a Group and not two separate bike.add lines?
Why do the stays copy the key points with clone()?
Practice
Add a console.log that prints each part's world position of its first child, using getWorldPosition.
Make the stays 20 percent thicker without editing two places.
Done when you can
Your bike.js builds the stays as two named groups of two tubes
Your page turns the stand and the bike together
The console prints
bike: 448 triangles