Shader Programming: Introduction to Noise
A little foreword
Hi! First blog! This semester, I am taking a module on shader programming, and I've decided to write about things I learn as both an easy way to remember them and as a refresher for other students. Huge credit to my lecturer Hauke Thiessen for teaching us this and taking the time to host the module.
This is all very beginner stuff, so if anything is wrong or not the best way to do things feel free to @ me on BlueSky or however else you may know me! :D
There was a class before this on the very very basics of ShaderToy but I will skip that one as I think pretty much all the important stuff is redone here.
Noise in Shaders
White Noise
Noise is very useful in shaders for infinite reasons, and it's good to know how it works. This lesson covered some basic noise information like white noise, blue noise, dithering, and value noise.
Simple white noise is pretty much just ""random"" values, but computers can't exactly produce random values, so we use hash functions. Hash functions essentially take an input and return an entirely unrelated output which can't easily be traced to the input. Even small changes in the values of a hash function will produce wildly different results.
A simple hash function will take a value, let's say "x", get the sine of x, then use the fraction of x as the return value. On its own this is pretty uniform, so we multiply it by constant values to give a bit more of a crazy result.
An example of a hash function which takes one input and gives one output may be:
// The name Hash11 comes from the format Hash[no. inputs][no. outputs]
// Not required, but a nice to have
float Hash11(float x)
{
return fract(sin(x * 100.) * 410.3);
}

The numbers may take some tweaking to get a nice value without any noticeable repetition, but once you have something that works, you never really have to change it. See a ShaderToy example here!
Adding another input to this noise is simple, just add it alongside the x value. See an example with a UV input:
float Hash21(vec2 uv){
// \/ here!
return fract(sin(uv.x * 100. + uv.y * 649.) * 4564.);
}

This noise still has some patterning at larger scales, but it will be fine for this blog.
Dithering with White Noise
Dithering can be used to create an illusion of more values between two set values. For example, it can be used to offset colour banding or even create a transparent look on a material.
Sometimes when displaying colours on screen, the 256 values per channel isn't quite enough to display the gradient cleanly, especially if using only one channel or a short value range. As an extreme example of this, we can display the x channel of the UV and limit it to 30 values, to get the following banded result:
// Default mainImage contents and UV definition above...
vec3 colour = vec3(round(uv.x*30.)/30.);

As an extremely basic solution, we can overlay the noise on our colour gradient as so:
// Default mainImage, UV, colour definitions above...
// Here we add the noise on half opacity and extra adjustments to taste
colour += Hash(uv) * .5 - .2;

As you can see, the bands are slightly less visible, but I wouldn't call this a good option. The gradient has become extremely noisy and doesn't really provide the same look a smooth gradient would. This is where we can begin to use Blue Noise to get a cleaner transition between the values.
Sidenote : Transparency
For transparency, you can use the step function to just return whole number (0||1) values.
colour = vec3(step(uv.x, Hash21(uv)));
Blue Noise
Blue Noise is a more uniform type of noise. While still random, it is more visually appealing and provides a more even distribution. Blue noise is a lot more complex to code ourselves, so for this case we can use a texture of pre made blue noise. Luckily, ShaderToy provides a blue noise texture we can use which can be activated using the iChannels under the code editors.
When using these textures, don't forget to account for the aspect ratio to prevent distortion.
float BlueNoise(vec2 uv)
{
// Here is our UV adjustment to account for the texture size (1024)
vec2 SampleUv = vec2(uv.x * iResolution.x / iResolution.y, uv.y)
* iResolution.y/1024.;
// Then we simply use the built in texture function to map it.
return texture(iChannel0, SampleUv).y;
}
We can then use this to get way nicer gradients, in this example we use the step function to get a recognizable gradient using 1-bit colour:
vec3 col = vec3(step(uv.x, BlueNoise(uv)));

Note: This is not using the reduced channel gradient overlay as the white noise example, which I will admit is a little unfair to the white noise, but trust me this one looks better!
It's definitely not perfect! But it looks a lot more visually appealing than the regular white noise.
Noise Frequencies
So far we have made basic single layer noises, but to get slightly more complex noise we can layer multiple iterations of noise. To do this, we can generate our noise at variable frequencies.
First we must make one interaction at a lower frequency. To do this we will tile our UVs and calculate noise per point on our tiles.
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
// Account for aspect ratio
uv.x *= iResolution.x / iResolution.y;
// Define our frequency
uv *= 8.;
// The UV within the current cell
vec2 localUV = fract(uv);
// The index of the current cell
vec2 cellIndex = floor(uv);
// ^ Combining these two values will give you the original UV position
fragColor = vec4(localUV, .0, 1.0);
}

This shows us our tiles! You can change the amount of tiles by changing the amount we multiply our UV by.
The next part gets a little long, but it's still very simple. Back to using hash functions, we must generate a hash value for each point on each cell.
float lowerLeft = Hash21(cellIndex);
float lowerRight = Hash21(cellIndex + vec2(1., 0));
float upperLeft = Hash21(cellIndex + vec2(0., 1.));
float upperRight = Hash21(cellIndex + vec2(1., 1.));
The vec2s being added to the bottom three are done so we can offset their positions from the lower left co-ordinate. If the lowerLeft position is (0,0), then the lowerRight must be (1,0), and so on. This might be a little hard to visualize so you can always draw it out if needed.
Right now we have all these values but we can't display them all at once. We want to create a gradient across each cell according to these values. That is where the localUV value we defined earlier will come in handy. We can use the mix(a,b,x) function to gradually move between the values on these points according to the position of our localUV.
We have to do this in three steps, as mix will only take two inputs at a time, so we will:
- Mix between the lower values using
localUV.x - Mix between the upper values using
localUV.x - Mix between the lower mix result and the upper mix result using
localUV.y
float lowerBlend = mix(lowerLeft, lowerRight, localUV.x);
float upperBlend = mix(upperLeft, upperRight, localUV.x);
float totalBlend = mix(lowerBlend, upperBlend, localUV.y);
if we then use our totalBlend value as our colour, we get this:

We can improve this even further by applying a smoothstep to our localUV value. Because localUV is always a value between 0-1, we can use this formula:
x * x * (3. - 2. * x)
So, then:
localUV = localUV * localUV * (3.0 - 2. * localUV);
With this ready, we can begin to layer our noises. For this, we can move all of this frequency based noise code into its own function so we can easily use it again:
float NoiseLayer(vec2 uv, float frequency)
{
// Tiling frequency, the higher, the more tiles
uv *= frequency;
vec2 localUV = fract(uv);
// Quick smoothstep for range 0-1
localUV = localUV * localUV * (3.0 - 2. * localUV);
vec2 cellIndex = floor(uv);
// Lower point values
float lowerLeft = Hash21(cellIndex );
float lowerRight = Hash21(cellIndex + vec2(1., 0));
// Upper point values
float upperLeft = Hash21(cellIndex + vec2(0., 1.));
float upperRight = Hash21(cellIndex + vec2(1., 1.));
// Blending the values
float lowerBlend = mix(lowerLeft, lowerRight, localUV.x);
float upperBlend = mix(upperLeft, upperRight, localUV.x);
float totalBlend = mix(lowerBlend, upperBlend, localUV.y);
return totalBlend;
}
Then, in our mainImage we can use this as many times as we want. To make things easier, we can then create a loop and simply add noise values upon each other. To create a more complex noise, we need to consider two things. We must:
- Increase the frequency of the noise
- Reduce the opacity of the noise
We do this on each iteration. This will fill the noise with smaller values but at increasingly lower strengths so the image is not overpowered. There is one final thing we must consider in this regard, which is not to blow out our image. By adding all of these values we will end up with colours far above 1, which will cause the image to be too bright. To counteract this, we can keep track of all the opacities being added, and then divide our final result by this value.
Our main function should be as follows:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
uv.x *= iResolution.x / iResolution.y;
// Set our starting frequency, in this case two
uv *= 2.;
vec3 colour = vec3(0.);
// Init a starting value for summedOpacity as this will be needed
// outside of the loop
float summedOpacity = 0.;
// The value of 10 can be changed,
// but rarely will we need a to go beyond it
for(int i = 0; i < 10; i++)
{
// On each iteration, we want to half the opacity but double the
// frequency, we can do this by powering our index.
float opacity = pow(.5, float(i));
float frequency = pow(2., float(i));
// Using the NoiseLayer function we created, we then add
// the values on top of eachother
colour += vec3(NoiseLayer(uv, frequency)*opacity);
// The opacity of the layer being added this iteration can then be
// added to our summedOpacity value
summedOpacity += opacity;
}
// Then by dividing this colour, we return to a normal range of brightness
colour /= summedOpacity;
fragColor = vec4(colour,1.0);
}
With all of this in place, we should then get the following result:

Value noise! This can be very useful for textures, offsets, anything you could really want a noisy value in, and is quite simple to achieve.
See the full ShaderToy example here!
This is everything we covered this week, but next week we will move onto a bit more complex forms of noise. Thank you for reading and of course thank you to my lecturer for hosting this class :)
If you have any questions or something doesn't work, you can contact me and I will try my best to help!