Learn Creative Coding (#35) - Noise on the GPU

Back in episode 12 we built Perlin noise from scratch in JavaScript. We wrote the permutation table, the gradient vectors, the fade function, the interpolation. It worked. You could generate beautiful organic textures, rolling hills, wispy clouds. But you also felt the cost -- computing noise per pixel on the CPU, one pixel at a time, meant anything above a few hundred pixels was visibly slow. A full 1920x1080 noise field? Go get coffee.
On the GPU, that same noise field renders in microseconds. Every pixel computes its noise value simultaneously. The fragment shader runs for all two million pixels at once, in parallel. And the techniques you can build on top of it -- fractal brownian motion, domain warping, curl noise -- these become real-time instead of "render overnight." This is where shaders go from interesting to indispensable.
But there's a catch. GLSL fragment shaders can't use arrays the way JavaScript can. No permutation tables. No lookup arrays for gradient vectors. The classic Perlin implementation we wrote in episode 12 doesn't translate directly. We need a different approach -- hash functions that generate pseudo-random values from coordinates, entirely through arithmetic. No memory lookups, just math.
The GPU hash function
In JavaScript-land, our noise used a permutation table -- a shuffled array of 256 integers that we indexed into to get pseudo-random values. In GLSL, we replace that with a hash function. The most common one in shader art:
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
What's happening here? dot(p, vec2(127.1, 311.7)) takes a 2D coordinate and projects it onto a line using those arbitrary constants. sin() of a large number oscillates so fast that it looks random. Multiply by 43758 to amplify tiny differences. fract() takes just the fractional part, giving us a value between 0 and 1.
Is this mathematicaly perfect randomness? No. There are known patterns if you zoom in far enough or use very specific coordinate ranges. But for visual purposes it's excellent. You'd need a magnifying glass and bad luck to spot artifacts. This is the hash function you'll see in 90% of shader code on Shadertoy, The Book of Shaders, everywhere. It's the standard tool.
Why those specific numbers -- 127.1, 311.7, 43758.5453? They're chosen because they work well empirically. The dot product constants are large enough that small coordinate changes produce large phase changes in the sin. The multiplier is large and irrational enough that fract gives good distribution. People have tested thousands of constants and these are the survivors. You can tweak them if you want -- some people use vec2(12.9898, 78.233) and 43758.5453123 -- but honestly, the standard ones are fine.
Value noise: the simplest GPU noise
Value noise is the easiest type to understand. Random values at grid points, smooth interpolation between them:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float valueNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
// random values at the four corners of the cell
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
// smooth interpolation
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float n = valueNoise(uv * 8.0);
gl_FragColor = vec4(vec3(n), 1.0);
}
floor(p) gives the integer grid coordinate (which cell are we in). fract(p) gives the position within that cell (0 to 1 in each axis). We hash the four corner coordinates to get random values, then interpolate bilinearly using the smoothed position.
The smoothing function f * f * (3.0 - 2.0 * f) is the Hermite interpolation curve -- same smoothstep polynomial we've been using since episode 33. Without it, the interpolation between cells would be linear and you'd see visible grid lines. The smooth version gives the noise that soft, flowing quality.
Value noise is simple but has a known weakness: it tends to look blocky. The random values are at grid corners, and the interpolation between them creates subtle axis-aligned artifacts. For most creative work it's perfectly fine. For work that needs to look truly organic, gradient noise is better.
Gradient noise: Perlin-style on the GPU
Gradient noise uses random gradients at grid points instead of random values. Instead of interpolating between four numbers, we compute a dot product between a random gradient vector and the distance from each corner, then interpolate those dot products. This is essentially what we built in episode 12, but reimplemented for the GPU:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453) * 2.0 - 1.0;
}
float gradientNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
// fade curve: 6t^5 - 15t^4 + 10t^3 (improved Perlin)
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
// gradients at corners, dot with distance vectors
float a = dot(hash2(i), f);
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float n = gradientNoise(uv * 6.0) * 0.5 + 0.5;
gl_FragColor = vec4(vec3(n), 1.0);
}
The hash2 function returns a 2D vector instead of a scalar -- that's our random gradient. fract(sin(p) * 43758.5453) * 2.0 - 1.0 maps the hash output from 0..1 to -1..1, giving gradients that point in all directions.
The fade curve f * f * f * (f * (f * 6.0 - 15.0) + 10.0) is Ken Perlin's improved fade function from 2002. Remember from episode 12, the original 3t^2 - 2t^3 had discontinuities in the second derivative that caused visual artifacts. The 6t^5 - 15t^4 + 10t^3 version has continuous first AND second derivatives, so the noise looks smoother. Same math, just translated from our JavaScript fade() function into one line of GLSL.
The output is centered around 0 (ranges roughly -0.7 to 0.7), so the * 0.5 + 0.5 shifts it into visible brightness range. Compare this to the value noise -- gradient noise looks more organic, less blocky, with better-distributed features. The difference is subtle at normal scales but becomes obvious when you layer octaves.
Animating noise
Static noise is useful for textures, but what we really want is movement. Adding u_time to the noise input makes the pattern drift and evolve:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453) * 2.0 - 1.0;
}
float gradientNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash2(i), f);
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
// slow drift
float n = gradientNoise(uv * 5.0 + u_time * 0.3) * 0.5 + 0.5;
// color using the noise value
vec3 color;
color.r = n * 0.8;
color.g = n * n * 0.6;
color.b = sqrt(n) * 0.7;
gl_FragColor = vec4(color, 1.0);
}
Adding u_time * 0.3 to the input shifts the noise pattern sideways over time. The * 0.3 keeps it slow -- fast-moving noise looks like TV static, slow-moving noise looks like clouds drifting. The color mapping uses different curves per channel (n, n*n, sqrt(n)) so the dark and bright areas each have their own tint. Dark regions are blueish, bright regions are orange-warm. One of those tricks that takes no effort but makes the result look ten times better.
You could also animate in a completely different dimension. GLSL noise functions can work in 3D -- use (uv.x, uv.y, u_time) as a 3D input and the noise evolves without drifting in any screen direction. It just... changes. Like watching the surface of water from directly above. We're keeping it to 2D noise for this episode but it's worth knowing the option exists.
Fractal Brownian Motion: layered noise
A single octave of noise looks like soft rolling hills. Real natural textures -- clouds, terrain, marble -- have detail at multiple scales simultaneously. Big features AND small features. That's what fractal brownian motion (fBm) gives you: layered noise octaves, each at higher frequency and lower amplitude.
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453) * 2.0 - 1.0;
}
float gradientNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash2(i), f);
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for (int i = 0; i < 6; i++) {
value += amplitude * gradientNoise(p * frequency);
frequency *= 2.0; // lacunarity
amplitude *= 0.5; // gain (persistence)
}
return value;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float n = fbm(uv * 4.0) * 0.5 + 0.5;
gl_FragColor = vec4(vec3(n), 1.0);
}
Each iteration of the loop adds a "layer" of noise. The first layer is low-frequency, high-amplitude -- the big shapes. Each subsequent layer doubles the frequency (more detail) and halves the amplitude (less influence). After 6 octaves you get noise that has both large-scale structure and fine-grained detail. It looks like clouds, or terrain from above, or water surface patterns.
The two control parameters:
- Lacunarity (frequency multiplier per octave): 2.0 is standard. Higher values skip intermediate frequencies, creating more "gappy" detail. Lower values (1.5) create smoother results with more gradual detail buildup.
- Gain/persistence (amplitude multiplier per octave): 0.5 is standard. Higher values (0.6, 0.7) give more weight to fine detail, making the result rougher and more turbulent. Lower values (0.3) make the result smoother, dominated by the low-frequency layers.
Try changing them. Lacunarity 3.0 with gain 0.3 gives a completely different character than the defaults. These two knobs reshape the entire texture.
Domain warping: noise feeding noise
This is where it gets wild. Domain warping means using noise to distort the coordinates before sampling more noise. Instead of noise(p), you compute noise(p + noise(p)). The first noise value pushes the coordinate around, and the second noise samples from the distorted position. The result looks like fluid, or smoke, or geological formations:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453) * 2.0 - 1.0;
}
float gradientNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash2(i), f);
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 5; i++) {
v += a * gradientNoise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
vec2 p = uv * 3.0;
// first warp
float t = u_time * 0.15;
vec2 q = vec2(fbm(p + vec2(0.0, 0.0) + t),
fbm(p + vec2(5.2, 1.3) + t * 0.7));
// second warp (warp the warp)
vec2 r = vec2(fbm(p + 4.0 * q + vec2(1.7, 9.2) + t * 0.3),
fbm(p + 4.0 * q + vec2(8.3, 2.8) + t * 0.5));
float n = fbm(p + 4.0 * r);
// color based on warp layers
vec3 color = vec3(0.0);
color = mix(vec3(0.05, 0.05, 0.15), vec3(0.4, 0.15, 0.1), clamp(n * 2.0, 0.0, 1.0));
color = mix(color, vec3(0.1, 0.3, 0.5), clamp(length(q) * 0.8, 0.0, 1.0));
color = mix(color, vec3(0.6, 0.35, 0.2), clamp(length(r) * 0.6, 0.0, 1.0));
gl_FragColor = vec4(color, 1.0);
}
Two layers of warping. q is noise that distorts the original coordinates. r is noise that distorts the already-distorted coordinates. The final fbm(p + 4.0 * r) samples from doubly-warped space. The result is these gorgeous swirling, flowing patterns that look like aerial photography of geological formations or close-ups of marble.
The + vec2(5.2, 1.3) offsets are crucial. Without them, the two components of q would sample from the same noise field and produce correlated distortions. The arbitrary offsets ensure each component gets a different-looking noise pattern, producing more complex, less symmetric warping.
Adding u_time to the warp coordinates makes the whole thing animate -- slow, organic, like watching smoke unfold in slow motion. This is one of those shaders that you can stare at for ten minutes. On the GPU it runs at 60fps even with the double warp. On the CPU you'd be lucky to get one frame per second.
This technique comes from Inigo Quilez's article "Warping" -- if you want the full mathematical breakdown, his site has the definitive reference. I still re-read it every few months and pick up something new.
Curl noise: flow without divergence
Curl noise is a specialised variant that creates divergence-free flow fields. What does that mean? Imagine dropping leaves into a stream. They swirl and flow but never pile up in one spot or leave gaps. That's divergence-free motion. Regular noise used as a velocity field would create areas where particles accumulate (sinks) and areas where they spread apart (sources). Curl noise eliminates that.
The math: take the partial derivatives of a noise field and rotate them 90 degrees.
vec2 curlNoise(vec2 p) {
float eps = 0.01;
// partial derivative in x: (noise(x+eps) - noise(x-eps)) / (2*eps)
float nx = fbm(p + vec2(eps, 0.0)) - fbm(p - vec2(eps, 0.0));
float ny = fbm(p + vec2(0.0, eps)) - fbm(p - vec2(0.0, eps));
// rotate 90 degrees: (dy, -dx)
return vec2(ny, -nx) / (2.0 * eps);
}
We approximate the gradient of the noise field using finite differences -- sample slightly to the left and right, slightly above and below, and take the difference. Then swap and negate to get the perpendicular direction. The result is a vector field where particles follow smooth loops and spirals without ever converging or diverging.
Here's a visualization that shows the curl field as color -- the direction and magnitude mapped to hue and brightness:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453) * 2.0 - 1.0;
}
float gradientNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash2(i), f);
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * gradientNoise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
vec2 curlNoise(vec2 p) {
float eps = 0.01;
float nx = fbm(p + vec2(eps, 0.0)) - fbm(p - vec2(eps, 0.0));
float ny = fbm(p + vec2(0.0, eps)) - fbm(p - vec2(0.0, eps));
return vec2(ny, -nx) / (2.0 * eps);
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
vec2 curl = curlNoise(uv * 3.0 + u_time * 0.1);
// map direction to hue, magnitude to brightness
float angle = atan(curl.y, curl.x);
float mag = length(curl);
vec3 color;
color.r = sin(angle) * 0.4 + 0.5;
color.g = sin(angle + 2.094) * 0.35 + 0.45;
color.b = sin(angle + 4.189) * 0.4 + 0.5;
color *= smoothstep(0.0, 2.0, mag);
gl_FragColor = vec4(color, 1.0);
}
The colors show flow direction -- similar hues flow the same way. You can see the loops and vortices. This is the same technique used in fluid simulations, smoke effects, and those beautiful flow-field visualizations you see all over generative art Twitter.
Curl noise is expensive though -- it evaluates fbm four times per pixel (the four offset samples for the finite differences). With 4 octaves of noise each, that's 16 noise evaluations per pixel for the curl alone. On a modern GPU it's still fast. On a phone it might chug a bit. Worth knowing.
CPU vs GPU: the performance gap
Let's put a number on it. Our JavaScript Perlin noise from episode 12, computing a single octave across a 1920x1080 canvas: about 70-100 milliseconds on a decent laptop. That's one frame of basic noise. Add six octaves of fBm and you're at half a second. Add domain warping and it's multiple seconds per frame. Completely unusable for animation.
The same computation on the GPU: the 6-octave fBm runs at 60fps on basically any dedicated GPU made in the last decade. Domain warping with double warp? Still 60fps. You might dip to 30fps on integrated laptop graphics with heavy shaders, but even that's "realtime" -- you're seeing smooth animation, not slide shows.
The reason is parallelism. A CPU processes one pixel, then the next, then the next. A GPU processes all of them at the same time. A mid-range GPU has thousands of shader cores. Each pixel's noise calculation is independent -- it doesn't need to know what its neighbor computed. That's the perfect workload for massive parallelism. It's like the difference between one person filling out a spreadsheet row by row versus a thousand people each filling out one cell simultaneously.
Here's a quick way to feel the difference yourself. This shader combines fBm with the pattern techniques from episode 34 -- noise-modulated stripes. On the CPU you'd need nested loops and it'd crawl. On the GPU it's instant:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453) * 2.0 - 1.0;
}
float gradientNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash2(i), f);
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * gradientNoise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
// noise-warped stripes
float warp = fbm(uv * 4.0 + u_time * 0.1) * 0.5;
float stripe = step(0.5, fract((uv.x + warp) * 12.0));
vec3 col1 = vec3(0.08, 0.06, 0.15);
vec3 col2 = vec3(0.7, 0.35, 0.2);
vec3 color = mix(col1, col2, stripe);
gl_FragColor = vec4(color, 1.0);
}
Straight stripes, bent by noise. The warp value shifts each pixel's stripe position differently, turning rigid vertical lines into flowing organic ribbons. This is the real payoff of combining the techniques from the last few episodes -- patterns from episode 34, noise from today, all running together at full speed.
A creative exercise: animated domain-warped noise with a palette
Let's build something beautiful. An animated domain-warped fBm with cosine palette coloring. A living, breathing abstract canvas:
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453) * 2.0 - 1.0;
}
float gradientNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash2(i), f);
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 5; i++) {
v += a * gradientNoise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
vec3 cosinePalette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
return a + b * cos(6.28318 * (c * t + d));
}
void main() {
vec2 uv = (gl_FragCoord.xy - u_resolution * 0.5) / u_resolution.y;
vec2 p = uv * 3.0;
float t = u_time * 0.12;
// domain warp
vec2 q = vec2(fbm(p + vec2(1.0, 3.0) + t),
fbm(p + vec2(4.7, 2.1) + t * 0.8));
float n = fbm(p + 3.5 * q);
// cosine palette: warm tones with blue shadows
vec3 color = cosinePalette(
n * 1.5 + t * 0.3,
vec3(0.5, 0.4, 0.4), // brightness offset
vec3(0.5, 0.4, 0.3), // contrast
vec3(1.0, 0.7, 0.4), // frequency
vec3(0.0, 0.15, 0.2) // phase
);
// darken edges slightly
float vignette = 1.0 - length(uv) * 0.5;
color *= vignette;
gl_FragColor = vec4(color, 1.0);
}
The cosine palette function (from Inigo Quilez, again -- the man is everywhere) maps a single value to an RGB color through cosine curves. The four parameters control brightness, contrast, frequency, and phase of each color channel independently. By tuning those eight vec3 values you can create any color scheme -- warm earths, cool oceans, neon synthwave, sunset gradients. The noise value drives the palette, so different noise heights get different colors. Combined with domain warping, you get these rich swirling patterns where color flows through the composition like paint.
This one runs endlessly. The u_time * 0.12 makes it evolve slowly -- you can watch for minutes and it never quite repeats. It's the kind of thing you'd put on a gallery screen or project onto a wall at an event. And the entire thing is maybe 50 lines of GLSL.
Where noise leads
Noise on the GPU is the foundation for basically everything interesting in shader art. Terrain generation, cloud rendering, water surfaces, fire effects, procedural textures for 3D models -- all noise, all the time. The fBm and domain warping techniques from this episode show up again and again.
In the coming episodes we'll use noise with shader feedback loops, where the output of one frame feeds into the next, creating trails and accumulation effects. And when we get to reaction-diffusion systems much later, domain warping will be the key to making those patterns feel natural instead of clinical. The noise toolkit you've built today -- hash, value noise, gradient noise, fBm, domain warping, curl noise -- that's a toolkit you'll reach for in nearly every shader project from here on.
The performance angle matters too. Being able to compute complex noise in real time changes what you're willing to attempt. On the CPU, domain warping was a "render and save" technique. On the GPU, it's a "play with it live and tweak parameters until something clicks" technique. That interactive feedback loop is what makes creative coding on the GPU feel different from creative coding on the CPU. You experiment faster. You discover more. You get into flow more easily because there's no waiting :-)
't Komt erop neer...
- GPU noise replaces the CPU permutation table with a hash function:
fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453)-- pure arithmetic, no memory lookups - Value noise: random values at grid corners, smoothly interpolated. Simple but slightly blocky
- Gradient noise: random gradients at grid corners, dot product with distance, then interpolate. The GPU version of Perlin noise from episode 12
- The improved fade curve
6t^5 - 15t^4 + 10t^3eliminates second-derivative discontinuities for smoother results - Fractal Brownian Motion (fBm): layer noise octaves at increasing frequency and decreasing amplitude. Lacunarity and gain are your two control knobs
- Domain warping:
noise(p + noise(p))distorts the sampling coordinates, creating fluid and geological patterns. Double warping amplifies the effect - Curl noise: derivatives of noise rotated 90 degrees, producing divergence-free flow fields perfect for particle motion and fluid simulation
- The GPU computes all pixels in parallel -- a full-canvas fBm that takes hundreds of milliseconds on CPU runs at 60fps on GPU
- Cosine palettes map noise values to smooth, tunable color gradients with just four vec3 parameters
Sallukes! Thanks for reading.
X
Congratulations @femdev! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)
Your next target is to reach 70 posts.
You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word
STOP