Trilha 15 · Gráficos 2D e 3D

Rasterização

Como triângulos matemáticos se transformam em pixels discretos — o algoritmo central de toda GPU, desde consoles até placas de vídeo modernas.

① Intuição

Triângulos são primitivas universais

Qualquer superfície 3D pode ser aproximada por triângulos. Um modelo humano num jogo tem decenas de milhares de triângulos. Uma cena de filme Pixar pode ter bilhões. Por que triângulos? Porque são a forma mais simples com área — e sempre planares (três pontos sempre definem um plano único).

O processo de converter um triângulo em pixels é chamado de rasterização. O algoritmo clássico usa scanlines: para cada linha horizontal, encontra onde as arestas do triângulo a intersectam, e preenche todos os pixels entre essas interseções. GPUs modernas usam o teste baricêntrico, que é mais paralelizável.

Por triângulos e não quadrados? Um quadrado com vértices em posições arbitrárias pode ser não-planar (os 4 pontos não ficam no mesmo plano). Isso cria ambiguidade na rasterização. Triângulos são sempre planares — nunca há ambiguidade. Quads são decompostos em 2 triângulos antes de rasterizar.
② Visualização interativa

Veja a rasterização acontecer

Escolha um triângulo e clique em Rasterizar. O algoritmo percorre cada linha horizontal (scanline) e preenche os pixels dentro do triângulo.

GRID DE PIXELS — 22×16
A(3,2)B(18,4)C(9,13)
ALGORITMO SCANLINE
// y de 2 a 13
for y = yMin to yMax:
xL = interseção_esq(y)
xR = interseção_dir(y)
for x = xL to xR:
pintar pixel(x, y)
TESTE BARICÊNTRICO
Para cada pixel (x+0.5, y+0.5): calcule os 3 cross products das arestas. Se todos tiverem o mesmo sinal → pixel dentro do triângulo.
PROGRESSO
0 pixels preenchidos
de 81 totais
③ Explicação técnica

Scanline e teste baricêntrico

// Rasterização por scanline
function rasterize(A, B, C, paintPixel) {
  const yMin = Math.min(A.y, B.y, C.y);
  const yMax = Math.max(A.y, B.y, C.y);

  for (let y = yMin; y <= yMax; y++) {
    // Encontrar onde as arestas cortam a linha y
    const xIntersections = getEdgeIntersections(A, B, C, y);
    const xL = Math.min(...xIntersections);
    const xR = Math.max(...xIntersections);

    for (let x = xL; x <= xR; x++) {
      paintPixel(x, y);
    }
  }
}

Teste baricêntrico (usado na GPU)

// Teste baricêntrico (moderno — paralelizável na GPU)
function edgeFn(ax, ay, bx, by, px, py) {
  return (bx - ax) * (py - ay) - (by - ay) * (px - ax);
}

function insideTriangle(A, B, C, px, py) {
  // Testa o pixel no centro (px+0.5, py+0.5)
  const d0 = edgeFn(A.x, A.y, B.x, B.y, px, py);
  const d1 = edgeFn(B.x, B.y, C.x, C.y, px, py);
  const d2 = edgeFn(C.x, C.y, A.x, A.y, px, py);

  // Se todos positivos (ou todos negativos): dentro!
  return (d0 >= 0 && d1 >= 0 && d2 >= 0)
      || (d0 <= 0 && d1 <= 0 && d2 <= 0);
}

// Vantagem: GPU testa TODOS os pixels de uma bounding box
// em paralelo — sem o loop serial de scanline.
// Além disso, d0/d1/d2 são as coordenadas baricêntricas
// que permitem interpolar cor, UV e normal por pixel.
Coordenadas baricêntricas e interpolação: os três valores d0, d1, d2 do teste baricêntrico (normalizados para somar 1) são as coordenadas baricêntricas do pixel. Elas dizem "quão próximo estou de cada vértice". Isso permite interpolar qualquer atributo de vértice (cor, UV, normal) suavemente pela superfície do triângulo — o que o hardware de rasterização faz automaticamente antes de passar o fragmento ao fragment shader.
④ Projeto para programar

Rasterizador em JavaScript

Mini projeto: implemente fillTriangle(ctx, A, B, C, color) usando o algoritmo de scanline — sem usar ctx.fill() do Canvas. Confirme visualmente que o resultado é idêntico ao método nativo.

Projeto principal: implemente Gouraud shading: cada vértice tem uma cor diferente. Use coordenadas baricêntricas para interpolar a cor de cada pixel. Renderize um triângulo com vértices vermelho, verde e azul — o centro deve ser cinza.

Desafio extra: implemente um Z-buffer manual: mantenha um array zBuffer[y][x] com profundidade infinita inicial. Ao rasterizar cada triângulo, interpole Z baricêntricamente e só pinte o pixel se o novo Z for menor que o armazenado. Teste com dois triângulos sobrepostos.

⑤ Exercícios rápidos

Teste sua intuição

Por que todo motor gráfico 3D usa triângulos como primitiva fundamental?
No teste baricêntrico, o que significa quando os três valores edgeFn() têm o mesmo sinal (todos positivos ou todos negativos)?
O que é o Z-buffer e como ele resolve a sobreposição de triângulos?
⑥ Aplicações no mundo real

Onde você encontra isso

🟥

Triangle strips e fans

Meshes otimizadas usam "triangle strips" onde triângulos adjacentes compartilham vértices — cada novo triângulo precisa só de 1 vértice extra (não 3). Uma tira de N triângulos usa N+2 vértices no lugar de 3N. Reduz largura de banda de memória em 3x.

🖥️

Rasterização vs. ray tracing

Rasterização projeta e processa triângulos um de cada vez. Ray tracing lança raios da câmera e intersecta com todos os objetos — muito mais realista (reflexos, refrações corretos) mas muito mais lento. GPUs RTX têm hardware dedicado para ray tracing (RT cores).

📱

Tile-based rendering (mobile)

GPUs de celular (Mali, Adreno) dividem a tela em tiles de 16×16 pixels e rasterizam tile por tile — tudo na memória on-chip ultra-rápida, sem precisar ir à RAM externa a cada pixel. Isso economiza ~5x de energia comparado ao rasterizador "imediato" de GPUs desktop.

← Anterior: Projeção 3D Próxima: Pipeline gráfico →