2019,不管是不是VR元年,VR行业确实在这一年势头凶猛,VR设备跨越式发展,然而VR内容确相对滞后。强调独占性的各大VR内容平台,更是将开发商分割成了不同的阵营。这项新的设备媒介同时驱动了浏览器开发商对Web进行虚拟现实的支持,WebVR的发展正集中于不可思议的视觉体验以及创建在线虚拟现实环境的工具之上。
WebVR 1.0 API 提议简介
Mozilla VR团队正致力于在浏览器中支持在线创作及显示VR内容,并已获得重大突破。通过与Google Chrome团队[Brandon Jones]的密切合作, Mozilla团队发布了WebVR API 1.0版本。
本文主要讨论的是本版本API的基本使用方式,这需要读者能够理解一些复杂的概念,例如:
【数学中的矩阵】
( https://en.wikipedia.org/wiki/Matrix_%28mathematics%29 )
此外,您也可以通过阅读 [A-Frame]( https://aframe.io/ )
或者
[WebVR boilerplate]( https://github.com/borismus/webvr-boilerplate/ )以快速入门WebVR。
如何开始实施WebVR
想要从现在开始吗?目前,开发者可以通过使用Brandon Jone的
[实验版Chromium]
( https://drive.google.com/a/mozilla.com/folderview?id=0BzudLt22BqGRbW9WTHMtOWMzNjQ&usp=sharing#list )来做一些验证性的实验。
[three.js] (http://threejs.org/ )
[WebVR Polyfill]( https://github.com/borismus/webvr-polyfill/ )
VR体验的构成要素
让我们了解一下VR体验的关键构成要素:
1. 我们所渲染的虚拟现实显示的内容。
2. 用户姿态,头盔在空间中的位置及方向。
3. 眼部参数定义的视距立体间隔或立体视觉(stereo separation)以及视野范围。
现在让我们看一下使内容呈现在VR头盔中的执行顺序:
1. 使用`navigator.getVRDisplays()` 来检索VR设备。
2. 创建一个canvas元素来渲染显示内容。
3. 在canvas元素中,使用`VRDisplay.requestPresent()`。
4. 创建一个VR设备特定的动画循环,以执行内容渲染。
1)使用`VRDisplay.getPose()` 更新用户动作。
2)进行计算和渲染。
3)使用 `VRDisplay.submitFrame()` 来告诉合成器,canvas元素准备好在VR设备显示的时间。
下述部分描述了每一个行为的具体细节。
关于VR显示设备
VR显示设备对于显示有着许多非常特殊的需求,比如:
[帧率(frame rate)]( https://en.wikipedia.org/wiki/Frame_rate ),
[视野范围(field of view)]
( https://en.wikipedia.org/wiki/Field_of_view ),以及从标准桌面显示中,拆分独立处理的内容表现方式。
检索VR显示设备
为了使得使用浏览器进行VR设备检索成为可能,可以使用`navigator.getVRDisplays()`方法,该方法返回一个由包含一系列`VRDisplay`对象的数组构成的约定(Promise):
…
navigator.getVRDisplays().then(function (displays) {
if (!displays.length) {
// 支持WebVR,没有发现任何VR显示设备。
return;
}
//处理 VRDisplay 对象 (作为全局变量)。
vrDisplay = displays.length[0];
}).catch(function (err) {
console.error('Could not get VRDisplays', err.stack);
});
…
注意:
* 您必须在对VR设备进行检索之前,保证您的VR头盔已经接入并开启。
* 如果您没有VR头盔,你可以通过将`about:config`打开以及设置`dom.vr.cardboard.enabled`的值为`true`,来进行模拟仿真。
* [Firefox Nightly for Android]( https://nightly.mozilla.org/#Android )的用户或者 [Firefox for iOS]( https://www.mozilla.org/en-US/firefox/ios/ )的用户可以通过使用
[Google Cardboard]( https://www.google.com/get/cardboard/ )进行CardBorad设备的检索。
创建渲染对象
为确定渲染对象的大小(比如,你的canvas大小),请创建一个能够覆盖双眼视野范围的渲染对象。为确定每只眼睛的视野范围(以像素为单位),参考以下代码:
…
// 使用 'left'(左) 或 'right'(右).
var eyeParameter = vrDisplay.getEyeParameters('left');
var width = eyeParameter.renderWidth;
var height = eyeParameter.renderHeight;
…
将内容显示在VR头盔中
为了让内容呈现在头盔中,你需要使用`VRDisplay.requestPresent()`方法,该方法使用了WebGL元素作为参数,表示要显示的可视面。
为了防止API被冗余调用,浏览器需要一个用户初始化事件,确保用户是第一次进入VR模式。换句话说,用户必须选择开启VR,所以我们将“进入VR”设定成一个按钮作为`click`事件触发。
…
// 选择 WebGL canvas元素。
var webglCanvas = document.querySelector('#webglcanvas');
var enterVRBtn = document.querySelector('#entervr');
enterVRBtn.addEventListener('click', function () {
// 将当前的WebGL canvas设置为VR显示。
vrDisplay.requestPresent({source: webglCanvas});
});
// 退出当前显示。
vrDisplay.exitPresent();
…
设备特定的 `requestAnimationFrame`
既然我们建立了渲染对象,以及必要的渲染所需参数使得内容可以在头盔中正确地显示出来,我们现在可以创建一个场景的循环渲染。
我们希望通过使用回调方法`VRDisplay.requestAnimationFrame`以此来优化VR显示设备的刷新率:
…
var id = vrDisplay.requestAnimationFrame(onAnimationFrame);
function onAnimationFrame () {
// 循环渲染。
id = vrDisplay.requestAnimationFrame(onAnimationFrame);
}
// 结束渲染循环。
vrDisplay.cancelRequestAnimationFrame(id);
…
这与你所熟悉的标准回调函数 `window.requestAnimationFrame()`的用法是一致的。我们使用这个回调函数以适应显示内容中动作的位置和方向的不断更新,并对VR显示进行渲染。
从VR显示中捕获动作姿态信息
我们需要通过使用`VRDisplay.getPose()`方法来捕获头盔的位置和方向:
…
var pose = vrDisplay.getPose();
// 返回一个四元组。
var orientation = pose.orientation;
// 返回一个三维绝对位置向量。
var position = pose.position;
…
请注意:
* 如果无法确定位置及方向,这些信息将返回`null`。
更多详情请参考 [VRStageCapabilities]
( https://mozvr.github.io/webvr-spec/#interface-vrdisplaycapabilities )以及 [VRPose]( https://mozvr.github.io/webvr-spec/#interface-vrpose )。
投射场景到VR显示
为了使头盔中的场景准确的进行立体渲染,我们需要提供一些眼部参数,例如眼部偏移量(基于 [瞳距(interpupillary distance or IPD)]
( https://en.wikipedia.org/wiki/Interpupillary_distance ),以及视野范围(FOV)。
…
// 用左右眼参数之一作为参数传入.
var eyeParameters = vrDisplay.getEyeParameters('left');
// 根据VRPose翻译完世界坐标之后,继续减去眼部偏移量进行变换
var eyeOffset = eyeParameters.offset;
// 使用映射矩阵进行映射。
var eyeMatrix = makeProjectionMatrix(vrDisplay, eyeParameters);
// 将eyeMatrix显示到你的view中。
…
/**
* 生成映射矩阵
* @param {object} display - VRDisplay
* @param {number} eye - VREyeParameters
* @returns {Float32Array} 4×4 映射矩阵
*/
function makeProjectionMatrix (display, eye) {
var d2r = Math.PI / 180.0;
var upTan = Math.tan(eye.fieldOfView.upDegrees * d2r);
var downTan = Math.tan(eye.fieldOfView.leftDegrees * d2r);
var rightTan = Math.tan(eye.fieldOfView.rightDegrees * d2r);
var leftTan = Math.tan(eye.fieldOfView.leftDegrees * d2r);
var xScale = 2.0 / (leftTan + rightTan);
var yScale = 2.0 / (upTan + downTan);
var out = new Float32Array(16);
out[0] = xScale;
out[1] = 0.0;
out[2] = 0.0;
out[3] = 0.0;
out[4] = 0.0;
out[5] = yScale;
out[6] = 0.0;
out[7] = 0.0;
out[8] = -((leftTan - rightTan) * xScale * 0.5);
out[9] = (upTan - downTan) * yScale * 0.5;
out[10] = -(display.depthNear + display.depthFar) / (display.depthFar - display.depthNear);
out[12] = 0.0;
out[13] = 0.0;
out[14] = -(2.0 * display.depthFar * display.depthNear) / (display.depthFar - display.depthNear);
out[15] = 0.0;
return out;
}
…
向头盔提交帧
VR对于降低用户运动产生的不连续性做了一定的优化。这对舒服的体验(不使人眩晕)至关重要。直接使用`VRDisplay.getPose()` 和 `VRDisplay.submitFrame()`方法来对这一现象进行控制:
…
// 渲染和计算不依赖于姿态。
// ...
var pose = vrDisplay.getPose();
// 在此将你生成的眼部矩阵适配到view中。
// 尽量在此处减少操作数量。
// ...
vrDisplay.submitFrame(pose);
//在提交之后对帧作出的任何操作不会给VR带来延迟。此时可以渲染其他的view。
// ...
…
一般而言,正确的姿势是,尽可能早的调用`VRDisplay.submitFrame()`方法,并尽可能晚地调用`VRDisplay.getPose()`方法。
### 示例Demo
https://toji.github.io/webvr-samples/