Three.js Scroll Camera Path — GSAP ScrollTrigger WebGL Flythrough

Three.js Scroll Camera Path · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Camera-moves-through-fixed-gallery: shapes stay put, the camera travels — matching a real dolly shot
Plain-object scrub target: GSAP tweens a small {z, lookZ} object, manually applied to the camera each frame
Shifting look target: camera.lookAt() uses a target that moves ahead of the camera, not a fixed static axis
Smoothed scrub (0.6): camera motion glides toward scroll position instead of snapping with zero latency
Pinned stage with extended scroll room: +=400% gives five shapes each a clear, unhurried moment
Independent per-shape rotation: every shape spins on its own clock, decoupled from scroll position entirely
Fully reversible: scrolling back up replays the exact same flythrough backward, no extra code required
Combines GSAP ScrollTrigger with Three.js: each library handles exactly the one job it's built for

About this UI Snippet

How to Build a Scroll-Scrubbed Three.js Camera Flythrough With GSAP

Screenshot of the Three.js Scroll Camera Path snippet rendered live

The Three.js Scroll Camera Path snippet pins a WebGL scene in place and flies the camera through a small gallery of five distinct 3D shapes as the visitor scrolls — with the scrollbar directly controlling camera position, not a timer — combining GSAP's ScrollTrigger plugin with core Three.js, both loaded from a CDN.

The camera moves, not the shapes

It would be tempting to animate each shape's own position as the visitor scrolls past it, but this snippet does the opposite: every shape sits at a fixed Z position along one axis, placed once at startup, and it's the camera that travels through that fixed gallery. This mirrors how a real flythrough or dolly shot works, and it means adding a sixth or seventh shape to the gallery requires no changes to the animation logic at all — just another entry in the shape list at whatever Z depth you choose.

A plain object as the tweened value, not the camera directly

Rather than handing GSAP the camera object to tween directly, the snippet tweens a tiny plain object — { z, lookZ } — with GSAP's ScrollTrigger scrub, and then, every single animation frame, manually copies those two numbers onto the camera's actual position and look-target. This indirection matters: it decouples "what GSAP is scrubbing" from "how Three.js renders it," which means the same scrubbed values could just as easily drive a camera path with curves, banking, or multiple axes without changing how the ScrollTrigger timeline itself is defined.

The look target moves too, not just the camera

If only the camera's Z position moved while it kept staring straight down a fixed axis, the flythrough would feel mechanical and flat. Instead, a separate lookZ value — always kept a fixed distance ahead of the camera's own Z — is tweened in lockstep, and camera.lookAt() is called every frame with that shifting target. This is what gives the flythrough a sense of "heading toward" the next shape rather than sliding sideways past a static viewing direction.

scrub: 0.6, not scrub: true

Unlike a typing effect where the scrubbed value must snap exactly to scroll position, camera motion benefits from a small amount of smoothing. scrub: 0.6 tells ScrollTrigger to let the tweened values glide toward the current scroll position over roughly six-tenths of a second rather than matching it with zero latency, which smooths out the inevitable jitter of real trackpad and mouse-wheel input into something that reads as a deliberate, cinematic camera move rather than a jerky, frame-perfect scrollbar-slave.

A pinned stage, +=400% of extra scroll distance

The #scpStage section is pinned in place for +=400% of the viewport's height worth of additional scrolling — enough room for five distinct shapes to each get a clear, unhurried moment as the camera passes through their vicinity. Widening that value stretches the whole flythrough across more scroll distance, giving each shape more screen time; narrowing it compresses the whole trip into a quicker scroll.

Each shape spins independently, at its own rate

While the camera trip itself is precisely scroll-driven, every individual shape's own rotation runs on the regular requestAnimationFrame clock, completely independent of scroll position, each at a slightly different speed. This keeps the gallery feeling alive even while the visitor pauses scrolling mid-flight — the camera holds still, but the shapes keep turning.

Why combine two libraries instead of one

Three.js has no built-in concept of "scroll," and GSAP's ScrollTrigger has no concept of a 3D scene — each library does exactly one job well. This is the same combination used by the scroll typewriter snippet, just applied to camera position instead of character opacity, and it's a useful pattern to internalize: any property that can be represented as a plain number or object can be scrubbed by ScrollTrigger, then applied manually to whatever rendering system — DOM, Canvas 2D, or WebGL — actually needs it. Pair this flythrough with a starfield warp as a "before" scene, or contrast its cinematic pacing against the physically-orbiting motion of orbit rings.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not have to piece together how GSAP and Three.js hand off values to each other by reading both libraries' docs separately. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the timeline tweens a plain {z, lookZ} object instead of the camera itself, or why the look target needs its own tweened value rather than the camera always facing a fixed direction. The same assistant can help optimize it, for instance checking whether the per-frame camera.lookAt() call could be replaced with a cheaper quaternion slerp for very long galleries, or whether shape geometries far from the current camera position could be temporarily hidden to save render cost. It is also useful for extending the flythrough: ask it to add a curved (not perfectly straight) camera path using a CatmullRomCurve3, fade each shape's opacity in and out as the camera approaches and passes it, or synchronize a text caption per shape that appears exactly when the camera is nearest to it. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll-scrubbed camera flythrough" in plain HTML, CSS, and JavaScript using Three.js, GSAP, and GSAP's ScrollTrigger plugin, all loaded from a CDN (no bundler, no build step).

Requirements:
- A pinned section containing a full-size canvas, with a WebGLRenderer and PerspectiveCamera sized to match it and updated on window resize including camera aspect ratio.
- At least five distinct 3D shapes (different geometries, e.g. an icosahedron, a torus, a box, an octahedron, and a torus knot), each placed once at a fixed, increasingly negative Z position along one axis so they form a sequence a camera could travel through, with a small randomized X/Y offset per shape.
- Do not animate the shapes' positions based on scroll — only the camera should move through the fixed gallery.
- Register a GSAP timeline on a ScrollTrigger targeting the pinned section, with pin: true, a start at the top of the viewport, a numeric scrub value around 0.5 to 0.7 (not scrub: true) for smoothed rather than zero-latency motion, and an end several hundred percent of the viewport tall.
- Inside that timeline, tween a plain JavaScript object (not the camera itself) containing at least a camera-Z value and a separate look-target-Z value, animating both from their starting values down to values just past the furthest shape's position.
- Every animation frame (using requestAnimationFrame, independent of the ScrollTrigger callback), copy the tweened object's current camera-Z value onto the actual camera's Z position, set a look-target vector's Z coordinate from the tweened look-target value, and call the camera's look-at method with that target.
- Independently of the scroll-driven camera motion, continuously rotate each shape on its own axis at its own speed inside the same animation frame loop, so the shapes keep animating even while scrolling is paused.
- Confirm that scrolling back upward reverses the entire flythrough exactly, since the camera and look-target values are fully driven by the ScrollTrigger scrub rather than a one-way timer.

Step by step

How to Use

  1. 1
    Load all three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js from the CDN panel, in that order.
  2. 2
    Paste HTML, CSS, and JSFive shapes appear in a pinned 3D scene; scrolling begins the flythrough.
  3. 3
    Scroll down slowlyThe camera flies past each shape in sequence, tied directly to scroll position.
  4. 4
    Scroll back upThe flythrough reverses exactly, since the tweened camera values are fully scrubbed, not timer-based.
  5. 5
    Add or reposition shapesEdit the SHAPES array's z values and geometry types to change the gallery's contents and spacing.
  6. 6
    Stretch or compress the tripChange the ScrollTrigger end value (+=400%) for a longer, slower flythrough or a shorter, quicker one.

Real-world uses

Common Use Cases

Product feature flythroughs
Walk visitors through several product features or capabilities, one 3D shape or icon per scroll section.
Portfolio and case-study pages
A cinematic camera path through 3D work samples reads as far more crafted than a static image gallery.
Teaching ScrollTrigger plus WebGL
A focused example of scrubbing a plain object with GSAP and manually applying it to a Three.js camera.
Brand storytelling sections
Use each shape as a milestone or chapter marker in a longer scroll-driven brand or origin story.
Game world or level previews
Preview a game's environments or assets as a scroll-controlled flythrough before the visitor plays.
Space and sci-fi themed sites
Pair with a starfield warp intro section for a full space-travel scroll narrative.

Got questions?

Frequently Asked Questions

Keeping every shape at a fixed Z position and moving only the camera mirrors how a real flythrough or dolly shot works, and it means the gallery scales cleanly — adding a sixth shape only requires one new entry in the shape list, with no changes needed to the scroll or camera logic itself.

Tweening a small {z, lookZ} object and manually copying its values onto the camera every frame decouples what ScrollTrigger is scrubbing from how Three.js renders it. This makes it straightforward to later swap in a curved path, add banking rotation, or drive multiple cameras from the same scrubbed values without changing how the ScrollTrigger timeline itself is defined.

If the camera only translated forward while always looking down a fixed, static direction, the flythrough would feel mechanical, like sliding sideways past a wall. Moving the look target a fixed distance ahead of the camera's own position, and calling camera.lookAt() with it every frame, gives the camera a genuine sense of heading toward whatever comes next.

A numeric scrub value tells ScrollTrigger to let the tweened values glide toward the current scroll position over that many seconds, rather than matching it with zero latency. Camera movement benefits from this small smoothing — it filters out the jitter of real trackpad and wheel input into motion that reads as a deliberate cinematic move rather than a jerky, frame-perfect scrollbar-slave.

Yes. The ScrollTrigger end value (+=400%) controls how much scroll distance the whole trip spans — increase it to stretch the flythrough across more scrolling (giving each shape more relative screen time), or decrease it to compress the whole gallery into a shorter scroll.

Yes. Click JSX for a React component, Vue for a Vue 3 SFC, Angular for a standalone component, or Tailwind for a React + Tailwind CSS utility-class version. Set up the renderer, shapes, and GSAP timeline inside a mount effect against refs (not document queries), and revert a gsap.context (or manually kill the ScrollTrigger instance) plus call renderer.dispose() on cleanup so the pin and WebGL context don't linger after the component unmounts.