Anatomia de um shader e fluxo de dados
O que entra num shader e o que sai dele, do buffer de vértices até a cor na tela.
Como funciona
Um shader nunca roda sozinho. Um código na CPU (JavaScript, no WebGL2) prepara tudo e depois faz uma chamada de
desenho: “desenhe estes 3000 vértices como triângulos, com este programa”. O programa é um par de shaders ligados, um
shader de vérticeO programa da GPU que roda uma vez para cada vértice de uma malha. Ele posiciona o vértice na tela escrevendo gl_Position e repassa valores para o shader de fragmento. Shader de vértice O programa da GPU que roda uma vez para cada vértice de uma malha. Ele posiciona o vértice na tela escrevendo gl_Position e repassa valores para o shader de fragmento. Shader de fragmento O programa da GPU que roda uma vez para cada pixel coberto por um triângulo e decide a cor dele.
Quatro tipos de dado
- Atributos são por vértice: uma posição, uma normal, uma UV, lidas de buffers que o código host preencheu. O
shader de vértice os declara com
in, e cada execução recebe os valores de um vértice. - UniformsUm valor que o código host define antes de uma chamada de desenho e que fica igual para todos os vértices e pixels dela, como o tempo, o tamanho do canvas ou uma cor.Renderização
Uniform
Um valor que o código host define antes de uma chamada de desenho e que fica igual para todos os vértices e pixels dela, como o tempo, o tamanho do canvas ou uma cor.
são por chamada de desenho: o tempo, o tamanho do canvas, uma cor, uma matriz de transformação. O host os define comgl.uniform*()antes de desenhar, e todas as execuções dos dois shaders veem o mesmo valor. Texturas também chegam assim, como uniformssampler2D. - Varyings levam dados do shader de vértice para o shader de fragmento. O shader de vértice escreve um
out, o shader de fragmento lê umincom o mesmo nome e tipo, e no meio do caminho o rasterizadorA etapa fixa da GPU que transforma cada triângulo em fragmentos, um para cada centro de pixel que ele cobre, e interpola as saídas do shader de vértice para cada um deles.RenderizaçãoRasterização
A etapa fixa da GPU que transforma cada triângulo em fragmentos, um para cada centro de pixel que ele cobre, e interpola as saídas do shader de vértice para cada um deles.
mistura os valores dos três vértices para cada pixel do triângulo. - Saídas deixam o shader de fragmento com
out vec4e caem no framebuffer: o canvas, ou uma textura em que o host escolheu desenhar.
Alguns valores são embutidos em vez de declarados. O shader de vértice precisa escrever gl_Position, o lugar da tela
onde o vértice vai parar. O shader de fragmento pode ler gl_FragCoord, a posição do pixel na janela.
Um programa completo
Este é o menor par útil em GLSL ES 3.00, a linguagem do WebGL2. O shader de vértice repassa a UV e posiciona o vértice; o shader de fragmento transforma a UV interpolada numa cor que pulsa com o tempo:
#version 300 es
// shader de vértice
in vec3 aPosition; // atributo: a posição deste vértice
in vec2 aUv; // atributo: a UV deste vértice
uniform mat4 uModelViewProjection; // uniform: igual para o desenho inteiro
out vec2 vUv; // varying: entregue ao shader de fragmento
void main() {
vUv = aUv;
gl_Position = uModelViewProjection * vec4(aPosition, 1.0);
}
#version 300 es
// shader de fragmento
precision highp float;
in vec2 vUv; // o varying, interpolado para este pixel
uniform float uTime; // segundos, definidos pelo host a cada quadro
out vec4 fragColor; // o que cai no framebuffer
void main() {
float pulse = 0.5 + 0.5 * sin(uTime);
fragColor = vec4(vUv, pulse, 1.0);
}
Algumas linhas aqui são cerimônia que o compilador exige. #version 300 es tem que ser a primeiríssima linha, sem
nada antes, nem comentário nem linha em branco; um shader sem ela é lido como o antigo GLSL ES 1.00. O shader de
fragmento não tem precisão padrão para floats, então precision highp float; é obrigatório nele; o shader de vértice
já usa highp por padrão. Na hora de ligar o programa, cada in do shader de fragmento precisa casar com um out do
shader de vértice em nome e tipo, ou a ligação falha.
Os mesmos dados, escondidos
A maioria das ferramentas escreve essa cerimônia por você e entrega só a parte interessante:
- Páginas no estilo Shadertoy desenham um retângulo sobre o canvas inteiro e chamam a sua
mainImage(out vec4 fragColor, in vec2 fragCoord)para cada pixel, com uniforms como o tempo e a resolução já declarados. - O Godot escreve o
main(), os atributos e as matrizes sozinho. Você preenche as funçõesvertex()efragment()e conversa com ele por variáveis embutidas: lêUV,NORMALeTIME, escreveVERTEXemvertex()eALBEDOouALPHAemfragment(). Umuniformque você declara vira uma propriedade no inspetor do material (um slider, comhint_range), e umvaryingleva os seus próprios dados de uma função para a outra. - Os cards deste site vão um passo além: você escreve
vec4 effect(Inputs i)e lêi.uv,i.time,i.normale o resto de uma única struct. A moldura do site declara os atributos, uniforms e varyings de verdade ao redor dela, tanto para o WebGL2 quanto para o Godot (veja coordenadas UVUma posição 2D numa superfície ou na tela, em geral de 0 a 1 nos dois eixos. Shaders desenham formas e padrões como funções dela.FundamentoCoordenadas UV
Uma posição 2D numa superfície ou na tela, em geral de 0 a 1 nos dois eixos. Shaders desenham formas e padrões como funções dela.
uv = gl_FragCoord.xy / resolutionFontes).- gl_FragCoord (abre em uma nova aba)Referência
- UV mapping (abre em uma nova aba)Referência
Os nomes mudam de ferramenta para ferramenta, mas o fluxo não: dados por vértice entrando, configurações por desenho ao lado, valores passados do vértice para o fragmento, uma cor saindo.