webdevpuneet.com

Three.js Scroll Wave Terrain — GSAP Synthwave Grid

Three.js Scroll Wave Terrain · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<section class="wtr-top"><p>Scroll ↓ to raise the landscape</p></section>
<section class="wtr-stage" id="wtrStage">
  <canvas id="wtrCanvas"></canvas>
</section>
<section class="wtr-bottom"><p>The horizon settles.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), 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

Scroll drives wave amplitude while a separate real-time clock keeps the hills drifting toward the camera
Original vertex positions stored once and re-read each frame, so displacement never accumulates error
Two overlaid sine waves at different frequencies produce organic, non-repeating rolling terrain
Camera height eases downward as the hills rise so they fill the frame at full amplitude
Linear fog matched to the CSS gradient hides the grid edge and blends the canvas into the page
Neon sun disc on the horizon for a complete synthwave backdrop
Single BufferAttribute rewritten in place — one geometry upload per frame at 80×80 subdivision
Pinned, smoothed scrub (0.5), and fully reversible — scrolling up flattens the plane exactly

About this UI Snippet

How to Build a Scroll-Driven Wave Terrain With Three.js and GSAP

Screenshot of the Three.js Scroll Wave Terrain snippet rendered live

The Three.js Scroll Wave Terrain snippet starts with a perfectly flat wireframe plane and raises it into drifting synthwave hills as the visitor scrolls — the scrollbar controls the wave amplitude directly, not a timer — using core Three.js vertex displacement and GSAP's ScrollTrigger plugin, both loaded from a CDN.

Scroll controls amplitude, time controls flow

The effect cleanly separates two inputs. A scroll-scrubbed value amp runs from 0 to 1 and multiplies the height of every hill, so scrolling literally raises the landscape out of flatness. Meanwhile a real-time clock t shifts the wave pattern forward every frame, so the hills always drift toward the camera regardless of scroll. Pausing the scroll freezes the *height* of the terrain but not its *motion* — the frozen hills keep rolling, which is what makes the scene feel alive rather than stuck.

Original vertex positions are kept, not overwritten

A PlaneGeometry with an 80×80 subdivision is rotated flat onto the ground, and a copy of its starting vertex array is stored once. Every frame the height field is recomputed from those *original* X/Z coordinates rather than from the last frame's displaced positions. This matters: if you displaced the already-displaced vertices, errors would accumulate and the terrain would drift into garbage within seconds. Reading from the pristine base each time keeps the surface mathematically stable no matter how long it runs.

Two overlaid sine waves for organic hills

A single sine wave produces obvious, repetitive corrugations. Layering two waves at different frequencies and axes — one along X, one mixing X and Z — creates interference that reads as natural, non-repeating rolling terrain. Both waves are scaled by the same scroll amplitude and animated by the same time value, so the whole surface rises and drifts as one coherent field.

The camera dips as the hills grow

To keep the composition working across the whole scroll, the camera's height eases downward as amp increases and its look target lifts slightly. When the terrain is flat the camera sits high looking across an empty plane; as the hills rise the camera drops so they fill the frame dramatically. Both camera moves derive from the same scrubbed amplitude, so they reverse perfectly on scroll-up.

Synthwave backdrop with fog and a neon sun

A CircleGeometry sun sits on the horizon and linear Fog fades the far edge of the plane into the purple gradient background, hiding where the finite grid ends and concentrating detail near the camera. The CSS background gradient and the fog color are matched so the WebGL canvas blends seamlessly into the surrounding scroll sections.

scrub: 0.5, pinned, fully reversible

A small numeric scrub smooths the amplitude against noisy input, and pinning for +=400% gives the terrain room to rise gradually. Because the height, camera dip, and look target all derive from one scrubbed value, scrolling back up flattens the landscape exactly. This same vertex-displacement approach powers the particle wave snippet; here it drives a solid grid instead of points. Pair it with a scroll tunnel or starfield warp for a full retro-futurist scroll journey.

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 flat neon wireframe grid appears under a synthwave sun in a pinned 3D stage.
  3. 3
    Scroll downThe grid rises into rolling hills that drift toward the camera as the camera dips lower.
  4. 4
    Scroll back upThe hills flatten back to a plane exactly, since the amplitude is fully scrubbed.
  5. 5
    Reshape the terrainEdit the two sine-wave frequencies and amplitudes to change the character of the hills.
  6. 6
    Tune the pacingAdjust the ScrollTrigger end value (+=400%) for a slower or faster rise of the landscape.

Real-world uses

Common Use Cases

Retro and synthwave landing pages
Open a music, game, or event site with a neon grid that rises out of flatness as visitors scroll.
Immersive section transitions
Use the rising terrain as a dramatic backdrop between content blocks in a long scroll page.
Teaching vertex displacement
A readable example of animating a PlaneGeometry height field from a stored base without accumulation error.
Game and world previews
Hint at a game's landscape by literally growing terrain under a synthwave sun as the page unfolds.
Data-landscape metaphors
Imply terrain of data before a chart section, then transition into a horizontal gallery.
Generative background art
Swap the wave functions for noise to produce bespoke generative landscapes tied to scroll depth.

Got questions?

Frequently Asked Questions

Scroll controls one value (amp) that scales hill height, while a real-time clock (t) shifts the wave pattern forward every frame. This lets the terrain keep drifting toward the camera even when scrolling is paused — the height freezes but the motion does not, which keeps the scene feeling alive instead of stuck at a scroll position.

A copy of the plane's starting vertex array is stored once, and each frame the height is computed from those pristine X/Z coordinates. If you displaced the already-displaced vertices instead, small errors would compound and the surface would drift into garbage within seconds. Reading the base every frame keeps the terrain mathematically stable indefinitely.

A single sine wave produces obvious, repetitive corrugations. Overlaying two waves at different frequencies and axes creates interference that reads as natural, non-repeating rolling hills. Both are scaled by the same scroll amplitude and animated by the same clock, so the whole surface rises and drifts as one coherent field.

The camera's height eases downward and its look target lifts as amp increases, both derived from the same scrubbed value. When the plane is flat the camera looks across an empty surface from above; as the hills grow the camera drops so they fill the frame. Because both moves derive from amp, they reverse exactly on scroll-up.

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 plane, store the base vertex array, and set up the GSAP timeline inside a mount effect against a canvas ref, and on cleanup kill the ScrollTrigger and call geometry.dispose() and renderer.dispose() so buffers and the WebGL context are freed on unmount.