Pular para o conteúdo
Renderização · Etapas do pipelineIntermediário

Shader de vértice

O programa que roda uma vez por vértice, posiciona-o na tela e entrega valores ao shader de fragmento.

Demo ao vivo em breve

Como funciona

Uma malha é uma lista de vértices: posições, em geral cada uma com uma normal e uma UV, agrupadas em triângulos. Antes de qualquer coisa ser desenhada, cada vértice precisa ser posicionado na tela. É isso que o shader de vértice faz. Ele roda uma vez por vértice, lê os atributos desse vértice e precisa escrever gl_Position: onde o vértice vai parar, num sistema de coordenadas chamado espaço de recorte (clip space). Qualquer outra coisa que ele queira que o shader de fragmento saiba, ele escreve como um varying.

Do modelo até a tela

A posição de um vértice começa nas coordenadas do próprio modelo (espaço do objeto): os números que o artista 3D exportou, com a origem do modelo em (0, 0, 0). Levá-la até a tela é uma cadeia de produtos de matrizes:

  1. a matriz de modelo coloca o objeto no mundo (espaço do mundo);
  2. a matriz de visão move o mundo para que a câmera fique na origem, olhando para −z (espaço da câmera);
  3. a matriz de projeção espreme o volume visto pela câmera no espaço de recorte e, numa câmera em perspectiva, guarda a distância em w.
#version 300 es
in vec3 aPosition;
in vec3 aNormal;
uniform mat4 uModel;
uniform mat4 uViewProjection; // visão e projeção, multiplicadas uma vez no host
out vec3 vWorldNormal;

void main() {
    vec4 world = uModel * vec4(aPosition, 1.0);
    vWorldNormal = mat3(uModel) * aNormal; // serve enquanto o modelo não tiver escala não uniforme
    gl_Position = uViewProjection * world;
}

O shader de vértice para no espaço de recorte. A divisão por w, que deixa as coisas distantes menores, acontece em seguida, na etapa fixa de rasterização.

Movendo vértices

Como a posição é dele, o shader de vértice também pode movê-la. Ondas, bandeiras ao vento, grama balançando e criaturas respirando são todos deslocamento de vértices: calcule um deslocamento a partir do tempo e da posição e some antes de projetar.

// uma ondulação ao longo da normal da superfície, antes das matrizes
float height = 0.05 * sin(aPosition.x * 8.0 + uTime * 2.0);
vec3 moved = aPosition + aNormal * height;

Três limites moldam todo efeito feito desse jeito:

  • Ele vê um vértice de cada vez. Não enxerga os vizinhos, não cria nem remove vértices.
  • O detalhe vem da malha. Um quad tem quatro vértices, então nenhum deslocamento faz ele ondular; uma bandeira precisa de um plano subdividido. O shader de fragmento consegue fingir detalhes pequenos, mas a silhueta só muda onde existem vértices.
  • As normais não acompanham. Mover os vértices deixa as normais guardadas apontando para o lado antigo, e a iluminação continua mostrando a superfície plana. O shader precisa recalcular a normal, em geral a partir da derivada da mesma função que moveu o vértice.

Barato, depois caro

Uma malha costuma ter bem menos vértices do que os pixels que ela cobre, então um trabalho feito por vértice e interpolado sai mais barato do que o mesmo trabalho por pixel. Por isso a iluminação já foi calculada por vértice. O problema é que a interpolação é linear: qualquer coisa que curve mais rápido que os triângulos, como um brilho especular fino, sai errada.

O shader de vértice também desenha passadas de tela cheia. Sem buffer nenhum, ele consegue montar um triângulo que cobre a tela inteira a partir de gl_VertexID, o índice do vértice sendo processado:

#version 300 es
out vec2 vUv;

void main() {
    // vértices 0, 1, 2 → (0, 0), (2, 0), (0, 2): um triângulo com o dobro do tamanho da tela
    vec2 corner = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
    vUv = corner;
    gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}

As partes fora da tela são recortadas, e a parte visível fica com UVs de 0 a 1.

No Godot e neste site

Num shader spatial do Godot, a etapa de vértice é a função vertex(). VERTEX e NORMAL chegam no espaço do modelo, e o Godot aplica as matrizes de modelo, visão e projeção depois que a função termina. render_mode world_vertex_coords entrega os dois no espaço do mundo, render_mode skip_vertex_transform deixa as transformações de modelo e de visão com você (o Godot ainda aplica a projeção), e escrever POSITION define a posição no espaço de recorte diretamente.

Neste site, um card que move vértices declara uma entrada de vértice no seu render.json: Inputs <nome>(Inputs i), que devolve i com uma nova position e uma nova normal no espaço do mundo. A moldura projeta o resultado e o interpola até o effect().

Leitura complementar