Posicionar objetos virtuais em visualizações do mundo real

A API Hit Test permite posicionar itens virtuais em uma visualização do mundo real.

Joe Medley
Joe Medley

A API WebXR Device foi lançada no outono passado no Chrome 79. Como mencionado, a implementação da API no Chrome está em desenvolvimento. O Chrome tem o prazer de anunciar que parte do trabalho foi concluído. No Chrome 81, dois novos recursos chegaram:

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, presumimos que você já saiba criar uma sessão de realidade aumentada e como executar um loop de frame. Se você não conhece esses conceitos, leia os artigos anteriores desta série.

Exemplo de sessão de RA imersiva

O código deste artigo é baseado, mas não é idêntico, no encontrado no exemplo de teste de acerto do Grupo de trabalho da Web imersiva (demonstração, fonte). Neste exemplo, você pode colocar girassóis virtuais em superfícies do mundo real.

Quando você abrir o app pela primeira vez, vai aparecer um círculo azul com um ponto no meio. O ponto é a interseção entre uma linha imaginária do dispositivo até 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 acerto fornece a posição e a orientação do ponto de interseção, mas nada sobre as superfícies em si.

Esse círculo é chamado de retículo, que é uma imagem temporária que ajuda a colocar um objeto na realidade aumentada. Se você tocar na tela, um girassol será colocado na superfície no local e na orientação do retículo independentemente de onde você tocou na tela. O retículo continua se movendo com o dispositivo.

Uma retícula renderizada em uma parede, Lax ou Strict, dependendo do contexto
O retículo é uma imagem temporária que ajuda a posicionar um objeto na realidade aumentada.

Criar a retícula

Você precisa criar a imagem da retícula, já que ela não é fornecida pelo navegador ou pela API. O método de carregamento e renderização é específico do framework. Se você não estiver renderizando diretamente usando o WebGL ou o WebGL2, consulte a documentação do framework. Por esse motivo, não vou entrar em detalhes sobre como a retícula é desenhada na amostra. Abaixo, mostro uma linha dele por um 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 solicitar '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

Nos artigos anteriores, apresentei o código para entrar em uma sessão de XR. Mostrei uma versão abaixo com algumas adições. Primeiro, adicionei o listener de eventos select. Quando o usuário toca na tela, uma flor é colocada na visualização da câmera com base na posição do retículo. Vou descrever esse ouvinte de evento mais adiante.

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

Observe que o código destacado chama XRSession.requestReferenceSpace() duas vezes. No começo, isso me pareceu confuso. Perguntei por que o código de teste de hit não solicita um frame de animação (iniciando o loop do frame) e por que o loop de frame parece não envolver testes de hit. A origem da confusão foi um mal-entendido sobre os 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 este exemplo usando uma plataforma independente e que você tem um fone de ouvido e um controle. Para medir distâncias do controlador, você usaria um frame de referência centrado no controlador. 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 desenhar imagens, uso o espaço de referência local, que me dá estabilidade em termos de ambiente. Depois de receber isso, inicie o loop de frame chamando requestAnimationFrame().

Para o teste de hit, uso o espaço de referência viewer, que é baseado na postura do dispositivo no momento do teste de hit. O rótulo "visualizador" é um pouco confuso neste contexto porque estou falando sobre um controlador. Faz sentido pensar no controlador como um leitor eletrônico. Depois de receber isso, eu chamo xrSession.requestHitTestSource(), que cria a origem dos dados do teste de acerto que vou usar ao desenhar.

Como executar um loop de frame

O callback requestAnimationFrame() também recebe um novo código para processar o teste de hit.

Conforme você move o dispositivo, o retículo precisa se mover com ele enquanto tenta encontrar superfícies. Para criar a ilusão de movimento, desenhe o retículo em cada frame. Mas não mostre a retícula se o teste de acerto falhar. Para o retículo que criei anteriormente, 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, teste 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 chamo getHitTestResults(). Ele usa o hitTestSource como argumento e retorna uma matriz de instâncias HitTestResult. O teste de hit pode encontrar várias superfícies. A primeira na matriz é a mais próxima da câmera. Na maioria das vezes, ele é usado, mas uma matriz é retornada para casos de uso avançados. Por exemplo, imagine que sua câmera está apontada para uma caixa em uma mesa em um piso. É possível que o teste de hit retorne as três superfícies no array. Na maioria dos casos, é a caixa que me interessa. Se o comprimento da matriz retornada for 0, ou seja, se nenhum teste de acerto for retornado, continue em frente. 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 hit. O processo básico é este. Receba uma pose do resultado do teste de acerto, transforme (mova) a imagem do retículo para a posição do teste de acerto e defina a propriedade visible como verdadeira. A pose representa a posição 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 posicionar 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 colocá-lo. Como a retícula em movimento fornece uma fonte constante de testes de acerto, a maneira mais simples de colocar um objeto é desenhá-lo no local da retícula 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 é seguir o código de exemplo ou testar o codelab. Espero ter trazido informações suficientes para entender os dois.

Ainda não terminamos de criar APIs da Web imersiva, nem de longe. Publicaremos novos artigos aqui à medida que progredirmos.

Foto de Daniel Frank no Unsplash