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 lançada no último trimestre no Chrome 79. Como dissemos, a implementação da API pelo Chrome ainda está em andamento. O Chrome tem o prazer de anunciar que parte do trabalho foi concluída. No Chrome 81, dois novos recursos chegaram:

Neste artigo, abordamos a API WebXR Hit Test, um meio de colocar objetos virtuais em uma visualização de câmera do mundo real.

Neste artigo, supomos que você já saiba criar uma sessão de realidade aumentada e que saiba executar um loop de frame. Se você não conhece esses conceitos, leia os artigos anteriores desta série.

Amostra da sessão de RA imersiva

O código deste artigo é baseado, mas não é idêntico ao código encontrado no exemplo de teste de hit do Immersive Web Working Group (demonstração, fonte). Este exemplo permite colocar girassóis virtuais em superfícies do mundo real.

Ao abrir o app pela primeira vez, você verá um círculo azul com um ponto no meio. O ponto é a interseção entre uma linha imaginária do seu dispositivo até o ponto no ambiente. Ele se move conforme você move o dispositivo. Ao encontrar pontos de intersecção, ele parece se ajustar a superfícies como pisos, tampo de mesa e paredes. Isso acontece porque o teste de hit 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, uma imagem temporária que ajuda a colocar um objeto em realidade aumentada. Se você tocar na tela, um girassol será colocado na superfície no local do retículo e na orientação do ponto do retículo, independentemente de onde você tocou na tela. O retículo continua a se mover com o dispositivo.

Um retículo renderizado em uma parede, Lax ou Strict, dependendo do contexto
O retículo é uma imagem temporária que ajuda a colocar um objeto em realidade aumentada.

Criar o retículo

Crie a imagem do retículo, porque ela não é fornecida pelo navegador ou pela API. O método de carregamento e desenho é específico para o 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 o retículo é desenhado na amostra. Vou mostrar abaixo uma linha dela apenas por um motivo: para que, nas próximas amostras de código, você saiba a que me referi quando usar 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

Nos artigos anteriores, apresentei o código para entrar em uma sessão de XR. 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 posição do retículo. 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

Observe que o código destacado chama XRSession.requestReferenceSpace() duas vezes. No início, achei isso 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. O motivo 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á visualizando essa amostra usando um rig independente e que tem um fone de ouvido e um controlador. Para medir distâncias do controlador, você usaria um frame de referência centrado no controlador. Mas, para desenhar algo na tela, é preciso usar coordenadas centradas no usuário.

Neste exemplo, o visualizador e o controle são o mesmo dispositivo. Mas eu 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 imagens, uso o espaço de referência local, que me dá estabilidade em termos de ambiente. Depois de receber isso, inicio o loop do frame chamando requestAnimationFrame().

Para testes de hit, uso o espaço de referência viewer, que é baseado na pose do dispositivo no momento do teste de hit. O rótulo "visualizador" é um pouco confuso nesse contexto, porque estou falando de um controle. Faz sentido se você pensar no controle como um visualizador eletrônico. Depois de fazer 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 quadro. Não mostre o retículo se o teste de hit falhar. Então, 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. Testei que 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(). Ele usa hitTestSource como um argumento e retorna uma matriz de instâncias HitTestResult. O teste de hit pode encontrar várias plataformas. O primeiro na matriz é o mais próximo da câmera. Na maioria das vezes você a 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 no chão. É possível que o teste de hit retorne as três superfícies na matriz. Na maioria dos casos, é a caixa que eu mais gosto. Se o comprimento da matriz retornada for 0, ou seja, se nenhum teste de hit for retornado, prossiga 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. Este é o processo básico. Tire uma pose do resultado do teste de hit, transforme (mova) a imagem do retículo para a posição de teste de hit e defina a propriedade visible como "true". 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 na RA quando o usuário toca na tela. Já adicionei um manipulador de eventos select à sessão. Confira acima.

Nesta etapa, o importante é saber onde colocá-lo. Como o retículo em movimento fornece uma fonte constante de testes de hit, a maneira mais simples de colocar um objeto é desenhá-lo no local do retículo no último teste de hit.

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 resolver isso é analisando o exemplo de código ou testar o codelab. Espero ter fornecido informações suficientes para você entender as duas.

Não acabamos de criar APIs da Web imersivas, nem de longe. Vamos publicar novos artigos aqui conforme progredirmos.

Foto de Daniel Frank no Unsplash