I'm trying to force perspective into the horizon of a Shadertoy scene by drawing horizontal lines at decreasing distances as y grows to create an outrun-esque scene, but can't figure out which functions to use. I know the spaces need to be asymptotic up to uv.y/2, and thought I could use a combination of smoothstep and modulo like:

if (mod(smoothstep(-0.07, 2.0, uv.y/1.5), 0.03) < 0.001) {
    fragColor = vec4(1.0);

This does work, but the lines start wide and quickly become constant width rather than getting smaller. I'd like to offset these lines with iTime so will need to modulo that as well (eg below), but in other tests I get an effect which is nearly correct but the lines bug and either don't draw sometimes, or draw too thick.

if (mod(round(fragCoord.y+mod(iTime*150.0, 500.0)), iResolution.y/2.0-fragCoord.y) < 10.0) {
        fragColor = 1.0;

Am I on the right lines?


We tackle this problem by computing, like shaders do, the color of a certain pixel with position $y$ at timestamp $t$, namely $I_{x, y}(t)$. All we want to know if that pixel must draw a bar on timestamp $t$. So simple as that, we start with

float horizon = 0.5;

void mainImage( out vec4 fragColor, in vec2 fragCoord ) 
   // Normalized pixel coordinates (from 0 to 1)
   vec2 uv = fragCoord / iResolution.xy;

   float position = uv.y;
   float time = iTime;

   // everything above our horizon we make white
   if (position > horizon - 0.01) {
      fragColor = vec4(1.0);

   // check if pixel is on the bar, and if so color it white.
   float color = isOnBar(time, position);

   fragColor = vec4(color, color, color, 1.0);

Next step is to determine whether that pixel actually is on the bar. This problem can be solved with the rules of gravity. We visualize the bars falling down from the horizon. For any pixel $I_{x, y}$, we can say the pixel is on the first bar if

$h_{bar}(t) = I_{y}(t)$.

We know however that bars fall with gravity from the horizon, so:

$h_{bar}(t) = h_{horizon} - gt^{2}$.

Hence, the falling time $ t_{\text{fall}} $, when the bar hits the pixel, can be calculated with

$ t_{\text{fall}} = \sqrt{ \frac{h_{horizon} - I_{y}(t_{\text{fall}})}g } $.

However, this was just our first bar. Any other time a bar hitting that pixel, is when $ (t - t_{\text{fall}}) \bmod a = 0 $, where $ a $ is our falling frequency.

e.g. first bar hits a pixel after 3 seconds, and every 2 seconds another bar falls, then for instance on the 7th second a bar again hits our pixel: $ (7 - 3) \bmod 2 $ is indeed $0$.

How much room we give when computing that modulo close to zero, is the width of our bar. Right now, we can incorporate the widening of the bars when they drop.

float gravity = 0.1;
float timeBetweenBars = 0.5; 

float isOnBar(float time, float position) {
   float timeOfHit = sqrt((horizon - position) / gravity);
   float moddedTime = mod(time - timeOfHit, timeBetweenBars);

   // the lower the pixel, the more room we give to catching the bar with this pixel.
   if (moddedTime < 0.1 * (1.0 - position)) {
      return 1.0;
   return 0.0;

Here is the code on ShaderToy

Horizon Outrun-esque scene

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.