Copied!
Blog

How to Recreate a WebGL Shader Effect from an Award-Winning Site

Most shader tutorials start from a blank file. Start from a working effect instead: find the canvas, capture the shader, read what drives it, and rebuild it.

← Blog
Swirling pink, teal and blue liquid paint flowing across a surface
Photo via Unsplash

October 7, 2026

5 min readIncludes interactive demos

Nearly every guide to shader effects starts the same way: an empty file and a lecture on gl_FragColor. That is a fine way to learn the language and a slow way to learn what a good effect actually looks like in code. The faster route is to start from an effect that already works — a liquid hero, a noise-warped image, a scroll-driven dissolve on a site you admire — and take it apart until you understand every line. This is the workflow we use, in the order we use it.

Step 1: confirm it is really WebGL

Plenty of “shader-looking” effects are not shaders at all. A blurred, animated gradient can be pure CSS; a displacement ripple can be an SVG feTurbulence filter; a particle field can be a 2D canvas. Open DevTools, find the <canvas> element, and check whether it holds a WebGL context (canvas.getContext('webgl2') returns the existing context rather than null if it was created that way). No canvas, or a 2D context, means you are reading the wrong chapter.

It also pays to check that the effect is active. A shader sitting in a bundle proves it exists, not that it runs. Scroll, hover and wait before you decide which program on the page is the one you are looking at.

Step 2: capture the shader source

By hand, you can use a WebGL inspector such as Spector.js to record a frame and list every program, its vertex and fragment shaders, its uniforms and its draw calls. It works, but sites that compile many programs, or load shaders as minified strings inside a bundle, make this a slog.

This is the job reverse_engineer_webgl does in a single call: it loads the page in a real browser, lets the canvas draw, and reports the programs that actually ran — shader source, uniform names and values, draw-call counts — instead of whatever happens to be in the JavaScript. If the effect only appears after scrolling or a click, drive the page first with interact_with_page, then capture. For the wider walkthrough of picking the right tool for each question, see how to reverse-engineer a live site with Enrichr, step by step.

Step 3: read the shader like a recipe

Once you have the source, resist the urge to paste it into your project. Read it in three passes.

Step 4: find what drives the uniforms

The shader only describes how to draw one frame. What makes it feel alive is whatever writes to the uniforms between frames: elapsed time, pointer position, or a scroll-scrubbed progress value. In the sites we capture, that last one is usually GSAP and ScrollTrigger changing a plain JavaScript number while the render loop copies it into the shader — the split we describe in Three.js and GSAP: the two libraries behind almost every award-site hero. Run reverse_engineer_animations against the same URL and you get the other half of the bridge: the trigger, the scrub value, the easing.

Step 5: rebuild it in a minimal harness

Start with the smallest scene that can show the effect: one full-screen quad, one ShaderMaterial, no lights, no camera tricks. Here is a stripped down version of the pattern — a noise-warped gradient with time and pointer uniforms — that you can swap the captured source into.

const uniforms = {
  uTime:  { value: 0 },
  uMouse: { value: new THREE.Vector2(0.5, 0.5) },
};

const material = new THREE.ShaderMaterial({
  uniforms,
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = vec4(position.xy, 0.0, 1.0);
    }`,
  fragmentShader: `
    precision highp float;
    varying vec2 vUv;
    uniform float uTime;
    uniform vec2 uMouse;

    float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
    float noise(vec2 p) {
      vec2 i = floor(p), f = fract(p);
      f = f * f * (3.0 - 2.0 * f);
      return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x),
                 mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);
    }

    void main() {
      vec2 uv = vUv;
      float n = noise(uv * 4.0 + uTime * 0.15);
      uv += (n - 0.5) * 0.08 + (uMouse - 0.5) * 0.02;
      vec3 col = mix(vec3(0.05, 0.04, 0.12), vec3(0.55, 0.35, 0.95), noise(uv * 3.0 + n));
      gl_FragColor = vec4(col, 1.0);
    }`,
});

scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));

renderer.setAnimationLoop((t) => {
  uniforms.uTime.value = t * 0.001;
  renderer.render(scene, camera);
});

Get the static frame matching first, with time frozen at the captured value. Only then wire up pointer and scroll. Debugging a shader and its motion at the same time is how an afternoon disappears.

Step 6: verify against the original

“Looks about right” is not a test. Take a screenshot of the original and of your rebuild at the same viewport size and the same uniform values, and compare them side by side. The usual reasons a faithful port still looks off:

What is fair game. Learning the technique — the noise function, the uniform wiring, the scroll bridge — and rebuilding it in your own project is how everyone in this field learns. Shipping someone else’s shader, textures or branding as your own is a different thing. Rebuild the effect, then make it yours: change the maths, the palette and the content. We cover the broader line in how to rebuild an Awwwards Site of the Day as a learning exercise.

Where to go from here

Once one effect makes sense, the next is much faster, because the vocabulary repeats: noise, warped coordinates, a handful of uniforms, one driver. If you want the bigger picture of why these effects cluster on the same few libraries, read building the special effects: shaders, scroll-linked motion, and the other 10%, and if you want to recover a whole 3D scene rather than a single shader, the extract_scene_graph tool is built for that. Prefer to browse before you capture? The Enrichr gallery has real captures to start from.