Tudo sobre o loop de frames
Recentemente, publiquei o artigo A realidade virtual chega à Web, que apresentou os conceitos básicos da API WebXR Device. Também forneci instruções para solicitar, iniciar e encerrar uma sessão de XR.
Este artigo descreve o loop de frames, que é um loop infinito controlado pelo user agent em que o conteúdo é desenhado 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 é
O WebGL e o WebGL2 são os únicos meios de renderizar conteúdo durante um loop de frames em um app WebXR. Felizmente, muitos frameworks fornecem uma camada de abstração sobre o WebGL e o WebGL2. Esses frameworks incluem o three.js, babylonjs e o PlayCanvas, enquanto o A-Frame e o React 360 foram projetados para interagir com o WebXR.
Este artigo explica os conceitos básicos de um loop de frames usando o exemplo de sessão de RV imersiva do Immersive Web Working Group's (demonstração, origem). Se você quiser se aprofundar no WebGL ou em um dos frameworks, há uma lista crescente de recursos on-line.
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ó são nomeados 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, o que estou chamando de "o 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 espectador e do dispositivo de entrada 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 de tempo para explicar. Eu os abordo em detalhes em Realidade
aumentada. O exemplo que estou usando como base para este
artigo usa um 'local' espaço de referência, 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 exata 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 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 adapta melhor ao 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. XRView objetos são retornados em uma matriz de
o XRViewerPose objeto. 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 normalmente têm duas visualizações, uma para cada olho.
XRWebGLLayer
As camadas fornecem uma origem de imagens de bitmap e descrições de como essas imagens serão renderizadas no dispositivo. Essa descrição não captura o que esse jogador faz. Passei a pensar nele como um intermediário entre um dispositivo e um WebGLRenderingContext. O MDN tem uma visão muito parecida, afirmando que ele "fornece uma vinculação" entre os dois. Assim, ele fornece acesso aos outros jogadores.
Em geral, os objetos 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 recuperá-lo do XRWebGLLayer, você o transmite para o WebGLRenderingContext atual. Além de chamar bindFramebuffer() (mais sobre isso mais tarde), você nunca vai 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.
XRWebGLLayer e WebGLRenderingContext
O jogo
Agora que sabemos quem são os jogadores, 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 aplicativos de RV, os frames por segundo podem variar de 60 a 144. A RA para Android é executada a 30 frames por segundo. Seu código não deve presumir nenhuma taxa de frames específica.
O processo básico para o loop de frames é assim:
- Chame
XRSession.requestAnimationFrame(). Em resposta, o user agent invoca oXRFrameRequestCallback, que é definido por você. - Dentro da função de callback:
- Chame
XRSession.requestAnimationFrame()novamente. - Receba a pose do espectador.
- Transmita ("vincule") o
WebGLFramebufferdoXRWebGLLayeraoWebGLRenderingContext. - Itere sobre cada objeto
XRView, recuperando oXRViewportdoXRWebGLLayere transmitindo-o aoWebGLRenderingContext. - Desenhe algo no framebuffer.
- Chame
Como as etapas 1 e 2a foram abordadas no artigo anterior, vou começar na etapa 2b.
Receba a pose do 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. Eu recebo a pose do espectador chamando 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 ao entrar na sessão atual. Como você deve se 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.
A pose informa ao aplicativo onde o espectador está. A renderização real da imagem usa objetos XRView, que vou abordar em breve.
Antes de continuar, testo 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 XR de saber onde ele e os dispositivos de entrada estão em relação ao ambiente. O rastreamento pode ser perdido de várias maneiras e varia dependendo do método usado para rastreamento. Por exemplo, se as câmeras no headset ou no smartphone forem usadas para rastrear o dispositivo, ele poderá perder a capacidade de determinar onde 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 um aviso de permissão. O navegador poderá parar de fornecer poses ao aplicativo enquanto isso estiver acontecendo. Mas já chamei XRSession.requestAnimationFrame() para que, se o sistema puder se recuperar, o loop de frames continue. Caso contrário, o user agent vai encerrar a sessão e chamar o manipulador 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 instruí que ela criasse um contexto de renderização 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 ao objeto de sessão com updateRenderState(), além de 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)
});
Transmita ("vincule") 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
}
}
Itere sobre cada objeto XRView
Depois de receber a pose e vincular o framebuffer, é hora de receber as janelas de visualização. O XRViewerPose contém uma matriz de interfaces XRView, cada uma representando uma tela ou uma parte de uma tela. Elas contêm informações necessárias para renderizar conteúdo posicionado corretamente para o dispositivo e o espectador, 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 eu faço um loop e desenho uma imagem separada para cada uma.
Ao implementar a 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, 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
}
}
}
Transmita 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 para meu dispositivo. Assim como o 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, tive um debate com alguns colegas sobre a nomenclatura do objeto webGLRenContext. Os scripts de exemplo e a maioria do código WebXR chamam essa variável de gl. Quando estava trabalhando para entender os exemplos, eu sempre me esquecia do que gl se referia. Eu o chamei de webGLRenContext para lembrar você enquanto aprende que essa é uma instância de WebGLRenderingContext.
O motivo é que o uso de gl permite que os nomes dos métodos pareçam seus equivalentes na API OpenGL ES 2.0, usada para criar RV em linguagens compiladas. Esse fato é óbvio se você já escreveu apps de RV usando o OpenGL, mas confuso se você é completamente novo nessa tecnologia.
Desenhe 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 na parte de cima.
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 melhorias futuras nas próprias interfaces, siga recursos individuais no Chrome Status.