if (navigator.xr) {
// 1. 检查是否支持 immersive-vr 模式
navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
if (supported) {
const btn = document.createElement('button')
btn.textContent = '进入 VR'
btn.onclick = onBtnClick
document.body.appendChild(btn)
}
});
}
let gl
function onBtnClick() {
navigator.xr.requestSession('immersive-vr').then(session => {
// 2. 请求 VR 会话
const canvas = document.createElement('canvas');
gl = canvas.getContext('webgl', { xrCompatible: true });
// 3. 与创建普通 WebGL 不同,这里需要设置 xrCompatible 参数
session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl) });
// 更新会话的渲染层,后续渲染会渲染在该层上
session.requestAnimationFrame(onXRFrame);
})
}
function onXRFrame(time, frame) {
const session = frame.session;
// 4. 这个 session 是上面请求的 session
// 需要使用 session 上的 requestAnimationFrame
// 而不是 window 上的
session.requestAnimationFrame(onXRFrame);
const glLayer = session.renderState.baseLayer;
// 绑定 framebuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
// 随着时间变化清除色
gl.clearColor(Math.cos(time / 2000),
Math.cos(time / 4000),
Math.cos(time / 6000), 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
navigator.xr
是 WebXR 的入口,它是一个 XRSystem 对象,它只有两个方法,isSessionSupported
检查目标模式是否支持和 requestSession
请求目标模式会话。模式分为 ar 和 vr。isSessionSupported
方法检测是否支持目标模式,如果支持就可以提示用户可以进入 VR 模式。这里不能直接进入 VR 会话,需要在用户交互的回调函数中请求进入,类似于音频的自动播放限制。requestSession
方法请求目标模式的 XRSession,有了 XRSession 后,需要给它设置一个渲染层,后续渲染的画面会渲染到该渲染层上,和创建 WebGL 上下文一样,这里通过 canvas
元素的 getContext
方法获取,唯一不同的是需要传入 xrCompatible
参数,让 GL 上下文由 XR 适配器创建,这样才能与 XR 兼容。requestAnimationFrame
方法来渲染画面到 VR 设备。与 window.requestAnimationFrame
类似,不过它多接收一个 XRFrame 参数,上面保存了这一帧的信息,接下来渲染和 WebGL 中是一样的,不过需要将画面渲染到 XRSession 渲染层的 fr
amebuffer
中。navigator.xr
访问到 XRSystem 对象。该对象上面只有两个方法和一个事件,其签名如下所示。[interface XRSystem : EventTarget { ]
// Methods
Promise<boolean> isSessionSupported(XRSessionMode mode);
[Promise<XRSession> requestSession(XRSessionMode mode, optional XRSessionInit options = {}); ]
// Events
attribute EventHandler ondevicechange;
};
inline
渲染画面到页面上,就和使用普通 WebGL 渲染是一样的,浏览器应该支持该模式immersive-vr
渲染到 VR 设备immersive-ar
渲染到 AR 设备,该模式定义在 WebXR Augmented Reality Module 中isSessionSupported
方法用于查询给定模式在当前环境下是否支持,该方法并不能给出 100% 的检测结果,但是它会非常快速,也不会激活 VR 设备。requestSession
该方法请求创建 XRSession 并进入指定模式的会话,后面所有的渲染,用户位置信息等都是基于该 XRSession 对象,它的第一个参数是会话模式字符串,第二个参数是可选的功能字符串,因为不是所有 XR 设备都支持所有功能,另外有些功能会输出敏感信息,与其让用户在使用时进行权限提示,不如在一开始就一次性提示,改参数签名如下。dictionary XRSessionInit {
sequence<any> requiredFeatures;
sequence<any> optionalFeatures;
};
"viewer"
功能,如果是 AR 或 VR 会话还会自动包含 "local"
功能。另外还有 "local-floor"
、"bounded-floor"
和 "unbounded"
功能,它们都需要用户同意才能使用,这些功能字符串代表的意思将在下面章节中讲解。ondevicechange
事件会在设备发生变化时触发,例如本来没有 VR 设备,但是后面接入了,或者有 VR 设备但是连接断掉了。触发该事件,之前的 XRSession 会结束,渲染上下文也会被清除,都需要重新创建。enum XRVisibilityState {
"visible",
"visible-blurred",
"hidden",
};
[interface XRSession : EventTarget { ]
// Attributes
readonly attribute XRVisibilityState visibilityState;
readonly attribute float? frameRate;
readonly attribute Float32Array? supportedFrameRates;
[readonly attribute XRRenderState renderState; ]
[readonly attribute XRInputSourceArray inputSources; ]
// Methods
undefined updateRenderState(optional XRRenderStateInit state = {});
Promise<undefined> updateTargetFrameRate(float rate);
[Promise<XRReferenceSpace> requestReferenceSpace(XRReferenceSpaceType type); ]
unsigned long requestAnimationFrame(XRFrameRequestCallback callback);
undefined cancelAnimationFrame(unsigned long handle);
Promise<undefined> end();
// Events
attribute EventHandler onend;
attribute EventHandler oninputsourceschange;
attribute EventHandler onselect;
attribute EventHandler onselectstart;
attribute EventHandler onselectend;
attribute EventHandler onsqueeze;
attribute EventHandler onsqueezestart;
attribute EventHandler onsqueezeend;
attribute EventHandler onvisibilitychange;
attribute EventHandler onframeratechange;
};
visibilityState
属性是 XR Session 当前显示的状态,一共有下面 3 个状态。visible
XR 渲染的画面正常展示个用户visible-blurred
用户可以看见 XR 渲染的画面,但是失焦了,此时渲染的帧率可能会被限制,画面也可能被模糊处理hidden
用户看不到当前画面,requestAnimationFrame
回调将不会被处理onvisibilitychange
事件来监听可见性变换。frameRate
属性是设备的名义帧率,它并不是真实渲染的帧率。supportedFrameRates
属性是设备支持的目标帧率。onframeratechange
事件。updateTargetFrameRate
方法可以更新会话目标帧率,如果会话没有名义帧率或者设置的帧率不在 supportedFrameRates
中,会直接报错 reject
。inputSources
属性是摄入设备列表,例如 VR 手柄。当输入设备发生变化时会触发 oninputsourceschange
事件。onselect
和 onsqueeze
等相关事件会在用户按下主功能按键或主挤压按键时触发。requestReferenceSpace
方法会返回 XRReferenceSpace
对象,主要用于跟踪空间信息,后面章节将会详细讲解该对象。requestAnimationFrame
方法中需要产生新的帧给用户,它基本与 window.requestAnimationFrame
类似,唯一不同的是,它的回调函数的第二个参数是一个 XRFrame
对象。cancelAnimationFrame
方法与 window.cancelAnimationFrame
类似。renderState
属性是 XRSession 可配置渲染参数的值,例如配置远或近的深度、FOV 等属性,该对象签名如下。dictionary XRRenderStateInit {
double depthNear;
double depthFar;
double inlineVerticalFieldOfView;
XRWebGLLayer? baseLayer;
sequence<XRLayer>? layers;
};
[interface XRRenderState { ]
readonly attribute double depthNear;
readonly attribute double depthFar;
readonly attribute double? inlineVerticalFieldOfView;
readonly attribute XRWebGLLayer? baseLayer;
};
depthNear
是透视矩阵的近裁切面,默认为 0.1
depthFar
是透视矩阵的远裁切面,默认为 1000
inlineVerticalFieldOfView
是 inline
模式下的垂直 FOV,默认为 90 度弧度(其他模式为null
)baseLayer
渲染层,是 XR 合成器获取图片的地方layers
自定义合成层,目前还不支持,配置将直接报错updateRenderState
方法可以更新这些参数,在获取到 XRSession 后,必须更新的一个属性是 baseLayer
,它是一个 XRWebGLLayer
对象,该对象下面会详细讲解,可以通过 new XRWebGLLayer(XRSession, WebGLRenderingContext)
来构建一个。WebGLRenderingContext
是需要XR 兼容的,有两种方法来创建 XR 兼容的上下文。xrCompatible
参数。const canvas = document.createElement('canvas');
gl = canvas.getContext('webgl', { xrCompatible: true });
makeXRCompatible
方法。const canvas = document.createElement('canvas');
gl = canvas.getContext('webgl');
gl.makeXRCompatible().then(() => {
xrSession.updateRenderState({ baseLayer: new XRWebGLLayer(xrSession, gl) });
});
makeXRCompatible
将会直接 reject
。// 监听上下文丢失
canvas.addEventListener("webglcontextlost", (event) => {
// 表明自己处理上下文恢复
event.preventDefault();
});
canvas.addEventListener("webglcontextrestored", () => {
// 上下文恢复,重新加载必要资源
loadSceneGraphics();
});
typedef (WebGLRenderingContext or
WebGL2RenderingContext) XRWebGLRenderingContext;
dictionary XRWebGLLayerInit {
boolean antialias = true;
boolean depth = true;
boolean stencil = false;
boolean alpha = true;
boolean ignoreDepthValues = false;
double framebufferScaleFactor = 1.0;
};
[ ]
interface XRWebGLLayer: XRLayer {
constructor(XRSession session,
XRWebGLRenderingContext context,
optional XRWebGLLayerInit layerInit = {});
// Attributes
readonly attribute boolean antialias;
readonly attribute boolean ignoreDepthValues;
attribute float? fixedFoveation;
[readonly attribute WebGLFramebuffer? framebuffer; ]
readonly attribute unsigned long framebufferWidth;
readonly attribute unsigned long framebufferHeight;
// Methods
XRViewport? getViewport(XRView view);
// Static Methods
static double getNativeFramebufferScaleFactor(XRSession session);
};
XRWebGLLayerInit
对象,其中的 antialias
、depth
、stencil
和 alpha
与 WebGL 中的意义一样,这里不再详细讲解。ignoreDepthValues
表示 XR 合成器是否可以读取深度缓存信息来帮助合成器渲染,如果深度缓存中存储的不是当前的场景深度缓存,那么合成器如果读取该值,可能会造成画面出现伪影。该参数或属性表示合成器是否忽略读取深度缓存。framebufferScaleFactor
属性表示对 framebuffer 的缩放,UA 会有个缩放为 1 的默认 framebuffer 大小,该大小可能和 native 大小不一致,例如有些设备推荐使用低分辨率来保证性能。通过 framebufferScaleFactor
参数可以设置 UA 创建 framebuffer 的大小,例如缩放 0.5 将创建宽高是默认一半的 framebuffer。getNativeFramebufferScaleFactor
静态方法获取 native 大小的缩放,如下所示。const nativeScaleFactor = XRWebGLLayer.getNativeFramebufferScaleFactor(xrSession);
const glLayer = new XRWebGLLayer(xrSession, gl, { framebufferScaleFactor: nativeScaleFactor });
xrSession.updateRenderState({ baseLayer: glLayer });
function rescaleWebGLLayer(scale) {
let glLayer = new XRWebGLLayer(xrSession, gl, { framebufferScaleFactor: scale });
xrSession.updateRenderState({ baseLayer: glLayer });
});
fixedFoveation
属性表示固定注视点渲染级别,0 表示最小,1 表示最大,该技术会在用户注视的地方使用高分辨率,视线边缘使用低分辨率,提高性能,如果设备不支持,该属性则为 null。该值可以动态修改,修改过后的下一帧将应用最新的值。framebuffer
属性表示是最终画面要渲染到的地方,如果是 inline
模式该值为 null
。该 framebuffer
不能被检查和操作,如果对它执行 deleteFramebuffer
、getFramebufferAttachmentParameter
等方法将会直接报错,如果在session.requestAnimationFrame
回调函数外面操作也会报错。framebufferWidth
和 framebufferHeight
属性分别表示 framebuffer
的宽高。requestAnimationFrame
回调函数中获取,它的签名如下。[interface XRFrame { ]
[readonly attribute XRSession session; ]
readonly attribute DOMHighResTimeStamp predictedDisplayTime;
XRViewerPose? getViewerPose(XRReferenceSpace referenceSpace);
XRPose? getPose(XRSpace space, XRSpace baseSpace);
};
requestAnimationFrame
回调函数中有效,一旦控制权返回给浏览器,XRFrame 就会被标记为失效,这时候再去访问上面的方法将会直接报错。session
属性是创建它的 XRSession 对象。predictedDisplayTime
是预测的该帧在设备上显示的时间点,对于 inline
模式,该值与 requestAnimationFrame
第一个参数相同。getViewerPose
方法返回当前 XRFrame 时间点, XR 设备关联的referenceSpace 中观察者的空间姿势信息。getPose
方法返回在当前 XRFrame 时间点,给定空间相对于 baseSpace 空间的姿势信息。enum XRReferenceSpaceType {
"viewer",
"local",
"local-floor",
"bounded-floor",
"unbounded"
};
[ ]
interface XRReferenceSpace : XRSpace {
[XRReferenceSpace getOffsetReferenceSpace(XRRigidTransform originOffset); ]
attribute EventHandler onreset;
};
viewer
表示具有原生原点的跟踪空间,一般用于不进行任何跟踪场景,任何设备都应该支持该类型local
表示只跟踪用户旋转,不跟踪位置,可以理解为坐下,只用头部来观看场景local-floor
与 local
类型相似,但是它是站立着的bounded-floor
表示在安全区内跟踪旋转和位置,用户可以完全与场景进行交互unbounded
表示用户可以自由在场景中移动和旋转,没有安全区限制bounded-floor
类型,返回的是 XRBoundedReferenceSpace 对象,该对象继承于 XRReferenceSpaceType,签名如下。[ ]
interface XRBoundedReferenceSpace : XRReferenceSpace {
readonly attribute FrozenArray<DOMPointReadOnly> boundsGeometry;
};
boundsGeometry
属性用于表示安全区,它是顺时针的点的数组。getOffsetReferenceSpace
方法用于对空间进行调整,例如用手柄对现有空间进行一些旋转调整。onreset
事件在空间被重置时触发,例如,用户校准 XR 设备或 XR 设备重连后自动切回原点。[interface XRPose { ]
[readonly attribute XRRigidTransform transform; ]
[readonly attribute DOMPointReadOnly? linearVelocity; ]
[readonly attribute DOMPointReadOnly? angularVelocity; ]
readonly attribute boolean emulatedPosition;
};
transform
属性用于描述位置和旋转。linearVelocity
属性用于描述线速度,米每秒。angularVelocity
属性用于描述角速度,弧度每秒。emulatedPosition
属性表示 transform
属性中的位置信息是否是模拟估计出来的。[interface XRViewerPose : XRPose { ]
[readonly attribute FrozenArray<XRView> views; ]
};
views
属性,它表示用户左眼或右眼看到的场景,必须每个 XRView 才能在 XR 设备上正确展示场景。[ ]
interface XRRigidTransform {
constructor(optional DOMPointInit position = {}, optional DOMPointInit orientation = {});
[readonly attribute DOMPointReadOnly position; ]
[readonly attribute DOMPointReadOnly orientation; ]
readonly attribute Float32Array matrix;
[readonly attribute XRRigidTransform inverse; ]
};
position
属性用于描述位置信息。orientation
属性用于描述旋转信息,它是四元数。matrix
属性是描述位置和旋转的矩阵,和 WebGL 一样是列主序。inverse
属性返回当前 XRRigidTransform 对象逆对象。enum XREye {
"none",
"left",
"right"
};
[interface XRView { ]
readonly attribute XREye eye;
readonly attribute Float32Array projectionMatrix;
[readonly attribute XRRigidTransform transform; ]
readonly attribute double? recommendedViewportScale;
undefined requestViewportScale(double? scale);
};
eye
属性用于表示该 XRView 对应的眼睛,如果设备不区分左右眼则为 'none'
。
projectionMatrix
属性为投影矩阵。transform
属性表示 getViewerPose()
方法中提供的旋转和位置信息。recommendedViewportScale
属性为设备推荐缩放。requestViewportScale
方法可以修改该 XRView 缩放,该方法可以频繁调用,直到 xrWebGLLayer.getViewport(xrView)
获取它的 viewport 时才生效。[interface XRViewport { ]
readonly attribute long x;
readonly attribute long y;
readonly attribute long width;
readonly attribute long height;
};
xrSession.requestAnimationFrame((time, xrFrame) => {
const viewer = xrFrame.getViewerPose(xrReferenceSpace);
gl.bindFramebuffer(xrWebGLLayer.framebuffer);
for (xrView of viewer.views) {
const xrViewport = xrWebGLLayer.getViewport(xrView);
gl.viewport(xrViewport.x, xrViewport.y, xrViewport.width, xrViewport.height);
}
});
enum XRHandedness {
"none",
"left",
"right"
};
enum XRTargetRayMode {
"gaze",
"tracked-pointer",
"screen"
};
[ ]
interface XRInputSource {
readonly attribute XRHandedness handedness;
readonly attribute XRTargetRayMode targetRayMode;
[readonly attribute XRSpace targetRaySpace; ]
[readonly attribute XRSpace? gripSpace; ]
[readonly attribute FrozenArray<DOMString> profiles; ]
};
handedness
属性表示该输入设备是哪个手握持,如果不区分左右手或不知道则为 'none'
。targetRayMode
属性用于描述如何呈现目标射线,gaze
类型为用户注视输入,tracked-pointer
类型为手柄输入的激光射线,一般为从食指射出,screen
为 inline
模式下的鼠标或触屏输入。targetRaySpace
是 XRSpace 对象,用于跟踪该输入源的光线的旋转和位置信息。gripSpace
是 XRSpace 对象,用于跟踪该输入设备(VR 手柄)的旋转和位置信息。profiles
输入源的描述信息,通过它可以获取是哪个平台的 VR 手柄,这样就可以加载不同的 VR 手柄模型。function main() {
const xr = navigator.xr
let refSpace
// 第一步检查当前环境
if (xr) {
xr.isSessionSupported('immersive-vr').then((supported) => {
if (supported) {
const btn = document.createElement('button')
btn.textContent = '进入 VR'
btn.onclick = onBtnClick
document.body.appendChild(btn)
} else {
document.body.innerHTML = '当前设备不支持 VR'
}
}).catch(() => {
document.body.innerHTML = '检测失败'
})
} else {
document.body.innerHTML = '当前浏览器不支持 WebXR'
}
// 当前支持 VR 并且用户有意进入 VR
function onBtnClick () {
// 请求进入 VR 会话
xr.requestSession('immersive-vr').then(session => {
initWebGL() // 初始化 WebGL,创建 gl 上下文 等
session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl) });
// 设置渲染层
// 请求 local 空间,我们只需要跟踪用户头部旋转
session.requestReferenceSpace('local').then(s => {
refSpace = s
session.requestAnimationFrame(onXRFrame); // 开始渲染
})
})
}
function onXRFrame(time, frame) {
const session = frame.session;
session.requestAnimationFrame(onXRFrame);
const glLayer = session.renderState.baseLayer;
gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
// 设置渲染 framebuffer
const pose = frame.getViewerPose(refSpace)
// 获取旋转和视图信息
if (pose) {
pose.views.forEach(v => {
// 渲染每一个 view,左眼和右眼
const vp = glLayer.getViewport(v)
gl.viewport(vp.x, vp.y, vp.width, vp.height)
// 设置 gl 的viewport
gl.uniform1f(eyeLoc, v.eye === 'right' ? 1 : 0)
// 告诉着色器是左眼还是右眼,
v.transform.matrix[12] = 0
v.transform.matrix[13] = 0
v.transform.matrix[14] = 0
// local 类型,也可能传递位置信息,这里将它去除
gl.uniformMatrix4fv(martixLoc, false, mat4.mul(mat4.create(), v.transform.matrix, mat4.invert(mat4.create(), v.projectionMatrix)));
// 告诉着色器矩阵信息
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
// 渲染模型
})
}
}
}
}
上面例子中渲染全景图片方式使用的是 equirectangular-3d 投影,这部分代码和本篇文章关联不大,这里就忽略相关的代码。
inline
、immersive-vr
和 immersive-ar
, inline
模式还是渲染在浏览器页面中,而 immersive-vr
则是访问 VR 设备,将画面渲染到 VR 设备中,整体渲染过程与普通 WebGL 程序差不多,只不过画面要渲染到 XRWebGLLayer 的 framebuffer 中,并且区分左右眼。