Vue 3 与 WebXR API
概述
WebXR API 是一项用于创建虚拟现实(VR)和增强现实(AR)体验的 Web 标准 API,允许 Web 应用访问 XR 设备(如 VR 头显、AR 眼镜)并渲染 3D 内容。在 Vue 3 应用中集成 WebXR API 可以为用户提供沉浸式的 3D 体验,增强应用的交互性和视觉吸引力。
核心知识
1. WebXR API 基本概念
- 作用:提供访问 XR 设备和渲染 3D 内容的能力
- 支持的体验类型:
- 虚拟现实(VR):完全沉浸式的 3D 环境
- 增强现实(AR):将 3D 内容叠加到真实世界之上
- 混合现实(MR):结合 VR 和 AR 的特性
- 浏览器支持:主流现代浏览器(Chrome、Firefox、Edge)
- 安全限制:需要用户授权,只能在 HTTPS 环境下使用
2. WebXR API 核心接口
- XRSystem:提供访问 XR 设备的入口点(通过
navigator.xr访问) - XRSession:表示一个 XR 会话,管理 XR 体验的生命周期
- XRFrame:表示一帧 XR 内容,包含姿态和视图信息
- XRReferenceSpace:定义虚拟空间坐标系
- XRViewerPose:表示观察者在虚拟空间中的姿态
- XRView:表示单个视图(如左眼或右眼)
- XRInputSource:表示用户输入设备(如控制器、手势)
3. WebXR API 工作流程
- 检查浏览器是否支持 WebXR API
- 请求用户授权访问 XR 设备
- 创建 XR 会话(XRSession)
- 设置参考空间(XRReferenceSpace)
- 请求动画帧(requestAnimationFrame)
- 在每一帧中获取姿态和视图信息
- 渲染 3D 内容
- 处理用户输入
- 结束 XR 会话
4. 参考空间类型
- viewer:以观察者为中心的坐标系
- local:以本地环境为中心的坐标系,适合坐姿或站姿体验
- local-floor:类似于 local,但 y=0 对应地面
- bounded-floor:定义了一个有边界的空间,适合房间尺度体验
- unbounded:无边界的空间,适合大尺度体验
前端实现(Vue 3)
4.1 基础的 VR 场景设置
<template>
<div>
<h2>Vue 3 WebXR 基础示例</h2>
<div ref="canvasContainer" class="canvas-container">
<canvas ref="xrCanvas" width="800" height="600"></canvas>
</div>
<div class="controls">
<button @click="enterVR" :disabled="!isSupported || isInSession">
进入 VR 模式
</button>
<button @click="exitVR" :disabled="!isInSession">
退出 VR 模式
</button>
</div>
<div v-if="!isSupported" class="error">
您的浏览器不支持 WebXR API
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
const xrCanvas = ref(null);
const canvasContainer = ref(null);
const isSupported = ref(false);
const isInSession = ref(false);
const error = ref('');
let xrSession = null;
let xrReferenceSpace = null;
let renderer = null;
let scene = null;
let camera = null;
let cube = null;
// 初始化 Three.js 场景
const initThreeJS = () => {
// 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: xrCanvas.value,
antialias: true
});
renderer.setSize(800, 600);
renderer.setPixelRatio(window.devicePixelRatio);
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
// 创建相机
camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
camera.position.z = 5;
// 添加灯光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加一个立方体
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({
color: 0x0077ff,
metalness: 0.5,
roughness: 0.5
});
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 添加网格地面
const gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);
// 渲染初始帧
renderer.render(scene, camera);
};
// 检查 WebXR 支持
const checkWebXRSupport = () => {
isSupported.value = !!navigator.xr && !!navigator.xr.isSessionSupported;
};
// 请求 XR 会话
const requestXRSession = async () => {
try {
// 检查是否支持 VR 会话
const supported = await navigator.xr.isSessionSupported('immersive-vr');
if (!supported) {
error.value = '您的设备不支持 VR 会话';
return null;
}
// 请求 VR 会话
const session = await navigator.xr.requestSession('immersive-vr', {
requiredFeatures: ['local-floor']
});
return session;
} catch (err) {
error.value = `请求 XR 会话失败: ${err.message}`;
return null;
}
};
// 进入 VR 模式
const enterVR = async () => {
if (isInSession.value) return;
error.value = '';
try {
// 请求 XR 会话
xrSession = await requestXRSession();
if (!xrSession) return;
isInSession.value = true;
// 设置渲染器为 XR 兼容
renderer.xr.enabled = true;
// 添加 XR 会话事件监听器
xrSession.addEventListener('end', onXRSessionEnded);
// 请求参考空间
xrReferenceSpace = await xrSession.requestReferenceSpace('local-floor');
// 设置 XR 会话的渲染目标
renderer.setSession(xrSession);
// 启动 XR 渲染循环
animate();
} catch (err) {
error.value = `进入 VR 模式失败: ${err.message}`;
isInSession.value = false;
if (xrSession) {
xrSession.end();
xrSession = null;
}
}
};
// 退出 VR 模式
const exitVR = () => {
if (!isInSession.value || !xrSession) return;
xrSession.end();
};
// XR 会话结束处理
const onXRSessionEnded = () => {
isInSession.value = false;
renderer.xr.enabled = false;
renderer.setSession(null);
xrSession = null;
xrReferenceSpace = null;
};
// 动画循环
const animate = () => {
if (!xrSession) return;
// 请求下一帧
xrSession.requestAnimationFrame((time, frame) => {
renderXRFrame(time, frame);
animate();
});
};
// 渲染 XR 帧
const renderXRFrame = (time, frame) => {
if (!frame || !xrSession || !xrReferenceSpace) return;
// 更新立方体旋转
if (cube) {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
}
// 获取会话的参考空间视图
const session = frame.session;
const pose = frame.getViewerPose(xrReferenceSpace);
if (pose) {
// 渲染每一帧
renderer.render(scene, pose);
}
};
// 窗口大小调整处理
const handleResize = () => {
if (!xrCanvas.value || !canvasContainer.value) return;
const width = canvasContainer.value.clientWidth;
const height = canvasContainer.value.clientHeight;
xrCanvas.value.width = width;
xrCanvas.value.height = height;
if (camera) {
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
if (renderer) {
renderer.setSize(width, height);
}
};
// 组件挂载时初始化
onMounted(() => {
// 检查 WebXR 支持
checkWebXRSupport();
// 初始化 Three.js 场景
initThreeJS();
// 添加窗口大小调整监听器
window.addEventListener('resize', handleResize);
handleResize();
});
// 组件销毁前清理
onBeforeUnmount(() => {
// 移除事件监听器
window.removeEventListener('resize', handleResize);
// 结束 XR 会话
if (xrSession) {
xrSession.end();
}
// 销毁渲染器
if (renderer) {
renderer.dispose();
}
});
</script>
<style scoped>
.canvas-container {
width: 100%;
max-width: 800px;
margin: 0 auto;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: auto;
}
.controls {
margin: 1rem 0;
display: flex;
gap: 0.5rem;
justify-content: center;
}
button {
padding: 0.5rem 1rem;
cursor: pointer;
}
.error {
color: red;
margin: 1rem 0;
text-align: center;
}
</style>5. 创建可复用的 WebXR Composable
// composables/useWebXR.js
import { ref, onMounted, onBeforeUnmount } from 'vue';
export function useWebXR() {
const isSupported = ref(false);
const isInSession = ref(false);
const session = ref(null);
const referenceSpace = ref(null);
const error = ref('');
const xrFrame = ref(null);
const viewerPose = ref(null);
let animationFrameId = null;
let onSessionStartCallback = null;
let onSessionEndCallback = null;
let onFrameCallback = null;
// 检查 WebXR 支持
const checkSupport = () => {
isSupported.value = !!navigator.xr && !!navigator.xr.isSessionSupported;
};
// 请求 XR 会话
const requestSession = async (mode = 'immersive-vr', options = {}) => {
try {
const supported = await navigator.xr.isSessionSupported(mode);
if (!supported) {
throw new Error(`不支持 ${mode} 模式`);
}
const xrSession = await navigator.xr.requestSession(mode, options);
return xrSession;
} catch (err) {
throw new Error(`请求 XR 会话失败: ${err.message}`);
}
};
// 进入 XR 模式
const enterXR = async (mode = 'immersive-vr', options = {}) => {
if (isInSession.value) return;
error.value = '';
try {
// 请求 XR 会话
const xrSession = await requestSession(mode, options);
session.value = xrSession;
isInSession.value = true;
// 添加事件监听器
xrSession.addEventListener('end', onSessionEnded);
xrSession.addEventListener('select', onSelect);
// 请求参考空间
const refSpace = await xrSession.requestReferenceSpace('local-floor');
referenceSpace.value = refSpace;
// 调用会话开始回调
if (onSessionStartCallback) {
onSessionStartCallback(xrSession);
}
// 启动渲染循环
startAnimationLoop(xrSession);
return xrSession;
} catch (err) {
error.value = err.message;
throw err;
}
};
// 退出 XR 模式
const exitXR = () => {
if (!isInSession.value || !session.value) return;
session.value.end();
};
// XR 会话结束处理
const onSessionEnded = () => {
isInSession.value = false;
// 停止渲染循环
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
// 调用会话结束回调
if (onSessionEndCallback) {
onSessionEndCallback();
}
// 清理引用
session.value = null;
referenceSpace.value = null;
xrFrame.value = null;
viewerPose.value = null;
};
// 处理选择事件(如控制器点击)
const onSelect = (event) => {
console.log('选择事件:', event);
};
// 启动动画循环
const startAnimationLoop = (xrSession) => {
const animate = (time, frame) => {
if (!frame) return;
xrFrame.value = frame;
// 获取观察者姿态
if (referenceSpace.value) {
viewerPose.value = frame.getViewerPose(referenceSpace.value);
}
// 调用帧回调
if (onFrameCallback) {
onFrameCallback(time, frame);
}
// 请求下一帧
animationFrameId = xrSession.requestAnimationFrame(animate);
};
animationFrameId = xrSession.requestAnimationFrame(animate);
};
// 设置会话开始回调
const onSessionStart = (callback) => {
onSessionStartCallback = callback;
};
// 设置会话结束回调
const onSessionEnd = (callback) => {
onSessionEndCallback = callback;
};
// 设置帧回调
const onFrame = (callback) => {
onFrameCallback = callback;
};
// 获取参考空间
const getReferenceSpace = async (type = 'local-floor') => {
if (!session.value) {
throw new Error('没有活动的 XR 会话');
}
const refSpace = await session.value.requestReferenceSpace(type);
referenceSpace.value = refSpace;
return refSpace;
};
// 组件挂载时检查支持
onMounted(() => {
checkSupport();
});
// 组件销毁前清理
onBeforeUnmount(() => {
if (isInSession.value && session.value) {
session.value.end();
}
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
});
return {
isSupported,
isInSession,
session,
referenceSpace,
error,
xrFrame,
viewerPose,
enterXR,
exitXR,
getReferenceSpace,
onSessionStart,
onSessionEnd,
onFrame
};
}6. 使用 Composable 的示例
<template>
<div>
<h2>使用 WebXR Composable</h2>
<div ref="canvasContainer" class="canvas-container">
<canvas ref="xrCanvas"></canvas>
</div>
<div class="controls">
<button @click="enterVR" :disabled="!isSupported || isInSession">
进入 VR
</button>
<button @click="exitVR" :disabled="!isInSession">
退出 VR
</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="isInSession" class="status">正在 VR 模式中...</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { useWebXR } from './composables/useWebXR';
import * as THREE from 'three';
import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
const xrCanvas = ref(null);
const canvasContainer = ref(null);
let renderer = null;
let scene = null;
let camera = null;
let cube = null;
// 使用 WebXR Composable
const {
isSupported,
isInSession,
error,
enterXR,
exitXR,
onSessionStart,
onFrame
} = useWebXR();
// 初始化 Three.js 场景
const initScene = () => {
// 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: xrCanvas.value,
antialias: true
});
renderer.setSize(canvasContainer.value.clientWidth, canvasContainer.value.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
// 创建相机
camera = new THREE.PerspectiveCamera(
75,
canvasContainer.value.clientWidth / canvasContainer.value.clientHeight,
0.1,
1000
);
// 添加灯光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加立方体
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({
color: 0x0077ff,
metalness: 0.5,
roughness: 0.5
});
cube = new THREE.Mesh(geometry, material);
cube.position.y = 0.5;
scene.add(cube);
// 添加网格地面
const gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);
// 渲染初始帧
renderer.render(scene, camera);
};
// 进入 VR 模式
const enterVR = async () => {
try {
await enterXR('immersive-vr', {
requiredFeatures: ['local-floor']
});
} catch (err) {
console.error('进入 VR 失败:', err);
}
};
// 窗口大小调整
const handleResize = () => {
if (!canvasContainer.value || !renderer) return;
const width = canvasContainer.value.clientWidth;
const height = canvasContainer.value.clientHeight;
renderer.setSize(width, height);
if (camera) {
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
};
// 组件挂载时初始化
onMounted(() => {
initScene();
window.addEventListener('resize', handleResize);
// 设置 XR 会话开始回调
onSessionStart((session) => {
// 启用 XR 渲染
renderer.xr.enabled = true;
renderer.setSession(session);
});
// 设置帧回调
onFrame((time, frame) => {
if (!frame) return;
// 更新立方体旋转
if (cube) {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
}
// 渲染场景
renderer.render(scene, frame);
});
});
</script>
<style scoped>
.canvas-container {
width: 100%;
max-width: 800px;
height: 600px;
margin: 0 auto;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
}
.controls {
margin: 1rem 0;
display: flex;
gap: 0.5rem;
justify-content: center;
}
button {
padding: 0.5rem 1rem;
cursor: pointer;
}
.error {
color: red;
margin: 1rem 0;
text-align: center;
}
.status {
color: green;
margin: 1rem 0;
text-align: center;
}
</style>最佳实践
1. 用户体验优化
- 提供清晰的入口和出口:明确的按钮让用户可以轻松进入和退出 XR 模式
- 提供适应期:给用户时间适应 VR 环境,避免立即展示复杂内容
- 优化性能:确保 3D 场景的性能良好,避免卡顿
- 提供舒适的视角:避免快速移动或旋转,减少晕动症
- 支持多种输入方式:适应不同的 XR 控制器和输入设备
2. 性能优化
- 优化 3D 模型:减少多边形数量,使用纹理压缩
- 使用高效的渲染技术:如 instancing、LOD(细节层次)
- 优化动画:减少每帧的计算量
- 使用 WebGL 最佳实践:如批处理绘制调用
- 监控性能:使用浏览器开发者工具监控帧率和资源使用
3. 可访问性考虑
- 支持非 XR 模式:为不支持 XR 的设备提供替代体验
- 提供调整选项:允许用户调整亮度、音量等设置
- 支持语音控制:为无法使用控制器的用户提供语音控制选项
- 考虑色盲用户:使用对色盲友好的颜色方案
4. 安全和隐私
- 获取用户授权:在访问 XR 设备前获得用户明确授权
- 保护用户数据:不要收集或传输敏感的 XR 数据
- 使用 HTTPS:确保应用在安全上下文下运行
- 遵守隐私法规:符合 GDPR、CCPA 等隐私法规
5. 跨设备兼容性
- 测试多种 XR 设备:在不同的 VR 头显和 AR 设备上测试
- 支持不同的参考空间:根据设备能力调整参考空间类型
- 处理不同的输入设备:适应不同的控制器和手势输入
- 提供降级方案:为不支持 XR 的设备提供 2D 或 3D 替代体验
常见问题与解决方案
1. 无法进入 XR 模式
- 原因:
- 浏览器不支持 WebXR API
- 设备不支持 XR 会话
- 没有获得用户授权
- 非 HTTPS 环境
- 解决方案:
- 检查浏览器和设备支持
- 确保在 HTTPS 环境下运行
- 提供清晰的授权请求
2. XR 场景卡顿
- 原因:
- 3D 模型过于复杂
- 渲染计算量过大
- 设备性能不足
- 解决方案:
- 优化 3D 模型和纹理
- 减少每帧的计算量
- 使用 LOD 和实例化技术
- 提供性能设置选项
3. 控制器不工作
- 原因:
- 控制器不支持或未连接
- 输入事件处理不正确
- 控制器模型未正确加载
- 解决方案:
- 检查控制器连接状态
- 正确处理输入事件
- 使用 Three.js 提供的控制器模型工厂
4. 晕动症问题
- 原因:
- 场景移动过快
- 视角旋转过于剧烈
- 帧率过低
- 解决方案:
- 减少场景移动速度
- 平滑视角旋转
- 优化性能,确保稳定的帧率
- 提供舒适模式选项
5. 参考空间错误
- 原因:
- 请求的参考空间类型不被支持
- 参考空间转换失败
- 解决方案:
- 检查设备支持的参考空间类型
- 使用 fallback 参考空间
- 正确处理参考空间转换
高级学习资源
1. 官方文档
2. 深度教程
3. 相关库和工具
- Three.js:用于 3D 渲染
- Babylon.js:另一个 3D 渲染引擎
- A-Frame:用于构建 VR 体验的声明式框架
- WebXR Emulator:Chrome 扩展,用于模拟 XR 设备
4. 示例项目
5. 视频教程
实践练习
1. 基础练习:创建简单的 VR 场景
- 创建 Vue 3 应用,集成 Three.js 和 WebXR API
- 实现基本的 VR 场景,包含一个旋转的立方体
- 添加进入和退出 VR 模式的按钮
2. 进阶练习:添加交互功能
- 在 VR 场景中添加可交互的对象
- 实现控制器点击功能
- 添加控制器模型
- 实现基本的物理碰撞
3. 高级练习:创建 AR 体验
- 实现增强现实场景
- 将 3D 内容叠加到真实世界之上
- 实现平面检测功能
- 添加虚拟物体放置功能
4. 综合练习:创建完整的 VR 应用
- 创建一个完整的 VR 应用,如虚拟展厅或游戏
- 实现多个场景和交互功能
- 添加音效和背景音乐
- 实现保存和加载功能
5. 挑战练习:创建多人 VR 体验
- 实现多人 VR 功能
- 使用 WebSockets 实现实时通信
- 实现用户头像和位置同步
- 添加语音聊天功能
总结
WebXR API 为 Vue 3 应用提供了强大的 XR 能力,可以创建沉浸式的虚拟现实和增强现实体验。通过合理使用 WebXR API 和 Three.js 等库,可以为用户提供丰富的 3D 交互体验。掌握 WebXR API 的实现原理和最佳实践,对于构建现代化、沉浸式的 Vue 3 应用至关重要。