Many beginner tends to program in Shadertoy as they would program in C: by explicitly drawing every elements. But shaders are called for every pixels so drawing the full scene each time can be very costly, and it’s way more efficient to determine what is the only – or the few – element(s) that may cover the current pixel, if feasible. Which is often the case for repetitive patterns, e.g. a spiral of dots. Once the candidate element is determined, then we can draw it (which might by itself be costly, if it was not a simple dot like here).
Let see how we can get to one form of algorithm to the other.
A Shadertoy user once proposed a “C like” shader similar to this:
#define rot(a) mat2( cos(a),-sin(a),sin(a),cos(a) ) void mainImage( out vec4 O, vec2 u ) { vec2 R = iResolution.xy, U = ( 2.*u - R ) / R.y, p = vec2(.01,0); float PI = 3.14159, phi = iTime * .01 + 0.1, // or (1. + sqrt(5.))/2., a = phi * 2.*PI, d = 1e9; for( int i = 0; i < 1400; i++) { d = min( d, length(U - p) - .001 ); p *= rot(a); p = normalize(p) * ( length(p) + .0015 ); } O = vec4( smoothstep(3./iResolution.y, 0., d - .01) ); }
p starts at vec2(.01,0), then at each step it is rotated by a and its distance to center increased by .0015. The loop is called 1400 times for every pixel, which can be quite costly (especially in fullscreen), while at most one dot will cover the pixel. May we determine which ?
First, let makes the current coordinates explicit, from the description above (NB: I personally prefer to loop on floats to avoid loads of casting):
p = ( .01+ .0015*i ) *CS(i*a); with #define CS(a) vec2(cos(a),sin(a))
Now, which i yields the dot closest to U ?
Let’s do the maths !
p ~= U considered in polar coordinates yields .01+ .0015*i ~= length(U) and i*a ~= atan(U.y,U.x)
The first gives i0 ~= ( length(U) - .01 ) / .0015 and the second gives i1 ~= atan(U.y,U.x) / a , or indeed, i1' = i1 +k*2.*PI/a where k is the – unknown – spire number.
We can find k such that i0 ~= i1': k ~= ( i0 - i1 ) / (2.*PI/a ) .
k must be integer, so we should consider either the floor or cell of this. round would do it for a small dot, but with the larger one here we will need both values to cover the full dot (and for an even larger one we should visit a bit more integers around).
Then, i = round( i1 + k * 2.*PI/a )
Which gives the loopless version of the shader ( the 2-values loop n is just to parse the floor and cell ) :
#define CS(a) vec2(cos(a),sin(a)) void mainImage( out vec4 O, vec2 u ) { vec2 R = iResolution.xy, U = ( 2.*u - R ) / R.y; float PI = 3.14159, phi = iTime*0.001 + 0.1, // or phi = (1. + sqrt(5.))/2., a = phi * 2.*PI, i0 = ( length(U) - .01 ) /.0015, i1 = ( mod( atan(U.y,U.x) ,2.*PI) )/ a, // + k*2PI/a k = floor( (i0-i1) / (2.*PI/a) ), i, d = 1e9; for (float n = 0.; n < 2.; n++) { i = round( i1 + k++ * 2.*PI/a ); vec2 p = ( .01+ 0.0015*i ) *CS(i*a); d = min( d, length(U - p) - .001 ); } O = vec4(smoothstep(3./iResolution.y, 0., d - .01)); }
Not only the cost per pixel is hugely decreased (by about 1000), but also the cost is now totally independent to the number of dots ! Which is good news when designing a pattern.
See more ( including simpler ) loopless shaders.
Note that when designing a spiral shader “the procedural way” from scratch, we could follow a totally different strategy: converting U to spiral coordinates, then splitting it in chunks using fract. The not-trivial part is then the drawing of the dot, since it must not be done in spiral coordinates or it would appear deformed. So we must convert back the center and neighborhood to screen coordinates. See example.