Custo e precisão
O que deixa um shader lento, o que o deixa errado e como ler a linha de custo de cada card.
Como funciona
Um shader de fragmentoO programa da GPU que roda uma vez para cada pixel coberto por um triângulo e decide a cor dele. Shader de fragmento O programa da GPU que roda uma vez para cada pixel coberto por um triângulo e decide a cor dele.
O que custa o quê
- Aritmética é o trabalho mais barato: somas, multiplicações,
mix,dot,clamp. As GPUs são feitas para fazer muito disso em paralelo. Funções comosin,exp,powesqrtcustam algumas vezes mais e ainda assim são baratas perto da memória. - Leituras de textura buscam dados na memória. A GPU esconde boa parte da espera trabalhando em outros pixels, mas toda leitura consome banda, e muitas leituras por pixel (um blur largo, um kernel grande) são o motivo mais comum de um efeito ser lento. Uma leitura dependente, cujas coordenadas vêm de uma leitura anterior ou de contas pesadas, não pode começar antes, e é mais lenta em algumas GPUs de celular.
- Desvios são baratos quando pixels vizinhos vão para o mesmo lado. As GPUs rodam os pixels em grupos que executam
em sincronia, então quando os pixels de um grupo discordam, o grupo roda os dois lados e guarda os resultados de que
precisa. Um
ifsobre um uniform sai praticamente de graça; umifsobre ruído pode custar os dois lados. - Laços custam cada iteração. Um laço com contagem fixa é fácil para o compilador; um laço que roda conforme um valor calculado por pixel se comporta como um desvio divergente.
Pixels e passes
Quantos pixels você sombreia muitas vezes importa mais do que como você os sombreia.
- OverdrawSombrear o mesmo pixel mais de uma vez num quadro, como fazem camadas transparentes sobrepostas. Cada camada roda o shader de fragmento de novo.Fundamento
Overdraw
Sombrear o mesmo pixel mais de uma vez num quadro, como fazem camadas transparentes sobrepostas. Cada camada roda o shader de fragmento de novo.
Fontes. Cada camada de geometria transparente roda o shader de fragmento de novo nos mesmos pixels. Dez partículas sobrepostas custam dez vezes uma. Geometria opaca desenhada da frente para trás deixa o teste de profundidade descartar pixels escondidos antes de sombreá-los, mas- GPU optimization (abre em uma nova aba)Documentação
- WebGL best practices (abre em uma nova aba)Documentação
discarde escrever emgl_FragDepthpodem desligar esse teste antecipado. - Passes. Um pass de pós-processamento lê e escreve uma imagem de tela cheia. Cada pass a mais acrescenta essa banda, mesmo que o shader dele seja trivial. Renderizar um pass na metade da resolução reduz os pixels a um quarto.
- Filtros separáveis. Um blur que pondera uma vizinhança de 9×9 lê 81 texels por pixel. Dividi-lo num pass horizontal de 9 e um vertical de 9 lê 18, ao preço de um pass a mais. Para qualquer coisa maior que alguns texels a divisão ganha, e é por isso que o blur gaussiano separável usa dois passes.
Meça, não chute
Os compiladores reorganizam o código e as GPUs diferem, então raciocinar sobre custo só leva até certo ponto. Um teste
rápido: diminua o canvas ou a resolução de renderização. Se o tempo de quadro cai junto, o shader de fragmento é o
gargalo. As engines têm profilers (o Visual Profiler do Godot mostra o tempo de GPU por etapa), e o WebGL 2 pode medir
o trabalho da GPU com a extensão EXT_disjoint_timer_query_webgl2 quando o navegador a oferece.
Cada card deste site tem uma linha de custo: operações matemáticas, leituras de textura e passes. Elas são contadas a partir do código do efeito, não cronometradas, então leia-as como uma comparação entre cards, não como milissegundos. Leituras de textura e passes pesam mais que operações matemáticas.
Precisão
Os floats do GLSL vêm em três precisões. highp é um float de 32 bits na maioria do hardware. mediump só garante
uma precisão relativa de cerca de 1 parte em 1000 e um alcance de cerca de ±16.000. GPUs de desktop calculam tudo em
precisão total, seja lá o que você declarar, então um problema de mediump muitas vezes só aparece no celular.
Declare precision highp float; a menos que você tenha medido um motivo para não fazer isso; o GLSL ES 3.00 exige que
os shaders de fragmento o suportem.
Até o highp acaba. Um float de 32 bits tem 24 bits de mantissa, então a distância entre valores vizinhos cresce com
o tamanho do número. Um uniform time contando segundos vai bem por minutos, mas depois de um dia (86.400 s) essa
distância é de cerca de 0,008 s, e sin(time * 10.0) anda aos saltos de forma visível. Coordenadas de mundo grandes
têm o mesmo problema no espaço. Faça o tempo dar a volta na CPU, num intervalo em que a sua animação se repete, e
mantenha as coordenadas perto da origem.
NaN e infinito
Algumas operações não têm resposta real, e a GPU devolve NaN (not a number, “não é um número”) ou infinito em vez de falhar:
sqrt(x)elog(x)de um número negativo,pow(x, y)comxnegativo- divisão por zero, incluindo o
normalizede um vetor nulo asineacosfora de -1..1
O NaN se espalha por toda operação em que encosta. Num pixel isolado ele aparece como um pontinho preto ou branco;
passe esse pixel por um blur ou um bloom e ele vira um bloco. Limite as entradas onde elas podem sair do intervalo,
por exemplo Mostrar valores como cor Escrever um valor intermediário na cor de saída para enxergá-lo, remapeado para 0–1 antes quando preciso. A principal ferramenta de depuração de um shader.sqrt(max(x, 0.0)) e pow(max(x, 0.0), k), e some um epsilon pequeno antes de dividir por algo que pode
chegar a zero. Para achar onde um NaN começa, pinte-o de uma cor visível (veja
mostrar valores como corEscrever um valor intermediário na cor de saída para enxergá-lo, remapeado para 0–1 antes quando preciso. A principal ferramenta de depuração de um shader.color = v × 0.5 + 0.5 (v ∈ −1..1)