A realidade virtual chega à Web

Alguns conceitos básicos para preparar você para um espectro de experiências imersivas: realidade virtual, realidade aumentada e tudo mais.

Joe Medley
Joe Medley

As experiências imersivas chegaram à Web no Chrome 79. A API WebXR Device traz realidade virtual, enquanto o suporte para realidade aumentada chega no Chrome 81. Uma atualização da API GamePad estende o uso avançado de controles para VR. Outros navegadores vão oferecer suporte a essas especificações em breve, incluindo Firefox Reality, Oculus Browser, Edge e Helio do Magic Leap, entre outros.

Este artigo inicia uma série sobre a Web imersiva. Esta edição aborda a configuração de um aplicativo WebXR básico, além de entrar e sair de uma sessão XR. Artigos posteriores vão abordar o loop de frames (o carro-chefe da experiência WebXR), os detalhes da realidade aumentada e a API WebXR Hit Test, uma maneira de detectar superfícies em uma sessão de RA. Salvo indicação em contrário, tudo o que abordo neste e nos próximos artigos se aplica igualmente à RA e à RV.

O que é a Web imersiva?

Embora usemos dois termos para descrever experiências imersivas (realidade aumentada e realidade virtual), muitas pessoas pensam nelas em um espectro que vai da realidade completa ao virtual completo, com graus de imersão entre eles. O "X" em XR reflete esse pensamento, sendo uma espécie de variável algébrica que representa qualquer coisa no espectro de experiências imersivas.

Um gráfico que ilustra o espectro de experiências visuais, da realidade completa à imersão total.
O espectro de experiências imersivas

Exemplos de experiências imersivas:

  • Jogos
  • Vídeos em 360°
  • Vídeos tradicionais em 2D (ou 3D) apresentados em ambientes imersivos
  • Compra de imóveis
  • Conferir os produtos na sua casa antes de comprar
  • Arte imersiva
  • Algo legal que ninguém pensou ainda

Conceitos e uso

Vou explicar algumas noções básicas sobre o uso da API WebXR Device. Se você precisar de mais detalhes do que eu forneci, confira os exemplos do WebXR do Immersive Web Working Group ou os materiais de referência cada vez maiores da MDN. Se você conhece as primeiras versões da API WebXR Device, dê uma olhada em todo esse material. Houve mudanças.

O código neste artigo é baseado no exemplo básico do Immersive Web Working Group (demonstração, fonte), mas foi editado para fins de esclarecimento e simplicidade.

Parte da criação da especificação WebXR foi o desenvolvimento de medidas de segurança e privacidade para proteger os usuários. Consequentemente, as implementações precisam obedecer a determinados requisitos. Uma página da Web ou um app precisa estar ativo e em foco antes de poder solicitar algo sensível do espectador. As páginas da Web ou os apps precisam ser veiculados por HTTPS. A API foi projetada para proteger as informações obtidas de sensores e câmeras, que são necessárias para o funcionamento dela.

Solicitar uma sessão

Para iniciar uma sessão de XR, é necessário um gesto do usuário. Para isso, use a detecção de recursos para testar XRSystem (via navigator.xr) e faça uma chamada para XRSystem.isSessionSupported(). Nas versões 79 e 80 do Chrome, o objeto XRSystem era chamado de XR.

No exemplo abaixo, indiquei que quero uma sessão de realidade virtual com o tipo de sessão 'immersive-vr'. Os outros tipos de sessão são 'immersive-ar' e 'inline'. Uma sessão inline é para apresentar conteúdo em HTML e é usada principalmente para conteúdo teaser. A amostra Sessão de RA imersiva demonstra isso. Vou explicar isso em um artigo posterior.

Depois de saber que as sessões de realidade virtual são compatíveis, eu ativo um botão que me permite adquirir um gesto do usuário.

if (navigator.xr) {
  const supported = await navigator.xr.isSessionSupported('immersive-vr');
  if (supported) {
    xrButton.addEventListener('click', onButtonClicked);
    xrButton.textContent = 'Enter VR';
    xrButton.enabled = supported; // supported is Boolean
  }
}

Depois de ativar o botão, espero um evento de clique e solicito uma sessão.

let xrSession = null;
function onButtonClicked() {
  if (!xrSession) {
    navigator.xr.requestSession('immersive-vr')
    .then((session) => {
      xrSession = session;
      xrButton.textContent = 'Exit XR';
      onSessionStarted(xrSession);
    });
  } else {
    xrSession.end();
  }
}

Observe a hierarquia de objetos neste código. Ele se move de navigator para xr e para uma instância XRSession. Nas primeiras versões da API, um script precisava solicitar um dispositivo antes de solicitar uma sessão. Agora, o dispositivo é adquirido implicitamente.

Entrar em uma sessão

Depois de receber uma sessão, preciso iniciá-la e entrar nela. Mas primeiro, preciso configurar algumas coisas. Uma sessão precisa de um manipulador de eventos onend para que o app ou página da Web possa ser redefinido quando o usuário sair.

Também vou precisar de um elemento <canvas> para desenhar minha cena. Ele precisa ser um WebGLRenderingContext ou WebGL2RenderingContext compatível com XR. Todos os desenhos são feitos usando esses elementos ou um framework baseado em WebGL, como o Three.js.

Agora que tenho um lugar para desenhar, preciso de uma fonte de conteúdo para usar. Para isso, crio uma instância de XRWebGLLayer. Eu o associo à tela chamando XRSession.updateRenderState().

Depois de entrar em uma sessão, preciso de uma maneira de determinar onde as coisas estão na realidade virtual. Preciso de um espaço de referência. Um espaço de referência 'local-floor' é aquele em que a origem está localizada perto do espectador e o eixo y é 0 no nível do chão e não deve se mover. Existem outros tipos de espaços de referência, mas esse é um assunto mais complicado do que posso abordar aqui. Salvo o espaço de referência em uma variável porque vou precisar dele ao desenhar na tela.

function onSessionStarted(xrSession) {
  xrSession.addEventListener('end', onSessionEnded);

  let canvas = document.createElement('canvas');
  webGLRenContext = canvas.getContext('webgl', { xrCompatible: true });

  xrSession.updateRenderState({
    baseLayer: new XRWebGLLayer(xrSession, webGLRenContext)
  });

  xrSession.requestReferenceSpace('local-floor')
  .then((refSpace) => {
    xrRefSpace = refSpace;
    xrSession.requestAnimationFrame(onXRFrame);
  });
}

Depois de receber um espaço de referência, chamo XRSession.requestAnimationFrame(). Esse é o início da apresentação de conteúdo virtual, que é feita no loop de frames.

Executar um loop de frames

O loop de frames é um loop infinito controlado pelo user agent em que o conteúdo é desenhado repetidamente na tela. O conteúdo é desenhado em blocos discretos chamados frames. A sucessão de frames cria a ilusão de movimento. Para aplicativos de RV, os quadros por segundo podem variar de 60 a 144. A RA para Android funciona a 30 quadros por segundo. Seu código não pode presumir uma taxa de frames específica.

O processo básico para o loop de frames é:

  1. Ligue para a XRSession.requestAnimationFrame(). Em resposta, o user agent invoca o XRFrameRequestCallback, que é definido por você.
  2. Dentro da função de callback:
    1. Ligue para XRSession.requestAnimationFrame() novamente.
    2. Receba a postura do espectador.
    3. Transmita ('bind') o WebGLFramebuffer do XRWebGLLayer para o WebGLRenderingContext.
    4. Itere cada objeto XRView, recuperando o XRViewport do XRWebGLLayer e transmitindo-o ao WebGLRenderingContext.
    5. Desenhe algo no framebuffer.

O restante deste artigo descreve a etapa 1 e parte da etapa 2, configurando e chamando o XRFrameRequestCallback. Os itens restantes da etapa 2 são abordados na parte II.

O XRFrameRequestCallback

O XRFrameRequestCallback é definido por você. Ele usa dois parâmetros: uma DOMHighResTimeStamp e uma instância de XRFrame. O objeto XRFrame fornece as informações necessárias para renderizar um único frame na tela. O argumento DOMHighResTimeStamp é para uso futuro.

Antes de fazer qualquer outra coisa, vou solicitar o próximo frame de animação. Como já foi dito, o tempo dos frames é determinado pelo user agent com base no hardware subjacente. Solicitar o próximo frame primeiro garante que o loop de frames continue se algo durante o callback gerar um erro.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  // Render a frame.
}

Agora é hora de desenhar algo para o espectador. Isso é assunto para a parte II. Antes de ir até lá, vou mostrar como encerrar uma sessão.

Encerrar a sessão

Uma sessão imersiva pode terminar por vários motivos, incluindo o encerramento pelo seu código com uma chamada para XRSession.end(). Outros motivos incluem o headset estar desconectado ou outro aplicativo assumir o controle dele. Por isso, um aplicativo bem-comportado precisa monitorar o evento end. Quando isso acontecer, descarte a sessão e os objetos de renderização relacionados. Não é possível retomar uma sessão imersiva encerrada. Para entrar novamente na experiência imersiva, meu app precisa iniciar uma nova sessão.

Lembre-se de que, em Como entrar em uma sessão, durante a configuração, adicionei um manipulador de eventos onend.

function onSessionStarted(xrSession) {
  xrSession.addEventListener('end', onSessionEnded);
  // More setup…
}

Dentro do manipulador de eventos, restaure o estado do app antes que o usuário tenha entrado em uma sessão.

function onSessionEnded(event) {
  xrSession = null;
  xrButton.textContent = 'Enter VR';
}

Conclusão

Não expliquei tudo o que você precisa para escrever um aplicativo Web XR ou AR. Espero ter dado informações suficientes para você começar a entender o código e fazer testes. No próximo artigo, vou explicar o loop de frames, que é onde o conteúdo é exibido na tela.