WebXR 与 WebGL/Three.js 网页三维

本文讲解 WebXR 与 Three.js 的网页三维实践,回答 WebXR 支持哪些设备、会话模式怎么选、参考空间与坐标系怎么用、性能为什么容易崩等实战问题。覆盖能力探测、immersive-vr 与 immersive-ar 会话、参考空间与输入源、XRFrame 渲染循环、Three.js 集成、图层与立体渲染、命中检测、移动浏览器性能与部署兼容,并给出代码、权衡与常见坑。

引言

WebXR 的价值在于零安装分发:一个链接就能让用户进入 AR 或 VR,不需要应用商店审核、不需要下载几百兆。这对营销展示、电商预览、教育课件这类「低频、轻量、要传播」的场景几乎是唯一合理的选择。

但 WebXR 的性能天花板也低得多。浏览器要管 JS 执行、GC、图层合成、页面布局,留给三维渲染的预算被压缩得很紧。在手机上跑 WebXR AR,能稳定 30 fps 已属不易,而一体机浏览器的 WebXR 虽然能到 72 或 90 fps,但可用内存与纹理带宽都比原生应用紧张。

工程上真正要解决的问题是:能力探测要准(否则黑屏)、参考空间要理解(否则内容飘)、渲染循环要贴合 XRFrame(否则掉帧)、降级路径要完整(不支持 WebXR 时给什么)。本文按这条主线展开,代码以原生 WebXR API 与 Three.js 两条线对照。整体定位见 空间计算与 XR 技术全景 。

目录

  1. WebXR 是什么
  2. 能力探测与设备支持
  3. 会话模式与生命周期
  4. 参考空间与坐标系
  5. 输入源与手柄
  6. 渲染循环与 XRFrame
  7. Three.js 集成
  8. 图层与立体渲染
  9. 命中检测与锚点
  10. 性能优化
  11. 部署与兼容
  12. 局限与替代方案
  13. 权衡取舍
  14. 常见坑清单
  15. 小结

1. WebXR 是什么

WebXR Device API 是 W3C 标准,取代了早期的 WebVR。它提供三件事:

  • 设备发现:navigator.xr.isSessionSupported() 探测能力。
  • 会话管理:requestSession() 进入 immersive-vr 或 immersive-ar。
  • 帧循环:session.requestAnimationFrame() 给出带位姿的 XRFrame。
// 最小可用流程
if (navigator.xr && await navigator.xr.isSessionSupported('immersive-vr')) {
  const session = await navigator.xr.requestSession('immersive-vr', {
    requiredFeatures: ['local-floor'],
    optionalFeatures: ['hand-tracking', 'layers']
  });
  session.requestAnimationFrame(onXRFrame);
}

与原生 XR 相比,WebXR 的抽象层级更接近 OpenXR:同样是「会话 + 参考空间 + 帧循环 + 图层」四件套,理解了 OpenXR 的模型,WebXR 上手很快。

2. 能力探测与设备支持

探测必须分层,因为「浏览器支持 WebXR」不等于「这台设备支持你要的模式」:

async function probe() {
  if (!('xr' in navigator)) return { supported: false, reason: 'no-xr' };
  const out = {};
  out.vr = await navigator.xr.isSessionSupported('immersive-vr');
  out.ar = await navigator.xr.isSessionSupported('immersive-ar');
  // 注意:isSessionSupported 在非安全上下文会直接 reject
  return out;
}

支持现状(截至 2026 年):

平台immersive-vrimmersive-ar备注
Quest 浏览器支持部分支持需 HTTPS
Chrome Android部分支持(ARCore)需 ARCore 服务
Safari iOS不支持不支持无 WebXR
Vision Pro Safari支持支持需用户授权
桌面 Chrome需头显/模拟器需 WebXR API 模拟器开发用

iOS Safari 不支持 WebXR 是最大的现实约束:想做 iOS 上的网页 AR,只能用 Quick Look(USDZ)或第三方库降级方案。

3. 会话模式与生命周期

三种模式:

模式说明典型用途
immersive-vr全屏沉浸,独占显示VR 游戏、漫游
immersive-ar透视叠加网页 AR 展示
inline页面内嵌,无独占预览、降级

生命周期与会话事件:

session.addEventListener('end', () => { /* 清理资源 */ });
session.addEventListener('inputsourceschange', onInputsChange);
session.addEventListener('visibilitychange', () => {
  // 用户摘下头显或切标签页
});

// 结束会话必须显式调用,否则浏览器可能保留资源
await session.end();

工程要点:

  • 进入前必须有用户手势:requestSession 必须在点击等用户手势的调用栈里,否则被浏览器拒绝。
  • 退出要清理:监听 end 事件释放渲染目标与纹理,否则反复进出会内存泄漏。
  • visibilitychange 要处理:头显摘下时暂停渲染,省电且避免状态错乱。

3.1 特性声明的两种语义

requiredFeatures:不满足则 requestSession 直接抛错
  local-floor / bounded-floor / hit-test / anchors

optionalFeatures:不满足则静默忽略,应用需自行探测
  hand-tracking / layers / dom-overlay / plane-detection

经验规则:核心玩法依赖的能力放 required,增强体验的能力放 optional。把 layers 放进 required 会导致在不支持的浏览器上完全无法进入,而它其实只影响清晰度。

4. 参考空间与坐标系

参考空间(Reference Space)决定「位姿是相对什么说的」,是 WebXR 最容易理解错的部分:

参考空间原点Y 轴适用
viewer用户眼睛无所谓仅需头部相对运动
local会话开始时头显位置无重力对齐VR 站立
local-floor地面投影垂直向上,地面 y=0VR 站立/房间
bounded-floor地面投影同上,含边界房间尺度
unbounded起点重力对齐大空间漫游
const refSpace = await session.requestReferenceSpace('local-floor');
// 每帧:取左右眼视图矩阵与投影矩阵
const viewerPose = frame.getViewerPose(refSpace);
for (const view of viewerPose.views) {
  // view.transform.matrix  → 视图矩阵(列主序)
  // view.projectionMatrix  → 投影矩阵
  // view.eye               → 'left' | 'right' | 'none'
}

最常见的错误是用了 local 却按 local-floor 理解,导致用户看到的虚拟地面与自己脚下差 1.6 米。VR 内容应优先用 local-floor 或 bounded-floor。

5. 输入源与手柄

输入源通过 session.inputSources 获取,每个源包含手部/手柄、目标射线空间与游戏手柄映射:

function onInputsChange(e) {
  for (const src of session.inputSources) {
    console.log(src.handedness, src.targetRayMode, src.profiles);
    // targetRayMode: 'tracked-pointer' | 'gaze' | 'screen'
  }
}

// 每帧读取手柄位姿
const src = session.inputSources[0];
const pose = frame.getPose(src.targetRaySpace, refSpace);
if (pose) {
  const m = pose.transform.matrix;  // 手柄在世界中的位姿
}
// 按键:XRSession 的 select / squeeze 事件
session.addEventListener('select', () => { /* 扳机 */ });
session.addEventListener('squeeze', () => { /* 侧握 */ });

关键点:

  • select 是语义事件,可能是扳机、捏合或屏幕点击,不要硬编码为「扳机」。
  • targetRayMode 为 gaze 表示注视输入(无手柄的 AR),交互要改用注视加停留。
  • 手部追踪需要 optionalFeatures: ['hand-tracking'],且只在支持设备上可用。

6. 渲染循环与 XRFrame

绝不能继续用 window.requestAnimationFrame,必须用 session.requestAnimationFrame,因为只有它提供带预测位姿的 XRFrame:

function onXRFrame(time, frame) {
  session.requestAnimationFrame(onXRFrame);   // 先注册下一帧
  const pose = frame.getViewerPose(refSpace);
  if (!pose) return;                          // 位姿不可用,跳过本帧

  const glLayer = session.renderState.baseLayer;
  gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);

  for (const view of pose.views) {
    const vp = glLayer.getViewport(view);
    gl.viewport(vp.x, vp.y, vp.width, vp.height);
    renderScene(view.projectionMatrix, view.transform.inverse.matrix);
  }
}

要点:

  • baseLayer 的 framebuffer 由浏览器提供,应用只负责往里画。
  • frame.getViewerPose 可能返回 null,必须判空,否则崩溃。
  • 每帧必须调用一次 requestAnimationFrame,否则循环停止。

7. Three.js 集成

Three.js 通过 WebGLRenderer.xr 封装了上述细节:

import * as THREE from 'three';
import { ARButton } from 'three/addons/webxr/ARButton.js';

const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.xr.enabled = true;
renderer.xr.setReferenceSpaceType('local-floor');
document.body.appendChild(ARButton.createButton(renderer, {
  requiredFeatures: ['hit-test'],
  optionalFeatures: ['dom-overlay'],
  domOverlay: { root: document.getElementById('overlay') }
}));

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera();
renderer.setAnimationLoop((t, frame) => {
  // frame 是 XRFrame,可直接做命中检测
  renderer.render(scene, camera);
});

要点:

  • 必须用 renderer.setAnimationLoop,它会自动切换到 XR 帧循环。
  • camera 不需要手动更新,Three.js 从 XRFrame 取位姿写入相机。
  • renderer.xr.getController(i) 拿到控制器对象,用于射线与模型挂载。

7.1 控制器射线

const controller = renderer.xr.getController(0);
controller.addEventListener('selectstart', onSelect);
scene.add(controller);

const line = new THREE.Line(
  new THREE.BufferGeometry().setFromPoints([
    new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1)
  ]),
  new THREE.LineBasicMaterial({ color: 0xffffff })
);
controller.add(line);   // 射线跟随控制器

8. 图层与立体渲染

WebXR 支持多层图层,可显著提升清晰度:

图层类型说明用途
XRWebGLLayer基础投影层常规三维渲染
XRQuadLayer平面四边形高清 UI、视频
XRCylinderLayer柱面环绕 UI
XREquirectLayer全景360 视频
XRProjectionLayer多视图投影立体渲染

Quad Layer 是 WebXR 里最实用的优化:把 UI 从三维场景里抽出来,用独立的高分辨率四边形渲染,既不占用三维渲染分辨率,也不受立体渲染影响。这在 VR 里能让文字清晰度提升数倍。

const quadLayer = new XRQuadLayer(session, {
  space: refSpace,
  viewPixelWidth: 1024, viewPixelHeight: 1024,
  isStatic: true
});
// 需要在 requestSession 时声明 optionalFeatures: ['layers']

注意图层支持是可选特性,必须探测后再用,不支持时退回到三维内贴图。

9. 命中检测与锚点

immersive-ar 的命中检测等价于 ARCore/ARKit 的射线检测:

const viewerSpace = await session.requestReferenceSpace('viewer');
const hitTestSource = await session.requestHitTestSource({
  space: viewerSpace
});

function onXRFrame(t, frame) {
  const hits = frame.getHitTestResults(hitTestSource);
  if (hits.length) {
    const pose = hits[0].getPose(refSpace);
    reticle.visible = true;
    reticle.matrix.fromArray(pose.transform.matrix);
  } else {
    reticle.visible = false;
  }
}

要点:

  • 必须声明 requiredFeatures: ['hit-test'],否则 requestHitTestSource 抛错。
  • 命中检测是相对 viewer 空间的射线,即从眼睛往前打,与手机屏幕点击不同。
  • 锚点用 XRAnchor,通过 frame.createAnchor(pose, refSpace) 创建,跨会话持久化能力弱于原生。

10. 性能优化

浏览器三维的性能约束比原生严,主要瓶颈与对策:

瓶颈现象对策
DrawCall 过多CPU 卡顿合批、实例化、合并几何
纹理过大首次加载慢、显存爆压缩纹理、降分辨率
后处理链每帧多次全屏采样移动端关掉或简化
JS 主线程动画与逻辑卡顿用 Web Worker 分担
GC 抖动周期性掉帧复用对象,避免每帧 new
阴影贴图GPU 占用高关闭实时阴影,用假阴影
// 复用对象,避免每帧分配(GC 是掉帧的隐形杀手)
const _v = new THREE.Vector3();
const _q = new THREE.Quaternion();
function updatePositions() {
  _v.set(x, y, z);        // 复用,不 new
  mesh.position.copy(_v);
}

进一步的算力卸载可参考 WebAssembly 浏览器内 AI 推理 ,把密集计算移出主线程是浏览器侧最有效的优化之一。

11. 部署与兼容

WebXR 的部署约束比普通网页多:

  • 必须 HTTPS:isSessionSupported 在非安全上下文直接 reject,localhost 例外。
  • 必须用户手势进入:不能自动进入沉浸模式。
  • 权限提示:进入 AR 会请求相机权限,需在 UI 上提前说明。
  • 降级路径:不支持 WebXR 时退回到鼠标拖拽的 360 预览或内嵌视频。
  • 首屏加载:三维资源体积大,必须做按需加载与进度提示。
降级决策树:
  支持 immersive-ar  → AR 模式(相机 + 命中检测)
  支持 immersive-vr  → VR 模式(手柄 + 传送)
  仅支持 inline      → 页面内 3D 预览(鼠标拖拽)
  完全不支持          → 视频或图片展示

12. 局限与替代方案

WebXR 的能力边界要提前认清:

  • 无持久化世界地图:不能像 ARKit 那样保存世界地图重定位。
  • 无遮挡:WebXR 的 AR 没有深度 API,虚拟物体无法被真实物体遮挡。
  • 无高频追踪:位姿精度与延迟都弱于原生。
  • iOS 无支持:这是最大的市场缺口。
  • 内存上限低:移动浏览器可用内存常低于 500 MB。

替代与互补方案:

需求方案
iOS 上的网页 ARQuick Look(USDZ)或 8th Wall 类商业方案
高保真展示原生 App 或视频
轻量 360 预览Three.js + 鼠标拖拽
需要持久化原生 AR + 云锚点

13. 权衡取舍

  • WebXR 与原生 App:前者免安装、迭代快,后者性能与能力全,按分发需求选。
  • local 与 local-floor:前者简单但地面高度不对,VR 应用一律用后者。
  • 三维内 UI 与 Quad Layer:后者清晰度高但支持不全,需做能力探测与降级。
  • 实时阴影与假阴影:移动浏览器上实时阴影几乎不可用,优先贴图阴影。
  • 手部追踪与控制器:手部追踪免设备但精度低,浏览器上稳定性更差。
  • 高分辨率与帧率:移动浏览器上二者不可兼得,展示类优先保帧率。

14. 常见坑清单

  • 用 window.requestAnimationFrame:拿不到 XRFrame,位姿无法更新,画面静止。
  • 忘记判空 getViewerPose:位姿不可用时返回 null,直接崩溃。
  • 在非用户手势里 requestSession:浏览器直接拒绝,表现为「点击没反应」。
  • 参考空间用错:用 local 当 local-floor,用户站在地下或悬空。
  • 不监听 end 事件:反复进出会话导致显存泄漏,最终崩溃。
  • 把 select 当扳机:在 AR 里 select 可能是屏幕点击,交互逻辑要按语义写。
  • 不声明 requiredFeatures:用 hit-test 却不声明,运行时抛错。
  • 每帧 new 对象:GC 抖动导致周期性掉帧,必须复用。
  • 依赖 iOS Safari:不支持 WebXR,必须有降级方案。
  • 忽略 HTTPS:本地能跑,部署后 isSessionSupported 直接 reject。

15. 小结

WebXR 的工程主线是:分层探测能力 → 用用户手势进入会话 → 选对参考空间 → 在 XRFrame 里渲染 → 用图层提升清晰度 → 用命中检测做放置 → 准备完整的降级路径。它最大的优势是分发,最大的代价是性能与能力受限。

判断一个项目该不该用 WebXR,只需问:用户是否愿意为了这次体验安装一个 App。如果答案是否定的(营销、展示、课件),WebXR 是对的选择;如果用户会长期使用(游戏、工具),原生仍是唯一选项。

若要在网页里做更重的计算(如姿态估计、图像处理),下一步读 WebAssembly 浏览器内 AI 推理 ;若要进入头显做完整交互体系,读 Unity XR Interaction Toolkit 交互体系 。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AR 与 VR」更多文章

  1. XR 培训与仿真应用
  2. XR 控制器与输入设备
  3. XR 内容分发与商店上架