]

Thorne Brandt

Mobile Menu

Book of Shaders : Scrolling Bricks

GLSL Study

May 2nd, 2022

I've been studying writing glsl shaders from scratch and one of the best resources available for developing intuitive understanding is The Book of Shaders, but I've noticed that the author has left it unfinished. for the better part of a decade and there doesn't seem to be much available online with actual answers to the end-of-chapter challenges, so I felt like a lot of people were permanently left in the dark wondering whether or not they actually learned anything and eventually gave up on studying shaders. I decided I would try to blog as many as I could in case people were stuck.

In the Patterns chapter, there is a challenge to make alternating rows of a matrix scroll at different speeds, and then repeat this effect vertically. The first part of this challenge is to make scrolling tiles. That part is fairly simple.

The most interesting hurdle of this problem for me was how to make the animated u_time variable start and stop. The chapter gives you a clue by dividing of the space with a modulo of two and a step. But how would you apply this to an always updating time value that endlessly accumlates towards the heat death of the universe? How would you pause, and then seamlessly pick up where you left off when you cannot store values in a shader? The answer feels like a trick. Through trial and error from the in-browser, xy equatic mapper, I discovered this re-usable equation snippet.


    st.x += step(1.0, mod(u_time, 2.0)) * u_time;

The idea is that we know that we need to multiply by zero half of the time, this is accomplished by wrapping a mod in a step. The step servers as a useful if/else statement and becomes an on/off switch for anything it's multiplied by. The trick part is that factorial of two allows the illusion that the movement is seamless because it stops and starts only when the scrolling has completed 100%, ( or +1 in normalized shader space.) So it is still popping, it's just popping to visually the same spot ( From 2 to 1 )

Next we need to isolate the inverse of this paused moment in time, so that the switch-off becomes a switch-on for a different behaviour. ( In this case, the vertical scrolling. ) Since we're dividing the modulo of the time value by two, we only need to add half of this to the u_time within the modulo to get the opposite off/on swtich. ( Multiplying the entire thing by negative one causes the matrix to scroll in the opposite direction, which is also useful infromation. We also switch the x with y to affect the vertical position.

    
        st.y += step(1.0, mod(u_time + 1.0, 2.0)) * u_time;
    

But what if we wanted to change the speed? Obviously we can just multiply by a speed. This works for one, and certain other values (even ints) but for most numbers and floats it was always popping in an unpleasant way. I realized the modulo needed to be multiplied by the inverse of the speed, so it was just a simple division.


    st.y += step(1.0, mod(u_time + (1.0/speed) * speed, 2.0)) * u_time * speed;

And to bring it all together.


uniform vec2 u_resolution;
uniform float u_time;


vec2 scrollingTiles(vec2 _st, float _zoom, float _speed){
    _st *= _zoom;
    _st.x += step(1.0, mod(u_time * _speed, 2.0)) * u_time * _speed * step(1., mod(_st.y ,2.0)); //scrolling right
    _st.x -= step(1.0, mod(u_time * _speed, 2.0)) * u_time * _speed * step(1., mod(-_st.y ,2.0)); //scrolling left 
    _st.y += step(1.0, mod((u_time + 1.0/_speed) * _speed, 2.0)) * u_time * _speed * step(1., mod(_st.x,2.0)); //scrolling up
    _st.y -= step(1.0, mod((u_time + 1.0/_speed) * _speed, 2.0)) * u_time * _speed * step(1., mod(-_st.x,2.0)); //scrolling down 
    return fract(_st);
}



float circle(in vec2 _st, in float _radius){
    vec2 l = _st-vec2(0.5);
    return 1.-smoothstep(_radius-(_radius*0.25),
                         _radius+(_radius*0.25),
                         dot(l,l)*4.0);
}

float box(vec2 _st, vec2 _size){
    _size = vec2(0.5)-_size*0.5;
    vec2 uv = smoothstep(_size,_size+vec2(1e-4),_st);
    uv *= smoothstep(_size,_size+vec2(1e-4),vec2(1.0)-_st);
    return uv.x*uv.y;
}

void main(void){
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 color = vec3(0.0);
    st = scrollingTiles(st,9.0, 0.33);
    color = vec3(circle(st,0.33));
    gl_FragColor = vec4(color,1.0);
}

Which produces the following mesmerizing result: