Trilha 15 · Gráficos 2D e 3D

Shaders e programabilidade

Fragment shaders: programando o comportamento por pixel em GLSL — gradientes, ruído procedural, ondas animadas e os fundamentos que tornam jogos modernos visualmente ricos.

① Intuição

Uma função que roda por pixel

Um fragment shader é uma função que roda uma vez por pixel — em paralelo, para todos os pixels da tela ao mesmo tempo. Ela recebe a posição do pixel e uniformes (constantes como o tempo, resolução, texturas) e retorna uma cor RGBA.

Antes dos shaders programáveis (pré-2001), a GPU tinha um pipeline fixo: você podia escolher entre iluminação Phong ou Gouraud, mas nada mais. Com shaders, o comportamento por pixel é completamente programável em GLSL (OpenGL), HLSL (DirectX) ou WGSL (WebGPU). Isso permite reflexos físicos, subsurface scattering, pele realista, água — qualquer efeito visual imaginável.

Shadertoy e comunidade: o site shadertoy.com tem milhares de shaders impressionantes escritos só em fragment shader — paisagens montanhosas, nuvens volumétricas, oceanos, planetas — tudo gerado proceduralmente por pixel, sem nenhum dado 3D externo. A entrada é apenas a posição do pixel e o tempo.
② Visualização interativa

Simulador de pixel shaders

Cada shader roda em cada pixel do canvas em paralelo. Ajuste os uniforms com os sliders e veja o resultado em tempo real.

PIXEL SHADER — 200×200px (cada pixel = 1 thread)
Gradiente UV

Usa as coordenadas UV (0..1) diretamente como cor. Clássico para debugar UVs.

ESTRUTURA DE UM SHADER
// Roda 1x por pixel em paralelo
vec4 fragment(
vec2 uv, // 0..1
float time // uniforms
) {
// calcular cor...
return vec4(r, g, b, a);
}
Paralelismo real: GPUs executam milhares de pixels simultaneamente. Um shader de 200×200px = 40.000 threads em paralelo — impossível na CPU sem overhead brutal.
③ Explicação técnica

Fragment shader GLSL completo

// Fragment shader GLSL completo
// Roda uma vez por pixel, em paralelo

precision mediump float;

// Uniforms: constantes passadas pela CPU para todos os pixels
uniform float uTime;        // tempo em segundos
uniform vec2  uResolution;  // tamanho da tela em pixels
uniform sampler2D uTexture;  // textura

// Varyings: interpolados pelo rasterizador (por pixel)
varying vec2 vUV;     // coordenadas de textura 0..1
varying vec3 vNormal; // normal da superfície

void main() {
  // Gradiente animado com seno
  vec2 uv = vUV;
  float wave = sin(uv.x * 10.0 + uTime) * 0.5 + 0.5;

  // Amostra textura e mistura com cor de onda
  vec4 texColor  = texture2D(uTexture, uv);
  vec4 waveColor = vec4(0.36, 0.62, 1.0, 1.0);
  vec4 finalColor = mix(texColor, waveColor, wave * 0.3);

  // Iluminação Phong simples
  vec3 lightDir = normalize(vec3(1, 1, 2));
  float diff = max(dot(vNormal, lightDir), 0.0);
  finalColor.rgb *= (0.3 + diff * 0.7); // ambient + diffuse

  gl_FragColor = finalColor;
}

Ruído procedural e FBM

// Ruído procedural — base de texturas de nuvem, terreno, fogo

// Hash pseudo-aleatório (determinístico)
float hash(vec2 p) {
  p = fract(p * vec2(234.34, 435.345));
  p += dot(p, p + 34.23);
  return fract(p.x * p.y);
}

// Value noise: interpola valores de hash numa grade
float noise(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  vec2 u = f * f * (3.0 - 2.0 * f); // smoothstep

  return mix(mix(hash(i + vec2(0,0)), hash(i + vec2(1,0)), u.x),
             mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y);
}

// FBM: fractal Brownian Motion — soma oitavas de ruído
float fbm(vec2 p) {
  float v = 0.0, a = 0.5;
  for (int i = 0; i < 5; i++) {
    v += a * noise(p);
    p *= 2.0;  a *= 0.5;  // dobra frequência, metade amplitude
  }
  return v;
}
SDF (Signed Distance Fields): uma técnica poderosa em fragment shaders onde cada pixel consulta uma "função de distância" que diz o quão longe está de uma forma. Círculo: length(p) - r. Combinando SDFs com operações de união/interseção/subtração, você renderiza geometria complexa sem nenhuma malha 3D — tudo pelo cálculo matemático em cada pixel.
④ Projeto para programar

Seu primeiro shader

Mini projeto: escreva um fragment shader WebGL que cria um gradiente circular animado — a cor muda com sin(length(uv - vec2(0.5)) * 10.0 - uTime). Ajuste frequência e velocidade com uniforms.

Projeto principal: implemente um shader de "água" no shadertoy.com: combine duas camadas de ondas senoidais com frequências e direções diferentes, use fbm() para distorção, e adicione reflexo do céu (gradiente de azul claro a branco na vertical).

Desafio extra: implemente ray marching de uma esfera usando SDF: o fragment shader "caminha" ao longo de um raio da câmera, consultando sdf_sphere(p, center, radius) a cada passo. Quando a distância for menor que 0.001, você atingiu a superfície. Calcule a normal por gradiente numérico e aplique iluminação Phong.

⑤ Exercícios rápidos

Teste sua intuição

Qual a diferença fundamental de execução entre vertex shader e fragment shader?
O que são "uniforms" em GLSL?
O que é "ruído procedural" e qual sua vantagem sobre uma textura de ruído?
⑥ Aplicações no mundo real

Onde você encontra isso

🌊

Água e oceanos em jogos

Far Cry, Assassin's Creed e God of War renderizam água com shaders de ondas (somas de senóides com direções e frequências variadas), reflexo da câmera (screen-space reflections), espuma procedural (fbm) e refração do fundo. Tudo calculado por pixel no fragment shader.

🌌

Shadertoy e arte generativa

shadertoy.com é uma comunidade onde artistas criam visuais impressionantes só em fragment shaders — planetas, galáxias, nuvens volumétricas — sem nenhum dado 3D. A técnica de ray marching permite renderizar objetos via funções SDF sem triângulos.

🎬

Pós-processamento

Após renderizar a cena, jogos aplicam uma passagem de pós-processamento: bloom (brilho em áreas claras), depth of field (desfoque de profundidade), SSAO (oclusão ambiente), motion blur, tonemapping HDR → LDR. Tudo são fragment shaders que lêem o framebuffer como textura.

← Anterior: Pipeline gráfico ✓ Concluir trilha: Gráficos 2D e 3D