Grainy Perlin Noise Shader for Web

Published On 2025, 03/28

Wow, it’s behind this text! For the longest time, the background of this website was a solid color, which I loved for its minimalism. But I’m addicted to motion, so I couldn’t resist adding something subtly animated. I’ve always been drawn to soft gradients with grainy texture. Taking some inspiration from Keito Yamada’s personal site , I realized I wanted to do something similar - a gentle, moving, grainy gradient.

My approach was fairly simplified in comparison:

  1. Use Perlin noise to generate smooth, almost cloud-like patterns.

  2. Add hash-based aglorithimn for a grainy, animated effect.

  3. Use a light mode toggle to switch between light and dark gradients.

frag.glsl
        precision mediump float;
    uniform vec3      iResolution;           // viewport resolution (in pixels)
    uniform float     iTime;                 // shader playback time (in seconds)
    uniform float     iTimeDelta;            // render time (in seconds)
    uniform float     iFrameRate;            // shader frame rate
    uniform int       iFrame;                // shader playback frame
    uniform float     iChannelTime[4];       // channel playback time (in seconds)
    uniform vec3      iChannelResolution[4]; // channel resolution (in pixels)
    uniform vec4      iMouse;                // mouse pixel coords. xy: current (if MLB down), zw: click
    uniform vec4      iDate;                 // (year, month, day, time in seconds)
    uniform float     iSampleRate;           // sound sample rate (i.e., 44100)
    uniform float     uLightMode;            // to keep track of light/dark mode

    float hash(vec2 p) {
        float h = dot(p, vec2(113.7, 271.9));  // hash seed
        return -1.0 + 2.0 * fract(sin(h) * 38942.3794);  // multiplier
    }

    vec2 perlinHash(vec2 p) {
        p = p * mat2(123.4, 343.2, 199.2, 237.6);  // matrix
        p = -1.0 + 2.0 * fract(sin(p) * 37819.2345);  // multiplier
        return sin(p * 6.283 + iTime * 1.2);
    }

    float perlinNoise(vec2 p) {
        vec2 cell = floor(p);
        vec2 local = p - cell;
        vec2 fade = local * local * (3.0 - 2.0 * local);

        float a = dot(perlinHash(cell + vec2(0.0, 0.0)), local - vec2(0.0, 0.0));
        float b = dot(perlinHash(cell + vec2(0.0, 1.0)), local - vec2(0.0, 1.0));
        float c = dot(perlinHash(cell + vec2(1.0, 0.0)), local - vec2(1.0, 0.0));
        float d = dot(perlinHash(cell + vec2(1.0, 1.0)), local - vec2(1.0, 1.0));

        float xMix = mix(a, c, fade.x);
        float yMix = mix(b, d, fade.x);
        return mix(xMix, yMix, fade.y);
    }

    float layeredNoise(vec2 p) {
        float sum = 0.0;
        float amplitude = 1.0;
        float totalAmplitude = 0.0;
        
        p *= 3.5;  // starting frequency

        for (int i = 0; i < 4; i++) {
            sum += amplitude * perlinNoise(p);
            totalAmplitude += amplitude;
            p *= 2.2;
            amplitude *= 0.55;
        }

        return sum / totalAmplitude;
    }

    void main() {
        vec2 uv = gl_FragCoord.xy / iResolution.xy;
        uv *= vec2(iResolution.x / iResolution.y, 1.0);

        vec2 center = vec2(0.2, 0.5);
        float zoom = 0.11;
        vec2 adjustedUV = (uv - center) * zoom + center;

        float f = layeredNoise(adjustedUV);
        f += 0.2 * hash(uv * 2.0);

        if (uLightMode > 0.5) {
            f = f * 0.75 + 0.85 * 1.1;
        } else {
            f = f * 0.53 + 0.0;
        }

        gl_FragColor = vec4(vec3(f), 1.0);
    }
  

Figuring out how to incorporate the shader on my website was surprisingly more challenging. At first, I tried using the