Shader de vértice
O programa que roda uma vez por vértice, posiciona-o na tela e entrega valores ao shader de fragmento.
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 Varying Um valor que o shader de vértice escreve para cada vértice (um out) e o shader de fragmento lê (um in). O rasterizador o mistura ao longo do triângulo, então cada pixel recebe a sua própria mistura.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 varyingUm valor que o shader de vértice escreve para cada vértice (um out) e o shader de fragmento lê (um in). O rasterizador o mistura ao longo do triângulo, então cada pixel recebe a sua própria mistura.
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:
- a matriz de modelo coloca o objeto no mundo (espaço do mundo);
- a matriz de visão move o mundo para que a câmera fique na origem, olhando para −z (espaço da câmera);
- 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 Rasterizaçã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.w, que deixa as coisas distantes menores, acontece em
seguida, na etapa fixa de rasterizaçãoA 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.
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().