Tudo sobre o loop de frames
Recentemente, publiquei A realidade virtual chegou à Web, um artigo que apresentou conceitos básicos por trás da API WebXR Device. Também forneci instruções para solicitar, entrar e encerrar uma sessão de XR.
Este artigo descreve o loop de frame, que é um loop infinito controlado pelo user agent em que o conteúdo é renderizado repetidamente na tela. O conteúdo é exibido em blocos discretos chamados frames. A sucessão de frames cria a ilusão de movimento.
O que este artigo não é
O WebGL e o WebGL2 são os únicos meios de renderizar conteúdo durante um loop de frame em um app WebXR. Felizmente, muitos frameworks oferecem uma camada de abstração sobre o WebGL e o WebGL2. Esses frameworks incluem three.js, babylonjs e PlayCanvas, enquanto A-Frame e React 360 foram projetados para interagir com o WebXR.
Este artigo não é um tutorial sobre WebGL nem sobre frameworks. Ele explica os conceitos básicos de um loop de frame usando o exemplo de sessão de RV imersiva do Grupo de trabalho da Web imersiva (demonstração, fonte). Se você quiser se aprofundar no WebGL ou em um dos frameworks, a Internet oferece uma lista crescente de artigos.
Os jogadores e o jogo
Ao tentar entender o loop de frames, eu me perdia 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 entender, vou descrever os objetos, que estou chamando de "jogadores". Em seguida, vou descrever como eles interagem, que estou chamando de "jogo".
Os jogadores
XRViewerPose
Uma pose é a posição e a orientação de algo no espaço 3D. Tanto os espectadores
quanto os dispositivos de entrada têm uma pose, mas é a pose do espectador que nos interessa
aqui. As poses do dispositivo de entrada e do espectador têm um atributo transform
que descreve
a posição como um vetor e a orientação como um quaternion em relação à
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 levam um pouco para serem explicados. Eu falo sobre eles em detalhes em Realidade
aumentada. O exemplo que estou usando como base para este
artigo usa 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 piso bem definido,
e a posição precisa 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 dela como um vetor e a orientação.
Eles são fornecidos como um par de vetor/quaternion e como uma matriz equivalente.
Você pode usar qualquer representação, dependendo de qual se encaixa melhor no seu código. Cada
visualização corresponde a uma tela ou a uma 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 de RA tem uma visualização, que pode ou não cobrir a tela do dispositivo.
Os headsets geralmente têm duas visualizações, uma para cada olho.
XRWebGLLayer
As camadas fornecem uma fonte de imagens bitmap e descrições de como essas imagens
vão ser renderizadas no dispositivo. Essa descrição não captura exatamente o que
esse player faz. Comecei a pensar nisso como um intermediário entre um dispositivo e um
WebGLRenderingContext
. O MDN tem a mesma opinião, afirmando que ele "oferece
uma vinculação" entre os dois. Assim, ele fornece acesso aos outros jogadores.
Em geral, os objetos do WebGL armazenam informações de estado para renderizar gráficos 2D e 3D.
WebGLFramebuffer
Um framebuffer fornece dados de imagem para o WebGLRenderingContext
. Depois
de extrair o valor do XRWebGLLayer
, basta transmiti-lo para o
WebGLRenderingContext
atual. Além de chamar bindFramebuffer()
(mais informações
mais adiante), você nunca vai acessar esse objeto diretamente. Basta transmiti-lo de
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
. Uma
corresponde ao dispositivo do espectador e a outra corresponde à página da Web.
WebGLFramebuffer
e XRViewport
são transmitidos do primeiro para o segundo.
O jogo
Agora que sabemos quem são os jogadores, vamos analisar o jogo que eles jogam. É um jogo que recomeça a cada frame. Os frames fazem parte de um loop de frames que acontece a uma taxa que depende do hardware. Para aplicativos de RV, os frames por segundo podem variar de 60 a 144. A RA para Android é executada a 30 quadros por segundo. Seu código não deve assumir nenhuma frame rate específica.
O processo básico para o loop de frames é assim:
- Ligue para a
XRSession.requestAnimationFrame()
. Em resposta, o agente do usuário invoca oXRFrameRequestCallback
, que é definido por você. - Dentro da função de callback:
- Ligue para
XRSession.requestAnimationFrame()
novamente. - Posicione o espectador.
- Transmita ('bind') o
WebGLFramebuffer
doXRWebGLLayer
para oWebGLRenderingContext
. - Itere cada objeto
XRView
, recuperando oXRViewport
doXRWebGLLayer
e transmitindo-o aoWebGLRenderingContext
. - Desenhe algo no framebuffer.
- Ligue para
Como as etapas 1 e 2a foram abordadas no artigo anterior, vou começar pela etapa 2b.
Posicione o espectador
Provavelmente não é preciso dizer. Para desenhar qualquer coisa em RA ou RV, preciso saber
onde o espectador está e para onde ele está olhando. A posição e a
orientação do espectador são fornecidas por um objeto
XRViewerPose. Para
conseguir a pose do espectador, chame XRFrame.getViewerPose()
no frame de animação
atual. Eu transmito 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ê pode
lembrar, preciso transmitir 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 espectador está. A renderização de imagem real usa
objetos XRView
, que vou abordar em breve.
Antes de continuar, teste se a pose do espectador foi retornada caso o sistema perca o rastreamento ou bloqueie a pose por motivos de privacidade. O rastreamento é a capacidade do dispositivo de XR de saber onde ele e/ou os 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 o rastreamento. Por exemplo, se as câmeras do fone de ouvido ou do smartphone forem usadas para rastreamento, o dispositivo 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 é se o headset estiver mostrando
uma caixa de diálogo de segurança, como uma solicitação de permissão. O navegador pode parar de fornecer
poses ao aplicativo enquanto isso acontece. Mas eu já chamei
XRSession.requestAnimationFrame()
para que, se o sistema puder se recuperar, o loop
de frames continue. Caso contrário, o agente do usuário encerrará a sessão e chamará o
gerenciador de eventos end
.
Um pequeno desvio
A próxima etapa exige objetos criados durante a configuração
da sessão. Lembre-se de que
criei uma tela e a instruí a criar um contexto de renderização do WebGL compatível com XR, que consegui 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 ao objeto de sessão por 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 o framebuffer padrão
dos contextos de renderização. Isso é chamado de "vinculação" na linguagem do WebGL.
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 sobre cada objeto XRView
Depois de receber a pose e vincular o framebuffer, é hora de receber os
viewports. O XRViewerPose
contém uma matriz de interfaces XRView, cada uma
representando uma tela ou uma parte dela. Eles contêm informações
necessárias para renderizar conteúdo posicionado corretamente para o dispositivo e
o visualizador, como o campo de visão, o deslocamento do olho e outras propriedades ópticas.
Como estou desenhando para dois olhos, tenho duas visualizações, que são percorridas e desenham
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 por uma visualização, isso permite que você tenha um único caminho de renderização para um espectro 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
se refere ao que é observável em uma tela. Mas, para desenhar nessa
visualização, preciso de coordenadas e dimensões específicas do meu dispositivo. Assim como no
framebuffer, eu os solicito do XRWebGLLayer
e os transmito para o
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
}
}
}
O webGLRenContext
Ao escrever este artigo, tive um debate com alguns colegas sobre a nomenclatura do
objeto webGLRenContext
. Os scripts de exemplo e a maioria do código do WebXR chamam
essa variável de gl
. Quando eu estava trabalhando para entender as amostras, eu continuava
esquecendo o que gl
se referia. Chamei de webGLRenContext
para lembrar você
enquanto você aprende que essa é uma instância de WebGLRenderingContext
.
Isso acontece porque o uso de gl
permite que os nomes de método se pareçam com as
contrapartes na API OpenGL ES 2.0, usada para criar RV em linguagens
compiladas. Isso é óbvio se você já criou apps de RV usando o OpenGL, mas
confuso se você não tem experiência com essa tecnologia.
Desenhar algo no framebuffer
Se você estiver se sentindo muito ambicioso, poderá usar o WebGL diretamente, mas não recomendo isso. É muito mais simples usar um dos frameworks listados acima.
Conclusão
Este não é o fim das atualizações ou artigos do WebXR. Você pode encontrar uma referência para todas as interfaces e membros do WebXR no MDN. Para conferir as próximas melhorias nas interfaces, siga os recursos individuais no Status do Chrome.
Foto de JESHOOTS.COM no Unsplash