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 工作流程

  1. 检查浏览器是否支持 WebXR API
  2. 请求用户授权访问 XR 设备
  3. 创建 XR 会话(XRSession)
  4. 设置参考空间(XRReferenceSpace)
  5. 请求动画帧(requestAnimationFrame)
  6. 在每一帧中获取姿态和视图信息
  7. 渲染 3D 内容
  8. 处理用户输入
  9. 结束 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. 相关库和工具

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 应用至关重要。

« 上一篇 Vue 3与Web Speech API - 实现语音交互功能的核心技术 下一篇 » Vue 3与WebGL高级应用 - 实现高性能图形渲染的核心技术