WebXR基本概念和应用程序开发简介

techbrood 发表于 2019-09-02 18:31:12

标签: webxr, webar, webvr, webgl

- +

XR是VR,AR和MR的统称,VR,AR是从感官体验的角度来区分的,

VR是用户借助外设输入输出(头戴、手柄、体感、运动感知等软硬件系统)来和纯虚拟场景的交互体验,

AR也是用户借助外设来体验额外的虚拟内容,区别是虚拟内容是叠加在真实世界上,其方式可以是通过透射或者视频叠加。

WebXR是基于网页的XR应用程序,可以用来支持一些本地XR应用不那么适合的场景,

比如一些短小精干时效不长的营销推广页面、在线沉浸式视频、电商、在线小游戏和艺术创作等。


WebXR应用程序的生命周期一般如下:

  1. 查询系统是否支持所需的xr模式。

  2. 如果支持可用,应用程序会向用户公布xr功能。

  3. 从设备请求沉浸式xr会话以响应用户激活事件(选择进入xr模式)。

  4. 使用会话运行渲染循环,生成要在xr设备上显示的图形帧。

  5. 持续渲染直到用户选择退出xr模式。

  6. 结束xr会话。

查询XR能力

与xr设备的交互是通过XRSession接口完成的,但是在任何启用xr的页面请求会话之前,它应该首先查询以确定当前硬件和UA是否支持所需的xr内容类型。如果是,那么页面可以向用户公布xr功能。(例如,通过向页面添加一个按钮,用户可以单击该按钮启动xr内容。)

navigator.xr.supportsSession 函数用于检查设备是否支持应用程序所需要的xr功能,该xr功能通过一个模式字符串来描述(比如immersive-vr表示vr功能),该函数返回一个 Promise,该promise用来确定是否可以使用该模式成功创建XRSession 。如果不能,调用将被拒绝。

这种查询方式是必要的,因为它允许应用程序在请求 XRSession 之前检测哪些xr模式可用,而xr会话可能会启用xr设备传感器并开始展示,这可能会在某些系统上产生大量的电源或性能开销,并可能产生副作用,例如接管用户的屏幕、启动状态托盘,或终止其他应用程序对xr硬件的访问。调用 navigator.xr.supportsSession 不得干扰系统上任何正在运行的xr应用程序,也不得有任何用户可见的副作用。


可以请求两种xr模式:

内联会话(Inline):请求会话时的默认模式,但可以用“inline”枚举值显式指定。内联会话不能在xr设备上显示内容,但可以访问设备跟踪信息并使用它在页面上呈现内容。(这种技术,即渲染到页面的场景响应设备移动,有时被称为“magic window”模式。)实现WebXR设备API的UA必须确保可以创建内联会话,而不管xr设备是否存在,除非被页面功能策略阻止。

沉浸式会话(Immersive VR):“沉浸式虚拟现实”模式。沉浸式虚拟现实内容直接呈现给xr设备(例如:显示在虚拟现实头戴设备上)。沉浸式虚拟现实会话必须在用户激活事件中或在另一个明确指示允许沉浸式会话请求的回调中请求。

应该注意的是,沉浸式虚拟现实会话仍然可以通过全息透镜等透明显示器显示用户环境(这样就延伸为增强现实的内容)。用“沉浸式会话”来描述可能更合适。

下面是代码请求示例:

async function checkForXRSupport() {  
  // Check to see if there is an XR device available that supports immersive VR
  // presentation (for example: displaying in a headset). If the device has that
  // capability the page will want to add an "Enter VR" button to the page (similar to
  // a "Fullscreen" button) that starts the display of immersive VR content.
  navigator.xr.supportsSession('immersive-vr').then(() => {    
      var enterXrBtn = document.createElement("button");    
      enterXrBtn.innerHTML = "Enter VR";    
      enterXrBtn.addEventListener("click", beginXRSession);    
      document.body.appendChild(enterXrBtn);
  }).catch((reason) => {    
      console.log("Session not supported: " + reason);
  });
}

会话请求

在确认应用程序所需要的XR模式后,应用程序需要使用 navigator.xr.requestsession() 方法请求 XRSession 实例,以便与xr设备的显示或跟踪功能交互。如上述章节所述,这个请求是在用户主动点击“Enter VR”按钮激活xr模式后发起的:

function beginXRSession() {  
  // requestSession must be called within a user gesture event
  // like click or touch when requesting an immersive session.
  navigator.xr.requestSession('immersive-vr')
      .then(onSessionStarted)
      .catch(err => {        // May fail for a variety of reasons. Probably just want to
        // render the scene normally without any tracking at this point.
        window.requestAnimationFrame(onDrawFrame);
      });
}

上述代码中,requestSession方法返回一个在成功时解析为 XRSession 的承诺(promise)。除了 XRSessionMode 之外,开发人员还可以提供包含返回会话必需功能的 XRSessionInit 字典。

如果 supportsSession 确定了支持某给定模式,那么 requestSession 请求具有相同模式的会话理应也成功,除非特殊情况(例如在非用户主动激活情况下调用该请求)。

在整个UA中,每个XR硬件设备一次只允许一个沉浸式会话。如果UA已经有一个活跃或等待中的沉浸式会话请求,则必须拒绝新的请求。当沉浸式会话处于活动状态时,所有内联会话都将挂起。除非与另一个明确需要内联会话的选项配对,否则不需要在用户激活事件中创建内联会话。

会话启动后,必须进行一些设置以准备渲染。

  • 首先需要创建一个 XRReferenceSpace 来建立一个参考空间,在这个空间中可以定义 XRViewerPose 位姿数据;

  • 然后需要创建一个 XRWebGLLayer 并设置为 XRSession 的 renderState.baseLayer 。未来沉浸式会话有可能会支持多个渲染层;

  • 然后调用 XRSession.requestAnimationFrame 来启动渲染循环。

let xrSession = null;let xrReferenceSpace = null;function onSessionStarted(session) {  
  // Store the session for use later.
  xrSession = session;  xrSession.requestReferenceSpace('local')
  .then((referenceSpace) => {
    xrReferenceSpace = referenceSpace;
  })
  .then(setupWebGLLayer) // Create a compatible XRWebGLLayer
  .then(() => {    // Start the render loop
    xrSession.requestAnimationFrame(onDrawFrame);
  });
}

渲染层设置

渲染到设备上的内容是通过一个 XRWebGLLayer 来定义的。这通过 XRSession 的 updateRenderState() 函数来设置, updateRenderState() 函数接受一个包含许多和会话渲染相关选项的字典对象,包括 baseLayer。只有字典中定义的选项会被更新。

规范的未来扩展将定义新的层类型。例如:将添加一个新的层类型,以允许与添加到浏览器中的任何新图形API一起使用。在未来的API修订版中,还可能增加一种能力,即一次使用多个层,并由UA组合这些层。

为了让WebGL画布(canvas)与XRWebGLLayer一起使用,它的上下文(context)必须与xr设备兼容。对于不同的环境,这可能意味着不同的事情。例如,在台式计算机上,这可能意味着必须针对xr设备所连台式机的显卡创建上下文。但是,在大多数移动设备上,这不是一个问题,因此上下文将始终兼容。在任何情况下,webxr应用程序在与XRWebGLLayer一起使用之前必须采取措施确保webgl上下文兼容性。

在确保画布兼容性方面,应用程序可分为两大类:

XR增强(XR Enhanced):该应用程序可以利用XR硬件,但它被用作增强用户体验而不是体验的核心部分。大多数用户可能不会与应用程序的XR功能进行交互,因此要求他们在应用程序生命周期的早期做出以XR为中心的决策会让人感到困惑和不恰当。一个例子是具有嵌入式360照片库或视频的新闻网站。 (我们预计绝大多数早期WebXR内容都属于这一类。)

这种应用程序应该调用WebGLRenderingContextBasemakeXRCompatible()方法。这将在允许使用它的上下文中设置兼容性位。尝试使用它们创建XRWebGLLayer时,没有兼容性位的上下文将失败。如果上下文尚未与XR设备兼容,则上下文将丢失并尝试使用兼容的显卡重新创建自身。页面负责正确处理WebGL上下文丢失,并在响应中重新创建任何必要的WebGL资源。如果页面未处理上下文丢失,则makeXRCompatible()返回的promise将失败。promise也可能由于各种其他原因而失败,例如由不同的,不兼容的XR设备主动使用的上下文。

let glCanvas = document.createElement("canvas");
let gl = glCanvas.getContext("webgl");
function setupWebGLLayer() {  
  // Make sure the canvas context we want to use is compatible with the current xr device.
  return gl.makeXRCompatible().then(() => {    
    // The content that will be shown on the device is defined by the session's
    // baseLayer.
    xrSession.updateRenderState({ baseLayer: new XRWebGLLayer(xrSession, gl) });
  });
}

XR中心(XR Centric):应用程序的主要用途是显示xr内容,因此它不介意以xr为中心的方式初始化资源,这可能包括在应用程序启动时就要求用户选择头戴设备。这些类型的应用程序可以通过在webgl上下文创建参数中设置xrCompatible标志,来避免调用makeXRCompatible以及可能触发的上下文丢失。

let gl = glCanvas.getContext("webgl", { xrCompatible: true });

通过以上任一方法确保与xr设备的上下文兼容性可能会对页面中的其他图形资源产生副作用,例如导致整个UA从使用集成显卡切换到使用独立显卡来显示。

如果系统的底层XR设备发生更改(由navigator.xr 发出 devicechange 事件),则任何先前设置的上下文兼容性位都将被清除,并且在将上下文与XRWebGLLayer一起使用之前,需要再次调用makeXRCompatible。任何活动会话也将结束,因此需要创建新XRWebGLLayer相应的新XRSession

主渲染循环

WebXR设备API提供了有关通过XRFrame对象渲染的当前帧的信息,开发人员必须在渲染循环的每次迭代中检查该对象。从这个对象中,可以查询帧的XRViewerPose,它包含有关渲染所需视图的全部信息,以便场景在xr设备上正确显示。

XRWebGLLayer对象不会自动更新。要显示新帧,开发人员必须使用XRSessionrequestAnimationFrame()方法。运行requestAnimationFrame()回调函数时,会同时传递时间戳和XRFrame。它们将包含新的渲染数据,在回调中必须把这些数据绘制到XRWebGLLayer帧缓冲区(framebuffer)中。

对于每次requestAnimationFrame()回调或与跟踪数据关联的某些事件都需要创建新的XRFrameXRFrame对象充当XR设备状态和所有相关输入的快照。这些状态可以表示历史数据、当前传感器数据或未来的映射。由于XRFrame具有时间敏感性,因此它仅在执行传递给它的回调期间有效。一旦控制返回到浏览器后,任何活跃的XRFrame对象都将标记为不活跃。调用非活跃XRFrame的任何方法都将引发InvalidStateError错误。

在调用当前批处理中的requestAnimationFrame()回调之前,XRFrame还复制了一份XRSessionrenderState,例如 depthNear/Far 值和 baseLayer。当计算视图信息(如投影矩阵)和XR硬件合成帧时,将使用此捕获的renderState。在处理下一个XRFrame回调之前,开发人员对updateRenderState()的任何后续调用都不会应用。

XRFrame使用和window.requestAnimationFrame()回调相同的时间戳。这意味着时间戳是一个DOMHighResTimeStamp,设置为帧的回调开始处理时的当前时间。一个帧中的多个回调将接收相同的时间戳,即使在处理以前的回调期间已经过了一些时间。在将来,如果API需要提供额外的、特定于XR的计时信息,建议通过XRFrame对象。

XRWebGLLayer帧缓冲区由UA创建,其行为类似于画布(canvas)的默认帧缓冲区。使用framebufferTexture2DframebufferRenderbuffergetFramebufferAttachmentParametergetRenderbufferParameter都将产生一个INVALID_OPERATION无效操作)的错误。此外,在XRSessionrequestAnimationFrame()回调之外,帧缓冲区将被视为不完整,调用checkFramebufferStatus时返回FRAMEBUFFER_UNSUPPORTED(帧缓冲区不支持)。尝试绘制、清除或从中读取时,都会产生WebGL规范所定义的INVALID_FRAMEBUFFER_OPERATION(无效帧缓冲区操作)错误。

一旦绘制到帧缓冲区,XR设备将会持续显示XRWebGLLayer帧缓冲区的内容,有可能会重新投影以匹配头部运动,而不管页面是否继续处理新帧。未来的规范可能会启用其他类型的层,例如视频层,这些层可以自动同步到设备的刷新率。

观者跟踪

每个XRFrame场景都将从“观者”的角度来绘制,所谓“观者”就是查看场景的用户或设备,由XRViewerPose描述。开发人员通过在XRFrame上调用getViewerPose()返回当前XRViewerPose以及其所在的参考空间XRReferenceSpace。由于XR跟踪系统的特性,该函数不能确保一定返回值,开发人员需要做适当的处理。具体哪些情况将导致getViewerPose()失败以及处理这些情况的推荐做法,请参阅空间跟踪(Spatial Tracking)相关描述。

XRViewerPose包含一个views属性,它是一个XRView数组。每个XRView都有一个projectionMatrixtransform,用于WebGL渲染。XRView还被传递给XRWebGLLayergetViewport()方法,以确定渲染时WebGL视口(viewport)的设置。这可以确保将场景的适当视图渲染到XRWebGLLayer帧缓冲区的正确部分,以便在XR硬件上正确显示。

function onDrawFrame(timestamp, xrFrame) {  
  // Do we have an active session?
  if (xrSession) {    
      let glLayer = xrSession.renderState.baseLayer;    
      let pose = xrFrame.getViewerPose(xrReferenceSpace);    
      if (pose) {      
          // Run imaginary 3D engine's simulation to step forward physics, animations, etc.
          scene.updateScene(timestamp, xrFrame);      
          gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);      
          for (let view of pose.views) {        
              let viewport = glLayer.getViewport(view);        
              gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);        
              drawScene(view);
          }
     }    
     // Request the next animation callback
     xrSession.requestAnimationFrame(onDrawFrame);
  } else {    
     // No session available, so render a default mono view.
     gl.viewport(0, 0, glCanvas.width, glCanvas.height);    
     drawScene();    // Request the next window callback
     window.requestAnimationFrame(onDrawFrame);
  }
}

function drawScene(view) {  
    let viewMatrix = null;  
    let projectionMatrix = null;  
    if (view) {
        viewMatrix = view.transform.inverse.matrix;
        projectionMatrix = view.projectionMatrix;
    } else {
        viewMatrix = defaultViewMatrix;
        projectionMatrix = defaultProjectionMatrix;
    }  
    // Set uniforms as appropriate for shaders being used

    // Draw Scene
}

因为XRViewerPose继承自XRPose,所以它还包含一个transform,用来描述相对于XRReferenceSpace原点的整个观看者的位置和方向。这主要用于为旁观者视图或多用户环境来呈现(比如共享)当前用户的视觉呈现。

会话可见性

UA可以随时临时隐藏会话。隐藏会话时,会限制对XR设备状态的访问,并且不会处理渲染帧。隐藏的会话可能在某个时间点会再次可见,通常是在用户完成了触发会话隐藏的某个操作之后。

如果允许页面继续读取头戴位置可能存在安全或隐私风险(例如当用户使用虚拟键盘输入密码或url时,可能可以从头部运动推断出用户的输入),或者如果UA外部的内容遮挡了页面的输出,在这种情况下,UA可以隐藏会话。

在其他一些情况下,UA也可以选择保持会话内容可见但被“模糊”化,这表示会话内容仍然可见但不再在前景中。会话被模糊化时其刷新速度可能较慢,或者根本不刷新,从设备查询的姿势也可能不太准确,所有输入跟踪都将不可用。如果用户戴着头戴设备,则UA应呈现一个跟踪的环境(一个对用户头部运动保持响应的场景),或者在页面被限制时重新发送受限制的内容,以防止用户不适。

会话应在模糊时继续请求和绘制帧,但不应期待其以正常XR硬件设备帧速率来处理。UA可以将这些帧用作其跟踪环境或页面合成的一部分,尽管模糊会话产生的帧的准确表示在平台之间会有所不同。它们可能部分被遮挡,字面上模糊不清,变灰,或以其他方式被忽略。

应用程序可能希望通过停止游戏逻辑、隐藏内容或暂停媒体来响应会话隐藏或模糊事件。为此,应用程序应该监听来自XRSessionvisibilitychange事件。例如,一个360°媒体播放器可能会在视频被遮挡时暂停播放。

xrSession.addEventListener('visibilitychange', xrSessionEvent => {  
    switch (xrSessionEvent.session.visibilityState) {    
        case 'visible':      
            resumeMedia();      
            break;    
        case 'visible-blurred':      
            pauseMedia();      
            // Allow the render loop to keep running, but just keep rendering the last
            // frame. Render loop may not run at full framerate.
            break;    
        case 'hidden':      
            pauseMedia();      
            break;
  }
});


终止会话

当不再需要使用XRSession时,它会被“结束”。结束的会话对象将被分离,该对象上的所有操作都将失败。已结束的会话无法被还原,如果需要新的活动会话,则必须调用navigator.xr.requestSession()来重新请求。

要手动结束会话,应用程序可以调用XRSessionend()方法。这将返回一个promise对象,其解析结果表明该会话向XR硬件设备的渲染已停止。会话结束后,应用程序需要的任何后续动画都应使用window.requestAnimationFrame()来完成。

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);
}

会话结束的原因可能有很多种。例如:用户可以通过对UA的手势强制结束,其他本地应用程序可以独占地访问XR硬件设备,或者XR硬件设备可能与系统断开连接。此外,如果系统的底层XR设备更改(由navigator.xr对象上的devicechange事件发出信号),则任何活动的XR会话都将结束。这适用于沉浸式会话和内联会话。良好的应用程序应该监视XRSession上的结束事件,以检测UA何时强制会话结束。

xrSession.addEventListener('end', onSessionEnd);

如果UA需要暂时停止会话的使用,则应暂停会话而不是结束会话。

possitive(4) negative(0) views2279 comments0

发送私信

最新评论

请先 登录 再评论.
相关文章