Shaders são pequenos programas executados na GPU como parte do processo de renderização de uma cena. Uns decidem onde a geometria aparece na tela, enquanto outros decidem a aparência dessa geometria depois de desenhada. A primeira distinção útil nesse processo é entre vertex shaders e fragment shaders. Um vertex shader executa uma vez para cada vértice na sua malha, enquanto um fragment shader executa para cada fragmento gerado dentro da cobertura do triângulo na tela, aproximadamente um por amostra de pixel coberta. Esses dois estágios fazem trabalhos diferentes e executam em taxas muito distintas, o que explica grande parte do comportamento, custo e resultado visual que você observa na renderização em tempo real.
Este artigo constrói um modelo concreto de onde cada estágio roda, quais dados lê e quais dados produz. Ele começa com a divisão entre vértice e fragmento porque essa é a base para o restante do pipeline gráfico. Mais adiante, uma vez que essa base esteja clara, posicionamos geometry shaders, tessellation shaders e compute shaders em contexto.
Estágios de Shader no Pipeline Gráfico
A maioria dos mecanismos 3D em tempo real segue um pipeline de rasterização, ou seja, eles partem de dados de triângulos e os transformam na imagem 2D que você vê na tela. Nesse processo, os vértices entram primeiro, depois se tornam primitivas como triângulos, então a rasterização converte esses triângulos em fragmentos e, por fim, os resultados sobreviventes são escritos no framebuffer, que é o buffer de imagem que armazena o quadro atual antes de ser exibido. Os dois estágios programáveis centrais nesse caminho são o vertex shader antes da rasterização e o fragment shader após a rasterização. Estágios opcionais de shader podem adicionar, refinar ou computar dados ao redor desse caminho, mas a passagem do vértice para o fragmento é a base para a renderização comum de triângulos. Portanto, a pergunta mais importante é: o que cada estágio recebe como entrada?
- Entrada do vertex shader: um vértice por vez (posição, normal, UV, tangentes, atributos personalizados)
- Saída do vertex shader: posição transformada no clip-space mais valores por vértice que serão suavemente interpolados pelo triângulo, como cor, normais ou UVs
- Entrada do fragment shader: esses valores interpolados para cada fragmento, mais texturas, uniforms e parâmetros de material
- Saída do fragment shader: um ou mais valores de cor/profundidade escritos nos render targets
Conforme o triângulo se expande pelo painel direito, a diferença entre o trabalho de vértice e o trabalho de fragmento fica muito mais fácil de perceber.
O triângulo ainda tem apenas três vértices, mas a cobertura rasterizada atinge muito mais células da tela, então o trabalho de fragmento cresce muito mais rápido que o trabalho de vértice.
Mudar para Optional stages posiciona tessellation, geometry e compute em relação a esse caminho principal entre vértice e fragmento.
Na visualização de estágios opcionais, tessellation e geometry devem ser entendidos como processamento extra que acontece antes da rasterização, não entre a rasterização e o fragment shader. Compute shaders são separados: são programas de GPU, mas não fazem parte desse caminho de triângulo a fragmento.
Mesmo quando o triângulo permanece pequeno, alternar entre Vertex work e Fragment work mostra que a ordem dos estágios não muda com a carga de trabalho.
Quando o triângulo preenche mais o painel do espaço de tela, o lado do fragmento cresce em densidade visual enquanto a quantidade de vértices dos cantos permanece inalterada.
A visão Optional stages então deixa clara a distinção estrutural: tessellation e geometry ainda alimentam a rasterização, enquanto compute permanece à parte em seu próprio caminho de execução.
Uma maneira prática de lembrar disso é acompanhar onde a contagem explode. Uma malha pode ter dezenas de milhares de vértices, mas um desenho em tela cheia pode atingir milhões de fragmentos. Como a contagem de fragmentos costuma ser muito maior, operações matemáticas caras em fragment shaders geralmente consomem mais tempo de quadro do que as mesmas operações em vertex shaders.
O Que É um Vertex Shader?
Um vertex shader geralmente é responsável pelo posicionamento geométrico. Sua tarefa mais comum é multiplicar cada posição de vértice pelas matrizes model, view e projection. Ele também pode preparar valores para estágios posteriores, como normais no espaço do mundo, coordenadas de textura ou dados específicos de efeitos, como um valor de máscara, um fator de mistura ou um vetor direcional que o fragment shader usará mais tarde. Em termos diretos, um vertex shader é um programa de GPU que executa uma vez para cada vértice de entrada em uma chamada de desenho. Ele não pode sombrear cada pixel coberto porque a rasterização ainda não aconteceu. Sua saída principal é uma posição no clip-space, além de quaisquer valores por vértice que devam ser interpolados pela primitiva para o trabalho posterior do fragmento.
Matematicamente, a transformação canônica geralmente se parece com isto:
Aqui, [x,y,z,1]^T é a posição original do vértice escrita em coordenadas homogêneas.
M é a matriz model, que posiciona o objeto no mundo.
V é a matriz view, que expressa a cena do ponto de vista da câmera.
P é a matriz projection, que mapeia essa posição no espaço da câmera para o clip-space, permitindo que a GPU prossiga para a renderização no espaço de tela.
O detalhe importante não é a fórmula em si, mas a taxa de execução. Se seu modelo tem 20.000 vértices, esse shader executa cerca de 20.000 vezes para essa chamada de desenho. Ele não executa para cada pixel na tela.
No playground abaixo, o triângulo cinza representa os dados de vértice de entrada e o triângulo azul é a saída transformada após uma cadeia de transformação simplificada. Arrastar os cantos cinzas remodela a malha original, enquanto arrastar o resultado azul, sua alça de rotação ou sua alça de escala altera as posições transformadas que o estágio de vértice entregaria à rasterização. O importante a notar é que todo esse movimento ainda acontece movendo apenas os cantos do triângulo no normalized device space, sem criar qualquer novo detalhe interior.
Quando o triângulo fonte é remodelado à esquerda, a saída à direita mantém a mesma transformação por vértice, o que facilita separar a estrutura da malha do trabalho de posicionamento posterior. Rotacionar ou transladar o triângulo azul mostra os vértices transformados sendo reposicionados como um grupo, e aumentar a escala faz o triângulo ocupar mais espaço na tela sem criar cantos geométricos extras. Essa maior pegada na tela também sugere por que o custo de fragmento pode aumentar mesmo quando este estágio ainda processa apenas três vértices.
Uma consequência prática dessa divisão de estágios é que vertex shaders são bons para modelar e preparar geometria, enquanto fragment shaders são melhores para detalhes finos de imagem dentro de cada triângulo. Se você empurrar trabalho de aparência do tipo pixel para o estágio de vértice, o resultado geralmente parece blocoso ou instável porque as saídas do vértice são conhecidas apenas nos cantos do triângulo e depois interpoladas pela superfície. A interpolação é útil, mas não equivale a uma computação verdadeira por fragmento.
O Que É um Fragment Shader?
Depois que as primitivas são rasterizadas, a GPU gera fragmentos. Cada fragmento possui varyings interpolados vindos do estágio de vértice. Agora o fragment shader decide a aparência da superfície visível: cor base, detalhes de textura, resposta de iluminação, lógica de transparência e, às vezes, se um fragmento deve ser descartado. Em termos diretos, um fragment shader é um programa de GPU que executa para cada fragmento gerado, aproximadamente cada amostra de pixel coberta antes que depth, stencil, blending e escritas em render targets finalizem o quadro. Ele recebe dados interpolados, não os vértices brutos da malha. Isso o torna o lugar certo para decisões de material no nível do pixel.
Este estágio é onde os materiais se transformam em detalhes de imagem. Se você amostrar uma textura, combinar mapas normais, calcular termos BRDF, aplicar névoa e mesclar camadas, esse trabalho geralmente acontece aqui.
A primeira coisa a tornar concreta é que um fragment shader não recebe três valores separados de canto e então escolhe um. Ele recebe valores que foram mesclados pelo triângulo durante a rasterização. Conforme a sonda se move pela visualização abaixo, a cor do fragmento inspecionado muda continuamente porque a GPU está misturando as três saídas dos vértices por peso baricêntrico antes que o sombreamento do fragmento comece.
Observe o que muda suavemente pelo triângulo: não os valores originais dos vértices diretamente, mas valores interpolados. Essa etapa de interpolação é uma das razões centrais pelas quais os estágios de vértice e fragmento são pareados. O estágio de vértice prepara os pontos finais dos dados, e o estágio de fragmento usa valores contínuos entre esses pontos para calcular a aparência final.
As normais seguem a mesma regra. Elas são frequentemente definidas uma vez por vértice, depois mescladas pela primitiva, e só então normalizadas e usadas para iluminação. O próximo explorador torna essa passagem visível: mudar as direções das normais dos vértices remodela o campo de normais pelo triângulo, enquanto mover a sonda revela a diferença entre a normal bruta mesclada e a direção normalizada que é realmente útil para sombreamento.
Esse detalhe é importante porque muitos modelos de iluminação dependem dessa normal interpolada, e não de uma direção plana e uniforme para todo o triângulo. Se a normal por fragmento muda suavemente, a resposta difusa e especular também pode mudar suavemente. Se as entradas são muito grosseiras ou a iluminação é calculada cedo demais, o resultado começa a perder detalhes que deveriam existir entre os vértices.
Essa compensação fica mais clara na comparação de iluminação abaixo. Com uma malha grosseira, a versão por vértice só pode amostrar o brilho especular em alguns cantos e interpolar o resultado pelo interior, então a região brilhante pode parecer borrada ou mal posicionada. Conforme a luz se move, a versão por fragmento permanece mais fiel porque a iluminação é calculada em cada fragmento coberto, em vez de ser aproximada a partir de algumas amostras de vértice. Aumentar a densidade da malha reduz a diferença, e é por isso que a iluminação por vértice pode ser aceitável para algumas superfícies e claramente insuficiente para outras.
Vertex Shader vs Fragment Shader
A maneira mais rápida de comparar esses estágios é fazer as mesmas quatro perguntas para ambos.
-
Com que frequência ele executa? Vertex shader: uma vez por vértice. Fragment shader: uma vez por fragmento.
-
Qual é o propósito principal? Vertex shader: transformação geométrica e configuração de varyings. Fragment shader: sombreamento final e saída de cor/profundidade.
-
Quais dados dominam sua entrada? Vertex shader: atributos da malha mais uniforms de transformação. Fragment shader: varyings interpolados, texturas, luzes, uniforms de material.
-
Qual padrão de desempenho é típico? Vertex shader: escala com a complexidade geométrica. Fragment shader: escala com a cobertura de tela e overdraw.
Essas diferenças implicam regras práticas de otimização. Se um efeito pode ser aproximado com matemática por vértice e interpolação, ele pode ser mais barato. Se a precisão precisa ser exata ao pixel, como na resposta especular, mapeamento de normais ou detalhes procedurais finos, ele pertence ao estágio de fragmento, mesmo que o custo aumente.
Erros Comuns na Seleção de Estágios
Um erro frequente é colocar lógica excessiva nos fragmentos sem considerar a cobertura. Um pós-efeito em tela cheia a 4K pode executar muitos milhões de invocações de shader por quadro. Outro erro é empurrar lógica de aparência cedo demais para os vértices e depois se perguntar por que o detalhe desaparece em triângulos grandes.
Um processo de decisão simples ajuda:
- Essa computação define o posicionamento do objeto? Coloque no vértice.
- Ela define a aparência no nível do pixel? Coloque no fragmento.
- Ela precisa de informações de pixels vizinhos de dados já renderizados? Geralmente isso significa uma passagem de pós-processamento posterior, possivelmente compute.
Esse processo não é perfeito, mas evita a maioria dos erros arquiteturais em código de renderização em tempo real, especialmente quando comparado a técnicas como ray marching com campos de distância com sinal, que estão completamente fora do caminho padrão de rasterização de triângulos.
Outros Tipos de Shader em Contexto
Geometry Shaders
Geometry shaders executam por primitiva após o estágio de vértice. Eles podem emitir novas primitivas, sendo úteis para efeitos específicos como saídas de mapas de sombra em camadas ou expansão de linhas. No entanto, eles são frequentemente evitados em caminhos críticos de desempenho porque podem se tornar um gargalo de throughput. Muitos mecanismos modernos preferem alternativas como instancing, mesh shaders em APIs compatíveis ou geração orientada por compute, e a visão geral do pipeline de renderização da Khronos é uma referência formal útil se você quiser a ordenação de estágios no nível da API por trás desse resumo.
Tessellation Shaders
Tessellation é dividida em estágios de controle e avaliação. Ela subdivide primitivas de patch para adicionar densidade geométrica na GPU. Isso pode melhorar superfícies curvas e displacement mapping quando o detalhe no espaço de tela assim exige. Para a intuição de curvas de nível mais baixo por trás de caminhos espaciais controlados por alças, visualizar curvas Bezier 3D e a construção de De Casteljau é um complemento útil antes de passar de curvas para patches. A compensação é o aumento da complexidade e restrições de hardware/API, então muitas equipes o usam seletivamente.
Compute Shaders
Compute shaders não estão vinculados à rasterização. Eles executam kernels de GPU de propósito geral em grupos de threads e são amplamente usados para simulação, culling, atualização de partículas, preparação de iluminação clusterizada, denoising e pós-processamento. Em renderizadores modernos, compute frequentemente coopera com passagens gráficas tradicionais em vez de substituí-las, e essas passagens frequentemente consomem entradas procedurais construídas a partir de funções de ruído como value noise, Perlin noise e fractal noise. Para uma explicação voltada a iniciantes sobre entradas, saídas e execução local de estágios de shader, a introdução a shaders do LearnOpenGL continua sendo um bom complemento.
Um mapa mental útil é:
- Vertex + Fragment: caminho central da rasterização gráfica
- Geometry + Tessellation: estágios opcionais de amplificação/refinamento geométrico
- Compute: caminho de processamento paralelo geral que pode alimentar ou consumir dados de renderização
Construindo Intuição para Projetos Reais
Ao depurar problemas de renderização, identifique a fronteira do estágio onde os dados incorretos aparecem primeiro. Se as posições transformadas estão erradas antes da rasterização, inspecione a lógica do vértice. Se a geometria parece correta, mas a cor/iluminação está errada, inspecione a lógica do fragmento. Se a topologia ou subdivisão está errada, inspecione os estágios de geometry/tessellation. Se os buffers de pré-processamento estão errados, inspecione os kernels de compute.
Você também pode traçar o perfil por intenção do estágio. Custo alto de vértice geralmente acompanha malhas densas ou skinning pesado. Custo alto de fragmento geralmente acompanha grande cobertura de tela, matemática de material cara ou overdraw de camadas transparentes. Essa divisão dá direção imediata para experimentos de otimização, e se você quiser uma explicação mais profunda da etapa de interpolação entre os estágios programáveis, a lição do Scratchapixel sobre rasterização é o acompanhamento mais relevante.
Resumo
Vertex shaders e fragment shaders são diferentes porque executam sobre unidades de trabalho distintas. Vertex shaders processam pontos da malha e preparam dados interpolados. Fragment shaders processam fragmentos rasterizados e calculam a aparência final.
Se você mantiver esse modelo de execução em mente, a maioria das decisões sobre o pipeline se torna mais clara:
- Matemática de posicionamento e preparação de varyings em vertex shaders.
- Lógica de material e iluminação com precisão de pixel em fragment shaders.
- Use geometry e tessellation apenas quando suas capacidades específicas forem necessárias.
- Use compute para tarefas gerais de GPU fora do fluxo estrito de rasterização.
Esse modelo escala de demos simples a renderizadores de produção e torna o código de shader mais fácil de raciocinar, otimizar e depurar.