8.1 Visão Geral do Projeto
Esta é uma aplicação de Realidade Aumentada WebXR para o Meta Quest 3 que combina:
- AR passthrough — a sala real é visível através das câmaras dos auscultadores
- Interação hit-test — um cursor virtual que se encaixa em superfícies do mundo real
- Deteção de objetos por IA — TensorFlow.js reconhece objetos no feed da câmara e exibe etiquetas no espaço 3D
- Compreensão de Cena — A deteção de planos WebXR visualiza superfícies detetadas (chão, paredes, mesa)
URL em direto: http://92.113.18.92/
Em fila indiana:
index.html

8.2 Arquitetura do Projeto
📄 index.html (tudo-em-um)
│
├── HTML → canvas + botão UI + elemento de vídeo oculto
├── CSS → fundo transparente, interface sobreposta
└── JavaScript
├── Three.js → motor de renderização 3D (cena, câmara, materiais)
├── WebXR API → sessão AR, hit-test, deteção de plano
└── TF.js → Inferência de IA (modelo coco-ssd)
Porquê Three.js pura (sem A-Frame)?
Durante o desenvolvimento, descobriu-se que o A-Frame tem o seu próprio loop interno de renderização que entra em conflito com uma sessão WebXR explícita de ar imersiva. O A-Frame inicia uma sessão de immersive VR (opaca, sem passthrough) em vez de immersive-ar. Mudar para Three.js puro deu-nos controlo direto tanto sobre o tipo de sessão como sobre o loop de renderização.
8.3 Stack de Tecnologia
|
Tecnologia |
Versão |
Função |
|
Three.js |
0.157.0 |
Renderização 3D, geometria, materiais |
|
WebXR Device API |
Nativo do navegador |
Sessão AR, teste de acerto, deteção de avião |
|
TensorFlow.js |
4.15.0 |
Motor de inferência ML no navegador |
|
COCO-SSD |
2.2.3 |
Modelo de deteção de objetos pré-treinado |
|
getUserMedia API |
Nativo do navegador |
Stream de câmara para análise TF.js |
8.4 Fluxo de Aplicação
Carregamento da página
→ modelo TF.js (coco-ssd) carrega de forma assíncrona (~6MB)
→ Verificar: navigator.xr.isSessionSupported('immersive-ar')
→ botão "Enter AR" torna-se ativo
O utilizador clica em "Enter AR"
→ Pedido: navigator.xr.requestSession('immersive-ar', {...})
→ Quest ativa o Passthrough (câmara visível através de auscultadores)
→ Simultaneamente: getUserMedia() → stream de câmara para TF.js
→ renderer.setAnimationLoop() inicia (ciclo de renderização XR)
Cada frame (~72fps no Quest 3)
o scene.background = null (garante transparência)
o Teste de acerto → atualiza a posição do cursor
o A deteção de planos → atualiza a visualização da superfície
o A cada 2 segundos → runDetection() → inferência de IA
o renderer.render(cena, câmara)
8.5 Explicação detalhada do código
1. HTML Estrutura (linhas 1–78)
Um elemento oculto <vídeo> que recebe o fluxo getUserMedia(). TensorFlow.js usa-o como entrada para inferência — nunca é apresentado ao utilizador, apenas analisado.

Uma camada de interface sobreposta a flutuar acima da Three.js tela. Eventos de ponteiro: nenhum no contentor mas eventos de ponteiro: todos apenas no botão — para que a sobreposição não bloqueie interações 3D.
2. Three.js Inicialização (linhas 84–109)
alpha: true cria uma tela WebGL com um canal alfa transparente — isto é um pré-requisito para passthrough. xr.enabled = true ativa a integração integrada com WebXR da Three.js.
A câmara é adicionada à cena. Isto é importante porque os objetos ligados à câmara (entidades filhas) precisam de estar na hierarquia da cena.
A geometria do anel é rodada -90° no eixo X, ficando horizontalmente plana nas superfícies detetadas. Sem esta rotação, ficaria na vertical.
3. makeLabel() — Texto Sprite (linhas 113–138)
TRÊS. Sprite é um objeto que está sempre virado para a câmara (outdoors) — ideal para rótulos no espaço 3D. É desenhado numa tela HTML, convertido numa CanvasTexture e aplicado a um SpriteMaterial.
depthTest: false garante que a etiqueta é sempre visível mesmo que esteja geometricamente "atrás" de outro objeto 3D.
4. detecçãoTo3D() — Projeção 2D → 3D (linhas 140–163)
Este é o núcleo matemático da integração IA-AR.
Exemplo: Se a IA detetar uma cadeira no terço esquerdo do vídeo, nx ≈ -0,17. Isto converte-se num ângulo negativo (à esquerda do eixo do olhar). A etiqueta é colocada 1,8m à frente da câmara nessa direção.
5. makeBox3D() e bbox2dSize3D() — Caixas Delimitadoras 3D (linhas 165–182)
EdgesGeometry + LineSegments renderiza apenas as arestas de uma caixa — o efeito de uma borda fina em wireframe sem superfície preenchida.
A fórmula totalW é a projeção de perspetiva padrão: à distância d, a largura visível depende do FOV. A partir disto, deduzimos quantos metros correspondem aos píxeis da caixa delimitadora.
6. runDetection() — Pipeline de Inferência de IA (linhas 205–247)
cocoModel.detect(tfVideo) aceita diretamente um elemento <vídeo> — TF.js lê internamente dados de píxeis do quadro de vídeo atual.
Formato Pred.bbox : [X, Y, Largura, Altura] em pixels.
Cada deteção cria um par JS (sprite, caixa) que dura 4 segundos.
7. Sessão WebXR — Detalhes Chave (linhas 274–285)
Porquê a realidade aumentada imersiva e não a realidade virtual imersiva?
- immersive-VR = fundo preto opaco (experiência de capacete VR)
- immersive-ar = câmara passthrough + conteúdo virtual sobreposto
o espaço de referência do piso local fixa o sistema de coordenadas ao piso da sala — y=0 está ao nível do piso.
O teste de acerto e a deteção de aviões são opcionais porque não são suportados em todos os dispositivos e versões do navegador — declará-los como opcionais impede que a sessão falhe se não estiverem disponíveis
8. Loop de Renderização (linhas 335–395)
Porque setAnimationLoop e não requestAnimationFrame?
O loop de renderização Three.js XR deve estar integrado com o callback de frames WebXR. renderer.setAnimationLoop() liga-se automaticamente à sessão XR quando renderer.xr.enabled = true. A alternativa (chamar manualmente session.requestAnimationFrame()) requer chamar o renderer.render() manualmente, mas corre o risco de perder as matrizes corretas de visualização XR que Three.js se aplicam internamente.
scene.background = null deve ser chamado em cada frame porque Three.js pode redefinir este valor internamente durante certas operações.
9. Deteção de Planos (linhas 355–378)
plane.planeSpace é um XRSpace que rastreia a superfície física. frame.getPose() devolve a sua pose no espaço de referência escolhido.
O mapa DetectedPlanes (Map<XRPlane, TRÊS. Mesh>) impede a criação de malhas duplicadas para a mesma superfície entre frames.
8.6 Limitações Conhecidas
|
Limitação |
Razão |
|
As etiquetas de IA não estão alinhadas perfeitamente com o objeto |
As câmaras passthrough do Quest e o getUserMedia entregam diferentes streams com diferentes FOV e calibração ótica |
|
A deteção de planos requer que a Configuração da Sala de Missões seja concluída |
O headset deve ter escaneado a sala anteriormente |
|
O getUserMedia pode ser recusado em alguns dispositivos |
Depende das permissões do navegador |
|
A inferência de IA não é em tempo real (executa-se a cada 2 segundos) |
O Coco-SSD é um modelo relativamente pesado; Alternativas mais leves (SSD MobileNet) dariam maior débito |
8.7 Código-fonte
https://github.com/bciric1/ARVR