Development

Building a 3D hero with Three.js and GSAP ScrollTrigger

Building a 3D hero with Three.js and GSAP ScrollTrigger

The hero section of melement.ink isn't a video or a static image. It's a real-time 3D corridor rendered in WebGL — and it responds to your scroll position, driving the camera deeper into the scene as you explore.

Here's how we built it, what decisions we made, and the performance tricks that keep it running at 60fps on most devices.

The concept

We wanted something that communicated depth — literally. Melement is an agency built on layers (brand, web, motion, labs), and a corridor metaphor captures that idea of moving through space, discovering what's further in.

The scene needed to:

Tech stack

The corridor geometry

The corridor is generated programmatically. Sixteen "gates" are spaced 9 units apart along the Z-axis. Each gate consists of:

``javascript
for (let i = 0; i < GATES; i++) {
const z = -i * SPACING;
for (const sx of [-5, 5]) {
const slab = new THREE.Mesh(
new THREE.BoxGeometry(1, 7.6, 1.4), dark
);
slab.position.set(sx, 0.5, z);
corridor.add(slab);
}
}
`

Simple box geometry means the GPU has almost nothing to sweat over. The visual richness comes from lighting and post-processing, not polygon count.

Scroll-driven camera

GSAP's ScrollTrigger maps the hero section's scroll progress (0 → 1) to a target Z position. The camera interpolates toward that target each frame with a lerp factor of 0.08 — smooth enough to feel fluid, responsive enough to feel connected to your input.

`javascript
ScrollTrigger.create({
trigger: hero,
start: 'top top',
end: 'bottom bottom',
scrub: true,
onUpdate: (self) => { targetProg = self.progress; }
});

// In the render loop:
const targetZ = 6 + targetProg * (END - 6);
camZ += (targetZ - camZ) * 0.08;
camera.position.z = camZ;
`

Post-processing: the "film" look

Three passes run after the main render:

  1. UnrealBloomPass — Threshold 0.55, strength 0.85, radius 0.65. This makes the orange strips and edges glow without washing out the scene.
  1. Custom shader — Combines chromatic aberration (RGB channel offset that increases toward edges), a vignette (darken corners), and animated film grain.
  1. OutputPass — Tone mapping and colour space conversion.

The chromatic aberration also responds to scroll velocity — faster scrolling means more distortion, creating a subtle motion blur effect without actually blurring anything.

Performance budget

We set a hard rule: the scene must not drop below 55fps on a 2020 MacBook Air. To achieve this:

  • Pixel ratio capped at 2 (1.5 on mobile)
  • IntersectionObserver pauses rendering once the hero scrolls out of view
  • Particle count reduced on viewports under 760px (260 vs 520 dust particles)
  • No shadows — All lighting is direct and ambient; shadow maps would double the draw calls
  • Reduced motion — If the user prefers reduced motion, the entire scene is skipped and a static gradient fallback appears

Graceful degradation

If WebGL isn't available (old devices, aggressive privacy browsers), the body gets a no-gl class. CSS takes over with a radial gradient that captures the warm tone of the scene without any JavaScript.

`css
body.no-gl .hero {
height: 100vh;
background: radial-gradient(
120% 90% at 70% 30%,
rgba(252,142,0,.45), transparent 60%
), linear-gradient(160deg, var(--paper), var(--paper-2));
}
``

Lessons learned

  1. Post-processing is worth the budget. The bloom and aberration add 2–3ms per frame but transform the scene from "3D demo" to "cinematic."
  1. Scroll-linked animation needs interpolation. Direct binding feels janky. Lerping the camera position creates the illusion of physical mass.
  1. Always provide a fallback. Not everyone has WebGL, and that's fine. The site should still communicate the brand without it.
  1. Kill what you can't see. The IntersectionObserver pause is the single biggest performance win. Once the hero is offscreen, the GPU idles.

Ready to build something exceptional?

Whether it's a brand, a website, or a full digital product — we'd love to hear what you're working on.

Start a project