Pular para o conteúdo
Renderização · Etapas do pipelineIniciante

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.

Demo ao vivo em breve

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értice e um shader de fragmento, e a GPU roda os dois milhares ou milhões de vezes em paralelo durante essa única chamada. Cada execução enxerga só os próprios dados, então todo o ofício de escrever shaders começa por saber quais dados chegam a cada etapa, e como.

Quatro tipos de dado

  1. 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.
  2. Uniforms são por chamada de desenho: o tempo, o tamanho do canvas, uma cor, uma matriz de transformação. O host os define com gl.uniform*() antes de desenhar, e todas as execuções dos dois shaders veem o mesmo valor. Texturas também chegam assim, como uniforms sampler2D.
  3. 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ê um in com o mesmo nome e tipo, e no meio do caminho o rasterizador mistura os valores dos três vértices para cada pixel do triângulo.
  4. Saídas deixam o shader de fragmento com out vec4 e 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ções vertex() e fragment() e conversa com ele por variáveis embutidas: lê UV, NORMAL e TIME, escreve VERTEX em vertex() e ALBEDO ou ALPHA em fragment(). Um uniform que você declara vira uma propriedade no inspetor do material (um slider, com hint_range), e um varying leva 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.normal e 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 UV).

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.

Leitura complementar