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) % 3600e 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;ouuniform highp float uTime;. Um floatmediumppode 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.