Referência de trabalho: three.js e WebXR
Consulta, não leitura corrida. Cada entrada responde a uma pergunta de execução — “como ponho um objeto na cena”, “o que é fov”, “por que o clique acerta o lugar errado” — com o menor código que roda, o significado de cada parâmetro e o que acontece quando ele é preenchido errado.
Quando esta página foi escrita, e onde procurar o que mudou
Escrita em 9 de setembro de 2026, por volta das 21 horas. As assinaturas, os valores padrão e os nomes desta página foram extraídos da versão da biblioteca instalada no projeto do professor naquele momento — a série r169, resolvida como 0.169.0 no arquivo de trava —, e os exemplos foram compilados contra ela. A interface de realidade estendida do navegador não tem versão de projeto: ou o aparelho a implementa, ou não.
Uma referência de biblioteca tem prazo de validade, e o desta é o próximo salto de versão. Quando algum nome aqui não existir mais, ou um parâmetro tiver mudado de ordem, a página errada é esta, e não o programa. As fontes abaixo dizem o que vale hoje.
| Onde | O que traz |
|---|---|
| threejs.org/docs | a referência oficial da biblioteca, sempre na versão mais recente. Cada classe tem a lista completa de propriedades e métodos |
| threejs.org/manual | o manual introdutório, com explicações longas e exemplos interativos |
| threejs.org/examples | centenas de demonstrações executáveis, cada uma com o código à mostra |
| as notas de versão da biblioteca | o que mudou em cada série. É onde se descobre o que quebrou entre a instalada aqui e a atual |
| a documentação da interface, na MDN | a referência da interface do navegador, com o estado de suporte de cada recurso |
| a especificação, no W3C | a definição normativa da interface, para quando a documentação não bastar |
| immersiveweb.dev | portal da comunidade do padrão, com guias e notícias de suporte |
| os exemplos oficiais do padrão | demonstrações executáveis no visor, uma por recurso |
Todos os oito foram abertos e conferidos no dia em que esta página foi escrita. Dois candidatos caíram na conferência e não estão aqui: a tradução em português da documentação da interface devolveu erro, e um site de tabela de compatibilidade recusou a consulta. Endereço que não responde não entra — nem com ressalva.
A documentação da biblioteca está em inglês; a da interface do navegador tem tradução parcial para o português, e a versão em inglês é a que se mantém atualizada.
Como conferir a versão que o seu projeto usa, em vez de confiar nesta página: o arquivo de dependências do projeto declara a faixa aceita, e o arquivo de trava registra a versão exata resolvida na sua máquina. Divergindo entre si, o arquivo de trava tem razão; divergindo do que está escrito aqui, quem está desatualizada é esta página.
Como esta referência está montada
Toda tabela de parâmetros desta página tem quatro colunas, e a quarta é a que costuma faltar nas referências de biblioteca.
| Coluna | O que traz |
|---|---|
| Parâmetro | o nome exato, como o código o escreve |
| O que é | o significado, com a unidade quando existe |
| Padrão | o valor assumido quando o parâmetro é omitido |
| Se errar | o sintoma concreto de um valor mal escolhido |
As assinaturas e os padrões vêm da versão instalada no projeto do professor, e não da documentação de uma versão qualquer. Todo exemplo de código foi compilado contra a mesma configuração estrita que o projeto usa: o que está escrito aqui compila.
Onde um parâmetro aceita ângulo, a unidade está dita — a biblioteca mistura os dois casos, e essa mistura é fonte constante de erro. Câmera e arestas recebem graus; rotação de objeto recebe radianos.
Onde procurar
| A pergunta | A entrada |
|---|---|
| o que é malha, geometria, material, vértice | o vocabulário mínimo |
para que lado é o +X, e em que unidade eu escrevo |
eixos e unidades |
| como faço aparecer alguma coisa na tela | a cena mínima |
o que é fov, near, far, aspect |
a câmera |
| o que cada opção do renderizador faz | o renderizador |
| onde ponho o código que roda a cada quadro | o laço |
| que formas existem prontas, e o que é “segmento” | as formas prontas |
o que é roughness, metalness, side, emissive |
os materiais |
| por que meu objeto está preto | as luzes |
| como movo, giro e redimensiono | mover, girar, redimensionar |
qual a diferença entre rotation e quaternion |
a rotação |
| como faço um objeto acompanhar outro | agrupar |
| como sei o que o usuário apontou ou clicou | saber o que a pessoa apontou |
| como carrego um modelo pronto | carregar um modelo pronto |
| como ponho imagem, foto ou texto numa superfície | as texturas |
| como toco a animação que veio no modelo | a animação do modelo |
| como entro em realidade virtual | entrar em realidade virtual |
| o que é espaço de referência, e qual escolher | o espaço de referência |
| como entro em realidade aumentada | entrar em realidade aumentada |
| como leio gatilho, aperto e posição dos controles | ler os controles |
| como movo a pessoa pelo ambiente | mover a pessoa |
| por que caiu para dezoito quadros por segundo | o orçamento de desempenho |
| está tudo preto, ou nada abre — e agora | quando não funciona |
| o que significa aquela palavra | o glossário |
O vocabulário mínimo
Sete palavras sustentam tudo o que vem depois. Elas aparecem nas assinaturas, nas mensagens de erro e na documentação da biblioteca, e quem não as separa lê o resto por adivinhação.
Vértice é um ponto no espaço, com três coordenadas. É a unidade de que tudo é feito.
Triângulo é o que se forma com três vértices, e é a única superfície que a placa de vídeo sabe desenhar. Um cubo aparentemente feito de seis quadrados é, por dentro, doze triângulos.
Geometria é o conjunto de vértices e triângulos que define a forma — e nada mais. Uma geometria não tem cor, não tem posição no mundo e não aparece na tela sozinha. CylinderGeometry é a receita de um cilindro, e não um cilindro na cena.
Material define como a luz se comporta ao encontrar a superfície: que cor devolve, quão áspera ou polida ela é, se é transparente. Também não aparece sozinho.
Malha, em inglês Mesh, é o casamento dos dois: uma geometria mais um material, agora sim um objeto que existe na cena, com posição, rotação e escala próprias. A mesma geometria e o mesmo material podem servir a muitas malhas ao mesmo tempo, e essa partilha é o principal recurso de economia (o orçamento de desempenho).
Normal é a direção para a qual um pedaço de superfície aponta. A biblioteca a calcula sozinha nas geometrias prontas, e é ela que decide o quanto de luz aquele pedaço recebe. Superfície com a normal invertida fica preta ou desaparece, e é isso que MeshNormalMaterial existe para revelar.
Coordenada de textura, chamada UV, é o par de números que diz qual ponto de uma imagem cai sobre qual ponto da superfície. Também vem pronta nas geometrias da biblioteca; passa a importar quando o modelo vem de fora e a imagem aparece esticada.
Cena é a raiz da árvore de objetos. Tudo o que deve ser desenhado precisa estar pendurado nela, direta ou indiretamente. Objeto criado e não acrescentado à cena não aparece — e não gera erro nenhum.
Eixos, unidades e sentido de giro
| Convenção | Valor | Consequência prática |
|---|---|---|
| Unidade | metro | 0.4 é quarenta centímetros; altura de olho de pessoa em pé fica em torno de 1.6 |
+X |
para a direita | — |
+Y |
para cima | chão é o plano y = 0; gravidade, quando houver, desce em -Y |
+Z |
para trás, em direção a quem olha | o objeto “à sua frente” tem z negativo |
| Frente de um objeto | -Z local |
lookAt aponta o -Z do objeto para o alvo |
| Sentido de giro | anti-horário, olhando do lado positivo do eixo para a origem | ângulo positivo em rotation.y gira para a esquerda de quem olha de cima |
Ângulo em rotation |
radiano | Math.PI é meia volta; use MathUtils.degToRad(90) para não espalhar constantes |
A unidade é a convenção mais cara de desrespeitar. O padrão WebXR entrega as posições do aparelho em metros, sempre, e não há como reconfigurá-lo. Uma cena escrita em centímetros funciona no desktop, onde a câmera é ajustada até parecer certa, e revela o problema no instante em que alguém entra no visor: a bancada tem o tamanho de um prédio, e a pessoa, o de um parafuso.
O -Z como frente surpreende, e é a fonte do modelo importado que aparece de costas. Não é escolha da biblioteca: vem da convenção de câmera adotada pela computação gráfica, em que a câmera olha ao longo do próprio -Z.
A cena mínima que roda
Três objetos e um laço. Todo o resto se acrescenta a isto.
import { PerspectiveCamera, Scene, WebGLRenderer } from 'three';
const cena = new Scene();
const camera = new PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
const renderer = new WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
camera.position.set(0, 1.6, 2);
camera.lookAt(0, 1, 0);
renderer.setAnimationLoop(() => {
renderer.render(cena, camera);
});| Peça | Papel |
|---|---|
Scene |
a raiz da árvore. Não recebe parâmetro nenhum |
PerspectiveCamera |
de onde se olha, e o quanto se enxerga (a câmera) |
WebGLRenderer |
quem desenha, e quem conversa com a placa de vídeo (o renderizador) |
renderer.domElement |
o elemento canvas que o renderizador criou; precisa entrar na página |
renderer.render(cena, camera) |
desenha um quadro, uma vez |
setAnimationLoop |
manda desenhar continuamente (o laço) |
Quatro coisas que faltam nesse exemplo e que produzem “não aparece nada”: não há luz (as luzes), não há objeto acrescentado à cena (pôr um objeto na cena), o canvas pode ter altura zero por falta de estilo, e a câmera pode estar dentro do objeto que deveria enxergar.
A câmera em perspectiva, parâmetro a parâmetro
new PerspectiveCamera(fov, aspect, near, far)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
fov |
campo de visão vertical, em graus: o ângulo de abertura entre a borda de cima e a de baixo do que a câmera enxerga | 50 |
valor alto (acima de 90) distorce as bordas e produz sensação de lente olho de peixe; valor baixo (abaixo de 20) achata a profundidade e parece um zoom exagerado |
aspect |
proporção largura ÷ altura da área de desenho | 1 |
proporção diferente da do canvas estica ou achata a imagem inteira; é o erro que aparece ao redimensionar a janela sem atualizar este campo |
near |
distância mínima, em metros, a partir da qual a câmera enxerga | 0.1 |
valor pequeno demais consome a precisão de profundidade e faz superfícies próximas piscarem uma sobre a outra; objeto mais perto que este valor some |
far |
distância máxima, em metros, até onde a câmera enxerga | 2000 |
valor muito maior que o necessário desperdiça precisão de profundidade; objeto além dele desaparece sem aviso |
O nome técnico do volume que esses quatro números delimitam é frustum: a pirâmide truncada que vai do plano near ao plano far, com abertura dada por fov e aspect. Só o que está dentro dela chega à tela.
A precisão de profundidade é uma razão, e não uma diferença. O que estraga o desenho é o quociente far / near, e não a distância entre os dois. Passar near de 0.1 para 0.01 piora dez vezes; aumentar far de 100 para 1000 piora dez vezes. Para uma cena de bancada, 0.05 e 50 são generosos.
import { PerspectiveCamera, WebGLRenderer } from 'three';
export function acompanharTamanho(
camera: PerspectiveCamera,
renderer: WebGLRenderer,
tela: HTMLCanvasElement,
): void {
const largura = tela.clientWidth;
const altura = Math.max(1, tela.clientHeight);
renderer.setSize(largura, altura, false);
camera.aspect = largura / altura;
camera.updateProjectionMatrix();
}updateProjectionMatrix é obrigatório depois de mexer em fov, aspect, near ou far. A câmera guarda esses valores numa matriz calculada uma vez; sem a chamada, o campo novo fica no objeto e não chega ao desenho — o sintoma é a imagem continuar esticada depois de um redimensionamento aparentemente tratado.
O terceiro argumento de setSize, aqui false, diz para não escrever largura e altura no estilo do canvas. Com true, a biblioteca fixa o tamanho em CSS e passa a brigar com o seu leiaute.
Dentro de uma sessão imersiva, esta câmera sai de cena: quem define posição, orientação e campo de visão é o aparelho, e a câmera em uso passa a ser a de renderer.xr.getCamera(). Mexer em fov durante a sessão não tem efeito, e é o que se espera — o campo de visão ali é uma característica física do visor.
Câmera ortográfica
new OrthographicCamera(left, right, top, bottom, near, far)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
left, right |
as bordas esquerda e direita do volume visível, em unidades de mundo — e não em pixels | -1, 1 |
os padrões -1 e 1 abrem uma janela de dois metros: numa cena de bancada, a peça inteira cabe num canto e parece minúscula |
top, bottom |
as bordas superior e inferior | 1, -1 |
largura e altura fora da proporção da área de desenho esticam a imagem — aqui não há aspect para corrigir isso, e a conta é sua |
near, far |
os mesmos limites de profundidade da câmera em perspectiva | 0.1, 2000 |
vale a mesma razão far / near; e como aqui não há perspectiva, objeto atrás do plano near some sem ficar maior antes |
Aqui não há fov, porque não há perspectiva: dois objetos do mesmo tamanho aparecem do mesmo tamanho, perto ou longe. Serve para planta baixa, minimapa, vista técnica e painel de interface medido em unidades de mundo. Não serve para o que se vê dentro do visor — a visão humana tem perspectiva, e uma cena ortográfica em estereoscopia não fecha.
O renderizador, opção por opção
new WebGLRenderer(parâmetros?)
| Opção | O que é | Padrão | Se errar |
|---|---|---|---|
canvas |
o elemento onde desenhar | a biblioteca cria um | omitido, o canvas novo nasce fora da página: sem appendChild, o programa roda inteiro e a tela fica em branco, sem erro |
antialias |
suaviza as bordas serrilhadas das formas | false |
desligado, toda aresta diagonal aparece escadeada — é o defeito visual mais notado, e o mais barato de corrigir |
alpha |
permite que o fundo do canvas seja transparente |
false |
é o que produz a tela preta em realidade aumentada: sem ele o fundo é opaco e cobre a imagem da câmera, com o programa perfeitamente funcional |
premultipliedAlpha |
como o canal de transparência se combina com a cor | true |
desligado sem que a textura esteja no mesmo convênio, bordas transparentes ganham um halo claro em volta |
stencil |
reserva o buffer de estêncil, usado em recorte e máscara | false |
ligado sem uso, gasta memória de vídeo por quadro; desligado com recorte em uso, a máscara simplesmente não recorta |
depth |
reserva o buffer de profundidade, que decide o que fica na frente | true |
desligado, os objetos se desenham na ordem de inserção e o que está atrás aparece na frente |
preserveDrawingBuffer |
mantém o desenho após o quadro, permitindo capturá-lo | false |
desligado, toDataURL no canvas devolve imagem em branco; ligado sem necessidade, custa uma cópia do quadro inteiro |
powerPreference |
pede ao navegador a placa dedicada ou a econômica | 'default' |
em portátil com duas placas, o padrão costuma cair na integrada — e a queda de quadros aparece só no aparelho de quem usa |
failIfMajorPerformanceCaveat |
recusa criar o contexto se só houver renderização por software | false |
desligado, a página abre e roda a dois quadros por segundo em vez de avisar que aquela máquina não serve |
| Ajuste depois de criado | O que faz |
|---|---|
setSize(l, a, atualizarEstilo?) |
define a resolução de desenho |
setPixelRatio(n) |
quantos pixels reais por pixel de CSS; limite a 2 (o orçamento de desempenho) |
setClearColor(cor, alfa?) |
a cor com que o quadro começa. Em realidade aumentada, deixe o alfa em 0 |
outputColorSpace |
espaço de cor da saída; o padrão já é o adequado para tela |
shadowMap.enabled |
liga o cálculo de sombras (as luzes) |
xr.enabled |
liga o modo de realidade estendida (entrar em realidade virtual) |
setAnimationLoop(f) |
instala o laço (o laço) |
dispose() |
libera os recursos do renderizador |
Nunca crie mais de um renderizador por página. Cada um abre um contexto de vídeo próprio, e o navegador impõe um limite pequeno de contextos simultâneos: o sintoma é a cena mais antiga apagar sozinha quando a nova nasce.
O laço e a passagem do tempo
Use renderer.setAnimationLoop, e não requestAnimationFrame da janela. Dentro de uma sessão imersiva, quem decide a hora do próximo quadro é o aparelho, com o ritmo dele; o relógio da janela para de servir e a cena congela no visor enquanto continua andando na tela.
import { Clock, PerspectiveCamera, Scene, WebGLRenderer } from 'three';
export function iniciarLaco(
renderer: WebGLRenderer,
cena: Scene,
camera: PerspectiveCamera,
aCadaQuadro: (delta: number, quadro: XRFrame | undefined) => void,
): void {
const relogio = new Clock();
renderer.setAnimationLoop((_instante: number, quadro?: XRFrame) => {
aCadaQuadro(relogio.getDelta(), quadro);
renderer.render(cena, camera);
});
}| Peça | O que é |
|---|---|
| primeiro argumento | o instante do quadro, em milissegundos desde o início da página |
| segundo argumento | o quadro da sessão, presente só dentro de realidade virtual ou aumentada; é dele que saem pose e teste de contato |
new Clock(autoStart?) |
relógio auxiliar; autoStart (padrão true) o dispara na primeira leitura |
relogio.getDelta() |
segundos decorridos desde a leitura anterior |
relogio.getElapsedTime() |
segundos desde o início |
setAnimationLoop(null) |
desliga o laço |
Multiplique todo movimento pelo delta. Um giro escrito como objeto.rotation.y += 0.01 anda ao dobro da velocidade num aparelho de 120 quadros por segundo em relação a um de 60. Escrito como objeto.rotation.y += 0.6 * delta, anda a 0,6 radiano por segundo em qualquer um. A regra vale para deslocamento, giro, escala e contagem regressiva.
Chame getDelta uma vez por quadro, e só uma. Cada chamada zera o cronômetro interno; duas chamadas no mesmo quadro fazem a segunda devolver quase zero, e o sintoma é um movimento que anda pela metade sem razão aparente.
Pôr um objeto na cena
Sempre os mesmos três passos: uma geometria, um material, e uma malha que junta os dois.
import { CylinderGeometry, Mesh, MeshStandardMaterial, Scene } from 'three';
export function acrescentarEixo(cena: Scene): Mesh {
const forma = new CylinderGeometry(0.02, 0.02, 0.4, 24);
const acabamento = new MeshStandardMaterial({ color: 0x8899aa, roughness: 0.4 });
const eixo = new Mesh(forma, acabamento);
eixo.name = 'eixo';
eixo.position.set(0, 0.95, 0);
cena.add(eixo);
return eixo;
}new Mesh(geometria, material) — ambos opcionais, e omiti-los produz uma malha com forma e material padrão que serve para nada além de teste. O terceiro passo, cena.add, é o que costuma faltar: sem ele o objeto existe na memória, não gera erro e não aparece.
Uma malha aceita um material ou um arranjo de materiais. O arranjo só faz sentido em geometrias que declaram grupos — BoxGeometry declara seis, um por face —, e é assim que se faz um dado com seis números diferentes com uma só malha.
As formas prontas, parâmetro a parâmetro
Antes das tabelas, três palavras que se repetem em todas elas.
Segmento é em quantos pedaços a superfície é dividida para ser aproximada por triângulos. Um círculo com 8 segmentos é um octógono; com 64, parece redondo. Segmento é orçamento, não qualidade: dobrar o número dobra os triângulos, e em visor a conta se paga duas vezes, uma por olho.
Theta e phi são os dois ângulos com que se recorta uma forma de revolução. Nas geometrias abaixo, thetaStart diz onde o recorte começa e thetaLength diz quanto ele varre — ambos em radianos. Um thetaLength de Math.PI dá meia forma, e é assim que se faz uma cúpula, uma calha ou um corte de demonstração sem modelador.
Aberto ou tampado (openEnded) decide se as extremidades de cilindro e cone recebem tampa. Aberto, o objeto fica oco e mostra o interior — que só será visível se o material desenhar as duas faces (os materiais).
Caixa
new BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
width |
comprimento das arestas paralelas ao eixo X, em metros |
1 |
escala errada é quase sempre unidade trocada, não erro de código |
height |
comprimento das arestas paralelas ao eixo Y, em metros |
1 |
a caixa nasce centrada na origem, e metade desta altura fica abaixo do ponto onde ela foi posicionada: é o que produz o objeto meio enterrado no chão |
depth |
comprimento das arestas paralelas ao eixo Z, em metros |
1 |
omitido junto com os outros dois, sai um cubo de um metro — grande demais para bancada, e o reflexo é culpar a câmera |
widthSegments |
em quantas faces a largura se divide | 1 |
acima de 1 só ajuda se a caixa for deformada ou receber luz por vértice |
heightSegments |
em quantas faces a altura se divide | 1 |
subir os três segmentos multiplica os triângulos entre si: 10 × 10 × 10 dá seiscentas faces onde bastavam doze |
depthSegments |
em quantas faces a profundidade se divide | 1 |
mesmo custo multiplicativo; numa caixa que não vai ser deformada, qualquer valor acima de 1 é gasto sem efeito visível |
A caixa nasce centrada na origem: uma caixa de 0,4 de altura vai de -0,2 a +0,2 em Y. Para apoiá-la no chão, position.y recebe metade da altura. Isto vale para quase todas as geometrias, e é a causa do objeto “meio enterrado” no piso.
Esfera
new SphereGeometry(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
radius |
raio, em metros | 1 |
o padrão 1 dá uma esfera de dois metros de diâmetro; numa cena de bancada isso enche a sala, e o reflexo é procurar o defeito na câmera |
widthSegments |
segmentos horizontais, ao redor do equador. Mínimo 3 |
32 |
abaixo de 12 a silhueta fica visivelmente poligonal |
heightSegments |
segmentos verticais, do polo ao polo. Mínimo 2 |
16 |
valores baixos aparecem como faixas horizontais no sombreado, mesmo com widthSegments alto: os dois precisam subir juntos |
phiStart |
ângulo horizontal inicial, em radianos | 0 |
girar aqui move junto a costura da textura; para só orientar a esfera, gire a malha, não a geometria |
phiLength |
quanto varre na horizontal | 2π (volta inteira) |
π produz meia esfera cortada na vertical |
thetaStart |
ângulo vertical inicial, em radianos | 0 (polo norte) |
conta do polo norte para baixo, e não do equador: π/2 começa na linha do equador, não no topo |
thetaLength |
quanto varre na vertical | π (polo a polo) |
π/2 produz uma cúpula |
O padrão 32 × 16 já é generoso para uma peça pequena. Uma esfera de dois centímetros com 64 × 32 gasta oito vezes mais triângulos para uma diferença que ninguém enxerga a um metro.
Cilindro
new CylinderGeometry(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
radiusTop |
raio no topo, em metros | 1 |
0 aqui produz um cone, sem erro nem aviso — é a confusão mais comum quando se queria apenas um tubo fino |
radiusBottom |
raio na base | 1 |
valores diferentes produzem um tronco de cone; 0 no topo produz um cone |
height |
altura, em metros | 1 |
o cilindro nasce centrado na origem e em pé, com o eixo em Y: metade da altura fica abaixo do ponto onde ele foi posicionado |
radialSegments |
segmentos ao redor da circunferência | 32 |
abaixo de 8 o cilindro vira prisma visível |
heightSegments |
anéis de faces ao longo da altura | 1 |
maior que 1 só serve para deformar o cilindro depois |
openEnded |
true deixa as extremidades sem tampa |
false |
aberto e com material de face única, o interior fica invisível |
thetaStart |
ângulo inicial da varredura, em radianos | 0 (posição de três horas) |
o zero fica em +X, não em +Z: uma calha que deveria abrir para a frente abre para o lado |
thetaLength |
ângulo total varrido, em radianos | 2π |
π produz meio cilindro — uma calha |
O cilindro nasce em pé, com o eixo em Y. Para deitá-lo ao longo de Z, gire -π/2 em X. Esta é a geometria de eixo, pino, parafuso, tubo e disco: um disco é um cilindro de altura pequena.
Cone
new ConeGeometry(radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
radius |
raio da base, em metros | 1 |
o padrão 1 dá um cone de dois metros de largura; num marcador de direção de bancada, o valor de trabalho é centésimo disso |
height |
altura, em metros | 1 |
a ponta nasce em +Y, e a altura conta a partir do centro: o cone fica meio enterrado se position.y não somar height / 2 |
radialSegments |
segmentos ao redor da base | 32 |
abaixo de 8 a base vira polígono visível; acima de 48, num marcador pequeno, gasta triângulo em silhueta que ninguém enxerga |
heightSegments |
anéis ao longo da altura | 1 |
maior que 1 só serve para deformar o cone depois; sem deformação, cada anel a mais é custo puro |
openEnded |
true deixa a base sem tampa |
false |
aberto e com material de face única, o interior fica invisível e o cone parece oco por dentro |
thetaStart |
ângulo inicial da varredura, em radianos | 0 (posição de três horas) |
girar aqui em vez de girar a malha desalinha a costura do cone em relação ao resto da peça |
thetaLength |
ângulo total varrido, em radianos | 2π |
menor que a volta produz uma fatia — e a face do corte só aparece com material de face dupla |
É o cilindro com radiusTop igual a zero, escrito de forma mais curta. Serve de seta, ponteiro e marcador de direção — com a ponta em +Y, e portanto girado para apontar aonde se quer.
Cápsula
new CapsuleGeometry(radius, length, capSegments, radialSegments)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
radius |
raio do corpo e das calotas, em metros | 1 |
entra duas vezes na altura total, uma por calota; contar só o length produz a cápsula um raio mais alta do que se pretendia de cada lado |
length |
comprimento apenas do trecho reto, sem as calotas | 1 |
pedir length igual à altura desejada dá uma cápsula alta demais — a conta é length + 2 × radius |
capSegments |
segmentos de curva de cada calota | 4 |
o padrão 4 deixa a calota visivelmente facetada de perto, que é justamente a distância de uso quando ela aproxima um dedo ou uma mão |
radialSegments |
segmentos ao redor da circunferência | 8 |
8 é baixo para uma cápsula vista de perto: a silhueta sai octogonal. Para volume de colisão, que não se desenha, 8 sobra |
A altura total é length + 2 × radius, e esquecer as calotas nessa conta é o que produz a cápsula maior do que o pretendido. É a forma usada como volume aproximado de braço, dedo e corpo em teste de contato.
Plano
new PlaneGeometry(width, height, widthSegments, heightSegments)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
width |
largura, ao longo de X, em metros |
1 |
um chão de 1 × 1 é um tapete de um metro: a pessoa dá dois passos e sai dele, e o efeito no visor é cair no vazio |
height |
altura, ao longo de Y, em metros |
1 |
o plano nasce em pé; usado como chão sem o giro de -π/2 em X, ele vira uma parede a um metro do rosto |
widthSegments |
divisões na largura | 1 |
deixado em 1, o plano tem quatro vértices: qualquer deformação em terreno ou pano não tem onde acontecer, e a superfície continua reta |
heightSegments |
divisões na altura | 1 |
um plano de 1 × 1 segmento tem quatro vértices: mapa de deslocamento aplicado sobre ele não faz absolutamente nada |
O plano nasce em pé, no plano XY, encarando +Z. Para usá-lo como chão, gire -π/2 em X. E como ele tem uma face só, visto por trás desaparece: um chão que some quando a pessoa se abaixa é um plano com material de face única (os materiais).
Círculo
new CircleGeometry(radius, segments, thetaStart, thetaLength)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
radius |
raio, em metros | 1 |
o círculo nasce em pé, no plano XY, como o plano — e some visto por trás com material de face única |
segments |
número de triângulos que aproximam o disco. Mínimo 3 |
32 |
abaixo de 12 o disco vira polígono à vista; com 3 sai um triângulo, e o resultado não é erro nem aviso |
thetaStart |
ângulo inicial, em radianos | 0 (posição de três horas) |
um indicador de progresso que deveria começar no topo começa à direita: o zero fica em +X, não em +Y |
thetaLength |
ângulo varrido, em radianos | 2π |
menor que a volta produz uma fatia de pizza — que é como se desenha progresso plano dentro da cena, e é o uso mais comum deste parâmetro |
Com thetaLength menor que a volta inteira, sai uma fatia de pizza — que é como se desenha um indicador de progresso plano dentro da cena.
Anel
new RingGeometry(innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
innerRadius |
raio do furo | 0.5 |
maior que o externo produz geometria degenerada, sem erro |
outerRadius |
raio externo, em metros | 1 |
a espessura visível do anel é a diferença entre os dois raios; próximos demais, a mira some a um metro de distância |
thetaSegments |
segmentos ao redor; mais deixa o anel mais redondo. Mínimo 3 |
32 |
abaixo de 16 a mira de superfície aparece como polígono, e num anel fino a falha é mais notada que numa esfera |
phiSegments |
anéis concêntricos entre os dois raios. Mínimo 1 |
1 |
subir aqui não deixa o anel mais redondo — quem faz isso é thetaSegments; só acrescenta triângulos |
thetaStart |
ângulo inicial, em radianos | 0 (posição de três horas) |
um arco de progresso que deveria abrir no topo abre à direita |
thetaLength |
ângulo varrido, em radianos | 2π |
menor que a volta produz um arco |
É a mira de superfície de quase todo exemplo de realidade aumentada: um anel plano, girado -π/2 em X para deitar no chão, posicionado onde o teste de contato encontrou superfície (entrar em realidade aumentada).
Toro e nó de toro
new TorusGeometry(radius, tube, radialSegments, tubularSegments, arc)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
radius |
do centro do toro ao centro do tubo, em metros | 1 |
o diâmetro externo é 2 × (radius + tube), e não 2 × radius: pedir o raio do furo aqui dá um toro maior do que o pretendido |
tube |
raio do próprio tubo, em metros | 0.4 |
igual ou maior que radius fecha o furo e a superfície se autointersecta — sai uma forma dobrada sobre si, sem erro |
radialSegments |
segmentos na seção do tubo | 12 |
o padrão 12 já mostra facetas num tubo grosso visto de perto |
tubularSegments |
segmentos ao longo do anel | 48 |
é o que decide se o anel parece redondo; abaixo de 24 a volta fica poligonal |
arc |
ângulo total do anel, em radianos | 2π |
menor que a volta produz um arco — e as duas extremidades ficam abertas, invisíveis com material de face única |
new TorusKnotGeometry(radius, tube, tubularSegments, radialSegments, p, q) acrescenta p (quantas voltas o traçado dá em torno do eixo de simetria, padrão 2) e q (quantas voltas dá em torno do círculo interno, padrão 3). É a peça com que se demonstra a biblioteca: milhares de triângulos, forma reconhecível, nenhuma utilidade estrutural.
Arestas
new EdgesGeometry(geometry, thresholdAngle)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
geometry |
a geometria de origem, de onde as arestas serão extraídas | null |
ela continua sendo usada pela malha original; descartá-la depois de extrair as arestas apaga as duas |
thresholdAngle |
em graus: uma aresta só é desenhada se o ângulo entre as normais das duas faces vizinhas passar deste valor | 1 |
valor baixo desenha toda subdivisão interna e produz uma bola de linhas; valor alto some com as arestas curvas |
Esta geometria não tem superfície: ela só produz as linhas, e precisa de LineSegments com um material de linha para aparecer. É como se mostra a estrutura de uma peça sem escondê-la atrás do próprio volume.
thresholdAngle de 15 graus é o valor de trabalho: mostra os cantos reais de uma peça e ignora a subdivisão de superfícies curvas.
O que toda geometria oferece
| Membro | O que faz |
|---|---|
parameters |
os valores com que ela foi criada, só de leitura. Mudá-los depois não altera a forma |
translate(x, y, z) |
desloca os vértices, mudando onde fica a origem do objeto |
rotateX/Y/Z(rad) |
gira os vértices de vez; é como se deita um plano na criação, sem tocar na malha |
scale(x, y, z) |
escala os vértices de vez |
center() |
recentra a geometria na origem |
computeBoundingBox() |
calcula a caixa envolvente, exposta em boundingBox |
computeVertexNormals() |
recalcula normais; necessário depois de mexer nos vértices à mão |
dispose() |
libera a memória de vídeo (o orçamento de desempenho) |
A diferença entre geometria.translate(0, 1, 0) e malha.position.y = 1 importa: o primeiro move os vértices e muda o ponto em torno do qual o objeto gira; o segundo move o objeto inteiro e preserva o centro de giro. Uma tampa que gira em torno do próprio centro, quando deveria girar na dobradiça, é este caso.
Materiais
Todos recebem um objeto de opções no construtor, e todas as opções são também propriedades que se pode mudar depois.
| Material | O que é | Quando usar |
|---|---|---|
MeshStandardMaterial |
modelo fisicamente plausível, com aspereza e metalicidade | o padrão de trabalho |
MeshBasicMaterial |
ignora a luz por completo: mostra a cor pura | mira, contorno, marcador, painel — o que precisa ter sempre a mesma cor |
MeshPhysicalMaterial |
o padrão mais verniz, transmissão, espessura e iridescência | vidro e verniz; é o mais caro de todos |
MeshNormalMaterial |
pinta cada ponto pela direção da normal | depuração: revela face invertida e normal errada |
LineBasicMaterial |
cor de linhas | arestas e traçados |
As opções comuns a todos
| Opção | O que é | Padrão | Se errar |
|---|---|---|---|
color |
cor base, como 0x8899aa ou '#8899aa' |
branco | cor escura com pouca luz vira preto indistinguível |
opacity |
opacidade, de 0 (invisível) a 1 |
1 |
sem transparent: true não tem efeito nenhum |
transparent |
liga o tratamento de transparência | false |
ligado sem necessidade, ordena o objeto na fila de transparentes e pode fazê-lo desenhar por cima do que deveria estar na frente |
alphaTest |
descarta o pixel cuja opacidade for menor que este valor | 0 |
em 0 nada é descartado, e uma folhagem recortada precisa de transparent, que a põe na fila dos transparentes e faz o recorte se desenhar por cima do que estava na frente |
side |
quais faces desenhar: FrontSide, BackSide, DoubleSide |
FrontSide |
plano visto por trás desaparece; DoubleSide custa o dobro de preenchimento |
visible |
desenha ou não | true |
esconder por aqui não desativa o teste de raio: o objeto invisível continua sendo clicado, e o clique parece acertar o nada |
depthTest |
respeita quem está na frente | true |
desligado, o objeto atravessa tudo — o que é desejável em mira e em interface |
depthWrite |
grava a própria profundidade | true |
desligar ajuda em vidro e fumaça sobrepostos |
wireframe |
desenha só as arestas dos triângulos | false |
ferramenta de inspeção, não de acabamento |
toneMapped |
participa do mapeamento de tons do renderizador | true |
ligado, a cor que chega à tela não é a que foi escrita: um painel de interface em 0xff0000 sai alaranjado e desbotado |
As opções do material padrão
| Opção | O que é | Padrão | Se errar |
|---|---|---|---|
roughness |
aspereza da superfície, de 0 (espelho) a 1 (fosco) |
1 |
0 sem mapa de ambiente devolve um preto espelhado sem nada para refletir |
metalness |
o quanto a superfície é metal — fisicamente é 0 ou 1, sem meio-termo |
0 |
metal (1) sem ambiente ao redor fica preto; é o erro mais comum com este material |
emissive |
cor emitida pela própria superfície, independentemente de luz | 0x000000, nenhuma |
no preto, o objeto depende inteiramente da luz da cena; e mesmo aceso ele não ilumina o que está em volta — quem esperava um abajur vê um objeto brilhante numa sala escura |
emissiveIntensity |
multiplicador da emissão | 1 |
mexer aqui com emissive no preto não faz nada: qualquer intensidade vezes preto é preto |
map |
textura de cor (as texturas) | null |
textura em espaço de cor errado deixa tudo lavado ou escuro demais |
normalMap |
textura que finge relevo alterando as normais | null |
relevo aparente sem custo de triângulo — e tratado como cor em vez de dado, o relevo sai torto |
normalScale |
intensidade do relevo fingido, em Vector2 |
(1, 1) |
valor negativo inverte o sentido do relevo: a saliência vira reentrância, e a iluminação fica sutilmente errada |
roughnessMap, metalnessMap |
mapas que variam aspereza e metal ponto a ponto | null |
são multiplicados pelos valores escalares acima; com metalness: 0, o mapa de metal não tem efeito nenhum |
aoMap |
oclusão ambiente pré-calculada | null |
exige o segundo conjunto de coordenadas de textura; sem ele, não aparece |
displacementMap |
desloca vértices de verdade | null |
exige geometria subdividida; num plano de 1 × 1 segmento não faz nada |
envMap |
ambiente refletido | null |
é o que falta quando metal fica preto |
flatShading |
desliga a suavização entre faces | false |
ligado sem intenção, a esfera aparece facetada mesmo com muitos segmentos, e o reflexo é procurar o defeito na geometria |
Metal preto é o sintoma mais frequente deste material. Com metalness: 1, a superfície não tem cor própria: ela devolve o que há em volta. Sem envMap e sem ambiente, não há o que devolver, e o resultado correto é preto. As saídas são baixar metalness, fornecer um mapa de ambiente, ou usar o material básico.
LineBasicMaterial aceita ainda linewidth, e é honesto saber que na maioria das plataformas ele é ignorado: a espessura fica em um pixel, por limitação do WebGL, e não da biblioteca. Linha grossa se faz com geometria de tubo, ou com os utilitários de linha dos complementos.
Material se compartilha: um só material para os cinquenta parafusos iguais, e não cinquenta materiais idênticos. Cada material distinto obriga o renderizador a trocar de estado, e a troca é justamente o que custa caro.
Luzes
Objeto com MeshStandardMaterial numa cena sem luz é um objeto preto. Esta é a causa mais comum de “programa funciona, tela escura” — e nada no console acusa.
| Luz | Assinatura | O que faz |
|---|---|---|
AmbientLight |
(color, intensity) |
acrescenta a mesma quantidade de luz a tudo, de todas as direções. Não produz volume nem sombra |
HemisphereLight |
(skyColor, groundColor, intensity) |
luz de cima com a cor do céu e de baixo com a cor do chão; dá ambiente sem achatar |
DirectionalLight |
(color, intensity) |
raios paralelos, como o sol. A direção é o que conta |
PointLight |
(color, intensity, distance, decay) |
lâmpada: irradia igualmente para todos os lados a partir de um ponto |
SpotLight |
(color, intensity, distance, angle, penumbra, decay) |
holofote: um cone de luz a partir de um ponto |
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
color |
cor da luz, em hexadecimal | 0xffffff |
luz colorida multiplica a cor do material; azul sobre amarelo dá quase preto |
intensity |
força da luz | 1 |
com o mapeamento de tons ligado, valores entre 1 e 3 são a faixa usual |
skyColor / groundColor |
cores de cima e de baixo, na hemisférica | 0xffffff |
iguais, a luz achata e vira ambiente puro; a diferença entre as duas é o que dá volume sem custo de sombra |
distance |
alcance máximo, em metros; 0 significa sem limite |
0 |
valor pequeno demais apaga a luz antes de ela chegar à cena |
decay |
com que rapidez a luz enfraquece com a distância | 2 |
2 é o valor fisicamente correto; menor achata a queda |
angle |
abertura do cone do holofote, em radianos, no máximo π/2 |
π/3 |
acima do máximo, o cone deixa de fazer sentido |
penumbra |
quanto da borda do cone é suavizada, de 0 a 1 |
0 |
0 produz uma borda dura e artificial |
import { DirectionalLight, HemisphereLight, Scene } from 'three';
export function iluminar(cena: Scene): DirectionalLight {
const ambiente = new HemisphereLight(0xdfe7ff, 0x35302b, 1.1);
const principal = new DirectionalLight(0xffffff, 2.2);
principal.position.set(1.5, 3, 1);
principal.target.position.set(0, 0.9, 0);
cena.add(ambiente, principal, principal.target);
return principal;
}A luz direcional não tem posição no sentido usual. O que conta é a direção que vai da posição dela até o alvo (target), e mudar (1.5, 3, 1) para (15, 30, 10) não altera nada, porque a direção é a mesma. E o alvo, quando movido, precisa estar na cena — sem isso, a matriz dele não é atualizada e a luz continua apontando para a origem.
Sombras se ligam à parte, e cobram caro:
| Passo | O que fazer |
|---|---|
| no renderizador | renderer.shadowMap.enabled = true |
| na luz | luz.castShadow = true |
| em quem projeta | objeto.castShadow = true |
| em quem recebe | objeto.receiveShadow = true |
| na qualidade | luz.shadow.mapSize.set(1024, 1024) |
| no enquadramento | ajustar luz.shadow.camera para cobrir só a área útil |
Cada luz com sombra desenha a cena de novo, do ponto de vista dela, a cada quadro. Em visor, com dois olhos, uma única luz com sombra pode custar mais que toda a cena. Comece sem sombras e ligue-as quando a cena estiver fechada.
Mover, girar, redimensionar
Todo objeto de cena — malha, grupo, linha, luz, câmera — herda de Object3D e carrega os mesmos campos.
| Campo | Tipo | O que é |
|---|---|---|
position |
Vector3 |
deslocamento em relação ao pai, em metros |
rotation |
Euler |
giro em radianos, aplicado na ordem XYZ por padrão |
quaternion |
Quaternion |
o mesmo giro, em forma que se compõe sem trava de eixo |
scale |
Vector3 |
fator multiplicativo; 1 é o tamanho original |
up |
Vector3 |
qual direção é “para cima” nas contas de lookAt; padrão (0, 1, 0) |
matrix |
Matrix4 |
a transformação local, calculada a partir dos quatro primeiros |
matrixWorld |
Matrix4 |
a transformação acumulada com a de todos os pais |
rotation e quaternion são duas vistas do mesmo giro: mexer num atualiza o outro. Não há como dessincronizá-los, e não existe escolher “usar um dos dois” — existe escolher por qual dos dois se escreve.
Vector3, o tipo mais usado de todos
new Vector3(x, y, z) — os três padrão 0.
| Método | O que faz | Cuidado |
|---|---|---|
set(x, y, z) |
escreve os três de uma vez | — |
copy(outro) |
copia os valores de outro vetor | não guarda referência; é cópia |
clone() |
devolve um vetor novo com os mesmos valores | aloca — evite dentro do laço |
add, sub, multiply |
operações componente a componente | modificam o próprio vetor, e não devolvem um novo |
addScaledVector(v, k) |
soma v multiplicado por k |
é a forma de aplicar velocidade vezes delta sem alocar |
length() |
comprimento | usa raiz quadrada |
lengthSq() |
comprimento ao quadrado | prefira ao comparar distâncias: dispensa a raiz |
normalize() |
reduz a comprimento 1, preservando a direção |
vetor de comprimento zero vira (0,0,0), não erro |
distanceTo(v) |
distância até outro ponto | — |
dot(v) |
produto escalar | com vetores normalizados, é o cosseno do ângulo entre eles |
cross(v) |
produto vetorial | dá a perpendicular aos dois |
applyQuaternion(q) |
gira o vetor | — |
applyMatrix4(m) |
aplica uma transformação completa | — |
setFromMatrixPosition(m) |
extrai só a posição de uma matriz | é como se lê onde um objeto está no mundo |
Quase todo método muda o objeto em que é chamado. a.add(b) altera a; quem espera um terceiro vetor de volta escreve a.clone().add(b). Confundir os dois é a origem do objeto que deriva alguns centímetros por quadro até sumir.
Rotação: quando rotation, quando quaternion
import { MathUtils, Object3D, Quaternion, Vector3 } from 'three';
export function girarDeVez(peca: Object3D): void {
peca.rotation.set(0, MathUtils.degToRad(30), 0);
}
export function acumularGiro(peca: Object3D, eixo: Vector3, graus: number): void {
const passo = new Quaternion().setFromAxisAngle(eixo.normalize(), MathUtils.degToRad(graus));
peca.quaternion.multiply(passo);
}| Situação | O que usar |
|---|---|
| orientação declarada uma vez, legível | rotation, com MathUtils.degToRad |
| giro que se acumula a cada quadro | quaternion.multiply |
| interpolar entre duas orientações | quaternion.slerp(destino, t) |
| copiar a orientação de outro objeto | quaternion.copy(outro.quaternion) |
| apontar para um ponto | lookAt(ponto), que recebe coordenada de mundo |
O problema que o quaternião resolve tem nome: trava de eixo. Ao compor giros em rotation, chega um ponto em que dois dos três eixos passam a girar em torno da mesma direção, e um grau de liberdade se perde — o sintoma é o objeto travar num plano ou dar um salto brusco no meio de um movimento contínuo.
MathUtils.degToRad(g) e MathUtils.radToDeg(r) existem para que ninguém espalhe Math.PI / 180 pelo código. MathUtils.clamp(v, min, max) e MathUtils.lerp(a, b, t) completam o que se usa no dia a dia.
Escala
scale.setScalar(0.5) reduz à metade nos três eixos. Escala não uniforme — scale.set(2, 1, 1) — deforma as normais e estraga a iluminação; quando for inevitável, prefira gerar a geometria já com as medidas certas.
Escala 0 em qualquer eixo achata o objeto e produz matriz não inversível: o objeto some, o teste de raio deixa de acertá-lo e nenhum erro é emitido. Para esconder, use visible = false.
Agrupar: o que é local e o que é do mundo
Group é um objeto sem forma própria que existe para carregar outros. Mover o grupo move tudo dentro dele, e cada filho mantém a posição relativa ao pai.
import { Group, Mesh, Object3D, Scene, Vector3 } from 'three';
export function montarConjunto(cena: Scene, corpo: Mesh, tampa: Mesh): Group {
const conjunto = new Group();
conjunto.name = 'conjunto';
conjunto.add(corpo, tampa);
tampa.position.set(0, 0.12, 0);
conjunto.position.set(0, 0.9, -0.3);
cena.add(conjunto);
return conjunto;
}
export function ondeEstaNoMundo(alvo: Object3D): Vector3 {
return alvo.getWorldPosition(new Vector3());
}| Operação | O que faz | Cuidado |
|---|---|---|
pai.add(a, b, …) |
anexa um ou vários | um objeto tem um pai; add o remove do anterior |
pai.remove(a) |
desanexa, sem destruir | não libera memória de vídeo (o orçamento de desempenho) |
pai.attach(a) |
anexa preservando a posição no mundo | é o que se usa ao pegar uma peça com a mão |
pai.clear() |
remove todos os filhos | — |
obj.traverse(fn) |
percorre o objeto e todos os descendentes | inclui o próprio objeto |
obj.traverseVisible(fn) |
percorre só o que está visível | — |
obj.getObjectByName(n) |
procura por name na subárvore |
devolve undefined se não achar |
obj.getWorldPosition(v) |
escreve no vetor a posição no mundo | precisa de um Vector3 de destino |
obj.getWorldQuaternion(q) |
escreve no quaternião a orientação acumulada no mundo | precisa de um Quaternion de destino |
obj.localToWorld(v) / worldToLocal(v) |
converte um ponto entre os dois sistemas | modifica o vetor recebido |
obj.visible |
esconde o objeto e os filhos | não desativa o teste de raio, que continua acertando |
obj.userData |
espaço livre para dados seus | sobrevive à clonagem |
A diferença entre add e attach é a que mais custa a aparecer. add preserva as coordenadas locais, e o objeto salta para outro ponto do mundo. attach recalcula as locais para que ele permaneça exatamente onde está. Pegar um objeto com o controle é attach; montá-lo num conjunto pela origem é add.
Matrizes se atualizam antes do desenho, não na hora da escrita. Quem lê matrixWorld ou getWorldPosition logo depois de mudar position, ainda no mesmo quadro, lê o valor anterior. A saída é chamar obj.updateMatrixWorld(true) antes da leitura.
Saber o que a pessoa apontou
Raycaster lança um raio e devolve o que ele atravessa, em ordem de distância.
new Raycaster(origin, direction, near, far)
| Parâmetro | O que é | Padrão | Se errar |
|---|---|---|---|
origin |
ponto de onde o raio parte, em coordenada de mundo | (0,0,0) |
passar coordenada local de um objeto que tem pai faz o raio sair de outro lugar da cena, e o teste não acerta nada |
direction |
direção do raio; precisa estar normalizada | (0,0,-1) |
direção não normalizada produz distâncias erradas nos resultados |
near |
ignora o que estiver mais perto que isto | 0 |
não aceita negativo |
far |
ignora o que estiver mais longe | infinito | limitar acelera o teste em cena grande |
import { Camera, Object3D, Raycaster, Vector2, type Intersection } from 'three';
const raio = new Raycaster();
const ponto = new Vector2();
export function sobOCursor(
evento: PointerEvent,
tela: HTMLCanvasElement,
camera: Camera,
candidatos: Object3D[],
): Intersection | undefined {
const caixa = tela.getBoundingClientRect();
ponto.x = ((evento.clientX - caixa.left) / caixa.width) * 2 - 1;
ponto.y = -((evento.clientY - caixa.top) / caixa.height) * 2 + 1;
raio.setFromCamera(ponto, camera);
return raio.intersectObjects(candidatos, true)[0];
}| Chamada | O que faz |
|---|---|
setFromCamera(ponto, camera) |
monta o raio que sai da câmera e passa pelo ponto da tela |
intersectObject(obj, recursivo?) |
testa um objeto |
intersectObjects(lista, recursivo?) |
testa vários, e devolve tudo ordenado por distância |
ray.origin / ray.direction |
o raio em si, para montá-lo à mão (é o caso do controle XR) |
As coordenadas de setFromCamera vão de -1 a 1, com o Y invertido em relação ao da página: -1 é a borda esquerda ou inferior, +1 a direita ou superior. O sinal de menos na segunda linha é essa inversão, e trocá-lo produz um clique que acerta o objeto espelhado na vertical.
O segundo argumento precisa ser true quase sempre. Um modelo carregado é um grupo com dezenas de malhas dentro; com false, o teste examina só o grupo, que não tem geometria, e não encontra nada.
Cada resultado traz:
| Campo | O que é |
|---|---|
object |
a malha atingida — não o grupo, e sim a folha da árvore |
distance |
distância da origem do raio até o ponto de contato, em metros |
point |
o ponto de contato, em coordenada de mundo |
face |
o triângulo atingido, com a normal dele |
uv |
a coordenada de textura no ponto, quando existe |
Dentro de sessão imersiva não há cursor, e o raio parte do controle:
import { Matrix4, Object3D, Raycaster } from 'three';
const temporaria = new Matrix4();
export function apontarComOControle(controle: Object3D, raio: Raycaster): void {
temporaria.identity().extractRotation(controle.matrixWorld);
raio.ray.origin.setFromMatrixPosition(controle.matrixWorld);
raio.ray.direction.set(0, 0, -1).applyMatrix4(temporaria);
}O -Z do controle é a direção para a frente, por convenção do padrão. Reaproveitar uma única Matrix4 declarada fora da função, como acima, evita alocar um objeto por quadro por controle — lixo suficiente para produzir engasgo perceptível em visor.
Carregar um modelo pronto
O formato é o glTF, de preferência na variante binária .glb, que traz malha, material, textura e animação num arquivo só.
import { Box3, Scene, Vector3 } from 'three';
import { GLTFLoader, type GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';
export async function carregar(url: string, cena: Scene): Promise<GLTF> {
const gltf: GLTF = await new GLTFLoader().loadAsync(url);
const caixa = new Box3().setFromObject(gltf.scene);
const tamanho = caixa.getSize(new Vector3());
if (tamanho.y > 2) {
gltf.scene.scale.setScalar(1 / tamanho.y);
}
cena.add(gltf.scene);
return gltf;
}| Membro | O que é |
|---|---|
loadAsync(url) |
devolve promessa; é a forma curta |
load(url, aoCarregar, aoProgredir, aoFalhar) |
com funções de retorno; use quando quiser barra de progresso |
gltf.scene |
o grupo a acrescentar à cena |
gltf.animations |
os clipes de animação (a animação do modelo) |
gltf.cameras |
câmeras que vieram no arquivo, se houver |
| Ferramenta de medida | O que faz |
|---|---|
new Box3().setFromObject(obj) |
a caixa que envolve o objeto, já com todas as transformações internas |
caixa.getSize(v) |
escreve as três dimensões no vetor |
caixa.getCenter(v) |
escreve o centro |
caixa.containsPoint(p) |
se um ponto está dentro |
caixa.intersectsBox(outra) |
se duas caixas se tocam — é o teste de colisão barato |
Medir antes de confiar. Modelo exportado em centímetros chega cem vezes maior; modelo com a origem no pé ou no centro muda onde ele encosta no chão. As duas coisas se descobrem com Box3, em três linhas, antes de alguém entrar no visor.
Onde pôr o arquivo. Com Vite, o que está na pasta pública é servido a partir da raiz do endereço, e o caminho no código é '/modelo.glb'. Arquivo referenciado por caminho relativo do código-fonte não chega ao pacote de produção — funciona em desenvolvimento e some depois da compilação.
Imagens, fotos e rótulos
import { CanvasTexture, MeshStandardMaterial, RepeatWrapping, TextureLoader } from 'three';
export function comFoto(url: string): MeshStandardMaterial {
const textura = new TextureLoader().load(url);
textura.wrapS = RepeatWrapping;
textura.wrapT = RepeatWrapping;
textura.repeat.set(2, 2);
textura.anisotropy = 4;
return new MeshStandardMaterial({ map: textura });
}
export function comRotulo(texto: string): MeshStandardMaterial {
const tela = document.createElement('canvas');
tela.width = 512;
tela.height = 128;
const pincel = tela.getContext('2d');
if (pincel !== null) {
pincel.fillStyle = '#101418';
pincel.fillRect(0, 0, tela.width, tela.height);
pincel.fillStyle = '#e8eef5';
pincel.font = '48px sans-serif';
pincel.fillText(texto, 24, 80);
}
return new MeshStandardMaterial({ map: new CanvasTexture(tela) });
}| Propriedade | O que é | Cuidado |
|---|---|---|
wrapS / wrapT |
o que fazer fora do intervalo 0..1, na horizontal e na vertical |
para repeat funcionar, precisa ser RepeatWrapping |
repeat |
quantas vezes a imagem se repete em cada direção | — |
offset |
deslocamento da imagem sobre a superfície | é como se anima um painel de rolagem sem mexer na geometria |
center / rotation |
ponto e ângulo de giro da imagem | ângulo em radianos |
anisotropy |
nitidez em superfícies vistas de raspão | valores acima do máximo da placa são reduzidos em silêncio |
colorSpace |
espaço de cor da imagem | textura de cor pede espaço de cor de tela; mapa de dados, como o de normais, não |
flipY |
inverte a imagem na vertical ao carregar | o glTF já vem no sentido certo; inverter aqui estraga |
needsUpdate |
avisa que o conteúdo mudou | obrigatório depois de redesenhar o canvas de origem |
dispose() |
libera a memória de vídeo | — |
Espaço de cor errado é o defeito silencioso das texturas. Uma foto tratada como dado linear aparece lavada; um mapa de normais tratado como cor produz relevo torto. A regra curta: o que representa cor recebe espaço de cor de tela; o que representa número — normais, aspereza, oclusão — não.
CanvasTexture é o caminho curto para texto dentro da cena: desenha-se no canvas com a interface de duas dimensões e usa-se o resultado como textura. É legível de perto e borra ao aproximar demais — para painel de interface dentro do ambiente, resolve o caso comum sem carregar biblioteca de tipografia tridimensional.
Tocar a animação que veio no modelo
import { AnimationMixer, LoopRepeat } from 'three';
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';
export function tocarPrimeira(gltf: GLTF): ((delta: number) => void) | undefined {
const primeira = gltf.animations[0];
if (primeira === undefined) {
return undefined;
}
const misturador = new AnimationMixer(gltf.scene);
const acao = misturador.clipAction(primeira);
acao.setLoop(LoopRepeat, Infinity);
acao.timeScale = 1;
acao.play();
return (delta: number) => misturador.update(delta);
}| Peça | O que é |
|---|---|
new AnimationMixer(raiz) |
quem aplica as animações à árvore de objetos daquela raiz |
clipAction(clipe) |
cria ou recupera a ação que toca aquele clipe |
acao.play() / stop() |
inicia e para |
acao.setLoop(modo, vezes) |
LoopOnce, LoopRepeat ou LoopPingPong |
acao.timeScale |
velocidade; 2 toca ao dobro, -1 toca de trás para a frente |
acao.fadeIn(s) / fadeOut(s) |
entra e sai suavemente, em segundos |
acao.crossFadeTo(outra, s) |
troca de animação sem salto |
acao.clampWhenFinished |
mantém a última pose ao terminar, em vez de voltar à primeira |
misturador.update(delta) |
avança o tempo; sem isto, nada anda |
O misturador não anda sozinho. Animação parada com o programa rodando é, quase sempre, o update(delta) esquecido — e o delta precisa vir do mesmo relógio do laço, em segundos.
As duas camadas, e por que a distinção economiza horas
| Camada | O que é | Como aparece |
|---|---|---|
| WebXR Device API | padrão do navegador. Não se instala e não tem versão de projeto do professor: ou o navegador a implementa, ou não | navigator.xr, e os tipos com prefixo XR |
| three.js | biblioteca instalada pelo projeto, substituível. Desenha a cena e conversa com a camada de baixo | Scene, Mesh, WebGLRenderer, e o subobjeto renderer.xr |
Quando o aparelho recusa alguma coisa, quem recusou foi a camada de baixo, e trocar de biblioteca não muda a resposta. Quando a cena aparece torta, escura ou vazia, quem desenhou foi a de cima. Boa parte do tempo perdido em depuração vai embora ao decidir, primeiro, de qual das duas é o problema.
Duas condições valem para tudo o que envolve a camada de baixo, e as duas falham sem mensagem de erro:
| Condição | Como se verifica | O que acontece se faltar |
|---|---|---|
| contexto seguro | window.isSecureContext |
o navegador não expõe a interface. A causa é o endereço servido, não o aparelho |
| interface presente | navigator.xr é undefined ou não |
não há o que perguntar. Ausência de resposta não é resposta negativa |
Servir por HTTPS é requisito da própria interface. Em rede local, o caminho usual é um certificado de desenvolvimento aceito no aparelho, ou um túnel que forneça endereço seguro.
Perguntar o que o aparelho suporta
export type Suporte = 'sim' | 'nao' | 'desconhecido';
export async function suporteDe(modo: XRSessionMode): Promise<Suporte> {
const xr: XRSystem | undefined = navigator.xr;
if (xr === undefined) {
return 'desconhecido';
}
try {
return (await xr.isSessionSupported(modo)) ? 'sim' : 'nao';
} catch {
return 'desconhecido';
}
}| Modo | O que é |
|---|---|
'inline' |
a cena na própria página, sem visor, sem sessão imersiva |
'immersive-vr' |
sessão imersiva que substitui o mundo |
'immersive-ar' |
sessão imersiva que sobrepõe ao mundo visível |
São três respostas, e não duas. Alguns navegadores rejeitam a promessa em vez de devolver falso — para um modo que não reconhecem, ou fora de contexto seguro. Tratar a rejeição como negativa transforma “não sei responder” em “este aparelho não serve”, e é assim que um relatório honesto vira um relatório confiante e errado.
Entrar em realidade virtual
O caminho curto, com o botão pronto dos complementos:
import { WebGLRenderer } from 'three';
import { VRButton } from 'three/examples/jsm/webxr/VRButton.js';
export function habilitarVr(renderer: WebGLRenderer): void {
renderer.xr.enabled = true;
renderer.xr.setReferenceSpaceType('local-floor');
document.body.appendChild(VRButton.createButton(renderer));
}VRButton.createButton(renderer, sessionInit?) devolve um elemento pronto que pede a sessão, mostra o motivo quando o aparelho não suporta e encerra ao sair. Enquanto o projeto está sendo montado, ele poupa a máquina de estados inteira.
Membro de renderer.xr |
O que faz |
|---|---|
enabled |
liga o modo XR do renderizador; sem isto, nada acontece |
isPresenting |
se está desenhando para o visor ou para a janela |
getSession() |
a sessão em curso, ou nulo |
setSession(s) |
entrega uma sessão aberta à mão |
getReferenceSpace() |
o espaço em vigor |
setReferenceSpace(e) |
troca o espaço — é como se faz locomoção (mover a pessoa) |
setReferenceSpaceType(t) |
declara o tipo desejado antes de a sessão abrir |
getCamera() |
a câmera que o aparelho está usando durante a sessão |
getController(i) |
nó de cena no raio do controle de índice i |
getControllerGrip(i) |
nó de cena no punho do mesmo controle |
getHand(i) |
nó de cena da mão rastreada, quando o recurso existe |
setFoveation(n) |
reduz a resolução na periferia, de 0 a 1, para ganhar desempenho |
A ordem importa. setReferenceSpaceType precisa vir antes de a sessão ser entregue: a biblioteca pede o espaço ao produzir o primeiro quadro. Invertidas as duas linhas, a cena é registrada contra o espaço errado e o console não acusa nada.
Abrindo a sessão à mão, sem o botão:
export async function abrirSessao(
modo: XRSessionMode,
opcionais: readonly string[],
): Promise<XRSession> {
const xr: XRSystem | undefined = navigator.xr;
if (xr === undefined) {
throw new Error('Este navegador não expõe a interface de realidade estendida.');
}
return xr.requestSession(modo, { optionalFeatures: [...opcionais] });
}Campo de requestSession |
O que é | Consequência |
|---|---|---|
requiredFeatures |
recursos obrigatórios | um item indisponível impede a sessão de abrir |
optionalFeatures |
recursos desejáveis | um item indisponível vira ausência observável, e a sessão abre |
domOverlay |
o elemento de página a sobrepor à cena | usado com o recurso dom-overlay |
Membro de XRSession |
O que é |
|---|---|
enabledFeatures |
o que a sessão de fato concedeu. É opcional no padrão: pode vir indefinido, e aí o estado é indeterminado, não negado |
requestReferenceSpace(tipo) |
pede um espaço; rejeita quando o aparelho não o oferece |
requestAnimationFrame(f) |
agenda o próximo quadro da sessão |
requestHitTestSource(opções) |
cria a fonte de teste de contato (entrar em realidade aumentada) |
inputSources |
as fontes de entrada ativas |
visibilityState |
'visible', 'visible-blurred' ou 'hidden' — o aparelho pode suspender sem encerrar |
end() |
encerra por decisão do programa |
evento 'end' |
avisa que a sessão terminou, inclusive quando quem terminou foi o usuário |
A escolha entre obrigatório e opcional é assimétrica. Pedir demais como obrigatório troca uma ausência que se poderia relatar por uma sessão que não abre — e o usuário só vê um botão que não faz nada.
Espaço de referência: onde fica o zero do mundo
É a decisão mais cara de mudar depois, porque toda coordenada escrita na cena depende dela.
| Espaço | Onde fica a origem | Quando usar |
|---|---|---|
viewer |
na cabeça de quem observa | último recurso; garantido pelo padrão em toda sessão |
local |
num ponto arbitrário, fixo desde o início da sessão | sentado, sem referência de piso |
local-floor |
no chão do espaço físico, sob quem observa | em pé, com o chão importando — o caso comum |
bounded-floor |
no chão, mais os limites da área livre que o aparelho conhece | quando se quer respeitar a área demarcada |
unbounded |
sem fronteira declarada | percursos longos, em que o erro acumulado precisa ser corrigido pelo aparelho |
Com local-floor, um objeto em y = 0 fica no chão da sala, e a altura escrita no código é altura de verdade. Com local, o mesmo objeto nasce na altura em que a cabeça estava quando a sessão abriu — e a bancada aparece na altura do rosto.
const PREFERIDOS: readonly XRReferenceSpaceType[] = ['local-floor', 'local', 'viewer'];
export async function escolherEspaco(sessao: XRSession): Promise<XRReferenceSpaceType> {
for (const tipo of PREFERIDOS) {
try {
await sessao.requestReferenceSpace(tipo);
return tipo;
} catch {
continue;
}
}
throw new Error('A sessão não concedeu espaço de referência algum.');
}A rejeição é a resposta, e não a falha. Pedir um espaço que o aparelho não oferece é como se pergunta a ele o que oferece. A lista termina em viewer porque o padrão garante esse; assim o laço sempre termina com resposta.
Entrar em realidade aumentada
Duas diferenças de fundo em relação à realidade virtual: o fundo precisa ser ausência de pintura, para a câmera aparecer atrás, e a cena precisa descobrir onde estão as superfícies reais.
import { Mesh, MeshBasicMaterial, RingGeometry, WebGLRenderer } from 'three';
import { ARButton } from 'three/examples/jsm/webxr/ARButton.js';
export function habilitarAr(renderer: WebGLRenderer, painel: HTMLElement): Mesh {
renderer.xr.enabled = true;
document.body.appendChild(
ARButton.createButton(renderer, {
requiredFeatures: ['hit-test'],
optionalFeatures: ['dom-overlay', 'anchors'],
domOverlay: { root: painel },
}),
);
const mira = new Mesh(
new RingGeometry(0.06, 0.08, 32).rotateX(-Math.PI / 2),
new MeshBasicMaterial({ color: 0x66ffcc }),
);
mira.visible = false;
return mira;
}Crie o renderizador com alpha: true e não pinte fundo opaco: cor de limpeza desenhada por cima da imagem da câmera é o que produz a tela preta em modo aumentado, com o programa perfeitamente funcional.
| Recurso | O que habilita | Garantido? |
|---|---|---|
hit-test |
lançar um raio contra as superfícies reais que o aparelho encontrou | não |
anchors |
prender um objeto a um ponto do mapa e deixar o aparelho corrigi-lo | não |
plane-detection |
receber os planos que o aparelho reconheceu | não |
dom-overlay |
uma camada de página por cima da cena, para botões e texto | não |
hand-tracking |
pose das mãos, sem controle | não |
light-estimation |
estimativa da luz do ambiente real | não |
Nenhum é garantido em aparelho nenhum — daí a assimetria entre obrigatório e opcional valer especialmente aqui.
O teste de contato precisa do quadro da sessão, e por isso mora dentro do laço:
import type { Object3D, WebGLRenderer } from 'three';
export function seguirSuperficie(
renderer: WebGLRenderer,
quadro: XRFrame | undefined,
fonte: XRHitTestSource | undefined,
mira: Object3D,
): void {
const espaco = renderer.xr.getReferenceSpace();
if (quadro === undefined || fonte === undefined || espaco === null) {
return;
}
const primeiro = quadro.getHitTestResults(fonte)[0];
if (primeiro === undefined) {
mira.visible = false;
return;
}
const pose = primeiro.getPose(espaco);
if (pose !== undefined) {
mira.visible = true;
mira.matrix.fromArray(pose.transform.matrix);
mira.matrix.decompose(mira.position, mira.quaternion, mira.scale);
}
}| Peça | O que é |
|---|---|
requestHitTestSource({ space }) |
pede a fonte uma vez, ao abrir a sessão, contra o espaço do observador |
frame.getHitTestResults(fonte) |
lê os contatos a cada quadro; lista vazia é normal |
resultado.getPose(espaco) |
posição e orientação do contato no espaço dado; pode não vir |
pose.transform.matrix |
a transformação como arranjo de dezesseis números |
matrix.decompose(p, q, s) |
separa a matriz em posição, orientação e escala do objeto |
O par fromArray mais decompose é a ponte entre as duas camadas: o padrão entrega matriz, a biblioteca trabalha com posição e quaternião.
Ler os controles
import type { WebGLRenderer, XRTargetRaySpace } from 'three';
export function ligarControle(renderer: WebGLRenderer, indice: number): XRTargetRaySpace {
const controle: XRTargetRaySpace = renderer.xr.getController(indice);
controle.addEventListener('selectstart', () => {
controle.userData['acionado'] = true;
});
controle.addEventListener('selectend', () => {
controle.userData['acionado'] = false;
});
controle.addEventListener('connected', () => {
controle.visible = true;
});
controle.addEventListener('disconnected', () => {
controle.visible = false;
});
return controle;
}| Evento | Quando dispara |
|---|---|
selectstart / selectend |
gatilho principal, ao acionar e ao soltar |
select |
acionamento completo; em aparelho sem controle, equivale ao toque na tela |
squeezestart / squeezeend |
aperto lateral, o mesmo par |
squeeze |
aperto completo |
connected / disconnected |
a fonte entrou ou saiu durante a sessão |
| Nó | Onde fica | Para que serve |
|---|---|---|
getController(i) |
no raio de apontamento | pendurar a linha de mira e montar o raio |
getControllerGrip(i) |
no punho | pendurar o modelo do controle |
getHand(i) |
na mão rastreada | articulações, quando o recurso existe |
Fontes de entrada aparecem e desaparecem com a sessão aberta: o controle que dormiu, a mão que saiu de campo, o segundo controle que só foi ligado depois. Código que as enumera uma vez, na abertura, funciona na demonstração e falha na mesa de quem usa — os eventos connected e disconnected existem por isso.
Para desenhar o controle na mão da pessoa, XRControllerModelFactory monta o modelo que o próprio aparelho informa, e o resultado entra no nó de punho.
Mover a pessoa pelo ambiente
Não se move a câmera dentro de uma sessão: quem a controla é o pescoço de quem está lá. O que se move é o mundo, deslocando a origem do espaço de referência.
import type { WebGLRenderer } from 'three';
export function saltar(renderer: WebGLRenderer, dx: number, dz: number): boolean {
const base = renderer.xr.getReferenceSpace();
if (base === null) {
return false;
}
const deslocado = base.getOffsetReferenceSpace(
new XRRigidTransform({ x: -dx, y: 0, z: -dz }),
);
renderer.xr.setReferenceSpace(deslocado);
return true;
}| Peça | O que é |
|---|---|
XRRigidTransform({x,y,z}, {x,y,z,w}?) |
um deslocamento com orientação opcional, em quaternião |
getOffsetReferenceSpace(t) |
deriva um espaço novo, deslocado em relação ao atual |
renderer.xr.setReferenceSpace(e) |
passa a usar o espaço derivado |
O sinal é a parte que se erra. A transformação recebe onde fica a origem do espaço novo dentro do atual; as poses lidas no espaço novo saem, portanto, com o deslocamento invertido. Para a pessoa avançar dois metros, a origem nova recai dois metros atrás.
Fazer assim, em vez de mover a câmera, é o que mantém o registro entre o que o pescoço faz e o que a cena mostra — e é a diferença entre um deslocamento confortável e um enjoo em trinta segundos. Deslocamento instantâneo, em saltos, enjoa menos que deslizamento contínuo.
Orçamento de desempenho
Em visor, cada quadro é desenhado duas vezes, uma por olho, e o alvo é 72 quadros por segundo ou mais. O que passa despercebido no desktop aparece imediatamente ali.
| Medida | O que fazer | Por quê |
|---|---|---|
| densidade de pixels | setPixelRatio(Math.min(devicePixelRatio, 2)) |
sem teto, tela densa desenha quatro vezes mais pixels do que se enxerga |
| repetição de objetos | cem parafusos iguais são um InstancedMesh, não cem malhas |
cada malha é uma ordem de desenho separada |
| material compartilhado | um material para muitos objetos | cada material distinto força troca de estado |
| segmentos de geometria | reduzir onde a silhueta não denuncia | triângulo desenhado duas vezes, uma por olho |
| sombras | desligadas até que se prove necessário | cada luz com sombra redesenha a cena inteira |
| alocação por quadro | nada de new Vector3() dentro do laço |
o coletor de lixo produz engasgo periódico |
| descarte | dispose() ao remover em definitivo |
remover da cena não libera memória de vídeo |
| foveação | renderer.xr.setFoveation(0.5) |
reduz resolução na periferia, onde a visão não distingue |
import { BoxGeometry, InstancedMesh, Matrix4, MeshStandardMaterial } from 'three';
export function campoDePecas(quantas: number): InstancedMesh {
const conjunto = new InstancedMesh(
new BoxGeometry(0.04, 0.04, 0.04),
new MeshStandardMaterial({ color: 0xb0b6bd }),
quantas,
);
const molde = new Matrix4();
for (let i = 0; i < quantas; i += 1) {
molde.setPosition((i % 10) * 0.06, 0.9, Math.floor(i / 10) * 0.06);
conjunto.setMatrixAt(i, molde);
}
conjunto.instanceMatrix.needsUpdate = true;
return conjunto;
}new InstancedMesh(geometria, material, count) — o count é o número máximo de cópias, reservado de uma vez; para exibir menos, ajuste conjunto.count sem recriar o objeto. Depois de escrever as matrizes, instanceMatrix.needsUpdate = true é obrigatório, ou todas as cópias ficam empilhadas na origem.
dispose é o que libera memória de vídeo, e a ausência dele é o vazamento que faz o ambiente ficar lento depois de vinte minutos, sem sintoma antes disso. Descarte a geometria, o material e cada textura do material — e só quando ninguém mais os usa, já que são compartilháveis.
Quando não funciona
| Sintoma | Causa provável | Onde confirmar |
|---|---|---|
| tela preta, sem erro no console | falta luz, ou material padrão sem luz alguma | as luzes |
| objeto preto, com luz na cena | metalness: 1 sem mapa de ambiente |
o material padrão |
| não aparece nada, e o objeto existe | faltou cena.add, ou está atrás da câmera, ou fora de near/far |
a câmera |
| a imagem estica ao redimensionar | aspect desatualizado, ou falta updateProjectionMatrix |
a câmera |
| superfícies piscam uma sobre a outra | razão far / near grande demais |
a câmera |
| o botão de entrar não aparece | página fora de HTTPS, ou navegador sem navigator.xr |
as duas camadas |
| em realidade aumentada, só se vê preto | falta alpha: true, ou há cor de limpeza opaca |
entrar em realidade aumentada |
| a cena inteira parece gigante | modelo em centímetros, ou cena fora de metros | eixos e unidades |
| a bancada nasce na altura do rosto | espaço local onde se queria local-floor |
o espaço de referência |
| o objeto está meio enterrado no chão | geometria centrada na origem; falta somar metade da altura | a caixa |
| o chão some quando me abaixo | plano com face única, visto por trás | o plano |
| o clique acerta o objeto errado | sinal do Y trocado, ou intersectObjects sem descer aos filhos |
saber o que a pessoa apontou |
| o clique não acerta nada em modelo carregado | segundo argumento false |
saber o que a pessoa apontou |
| o objeto salta ao ser pego | add onde o caso pedia attach |
agrupar |
| leio a posição e vem o valor antigo | matriz de mundo ainda não atualizada no quadro | agrupar |
| a animação do modelo não anda | falta mixer.update(delta) |
a animação do modelo |
| o movimento corre mais rápido num aparelho | movimento sem multiplicar por delta |
o laço |
| o movimento anda pela metade | getDelta() chamado duas vezes no mesmo quadro |
o laço |
| a textura repetida não repete | falta wrapS/wrapT em modo de repetição |
as texturas |
| o rótulo não atualiza | falta needsUpdate na textura de canvas |
as texturas |
| todas as cópias na origem | falta instanceMatrix.needsUpdate |
o orçamento de desempenho |
| lento depois de vinte minutos | falta de dispose no que foi removido |
o orçamento de desempenho |
| a cena antiga apagou sozinha | mais de um renderizador na mesma página | o renderizador |
Diante de qualquer sintoma, a primeira pergunta continua sendo a de as duas camadas: o problema está na camada que desenha ou na que fala com o aparelho? Erro de luz, escala e hierarquia é da de cima. Erro de sessão, espaço e recurso é da de baixo, e nenhuma mudança na cena o resolve.
Glossário
| Termo | O que é |
|---|---|
| anisotropia | ajuste de nitidez de textura em superfície vista de raspão |
| aspereza | o quanto uma superfície espalha a luz, em vez de refleti-la como espelho |
| buffer de profundidade | memória que guarda a distância de cada pixel, e decide o que fica na frente |
| caixa envolvente | a menor caixa alinhada aos eixos que contém um objeto |
| espaço de referência | o sistema de coordenadas contra o qual as poses do aparelho são relatadas |
| frustum | o volume em pirâmide truncada que a câmera enxerga |
| geometria | conjunto de vértices e triângulos que define uma forma |
| glTF | formato de troca de modelos tridimensionais; .glb é a variante binária |
| imersão | propriedade do sistema: o quanto ele substitui os estímulos do mundo |
| malha | geometria mais material, como objeto de cena |
| mapa de normais | textura que finge relevo alterando a direção das normais |
| material | descrição de como a luz se comporta na superfície |
| normal | direção para a qual um pedaço de superfície aponta |
| oclusão ambiente | escurecimento em cantos e frestas, onde a luz do ambiente chega menos |
| pose | posição mais orientação de algo, num dado espaço de referência |
| presença | resposta da pessoa: a sensação de estar ali |
| quaternião | forma de representar rotação sem trava de eixo |
| registro | alinhamento entre o virtual e o real, quando o real está visível |
| teste de contato | consulta que descobre onde um raio encontra superfície real |
| trava de eixo | perda de um grau de liberdade ao compor giros por ângulos de Euler |
| vértice | ponto no espaço, unidade de que a geometria é feita |