Pular para o conteúdo
Fundamento · TempoIniciante

Uniform de tempo

Os segundos decorridos, enviados a cada quadro, movem toda animação; escale, desloque e repita esse valor, e mantenha-o pequeno o bastante para continuar preciso.

Como funciona

Um shader não tem relógio. A cada quadro ele roda do zero, sem lembrar do anterior, então tudo o que se mexe precisa de um número vindo de fora que mude entre os quadros. Esse número é o tempo decorrido em segundos, enviado como um uniform: o mesmo valor para todos os pixels de um quadro, um pouco maior no seguinte.

De onde ele vem

Na web, o código do host lê o tempo e envia antes de cada desenho. O requestAnimationFrame chama de volta uma vez por atualização da tela, com um instante em milissegundos:

function frame(now) {               // now: milissegundos desde que a página carregou
  gl.useProgram(program);
  gl.uniform1f(timeLocation, now / 1000); // segundos
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// no escopo do arquivo:
uniform float uTime; // segundos

As engines já entregam esse valor: TIME no Godot, _Time.y na Unity, o nó Time na Unreal e no Shader Graph da Unity. Os cards deste site recebem como i.time. Como o valor é o tempo real decorrido e não uma contagem de quadros, a animação anda na mesma velocidade a 30 e a 144 quadros por segundo.

Velocidade, deslocamento, loop

Os segundos crus raramente são o valor que você quer, então a primeira linha da maioria dos shaders animados dá outra forma a eles:

float t = uTime * 0.5;               // velocidade: metade; negativo anda para trás
float d = uTime + 1.5;               // deslocamento: 1.5 segundo adiantado
float loop = fract(uTime / 4.0);     // loop: de 0 a 1 a cada 4 segundos, depois volta a 0
float cycle = mod(uTime, 4.0);       // o mesmo loop, contado em segundos (0 a 4)

Um deslocamento diferente por pixel ou por objeto quebra o movimento sincronizado: dê a cada célula de uma grade o próprio deslocamento e as células param de pulsar juntas. Um loop dá à animação um progresso de 0 a 1 que recomeça, e as páginas de easing e de funções periódicas transformam isso em movimento. Tudo o que precisa pausar, voltar ou tocar uma vez só fica melhor no host, que pode parar o relógio ou mandar um valor de progresso próprio.

Precisão: o tempo fica grosseiro

Um float de 32 bits tem 24 bits de precisão, então a distância entre valores vizinhos cresce com o valor. Perto de 1 segundo, a distância é de cerca de um décimo de milionésimo de segundo. Depois de uma hora (3600 s), fica em uns 0.0002 s, ainda tranquilo. Depois de um dia (86 400 s), fica em uns 0.008 s, meio quadro a 60 quadros por segundo, e qualquer coisa movida por sin(uTime * 20.0) começa a engasgar. Um ruído rolado pelo tempo fica visivelmente aos pulos antes disso, porque multiplica o erro.

Dois hábitos mantêm o tempo preciso:

  • Dê a volta no host. Os números do JavaScript têm 64 bits, então o host consegue manter o tempo pequeno antes de ele virar um float de 32 bits: envie (now / 1000) % 3600 e faça cada loop do shader dividir a hora por igual. Dar a volta dentro do shader é tarde demais; a precisão se perdeu quando o valor foi enviado.
  • Mantenha em highp. Num shader de fragmento, declare precision highp float; ou uniform highp float uTime;. Um float mediump pode ser um float de 16 bits em GPUs de celular, com degraus de uns 0.03 s depois de só 32 segundos.

Quadros contados como inteiro (uniform int uFrame) não têm problema de precisão e servem para efeitos que devem mudar exatamente uma vez por quadro, como padrões de amostragem com jitter, mas aceleram e desaceleram junto com a taxa de quadros.

Leitura complementar