Three.js Scroll Sand Dune Drift — Procedural Terrain Flyover

Three.js Scroll Sand Dune Drift · Scroll · Plain HTML, CSS & JS · Live preview

What's included

Features

Procedural hashed-sine heightmap displaces a 130x130-segment PlaneGeometry with no image texture
A single phase parameter drifts the entire ripple pattern — cheap, reversible, and image-free
Cached base x/z coordinates mean the per-frame loop only recomputes and writes Y — no attribute reallocation
geometry.computeVertexNormals() runs each update so golden-hour lighting responds correctly to reshaped slopes
Camera height is clamped against a live terrain sample directly beneath the flight path for a true low-altitude feel
Warm DirectionalLight, ambient fill, and matching THREE.Fog combine for a golden-hour look with no post-processing
Phase and camera path are both pure functions of scrubbed scroll progress — fully reversible by construction
Renderer sized via canvas.clientWidth/clientHeight with a resize handler and pixel ratio capped at 2

About this UI Snippet

How to Build a Scroll-Driven Desert Dune Flyover With Three.js

Screenshot of the Three.js Scroll Sand Dune Drift snippet rendered live

The Three.js Scroll Sand Dune Drift snippet displaces every vertex of a large plane with a procedural heightmap to sculpt rolling desert dunes, then drifts that heightmap's phase and flies a low camera over it as the visitor scrolls through a pinned stage — no image-based heightmap, no terrain library.

A hashed-sine heightmap instead of an image texture

Rather than loading a grayscale heightmap texture, the dune() function combines three layered sine ridges at different frequencies and phase offsets into one height value per (x, z) coordinate. This is cheap to evaluate for every vertex every frame and, crucially, accepts a phase parameter — shifting that single number slides the entire ridge pattern, which is what makes the dunes look like they are drifting rather than static.

Displacing a PlaneGeometry in place

A THREE.PlaneGeometry with 130x130 segments is rotated flat, and each vertex's original x/z is cached once into plain Float32Arrays (baseX/baseZ) so the per-frame update loop only has to look up those two coordinates and write a new Y via posAttr.setY, followed by geometry.computeVertexNormals() so the golden-hour lighting responds correctly to the newly sculpted slopes — the same displaced-plane technique used in wave terrain, here driven by ridge noise instead of a rippling sine grid.

Phase drift is a pure function of scroll progress

The ripple phase passed into dune() is eased * 6.0 — directly derived from the scrubbed, eased scroll progress, not from elapsed real time. That means the dune pattern at any given scroll position is always identical whether you arrived there scrolling down or back up, which is what keeps the whole flyover exactly reversible.

A camera path that samples the terrain it flies over

The camera doesn't just move along a fixed line — its target height is computed by sampling dune() directly beneath the flight path (groundY) and clamped so the camera never dips below the sand, giving a genuine low-altitude "skimming the ridgelines" feel rather than a floating drone shot with no relationship to the terrain underneath it.

Golden-hour palette

A low warm THREE.DirectionalLight positioned near the horizon, a warm ambient fill, and THREE.Fog tinted the same warm orange as the sky gradient behind the canvas combine to sell the golden-hour desert mood without any post-processing pipeline.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to reverse-engineer how a scroll-driven terrain flyover works without a heightmap texture or terrain engine. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain why the ripple phase is derived from scroll progress rather than elapsed time, or why computeVertexNormals is called after every height update. The same assistant can help you extend it — ask it to add a second, higher-frequency noise layer for finer sand texture, blend in a distant mountain silhouette, or add scattered rock/cactus instances that sit on the live terrain height. It can also help optimize further, for instance moving the heightmap evaluation into a vertex shader so the CPU loop disappears. Treat the code as a conversation starter, 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:

text
Build a "scroll-scrubbed procedural desert dune flyover" 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 it and updated on window resize including aspect ratio, plus THREE.Fog and a low warm DirectionalLight to establish a golden-hour desert mood.
- Create a large flat-shaded PlaneGeometry with roughly 130x130 segments, rotated to lie horizontal, and cache every vertex's original x/z coordinate into plain arrays before displacing it.
- Write a height function of (x, z, phase) that combines two or three layered sine waves at different frequencies and phase offsets to produce dune-like rolling ridgelines, with no image-based heightmap or external noise library.
- Each frame, recompute every vertex's Y coordinate from its cached x/z and a phase value derived from the current scrubbed scroll progress (not elapsed time), write it into the geometry's position attribute, set needsUpdate to true, and call computeVertexNormals so lighting responds correctly to the reshaped terrain.
- Register a GSAP tween on a ScrollTrigger targeting the pinned section, with pin: true, start at top top, a numeric scrub, and a multi-hundred-percent end, animating a single plain progress value from 0 to 1 with linear easing.
- Move the camera from a high, distant establishing position down to a low altitude that skims just above the dune ridgelines as progress advances, sampling the height function directly beneath the camera's flight path so it never dips below the sand.
- Confirm scrolling back up reverses both the camera's flight and the dune ripple drift exactly, since both depend only on the current scrubbed progress value.

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.

Source Code

<section class="dune-stage" id="duneStage">
  <div class="dune-intro-overlay"><p>Scroll ↓ to fly low over drifting desert dunes</p></div>
  <canvas id="duneCanvas"></canvas>
  <div class="dune-hud"><span id="dunePct">0</span>% flight</div>
</section>
<section class="dune-bottom"><p>Golden ridgelines, drifting to the horizon.</p></section>

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 JSA distant, high establishing shot of the dunes appears inside a pinned 3D stage with a live "% flight" read-out.
  3. 3
    Scroll downThe camera dives low over the ridgelines while the dune ripple pattern visibly drifts beneath it.
  4. 4
    Scroll back upThe flight retraces exactly in reverse and the ripple pattern un-drifts, since both are pure functions of progress.
  5. 5
    Retune the terrainAdjust the frequencies/amplitudes inside dune() for gentler rolling hills or sharper, choppier ridges.
  6. 6
    Adjust the pacingChange the ScrollTrigger end value (+=400%) for a slower or faster flyover.

Real-world uses

Common Use Cases

Travel and desert-tourism sites
Open a Sahara, Namib, or dune-buggy tour homepage with a hero that visibly flies the visitor over the landscape.
Outdoor and expedition gear brands
A sweeping terrain flyover suits adventure and gear brands better than a static product photo hero.
Procedural terrain and generative art portfolios
Showcase heightmap-displacement work with a piece that demonstrates phase-driven drift live on scroll.
Open-world or survival game landing pages
Pair with wave terrain or tunnel for a multi-biome scroll story.
Teaching vertex displacement and normals
A compact example of heightmap-driven vertex displacement plus computeVertexNormals for correct shading.
Scroll-story chapter breaks
Use the flyover as a mid-page transition between sections, similar to galaxy formation.

Got questions?

Frequently Asked Questions

Three layered sine ridges at different frequencies and phase offsets already produce convincing dune-like ridgelines without pulling in a noise library, and because the function is cheap, it can be re-evaluated for every one of the roughly 17,000 vertices every frame without a performance cost.

The dune() height function takes a phase parameter that shifts the sine ridges horizontally. Each frame, every vertex's Y is recomputed from its cached original x/z plus the current phase, so the same geometry buffer is reused — only the values written into it change, which is far cheaper than creating new geometry.

If phase advanced with a running clock, the dune pattern at a given scroll position would depend on how long the user had been on the page, and scrolling back up would not restore the earlier pattern. Deriving phase directly from the eased scroll progress value guarantees the terrain state is always identical for a given scroll position, in either direction.

Vertex normals determine how light reflects off each face. If they were left at their original flat-plane values after displacing the geometry, the golden-hour lighting would look flat and wrong on the newly sculpted slopes — recomputing normals every update keeps the shading responsive to the current terrain shape.

Yes. Click JSX for a React component, Vue for a Vue 3 SFC, Angular for a standalone component, or Tailwind for a React + Tailwind version. Build the geometry and cached coordinate arrays inside a mount effect keyed to a canvas ref, and on unmount kill the ScrollTrigger instance, dispose the geometry and material, and call renderer.dispose().