The importance of good noise
There are many articles to read about noise functions in computer graphics, especially now that a lot of people recently got interested in ray tracing, but it took me a long time to fully understood why noise characteristics are so important and I didn’t find a good resource on the Internet explaining it, so I’ll give it a shot.
Why noise is needed
Noise is used to generate sequences of semi-random numbers. I use these random numbers at several places in the rendering system, but here are a few examples:
-
Soft shadows from light sources that are not a single point. In this case, the light source is a sphere, so for each ray I trace towards a random point on that sphere.
-
Blurry reflections. For materials that are not perfect mirrors, I alter the surface normal a small amount for each ray. This gives the appearance of a rough surface.
-
Ambient occlusion, which darkens concave areas which are blocked from the incoming environment light. I shoot a number of rays on the hemi-sphere for each surface point and randomize the direction.
-
Volumetric fog, or god rays. In order to approximate lit fog I shoot rays along the line of sight towards each light source. Both the sample points along the line of sight and the direction towards the light source (if it’s not a point light) need noise.
I use noise in other places as well, but these are probably the easiest to explain. In some cases, several rays are shot for each pixel and the result is just the average of all samples. In other cases, there is just a single sample. In either cases, the visual result will be more or less noisy. I use denoising by spatially blurring and temporally accumulating the result over time.
Noise characteristics
In white noise, each sample is just a random number, without any consideration of the sequence as a whole, very much like rolling a die. Imagine the following sequence of random numbers between 0 and 9:
2 9 7 1 3 5 6 1 0 1 8 9 2 4 4
Since each sample has no “memory” of what has already been generated, it can generate similar numbers several times in a row, like the “1 0 1” found in the middle.
Now instead consider the following sequence, which are the exact same numbers but in a different order:
2 9 3 7 1 5 1 4 0 8 4 1 9 2 6
Since it’s the same numbers, they have the same average, but I swizzled them around so that adjacent numbers are always reasonably far apart. You can think of this as a signal with higher frequency. This is roughly what blue noise is trying to achieve, and that’s why noise functions are so intimately related to frequency spectrums.
Why blue noise is desirable
When working with computer graphics, blue noise is desirable, because we don’t want the same (or similar) result in two adjacent pixels on the screen, because that will make the spatial denoising filter less efficient.
Even without filtering, blue noise gives a smoother characteristic and more visually pleasing image, but that’s because there is a certain amount of filtering going on in our eyes and brains. Apparently, our retinal cells are arranged in a blue noise-like pattern. Pretty cool!
Unfiltered ambient occlusion with white noise
Same ambient occlusion with blue noise
To put it in simpler words, we want semi-random numbers that are as “spread out” as possible both vertically and horizontally. In one dimension, like the numbers above, it’s fairly easy, but when doing it in two dimensions it’s actually much harder, because you need to consider not only what’s before and and after each sample, but also what’s above and below. This is what two-dimensional blue noise does.
Temporal aspect
So far we didn’t consider time. Now imagine we have a good distribution of random numbers so that the number for a specific pixel is not similar to any of it’s neighbors. Let’s also add time.
Just like with pixel neighbors, we don’t want the random number for a specific pixel to be the same for two consecutive frames, because that will make the temporal filtering less efficient. You can think of this as just another dimension, so what we want is really three dimensional noise that is spread out both horizontally, vertically and over time.
This is where is gets complicated, because apparantly, when generating blue noise in three dimensions it loses some of it’s nice properties in two dimension, so a 2D slice of 3D blue noise will not be as good as pure 2D blue noise. I’m not good enough at math to fully understand why, but there is an in-depth article about it here
To overcome this issue, I use a trick based on the golden ratio, which is the most irrational number there is. The golden ratio is very useful for many things, and if you haven’t added that to your bag of tricks, you should. There is a really cool video explaining why it is so irrational here
Irrational numbers in general and the golden ratio in particular has this property that if you add it to a number between zero and and one take the fraction of that, you will get a new number that is far apart from the old one, yet never repeating itself when you do it over and over. This is exactly what we want! So, instead of using 3D blue noise, I use 2D blue noise and animate it using the golden ratio. Note that this is not a novel idea, a lot of people have been doing this before, for instance here
How to use it
The most efficient way of using blue noise is probably to use a precomputed texture with blue noise samples in it. It doesn’t have to be the same resolution as the render target. It’s fine to repeat it a few times before it gets visually noticable. I use a 512x512 sized blue noise texture. In order to achieve all the good blue noise properties it is important to line up the blue noise texture exactly with the fragments in the render target and not using any filtering when sampling it. You need a pixel-perfect mapping for it to work effiently.
To animate the noise over time, just do noise=mod(blueNoise+GOLDEN_RATIO*frameNumber, 1.0)
and you’re done. Make sure that frameNumber doesn’t get too big over time, or you’ll lose floating point precision, so noise=mod(blueNoise+GOLDEN_RATIO*(frameNumber%100), 1.0)
or similar will work fine.
Update: There has been a discussion on twitter where @tastytexel pointed out that as the blue noise sample wraps around it can introduce low frequency components. Imagine two consecutive samples that are 0.0 and 0.9 (good distribution). When adding the same offset 0.1 to both with fraction wrapping, the result will be 0.1 and 0.0 (bad distribution). The suggested fix is to reshape the noise through a triangular filter so that it wraps around nicely. Will update with more findings as I have tried this.
More dimensions
In many cases you want not just a single random number, but a 2D or 3D random vector. I use an RGB blue noise texture for that, so each pixel actually has three random numbers, where each channel has blue noise properties. You could just add the golden ratio to animate those, but I came to think of this blog post about the R2 sequence, which is a multi-dimensional generalization of the golden ratio.
So instead of adding the golden ration to each component when animating 3D noise, I add three different irrational numbers, one to each channel. In my experience this gives a better distribution of 2D and 3D vectors over time, since all components aren’t shifting the same amount.
Finally here is a link to a very useful database with free, precomputed blue noise textures of different sizes.