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. Conforme declarado, a implementação da API no Chrome ainda está em andamento. O Chrome tem o prazer de anunciar que parte do trabalho foi concluído. 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, suponho que você já sabe como 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 neste artigo é baseado no exemplo do teste de hit do grupo de trabalho da Web imersivo (demo, fonte, em inglês), mas não é idêntico. 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 desenhando 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
});

Entrar em uma sessão

Em artigos anteriores, apresentei um código para entrar em uma sessão de XR. Eu mostrei uma versão disso 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 pose 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 do teste de acerto 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 acerto. A origem da confusão foi um mal-entendido dos 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, use um frame de referência centralizado 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' que estou usando está se movendo.

Para o desenho de imagem, uso o espaço de referência local, que oferece 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 estamos falando de um controlador. Faz sentido pensar no controlador como um leitor eletrônico. Depois de receber isso, chamo xrSession.requestHitTestSource(), que cria a fonte dos dados do teste de hit 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.

À medida que você move o dispositivo, o retículo precisa se mover com ele para tentar 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 plataformas. A primeira na matriz é a mais próxima da câmera. Na maioria das vezes, você vai usar, 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, será 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 frame seguinte.

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 acerto. O processo básico é o seguinte. 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
}

Colocar um objeto

Um objeto é colocado em RA quando o usuário toca na tela. Já adicionei um manipulador de eventos select à sessão. Veja acima.

O importante nessa 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 fornecido informações suficientes para que você entenda 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