webdevpuneet.com

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

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

Copy & paste

Source Code

<section class="ght-intro"><h1>Scroll to fly the camera around a real glTF model</h1><p>Four camera checkpoints, interpolated live by scroll — grab and drag at any point to take over manually.</p></section>
<section class="ght-pin" id="ghtPin">
  <canvas id="ghtCanvas"></canvas>
  <div class="ght-stage" id="ghtStage">Front</div>
  <div class="ght-hint" id="ghtHint">Drag to take over · release to resume · Ctrl/Cmd + scroll to zoom</div>
  <div class="ght-zoom">
    <span class="ght-zoom-label">+</span>
    <input type="range" id="ghtZoom" class="ght-zoom-slider" min="0" max="100" step="1" />
    <span class="ght-zoom-label">&minus;</span>
  </div>
</section>
<section class="ght-outro"><p>Reached the last checkpoint — scroll back up to reverse the flythrough.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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 tours
Walk a visitor past specific views of a product, such as front, close-up detail, side and back, as named camera checkpoints interpolated from scroll.
Technical portfolio showpieces
Demonstrate a genuine camera path rather than a simple spin, with the camera position itself a function of scroll progress.
Museum virtual tours
Tour an artefact's features in sequence, with drag-to-orbit overriding the scripted path and a 900 ms delay before it resumes.
Simpler turntable comparison
Compare against the scroll GLB duck turntable, where the object spins and the camera stays fixed.
Longer scroll narratives
Use as a 3D centrepiece between sections, followed by a scroll reveal grid, with a stage label naming the nearest checkpoint.
Related: Scroll-Triggered GLB Model Carousel
See the Scroll-Triggered GLB Model Carousel for a related scroll pattern worth pairing with this one.

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.