在 Mozilla,一个小型侦察团队一直在尝试将网络的最佳功能,如互连性、无许可的内容创作以及远程代码的安全执行,与虚拟现实的沉浸式交互模式相结合。
从对 Oculus 的 DK2 头戴式显示器 的支持开始,我们已经让那些感兴趣的人能够开始尝试 VR。作为快速介绍,我想展示开发者在构建他们的第一个 VR 体验时需要考虑的渲染技术的一些差异。在这篇文章中,我们将重点介绍使用 WebGL API 集进行渲染的描述。
我在 WebVR 中的第一个渲染,斯坦福龙。Firefox 只需进入全屏模式,就会处理晕影效果、空间和色度失真。
同一场景的多个视图
需要做出的第一个重要区别:在显示器或屏幕上的传统观看中,我们将三维场景平铺到一个平面上(视窗)。虽然物体可能与视窗有不同的距离,但所有内容都是从一个视角渲染的。我们可能有多个绘图调用来构建我们的场景,但我们通常使用一个视图矩阵和一个投影矩阵来渲染所有内容,这些矩阵是在场景创建时计算的。
例如,视图矩阵可能包含有关虚拟相机相对于场景中所有其他物体的位置的信息,以及我们的方向(哪边是前方?哪边是上方?)。投影矩阵可能编码我们是否想要透视或正投影、视窗纵横比、我们的视场 (FOV) 以及绘制距离。
当我们从一个视角渲染场景到头戴式显示器 (HMD) 上渲染时,突然之间我们必须从两个不同的视角渲染所有内容两次!
过去,你可能使用过一个视图矩阵和一个投影矩阵,但现在你需要一对。与其选择视场 (FOV),你现在必须查询头戴式显示器用户的 FOV 设置,分别针对两只眼睛。任何最近去过眼科医生或做过眼科检查的人都可以证明,你的每只眼睛都有自己的 FOV!在渲染到远处的显示器时,这并不需要进行校正,因为显示器通常是当前视场中的一个子集,而头戴式显示器 (HMD) 包含整个视场 (FOV)。
Oculus SDK 具有一个配置实用程序,用户可以在其中设置每只眼睛的单独 FOV 和 瞳距 (IPD),基本上是两眼之间的距离,从瞳孔到瞳孔测量。
独特的视场为我们提供了两个独特的投影矩阵。因为你的眼睛也彼此偏移,所以它们也从观察者的位置具有不同的位置或平移。这给了我们两个不同的视图矩阵(每只眼睛一个)。重要的是要正确地获得这些矩阵,以便观察者的脑部能够正确地将两个不同的图像融合成一个。
如果不考虑 IPD 偏移,就无法实现正确的 视差 效果。视差对于区分不同物体的距离和深度感知非常重要。视差是当左右平移时,离你较远的物体比离你较近的物体移动速度更慢的现象。 Github 的 404 页面 是视差效果的一个很好的例子。
这就是为什么某些 360 度视频从每个方向的单个视图/镜头拍摄会导致前景物体与更远处的物体发生模糊。有关 360 度视频问题的更多信息, 这篇文章 是一个很好的参考。
我们还必须查询 HMD 以了解画布的大小应该设置为本机分辨率。
在渲染单眼视图时,我们可能会有这样的代码 -
function init () {
// using gl-matrix for linear algebra
var viewMatrix = mat4.lookAt(mat4.create(), eye, center, up);
var projectionMatrix = mat4.perspective(mat4.create(), fov, near, far);
var mvpMatrix = mat4.multiply(mat4.create(), projectionMatrix, viewMatrix);
gl.uniformMatrix4fv(uniforms.uMVPMatrixLocation, false, mvpMatrix);
};
function update (t) {
gl.clear(flags);
gl.drawElements(mode, count, type, offset);
requestAnimationFrame(update);
};
在 JS 中以及我们的 GLSL 顶点着色器中
uniform mat4 uMVPMatrix;
attribute vec4 aPosition;
void main () {
gl_Position = uMVPMatrix * aPosition;
}
...但在使用 webVR 从两个不同的视角渲染时,重新使用之前的着色器,我们的 JavaScript 代码可能更像
function init () {
// hypothetical function to get the list of
// attached HMD's and Position Sensors.
initHMD();
initModelMatrices();
};
function update () {
gl.clear(flags);
// hypothetical function that
// uses the webVR API's to update view matrices
// based on orientation provided by HMD's
// accelerometer, and position provided by the
// position sensor camera.
readFromHMDPS();
// left eye
gl.viewport(0, 0, canvas.width / 2, canvas.height);
mat4.multiply(mvpMatrix, leftEyeProjectionMatrix, leftEyeViewMatrix);
gl.uniformMatrix4fv(uniforms.uMVPMatrixLocation, false, mvpMatrix);
gl.drawElements(mode, count, type, offset);
// right eye
gl.viewport(canvas.width / 2, 0, canvas.width / 2, canvas.height);
mat4.multiply(mvpMatrix, rightEyeProjectionMatrix, rightEyeViewMatrix);
gl.uniformMatrix4fv(uniforms.uMVPMatrixLocation, false, mvpMatrix);
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(update);
};
在后续文章中,一旦 webVR API 有更多时间来完善,我们将介绍一些更具体的示例,并解释诸如四元数之类的概念!使用 WebGL2 的多个渲染目标(WebGL1 的 WEBGL_draw_buffers 扩展,目前 浏览器支持率低于 50%,更多 信息),或 WebGL2 的实例化(WebGL1 的 ANGLE_instanced_arrays 扩展,目前浏览器支持率为 89%)应该可以 不必显式调用 draw 两次。
有关渲染差异的更多信息,Oculus 文档 也是一个很好的参考。
90 赫兹刷新率和低延迟
在渲染时,我们显示更新和刷新显示器的速度受到硬件刷新率的限制。对于大多数显示器,此速率为 60 赫兹。这给了我们 16.66 毫秒的时间来绘制场景中的所有内容(减去浏览器合成器的一点时间)。requestAnimationFrame
将限制我们运行更新循环的速度,这可以防止我们进行不必要的额外工作。
Oculus DK2 的最大刷新率为 75 赫兹(每帧 13.33 毫秒),而目前计划于 2016 年第一季度发布的生产版本将具有 90 赫兹的刷新率(每帧 11.11 毫秒)。
所以,我们不仅需要从两个不同的视角渲染所有内容两次,而且我们只有三分之二的时间来完成它(16.66 毫秒 * 2 / 3 == 11.11)!虽然这似乎很困难,但通过各种技巧(降低场景复杂度、更小的渲染目标加上放大等),达到更低的帧时间是可行的。另一方面,减少硬件带来的延迟要困难得多!
我们不仅要关心帧速率,还要关心用户输入的延迟。实时渲染和预渲染之间的主要区别在于实时场景通常是动态生成的,通常使用观察者的输入。当用户移动头部或重新定位时,我们希望在他们移动时与他们看到移动结果显示在他们面前之间有一个紧密的反馈回路。这意味着我们希望更快地显示渲染结果,但随后我们就遇到了经典的双缓冲与 屏幕撕裂 问题。正如 Oculus 首席科学家 Michael Abrash 指出,我们希望用户交互与反馈呈现之间低于 20 毫秒的延迟。
当前台式机,更不用说移动设备,的图形硬件是否能胜任这项任务,还有待观察!
要获取更多信息或参与 WebVR
* MozVR 下载页面(在 Firefox 中使用 WebVR 所需的一切)
* WebVR 规范(正在变化中,可能会更改,有些东西 会 发生故障。)
* MDN 文档(正在进行中,将在规范更新时更改)
* web-vr-dicuss 公共邮件列表
* /r/webvr 子reddit