I work on BIM software for steel structures, so I wanted the first thing visitors see on my portfolio to be a steel frame they can actually explore. This post walks through how it is built and the decisions that keep it fast.

Modelling members, not boxes

Columns and beams use a real I-section profile. I draw the outline once with a Three.js Shape (two flanges and a web), extrude it to a length of one unit along Z, and centre it. Every column and beam is then that one geometry, stretched along its own axis.

const s = new THREE.Shape();
s.moveTo(-b, -d); s.lineTo(b, -d); s.lineTo(b, -d + flange); /* … web and top flange … */
const geo = new THREE.ExtrudeGeometry(s, { depth: 1, bevelEnabled: false });
geo.translate(0, 0, -0.5);   // centre the unit-length member

Connections matter as much as members in steel. Each beam end gets a bolted end plate with eight hex bolts and fillet-weld beads along the web and flanges. Braces end in gusset plates, columns have splice plates every second storey, and base plates are held down by anchor bolts.

Thousands of parts, a handful of draw calls

The frame has about 4,500 parts (over 2,600 of them bolts). Drawing each as its own mesh would be thousands of draw calls. Instead every part type is one InstancedMesh: the GPU gets one geometry and a list of transforms. The whole structure renders in about a dozen draw calls.

  • Each part stores its final position, rotation and length.
  • A member’s transform is composed from those values, so a unit-length beam becomes a 4.4 m beam.
  • Parts are collected first so every InstancedMesh is created with an exact count.

The build animation

Every part also has a start delay based on its storey: columns first, then beams, end plates, bolts and welds, then bracing. Each frame, the part eases down into place from slightly above while scaling up from zero. The timeline uses real elapsed time, so the build finishes on schedule even on a slow phone.

Click to inspect

In explore mode the camera is driven by OrbitControls. Clicking a part casts a ray from the pointer; Three.js reports which InstancedMesh was hit and the instance index, and I look up that index in a parallel array of labels (grid reference, level, section, bolt count). A slightly enlarged translucent copy of the same geometry, placed with the instance’s matrix, makes the highlight.

Keeping it light

  • Three.js is loaded with a dynamic import only after the page has rendered, so it never delays the first paint.
  • OrbitControls is a separate 4 KB chunk that loads only when someone opens the viewer.
  • Rendering pauses when the hero is off-screen or the tab is hidden.
  • Visitors who ask their OS to reduce motion get the finished structure as a still frame.

The result is a hero section that says what I do better than a paragraph could, without costing visitors anything if they never touch it.