Tudo sobre o loop de frames
Recentemente, publiquei A realidade virtual chega à Web, um artigo que introduziu conceitos básicos por trás da API WebXR Device (link em inglês). Também dei instruções para solicitar, inserir e encerrar uma sessão de XR.
Neste artigo, descrevemos o loop do frame, que é um loop infinito controlado pelo user agent em que o conteúdo é exibido repetidamente na tela. O conteúdo é desenhado em blocos discretos chamados de frames. A sucessão de frames cria a ilusão de movimento.
O que este artigo não aborda
WebGL e WebGL2 são as únicas formas de renderizar conteúdo durante um loop de frames em um app WebXR. Felizmente, muitos frameworks oferecem uma camada de abstração sobre o WebGL e WebGL2. Esses frameworks incluem three.js, babylonjs e PlayCanvas, enquanto o A-Frame e o React 360 foram projetados para interagir com o WebXR.
Este artigo não é um tutorial do WebGL nem do framework. Ele explica os conceitos básicos de um loop de frames usando o exemplo de sessão de RV imersiva do Immersive Web Working Group (demonstração, fonte). Se você quiser mergulhar no WebGL ou em um dos frameworks, a Internet fornece uma lista crescente de artigos.
Os jogadores e o jogo
Ao tentar entender o loop do frame, ficava me perdendo nos detalhes. Há muitos objetos em jogo, e alguns deles são nomeados apenas por propriedades de referência em outros objetos. Para ajudar você a manter a linha, vou descrever os objetos, que chamo de "jogadores". Depois, vou descrever como eles interagem, o que chamo de "o jogo".
Os jogadores
XRViewerPose
Uma pose é a posição e a orientação de algo no espaço 3D. Os espectadores
e os dispositivos de entrada têm uma pose, mas é a pose do espectador com a qual estamos preocupados
aqui. As poses do visualizador e do dispositivo de entrada têm um atributo transform
que descreve
a posição como vetor e a orientação como um quatérnio relativo à
origem. A origem é especificada com base no tipo de espaço de referência solicitado ao
chamar XRSession.requestReferenceSpace()
.
Os espaços de referência demoram um pouco para serem explicados. Elas são abordadas em detalhes na seção Realidade
aumentada. A amostra que estou usando como base para este
artigo utiliza um espaço de referência 'local'
, o que significa que a origem está na
posição do espectador no momento da criação da sessão sem um valor mínimo bem definido,
e a posição precisa dele pode variar de acordo com a plataforma.
XRView
Uma visualização corresponde a uma câmera que visualiza a cena virtual. Uma visualização também tem um
atributo transform
que descreve a posição como vetor e a orientação.
Eles são fornecidos como um par de vetor/quatérnio e como uma matriz equivalente. Você pode usar qualquer uma das representações, dependendo da que melhor se adapta ao seu código. Cada
visualização corresponde a uma tela ou parte de uma tela usada por um dispositivo para
apresentar imagens ao espectador. Os objetos XRView
são retornados em uma matriz do
objeto XRViewerPose
. O número de visualizações na matriz varia. Em dispositivos
móveis, uma cena em RA tem uma visualização, que pode ou não cobrir a tela do dispositivo.
Os headsets normalmente têm duas visualizações, uma para cada olho.
XRWebGLLayer
As camadas fornecem uma fonte de imagens em bitmap e descrições de como essas imagens
precisam ser renderizadas no dispositivo. Essa descrição não capta exatamente o que
esse player faz. Pense nele como um intermediário entre um dispositivo e um
WebGLRenderingContext
. O MDN adota basicamente a mesma visão, afirmando que "fornece
um vínculo" entre os dois. Dessa forma, ele dá acesso aos outros jogadores.
Em geral, objetos WebGL armazenam informações de estado para renderizar gráficos 2D e 3D.
WebGLFramebuffer
Um framebuffer fornece dados de imagem ao WebGLRenderingContext
. Depois
de recuperá-lo do XRWebGLLayer
, basta transmiti-lo ao
WebGLRenderingContext
atual. Além de chamar bindFramebuffer()
(falaremos mais sobre isso
posteriormente), você nunca acessará esse objeto diretamente. Você apenas o transmitirá do
XRWebGLLayer
para o WebGLRenderingContext.
XRViewport
Uma janela de visualização fornece as coordenadas e dimensões de uma região retangular no
WebGLFramebuffer
.
WebGLRenderingContext
Um contexto de renderização é um ponto de acesso programático para uma tela (o espaço em que
estamos desenhando). Para fazer isso, ele precisa de um WebGLFramebuffer
e um XRViewport.
Observe a relação entre XRWebGLLayer
e WebGLRenderingContext
. Um
corresponde ao dispositivo do espectador e o outro corresponde à página da Web.
WebGLFramebuffer
e XRViewport
são transmitidos do primeiro para o segundo.
O jogo
Agora que sabemos quem são, vamos analisar o jogo que eles jogam. É um jogo que começa de novo a cada frame. Lembre-se de que os frames fazem parte de um loop de frames que acontece em uma taxa que depende do hardware subjacente. Para aplicações de RV, os quadros por segundo podem variar de 60 a 144. A RA para Android é executada a 30 quadros por segundo. O código não deve presumir nenhum frame rate específico.
O processo básico para o loop do frame é semelhante ao seguinte:
- Chame
XRSession.requestAnimationFrame()
. Em resposta, o user agent invoca oXRFrameRequestCallback
, que é definido por você. - Dentro da função de callback:
- Chame
XRSession.requestAnimationFrame()
de novo. - Identifique a pose do espectador.
- Transmita ("bind") o
WebGLFramebuffer
doXRWebGLLayer
para oWebGLRenderingContext
. - Itere em cada objeto
XRView
, recuperando oXRViewport
doXRWebGLLayer
e transmitindo-o para oWebGLRenderingContext
. - Desenhe algo no framebuffer.
- Chame
Como as etapas 1 e 2a foram abordadas no artigo anterior, vou começar na etapa 2b.
Conferir a pose do espectador
É provável que não seja preciso dizer. Para desenhar algo em RA ou RV, preciso saber
onde o espectador está e para onde ele está olhando. A posição e
orientação do visualizador são fornecidas por um objeto
XRViewerPose. Posso
ver a pose do espectador chamando XRFrame.getViewerPose()
no frame de animação
atual. Eu transfiro o espaço de referência que adquiri ao configurar a
sessão. Os valores retornados por esse objeto são sempre relativos ao espaço de referência que solicitei quando entrei na sessão atual. Como vocês devem
se lembrar, preciso passar o espaço de referência atual ao solicitar a pose.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
if (xrViewerPose) {
// Render based on the pose.
}
}
Há uma pose do espectador que representa a posição geral do usuário, ou seja,
a cabeça do espectador ou a câmera do smartphone, no caso de um smartphone.
A pose informa ao aplicativo onde o visualizador está. A renderização real de imagens usa
objetos XRView
, que serão abordados em breve.
Antes de continuar, teste se a pose do espectador foi retornada caso o sistema perde o rastreamento ou bloqueie a pose por motivos de privacidade. O rastreamento é a capacidade do dispositivo XR de saber onde ele e/ou seus dispositivos de entrada estão em relação ao ambiente. O rastreamento pode ser perdido de várias maneiras e varia de acordo com o método usado para isso. Por exemplo, se as câmeras no fone de ouvido ou no smartphone forem usadas para rastrear o dispositivo, ele poderá perder a capacidade de determinar onde ele está em situações com pouca ou nenhuma luz, ou se as câmeras estiverem cobertas.
Um exemplo de bloqueio da pose por motivos de privacidade é que, se o fone de ouvido mostrar
uma caixa de diálogo de segurança, como uma solicitação de permissão, o navegador poderá parar de fornecer
posições ao app enquanto isso acontece. No entanto, já chamei
XRSession.requestAnimationFrame()
para que, se o sistema puder se recuperar, o loop
de frames continuará. Caso contrário, o user agent encerrará a sessão e chamará o
manipulador de eventos end
.
Um breve desvio
A próxima etapa requer objetos criados durante a configuração da
sessão. Lembre-se de que
criei uma tela e a orientei a criar um contexto de renderização do Web GL
compatível com XR, que recebi chamando canvas.getContext()
. Todo o desenho é feito usando
a API WebGL, a API WebGL2 ou um framework baseado em WebGL, como o Three.js. Esse
contexto foi transmitido para o objeto da sessão via updateRenderState()
, junto com uma
nova instância de XRWebGLLayer
.
let canvas = document.createElement('canvas');
// The rendering context must be based on WebGL or WebGL2
let webGLRenContext = canvas.getContext('webgl', { xrCompatible: true });
xrSession.updateRenderState({
baseLayer: new XRWebGLLayer(xrSession, webGLRenContext)
});
Transmitir ("bind") o WebGLFramebuffer
O XRWebGLLayer
fornece um framebuffer para o WebGLRenderingContext
fornecido especificamente para uso com o WebXR e substitui os contextos de renderização
padrão de framebuffer. Na linguagem WebGL, isso é chamado de "vinculação".
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
if (xrViewerPose) {
let glLayer = xrSession.renderState.baseLayer;
webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
// Iterate over the views
}
}
Iterar em cada objeto XRView
Depois de conseguir a pose e vincular o framebuffer, é hora de extrair as
janelas de visualização. O XRViewerPose
contém uma matriz de interfaces XRView, cada uma
representando uma tela ou parte de uma tela. Eles contêm informações
necessárias para renderizar o conteúdo posicionado corretamente para o dispositivo e
para o visualizador, como o campo de visão, o deslocamento do olhar e outras propriedades ópticas.
Como estou desenhando para dois olhos, tenho duas visualizações, que eu cruzo e desenho
uma imagem separada para cada uma.
Ao implementar para realidade aumentada baseada em smartphone, eu teria apenas uma visualização, mas ainda usaria um loop. Embora possa parecer inútil iterar em uma visualização, fazer isso permite que você tenha um único caminho de renderização para uma variedade de experiências imersivas. Essa é uma diferença importante entre o WebXR e outros sistemas imersivos.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
if (xrViewerPose) {
let glLayer = xrSession.renderState.baseLayer;
webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
for (let xrView of xrViewerPose.views) {
// Pass viewports to the context
}
}
}
Transmitir o objeto XRViewport para o WebGLRenderingContext
Um objeto XRView
refere-se ao que é observável em uma tela. Mas, para desenhar nessa visualização,
preciso de coordenadas e dimensões específicas para meu dispositivo. Assim como no
framebuffer, eles são solicitados pelo XRWebGLLayer
e transmitidos para
WebGLRenderingContext
.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
if (xrViewerPose) {
let glLayer = xrSession.renderState.baseLayer;
webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
for (let xrView of xrViewerPose.views) {
let viewport = glLayer.getViewport(xrView);
webGLRenContext.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
// Draw something to the framebuffer
}
}
}
webGLRenContext
Ao escrever este artigo, tive um debate com alguns colegas sobre a nomenclatura do
objeto webGLRenContext
. Os scripts de exemplo e a maioria dos códigos do WebXR
chamam essa variável de gl
de maneira simples. Quando eu estava trabalhando para entender as amostras, fiquei
esquecendo o que gl
se referia. Chamei de webGLRenContext
para lembrar
que você está aprendendo que esta é uma instância de WebGLRenderingContext
.
O motivo é que o uso de gl
permite que os nomes de métodos sejam parecidos com os
equivalentes na API OpenGL ES 2.0, usada para criar RV em linguagens
compiladas. Esse fato é óbvio se você criou apps de RV usando o OpenGL, mas
fica confuso se você ainda não conhece essa tecnologia.
Desenhar algo no framebuffer
Se você tiver uma ambição muito maior, poderá usar o WebGL diretamente, mas não recomendo isso. É muito mais simples usar uma das estruturas listadas na parte superior.
Conclusão
Este não é o fim das atualizações ou dos artigos sobre o WebXR. Você pode encontrar uma referência para todas as interfaces e membros do WebXR no MDN. Para as próximas melhorias das interfaces, siga os recursos individuais no Status do Chrome.
Foto de JESHOOTS.COM no Unsplash