GM Shaders Mini: Noise
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
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:
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);
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 to 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:
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!