Generating Random numbers in a fragment shader on mobile

Posted on

One of the things I hope to try out with this blog is to include brief programming experiments, whether in javascript or webgl.

Realising that webgl is supported in all main desktop browsers and most mobile browsers I thought I would try a shader experiment to show as the header for this blog. What I thought I would try is a random mash of pixilated colors that would change, similar to what star citizen has on the splash screen for their browser map.

The idea was to run entirely in the fragment shader and to just use a simple one liner (1) to generate random numbers and to not have any textures that would need to be downloaded by the client. On my desktop, this is what the random function produces.

Noise

This all worked fine until I tested this in Chrome on my phone where I just got a very odd result. Verifying that there was no compilation issues I soon found out that the issues is that GLES does not guarantee highp precision and that mediump may be as low as 10 bit significand. 10 binary bits plus 1 implied bit(2) leads to only about 3 and a bit digits or -2048 to 2048.

The devices I was testing on (Galaxy S 2 and S 3) have a mali400 which only supports 10 bits for the fragment shader. A newer device (Galaxy S 5) supported the full 24bits. Here is the noise is produces on my phone.

Noise

The code I was using to generate random numbers was the following.

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

The issues with this is that the numbers are relatively large and a number like 43758.5453 will just cut off to something like 43750. Multiplying all of this together just results in a large number and taking the fraction of this results in zero.

My solution was to modify the number generator I had to only use low numbers but have a large enough period to not be noticeable. Dr Google found a few threads where people had similar issues with the random number generator on mobile but no solutions so I tried a few things out.

float rand(vec2 co){
  //since opengl es only garantees that mediump will be 10 bits, we need to try and 
  //keep the numbers low. The actual constants are mostly arbilitary chosen with the
  //goal to give different weightings to the first or seccond element

  vec3 product = vec3(  sin( dot(co, vec2(0.129898,0.78233))),
                        sin( dot(co, vec2(0.689898,0.23233))),
                        sin( dot(co, vec2(0.434198,0.51833))) );
                        
                        
  vec3 weighting = vec3(4.37585453723, 2.465973, 3.18438);

  return fract(dot(weighting, product));
}

On my desktop this produces the following.

Noise

On my phone I get this.

Noise

At larger resolutions a pattern starts to emerge but for my purposes, it is fine. The constants are fairly arbitrary and could be tweaked to produce better results as I believe some precision is being lost.

Another issue is that I was counting milliseconds as after 2048 milliseconds (aka 2 seconds) precision was being lost.

My solution was to use 2 floats to store the time, split into seconds and milliseconds. This still limits seconds and issues may be seen on mobile systems after 15 minutes but the issues should start small.

(1) https://stackoverflow.com/questions/12964279/whats-the-origin-of-this-glsl-rand-one-liner
(2) http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0363d/CJAFCCDE.html