Scroll GLB Camera Flythrough — Free GSAP ScrollTrigger + Three.js glTF Tour

Scroll GLB Camera Flythrough · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Four-checkpoint camera path
Named position/look-at pairs interpolated live from scroll progress.
Real drag-to-override
OrbitControls dragging always takes immediate priority over the scroll path.
Explicit, deliberate handoff
A start/end-driven flag with a 900ms resume delay avoids an awkward snap.
Live stage label
Shows which named checkpoint the camera is currently nearest to.
Real detailed glTF model
Khronos' PBR-textured Damaged Helmet sample asset, loaded via GLTFLoader.
Async-safe scroll handling
The scroll handler and interaction flag both tolerate the model still loading.
Honest load-failure fallback
A logged error swaps in a placeholder mesh instead of a silent void.
Zoom multiplier, not a snap-back distance
A slider and Ctrl/Cmd-scroll zoom hold steady relative to every checkpoint.

About this UI Snippet

Scroll GLB Camera Flythrough — A Scroll-Driven Tour With a Real Manual Override

Screenshot of the Scroll GLB Camera Flythrough snippet rendered live

A single scroll-scrubbed rotation is one thing; touring a model past several distinct, named viewpoints — front, a close-up detail, the side, the back — is a genuinely different problem, because now the *camera's position itself* is what scroll drives, and a visitor dragging to look around wants to control that exact same property. This snippet builds both halves properly: a four-checkpoint camera path interpolated live from scroll progress, and a real, working handoff to manual OrbitControls dragging that doesn't fight the scroll path or snap awkwardly on release.

Checkpoints, not one continuous curve

CHECKPOINTS is a plain array of four { name, pos, look } objects — a camera position and look-at target for Front, Visor, Side, and Back. applyFlythrough(progress) maps the 0-1 scroll progress onto segments = 3 legs of the journey, works out which pair of adjacent checkpoints the current progress falls between, and linearly interpolates both the camera position and its look-at target between them with a small lerpVec3 helper. The result is one continuous camera move built from four fixed shots — you never have to hand-author every in-between frame.

Why dragging and the scroll path genuinely need a handoff

Unlike a demo where scroll spins the *model* and dragging orbits the *camera* (two different transforms that never conflict), here scroll is setting camera.position directly, and OrbitControls also sets camera.position when a visitor drags. Without coordination, both would fight for the same frame. The fix is a small isInteracting flag: controls' own 'start' and 'end' events flip it, and the ScrollTrigger's onUpdate simply returns early — skipping its own camera write — whenever isInteracting is true. A visitor's drag always wins immediately.

A deliberate delay before resuming, not an instant snap

The 'end' handler doesn't clear isInteracting immediately — it starts a 900ms setTimeout first. Release the drag and the scroll path doesn't yank the camera back to its "correct" scroll position the instant your mouse lifts; there's a brief pause that reads as intentional, then control returns to the scroll path smoothly on the next scroll-driven update.

A real, detailed glTF model, not a placeholder

THREE.GLTFLoader fetches Khronos' own "Damaged Helmet" sample asset — a genuinely PBR-textured .glb with visible surface detail (a visor, straps, battle damage) — specifically because a four-checkpoint tour is only worth taking if there's something to actually look at from each angle. If the model fails to load, a named error is logged and a placeholder icosahedron takes its place, so the scene is never silently empty.

Zoom as a multiplier, not an absolute distance

OrbitControls' built-in wheel-zoom is deliberately turned off (controls.enableZoom = false) so a plain scroll always advances the page instead of being hijacked. Zoom is reimplemented as a relative zoomFactor — a slider and Ctrl/Cmd + scroll both nudge it — applied as a radial scale on the checkpoint's own position-to-target offset inside applyFlythrough(), so "zoomed in 30%" means the same thing at every checkpoint despite their very different authored distances. While a visitor is mid-drag, setZoomFactor() instead rescales the camera's *current* offset directly, since the position at that moment came from free orbiting, not from a checkpoint.

Customizing it

Add more checkpoints for a longer tour, change CHECKPOINTS' pos/look values to frame different details, or shorten the 900ms resume delay for a snappier handoff. Pair it with scroll-scrubbed GLB turntable for the simpler object-rotation version of this same idea, or three product viewer for a non-scroll-driven OrbitControls baseline.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why scroll-driven camera position and OrbitControls dragging genuinely conflict here in a way that a scroll-driven object rotation wouldn't, and how the isInteracting flag combined with the 900ms resume delay resolves that conflict without either system fighting the other or snapping abruptly. It's also useful for extending the demo — ask it to add a fifth checkpoint, ease the interpolation between checkpoints with a non-linear curve instead of a plain lerp, or add small text captions that fade in/out per checkpoint describing what the visitor is currently looking at. Use the conversation to build real intuition for coordinating two systems that want to control the same property before applying the same handoff pattern to your own scroll-driven camera work.

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-driven camera flythrough" around a real 3D model in plain HTML, CSS, and JavaScript using Three.js (core, GLTFLoader, and OrbitControls, all loaded from a CDN with no bundler) plus GSAP with its ScrollTrigger plugin.

Requirements:
- A pinned full-viewport Three.js scene (GSAP ScrollTrigger pin: true) with an intro section before it and an outro section after, studio-lit with key, fill, and rim lights.
- Load a real .glb model using THREE.GLTFLoader pointed at a genuine, freely-licensed, CDN-hosted glTF binary URL with enough surface detail to be worth touring (e.g. one of Khronos' official glTF-Sample-Assets models) — do not substitute a primitive geometry.
- Define at least four named camera checkpoints, each with a camera position and a look-at target, representing distinct views of the model (e.g. front, a close-up detail, side, back).
- Using ScrollTrigger's scrub option, map the 0-1 scroll progress onto which pair of adjacent checkpoints the camera currently sits between and how far, and linearly interpolate both the camera's position and its look-at target between that pair every scroll update — so scrolling produces one continuous camera move built from the fixed checkpoints, reversible by scrolling back up.
- Set up OrbitControls on the camera with damping enabled so a visitor can click-and-drag the canvas to manually orbit at any time. Because both the scroll path and OrbitControls want to control the camera's position, implement a real handoff: track whether the user is currently interacting (via OrbitControls' own start/end events) and skip the scroll-driven camera update entirely while they are, so a manual drag always takes immediate priority — then resume the scroll-driven camera path smoothly after a short delay (a few hundred milliseconds) once the drag ends, rather than snapping the camera back instantly.
- Display a small label showing which named checkpoint the camera is currently closest to, derived from the same scroll progress used for the camera interpolation.
- Handle the model still being asynchronously in-flight when scroll events first fire, and handle the GLTFLoader's error callback by logging the real error and substituting a simple placeholder mesh so the scene is never blank if the model fails to load.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Add all five CDN scriptsthree.min.js, GLTFLoader.js, OrbitControls.js, gsap, and ScrollTrigger.
  2. 2
    Paste HTML, CSS, and JSThe helmet loads at the "Front" checkpoint.
  3. 3
    Scroll into the pinned sectionThe camera tours Front → Visor → Side → Back, interpolated live.
  4. 4
    Drag on the canvas mid-tourYou take over immediately — the scroll path pauses while you orbit.
  5. 5
    Release the dragAfter a short pause, the scroll path resumes from the correct position.
  6. 6
    Use the slider or Ctrl/Cmd + scroll to zoomPlain scroll always advances the page; zoom is a separate, opt-in gesture.
  7. 7
    Scroll back upThe tour reverses exactly, since it's a pure function of progress.

Real-world uses

Common Use Cases

Product detail scroll tours
Walk a visitor past specific features of a real 3D product model.
Portfolio and technical showpieces
Demonstrates a genuinely hard scroll-vs-camera-control coordination problem, solved cleanly.
Museum/collectible virtual tours
Tour an artifact's front, detail, and back as a guided scroll sequence.
glTF/OrbitControls teaching demos
A complete, real example of resolving scroll-driven and manual camera control.
Alongside the simpler turntable
Compare against scroll-scrubbed GLB turntable's conflict-free object-spin approach.
Longer scroll narratives
Use as a 3D centerpiece between other scroll reveal grid sections.

Got questions?

Frequently Asked Questions

Because here, scroll drives the CAMERA's position directly — the same property OrbitControls sets when a visitor drags. Two systems writing to camera.position on the same frame would genuinely fight. In the simpler turntable snippet, scroll instead drives the MODEL's own rotation while OrbitControls only ever moves the camera, so there's nothing to arbitrate there. Zoom follows the same discipline: OrbitControls' own wheel-zoom is turned off so a plain scroll never gets hijacked, and zoom is reimplemented as an explicit slider plus Ctrl/Cmd + scroll instead.

OrbitControls fires a 'start' event the instant a drag begins, which flips an isInteracting flag to true. The ScrollTrigger's onUpdate callback checks that flag first and returns immediately without touching the camera if it's true — so a visitor's drag always takes priority on the very next frame, with no delay.

Clearing isInteracting the instant the drag ends would let the scroll path immediately snap the camera back to wherever scroll position says it "should" be, which reads as jarring. Waiting 900ms after the 'end' event before resuming gives the moment room to feel intentional rather than like the visitor's input was overridden the second they let go.

applyFlythrough(progress) divides the 0-1 scroll progress into three equal segments (one per pair of adjacent checkpoints), determines which segment the current progress falls in and how far through it, then linearly interpolates both the camera position and look-at target between that pair's values. The camera path is built entirely from four fixed shots with no hand-authored in-between frames.

Set up the renderer, scene, GLTFLoader call, OrbitControls, and ScrollTrigger inside a mount effect, keeping the isInteracting flag, resumeTimer, and loaded model in refs so the onUpdate callback and controls event listeners can reach current values. Call controls.dispose(), renderer.dispose(), clearTimeout on any pending resume timer, and the ScrollTrigger instance's .kill() in the cleanup function.