Computação Gráfica na web,
do primeiro pixel ao WebGPU
Uma apostila completa e orientada ao mercado: fundamentos, matemática, Three.js, shaders, performance, R3F, XR e as técnicas mais modernas usadas em estúdios e produtos reais. Os capítulos seguem os eixos de um gizmo 3D: verde para começar, azul para evoluir, vermelho para dominar.
Y·00Como usar esta apostila
Esta apostila foi desenhada para levar você de zero conhecimento em computação gráfica até um nível em que você consegue disputar vagas reais de creative developer, desenvolvedor 3D web, engenheiro de visualização ou desenvolvedor de jogos web. Ela cobre teoria, prática e — em cada capítulo — como aquele conhecimento aparece no mercado.
As três trilhas + mercado
- Trilha básica (Y) — fundamentos de CG, matemática essencial, JavaScript e suas primeiras cenas Three.js. Ao final dela você já publica cenas 3D interativas.
- Trilha intermediária (Z) — o dia a dia profissional: texturas, PBR, modelos glTF, interação, organização de projetos.
- Trilha avançada (X) — o que diferencia salários: shaders, pós-processamento, GPGPU, performance, física, WebGPU/TSL, React Three Fiber, XR e jogos.
- Mercado — carreira, portfólio, roadmap de estudos e projetos que impressionam recrutadores.
Convenções
Caixas verdes trazem atalhos, boas práticas e truques de quem já trabalha com isso.
Caixas vermelhas apontam erros clássicos que causam bugs, vazamento de memória ou reprovação em entrevista técnica.
Caixas âmbar conectam o assunto do capítulo a vagas, projetos comerciais e perguntas de entrevista reais.
Caixas azuis propõem prática. Computação gráfica não se aprende lendo — se aprende renderizando.
Aprofundamento — blocos como este expandem a matemática e a teoria
Você pediu "do mínimo ao bastante profundo": o texto corrido traz o mínimo necessário para programar; os blocos de aprofundamento trazem a teoria completa (derivações, notação matemática, detalhes de hardware). Pule-os na primeira leitura e volte quando quiser dominar o assunto.
Ambiente de estudo
Você precisa apenas de um navegador moderno, um editor (VS Code) e Node.js. O fluxo profissional atual usa o bundler Vite:
npm create vite@latest meu-projeto-3d -- --template vanilla
cd meu-projeto-3d
npm install three
npm run dev
Para experimentos rápidos sem instalar nada, use CDN com import maps (é assim que as demos ao vivo desta apostila funcionam):
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.166.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.166.0/examples/jsm/"
} }
</script>
<script type="module">
import * as THREE from 'three';
</script>
Y·01Fundamentos de Computação Gráfica
O que acontece entre "um monte de números" e "uma imagem na tela": pixels, o pipeline gráfico, rasterização vs. ray tracing e como a GPU pensa.
O que é computação gráfica
Computação gráfica (CG) é a área que estuda como sintetizar imagens a partir de descrições matemáticas. Um modelo 3D é só uma lista de números — posições de vértices, cores, direções. O trabalho da CG é transformar esses números em pixels, dezenas de vezes por segundo, de forma convincente.
Ela se divide em grandes frentes que aparecem no mercado com nomes diferentes:
| Frente | O que faz | Onde aparece no mercado |
|---|---|---|
| Renderização em tempo real | Gera 30–120 imagens/segundo enquanto o usuário interage | Jogos, sites imersivos, configuradores de produto, XR, dashboards 3D — o foco desta apostila |
| Renderização offline | Leva minutos/horas por quadro em busca de fotorrealismo | Cinema, publicidade, arquitetura (V-Ray, Cycles, Arnold) |
| Processamento de imagem | Transforma imagens existentes | Filtros, visão computacional, pós-produção |
| Modelagem/geometria | Cria e manipula formas | CAD, Blender, escaneamento 3D, impressão 3D |
Pixels, framebuffer e frames
A tela é uma grade de pixels; cada pixel guarda uma cor, normalmente em RGB (vermelho, verde, azul, cada canal de 0 a 255 ou de 0.0 a 1.0). A região de memória que guarda a imagem sendo exibida chama-se framebuffer. Renderizar em tempo real significa preencher o framebuffer inteiro a cada frame — a 60 fps você tem cerca de 16,6 ms para desenhar tudo. Esse orçamento de tempo governa toda decisão de engenharia gráfica.
"Manter 60 fps" (ou 90+ em VR) é requisito de vaga. Times medem o tempo de frame em milissegundos: um efeito que custa 4 ms consome 1/4 do orçamento de um jogo a 60 fps. Falar em "ms por frame" em vez de "fps" já sinaliza maturidade em entrevistas.
O pipeline gráfico
GPUs modernas transformam geometria em pixels através de uma linha de montagem chamada pipeline gráfico. Simplificando:
- Vertex processing — cada vértice do modelo é transformado do espaço do objeto para a tela (é aqui que rodam os vertex shaders, cap. 12).
- Montagem de primitivas — vértices são agrupados em triângulos. Tudo em tempo real é triângulo: uma esfera "lisa" é só uma malha com triângulos suficientes.
- Rasterização — a GPU descobre quais pixels cada triângulo cobre, gerando fragments (candidatos a pixel).
- Fragment processing — cada fragment recebe uma cor calculada pelo fragment shader (luz, textura, material).
- Testes e blending — o depth test (z-buffer) decide o que fica na frente; transparências são misturadas; o resultado vai ao framebuffer.
A GPU é rápida porque executa esses estágios de forma massivamente paralela: milhares de núcleos processando vértices e fragments ao mesmo tempo. Essa é a diferença filosófica entre CPU (poucos núcleos, lógica complexa e sequencial) e GPU (milhares de núcleos, a mesma operação simples sobre muitos dados).
Aprofundamento — z-buffer, backface culling e o custo do overdraw
O z-buffer é uma textura invisível do tamanho da tela que guarda, por pixel, a profundidade do fragmento mais próximo já desenhado. Ao rasterizar um novo fragmento, a GPU compara sua profundidade com a armazenada: se estiver mais longe, é descartado. Isso resolve visibilidade sem ordenar triângulos — mas não funciona para objetos transparentes, que precisam ser ordenados de trás para frente (o Three.js faz isso por você, e é por isso que transparência é cara e cheia de artefatos).
Backface culling: por convenção, triângulos têm uma frente definida pela ordem dos vértices (anti-horário = frente). Triângulos de costas para a câmera são descartados antes da rasterização, cortando ~metade do trabalho em malhas fechadas. É por isso que, ao entrar "dentro" de um modelo, ele parece invisível — você está vendo as costas dos triângulos (controlável com material.side).
Overdraw é desenhar o mesmo pixel várias vezes (ex.: muitas camadas de partículas transparentes). Em mobile é um dos maiores assassinos de performance, pois a banda de memória do framebuffer é limitada.
Rasterização vs. ray tracing
Rasterização (o pipeline acima) projeta triângulos na tela — extremamente rápida, mas efeitos globais como reflexos, sombras e iluminação indireta precisam ser aproximados com truques. Ray tracing inverte a lógica: lança raios da câmera para a cena e simula fisicamente os caminhos da luz — naturalmente correto, historicamente lento. O presente é híbrido: placas com RT cores aceleram ray tracing em jogos nativos, e na web técnicas como path tracing em WebGPU já aparecem em demos e ferramentas (o pacote three-gpu-pathtracer é um exemplo). Para a web em produção, rasterização ainda domina.
Espaços de coordenadas
Uma pergunta central em CG: "essa posição está medida em relação a quê?" Um mesmo ponto passa por vários espaços até virar pixel:
- Object space — coordenadas relativas ao próprio modelo (a ponta do nariz do personagem).
- World space — depois de posicionar/rotacionar/escalar o modelo na cena.
- View space — o mundo visto do ponto de vista da câmera.
- Clip space / NDC — depois da projeção em perspectiva, tudo cai num cubo padrão de -1 a 1.
- Screen space — coordenadas finais em pixels.
Cada seta dessa cadeia é uma multiplicação por matriz — motivo pelo qual o próximo capítulo existe. O Three.js usa um sistema destro (right-handed) com +Y para cima: X para a direita, Y para cima, Z "saindo da tela" em direção a você.
Cor: sRGB, linear e HDR
Cores na web vivem no espaço sRGB, que é não-linear: os valores são codificados com uma curva (gamma) que aproveita melhor os bits onde o olho é mais sensível. Mas a matemática de iluminação só funciona em espaço linear — somar luzes em sRGB produz cores erradas. O fluxo correto (que o Three.js moderno já aplica por padrão) é: decodificar texturas sRGB → calcular luz em linear → converter de volta para exibição com tone mapping. Voltaremos a isso em detalhe no capítulo 08, porque errar gerência de cor é o motivo nº 1 de "meu render ficou lavado/escuro".
- Calcule quantos pixels uma tela 1920×1080 tem e quantas escritas de pixel por segundo acontecem a 60 fps (resposta: ~124 milhões — e a GPU ainda calcula luz para cada um).
- Abra qualquer jogo 3D e procure evidências de triângulos: silhuetas facetadas em objetos "redondos" distantes.
Y·02Matemática 3D essencial
Vetores, matrizes, quaternions e trigonometria — o mínimo para programar bem, com aprofundamentos completos para quem quer dominar.
Boa notícia: para usar Three.js você precisa de pouca matemática, porque a biblioteca implementa tudo. Mas para depurar, criar efeitos próprios e passar em entrevistas, você precisa entender o que as funções fazem. Este capítulo é o alicerce — volte a ele sempre.
Vetores
Um vetor 3D é um trio de números (x, y, z) que representa ou uma posição, ou uma direção com comprimento. Essa dupla interpretação é a chave: (0, 1, 0) pode ser "o ponto 1 metro acima da origem" ou "a direção para cima".
const v = new THREE.Vector3(3, 4, 0);
v.length(); // 5 → comprimento (Pitágoras em 3D: √(x²+y²+z²))
v.normalize(); // divide pelo comprimento → mesma direção, comprimento 1
v.add(outro); // soma componente a componente (encadear deslocamentos)
v.multiplyScalar(2); // escala o vetor
v.distanceTo(outro); // distância entre dois pontos
As duas operações mais importantes de toda a computação gráfica:
- Produto escalar (dot product) —
a.dot(b)retorna um número que mede o alinhamento entre dois vetores. Com vetores normalizados: 1 = mesma direção, 0 = perpendiculares, -1 = opostos. É literalmente a fórmula da iluminação difusa:brilho = max(0, normal.dot(direçãoDaLuz)). - Produto vetorial (cross product) —
a.cross(b)retorna um vetor perpendicular aos dois. Usado para calcular normais de triângulos, eixos de câmera e "para que lado virar".
Aprofundamento — as fórmulas e por que funcionam
Dot: a·b = aₓbₓ + a_yb_y + a_zb_z = |a||b|cos(θ). A segunda forma explica tudo: com vetores unitários, o dot é o cosseno do ângulo entre eles. Cosseno de 0° = 1 (alinhados), de 90° = 0. A lei de Lambert diz que uma superfície recebe luz proporcional ao cosseno do ângulo de incidência — por isso dot(N, L) é a iluminação difusa.
Cross: a×b = (a_yb_z − a_zb_y, a_zbₓ − aₓb_z, aₓb_y − a_yb_ₓ), com |a×b| = |a||b|sin(θ). A direção segue a regra da mão direita, e o comprimento é a área do paralelogramo formado — por isso o cross de duas arestas de um triângulo dá sua normal e o dobro da sua área. Diferente do dot, a ordem importa: a×b = −(b×a).
Interpolação linear (lerp): lerp(a, b, t) = a + (b−a)t — com t de 0 a 1, desliza de a até b. É a operação mais usada em animação. Sua prima smoothstep suaviza as pontas e domina o mundo dos shaders.
Matrizes e transformações
Uma matriz 4×4 é uma "máquina de transformar vetores": multiplicar um ponto por ela aplica, de uma vez, translação, rotação e escala. O poder está na composição: multiplicar duas matrizes gera uma matriz que aplica as duas transformações em sequência — assim a GPU transforma milhões de vértices com uma multiplicação cada.
// Você raramente cria matrizes na mão — o Three.js as monta a partir de:
mesh.position.set(0, 2, 0); // translação
mesh.rotation.y = Math.PI/4; // rotação (radianos!)
mesh.scale.setScalar(1.5); // escala
// Internamente: mesh.matrix = T · R · S (aplicada a cada vértice na GPU)
A ordem importa. Rotacionar e depois transladar ≠ transladar e depois rotacionar (multiplicação de matrizes não é comutativa). É a causa nº 1 de "meu objeto orbita em vez de girar no lugar": ele está sendo rotacionado depois de deslocado, então gira em torno da origem do mundo.
Aprofundamento — por que 4×4 e não 3×3? Coordenadas homogêneas e projeção
Rotação e escala são lineares e cabem em 3×3, mas translação não é linear (move a origem). O truque: adicionar uma 4ª coordenada w. Pontos viram (x, y, z, 1) e direções (x, y, z, 0) — e a translação vira multiplicação de matriz, com o bônus elegante de que direções (w=0) são imunes à translação, exatamente como devem ser.
A matriz de projeção em perspectiva usa o w de outra forma: ela copia a profundidade do ponto para w, e a GPU depois divide x, y, z por w (a perspective divide). Dividir pela distância é o que faz objetos distantes ficarem menores — a perspectiva inteira é uma divisão. A cadeia completa que todo vertex shader executa é:
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
Essa linha é possivelmente a mais importante da computação gráfica em tempo real. Memorize a ordem: modelo → mundo → câmera → clip.
Ângulos, Euler e quaternions
Three.js mede ângulos em radianos (círculo completo = 2π ≈ 6.283). THREE.MathUtils.degToRad(90) converte quando necessário.
Rotações têm duas representações:
- Ângulos de Euler (
mesh.rotation) — três ângulos em torno de X, Y, Z. Intuitivos, mas sofrem de gimbal lock (perda de um eixo em certas combinações) e interpolam mal. - Quaternions (
mesh.quaternion) — quatro números que codificam "gire θ graus em torno do eixo v". Sem gimbal lock, interpolação perfeita (slerp). É o que engines usam internamente e o que animações de modelos usam.
// Regra prática: leia/escreva Euler para coisas simples,
// use quaternion para compor rotações e interpolar:
const q = new THREE.Quaternion()
.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
mesh.quaternion.slerp(q, 0.1); // aproxima suavemente da rotação alvo
Aprofundamento — o que é um quaternion, de verdade
Um quaternion é um número hipercomplexo q = w + xi + yj + zk. Para rotações usamos quaternions unitários: dado um eixo unitário v e ângulo θ, q = (cos(θ/2), v·sin(θ/2)). Rotacionar um ponto p é calcular q·p·q⁻¹. Duas propriedades explicam sua adoção universal: compor rotações é multiplicar quaternions (barato e sem acúmulo de erro perceptível), e slerp interpola pelo caminho mais curto na esfera de rotações com velocidade angular constante — algo impossível de garantir com Euler. Curiosidade que cai em entrevista: q e −q representam a mesma rotação (cobertura dupla).
Trigonometria que você vai usar toda semana
// Movimento circular / órbita: o par (cos, sin) percorre um círculo
obj.position.x = Math.cos(t) * raio;
obj.position.z = Math.sin(t) * raio;
// Oscilação (flutuar, pulsar, balançar):
obj.position.y = Math.sin(t * 2) * 0.25;
// Ângulo até um alvo (ex.: inimigo olhar para o jogador):
const ang = Math.atan2(alvo.x - pos.x, alvo.z - pos.z);
Perguntas clássicas de entrevista para vagas 3D: "para que serve o dot product?", "por que quaternions em vez de Euler?", "o que é a matriz MVP?". Todas respondidas acima. Em teste prático, saber usar lerp/slerp para suavizar movimento é o que separa uma demo dura de uma demo profissional.
- No papel: normalize o vetor (3, 0, 4) e calcule o dot entre (1,0,0) e (0.707, 0.707, 0).
- Em código: faça um objeto orbitar outro usando cos/sin, e um terceiro sempre "olhar" para o primeiro com
lookAt().
Y·03JavaScript para gráficos
O subconjunto de JavaScript moderno que projetos 3D realmente usam: módulos, classes, o loop de animação e noções de Canvas 2D.
Se você já domina JS, passe direto ao capítulo 04. Se está começando, este capítulo cobre exatamente o que o restante da apostila assume.
ES Modules
// Projetos 3D modernos são 100% módulos ES:
import * as THREE from 'three'; // tudo sob um namespace
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
export function criaCena() { /* ... */ } // exportando o seu código
Essenciais da linguagem
// const por padrão; let quando reatribuir. Nunca var.
const velocidade = 2;
// Arrow functions — onipresentes em callbacks e loops:
const dobro = (x) => x * 2;
// Destructuring e spread:
const { x, y, z } = mesh.position;
const config = { ...padrao, cor: 0xff0000 };
// Classes — a base de como você organiza entidades 3D:
class Nave {
constructor(scene) {
this.mesh = new THREE.Mesh(geometria, material);
scene.add(this.mesh);
}
update(dt) { // dt = tempo desde o último frame
this.mesh.position.z -= 5 * dt;
}
}
// async/await — carregamento de modelos e texturas é assíncrono:
const gltf = await loader.loadAsync('/modelos/nave.glb');
O loop de animação e o delta time
Todo app em tempo real gira em torno de um loop que roda uma vez por frame, agendado com requestAnimationFrame — o navegador o chama em sincronia com o monitor:
let anterior = performance.now();
function loop(agora) {
const dt = (agora - anterior) / 1000; // delta em segundos
anterior = agora;
// ERRADO: obj.position.x += 0.01; ← velocidade muda com o fps do monitor!
// CERTO: unidades por SEGUNDO, escaladas pelo delta:
obj.position.x += 2 * dt; // 2 unidades/s em qualquer monitor
renderer.render(scene, camera);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
Ignorar o delta time é o erro nº 1 de iniciantes: a animação roda 2,4× mais rápido num monitor de 144 Hz do que num de 60 Hz. Em Three.js, use const clock = new THREE.Clock() e clock.getDelta(), ou o novo renderer.setAnimationLoop(loop) (obrigatório para XR).
Garbage collector e o padrão "aloque uma vez"
JavaScript tem coleta de lixo automática — e o coletor pode pausar seu app por alguns milissegundos, causando engasgos. Regra de ouro em código de frame: não crie objetos dentro do loop.
// RUIM: cria um Vector3 novo 60x por segundo → pressão no GC
function update() {
const dir = new THREE.Vector3(0, 0, -1);
...
}
// BOM: aloque fora, reutilize dentro (padrão usado no código-fonte do Three.js)
const _dir = new THREE.Vector3();
function update() {
_dir.set(0, 0, -1);
...
}
Aprofundamento — TypedArrays: como o JS conversa com a GPU
A GPU não entende arrays comuns de JS; ela recebe blocos binários contíguos. Por isso toda geometria vive em TypedArrays: Float32Array para posições/normais/UVs, Uint16Array/Uint32Array para índices. new Float32Array(3 * n) aloca n vértices intercalados como [x0,y0,z0, x1,y1,z1, …]. Entender isso destrava geometria procedural (cap. 05), partículas (cap. 14) e otimização (cap. 15) — e também explica por que modificar geometria exige marcar attribute.needsUpdate = true: o Three.js precisa saber que deve reenviar o buffer à GPU.
Canvas 2D: seu laboratório de conceitos
Antes do 3D, o elemento <canvas> com contexto 2D é um ótimo lugar para internalizar o loop, coordenadas e interpolação:
const ctx = document.querySelector('canvas').getContext('2d');
let t = 0;
function loop() {
t += 0.016;
ctx.fillStyle = '#101318';
ctx.fillRect(0, 0, 600, 300); // limpa o frame
const x = 300 + Math.cos(t) * 100; // órbita com trigonometria
const y = 150 + Math.sin(t) * 100;
ctx.beginPath();
ctx.arc(x, y, 12, 0, Math.PI * 2);
ctx.fillStyle = '#7bd88f';
ctx.fill();
requestAnimationFrame(loop);
}
loop();
No Canvas 2D: (1) faça 50 bolinhas quicarem nas bordas usando velocidade × delta; (2) faça uma bolinha perseguir o mouse com lerp (x += (mouseX - x) * 0.08) — esse easing exponencial é usado em produção o tempo todo.
Y·04Primeira cena Three.js
Os quatro pilares — cena, câmera, renderer e mesh — e uma cena completa, responsiva e animada, explicada linha a linha.
Por que Three.js
WebGL é a API de baixo nível que dá acesso à GPU no navegador — poderosa e verbosa (um triângulo colorido exige ~80 linhas). Three.js é a biblioteca que abstrai isso num modelo mental de estúdio de cinema: você monta uma cena com objetos e luzes, posiciona uma câmera e pede ao renderer para fotografar. É de longe a biblioteca 3D mais usada da web e a mais pedida em vagas (junto com sua camada React, o R3F, cap. 18).
A cena mínima, linha a linha
import * as THREE from 'three';
// 1. CENA — o "palco" que contém tudo (um grafo de objetos)
const scene = new THREE.Scene();
scene.background = new THREE.Color('#101318');
// 2. CÂMERA — em perspectiva, como o olho humano
const camera = new THREE.PerspectiveCamera(
60, // fov: campo de visão vertical em graus
window.innerWidth / window.innerHeight,// aspect: proporção do viewport
0.1, // near: nada mais perto que isso aparece
100 // far: nem mais longe que isso
);
camera.position.set(2, 2, 4); // afaste-se! tudo nasce em (0,0,0)
camera.lookAt(0, 0, 0);
// 3. MESH = GEOMETRIA (forma) + MATERIAL (aparência)
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: '#7bd88f', roughness: 0.4 })
);
scene.add(mesh);
// Materiais "Standard" reagem à luz — sem luz, tudo é preto:
const sol = new THREE.DirectionalLight('#ffffff', 2.5);
sol.position.set(3, 5, 2);
scene.add(sol, new THREE.AmbientLight('#ffffff', 0.3));
// 4. RENDERER — desenha a cena no <canvas>
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
// 5. LOOP DE ANIMAÇÃO
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
const dt = clock.getDelta();
mesh.rotation.y += 0.8 * dt; // rad/s, independente do fps
renderer.render(scene, camera);
});
// 6. RESPONSIVIDADE — sempre trate o resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); // recalcule a projeção!
renderer.setSize(window.innerWidth, window.innerHeight);
});
setPixelRatio(Math.min(devicePixelRatio, 2)) equilibra nitidez e performance: telas retina têm até 3 pixels físicos por pixel CSS, e renderizar em 3× custa 9× mais fragmentos. Limitar a 2 é o padrão da indústria.
O grafo de cena
A cena é uma árvore: objetos podem conter objetos, e transformações são relativas ao pai. Mover o pai move todos os filhos — é assim que uma roda gira junto com o carro:
const carro = new THREE.Group();
carro.add(chassi, roda1, roda2, roda3, roda4);
scene.add(carro);
carro.position.x = 10; // tudo se move junto
roda1.rotation.x += dt; // a roda ainda gira no próprio eixo (espaço local)
Diferencie sempre posição local (relativa ao pai, obj.position) de posição no mundo (obj.getWorldPosition(alvo)). Confundir as duas causa metade dos bugs de posicionamento.
Esta cena mínima é, literalmente, um exercício de entrevista: "monte um cubo girando com luz e resize correto, sem olhar documentação". Treine até sair de memória — cena, câmera, mesh, luz, renderer, loop, resize.
Monte a cena acima e depois: adicione um segundo cubo filho do primeiro (deslocado em X) e observe a órbita; troque a câmera para OrthographicCamera e compare; anime a cor com material.color.setHSL((t*0.1)%1, 0.7, 0.55).
Y·05Geometrias e materiais
O que existe dentro de um BufferGeometry, as primitivas prontas, geometria procedural e a escolha certa de material para cada situação.
Anatomia de uma geometria
Toda forma no Three.js é um BufferGeometry: um pacote de atributos por vértice guardados em TypedArrays:
position— xyz de cada vértice;normal— direção "para fora" da superfície em cada vértice (essencial para luz);uv— coordenadas de textura 0–1 (cap. 07);index— opcional: lista de índices que forma triângulos reutilizando vértices.
Primitivas prontas cobrem muita coisa: BoxGeometry, SphereGeometry, PlaneGeometry, CylinderGeometry, TorusGeometry, TorusKnotGeometry (a cobaia clássica de materiais), CapsuleGeometry, e as baseadas em curvas: ExtrudeGeometry, LatheGeometry, TubeGeometry.
Segmentos custam triângulos. SphereGeometry(1, 64, 32) tem ~4k triângulos; com 512×256 são ~260k — invisível de tão liso, caro à toa. Ajuste segmentação ao tamanho do objeto na tela.
Geometria procedural
Criar geometria por código é uma habilidade valorizada (terrenos, visualização de dados, arte generativa):
// Um terreno ondulado: comece de um plano e desloque os vértices
const geo = new THREE.PlaneGeometry(20, 20, 128, 128);
geo.rotateX(-Math.PI / 2); // deitar o plano
const pos = geo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i), z = pos.getZ(i);
pos.setY(i, Math.sin(x * 0.6) * Math.cos(z * 0.6) * 0.8);
}
geo.computeVertexNormals(); // recalcule normais após deformar!
pos.needsUpdate = true;
Esqueceu computeVertexNormals() depois de deformar? A iluminação continuará a do plano liso e o relevo parecerá "pintado". E lembre-se de needsUpdate = true sempre que alterar um atributo.
Materiais: qual usar
| Material | Reage à luz? | Uso típico |
|---|---|---|
MeshBasicMaterial | Não | Cores chapadas, wireframes, UI 3D, coisas emissivas simples — o mais barato |
MeshLambertMaterial | Difusa | Estética estilizada / low-poly, mobile de entrada |
MeshPhongMaterial | Difusa + brilho especular | Legado; hoje quase sempre substituído pelo Standard |
MeshStandardMaterial | PBR (metalness/roughness) | O padrão do mercado — cap. 08 |
MeshPhysicalMaterial | PBR estendido | Vidro (transmission), verniz (clearcoat), tecido (sheen), iridescência — configuradores premium |
MeshToonMaterial | Cel shading | Estética cartoon/anime |
MeshNormalMaterial | — | Debug: visualiza normais como cores |
ShaderMaterial/NodeMaterial | Você decide | Efeitos customizados — caps. 12 e 17 |
const mat = new THREE.MeshStandardMaterial({
color: '#c0c4ce',
roughness: 0.35, // 0 espelho ↔ 1 fosco
metalness: 1.0, // 0 dielétrico ↔ 1 metal
transparent: false, // ligue apenas se precisar (transparência é cara)
side: THREE.FrontSide // FrontSide (padrão) | DoubleSide | BackSide
});
Aprofundamento — normais suaves vs. duras e por que o cubo tem 24 vértices
A iluminação usa a normal do vértice, interpolada pelo triângulo. Se vértices de faces vizinhas compartilham a mesma normal (média das faces), a luz varia suavemente e a aresta "desaparece" — smooth shading. Se cada face tem seus próprios vértices com normais perpendiculares à face, a aresta fica nítida — flat shading. Por isso o BoxGeometry tem 24 vértices (4 por face), não 8: cada canto existe 3 vezes, um por face, cada cópia com sua normal. O mesmo vale para UVs: onde a textura "corta", vértices se duplicam. Vértice ≠ ponto no espaço; vértice = combinação única de atributos.
Crie uma "galeria de materiais": 6 TorusKnotGeometry lado a lado, cada um com um material da tabela, e uma luz orbitando. Depois faça o terreno procedural e anime as ondas no loop (recalculando normais).
Y·06Luzes, sombras e câmeras
Os tipos de luz e seus custos, sombras que não parecem quebradas, e o controle de câmera que 90% dos projetos usam.
Tipos de luz
| Luz | Comporta-se como | Custo | Sombras? |
|---|---|---|---|
AmbientLight | Claridade uniforme vinda de todo lugar | Grátis | Não |
HemisphereLight | Céu (cor de cima) + chão (cor de baixo) | Quase grátis | Não |
DirectionalLight | Sol: raios paralelos de uma direção | Baixo | Sim |
PointLight | Lâmpada: irradia em todas as direções | Médio | Sim (cara: 6 mapas) |
SpotLight | Holofote: cone com ângulo e penumbra | Médio | Sim |
RectAreaLight | Softbox/janela de estúdio | Alto | Não |
Uma receita de estúdio que funciona para quase tudo (o clássico three-point lighting adaptado): uma DirectionalLight forte como key, uma HemisphereLight fraca como fill, e — para realismo de verdade — um environment map fazendo o papel de luz ambiente (cap. 08, é isso que os sites bonitos usam).
Sombras sem sofrimento
Sombras em tempo real usam shadow mapping: a cena é renderizada do ponto de vista da luz para uma textura de profundidade, e cada pixel testa "a luz me vê?". Isso exige ligar três coisas:
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // bordas suaves
luz.castShadow = true;
mesh.castShadow = true; // projeta sombra
chao.receiveShadow = true; // recebe sombra
// Qualidade: resolução do mapa e "enquadramento" da câmera da luz
luz.shadow.mapSize.set(2048, 2048);
luz.shadow.camera.near = 0.5;
luz.shadow.camera.far = 30;
// Para DirectionalLight, aperte o frustum ortográfico ao redor da cena:
Object.assign(luz.shadow.camera, { left: -10, right: 10, top: 10, bottom: -10 });
luz.shadow.bias = -0.0005; // corrige "shadow acne" (listras/pontilhado)
Sombra pixelada? O frustum da câmera da luz está grande demais para a resolução do mapa — aperte-o. Superfícies com listras (acne)? Ajuste bias em passos pequenos. Sombra "descolando" do objeto? Bias grande demais (peter-panning). Use new THREE.CameraHelper(luz.shadow.camera) para ver o frustum. Em muitos projetos comerciais, sombras dinâmicas são substituídas por truques baratos: ContactShadows (drei), sombras assadas em textura (baked) ou um simples plano com gradiente radial.
Câmeras e OrbitControls
PerspectiveCamera(fov, aspect, near, far): fov alto (80–100°) = grande angular dramática; baixo (25–40°) = teleobjetiva achatada, ótima para produto. Evite near minúsculo com far gigante — a precisão do z-buffer se esgota e superfícies próximas "brigam" (z-fighting, aquele chuvisco piscante).
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // inércia suave — sempre ligue
controls.dampingFactor = 0.06;
controls.target.set(0, 1, 0); // ao redor do que orbitar
controls.minDistance = 2; controls.maxDistance = 12;
controls.maxPolarAngle = Math.PI / 2; // não deixa a câmera ir "abaixo do chão"
// no loop:
controls.update(); // obrigatório com damping
Alternativas que valem conhecer: MapControls (pan estilo mapa), PointerLockControls (FPS), TrackballControls, e a excelente lib camera-controls para transições cinematográficas.
Iluminação é onde clientes percebem qualidade. O combo "environment map + uma directional com sombra suave + tone mapping correto" é o esqueleto de praticamente todo configurador de produto e site premiado no Awwwards/FWA. Saber diagnosticar acne/peter-panning de sombra é pergunta de entrevista prática.
Monte um mini-estúdio: chão, 3 objetos, key light com sombra bem calibrada, OrbitControls com limites. Depois adicione um lil-gui (npm i lil-gui) para ajustar intensidade, roughness e posição da luz ao vivo — o hábito de expor parâmetros em GUI é marca de profissional.
Z·07Texturas e UVs
Como imagens vestem geometria: UV mapping, os mapas que compõem um material, filtragem, mipmaps e compressão para a web.
UV mapping
Cada vértice carrega uma coordenada UV — um ponto (u, v) entre 0 e 1 que diz "que pedaço da imagem cai aqui". A GPU interpola UVs pelo triângulo e amostra a textura. Criar bons UVs para modelos complexos é trabalho de artista 3D (o "unwrap" no Blender); seu trabalho como dev é entender e depurar.
const texLoader = new THREE.TextureLoader();
const mapa = texLoader.load('/texturas/tijolos_cor.jpg');
mapa.colorSpace = THREE.SRGBColorSpace; // ESSENCIAL para mapas de cor!
mapa.wrapS = mapa.wrapT = THREE.RepeatWrapping;
mapa.repeat.set(4, 4); // ladrilhar
const mat = new THREE.MeshStandardMaterial({ map: mapa });
Os mapas de um material completo
| Slot | Controla | Espaço de cor |
|---|---|---|
map | Cor base (albedo) | sRGB |
normalMap | Micro-relevo falso — detalhe sem triângulos | Linear |
roughnessMap / metalnessMap | Variação de acabamento por pixel | Linear |
aoMap | Oclusão ambiente assada (cantos escuros) | Linear |
emissiveMap | Regiões que emitem luz (telas, néon) | sRGB |
alphaMap | Transparência por pixel (folhas, grades) | Linear |
displacementMap | Desloca vértices de verdade (exige malha densa) | Linear |
Só map e emissiveMap são sRGB; os demais são dados, não cor. Marcar um normal map como sRGB entorta toda a iluminação — bug clássico e difícil de notar. E o aoMap historicamente exige um segundo conjunto de UVs (uv2) em versões antigas; nas atuais ele usa o mesmo UV por padrão.
Filtragem, mipmaps e anisotropia
Quando a textura aparece menor ou maior que sua resolução nativa, a GPU precisa decidir como amostrar. Mipmaps são versões pré-reduzidas (1/2, 1/4, …) geradas automaticamente que eliminam o chiado de texturas ao longe. Para pisos vistos em ângulo rasante, aumente a anisotropia:
mapa.anisotropy = renderer.capabilities.getMaxAnisotropy(); // tipicamente 16
Texturas com dimensões potência de 2 (512, 1024, 2048…) são a convenção segura para mipmaps e compressão. 4096+ só com muita justificativa: uma textura 4k RGBA crua ocupa 64 MB de VRAM.
Compressão de GPU: KTX2 / Basis
JPG/PNG são pequenos no download, mas a GPU os descomprime para bitmap cru na VRAM. Formatos comprimidos de GPU (Basis Universal empacotado em .ktx2) permanecem comprimidos na VRAM (4–8× menos memória), transcodificados para o formato nativo de cada dispositivo. É o padrão moderno para projetos sérios, junto com pipeline de otimização glTF (cap. 09):
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2 = new KTX2Loader()
.setTranscoderPath('/basis/')
.detectSupport(renderer);
Baixe um conjunto PBR gratuito (ambientCG.com ou Polyhaven) — cor, normal, roughness, AO — e monte um material completo num plano e numa esfera. Quebre de propósito: marque o normal map como sRGB e observe; desligue mipmaps (minFilter = THREE.LinearFilter) e veja o chiado ao longe.
Z·08PBR, cor e realismo
O modelo físico que padronizou a indústria, environment maps/IBL, tone mapping e o checklist de gerência de cor que separa amador de profissional.
O contrato do PBR
Physically Based Rendering é um acordo da indústria (jogos, cinema, web) para descrever materiais por propriedades físicas mensuráveis, de modo que o mesmo material pareça correto sob qualquer iluminação:
- Base color — a cor própria da superfície, sem sombra nem brilho embutidos;
- Metalness — binário na natureza: metais refletem com a própria cor e não têm cor difusa; dielétricos (plástico, madeira, pele) refletem branco fraco. Valores intermediários só em transições/sujeira;
- Roughness — microrrugosidade: controla se o reflexo é nítido (0) ou espalhado (1). É o controle mais expressivo de todos.
A regra de ouro do realismo: tudo reflete. No mundo real não existe superfície sem reflexo especular — a diferença é só o quão borrado ele é. Por isso a peça central do PBR é o ambiente:
Environment maps e IBL
Image-Based Lighting usa uma imagem panorâmica HDR do ambiente como fonte de luz e de reflexos. Um único .hdr de estúdio substitui um rig de várias luzes e é o segredo do acabamento dos sites de produto:
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
const hdr = await new RGBELoader().loadAsync('/env/studio_1k.hdr');
hdr.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = hdr; // ilumina TODOS os materiais PBR
// scene.background = hdr; // opcional: também exibir como fundo
scene.environmentIntensity = 1.0; // (r163+) dosagem global
HDRs gratuitos e excelentes: Polyhaven. Para produto, prefira HDRs de estúdio (softboxes claras sobre fundo neutro). Em cenas sem fundo visível, um HDR 1k basta — o custo de VRAM de HDR 4k raramente se justifica para reflexos.
Tone mapping e exposição
Luz calculada em linear pode ultrapassar 1.0 (HDR interno). Tone mapping comprime esse alcance para a tela de forma fotográfica, evitando estouros:
renderer.toneMapping = THREE.ACESFilmicToneMapping; // padrão de mercado
// r16x+: THREE.AgXToneMapping e THREE.NeutralToneMapping (Khronos) são
// alternativas modernas — AgX lida melhor com cores saturadas intensas.
renderer.toneMappingExposure = 1.0;
Checklist de gerência de cor
renderer.outputColorSpace = THREE.SRGBColorSpace(já é o padrão nas versões atuais);- Texturas de cor marcadas como sRGB; mapas de dados em linear (cap. 07);
- Um tone mapping escolhido e consistente com o restante do pipeline do time (designers aprovam cores vendo o resultado tonemapeado);
- Luzes com intensidades físicas plausíveis em vez de "ambient 3.0 para clarear" — se está escuro, o problema costuma ser exposição ou falta de environment.
Aprofundamento — a BRDF: o que o Standard calcula por pixel
Uma BRDF (função de distribuição de reflectância bidirecional) responde: dada luz chegando da direção L, quanta sai na direção V? O MeshStandardMaterial implementa a BRDF de microfacetas Cook-Torrance usada por Unreal/Unity/glTF: a superfície é modelada como micro-espelhos cuja distribuição estatística de orientações é controlada pela roughness (termo D, distribuição GGX), com um termo geométrico G (micro-sombreamento) e o termo de Fresnel F — a reflectância cresce em ângulos rasantes, motivo pelo qual até asfalto vira espelho contra o sol baixo (aproximação de Schlick: F = F0 + (1−F0)(1−cosθ)⁵). A parte difusa é Lambert. Para IBL, o environment map é pré-convoluído em níveis de mip por roughness (PMREM) — por isso reflexos ficam borrados em materiais foscos sem custo extra por frame.
"Deixar igual ao render do Blender/V-Ray do cliente" é tarefa recorrente em agências. O caminho: mesmo HDR, tone mapping combinado entre as ferramentas, texturas com espaço de cor correto. Quem domina esse checklist resolve em minutos o que trava times por dias — e é exatamente o tipo de história boa para contar em entrevista.
Monte a "esfera de calibração": uma grade 6×6 de esferas variando metalness (eixo X) e roughness (eixo Y) sob um HDR de estúdio. É o teste padrão da indústria para validar pipeline de cor — e um ótimo item de portfólio técnico.
Z·09Modelos glTF, animação e pipeline de assets
Carregar modelos profissionais, tocar e misturar animações, e o pipeline de otimização (Draco, Meshopt, KTX2) que projetos reais exigem.
glTF: o "JPEG do 3D"
glTF 2.0 (extensão .glb na forma binária) é o formato padrão para 3D em tempo real: carrega geometria, materiais PBR, texturas, hierarquia, câmeras, luzes e animações num único arquivo, pronto para GPU. Esqueça OBJ (sem PBR/animação) e use FBX apenas como formato de intercâmbio de ferramentas.
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const draco = new DRACOLoader().setDecoderPath(
'https://www.gstatic.com/draco/versioned/decoders/1.5.7/');
const loader = new GLTFLoader().setDRACOLoader(draco);
const gltf = await loader.loadAsync('/modelos/robo.glb');
const modelo = gltf.scene;
scene.add(modelo);
// Percorrer a hierarquia é rotina diária:
modelo.traverse((obj) => {
if (obj.isMesh) {
obj.castShadow = obj.receiveShadow = true;
}
});
const cabeca = modelo.getObjectByName('Head'); // nomes vêm do Blender
Animações: AnimationMixer
Modelos riggados trazem clipes (Idle, Walk, Run…). O AnimationMixer os reproduz e mistura:
const mixer = new THREE.AnimationMixer(modelo);
const acoes = {};
for (const clip of gltf.animations) acoes[clip.name] = mixer.clipAction(clip);
acoes.Idle.play();
// Transição suave entre estados — o coração de qualquer personagem:
function trocaPara(nome, duracao = 0.35) {
const nova = acoes[nome];
nova.reset().fadeIn(duracao).play();
atual?.fadeOut(duracao);
atual = nova;
}
// no loop:
mixer.update(dt);
Três mecanismos de deformação que você encontrará: skinning (esqueleto de bones move a malha — personagens), morph targets (interpolação entre poses da malha — expressões faciais), e animação de transformações simples (portas, hélices). Todos passam pelo mesmo mixer.
Pipeline de otimização de assets
Um glb saído do Blender raramente está pronto para produção. O kit da casa é o glTF-Transform (e o utilitário gltfpack/Meshopt):
# Inspecionar o que há dentro (sempre comece por aqui):
npx @gltf-transform/cli inspect modelo.glb
# Otimização completa: dedup, poda, Draco na malha, KTX2 nas texturas, resize
npx @gltf-transform/cli optimize modelo.glb modelo_otim.glb \
--compress draco --texture-compress ktx2 --texture-size 1024
- Draco — comprime geometria (5–10× menor; custo: decodificação via WASM no load);
- Meshopt — alternativa com decodificação quase instantânea, ótima para muitos assets;
- KTX2 — texturas comprimidas de GPU (cap. 07): menos download e menos VRAM.
Metas de referência para web: herói de configurador < 5 MB, cena completa < 15 MB, personagem de jogo web 15–40k triângulos. E teste sempre em 4G + celular médio — seu público não tem sua GPU.
"Recebi um modelo de 300 MB do cliente" é o dia a dia de agências. O profissional que domina gltf-transform inspect → optimize, conversa com artistas sobre limites de polígonos e monta LODs vale ouro. Vagas de "3D pipeline engineer" são exatamente isso.
Baixe um personagem animado gratuito (Quaternius, Kenney ou Mixamo→Blender→glb), carregue-o, liste os clipes no console e implemente troca de animação por teclado com crossfade. Depois rode o optimize e compare tamanho e tempo de carga.
Z·10Interatividade e raycasting
Clicar, arrastar e passar o mouse sobre objetos 3D; coordenadas normalizadas; drag & drop e cursores que respondem.
Raycasting: o clique em 3D
A tela é 2D e a cena é 3D — para saber "em que objeto cliquei", lança-se um raio da câmera através do pixel do mouse e pergunta-se o que ele atravessa:
const raycaster = new THREE.Raycaster();
const ponteiro = new THREE.Vector2();
window.addEventListener('pointermove', (e) => {
// converte pixels → NDC (-1 a +1, Y invertido)
ponteiro.x = (e.clientX / window.innerWidth) * 2 - 1;
ponteiro.y = -(e.clientY / window.innerHeight) * 2 + 1;
});
// No loop (para hover) ou no evento de clique:
raycaster.setFromCamera(ponteiro, camera);
const hits = raycaster.intersectObjects(cliqueveis, true); // true = recursivo
if (hits.length) {
const alvo = hits[0]; // o mais próximo
alvo.object.material.emissive?.set('#334455');
// alvo.point → posição exata do impacto no mundo
// alvo.face / alvo.uv → triângulo e coordenada de textura atingidos
}
Passe ao raycaster apenas a lista de objetos clicáveis, nunca scene.children com recursão em cenas grandes — raycasting é na CPU e malhas densas custam caro. Para modelos pesados, use caixas de colisão invisíveis ou a lib three-mesh-bvh, que acelera o teste em ordens de magnitude (e é padrão em produção).
Padrões de interação que o mercado pede
// 1. Hover com cursor e destaque (o feedback mínimo de qualquer configurador)
let sobre = null;
function atualizaHover() {
raycaster.setFromCamera(ponteiro, camera);
const hit = raycaster.intersectObjects(cliqueveis)[0]?.object ?? null;
if (hit !== sobre) {
sobre?.material.emissive.set('#000000');
hit?.material.emissive.set('#223344');
document.body.style.cursor = hit ? 'pointer' : 'default';
sobre = hit;
}
}
// 2. Arrastar no plano do chão: interseção raio × plano matemático
const chao = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const ponto = new THREE.Vector3();
raycaster.ray.intersectPlane(chao, ponto); // ponto do mouse no chão
selecionado?.position.copy(ponto);
Para gizmos de arrastar/rotacionar prontos, use DragControls e TransformControls dos addons (lembre de desligar o OrbitControls durante o drag: controls.enabled = false).
Sincronizando 3D com HTML
Rótulos, tooltips e hotspots costumam ser HTML posicionado sobre o canvas — projeta-se a posição 3D para a tela:
const p = objeto.position.clone().project(camera); // → NDC
const x = ( p.x * 0.5 + 0.5) * innerWidth;
const y = (-p.y * 0.5 + 0.5) * innerHeight;
rotulo.style.transform = `translate(${x}px, ${y}px)`;
rotulo.hidden = p.z > 1; // atrás da câmera
Os addons CSS2DRenderer/CSS3DRenderer fazem isso de forma estruturada.
Construa um mini-configurador: 5 objetos numa mesa; hover destaca e muda o cursor; clique abre um painel HTML com o nome e um seletor de cor que altera o material; um botão "focar" anima a câmera até o objeto (lerp da posição + do target dos controls).
Z·11Organização de cena, carregamento e UX
Estruturar projetos que crescem, gerenciar recursos e memória, e a experiência de carregamento — onde produtos 3D ganham ou perdem o usuário.
Arquitetura mínima que escala
// Um esqueleto usado (com variações) em estúdios reais:
// src/
// core/Experience.js ← singleton: renderer, scene, camera, loop, resize
// core/Assets.js ← manifest + LoadingManager + cache
// world/ ← cada "coisa" da cena é uma classe com update(dt)
// utils/Debug.js ← lil-gui atrás de ?debug na URL
class Experience {
constructor(canvas) { /* renderer, scene, camera, clock */ this.itens = []; }
add(item) { this.itens.push(item); } // itens têm update(dt)
tick(dt) { for (const i of this.itens) i.update?.(dt); }
}
Os princípios importam mais que a estrutura exata: um único loop distribuindo dt; entidades autocontidas com update() e dispose(); parâmetros expostos em GUI de debug; e eventos (ou um pequeno event bus) em vez de referências cruzadas.
LoadingManager e telas de carregamento
const manager = new THREE.LoadingManager();
manager.onProgress = (url, ok, total) => {
barra.style.transform = `scaleX(${ok / total})`;
};
manager.onLoad = () => iniciaExperiencia(); // só revele quando TUDO chegou
const texLoader = new THREE.TextureLoader(manager);
const gltfLoader = new GLTFLoader(manager);
Padrões de UX que sites premiados repetem: barra/percentual real (nunca spinner infinito), revelação animada da cena ao terminar (fade + leve dolly de câmera), e aquecimento de shaders — chame renderer.compileAsync(scene, camera) antes de revelar para evitar o engasgo da primeira renderização.
Memória: dispose ou vazamento
O garbage collector do JS não libera VRAM. Todo recurso de GPU precisa de descarte explícito ao sair de cena:
function descarta(obj) {
obj.traverse((n) => {
n.geometry?.dispose();
const mats = Array.isArray(n.material) ? n.material : [n.material];
for (const m of mats ?? []) {
for (const v of Object.values(m ?? {})) v?.isTexture && v.dispose();
m?.dispose();
}
});
obj.parent?.remove(obj);
}
// Monitore: renderer.info.memory (geometries/textures) e renderer.info.render.calls
SPAs que montam/desmontam cenas (rotas em React/Vue) vazam VRAM alegremente sem dispose() — a aba trava depois de alguns minutos de navegação. Verificar renderer.info após trocar de rota deveria ser parte do seu checklist de QA.
Perguntas reais de entrevista para vagas web-3D: "como você estrutura uma experiência Three.js grande?", "como funciona o descarte de recursos?", "o que você faz enquanto os assets carregam?". Este capítulo é o roteiro das respostas.
X·12Shaders e GLSL
Programar a GPU diretamente: a linguagem GLSL, vertex e fragment shaders, uniforms, e as técnicas (noise, sdf, deformação) por trás dos sites que ganham prêmios.
Aqui começa o território que diferencia salários. Materiais prontos cobrem o realismo; shaders cobrem tudo o que é único: água, hologramas, transições líquidas, dissoluções, auroras, o "toque" que faz um site parecer mágico.
O modelo mental
Um shader é um programinha que roda na GPU uma vez por vértice (vertex shader) ou uma vez por fragmento/pixel (fragment shader), milhões de vezes em paralelo — e cada execução é cega às outras. Você não escreve "um loop pelos pixels"; você escreve "a fórmula de UM pixel" e a GPU aplica a todos.
Três canais de dados alimentam um shader:
- attributes — dados por vértice (position, normal, uv) — só o vertex shader lê;
- uniforms — valores globais iguais para todos (tempo, mouse, cor, texturas) — é como o JS conversa com o shader a cada frame;
- varyings — valores que o vertex shader escreve e chegam interpolados pelo triângulo ao fragment shader (é assim que UVs e normais chegam por pixel).
GLSL em 60 segundos
// Tipos: float, vec2, vec3, vec4, mat4, sampler2D. Tipagem estrita: 1.0, não 1.
vec3 cor = vec3(1.0, 0.5, 0.0);
cor.rgb; cor.xy; cor.bgr; // "swizzling": remonte componentes à vontade
float b = dot(normal, luz); // built-ins: dot, cross, normalize, length,
b = clamp(b, 0.0, 1.0); // mix (lerp), step, smoothstep, fract, sin...
vec3 fin = mix(corA, corB, b); // mix é o lerp — a função mais usada do GLSL
vec4 tex = texture2D(mapa, vUv); // amostrar textura
ShaderMaterial completo
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColorA:{ value: new THREE.Color('#0b1e3f') },
uColorB:{ value: new THREE.Color('#6ea8ff') },
},
vertexShader: /* glsl */`
uniform float uTime;
varying vec2 vUv;
varying float vAltura;
void main() {
vUv = uv;
vec3 p = position;
// onda: desloca vértices em Z pelo seno da posição + tempo
p.z += sin(p.x * 3.0 + uTime * 2.0) * 0.15
+ sin(p.y * 2.0 + uTime * 1.3) * 0.10;
vAltura = p.z;
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}`,
fragmentShader: /* glsl */`
uniform vec3 uColorA, uColorB;
varying vec2 vUv;
varying float vAltura;
void main() {
// gradiente pela altura da onda + vinheta pelas UVs
vec3 cor = mix(uColorA, uColorB, smoothstep(-0.25, 0.25, vAltura));
float vin = smoothstep(1.0, 0.4, distance(vUv, vec2(0.5)) * 2.0);
gl_FragColor = vec4(cor * vin, 1.0);
}`,
});
// no loop:
material.uniforms.uTime.value = clock.getElapsedTime();
O arsenal clássico do fragment shader
- smoothstep(a, b, x) — a ferramenta universal para máscaras e transições suaves;
- fract / step — repetição e padrões (grades, listras, meio-tons);
- Noise — pseudo-aleatoriedade orgânica. Simplex/Perlin noise gera fogo, fumaça, terreno, distorção; FBM (somar oitavas de noise) gera nuvens e mármore. Copie implementações consagradas (Ashima/IQ) — ninguém escreve noise do zero;
- SDFs (signed distance functions) — descrever formas por distância matemática; base do ray marching (cenas inteiras sem geometria) e de UI/texto nítido em qualquer escala;
- Fresnel —
pow(1.0 - dot(normal, viewDir), 3.0)— bordas brilhantes; o segredo de hologramas, force fields e cristais.
Estendendo materiais prontos
Na prática profissional, você raramente reescreve PBR do zero — você injeta código nos shaders embutidos com onBeforeCompile (ou a lib three-custom-shader-material): mantém luz/sombra do Standard e adiciona só a sua deformação/cor. Nos capítulos 17 você verá a versão moderna disso: TSL, onde essa composição é nativa.
Aprofundamento — derivadas de tela, aliasing e antialiasing analítico
GLSL oferece dFdx()/dFdy() — quanto uma variável muda entre pixels vizinhos (a GPU rasteriza em blocos 2×2 justamente para isso). Com fwidth(x) = |dFdx| + |dFdy| você faz antialiasing analítico de qualquer padrão procedural: em vez de step(0.5, x) (borda dura serrilhada), use smoothstep(0.5 - fwidth(x), 0.5 + fwidth(x), x) — a transição passa a ter exatamente ~1 pixel em qualquer zoom. É também a base do texto por MSDF, padrão para tipografia nítida em 3D.
Shaders são o divisor de águas de vagas creative developer (as mais bem pagas do nicho web-3D). Portfólios vencedores quase sempre têm 2–3 experimentos de shader autorais. Recursos canônicos de estudo: The Book of Shaders, ShaderToy (leia e desmonte), e os artigos de Inigo Quilez.
(1) Gradiente animado com mix + sin(uTime). (2) Padrão de listras com fract, depois antialiase com fwidth. (3) Cole um simplex noise e faça uma "dissolução": if (noise(vUv*5.) < uProgress) discard; com borda emissiva via smoothstep. (4) Fresnel num torus knot = holograma.
X·13Pós-processamento
Render targets, o EffectComposer, os efeitos que definem o "look" (bloom, DOF, SSAO, grain) e o custo de cada um.
Render targets: renderizar para textura
O conceito que destrava tudo: em vez de desenhar na tela, desenhe num WebGLRenderTarget — uma textura. Essa textura pode virar entrada de outro shader (efeitos em tela cheia), uma TV dentro da cena, um espelho, um mini-mapa. Pós-processamento é exatamente isso: renderizar a cena para textura e passá-la por uma corrente de shaders de tela cheia.
EffectComposer
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass(
new THREE.Vector2(innerWidth, innerHeight),
0.6, // força
0.4, // raio
0.85 // threshold: só o que passa disso "vaza" luz
));
composer.addPass(new OutputPass()); // aplica tone mapping + sRGB no final
// no loop, no lugar de renderer.render():
composer.render();
// e no resize: composer.setSize(w, h)
Para projetos sérios considere a lib postprocessing (pmndrs): funde vários efeitos num único shader (muito mais rápida que empilhar passes) e alimenta o @react-three/postprocessing do ecossistema R3F.
O cardápio e seus custos
| Efeito | O que dá | Custo |
|---|---|---|
| Bloom | Brilho que "vaza" de áreas intensas — néon, sci-fi | Médio |
| Vignette / grain / chromatic aberration | Acabamento fílmico — o combo favorito de sites premiados | Baixo |
| Depth of Field | Desfoque fotográfico por distância | Alto |
| SSAO / GTAO | Oclusão ambiente de tela — "assenta" objetos na cena | Alto |
| SSR | Reflexos de tela em pisos molhados | Muito alto |
| SMAA / FXAA / TAA | Antialiasing pós (necessário: o MSAA do canvas não sobrevive ao composer sem configuração) | Baixo–médio |
| Outline | Contorno de seleção — padrão em ferramentas/configuradores | Médio |
Cada pass re-renderiza a tela inteira: em 4K retina, 3 passes ingênuos podem custar mais que a cena. Em mobile, limite-se a 1 pass combinado ou use "fake post": grain/vignette como um plano transparente ou CSS sobre o canvas.
Cena noturna com letreiros emissivos + bloom com threshold bem calibrado + vignette + grain leve. Compare o frame time (cap. 15) com e sem o composer, em resolução retina e não-retina.
X·14Partículas, instancing e GPGPU
De milhares a milhões de elementos: Points, InstancedMesh e a técnica GPGPU que anima tudo na GPU.
Points: a forma mais barata
const N = 20000;
const pos = new Float32Array(N * 3);
for (let i = 0; i < N * 3; i++) pos[i] = (Math.random() - 0.5) * 20;
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
const pontos = new THREE.Points(geo, new THREE.PointsMaterial({
size: 0.05, sizeAttenuation: true,
transparent: true, depthWrite: false, // combo padrão p/ partículas
blending: THREE.AdditiveBlending // brilho que soma = mágica
}));
scene.add(pontos);
Para partículas com comportamento único por elemento (fase, velocidade, cor), adicione atributos customizados e anime no vertex shader — CPU parada, GPU fazendo o trabalho. É assim que nascem os campos de partículas fluidos dos sites de agência.
InstancedMesh: mil malhas, um draw call
Quando os elementos são malhas de verdade (árvores, cadeiras, asteroides), InstancedMesh desenha todas as cópias em um único draw call, com transformação e cor por instância:
const inst = new THREE.InstancedMesh(geometria, material, 5000);
const m = new THREE.Matrix4();
for (let i = 0; i < 5000; i++) {
m.compose(posicaoAleatoria(), rotacaoAleatoria(), escala);
inst.setMatrixAt(i, m);
inst.setColorAt(i, new THREE.Color().setHSL(Math.random(), 0.6, 0.55));
}
inst.instanceMatrix.needsUpdate = true; // após alterar (e instanceColor também)
GPGPU: simulação na GPU
Para simular milhões de partículas (atração, fluidos, boids), a técnica é usar a própria GPU como computador: o estado (posição/velocidade de cada partícula) vive em texturas, e a cada frame um shader lê a textura do frame anterior e escreve a próxima em outro render target (ping-pong). O render final lê essas texturas no vertex shader para posicionar cada partícula. O Three.js traz o utilitário GPUComputationRenderer para orquestrar isso — e o capítulo 17 mostra a versão moderna com compute shaders WebGPU, que dispensa a gambiarra de texturas.
Aprofundamento — curl noise e os "fluidos de mentira"
Grande parte dos efeitos de "fumaça mágica" da web não resolve Navier-Stokes: usa curl noise — o rotacional de um campo de noise 3D. Por construção o rotacional tem divergência zero, ou seja, o campo de velocidades resultante não comprime nem cria matéria: partículas advectadas por ele se movem como um fluido incompressível, sem colisões nem pressão. Custa uma amostragem de noise por partícula e parece caro. Já simulações de fluido "reais" na web (tinta, fumaça interativa) usam grids Eulerianos com o método de Jos Stam (advect → diffuse → project), implementável em ping-pong de texturas.
"Quantos draw calls?" é a pergunta de entrevista favorita da área. Resposta esperada: instancing para repetição, atributos + vertex shader para animar em massa, GPGPU/compute quando a simulação em CPU não escala. Um experimento de GPGPU no portfólio (100k+ partículas interativas) chama atenção imediata.
(1) Galáxia com 50k Points: distribua em espiral, cor por distância ao centro, additive blending, rotação lenta. (2) Migre para animação 100% em vertex shader usando um atributo aOffset. (3) Grama: InstancedMesh de 10k lâminas com vento por shader.
X·15Performance e otimização
Medir antes de otimizar, o modelo mental CPU vs GPU, e o arsenal completo: draw calls, LOD, culling, resolução dinâmica e orçamento de memória.
Meça primeiro
import Stats from 'three/addons/libs/stats.module.js';
const stats = new Stats(); document.body.appendChild(stats.dom);
// no loop: stats.update()
console.log(renderer.info.render.calls, // draw calls por frame
renderer.info.render.triangles, // triângulos
renderer.info.memory); // geometrias/texturas vivas
Diagnóstico em 30 segundos: o frame está lento. Reduza a janela para 200px — se o fps sobe, o gargalo é GPU/fragmentos (resolução, shaders caros, overdraw, post); se não muda, é CPU (draw calls demais, JS pesado, física). Essa bisecção orienta tudo o que segue. Ferramentas profissionais: aba Performance do DevTools e o Spector.js (captura um frame WebGL, call por call).
Reduzindo trabalho de CPU: draw calls
Cada mesh visível gera ≥1 draw call, e cada call tem custo fixo de CPU. Metas de referência: < 100–300 calls em mobile, algumas centenas a ~1000 em desktop. O arsenal:
- InstancedMesh (cap. 14) para repetições;
- Merge de malhas estáticas com o mesmo material (
BufferGeometryUtils.mergeGeometries); - BatchedMesh (moderno, r160+): geometrias diferentes com o mesmo material em um call — o "sonho do merge sem perder objetos individuais";
- Atlas de texturas: menos materiais → mais coisas compartilham call.
Reduzindo trabalho de GPU
- Resolução é o custo dominante:
pixelRatio≤ 2, e resolução dinâmica (baixar o pixel ratio quando o frame time passa do orçamento) é técnica de gente grande; - LOD:
THREE.LODtroca malhas por versões simplificadas conforme a distância; - Frustum culling é automático, mas malhas mescladas gigantes nunca saem do frustum — equilibre merge × culling; para cenas de interiores, considere occlusion manual por setores;
- Sombras: menos luzes com sombra, mapas menores,
autoUpdate = falseem cena estática; - Materiais: Lambert/Basic onde ninguém notará; transparência e
DoubleSidesó onde necessário; - Texturas: KTX2 (menos VRAM e banda), tamanhos honestos.
Fluidez e memória
- Zero alocações no loop (cap. 03) — engasgos de GC são visíveis;
renderer.compileAsync()no carregamento — compilar shader no meio do jogo = travada;- Descarregue o que saiu de cena com
dispose()(cap. 11); - Trabalho pesado de CPU (pathfinding, geração procedural, decodificação) em Web Workers; loaders Draco/KTX2 já usam workers por baixo.
Otimização é a habilidade mais monetizável do nicho: consultores são contratados só para "fazer rodar no celular do cliente". Um case de portfólio no formato "antes: 14 fps / depois: 60 fps — o que fiz e como medi" vale mais que três demos bonitas.
Crie uma cena propositalmente ruim (2.000 meshes individuais, sombras 4096, pixel ratio 3) e otimize por etapas medindo cada ganho: instancing → sombras calibradas → pixel ratio 2 → LOD. Documente a jornada; isso vira um artigo de portfólio.
X·16Física
Motores de física na web, o padrão de sincronização física↔render, e colisões simples sem motor nenhum.
Quando você nem precisa de motor
Muita interação se resolve com matemática direta — e entrevistadores adoram ver isso:
// Colisão esfera-esfera: distância < soma dos raios
if (a.position.distanceTo(b.position) < rA + rB) { /* colidiu */ }
// Caixas alinhadas (AABB):
const boxA = new THREE.Box3().setFromObject(meshA);
if (boxA.intersectsBox(boxB)) { /* colidiu */ }
// Gravidade + pulo de plataforma, sem motor:
vel.y -= 9.8 * dt;
pos.y += vel.y * dt;
if (pos.y < 0) { pos.y = 0; vel.y = 0; noChao = true; }
Motores: qual escolher
| Motor | Perfil |
|---|---|
| Rapier | Rust→WASM, rápido, determinístico, mantido ativamente — a escolha padrão atual; wrappers prontos no R3F (@react-three/rapier) |
| cannon-es | JS puro, simples de aprender, ótimo para escopo pequeno |
| Ammo.js / Jolt | Ports WASM de motores AAA (Bullet, Jolt) — máxima capacidade, mais atrito de API |
O padrão universal: dois mundos sincronizados
O motor de física não sabe que o Three.js existe. Você mantém duas representações — o corpo físico e a mesh — e copia transformações a cada frame:
import RAPIER from '@dimforge/rapier3d-compat';
await RAPIER.init();
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
// corpo dinâmico + colisor
const body = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, 0));
world.createCollider(RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5), body);
// chão estático
world.createCollider(RAPIER.ColliderDesc.cuboid(20, 0.1, 20));
// no loop — passo FIXO para estabilidade/determinismo:
let acc = 0; const PASSO = 1 / 60;
function tick(dt) {
acc += dt;
while (acc >= PASSO) { world.step(); acc -= PASSO; }
const t = body.translation(), r = body.rotation();
mesh.position.set(t.x, t.y, t.z);
mesh.quaternion.set(r.x, r.y, r.z, r.w);
}
Colisores devem ser formas simples (caixas, esferas, cápsulas, convex hulls) mesmo que a malha visual seja complexa — trimesh dinâmico é caro e instável. Personagens usam cápsula + character controller (o Rapier tem um embutido). E física ama passo fixo: nunca passe dt variável cru para world.step().
"Derrube a pilha": 30 caixas empilhadas (InstancedMesh + um corpo por caixa) e uma esfera lançada da câmera no clique (raycaster dá a direção; body.applyImpulse dá o tiro). É a demo clássica de física — e diverte qualquer recrutador.
X·17WebGPU e TSL — o estado da arte
A nova API gráfica da web, o WebGPURenderer do Three.js, a Three.js Shading Language e os compute shaders que mudam o que é possível no navegador.
Por que WebGPU
WebGL (2011) é um espelho do OpenGL ES — uma arquitetura dos anos 90. WebGPU é a API moderna, espelhando Vulkan/Metal/DirectX 12, e já roda nos principais navegadores (Chrome/Edge desde 2023, com Firefox e Safari completando o suporte no ciclo 2024–2025). O que muda na prática:
- Compute shaders — programas de GPU de propósito geral, sem o teatro de "desenhar num quadrado" do GPGPU clássico: simulações de milhões de partículas, culling na GPU, skinning, IA;
- Menos overhead de CPU por draw call — pipelines pré-validados e bind groups tornam cenas com muitos objetos mais baratas;
- Shaders em WGSL, uma linguagem nova e mais estrita que o GLSL.
WebGPURenderer: a migração é suave
O Three.js aposta em transição indolor: o WebGPURenderer usa WebGPU quando disponível e cai automaticamente para WebGL2 quando não — mesmo código, dois backends:
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // inicialização é assíncrona
renderer.setAnimationLoop(loop); // o resto da sua cena não muda
TSL: shaders em JavaScript
A peça central da era moderna é a Three.js Shading Language: você escreve shaders como grafos de nós em JavaScript, e o Three.js compila para WGSL ou GLSL conforme o backend. Acabou escrever o mesmo efeito duas vezes — e acabou o onBeforeCompile: materiais são estendíveis por design:
import { MeshStandardNodeMaterial } from 'three/webgpu';
import { uv, time, sin, mix, color, positionLocal, vec3, uniform, Fn }
from 'three/tsl';
const mat = new MeshStandardNodeMaterial();
// Uniform "de verdade", controlável do JS:
const uProgress = uniform(0);
// Cor: gradiente animado — note que é JS componível, não string de GLSL
const faixa = sin(uv().y.mul(20).add(time.mul(2))).mul(0.5).add(0.5);
mat.colorNode = mix(color('#0b1e3f'), color('#6ea8ff'), faixa);
// E dá para deformar a posição mantendo TODA a iluminação PBR:
mat.positionNode = positionLocal.add(
vec3(0, sin(positionLocal.x.mul(3).add(time)).mul(0.1), 0)
);
// Lógica complexa vira função reutilizável:
const pulso = Fn(([t]) => sin(t.mul(6.28)).mul(0.5).add(0.5));
mat.emissiveNode = color('#ff6b6b').mul(pulso(uProgress));
Os node materials (MeshStandardNodeMaterial, MeshBasicNodeMaterial…) expõem "encaixes" para cada estágio: colorNode, positionNode, normalNode, emissiveNode, roughnessNode, opacityNode… Você customiza um pedaço e herda o resto do PBR — o que antes exigia cirurgia com onBeforeCompile.
Compute shaders com TSL
import { instancedArray, instanceIndex, Fn } from 'three/tsl';
const N = 500_000;
const posicoes = instancedArray(N, 'vec3'); // buffer que VIVE na GPU
const velocidades = instancedArray(N, 'vec3');
const passo = Fn(() => {
const p = posicoes.element(instanceIndex);
const v = velocidades.element(instanceIndex);
v.y.subAssign(9.8 * 0.016); // gravidade
p.addAssign(v.mul(0.016)); // integrar
})().compute(N);
// no loop:
renderer.compute(passo); // meio milhão de partículas, CPU intocada
Isso substitui o GPGPU de ping-pong de texturas do capítulo 14 por algo direto e legível — e é a técnica por trás das demos recentes de multidões, cabelos, tecidos e fluidos rodando no navegador.
Pós-processamento e o ecossistema novo
No mundo WebGPU o pós-processamento também virou nó: pass(scene, camera), bloom(), efeitos encadeados como expressões TSL — mais rápido e componível que o EffectComposer clássico. A direção do projeto é clara: TSL + WebGPURenderer é o presente-futuro do Three.js, com o pipeline WebGL clássico em manutenção.
Estratégia de adoção profissional (2025–2026): projetos novos com visual customizado → comece já em three/webgpu + TSL (o fallback WebGL2 protege o alcance). Projetos existentes estáveis → migre quando precisar de compute ou de um efeito que só o novo pipeline dá. Em toda decisão, meça no hardware do seu público.
"Experiência com WebGPU/TSL" começou a aparecer em vagas de ponta e é raro entre candidatos — uma janela de diferenciação real. Uma demo de compute (boids, partículas interativas em escala de milhões) hoje coloca um portfólio no topo da pilha.
(1) Porte sua cena do cap. 04 para WebGPURenderer e confirme o fallback desligando WebGPU nas flags do navegador. (2) Refaça o shader de ondas do cap. 12 em TSL com positionNode — compare a legibilidade. (3) Boids 2D com compute: dois buffers, um kernel de flocking, render por instancing.
X·18React Three Fiber e o ecossistema pmndrs
Three.js declarativo dentro do React: R3F, a caixa de ferramentas drei, e o stack com que grande parte das vagas web-3D trabalha hoje.
O que é o R3F
React Three Fiber é um renderizador React para Three.js: cada elemento JSX vira um objeto Three real (não é wrapper — é o próprio Three, com reconciliação do React). O ganho é a componibilidade: cenas viram componentes reutilizáveis, com estado, props e o ecossistema React inteiro:
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Environment, ContactShadows } from '@react-three/drei';
import { useRef, useState } from 'react';
function Caixa(props) {
const ref = useRef();
const [hover, setHover] = useState(false);
useFrame((state, dt) => { ref.current.rotation.y += dt; }); // o loop
return (
<mesh {...props} ref={ref}
onPointerOver={() => setHover(true)} // raycasting embutido!
onPointerOut={() => setHover(false)}>
<boxGeometry />
<meshStandardMaterial color={hover ? '#ff6b6b' : '#7bd88f'} />
</mesh>
);
}
export default function App() {
return (
<Canvas camera={{ position: [3, 2, 4], fov: 50 }} shadows>
<Caixa position={[0, 0.5, 0]} castShadow />
<Environment preset="studio" />
<ContactShadows opacity={0.4} blur={2} />
<OrbitControls enableDamping makeDefault />
</Canvas>
);
}
Repare no que veio de graça: eventos de ponteiro por objeto (sem raycaster manual), resize automático, e a convenção <nomeDaClasse args={[...]} /> mapeando qualquer classe do Three. Tudo desta apostila continua valendo — R3F é sintaxe, não outra engine; quem não sabe Three.js escreve R3F ruim.
O ecossistema pmndrs
| Pacote | O que entrega |
|---|---|
@react-three/drei | Centenas de helpers prontos: Environment, Text (SDF), Html (DOM em 3D), useGLTF, Instances, ScrollControls, MeshTransmissionMaterial (o vidro dos sites premiados)… |
@react-three/postprocessing | Pós-processamento declarativo e performático |
@react-three/rapier | Física: <RigidBody> em volta de qualquer mesh |
@react-three/xr | VR/AR sobre o cap. 19 |
zustand | Estado global leve — o padrão do nicho para conectar UI e cena |
leva | GUI de debug com uma linha por parâmetro |
gltfjsx | CLI que converte .glb em componente JSX navegável — pilar do fluxo com designers |
As três regras de performance do R3F
- Nunca use setState no frame. Animação contínua muta refs dentro de
useFrame; re-render do React 60×/s mata o app; - Memoize recursos (geometrias, materiais, vetores) com
useMemo— ou eles renascem a cada render; - Leia estado transitório sem assinar:
useStore.getState()dentro do frame, em vez de hook que re-renderiza.
Grande parte das vagas web-3D pede exatamente "React + Three.js (R3F)" — é o stack de agências e de produto (e-commerce 3D, ferramentas de design, dashboards). Estude os exemplos do site docs.pmnd.rs e os starters do ecossistema: são o vocabulário comum do nicho. Vue tem equivalente (TresJS), mas o mercado é majoritariamente React.
Refaça o mini-configurador do cap. 10 em R3F: modelo via useGLTF, materiais trocados por estado no zustand, Environment + ContactShadows, UI em HTML normal fora do Canvas. Publique na Vercel — pronto, um item de portfólio no stack do mercado.
X·19WebXR: VR e AR no navegador
Experiências imersivas sem instalar app: sessões XR, controles, interação e as regras de conforto e performance que XR impõe.
Por que WebXR importa
A promessa é forte: apontar o navegador do headset (Quest, Vision Pro) ou do celular para uma URL e entrar na experiência — sem loja, sem download. Casos reais: treinamento corporativo, visualização de arquitetura, "veja o móvel na sua sala" (AR de e-commerce), museus e ativações de marca.
Ativando XR no Three.js
import { VRButton } from 'three/addons/webxr/VRButton.js';
// (ARButton para realidade aumentada)
renderer.xr.enabled = true;
document.body.appendChild(VRButton.createButton(renderer));
// XR controla o timing dos frames — por isso setAnimationLoop é obrigatório:
renderer.setAnimationLoop(loop); // (você já estava usando, certo?)
Só isso já dá estereoscopia e rastreamento de cabeça. A câmera passa a ser controlada pelo headset; para "teleportar" o jogador, mova um grupo que contenha a câmera (o rig).
Controles e mãos
const ctrl = renderer.xr.getController(0); // raio de seleção/eventos
ctrl.addEventListener('selectstart', aoApertar); // gatilho
scene.add(ctrl);
const grip = renderer.xr.getControllerGrip(0); // modelo físico do controle
import { XRControllerModelFactory } from
'three/addons/webxr/XRControllerModelFactory.js';
grip.add(new XRControllerModelFactory().createControllerModel(grip));
scene.add(grip);
// Hand tracking: renderer.xr.getHand(0) + XRHandModelFactory
O padrão de interação universal é o laser pointer: um raycaster saindo do controle (a matriz do controle dá origem e direção) para apontar/selecionar. Em AR, o hit test da WebXR encontra superfícies reais para ancorar objetos.
As regras que XR não perdoa
- 72–90 fps ou enjoo. O orçamento por frame cai para ~11 ms — e renderizando duas vezes (um olho cada). Tudo do cap. 15 vale em dobro; use
renderer.xr.setFramebufferScaleFactor()como válvula; - Nunca mova a câmera do usuário sem ação dele (locomoção suave enjoa; prefira teleporte ou vinheta durante movimento);
- Escala 1:1: em XR, 1 unidade = 1 metro é lei — um botão a 3 metros de altura é um bug físico;
- UI a 1–3 m de distância, olhando para o usuário; texto grande e com alto contraste.
Sem headset? O Immersive Web Emulator (extensão de navegador) simula um Quest com controles. Em R3F, @react-three/xr encapsula tudo isso em componentes. E teste AR direto no celular — Chrome/Android suporta ARButton nativamente.
XR web é nicho pequeno mas com pouquíssima concorrência qualificada — consultorias de treinamento industrial, imobiliárias e agências de ativação pagam bem. O caso de negócio matador para vender: "seu cliente entra pelo link, sem instalar nada".
Galeria VR: uma sala com 6 quadros (planos com textura), teleporte apontando para o chão e um seletor que troca a exposição. Teste no emulador; se tiver um Quest, sirva com HTTPS local (vite --host + certificado) e entre pelo navegador do headset.
X·20Arquitetura de jogos web
O que separa uma demo de um jogo: game loop, máquinas de estado, ECS, input, câmeras de gameplay e uma nota honesta sobre multiplayer.
O loop de jogo completo
Jogos separam simulação (timestep fixo — física e regras determinísticas) de renderização (na taxa do monitor, interpolando entre estados):
const PASSO = 1 / 60; let acc = 0;
function frame(dt) {
acc += Math.min(dt, 0.25); // clamp: evita a "espiral da morte"
while (acc >= PASSO) { simula(PASSO); acc -= PASSO; }
renderiza(acc / PASSO); // alpha para interpolar visual
}
Máquinas de estado em dois níveis
// Nível jogo: Menu → Jogando → Pausa → GameOver
// Nível entidade: Idle → Run → Jump → Attack (casada com o crossfade do cap. 09)
const estados = {
idle: { entra(){ trocaPara('Idle'); }, update(dt){ if (input.mov) fsm.vai('run'); } },
run: { entra(){ trocaPara('Run'); }, update(dt){ move(dt); if (input.pulo) fsm.vai('jump'); } },
jump: { entra(){ vel.y = 6; trocaPara('Jump'); }, update(dt){ if (noChao) fsm.vai('idle'); } },
};
FSMs explícitas eliminam a sopa de booleanos (isJumping && !isAttacking && ...) que afunda projetos de iniciantes.
Organização: composição e ECS
Herança profunda (Inimigo extends Personagem extends Entidade…) quebra rápido. A indústria usa composição: entidades são bolsas de componentes de dados (Position, Velocity, Health, Renderable) processadas por sistemas que varrem quem tem os componentes certos. Para jogos web com muitas entidades, bibliotecas ECS como bitecs ou miniplex trazem isso pronto (e são amigas do cache da CPU). Para escopo pequeno, classes com update() (cap. 11) bastam — maturidade é dimensionar a arquitetura ao jogo.
Input e câmera de gameplay
// Input: acumule ESTADO, não reaja a eventos no gameplay
const teclas = new Set();
addEventListener('keydown', e => teclas.add(e.code));
addEventListener('keyup', e => teclas.delete(e.code));
// na simulação: const dir = (teclas.has('KeyD') ? 1 : 0) - (teclas.has('KeyA') ? 1 : 0);
// Câmera 3ª pessoa: siga um ALVO ATRASADO, nunca cole no jogador
alvoCam.lerp(jogador.position, 1 - Math.exp(-4 * dt)); // suavização correta p/ dt variável
camera.position.copy(alvoCam).add(offset);
camera.lookAt(alvoCam);
Complete o pacote com: gamepad (navigator.getGamepads()), toque virtual em mobile, áudio posicional (THREE.PositionalAudio) e juice — screen shake, partículas de impacto, hit-stop de 60 ms. Juice é 50% da sensação de qualidade.
Aprofundamento — multiplayer: o mínimo honesto
Tempo real usa WebSockets (ou WebRTC DataChannel/WebTransport para UDP-like). A arquitetura séria é servidor autoritativo: clientes enviam inputs, o servidor simula e devolve estado — nunca confie no cliente. Para esconder a latência: client-side prediction (aplique seu input localmente já), server reconciliation (corrija quando o servidor responder) e entity interpolation (renderize os outros ~100 ms no passado, interpolando entre snapshots). São os três pilares descritos nos artigos clássicos de Gabriel Gambetta e nos papers da Valve — leitura obrigatória antes de qualquer jogo online.
Jogos 100% web viraram indústria de novo: portais como Poki/CrazyGames pagam por receita de anúncio, estúdios de playable ads (versões jogáveis de 2–5 MB de jogos mobile) contratam constantemente, e gamificação aparece em marketing de grandes marcas. Playable ads, aliás, são um excelente primeiro emprego do nicho: escopo pequeno, restrições brutais de tamanho — tudo que esta apostila ensina.
Um mini-jogo completo em 2 semanas: personagem em 3ª pessoa (cápsula + FSM de animação), moedas colecionáveis (esfera-esfera), HUD em HTML, menu → jogo → game over, áudio e ao menos três efeitos de juice. Terminar e publicar vale mais que qualquer feature extra.
W·21Carreira, portfólio e roadmap
Onde estão as vagas, o que cada uma exige, como montar um portfólio que abre portas e um plano de estudos realista.
O mapa das vagas
| Cargo | O que faz | Capítulos-chave |
|---|---|---|
| Creative developer | Sites imersivos, campanhas, experiências premiadas (agências como as do circuito Awwwards/FWA e estúdios independentes) | 12–14, 13, 18 + animação (GSAP) e sensibilidade de design |
| Dev 3D de produto / e-commerce | Configuradores (carros, móveis, tênis), try-on, viewers de produto | 08–11, 15, 18 |
| Visualização de dados 3D / digital twins | Dashboards industriais, cidades, redes, científico (empresas de energia, logística, geo) | 05, 14, 15, 17 (+ d3/deck.gl como vizinhos) |
| Dev de jogos web / playable ads | Jogos HTML5, portais, ads jogáveis | 16, 20, 15 (tamanho de bundle é religião) |
| XR developer | Treinamento, arquitetura, ativações AR | 19, 15, 09 |
| Graphics/rendering engineer | Engines internas, ferramentas 3D (Figma-likes, CADs web, mapas) | Tudo, com peso em 01, 02, 12, 15, 17 — e frequentemente C++/WASM |
Como procurar: além de "Three.js" e "WebGL/WebGPU", busque por "creative developer", "R3F", "graphics engineer", "3D web". Boas fontes: quadros de vagas de comunidades de creative coding, o job board do Three.js Journey, empresas de produto 3D (configuradores, moda, imóveis) e estúdios cujo trabalho você admira — o nicho contrata muito por rede e por portfólio visto no X/LinkedIn.
O portfólio que funciona neste nicho
- O próprio site é a prova. Ele deve carregar rápido, rodar liso no celular e ter um momento 3D memorável — não dez;
- 3 a 5 peças fortes > 20 medianas. Combinação ideal: um configurador/produto (mercado), um experimento de shader/partículas autoral (técnica + arte), um projeto com modelo animado (pipeline), e um case de performance com números (engenharia);
- Cada peça com um mini-case escrito: problema, decisões, números (fps, MB, draw calls). É isso que o entrevistador técnico lê;
- Código público de pelo menos uma peça, limpo e organizado como o cap. 11;
- Publique o processo: GIFs de progresso no X/LinkedIn geram as conexões que geram convites. A comunidade de creative dev é pequena e ativa — participar é estratégia de carreira.
Roadmap de estudos sugerido
| Fase | Duração típica | Meta de saída |
|---|---|---|
| 1. Fundamentos (caps. 00–06) | 4–8 semanas | Cena autoral publicada: modelo, luz calibrada, sombras, órbita, responsiva |
| 2. Profissionalização (07–11) | 6–10 semanas | Configurador completo com glTF otimizado, loading UX e zero vazamentos |
| 3. Diferenciação (12–15) | 8–12 semanas | 2 experimentos de shader + 1 case de performance documentado |
| 4. Especialização (16–20) | contínuo | Escolha 1–2 trilhas (R3F, WebGPU, XR ou jogos) e vá fundo; primeiras candidaturas/freelas |
Recursos que a comunidade considera canônicos: a documentação e exemplos oficiais do Three.js (three.js.org), o curso Three.js Journey de Bruno Simon, The Book of Shaders, os artigos de Inigo Quilez, o Discord do Three.js e do pmndrs, e o Codrops (tutoriais de efeitos de sites reais). Para fundamentos acadêmicos: Fundamentals of Computer Graphics (Shirley) e as aulas abertas de CG da CMU (Keenan Crane).
Entrevistas: o que cai
- Conceitual: pipeline gráfico, dot/cross, matriz MVP, Euler×quaternion, sRGB×linear, o que é draw call, como funciona shadow mapping — tudo coberto nos caps. 01, 02, 06, 08;
- Prática ao vivo: a cena mínima de memória (cap. 04), diagnosticar uma cena lenta (cap. 15), implementar hover/click (cap. 10);
- Sistema: "como você arquitetaria um configurador com 200 SKUs?" — assets sob demanda + dispose (09, 11), instancing (14), estado desacoplado (18);
- Portfólio: espere destrinchar suas próprias peças em profundidade. Só apresente o que sabe defender linha a linha.
Combinações que aumentam valor imediatamente: 3D + animação de interface (GSAP/scroll) para agências; 3D + React sólido para produto; 3D + performance mobile comprovada para e-commerce; 3D + Blender básico (saber ajustar/exportar assets) para qualquer time pequeno. E freelance é porta de entrada real: projetos de configurador e landing 3D aparecem constantemente em plataformas e por indicação.
W·22Glossário rápido
| Albedo / base color | Cor própria da superfície, sem luz nem sombra embutidas |
| AABB | Caixa delimitadora alinhada aos eixos; colisão barata |
| Bake / assar | Pré-calcular (luz, AO, sombras) para textura, trocando custo de runtime por estático |
| BRDF | Função que descreve como uma superfície reflete luz |
| BVH | Hierarquia de volumes que acelera raycasting/colisões |
| Draw call | Uma ordem de desenho da CPU para a GPU; seu número domina o custo de CPU |
| Frustum | A "pirâmide de visão" da câmera; o que está fora é descartado (culling) |
| HDR / HDRI | Imagem com alcance de brilho além de 0–1; panorâmicas HDRI iluminam cenas (IBL) |
| LOD | Níveis de detalhe trocados por distância |
| Mipmap | Cadeia de versões reduzidas de uma textura para amostragem limpa ao longe |
| NDC | Coordenadas normalizadas de dispositivo (-1 a 1) após a projeção |
| Normal | Vetor perpendicular à superfície; a base de todo cálculo de luz |
| Overdraw | Pintar o mesmo pixel várias vezes por frame |
| PBR | Renderização fisicamente baseada (metalness/roughness) |
| Quaternion | Representação de rotação sem gimbal lock e com interpolação perfeita |
| Render target | Textura que serve de destino de renderização |
| Shader | Programa que roda na GPU por vértice (vertex) ou por pixel (fragment) |
| Skinning | Deformação de malha por esqueleto de bones |
| Tone mapping | Compressão do alcance HDR interno para a tela |
| TSL | Three.js Shading Language: shaders como nós em JS, compilados p/ WGSL ou GLSL |
| Uniform | Valor global passado do JS para o shader a cada frame |
| UV | Coordenada 0–1 que mapeia textura sobre a superfície |
| VRAM | Memória da GPU; texturas e geometrias vivem nela e exigem dispose() |
| WGSL | Linguagem de shaders do WebGPU |
| Z-buffer / depth | Textura de profundidade que resolve o que fica na frente |
| Z-fighting | Chuvisco entre superfícies coplanares por esgotamento de precisão de profundidade |