A API Hit Test permite posicionar itens virtuais em uma visualização do mundo real.
A API WebXR Device foi lançada no outono passado no Chrome 79. Como afirmado na época, a implementação da API pelo Chrome está em andamento. O Chrome tem o prazer de anunciar que parte do trabalho foi concluída. No Chrome 81, dois novos recursos foram lançados:
Este artigo aborda a API WebXR Hit Test, uma maneira de colocar objetos virtuais em uma visualização de câmera do mundo real.
Neste artigo, presumo que você já saiba como criar uma sessão de realidade aumentada e como executar um loop de frames. Se você não conhece esses conceitos, leia os artigos anteriores desta série.
- A realidade virtual chega à Web
- A realidade virtual chega à Web, parte II
- RA na Web: talvez você já saiba como usar
O exemplo de sessão de RA imersiva
O código neste artigo é baseado, mas não idêntico, ao encontrado na amostra de teste de ocorrência do Immersive Web Working Group (demonstração, fonte). Com este exemplo, você pode colocar girassóis virtuais em superfícies do mundo real.
Ao abrir o app pela primeira vez, você vai ver um círculo azul com um ponto no meio. O ponto é a interseção entre uma linha imaginária do dispositivo e o ponto no ambiente. Ele se move conforme você move o dispositivo. À medida que encontra pontos de interseção, ele parece se encaixar em superfícies como pisos, mesas e paredes. Isso acontece porque o teste de ocorrência fornece a posição e a orientação do ponto de interseção, mas não as próprias superfícies.
Esse círculo é chamado de retículo, uma imagem temporária que ajuda a posicionar um objeto em realidade aumentada. Se você tocar na tela, um girassol será colocado na superfície no local e na orientação do ponto do retículo, independente de onde você tocou na tela. A mira continua se movendo com o dispositivo.
Criar a mira
Você precisa criar a imagem do retículo, já que ela não é fornecida pelo navegador ou pela API. O método de carregamento e desenho é específico do framework.
Se você não estiver desenhando diretamente usando WebGL ou WebGL2, consulte a documentação do framework. Por isso, não vou entrar em detalhes sobre como a retícula é
desenhada na amostra. Abaixo, mostro uma linha dele por um único motivo: para que, em exemplos de código posteriores, você saiba a que me refiro quando uso a variável reticle.
let reticle = new Gltf2Node({url: 'media/gltf/reticle/reticle.gltf'});
Solicitar uma sessão
Ao solicitar uma sessão, você precisa pedir 'hit-test' na matriz
requiredFeatures, conforme mostrado abaixo.
navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['local', 'hit-test']
})
.then((session) => {
// Do something with the session
});
Como entrar em uma sessão
Em artigos anteriores, apresentei código para iniciar uma sessão de RV/RA. Mostrei
uma versão disso abaixo com algumas adições. Primeiro, adicionei o listener de eventos select. Quando o usuário tocar na tela, uma flor será colocada na visualização da câmera com base na postura da mira. Vou descrever esse listener de eventos mais tarde.
function onSessionStarted(xrSession) {
xrSession.addEventListener('end', onSessionEnded);
xrSession.addEventListener('select', onSelect);
let canvas = document.createElement('canvas');
gl = canvas.getContext('webgl', { xrCompatible: true });
xrSession.updateRenderState({
baseLayer: new XRWebGLLayer(session, gl)
});
xrSession.requestReferenceSpace('viewer').then((refSpace) => {
xrViewerSpace = refSpace;
xrSession.requestHitTestSource({ space: xrViewerSpace })
.then((hitTestSource) => {
xrHitTestSource = hitTestSource;
});
});
xrSession.requestReferenceSpace('local').then((refSpace) => {
xrRefSpace = refSpace;
xrSession.requestAnimationFrame(onXRFrame);
});
}
Vários espaços de referência
O código destacado chama XRSession.requestReferenceSpace() duas vezes. No começo, achei isso confuso. Perguntei por que o código de teste de ocorrência não
solicita um frame de animação (iniciando o loop de frames) e por que o loop de frames
parece não envolver testes de ocorrência. A origem da confusão foi um mal-entendido sobre espaços de referência. Os espaços de referência expressam relações entre uma origem e o mundo.
Para entender o que esse código está fazendo, imagine que você está vendo esta amostra usando um equipamento independente e tem um headset e um controle. Para medir distâncias do controlador, use um sistema de referência centrado nele. Mas, para desenhar algo na tela, você usaria coordenadas centradas no usuário.
Neste exemplo, o visualizador e o controlador são o mesmo dispositivo. Mas tenho um problema. O que eu desenho precisa ser estável em relação ao ambiente, mas o "controlador" com que estou desenhando está se movendo.
Para o desenho de imagens, uso o espaço de referência local, que me dá estabilidade em termos de ambiente. Depois disso, inicio o loop de frames chamando requestAnimationFrame().
Para testes de hit, uso o espaço de referência viewer, que é baseado na
posição do dispositivo no momento do teste de hit. O rótulo "visualizador" é um pouco confuso nesse contexto porque estou falando de um controlador. Isso faz sentido se você pensar no controlador como um visualizador eletrônico. Depois disso, chamo xrSession.requestHitTestSource(), que cria a origem dos dados de teste de ocorrência que vou usar ao desenhar.
Como executar um loop de frames
O callback requestAnimationFrame() também recebe um novo código para processar o teste de hit.
À medida que você move o dispositivo, o retículo precisa se mover com ele enquanto tenta encontrar superfícies. Para criar a ilusão de movimento, redesenhe o retículo em cada frame.
Mas não mostre a mira se o teste de ocorrência falhar. Então, para o retículo que criei
antes, defini a propriedade visible como false.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
reticle.visible = false;
// Reminder: the hitTestSource was acquired during onSessionStart()
if (xrHitTestSource && xrViewerPose) {
let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
if (hitTestResults.length > 0) {
let pose = hitTestResults[0].getPose(xrRefSpace);
reticle.visible = true;
reticle.matrix = pose.transform.matrix;
}
}
// Draw to the screen
}
Para desenhar algo em RA, preciso saber onde o espectador está e para onde ele está olhando. Então, testo se hitTestSource e xrViewerPose ainda são válidos.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
reticle.visible = false;
// Reminder: the hitTestSource was acquired during onSessionStart()
if (xrHitTestSource && xrViewerPose) {
let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
if (hitTestResults.length > 0) {
let pose = hitTestResults[0].getPose(xrRefSpace);
reticle.visible = true;
reticle.matrix = pose.transform.matrix;
}
}
// Draw to the screen
}
Agora eu chamo getHitTestResults(). Ela usa o hitTestSource como argumento
e retorna uma matriz de instâncias HitTestResult. O teste de hit pode encontrar várias superfícies. O primeiro na matriz é o mais próximo da câmera.
Na maioria das vezes, você vai usar esse método, mas uma matriz será retornada para casos de uso avançados. Por exemplo, imagine que a câmera está apontada para uma caixa em uma mesa no chão. É possível que o teste de ocorrência retorne todas as três superfícies na matriz. Na maioria dos casos, será a caixa que me interessa. Se o comprimento da matriz retornada for 0, ou seja, se nenhum teste de ocorrência for retornado, continue. Tente de novo no próximo frame.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
reticle.visible = false;
// Reminder: the hitTestSource was acquired during onSessionStart()
if (xrHitTestSource && xrViewerPose) {
let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
if (hitTestResults.length > 0) {
let pose = hitTestResults[0].getPose(xrRefSpace);
reticle.visible = true;
reticle.matrix = pose.transform.matrix;
}
}
// Draw to the screen
}
Por fim, preciso processar os resultados do teste de ocorrência. O processo básico é este: Receba
uma postura do resultado do teste de ocorrência, transforme (mova) a imagem do retículo para a posição do teste
de ocorrência e defina a propriedade visible como "true". A postura representa a postura de um ponto em uma superfície.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
reticle.visible = false;
// Reminder: the hitTestSource was acquired during onSessionStart()
if (xrHitTestSource && xrViewerPose) {
let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
if (hitTestResults.length > 0) {
let pose = hitTestResults[0].getPose(xrRefSpace);
reticle.matrix = pose.transform.matrix;
reticle.visible = true;
}
}
// Draw to the screen
}
Como colocar um objeto
Um objeto é colocado em RA quando o usuário toca na tela. Já adicionei um manipulador de eventos select à sessão. Consulte acima.
O importante nesta etapa é saber onde colocar o código. Como a mira móvel oferece uma fonte constante de testes de acerto, a maneira mais simples de colocar um objeto é desenhá-lo no local da mira no último teste de acerto.
function onSelect(event) {
if (reticle.visible) {
// The reticle should already be positioned at the latest hit point,
// so we can just use its matrix to save an unnecessary call to
// event.frame.getHitTestResults.
addARObjectAt(reticle.matrix);
}
}
Conclusão
A melhor maneira de entender isso é analisar o código de exemplo ou testar o codelab. Espero ter dado contexto suficiente para entender os dois.
Ainda não terminamos de criar APIs da Web imersivas, nem de longe. Vamos publicar novos artigos aqui à medida que avançarmos.
Foto de Daniel Frank no Unsplash