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) % 3600and 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;oruniform highp float uTime;. Amediumpfloat 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.