Improvements to the canonical one-liner GLSL rand() for OpenGL ES 2.0

Long title, agreed. If you have a short attention span, I’ll save you right here and give you the goods.

(1)Don’t use this:

float rand(vec2 co)
   return fract(sin(dot(co.xy,vec2(12.9898,78.233))) * 43758.5453);

(2)Do use this:

highp float rand(vec2 co)
    highp float a = 12.9898;
    highp float b = 78.233;
    highp float c = 43758.5453;
    highp float dt= dot(co.xy ,vec2(a,b));
    highp float sn= mod(dt,3.14);
    return fract(sin(sn) * c);

Version 1 is found in dozens of places on the internet. Try doing a search for “GLSL rand”, and among all the Perlin noise routines you’ll find the little gem in (1) tossed out many times. I don’t know the person who first wrote it, but hats off to him/her. It’s reliant on the fact that sin(x) modulates slowly, but sin(<huge multiplier>*x) modulates extremely quickly. So quickly that sampling the sin function at every fragment location effectively gives you “random” numbers.

Of course, this is all dependent on the GPU implementation and the quality of the sin calculation. On the BlackBerry Z10 (model STL100-2 using a Qualcomm Adreno 225) version 1 of the code works perfectly fine. I use this function to introduce background roughness into my Milkyway rendering for Ablative Air. The Milky Way image is rendered on the inside of a sphere. Without the noise, the texturing of the background looks crudely pixelated. With the noise, it almost looks like a Hubble photo. Here are two examples–not the exact same region, but you can get the idea of the difference.


Fig 1. Milky Way with noise



Fig 2. Milkyway without noise.


So, what’s the problem? I ported this renderer onto the Z30 which uses a slightly different GPU. The GPU in both cases is an Adreno (225 vs 320), but I suspect that this problem could occur on many different GPUs, as the random function relies on “problematic” behaviour.

And guess what? On the Adreno 320, my noise function completely disappeared. It became mostly all white (rand() returning 1.0) with black lines running through the noise at periodic intervals.  The effect is somewhat difficult to see underneath the Milky Way texture unless you magnify the image a lot. I saw it initially by hooking my game up to an HDMI equipped TV. In the samples below, I’ve removed the Milkyway texture to make it incredibly easy to spot. I’m just showing a texture built exclusively with the noise function.


Fig 3. Pure noise texture – Adreno 225 GPU



Fig 4. Pure noise texture – Adreno 320 GPU

As you can see, figure 4 is a little less random than you might like for noise. That is, it’s not random by any stretch of the imagination.

How has our trusty little GLSL random function failed us?

I had encountered a similar issue before in one of my other GLSL ports. My shader for Ablative Air’s laser “scattering” effect also relies on sin(), so I immediately suspected the sin function was overflowing on the Adreno 320 GPU. This is purely guesswork, but I think the implementation for that GPU does not precondition the sin() input to a reasonable 2PI range first. It just applies a Taylor series (or some other approximation) to the input value without any range check. The Adreno 320 implementation is likely a scooch faster, but it’s definitely less accurate than the earlier Adreno 225, certainly for this purpose.

My fix may be a little bit overkill, but it’s safe: force all constants to high precision and use mod to get the sin input value within a reasonable range first. My version is much safer, and shouldn’t rely on any particular quirks of the GPU’s sin function implementation.  Here’s the result of my fix running on the Adreno 320: perfectly acceptable noise, just like we wanted.


Fig 5. Adreno 320 GPU with improved noise function


In fact, my tweaks to this beautiful little algorithm is probably a little cleaner than the original function, even when running on the original Adreno 225. If you look closely at the noise in Figure 3, you can see tiny stripes of regular looking data peeking out. Squint up your eyes “snake eye” style, and look near the bottom centre of the image and you’ll see three vertically stacked diagonal slashes. It’s a little bit more apparent when the texture is in motion, but those patterns aren’t visible in figure 5 using the improved noise function. I suspect that the Adreno 225 GPU, while it provides a more robust sin() implementation, is still subject to floating point inaccuracies from the lack of high precision. Hence why I’ve kept the “highp” in there, just in case.

Leave a Reply