Posicionar objetos virtuais em visualizações do mundo real

A API de teste de hit permite posicionar itens virtuais em uma visualização do mundo real.

Joe Medley
Joe Medley

A API WebXR Device foi lançada no segundo semestre de 2019 no Chrome 79. Como afirmado, a implementação da API pelo Chrome está em desenvolvimento. 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, presumimos 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.

A amostra de sessão de RA imersiva

O código neste artigo é baseado, mas não idêntico, ao encontrado no exemplo de teste de hit do Immersive Web Working Group (demonstração, fonte). Neste exemplo, você pode 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 dispositivo até o ponto no ambiente. Ele se move conforme você move o dispositivo. À medida que encontra pontos de interseção, ele parece se ajustar a superfícies como pisos, tampos de mesa e paredes. Isso ocorre 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 posicionar um objeto na 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, independentemente de onde você tocou na tela. O retículo continua se movendo 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 posicionar um objeto em realidade aumentada.

Criar o retículo

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 esse motivo, não vamos entrar em detalhes sobre como o retículo é desenhado no exemplo. Abaixo, mostro uma linha dele por um único motivo: para que, em exemplos de código posteriores, você saiba a que estou me referindo 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 o código para entrar em uma sessão de XR. Mostramos abaixo uma versão disso 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 pose 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

O código destacado chama XRSession.requestReferenceSpace() duas vezes. No início, achei isso confuso. Perguntei por que o código do teste de hit não solicita um frame de animação (iniciando o loop de 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á visualizando essa amostra usando um equipamento independente e que tem um headset e um controle. Para medir as distâncias do controle, use um frame de referência centrado no controle. 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, inicio o loop de frame chamando requestAnimationFrame().

Para o teste de hit, uso o espaço de referência viewer, que se baseia na posição do dispositivo no momento do teste. O rótulo "visualizador" é um pouco confuso neste contexto porque estou falando de um controlador. Faz sentido se você pensar no controlador como um visualizador eletrônico. Depois de receber isso, chamo xrSession.requestHitTestSource(), que cria a origem dos dados do teste de hit 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.

Ao mover 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 o retículo se o teste de hit 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 qualquer coisa em RA, preciso saber onde o espectador está e para onde ele está olhando. Por isso, 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, chamo getHitTestResults(). Ele usa o hitTestSource como argumento e retorna uma matriz de instâncias de 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 usá-lo, 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 é o que me interessa. Se o comprimento da matriz retornada for 0, ou seja, se nenhum teste de hit 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 hit. O processo básico é o seguinte. Receba uma pose do resultado do teste de hit, transforme (mova) a imagem do retículo para a posição do teste de hit e defina a propriedade visible como verdadeira. A pose representa a pose 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. Confira acima.

O importante nesta etapa é saber onde colocá-lo. Como o retículo em movimento oferece uma fonte constante de testes de hit, a maneira mais simples de posicionar 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 entender isso é analisar o exemplo de código ou testar o codelab. Espero ter dado informações suficientes para que você entenda os dois.

Ainda não terminamos de criar APIs imersivas para a Web, nem de longe. Vamos publicar novas matérias aqui conforme avançarmos.