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

Montagem de primitivas e rasterização

Como a GPU transforma triângulos no espaço de recorte em fragmentos e mistura as saídas dos vértices para cada um.

Demo ao vivo em breve

Como funciona

Entre o shader de vértice e o shader de fragmento fica uma etapa que você não programa: a GPU a executa em hardware fixo, configurado por algumas chaves. Ela recebe vértices no espaço de recorte e produz fragmentos, um para cada pixel que um triângulo cobre, cada um com as suas entradas interpoladas. Entender o que acontece aqui explica por que as UVs não escorregam num plano inclinado, por que normais interpoladas pedem normalize() e por que as bordas saem serrilhadas.

Dos vértices a um triângulo na tela

  1. A montagem de primitivas agrupa os vértices em triângulos, do jeito que a chamada de desenho pediu: com gl.TRIANGLES, cada três vértices formam um; com gl.TRIANGLE_STRIP, cada vértice novo forma um triângulo com os dois anteriores.
  2. O recorte (clipping) mantém só o que está dentro do volume visível, onde −w ≤ x, y, z ≤ w. Um triângulo que cruza a borda é cortado em triângulos menores que não cruzam; um que está todo fora é descartado.
  3. A divisão de perspectiva transforma o espaço de recorte em coordenadas normalizadas de dispositivo (NDC): xyz / w. As três terminam entre −1 e 1 e, como uma projeção em perspectiva guarda a distância em w, o que está longe encolhe em direção ao centro.
  4. A transformação de viewport leva as NDC para pixels, conforme gl.viewport(x, y, width, height): −1..1 vira 0..largura e 0..altura, e z vira uma profundidade entre 0 e 1.
  5. O descarte de faces (face culling), quando ligado, elimina triângulos pelo lado para o qual estão virados. A ordem dos três vértices na tela, anti-horária por padrão, diz qual lado é a frente; gl.enable(gl.CULL_FACE) com gl.cullFace(gl.BACK) pula as faces de trás de malhas fechadas, que não apareceriam de qualquer jeito.

Quais pixels um triângulo cobre

Em seguida o rasterizador testa os centros dos pixels contra as três arestas do triângulo. Todo centro que cai dentro vira um fragmento. Um centro que cai exatamente sobre uma aresta compartilhada por dois triângulos tem que ir para exatamente um deles: a especificação do OpenGL ES exige que nenhum pixel seja perdido nem desenhado duas vezes ao longo de uma aresta compartilhada, então as malhas não têm frestas e malhas transparentes não mostram costuras dobradas.

Uma amostra por pixel significa que o pixel está dentro ou fora, e é daí que vêm as bordas serrilhadas, em escadinha. O multisampling (MSAA) testa vários pontos de amostra por pixel; como essas amostras são guardadas e combinadas é assunto das operações por amostra, que vêm depois.

Interpolação

Cada fragmento recebe o seu próprio valor de cada varying, misturado a partir dos três vértices do triângulo conforme a proximidade do fragmento a cada um. Um triângulo com um vértice vermelho, um verde e um azul mostra todas as misturas entre eles. Os pesos corrigem a perspectiva: o hardware divide por w enquanto mistura, então uma textura num chão que se estende ao longe mantém as linhas retas em vez de entortá-las.

A interpolação é linear, e isso tem consequências:

  • Vetores unitários encolhem. A mistura de duas normais de comprimento 1 é mais curta que 1, então o código de iluminação chama normalize() nas normais interpoladas antes de usá-las.
  • Curvas viram retas. Um valor calculado por vértice só está certo nos vértices; entre eles é uma mistura plana. É por isso que um brilho calculado por vértice fica facetado.
  • Alguns valores não deveriam ser misturados. Um id ou o índice de uma célula não faz sentido pela metade. O qualificador flat desliga a interpolação de um varying: todo fragmento recebe o valor de um único vértice, o último de cada triângulo no OpenGL ES.
// no shader de vértice
flat out int vMaterialId; // sem mistura: o triângulo inteiro recebe o valor de um vértice
out vec3 vNormal;         // misturado, com correção de perspectiva (o padrão, "smooth")

// no shader de fragmento
flat in int vMaterialId;
in vec3 vNormal;
// ...
vec3 n = normalize(vNormal); // mais curto que 1 entre os vértices, então normalize primeiro

Varyings inteiros precisam ser flat; o GLSL ES 3.00 não os interpola.

No Godot e neste site

A rasterização também não é programável no Godot. Um shader controla as partes que pode: o descarte de faces com render_mode cull_back (o padrão), cull_front ou cull_disabled para superfícies de dois lados, e o que chega ao fragment() por declarações varying.

Neste site, o cull de uma passada no render.json é "back", "front" ou "none". O contorno por casca invertida usa "front": ele desenha uma cópia levemente inflada da malha com as faces da frente descartadas, então só as faces de trás aparecem em volta da silhueta.

Leitura complementar