You Might Also Like
Three.js Scroll Asteroid Belt — InstancedMesh Dodge Run
Three.js Scroll Asteroid Belt Run · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
How to Build a Scroll-Driven Asteroid Belt Run With InstancedMesh and GSAP

The Three.js Scroll Asteroid Belt Run snippet flies the camera on a weaving, banking path through 320 tumbling asteroids — every rock a single InstancedMesh instance, so the whole field costs one draw call — with "gate" asteroids seeded deliberately close to the flight path that flash hot and increment a NEAR MISSES counter as the camera threads them. GSAP's ScrollTrigger scrubs the camera's Z; the corridor, the banking, and the drama are all construction-time guarantees.
One draw call for the whole field
Rendering 320 individual Mesh objects means 320 draw calls and 320 scene-graph nodes. InstancedMesh submits one geometry, one material, and a buffer of per-instance matrices — the GPU stamps the rock 320 times in a single call. Each frame the loop recomposes every matrix through a shared Object3D dummy (position from placement, rotation from clock-time tumble, scale from seed) and flags instanceMatrix.needsUpdate. With DynamicDrawUsage hinting the driver that this buffer changes every frame, the field animates as cheaply as a particle system while remaining fully lit, shadowed, solid geometry. The base rock itself is a DodecahedronGeometry with seeded per-vertex radial jitter, so instances read as debris rather than dice.
A corridor guaranteed at construction
Dodge-field scenes fail when a rock happens to spawn on the flight path. Here the path is two analytic functions — pathX(z) and pathY(z), sums of incommensurate sines that weave without repeating — and every asteroid is placed *relative to the path at its own Z*: a random angle around the centerline at a radius of at least 6.5 units of clearance. The corridor is therefore flyable by construction, no collision testing ever needed. The 12% of rocks flagged as gates get clearance 3.4 instead — close enough to feel dangerous, still outside the camera's envelope. Threat placement is a parameter, not luck.
Banking sells the steering
A camera translated along a curve looks like it is being dragged; a camera that *banks* looks like it is flying. The roll angle is the numerical X-derivative of the path — (pathX(z−1) − pathX(z+1)) × 0.09 — so the camera leans into every turn exactly as hard as the turn demands, with look-at aimed 14 units down-path. One derivative turns a slide into a cockpit, the first-person sibling of the bridge crossing's footstep bob.
Near-misses with scrub-safe counting
Gate rocks within 6 units of the camera tint hot orange via setColorAt — per-instance color, still one draw call — and increment the counter once per pass. The counted flag re-arms only when the camera retreats fully above the gate, so oscillating mid-pass cannot double-count, and scrolling back up genuinely re-arms the gates for another run. It is the honest-counter philosophy of the portal gate sequence adapted to stateful counting on a scrubbed axis.
Fog as density illusion
FogExp2 matched to the background makes rocks materialize gradually out of the dark ahead — the belt feels endless although only 320 rocks exist across 480 units, the same budget-infinity trick as the scroll tunnel. Star dust points scattered through the corridor depth supply parallax between rock encounters. For the physics-flavored cousin where the field reacts to you, see magnetic particles; for the warp-speed version with no obstacles, starfield warp.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not need to learn the InstancedMesh API by trial and error — this snippet is the complete working pattern: shared dummy, per-frame matrix recomposition, DynamicDrawUsage, per-instance color. Paste its HTML, CSS, and JS into an AI assistant like Claude and ask it to explain the corridor guarantee, the derivative banking, or the near-miss re-arm logic. The same assistant can escalate the run — a screen-edge red vignette flash on each near miss, engine particles trailing the camera, a second asteroid size class on its own InstancedMesh, thruster audio tied to bank angle via the WebAudio API, or 2,000 rocks with the count constant and clearances retuned. It can also flip the scene into an autopilot cruise by driving Z from clock time instead of scroll. Treat the code as a starting point to interrogate and reshape, not a finished artifact.
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:
Build a "scroll-driven asteroid belt run" in plain HTML, CSS, and JavaScript using Three.js and GSAP's ScrollTrigger plugin, all loaded from a CDN (no bundler, no build step).
Requirements:
- A pinned full-viewport section with a canvas, WebGLRenderer, PerspectiveCamera (~70° FOV, resized with aspect on window resize), FogExp2 matched to a near-black background, ambient + warm directional light, and ~1,000 star-dust points through the corridor depth.
- ONE InstancedMesh of ~320 asteroids: base DodecahedronGeometry(1, 0) with seeded per-vertex radial jitter (0.82–1.18, sin-hash, no Math.random) and computeVertexNormals; MeshStandardMaterial; instanceMatrix.setUsage(THREE.DynamicDrawUsage).
- An analytic weaving flight path: pathX(z) = sin(z × 0.021) × 9 + sin(z × 0.007) × 5, pathY(z) = cos(z × 0.016) × 5.
- Corridor guarantee: each asteroid spawns at its own Z, positioned at a seeded random angle around the path centerline with radial clearance ≥ 6.5 — except ~12% flagged as "gates" with clearance 3.4 and larger scale. No collision detection anywhere.
- One GSAP tween (ease "none") scrubbing camera Z from +6 to −486 on a ScrollTrigger with pin: true, scrub ~0.55, end ~+=600%.
- Each frame: camera position = (pathX(z), pathY(z), z), lookAt the path 14 units ahead, and BANKING via rotation.z = (pathX(z−1) − pathX(z+1)) × 0.09. Recompose every instance matrix through a shared Object3D dummy: seeded position/scale, clock-time tumble rotation; set instanceMatrix.needsUpdate.
- Near-miss system: gates within 6 units of the camera tint hot orange via setColorAt (flag instanceColor.needsUpdate) and increment a NEAR MISSES counter once per pass — counted on approach (dz > −1), re-armed only when the camera retreats 6+ units above the gate, so scrub jitter cannot double-count but a reverse run re-arms everything.
- An intro overlay fading once the flight starts.
- Confirm the corridor is never blocked and reverse scrolling flies the run backwards with gates flashing again.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
- 1Load the three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js in that order before the snippet JS.
- 2Paste HTML, CSS, and JSYou hover at the edge of a fog-shrouded asteroid field, rocks tumbling ahead — NEAR MISSES 0 in the HUD.
- 3Scroll to flyThe camera weaves and banks along the corridor as asteroids materialize from the fog and slide past on both sides.
- 4Thread the gatesOccasional rocks sit deliberately close — they flash hot orange as you pass and the near-miss counter ticks up.
- 5Scroll backThe run reverses; gates re-arm as you retreat above them, ready to count again on the next pass.
- 6Tune the dangerRaise the gate probability (0.88 threshold), lower gate clearance (3.4), or add rocks — the corridor stays flyable by construction.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each Mesh costs a draw call plus scene-graph overhead; 320 of them would bottleneck most devices. InstancedMesh submits geometry and material once with a buffer of per-instance matrices — one draw call stamps all 320 rocks. The frame loop recomposes matrices through a shared Object3D dummy and sets instanceMatrix.needsUpdate, with DynamicDrawUsage hinting the GPU driver that the buffer updates every frame. Scaling to 2,000 rocks changes one constant.
The path is analytic — pathX(z) and pathY(z) are sums of sines — and every asteroid is placed RELATIVE to the path evaluated at its own Z: random angle, radius of at least 6.5 units (3.4 for gates). Since placement starts from the centerline outward, no rock can occupy the corridor. It is a construction-time guarantee, so the scene needs no collision detection at runtime, ever.
Banking: camera.rotation.z is set to the numerical derivative of the lateral path — (pathX(z−1) − pathX(z+1)) × 0.09 — so it rolls into each turn proportionally to how sharp the turn is, while lookAt aims 14 units down-path. Translation alone reads as being carried; the derivative-driven roll reads as steering, for one subtraction per frame.
Each gate has a counted flag: it counts when the camera first comes within 6 units approaching from the front (dz > −1), and only re-arms once the camera retreats more than 6 units back above it. Small oscillations around the gate cannot re-trigger it, but a genuine reverse run re-arms every gate — deliberate state on top of a scrubbed axis, designed so both directions behave sensibly.
Yes. Export via the JSX, Vue, Angular, or Tailwind buttons. Build the InstancedMesh, asteroid parameter array, and ScrollTrigger in a mount effect against a canvas ref; the misses counter and dummy Object3D live in effect scope, with the HUD updated through a ref. On cleanup kill the ScrollTrigger, dispose the rock geometry/material (one of each — that is the instancing win), the star system, and call renderer.dispose().