WebGL Device API 实现画面渲染到VR设备

956 阅读10分钟

基本流程

  1. 检测当前环境释放支持目标模式
  2. 创建按钮,在用户交互之后,进入VR
  3. 创建XR Session
  4. 创建XR 兼容的WebGL Context
  5. 为XR Session设置渲染层
  6. 请求目标模式的 XRReferenceSpace
  7. 在session的requestAnimationFrame回调中循环渲染新的一帧画面

session的requestAnimationFrame回调中流程

  1. requestAnimationFrame的第二个参数为当前帧,从中获取 XR session
  2. 调用XRFrame 的getViewerPose,基于XRReferenceSpace获取当前 XRFrame 时间点, XR 设备关联的referenceSpace 中观察者的空间姿势信息XRViewerPose
  3. 从XR Session的renderState中获取baseLayer 渲染层,这是 XR 合成器获取图片的地方 5.绑定帧缓冲区,将画面渲染到 XRSession 渲染层的 framebuffer 中。 FrameBuffer的作用是存储WebGL绘制的内容
  4. 从获取的姿势信息中,找到views:XRView[],(XRView 表示单个视口,XR 设备向用户呈现的图像),循环遍历左右眼视口
  5. 根据XRView设置WebGL的XRViewport
  6. 其他内容渲染
  7. session.requestAnimationFrame调用回调函数自身

基本术语

XRSystem

navigator.xr 是 WebXR 的入口,它是一个 XRSystem 对象,它只有两个方法,isSessionSupported 检查目标模式是否支持和 requestSession 请求目标模式会话。模式分为 ar 和 vr。

XRSession

XRSession 表示一个 XR 会话,与 XR 设备互动就是通过该对象。 有了 XRSession 后,需要给它设置一个渲染层,后续渲染的画面会渲染到该渲染层上. 和创建 WebGL 上下文一样,这里通过 canvas 元素的 getContext 方法获取, 唯一不同的是需要传入 xrCompatible 参数,让 GL 上下文由 XR 适配器创建,这样才能与 XR 兼容。 请求session之后,必须更新会话的渲染层,后续渲染会渲染在该层上

属性

  1. renderState: XRRenderState,内含常用属性baseLayer,renderState 属性是 XRSession 可配置渲染参数的值,例如配置远或近的深度、FOV 等属性
  2. inputSources: XRInputSourceArray表示输入源

方法

  1. requestAnimationFrame,第二个参数frame:XRFrame,为当前帧的快照信息,这个方法用于渲染画面到 VR 设备。requestAnimationFrame 方法中需要产生新的帧给用户
  2. cancelAnimationFrame
  3. end 结束会话
  4. updateRenderState
  5. requestReferenceSpace

XRWebGLLayer

XRWebGLLayer 提供用于渲染的 WebGL framerbuffer,并且启用 XR 设备硬件加速 3D 渲染。XRWebGLLayer 对象不会自动更新。要呈现新帧,开发人员必须使用 XRSession 的 requestAnimationFrame() 方法。

属性

  1. framebuffer 属性表示是最终画面要渲染到的地方。

    WebGL中的framebuffer是一种可编程的图像渲染管线,它允许开发者将渲染的结果存储在内存中的一个缓冲区中,而不是直接输出到屏幕上。它的主要作用是在屏幕上绘制出复杂的3D场景时提高性能。

    在WebGL中,framebuffer是在GPU中创建和管理的。Framebuffer对象是WebGL的一种特殊类型的对象,用于存储渲染结果并将其呈现到屏幕上。GPU能够处理图形数据的速度比CPU快,因此将Framebuffer放在GPU中可以提高渲染效率。

    具体来说,framebuffer可以将渲染结果存储在一个纹理对象中,然后再将纹理对象绘制到屏幕上。这种方式可以有效地减少GPU与CPU之间的通信量,从而提高渲染性能。

    此外,framebuffer还可以用于实现一些高级的图像效果,例如屏幕空间环境光遮蔽(SSAO)、深度场景的后处理、抗锯齿(AA)等等。

    总之,framebuffer在WebGL中扮演着非常重要的角色,它能够提高渲染性能和实现各种复杂的图像效果,使得WebGL可以用来实现更加逼真的3D场景和交互式应用程序。

    关于framebuffer的更多信息,可参考这篇文章

方法

  1. getViewPort 根据XRView获取XRViewport,函数签名如下:
XRViewport? getViewport(XRView view);

XRReferenceSpace

XRReferenceSpace 对象,主要用于跟踪空间信息。XRReferenceSpace 继承于 XRSpace(空对象) 用于关联用户的物理空间,XRReferenceSpace 中的坐标系与 WebGL 中一致,+X 向右,+Y 向上,+Z 向后。

要想在空间追踪和场景几何方面发挥作用,你需要能够将XR设备的感知位置与空间的坐标系联系起来。这就是参考空间的作用。

方法

  1. getOffsetReferenceSpace

在WebXR API中,使用 getOffsetReferenceSpace 方法可以将现有参考空间的位置和方向做一定的偏移。偏移量由传入方法的变换矩阵(transform matrix)决定。这个偏移操作可以用来实现一些特定的应用场景,比如将参考空间的原点移动到不同的位置,或者改变参考空间的朝向。

对于类型为 local-floor 的参考空间,其定义为“相对于地面的本地坐标系”。也就是说,该参考空间的原点位置通常是接近于地面的,其高度通常被定义为零。因此,如果您使用 getOffsetReferenceSpace 方法来将参考空间的位置偏移一定的距离,那么参考空间的高度就会相应地发生变化。这就是为什么在您的情况下,参考空间的高度发生了变化的原因。

需要注意的是,如果您使用的变换矩阵并没有明确指定高度的偏移量,那么参考空间的高度可能会因为四舍五入或其他的计算方式而略微发生变化。如果您需要精确控制参考空间的高度,可以使用其他的变换矩阵来实现。

下面的代码演示了基于getOffsetReferenceSpace进行空间偏移的基本用法:

  useFrame((state: RootState, delta: number, frame?: THREE.XRFrame) => {
    if (flag.current && frame) {
      const baseReferenceSpace: XRReferenceSpace = gl.xr.getReferenceSpace();
      const viewerPose = frame?.getViewerPose(baseReferenceSpace);
      const newReferenceSpace: XRReferenceSpace =
        baseReferenceSpace.getOffsetReferenceSpace(viewerPose.transform);
      gl.xr.setReferenceSpace(newReferenceSpace);
      gl.xr.setReferenceSpaceType('local-floor');
      flag.current = false;
    }
  });

XRFrame

XRFrame 表示 XRSession 在给定时间点所有被跟踪状态的快照。

方法

  1. getViewerPose ,方法返回当前 XRFrame 时间点, XR 设备关联的referenceSpace 中观察者的空间姿势信息。 函数签名如下:
XRViewerPose? getViewerPose(XRReferenceSpace referenceSpace);

结构如下图

image.png

XRViewerPose

XRViewerPose 继承于 XRPose,描述用户在跟踪的 XR 场景中的状态 XRViewerPose 包含一个 views 属性,它是一个 XRViews 数组。每个 XRView 都有一个 projectionMatrix 和 transform,应该在使用 WebGL 渲染时使用。

因为XRViewerPose 继承自XRPose,它还包含一个transform,描述了相对于XRReferenceSpace 原点的整体位置和方向。

这主要用于为观众视图或多用户环境渲染查看器的视觉表示。

XRView

XRView 表示单个视口,XR 设备向用户呈现的图像。XRView 也被传递给 XRWebGLLayer 的 getViewport() 方法,以确定在渲染时 WebGL 视口应该设置为什么。这确保了场景的适当透视被渲染到“XRWebGLLayer”的“帧缓冲区”上的正确部分,以便在 XR 硬件上正确显示。

每个 XRView 的每个 transform 属性都是一个由 position 和 orientation 组成的 XRRigidTransform。这些应该被视为场景中虚拟“相机”的位置。

XRViewport

XRViewport 用于表示单个 XRView 表示的视口。

WebGLRenderingContext

为 HTML 元素的绘图表面提供了一个到 OpenGL ES 2.0 图形渲染上下文的接口canvas

const canvas = document.getElementById('myCanvas');
const gl = canvas.getContext('webgl');

一旦拥有画布的 WebGL 渲染上下文,就可以在其中进行渲染。如果您需要 WebGL 2.0 上下文,请参阅WebGL2RenderingContext;这提供了对 OpenGL ES 3.0 图形实现的访问。

方法

bindFramebuffer

在WebGL中,bindFramebuffer() 方法用于将渲染目标缓冲区绑定到WebGL上下文中的帧缓冲区对象。帧缓冲区对象是一个可用于存储渲染结果的内存区域,其中包括颜色缓冲区,深度缓冲区,以及模板缓冲区。

通过bindFramebuffer() 方法将帧缓冲区对象绑定到WebGL上下文中之后,所有的渲染操作都将会被写入到该帧缓冲区对象中。这使得开发者可以自定义渲染目标,例如在离屏的帧缓冲区上进行渲染,而不是直接渲染到屏幕上。这种技术在一些高级的图形技术中是非常常见的,例如实现后期处理效果、抗锯齿等等。

bindFramebuffer() 方法的语法如下:

void gl.bindFramebuffer(target, framebuffer);

其中,target 参数指定了帧缓冲区的绑定目标,可以为以下两个值之一:

  • gl.FRAMEBUFFER: 表示帧缓冲区对象本身。
  • gl.DRAW_FRAMEBUFFERgl.READ_FRAMEBUFFER: 表示绘制或读取帧缓冲区。

framebuffer 参数则指定了要绑定的帧缓冲区对象的句柄。如果该参数为 null,则表示将默认的帧缓冲区对象绑定到WebGL上下文中。

XRRigidTransform

偏移基准空间,是WebXR API表示三维几何由位置和方向的变换所描述的接口。 XRRigidTransform 用于指定整个 WebXR API 的转换,包括:

在这些地方使用 XRRigidTransform 而不是提供矩阵数据的裸数组具有优势。它会自动计算变换的逆,甚至缓存它,从而显著加快后续请求。

完整代码

let XRSession,XRReferenceSpace,gl;
// navigator.xr 是 WebXR 的入口,它是一个 XRSystem 对象,
// 它只有两个方法,isSessionSupported 检查目标模式是否支持和 requestSession 请求目标模式会话。
// 模式分为 ar 和 vr。
navigator.xr.addEventListener('devicechange',checkForXRSupport)
function checkForXRSupport(){
  const supported = navigator.xr.isSessionSupported('immersive-vr');
  if(supported){
    const button = document.createElement("button");
    button.textContent = 'enter VR';
    // 不能直接进入 VR 会话,需要在用户交互的回调函数中请求进入,类似于音频的自动播放限制。
    button.addEventListener("click",onClickEnterVRBtn);
    document.body.appendChild(button);
  }else{
    console.log('not support immersive-vr')
  }
}
async function onClickEnterVRBtn(){
  if(!XRSession){
    // 通过 requestSession 方法请求目标模式的 XRSession
    // XRSession 表示一个 XR 会话,与 XR 设备互动就是通过该对象
    XRSession =  await navigator.xr.requestSession('immersive-vr');
    onSessionStart();
  }else{
    endXRSession();
  }
}
function onSessionStart(){
  const canvas = document.createElement('canvas');
  gl = canvas.getContext('webgl',{ XRCompatible: true});
  // 有了 XRSession 后,需要给它设置一个渲染层,后续渲染的画面会渲染到该渲染层上.
  // 和创建 WebGL 上下文一样,这里通过 canvas 元素的 getContext 方法获取,
  // 唯一不同的是需要传入 xrCompatible 参数,让 GL 上下文由 XR 适配器创建,这样才能与 XR 兼容。
  // 更新会话的渲染层,后续渲染会渲染在该层上
  // renderState 属性是 XRSession 可配置渲染参数的值,例如配置远或近的深度、FOV 等属性
  // XRWebGLLayer 提供用于渲染的 WebGL framerbuffer,并且启用 XR 设备硬件加速 3D 渲染。
  XRSession.updateRenderState({ webGlLPlayer: new XRWebGLLayer(XRSession,gl)});
  // XRReferenceSpace 对象,主要用于跟踪空间信息
  // XRReferenceSpace 继承于 XRSpace(空对象) 用于关联用户的物理空间,XRReferenceSpace 中的坐标系与 WebGL 中一致,+X 向右,+Y 向上,+Z 向后。
  XRReferenceSpace = XRSession.requestReferenceSpace("local");
  // 利用 XRSession 上的 requestAnimationFrame 方法来渲染画面到 VR 设备。requestAnimationFrame 方法中需要产生新的帧给用户
  XRSession.requestAnimationFrame(onXRFrame);
}
// frame是XRFrame 类型的参数,上面保存了这一帧的信息
function onXRFrame(timestamp,frame){
  const session = frame.session;
  // getViewerPose 方法返回当前 XRFrame 时间点, XR 设备关联的referenceSpace 中观察者的空间姿势信息。
  // XRViewerPose 继承于 XRPose,描述用户在跟踪的 XR 场景中的状态
  const pose:XRViewerPose = frame.getViewerPose(XRReferenceSpace);
  if(pose){
    // 找到绘制内容的层
    // baseLayer 渲染层,是 XR 合成器获取图片的地方
    const glLayer = session.renderState.baseLayer;

    // 绑定 framebuffer,需要将画面渲染到 XRSession 渲染层的 framebuffer 中
    // FrameBuffer的作用是存储WebGL绘制的内容
    // bindFramebuffer方法用于将渲染目标缓冲区绑定到WebGL上下文中的帧缓冲区对象
    gl.bindFramebuffer(gl.FRAMEBUFFER,glLayer.framebuffer);
    // 随着时间变化清除色
    gl.clearColor(Math.cos(timestamp/2000),Math.cos(timestamp/4000),Math.cos(timestamp/6000),1.0);
    gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
    // views:表示左眼和右眼
    // XRViewerPose只比XRPose多出一个 views:Array<XRView> 属性,它表示用户左眼或右眼看到的场景,必须每个 XRView 才能在 XR 设备上正确展示场景。
    for(let view of pose.views){
      // 下面的参数view:XRView 表示单个视口,XR 设备向用户呈现的图像。
      let viewport:XRViewport = glLayer.getViewPort(view);
      // 设置 WebGL 的 viewport
      gl.viewport(viewport.x,viewport.y,viewport.width,viewport.height);
      // ...
    }

  }
  session.requestAnimationFrame(onXRFrame);
}

function endXRSession() {
  // Do we have an active session?
  if (XRSession) {
    // End the XR session now.
    XRSession.end().then(onSessionEnd);
  }
}

// Restore the page to normal after an immersive session has ended.
function onSessionEnd() {
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);

  XRSession = null;

  // Ending the session stops executing callbacks passed to the XRSession's
  // requestAnimationFrame(). To continue rendering, use the window's
  // requestAnimationFrame() function.
  window.requestAnimationFrame(onDrawFrame);
}