第25节:VR基础与WebXR API入门

发布于:2025-09-06 ⋅ 阅读:(15) ⋅ 点赞:(0)

第25节:VR基础与WebXR API入门

概述

虚拟现实(VR)正在重塑人机交互的边界,而WebXR让这一切在浏览器中成为可能。本节将深入探索WebXR技术体系,从设备集成到交互处理,从立体渲染到性能优化,为您提供构建沉浸式WebVR应用的完整解决方案。

在这里插入图片描述

WebXR生态系统建立在多层技术栈之上,其核心架构如下:

WebXR应用架构
应用层
渲染层
场景管理
交互处理
状态管理
立体渲染
异步渲染优化

核心原理深度解析

WebXR技术架构

WebXR API提供了访问VR/AR设备的标准化接口,其核心组件包括:

组件 功能描述 关键特性
XRSystem 设备检测和会话管理 设备枚举、功能检测
XRSession XR体验会话控制 渲染循环、输入处理
XRReferenceSpace 空间坐标系定义 6DoF追踪、空间锚点
XRInputSource 输入设备管理 控制器状态、手势识别

立体渲染原理

VR渲染与传统3D渲染的关键差异:

  1. 双眼视差渲染

    • 左眼和右眼分别渲染独立视角
    • 瞳距(IPD)调整和校准
    • 视口分割和投影矩阵计算
  2. 性能优化要求

    • 目标帧率:72-90 FPS(PC VR)、72 FPS(Quest)
    • 渲染分辨率:每眼1.4-2.0倍原生分辨率(超采样)
    • 绘制调用优化:每帧<100 draw calls

完整代码实现

高级WebXR集成系统

<template>
  <div ref="container" class="xr-container">
    <!-- 主渲染画布 -->
    <canvas ref="rendererCanvas" class="xr-canvas"></canvas>
    
    <!-- XR控制界面 -->
    <div v-if="!isXRSessionActive" class="xr-controls">
      <div class="xr-control-panel">
        <h2>WebXR体验控制器</h2>
        
        <div class="device-status">
          <div class="status-item">
            <span class="status-label">XR支持:</span>
            <span class="status-value" :class="{'supported': xrSupport}">
              {{ xrSupport ? '可用' : '不可用' }}
            </span>
          </div>
          
          <div class="status-item" v-if="xrSupport">
            <span class="status-label">设备类型:</span>
            <span class="status-value">{{ xrDeviceType || '未检测' }}</span>
          </div>
        </div>

        <div class="session-buttons" v-if="xrSupport">
          <button 
            @click="enterVR()" 
            :disabled="!canEnterVR"
            class="xr-button vr-button"
          >
            🕶️ 进入VR模式
          </button>
          
          <button 
            @click="enterAR()" 
            :disabled="!canEnterAR"
            class="xr-button ar-button"
          >
            📱 进入AR模式
          </button>
        </div>

        <div class="quality-settings" v-if="xrSupport">
          <h3>渲染质量设置</h3>
          
          <div class="setting-group">
            <label>渲染比例: {{ renderScale }}x</label>
            <input 
              type="range" 
              v-model="renderScale" 
              min="0.5" 
              max="1.5" 
              step="0.1"
            >
          </div>

          <div class="setting-group">
            <label>抗锯齿: {{ msaaEnabled ? '开启' : '关闭' }}</label>
            <input 
              type="checkbox" 
              v-model="msaaEnabled"
            >
          </div>
        </div>
      </div>
    </div>

    <!-- XR会话状态指示 -->
    <div v-if="isXRSessionActive" class="xr-session-info">
      <div class="session-stats">
        <span>FPS: {{ currentFPS }}</span>
        <span>DrawCalls: {{ currentDrawCalls }}</span>
        <span>控制器: {{ controllerCount }}</span>
      </div>
      
      <button @click="exitXR()" class="exit-button">
        🚪 退出XR
      </button>
    </div>

    <!-- 加载状态 -->
    <div v-if="isLoading" class="loading-overlay">
      <div class="loading-content">
        <div class="spinner"></div>
        <p>初始化XR环境...</p>
        <p class="loading-details">{{ loadingStatus }}</p>
      </div>
    </div>
  </div>
</template>

<script>
import { onMounted, onUnmounted, ref, reactive } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { VRButton } from 'three/addons/webxr/VRButton.js';
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';

export default {
  name: 'WebXRExperience',
  setup() {
    const container = ref(null);
    const rendererCanvas = ref(null);
    const isXRSessionActive = ref(false);
    const xrSupport = ref(false);
    const xrDeviceType = ref('');
    const canEnterVR = ref(false);
    const canEnterAR = ref(false);
    const isLoading = ref(false);
    const loadingStatus = ref('');
    const renderScale = ref(1.0);
    const msaaEnabled = ref(true);
    const currentFPS = ref(0);
    const currentDrawCalls = ref(0);
    const controllerCount = ref(0);

    let scene, camera, renderer, controls;
    let xrSession = null;
    let xrReferenceSpace = null;
    let xrButton = null;
    let controllers = [];
    let clock = new THREE.Clock();
    let frameCount = 0;
    let lastFpsUpdate = 0;

    // 初始化Three.js和WebXR环境
    const init = async () => {
      isLoading.value = true;
      loadingStatus.value = '初始化渲染器...';
      
      try {
        // 初始化Three.js核心组件
        initThreeJS();
        
        // 检测WebXR支持
        await checkXRSupport();
        
        // 创建场景内容
        createSceneContent();
        
        // 设置XR控制器
        setupXRControllers();
        
        // 启动渲染循环
        animate();
        
      } catch (error) {
        console.error('初始化失败:', error);
      } finally {
        isLoading.value = false;
      }
    };

    // 初始化Three.js
    const initThreeJS = () => {
      // 创建场景
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0x080808);
      
      // 创建相机
      camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.position.set(0, 1.6, 3); // 默认身高位置
      
      // 创建渲染器
      renderer = new THREE.WebGLRenderer({
        canvas: rendererCanvas.value,
        antialias: msaaEnabled.value,
        alpha: true
      });
      
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      renderer.xr.enabled = true;
      renderer.xr.setReferenceSpaceType('local-floor');
      
      // 添加VR按钮
      xrButton = VRButton.createButton(renderer);
      container.value.appendChild(xrButton);
      
      // 添加轨道控制器(非XR模式)
      controls = new OrbitControls(camera, renderer.domElement);
      controls.enableDamping = true;
    };

    // 检测WebXR支持
    const checkXRSupport = async () => {
      if (!navigator.xr) {
        xrSupport.value = false;
        throw new Error('WebXR API不可用');
      }
      
      try {
        // 检测VR支持
        canEnterVR.value = await navigator.xr.isSessionSupported('immersive-vr');
        
        // 检测AR支持
        canEnterAR.value = await navigator.xr.isSessionSupported('immersive-ar');
        
        xrSupport.value = canEnterVR.value || canEnterAR.value;
        
        if (canEnterVR.value) {
          xrDeviceType.value = 'VR设备';
        } else if (canEnterAR.value) {
          xrDeviceType.value = 'AR设备';
        }
        
      } catch (error) {
        console.error('XR支持检测失败:', error);
        xrSupport.value = false;
      }
    };

    // 创建场景内容
    const createSceneContent = () => {
      // 添加环境光
      const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
      scene.add(ambientLight);
      
      // 添加方向光
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight.position.set(5, 10, 5);
      directionalLight.castShadow = true;
      scene.add(directionalLight);
      
      // 创建地面
      const floorGeometry = new THREE.PlaneGeometry(20, 20);
      const floorMaterial = new THREE.MeshStandardMaterial({
        color: 0x888888,
        roughness: 0.8,
        metalness: 0.2
      });
      const floor = new THREE.Mesh(floorGeometry, floorMaterial);
      floor.rotation.x = -Math.PI / 2;
      floor.receiveShadow = true;
      floor.position.y = -0.1;
      scene.add(floor);
      
      // 创建交互物体
      createInteractiveObjects();
      
      // 创建环境边界指示
      createBoundaryIndicator();
    };

    // 创建交互物体
    const createInteractiveObjects = () => {
      // 创建可交互的立方体
      const cubeGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
      const cubeMaterial = new THREE.MeshStandardMaterial({
        color: 0xff0000,
        emissive: 0x440000,
        metalness: 0.7,
        roughness: 0.3
      });
      
      const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
      cube.position.set(0, 0.5, -1);
      cube.castShadow = true;
      cube.userData = { 
        interactive: true,
        originalPosition: cube.position.clone(),
        hovered: false
      };
      scene.add(cube);
      
      // 创建更多测试物体
      for (let i = 0; i < 5; i++) {
        const sphereGeometry = new THREE.SphereGeometry(0.3, 16, 16);
        const sphereMaterial = new THREE.MeshStandardMaterial({
          color: new THREE.Color().setHSL(i / 5, 0.8, 0.6),
          metalness: 0.2,
          roughness: 0.7
        });
        
        const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
        sphere.position.set(
          (i - 2) * 1.2,
          0.3,
          -2
        );
        sphere.castShadow = true;
        sphere.userData = { interactive: true };
        scene.add(sphere);
      }
    };

    // 创建边界指示
    const createBoundaryIndicator = () => {
      const boundaryGeometry = new THREE.RingGeometry(1.5, 1.8, 32);
      const boundaryMaterial = new THREE.MeshBasicMaterial({
        color: 0x00ffff,
        transparent: true,
        opacity: 0.3,
        side: THREE.DoubleSide
      });
      
      const boundary = new THREE.Mesh(boundaryGeometry, boundaryMaterial);
      boundary.rotation.x = -Math.PI / 2;
      boundary.position.y = 0.05;
      boundary.visible = false; // 默认隐藏
      boundary.name = 'boundary';
      scene.add(boundary);
    };

    // 设置XR控制器
    const setupXRControllers = () => {
      const controllerModelFactory = new XRControllerModelFactory();
      
      // 创建左右手控制器
      for (let i = 0; i < 2; i++) {
        const controller = renderer.xr.getController(i);
        controller.addEventListener('selectstart', onSelectStart);
        controller.addEventListener('selectend', onSelectEnd);
        controller.addEventListener('squeezestart', onSqueezeStart);
        controller.addEventListener('squeezeend', onSqueezeEnd);
        controller.addEventListener('connected', onControllerConnected);
        controller.addEventListener('disconnected', onControllerDisconnected);
        
        // 添加控制器模型
        const controllerGrip = renderer.xr.getControllerGrip(i);
        controllerGrip.add(controllerModelFactory.createControllerModel(controllerGrip));
        
        scene.add(controller);
        scene.add(controllerGrip);
        
        controllers.push(controller);
        
        // 创建射线指示器
        const geometry = new THREE.BufferGeometry().setFromPoints([
          new THREE.Vector3(0, 0, 0),
          new THREE.Vector3(0, 0, -1)
        ]);
        
        const line = new THREE.Line(geometry);
        line.name = 'line';
        line.scale.z = 5;
        line.visible = false;
        controller.add(line);
      }
    };

    // 控制器事件处理
    const onSelectStart = (event) => {
      const controller = event.target;
      const intersections = getControllerIntersections(controller);
      
      if (intersections.length > 0) {
        const object = intersections[0].object;
        if (object.userData.interactive) {
          // 抓取物体逻辑
          object.userData.grabbed = true;
          object.userData.controller = controller;
          object.userData.offset = new THREE.Vector3().copy(object.position)
            .sub(controller.position);
        }
      }
    };

    const onSelectEnd = (event) => {
      // 释放抓取的物体
      scene.traverse(object => {
        if (object.userData.grabbed) {
          object.userData.grabbed = false;
          object.userData.controller = null;
        }
      });
    };

    const onSqueezeStart = (event) => {
      // 传送功能
      const controller = event.target;
      const intersections = getControllerIntersections(controller, true);
      
      if (intersections.length > 0) {
        const hitPoint = intersections[0].point;
        teleportPlayer(hitPoint);
      }
    };

    const onSqueezeEnd = (event) => {
      // 传送结束
    };

    const onControllerConnected = (event) => {
      const controller = event.target;
      console.log('控制器已连接:', event.data);
      controllerCount.value++;
      
      // 显示射线指示器
      const line = controller.getObjectByName('line');
      if (line) {
        line.visible = true;
      }
    };

    const onControllerDisconnected = (event) => {
      const controller = event.target;
      console.log('控制器已断开连接');
      controllerCount.value--;
      
      // 隐藏射线指示器
      const line = controller.getObjectByName('line');
      if (line) {
        line.visible = false;
      }
    };

    // 获取控制器射线交点
    const getControllerIntersections = (controller, floorOnly = false) => {
      const tempMatrix = new THREE.Matrix4();
      tempMatrix.identity().extractRotation(controller.matrixWorld);
      
      const raycaster = new THREE.Raycaster();
      raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
      raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
      
      const objects = floorOnly ? 
        [scene.getObjectByName('floor')] : 
        scene.children.filter(obj => obj.userData.interactive);
      
      return raycaster.intersectObjects(objects, false);
    };

    // 玩家传送
    const teleportPlayer = (position) => {
      if (xrReferenceSpace) {
        // 计算相对于参考空间的偏移
        const offsetPosition = new THREE.Vector3(
          -position.x,
          -position.y,
          -position.z
        );
        
        // 创建新的参考空间
        const newReferenceSpace = xrReferenceSpace.getOffsetReferenceSpace(
          new XRRigidTransform(offsetPosition)
        );
        
        xrReferenceSpace = newReferenceSpace;
        renderer.xr.setReferenceSpace(xrReferenceSpace);
      }
    };

    // 进入VR模式
    const enterVR = async () => {
      if (!canEnterVR.value) return;
      
      isLoading.value = true;
      loadingStatus.value = '启动VR会话...';
      
      try {
        const session = await navigator.xr.requestSession('immersive-vr', {
          optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking']
        });
        
        await setupXRSession(session);
        
      } catch (error) {
        console.error('进入VR失败:', error);
        isLoading.value = false;
      }
    };

    // 进入AR模式
    const enterAR = async () => {
      if (!canEnterAR.value) return;
      
      isLoading.value = true;
      loadingStatus.value = '启动AR会话...';
      
      try {
        const session = await navigator.xr.requestSession('immersive-ar', {
          optionalFeatures: ['hit-test', 'dom-overlay']
        });
        
        await setupXRSession(session);
        
      } catch (error) {
        console.error('进入AR失败:', error);
        isLoading.value = false;
      }
    };

    // 设置XR会话
    const setupXRSession = async (session) => {
      xrSession = session;
      isXRSessionActive.value = true;
      
      // 设置会话事件监听
      session.addEventListener('end', onXRSessionEnd);
      session.addEventListener('visibilitychange', onXRVisibilityChange);
      
      // 设置参考空间
      xrReferenceSpace = await session.requestReferenceSpace('local-floor');
      
      // 连接渲染器
      await renderer.xr.setSession(session);
      
      // 显示边界指示
      const boundary = scene.getObjectByName('boundary');
      if (boundary) {
        boundary.visible = true;
      }
      
      isLoading.value = false;
    };

    // 退出XR
    const exitXR = async () => {
      if (xrSession) {
        await xrSession.end();
      }
    };

    // XR会话事件处理
    const onXRSessionEnd = () => {
      isXRSessionActive.value = false;
      xrSession = null;
      xrReferenceSpace = null;
      
      // 隐藏边界指示
      const boundary = scene.getObjectByName('boundary');
      if (boundary) {
        boundary.visible = false;
      }
      
      console.log('XR会话已结束');
    };

    const onXRVisibilityChange = () => {
      console.log('XR可见性变化:', xrSession.visibilityState);
    };

    // 更新函数
    const update = (deltaTime) => {
      // 更新控制器状态
      updateControllers();
      
      // 更新抓取的物体
      updateGrabbedObjects();
      
      // 更新性能统计
      updatePerformanceStats(deltaTime);
    };

    // 更新控制器状态
    const updateControllers = () => {
      controllers.forEach(controller => {
        const line = controller.getObjectByName('line');
        if (line && line.visible) {
          const intersections = getControllerIntersections(controller);
          
          // 更新射线长度和颜色
          if (intersections.length > 0) {
            line.scale.z = intersections[0].distance;
            line.material.color.set(intersections[0].object.userData.hovered ? 0x00ff00 : 0xffffff);
          } else {
            line.scale.z = 5;
            line.material.color.set(0xffffff);
          }
        }
      });
    };

    // 更新抓取的物体
    const updateGrabbedObjects = () => {
      scene.traverse(object => {
        if (object.userData.grabbed && object.userData.controller) {
          const controller = object.userData.controller;
          const worldPos = new THREE.Vector3();
          controller.getWorldPosition(worldPos);
          
          object.position.copy(worldPos).add(object.userData.offset);
        }
      });
    };

    // 更新性能统计
    const updatePerformanceStats = (deltaTime) => {
      frameCount++;
      lastFpsUpdate += deltaTime;
      
      if (lastFpsUpdate >= 1.0) {
        currentFPS.value = Math.round(frameCount / lastFpsUpdate);
        currentDrawCalls.value = renderer.info.render.calls;
        
        frameCount = 0;
        lastFpsUpdate = 0;
      }
    };

    // 动画循环
    const animate = () => {
      requestAnimationFrame(animate);
      
      const deltaTime = clock.getDelta();
      
      if (!isXRSessionActive) {
        controls.update();
      }
      
      update(deltaTime);
      
      if (renderer.xr.isPresenting) {
        renderer.render(scene, camera);
      } else {
        renderer.render(scene, camera);
      }
    };

    // 响应式设置
    watch(renderScale, (newScale) => {
      if (renderer.xr) {
        renderer.xr.setRenderTargetScale(newScale);
      }
    });

    watch(msaaEnabled, (newValue) => {
      if (renderer) {
        renderer.antialias = newValue;
        renderer.dispose();
        renderer.setSize(window.innerWidth, window.innerHeight);
      }
    });

    // 资源清理
    const cleanup = () => {
      if (xrSession) {
        xrSession.end();
      }
      
      if (renderer) {
        renderer.dispose();
      }
      
      if (xrButton && xrButton.parentNode) {
        xrButton.parentNode.removeChild(xrButton);
      }
    };

    onMounted(() => {
      init();
      window.addEventListener('resize', handleResize);
    });

    onUnmounted(() => {
      cleanup();
      window.removeEventListener('resize', handleResize);
    });

    const handleResize = () => {
      if (!camera || !renderer) return;
      
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    };

    return {
      container,
      rendererCanvas,
      isXRSessionActive,
      xrSupport,
      xrDeviceType,
      canEnterVR,
      canEnterAR,
      isLoading,
      loadingStatus,
      renderScale,
      msaaEnabled,
      currentFPS,
      currentDrawCalls,
      controllerCount,
      enterVR,
      enterAR,
      exitXR
    };
  }
};
</script>

<style scoped>
.xr-container {
  width: 100%;
  height: 100vh;
  position: relative;
  overflow: hidden;
}

.xr-canvas {
  width: 100%;
  height: 100%;
  display: block;
}

.xr-controls {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(10px);
}

.xr-control-panel {
  background: rgba(0, 0, 0, 0.9);
  padding: 2rem;
  border-radius: 15px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  max-width: 400px;
  width: 90%;
}

.xr-control-panel h2 {
  color: #00ffff;
  margin-bottom: 1.5rem;
  text-align: center;
}

.device-status {
  margin-bottom: 1.5rem;
}

.status-item {
  display: flex;
  justify-content: space-between;
  margin-bottom: 0.5rem;
  padding: 0.5rem;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 5px;
}

.status-value.supported {
  color: #00ff00;
}

.session-buttons {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.xr-button {
  padding: 1rem;
  border: none;
  border-radius: 8px;
  font-size: 1.1rem;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s ease;
}

.xr-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.vr-button {
  background: linear-gradient(45deg, #667eea, #764ba2);
  color: white;
}

.ar-button {
  background: linear-gradient(45deg, #f093fb, #f5576c);
  color: white;
}

.quality-settings {
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  padding-top: 1.5rem;
}

.quality-settings h3 {
  color: #00ffff;
  margin-bottom: 1rem;
  font-size: 1rem;
}

.setting-group {
  margin-bottom: 1rem;
}

.setting-group label {
  display: block;
  margin-bottom: 0.5rem;
  color: #ccc;
}

.setting-group input[type="range"] {
  width: 100%;
}

.setting-group input[type="checkbox"] {
  margin-left: 0.5rem;
}

.xr-session-info {
  position: absolute;
  top: 1rem;
  left: 1rem;
  background: rgba(0, 0, 0, 0.7);
  padding: 1rem;
  border-radius: 8px;
  backdrop-filter: blur(10px);
}

.session-stats {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 1rem;
  color: #00ffff;
  font-size: 0.9rem;
}

.exit-button {
  padding: 0.5rem 1rem;
  background: rgba(255, 0, 0, 0.3);
  border: 1px solid rgba(255, 0, 0, 0.5);
  border-radius: 5px;
  color: white;
  cursor: pointer;
  transition: background 0.3s ease;
}

.exit-button:hover {
  background: rgba(255, 0, 0, 0.5);
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.9);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.loading-content {
  text-align: center;
  color: white;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid rgba(255, 255, 255, 0.1);
  border-left: 4px solid #00ffff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 1rem;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.loading-details {
  color: #00ffff;
  font-size: 0.9rem;
  margin-top: 0.5rem;
}
</style>

高级XR特性实现

手部追踪集成

class HandTrackingManager {
  constructor(renderer, scene) {
    this.renderer = renderer;
    this.scene = scene;
    this.handMeshes = new Map();
    this.isHandTracking = false;
    
    this.initHandModels();
  }

  async initHandModels() {
    // 加载手部模型
    const handGeometry = await this.createHandGeometry();
    const handMaterial = new THREE.MeshBasicMaterial({
      color: 0x00ffff,
      transparent: true,
      opacity: 0.8
    });
    
    // 创建左右手模型
    this.handMeshes.set('left', new THREE.Mesh(handGeometry, handMaterial));
    this.handMeshes.set('right', new THREE.Mesh(handGeometry, handMaterial));
    
    this.handMeshes.forEach(hand => {
      hand.visible = false;
      this.scene.add(hand);
    });
  }

  async enableHandTracking(session) {
    try {
      // 请求手部追踪功能
      await session.updateRenderState({
        optionalFeatures: ['hand-tracking']
      });
      
      // 监听手部追踪数据
      session.addEventListener('handtracking', this.onHandTracking.bind(this));
      
      this.isHandTracking = true;
      
    } catch (error) {
      console.warn('手部追踪不可用:', error);
    }
  }

  onHandTracking(event) {
    const { hands } = event.data;
    
    hands.forEach(hand => {
      const handMesh = this.handMeshes.get(hand.handedness);
      if (handMesh) {
        this.updateHandPose(handMesh, hand);
      }
    });
  }

  updateHandPose(handMesh, handData) {
    handMesh.visible = true;
    
    // 更新手部关节位置
    // 这里需要根据handData中的关节数据更新手部模型
    // 简化实现:只更新整体位置
    handMesh.position.fromArray(handData.joints[0].position);
    handMesh.quaternion.fromArray(handData.joints[0].rotation);
  }

  createHandGeometry() {
    // 创建简化手部模型
    const geometry = new THREE.BoxGeometry(0.05, 0.1, 0.02);
    return geometry;
  }
}

空间锚点与持久化

class SpatialAnchorManager {
  constructor() {
    this.anchors = new Map();
    this.persistentAnchors = new Set();
  }

  async createAnchor(position, rotation, persistent = false) {
    try {
      // 创建XR锚点
      const anchorPose = new XRRigidTransform(position, rotation);
      const anchor = await xrSession.createAnchor(anchorPose, xrReferenceSpace);
      
      const anchorData = {
        anchor,
        position: position.clone(),
        rotation: rotation.clone(),
        createdAt: Date.now(),
        persistent
      };
      
      this.anchors.set(anchor, anchorData);
      
      if (persistent) {
        this.persistentAnchors.add(anchor);
        this.savePersistentAnchors();
      }
      
      return anchor;
      
    } catch (error) {
      console.error('创建锚点失败:', error);
      return null;
    }
  }

  async restorePersistentAnchors() {
    const savedAnchors = this.loadPersistentAnchors();
    
    for (const anchorData of savedAnchors) {
      await this.createAnchor(
        new THREE.Vector3().fromArray(anchorData.position),
        new THREE.Quaternion().fromArray(anchorData.rotation),
        true
      );
    }
  }

  savePersistentAnchors() {
    const anchorsToSave = Array.from(this.persistentAnchors).map(anchor => {
      const data = this.anchors.get(anchor);
      return {
        position: data.position.toArray(),
        rotation: data.rotation.toArray(),
        createdAt: data.createdAt
      };
    });
    
    localStorage.setItem('xr_persistent_anchors', JSON.stringify(anchorsToSave));
  }

  loadPersistentAnchors() {
    const saved = localStorage.getItem('xr_persistent_anchors');
    return saved ? JSON.parse(saved) : [];
  }
}

注意事项与最佳实践

  1. 性能优化关键点

    • 维持稳定的90FPS帧率
    • 使用实例化渲染减少draw calls
    • 实现基于视口的LOD系统
    • 优化着色器复杂度
  2. 用户体验最佳实践

    • 提供舒适的移动机制(传送/连续移动)
    • 实现适当的运动模糊和减震效果
    • 提供清晰的用户界面和反馈
    • 处理VR不适症(VR sickness)的缓解措施
  3. 设备兼容性处理

    • 检测设备能力并自适应调整
    • 提供多种输入方式支持
    • 处理不同设备的渲染特性差异

下一节预告

第26节:GPU加速计算与Compute Shader探索
将深入探讨WebGPU技术在现代浏览器中的应用,包括:Compute Shader原理、GPU并行计算、物理模拟加速、以及如何利用GPU进行通用计算来提升3D应用的性能和视觉效果。


网站公告

今日签到

点亮在社区的每一天
去签到