Skip to content
Fundamental · TimeBeginner

Time uniform

Elapsed seconds, passed in every frame, drive all motion; scale, offset and loop them, and keep them small enough to stay precise.

How it works

A shader has no clock. Each frame it runs from scratch with no memory of the last one, so anything that moves needs a number from outside that changes between frames. That number is the elapsed time in seconds, passed in as a uniform: the same value for every pixel in a frame, a little larger in the next.

Where it comes from

On the web the host code reads the time and uploads it before each draw. requestAnimationFrame calls back once per display refresh with a timestamp in milliseconds:

function frame(now) {               // now: milliseconds since the page loaded
  gl.useProgram(program);
  gl.uniform1f(timeLocation, now / 1000); // seconds
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// at file scope:
uniform float uTime; // seconds

Engines provide it already: TIME in Godot, _Time.y in Unity, the Time node in Unreal and in Unity’s Shader Graph. The cards on this site receive it as i.time. Because the value is real elapsed time and not a frame count, the motion runs at the same speed at 30 and at 144 frames per second.

Speed, offset, loop

Raw seconds are rarely the value you want, so the first line of most animated shaders reshapes them:

float t = uTime * 0.5;               // speed: half speed; negative runs backward
float d = uTime + 1.5;               // offset: 1.5 seconds ahead
float loop = fract(uTime / 4.0);     // loop: 0 to 1 every 4 seconds, then back to 0
float cycle = mod(uTime, 4.0);       // the same loop, counted in seconds (0 to 4)

An offset that differs per pixel or per object breaks up lockstep motion: give every cell of a grid its own offset and the cells stop pulsing together. A loop gives an animation a progress from 0 to 1 that restarts, which the easing and periodic pages turn into motion. Anything that should pause, rewind or play once is better handled by the host, which can stop the clock or send a progress value of its own.

Precision: time gets coarse

A 32-bit float has 24 bits of precision, so the gap between neighboring values grows with the value. Near 1 second the gap is about a ten-millionth of a second. After an hour (3,600 s) it is about 0.0002 s, still fine. After a day (86,400 s) it is about 0.008 s, half a frame at 60 frames per second, and anything driven by sin(uTime * 20.0) starts to stutter. Noise scrolled by time gets visibly jumpy even earlier, because it multiplies the error.

Two habits keep time precise:

  • Wrap it on the host. JavaScript numbers are 64-bit, so the host can keep time small before it becomes a 32-bit float: upload (now / 1000) % 3600 and make every loop in the shader divide the hour evenly. Wrapping inside the shader is too late; the precision was lost when the value was uploaded.
  • Keep it highp. In a fragment shader, declare precision highp float; or uniform highp float uTime;. A mediump float may be a 16-bit float on mobile GPUs, with steps of about 0.03 s after just 32 seconds.

Frames counted as an integer (uniform int uFrame) have no precision problem and suit effects that should change exactly once per frame, like jittered sampling patterns, but they speed up and slow down with the frame rate.

Further reading