View profile

GM Shaders Mini: Noise

XorDev
XorDev
Hello people,
Today we’re going to go over some common noise functions. These are useful all kinds of effects, like smoke, water, fire, clouds, terrain and so much more.
Tutorial Difficulty: Intermediate

Static, Value, Perlin, Worley, Voronoi and fractal noise.
Static, Value, Perlin, Worley, Voronoi and fractal noise.
Hash Function
A hash function takes an input and returns a scrambled, pseudo-random output.
Let’s say we have a float input “p” and we want a random float output between 0 and 1.
Most people use a sine-wave multiplied by a huge number and use the fractional part of that to get a “random” number between 0 and 1:
return fract(sin(p * 0.129898) * 43758.5453);
This is a 1D hash function because it has a single float input. The numbers here aren’t too important as long as doesn’t produce any obvious patterns.
This is much more useful with a vec2 input though:
return fract(sin(p.x*0.129898 + p.y*0.78233) * 43758.5453);
It’s essentially the same process, but we add each component with a different, random multiplier. You want to make sure the two numbers aren’t obvious ratios like 1 to 2 because then you’ll get obvious patterns. That’s why we include random fractions.
Here’s an illustration:
A bad hash on the left versus a good one on the right.
A bad hash on the left versus a good one on the right.
On the left side, we’re using simplified multipliers:
... p.x*0.12 + p.y*0.78 ...
So choose your “magic numbers” carefully! You don’t have to do a lot of math, just play around until you find something that looks good.
Another trick I’ve learned
This is the baseline that all the other noise algorithms will use:
float hash1(vec2 p)
{
  return fract(sin(p.x*0.129898 + p.y*0.78233) * 43758.5453);
}
I’m calling this “hash1” because there’s just one float output.
Value Noise
The principle of value noise is simple: It’s a low-resolution version of the hash function, with interpolation. So we need to divide the coordinates into cells, get the hash at the corners of each cell, and smoothly interpolate between them.
Let’s break it down into steps:
Step 1: Round
Round down the input coordinates to cells:
vec2 cell = floor(p);
This means each cell is 1 unit wide. This makes the math easier.
If you want it to be 64 pixels, then p should be the pixel coordinates divided by 64.
Step 2: Sample corners
Sample the 4 corners of each cell so that you can interpolate. Something like this:
const vec2 off = vec2(0,1);
float hash_corner00 = hash1(cell + off.xx);
float hash_corner10 = hash1(cell + off.yx);
float hash_corner01 = hash1(cell + off.xy);
float hash_corner11 = hash1(cell + off.yy);
Step 3: Interpolate
Next, we should interpolate between cell corners.
It will be easier to understand if you read the interpolation tutorial (or at least the “Cubic Filtering” section).
So first we need the sub-cell coordinates (fractional coordinates in each cell):
vec2 sub = p - cell;
Then we can apply the cubic interpolation formula (optional):
vec2 cube = sub*sub*(3.-2.*sub);
Next, we interpolate horizontally (top-left to top-right and bottom-left to bottom-right):
float hor0 = mix(hash_corner00, hash_corner10, sub.x);
float hor1 = mix(hash_corner01, hash_corner11, sub.x);
And finally, we can interpolate vertically to get the final result:
return mix(hor0, hor1, sub.y);
hor0 compared to horizontal+vertical interpolation
hor0 compared to horizontal+vertical interpolation
A full code example will be linked at the end.
This can be extended to higher dimensions, but it gets exponentially more expensive. In 3D, you need to sample 8 corners of each cell and interpolate horizontally 4 times, vertically 2 times, and depthwise once. I’ll leave that for you to research if you’d like!
Perlin Noise
Perlin noise is quite similar to value noise, but instead of interpolating single hash values, it interpolates between gradients that go in random directions.
This can produce smoother, more natural-looking results, but at a higher performance cost.
Let’s go through the steps:
Step 1: Vector Hash
We need a vec2 hash instead of a single value:
return fract(sin(p * mat2(0.129898, 0.78233, 0.81314, 0.15926)) * 43758.5453);
I’m calling this function “hash2” because it returns a vec2 output. The mat2 here is a shorter way of writing:
p.x * vec2(0.129898, 0.81314) + p.y*vec2(0.78233, 0.15926)
And to get a random unit vector:
return normalize(hash2(p) - 0.5);
I’m calling this “hash2_norm”. We’ll need both functions later.
Step 2: Sample corners
Just like with value noise, we divide into cells and sample direction vectors for the 4 corners:
vec2 dir_corner00 = hash2_norm(cell+off.xx);
vec2 dir_corner10 = hash2_norm(cell+off.yx);
vec2 dir_corner01 = hash2_norm(cell+off.xy);
vec2 dir_corner11 = hash2_norm(cell+off.yy);
But calculate the gradients, we need to compute the dot product of the direction and the difference between the sample point and the corner.
The dot product here just tells how far along the random axis, the current sample point is.
float grad_corner00 = dot(dir_corner00, off.xx-sub);
float grad_corner10 = dot(dir_corner10, off.yx-sub);
float grad_corner01 = dot(dir_corner01, off.xy-sub);
float grad_corner11 = dot(dir_corner11, off.yy-sub);
Don’t worry! This is the hardest part!
Step 3: Interpolate
The same process as with the value noise, but we’ll use “quintic” interpolation instead of cubic.
vec2 quint = sub*sub*sub*(10.0 + sub*(-15.0 + 6.0*sub));
Feel free to compare to cubic and linear interpolation!
Now we interpolate:
float hor0 = mix(grad_corner00, grad_corner10, quint.x);
float hor1 = mix(grad_corner01, grad_corner11, quint.x);
Note: these values can range from -sqrt(2.0) to +sqrt(2.0), so to get a value between 0 and 1, I multiply by 0.7 (approximately sqrt(0.5)) and add 0.5.
return mix(hor0, hor1, quint.y) * 0.7 + 0.5;
And that’s it. You should get something like this:
Beautiful Perlin (gradient) noise!
Beautiful Perlin (gradient) noise!
Conclusion
That seems like a good place to wrap up for today. I was planning to include Worley, Voronoi, and fractal noise, but that would probably make this a full tutorial instead of a mini!
I’ll continue off here next week with the other noise functions, but in the meantime, you can check out my full ShaderToy example here!
Thanks for reading. Talk to ya next week!
Did you enjoy this issue? Yes No
XorDev
XorDev @xordev

Weekly mini-tutorials for game development and beyond

In order to unsubscribe, click here.
If you were forwarded this newsletter and you like it, you can subscribe here.
Created with Revue by Twitter.