Vue3结合three和babylonjs实现3D数字展厅效果

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续分享更多前端和AI辅助前端编码新知识~~

写点笔记写点生活~写点经验。

在当前环境下,纯前端开发者可以通过技术深化、横向扩展、切入新兴领域以及产品化思维找到突破口。

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

基于Vue3+Three.js实现3D数字展厅

一、项目概述

3D数字展厅是一种虚拟展示空间,通过Web技术在浏览器中呈现,让用户可以沉浸式体验展览内容。本文档详细记录了基于Vue3和Three.js实现3D数字展厅的完整过程。

二、技术选型

2.1 核心技术栈

  • 前端框架:Vue 3 + TypeScript
  • 3D渲染引擎:Three.js
  • 构建工具:Vite
  • 状态管理:Vue 3 Composition API

2.2 技术选型理由

  1. Vue 3

    • 更小的打包体积,提高加载速度
    • Composition API提供更灵活的代码组织方式
    • 更好的TypeScript支持
  2. Three.js

    • WebGL的成熟封装,降低3D开发门槛
    • 丰富的插件生态系统
    • 活跃的社区和详尽的文档
  3. 其他考虑的选项

    • Babylon.js:功能更全面但学习曲线较陡
    • PlayCanvas:商业项目需付费

四、实现过程

4.1 环境搭建

  1. 创建Vue3项目
bash 复制代码
npm create vue@latest my-exhibition-hall
cd my-exhibition-hall
  1. 安装依赖
bash 复制代码
npm install three@latest
npm install @types/three --save-dev

4.2 实现Three.js核心功能

创建useThree.ts组合式函数,封装Three.js的核心功能:

typescript 复制代码
import * as THREE from 'three';
import { ref } from 'vue';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

// 定义场景加载选项接口
interface SceneLoadOptions {
  onProgress?: (progress: number) => void;
  onLoad?: () => void;
}

export function useThree() {
  // Three.js核心对象
  let scene: THREE.Scene | null = null;
  let camera: THREE.PerspectiveCamera | null = null;
  let renderer: THREE.WebGLRenderer | null = null;
  let controls: OrbitControls | null = null;
  
  // 初始化Three.js场景
  const initThreeScene = (): boolean => {
    try {
      // 初始化场景
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0xf0f0f0);
      
      // 初始化摄像机
      camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.position.set(0, 1.6, 5);
      
      console.log("Three.js scene initialized");
      return true;
    } catch (error) {
      console.error("Failed to initialize Three.js scene:", error);
      return false;
    }
  };
  
  // 渲染展厅
  const renderExhibitionHall = async (
    container: HTMLElement | null,
    options: SceneLoadOptions = {}
  ): Promise<boolean> => {
    if (!container) {
      console.error("Container element is required");
      return false;
    }
    
    try {
      // 确保场景已初始化
      if (!scene || !camera) {
        const initialized = initThreeScene();
        if (!initialized) return false;
      }
      
      // 创建渲染器
      renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.setSize(container.clientWidth, container.clientHeight);
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      renderer.shadowMap.enabled = true;
      container.innerHTML = "";
      container.appendChild(renderer.domElement);
      
      // 设置控制器
      if (camera && renderer) {
        controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
      }
      
      // 创建基础场景
      createBasicScene();
      
      // 开始渲染循环
      startRenderLoop();
      
      // 添加窗口大小调整监听
      window.addEventListener("resize", handleResize);
      
      options.onLoad?.();
      return true;
    } catch (error) {
      console.error("Failed to render exhibition hall:", error);
      return false;
    }
  };
  
  // 其他方法...
  
  return {
    initThreeScene,
    renderExhibitionHall,
    // 其他导出的方法...
  };
}

4.3 展厅场景创建

useThree.ts中添加创建展厅场景的方法:

typescript 复制代码
// 创建基础场景
const createBasicScene = () => {
  if (!scene) return;

  // 添加地板
  const floorGeometry = new THREE.PlaneGeometry(50, 50);
  const floorMaterial = new THREE.MeshStandardMaterial({
    color: 0xeeeeee,
    roughness: 0.8,
  });
  const floor = new THREE.Mesh(floorGeometry, floorMaterial);
  floor.rotation.x = -Math.PI / 2;
  floor.receiveShadow = true;
  scene.add(floor);

  // 添加墙壁
  const wallMaterial = new THREE.MeshStandardMaterial({
    color: 0xffffff,
    roughness: 0.9,
  });

  // 后墙
  const backWall = new THREE.Mesh(
    new THREE.PlaneGeometry(50, 15),
    wallMaterial
  );
  backWall.position.z = -25;
  backWall.position.y = 7.5;
  scene.add(backWall);

  // 侧墙
  const leftWall = new THREE.Mesh(
    new THREE.PlaneGeometry(50, 15),
    wallMaterial
  );
  leftWall.position.x = -25;
  leftWall.position.y = 7.5;
  leftWall.rotation.y = Math.PI / 2;
  scene.add(leftWall);

  // 添加灯光
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
  scene.add(ambientLight);

  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
  directionalLight.position.set(5, 10, 7);
  directionalLight.castShadow = true;
  scene.add(directionalLight);

  // 添加展品
  addExhibits();
};

4.4 添加展品

展品添加与管理功能:

typescript 复制代码
// 添加展品
const addExhibits = () => {
  if (!scene) return;

  // 展品位置
  const exhibitPositions = [
    { x: -8, y: 1, z: -5 },
    { x: -4, y: 1, z: -5 },
    { x: 0, y: 1, z: -5 },
    { x: 4, y: 1, z: -5 },
    { x: 8, y: 1, z: -5 },
  ];

  // 创建展台
  exhibitPositions.forEach((position, index) => {
    // 展台
    const standGeometry = new THREE.BoxGeometry(3, 0.2, 2);
    const standMaterial = new THREE.MeshStandardMaterial({
      color: 0x333333,
      roughness: 0.2,
      metalness: 0.8,
    });
    const stand = new THREE.Mesh(standGeometry, standMaterial);
    stand.position.set(position.x, position.y - 0.5, position.z);
    stand.castShadow = true;
    stand.receiveShadow = true;
    scene?.add(stand);

    // 展品(用简单几何体表示)
    const geometries = [
      new THREE.SphereGeometry(0.7),
      new THREE.BoxGeometry(1, 1, 1),
      new THREE.ConeGeometry(0.6, 1.5, 32),
      new THREE.TorusGeometry(0.5, 0.2, 16, 100),
      new THREE.DodecahedronGeometry(0.7),
    ];

    const geometry = geometries[index % geometries.length];
    const material = new THREE.MeshStandardMaterial({
      color: 0x1a75ff,
      roughness: 0.4,
      metalness: 0.6,
    });
    const exhibit = new THREE.Mesh(geometry, material);
    exhibit.position.set(position.x, position.y + 0.5, position.z);
    exhibit.castShadow = true;
    
    // 添加旋转动画
    const speed = 0.005 + Math.random() * 0.005;
    exhibit.userData = { rotationSpeed: speed };
    
    // 给展品添加交互性
    makeExhibitInteractive(exhibit, `展品 ${index + 1}`);
    
    scene?.add(exhibit);
  });
};

4.5 添加交互功能

为展品添加交互功能:

typescript 复制代码
// 交互状态
const hoveredExhibit = ref<THREE.Object3D | null>(null);
const selectedExhibit = ref<THREE.Object3D | null>(null);

// 使展品可交互
const makeExhibitInteractive = (object: THREE.Object3D, name: string) => {
  object.userData.name = name;
  interactiveObjects.push(object);
};

// 设置鼠标交互
const setupInteraction = () => {
  if (!renderer) return;
  
  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2();
  
  // 鼠标移动事件
  renderer.domElement.addEventListener('mousemove', (event) => {
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
    
    if (camera && scene) {
      raycaster.setFromCamera(mouse, camera);
      const intersects = raycaster.intersectObjects(interactiveObjects);
      
      if (intersects.length > 0) {
        const object = intersects[0].object;
        hoveredExhibit.value = object;
        document.body.style.cursor = 'pointer';
      } else {
        hoveredExhibit.value = null;
        document.body.style.cursor = 'auto';
      }
    }
  });
  
  // 点击事件
  renderer.domElement.addEventListener('click', () => {
    if (hoveredExhibit.value) {
      selectedExhibit.value = hoveredExhibit.value;
      // 显示展品详情
      showExhibitDetails(selectedExhibit.value);
    }
  });
};

4.6 展品详情展示

创建展品详情组件ExhibitDetail.vue

html 复制代码
<template>
  <div v-if="exhibit" class="exhibit-detail">
    <h2>{{ exhibit.userData.name }}</h2>
    <p class="description">{{ exhibit.userData.description || '暂无描述' }}</p>
    <div class="actions">
      <button @click="closeDetail">关闭</button>
      <button @click="showMore">了解更多</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
import * as THREE from 'three';

const props = defineProps<{
  exhibit: THREE.Object3D | null
}>();

const emit = defineEmits(['close', 'more']);

const closeDetail = () => {
  emit('close');
};

const showMore = () => {
  emit('more', props.exhibit);
};
</script>

<style scoped>
.exhibit-detail {
  position: absolute;
  bottom: 2rem;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(255, 255, 255, 0.9);
  padding: 1.5rem;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  max-width: 400px;
  z-index: 100;
}

/* 其他样式 */
</style>

4.7 场景导航与控制

创建导航控制组件NavigationControls.vue

html 复制代码
<template>
  <div class="navigation-controls">
    <div class="controls-group">
      <button @click="moveCamera('up')" title="向上看">
        <span class="icon">↑</span>
      </button>
      <button @click="moveCamera('down')" title="向下看">
        <span class="icon">↓</span>
      </button>
      <button @click="moveCamera('left')" title="向左看">
        <span class="icon">←</span>
      </button>
      <button @click="moveCamera('right')" title="向右看">
        <span class="icon">→</span>
      </button>
    </div>
    <div class="zoom-controls">
      <button @click="zoom('in')" title="放大">
        <span class="icon">+</span>
      </button>
      <button @click="zoom('out')" title="缩小">
        <span class="icon">-</span>
      </button>
    </div>
    <button class="reset-button" @click="resetView()" title="重置视图">
      <span class="icon">⟲</span>
    </button>
  </div>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';

const props = defineProps<{
  controls: any
}>();

const moveCamera = (direction: 'up' | 'down' | 'left' | 'right') => {
  if (!props.controls) return;
  
  switch (direction) {
    case 'up':
      props.controls.rotateX(-0.1);
      break;
    case 'down':
      props.controls.rotateX(0.1);
      break;
    case 'left':
      props.controls.rotateY(-0.1);
      break;
    case 'right':
      props.controls.rotateY(0.1);
      break;
  }
};

const zoom = (type: 'in' | 'out') => {
  if (!props.controls) return;
  
  if (type === 'in') {
    props.controls.dollyIn(1.1);
  } else {
    props.controls.dollyOut(1.1);
  }
  
  props.controls.update();
};

const resetView = () => {
  if (!props.controls) return;
  
  props.controls.reset();
};
</script>

<style scoped>
/* 样式代码 */
</style>

4.8 整合到主视图

创建主展厅页面ExhibitionHall.vue

html 复制代码
<template>
  <div class="exhibition-hall">
    <div 
      ref="sceneContainer" 
      class="scene-container"
      :class="{ 'loading': isLoading }"
    ></div>
    
    <div v-if="isLoading" class="loading-overlay">
      <div class="spinner"></div>
      <div class="progress">{{ Math.round(loadingProgress * 100) }}%</div>
    </div>
    
    <navigation-controls 
      v-if="!isLoading" 
      :controls="threeControls"
    />
    
    <exhibit-detail 
      v-if="selectedExhibit" 
      :exhibit="selectedExhibit"
      @close="selectedExhibit = null"
      @more="showMoreDetails"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { useThree } from '@/composables/useThree';
import NavigationControls from '@/components/NavigationControls.vue';
import ExhibitDetail from '@/components/ExhibitDetail.vue';

// 场景容器引用
const sceneContainer = ref<HTMLElement | null>(null);

// 加载状态
const isLoading = ref(true);
const loadingProgress = ref(0);

// 展品选择状态
const selectedExhibit = ref(null);

// 初始化Three.js
const { 
  renderExhibitionHall, 
  disposeThreeScene,
  getControls,
  setSelectedExhibit
} = useThree();

// 获取控制器
const threeControls = ref(null);

// 展品详情展示
const showMoreDetails = (exhibit: any) => {
  // 这里可以导航到详情页或显示模态框
  console.log('显示更多详情:', exhibit.userData.name);
};

// 组件挂载后初始化3D场景
onMounted(async () => {
  if (sceneContainer.value) {
    await renderExhibitionHall(sceneContainer.value, {
      onProgress: (progress) => {
        loadingProgress.value = progress;
      },
      onLoad: () => {
        isLoading.value = false;
        threeControls.value = getControls();
      }
    });
    
    // 监听展品选择事件
    setSelectedExhibit((exhibit) => {
      selectedExhibit.value = exhibit;
    });
  }
});

// 组件销毁前清理资源
onBeforeUnmount(() => {
  disposeThreeScene();
});
</script>

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

.scene-container {
  width: 100%;
  height: 100%;
}

.scene-container.loading {
  filter: blur(3px);
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: rgba(0, 0, 0, 0.7);
  color: white;
  z-index: 10;
}

/* 其他样式 */
</style>

五、性能优化

5.1 模型优化

  1. 使用合适的几何体

    • 尽量使用低多边形模型
    • 通过LOD (Level of Detail) 技术根据距离显示不同精度的模型
  2. 材质优化

    • 尽量复用材质
    • 使用纹理图谱(Texture Atlas)合并多个纹理
    • 对高分辨率纹理使用MIP映射

5.2 渲染优化

typescript 复制代码
// 使用实例化渲染重复对象
const createInstancedMeshes = () => {
  if (!scene) return;
  
  // 创建实例化网格
  const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
  const material = new THREE.MeshStandardMaterial({
    color: 0xaaaaaa,
    roughness: 0.8
  });
  
  // 创建100个实例
  const instancedMesh = new THREE.InstancedMesh(geometry, material, 100);
  
  // 设置每个实例的位置、旋转和缩放
  const matrix = new THREE.Matrix4();
  let i = 0;
  
  for (let x = -5; x < 5; x++) {
    for (let z = -5; z < 5; z++) {
      const position = new THREE.Vector3(x * 2, 0.25, z * 2);
      const scale = new THREE.Vector3(1, 1, 1);
      const quaternion = new THREE.Quaternion();
      
      matrix.compose(position, quaternion, scale);
      instancedMesh.setMatrixAt(i, matrix);
      i++;
    }
  }
  
  scene.add(instancedMesh);
};

5.3 其他优化技巧

  1. 渲染优化

    • 使用renderer.setPixelRatio限制像素比
    • 在交互暂停时降低渲染频率
  2. 资源管理

    • 实现资源预加载
    • 使用纹理压缩格式
  3. 场景管理

    • 实现场景分区和视锥体剔除
    • 使用Object Pooling管理频繁创建/销毁的对象

六、交互设计

6.1 交互实现

typescript 复制代码
// 实现射线拾取
const setupRaycaster = () => {
  const raycaster = new THREE.Raycaster();
  const pointer = new THREE.Vector2();
  
  // 鼠标移动
  const onPointerMove = (event: MouseEvent) => {
    if (!renderer || !camera || !scene) return;
    
    pointer.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1;
    pointer.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1;
    
    raycaster.setFromCamera(pointer, camera);
    
    const intersects = raycaster.intersectObjects(interactiveObjects);
    
    if (intersects.length > 0) {
      const object = intersects[0].object;
      // 处理hover效果
      if (hoverObject !== object) {
        if (hoverObject) resetHoverEffect(hoverObject);
        applyHoverEffect(object);
        hoverObject = object;
      }
    } else if (hoverObject) {
      resetHoverEffect(hoverObject);
      hoverObject = null;
    }
  };
  
  // 点击事件
  const onClick = () => {
    if (hoverObject) {
      // 处理点击效果
      selectObject(hoverObject);
    }
  };
  
  // 添加事件监听
  renderer?.domElement.addEventListener('pointermove', onPointerMove);
  renderer?.domElement.addEventListener('click', onClick);
  
  // 返回清理函数
  return () => {
    renderer?.domElement.removeEventListener('pointermove', onPointerMove);
    renderer?.domElement.removeEventListener('click', onClick);
  };
};

6.2 用户体验优化

  1. 加载进度条

    • 使用进度管理器显示加载进度
    • 提供加载完成的回调
  2. 过渡动画

    • 使用GSAP库实现平滑的相机过渡
    • 实现展品聚焦的动画效果

七、响应式设计

7.1 屏幕适配

typescript 复制代码
// 响应窗口大小变化
const handleResize = () => {
  if (!camera || !renderer) return;
  
  const container = renderer.domElement.parentElement;
  if (!container) return;
  
  const width = container.clientWidth;
  const height = container.clientHeight;
  
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  
  renderer.setSize(width, height);
};

// 监听窗口大小变化
window.addEventListener('resize', handleResize);

7.2 移动设备优化

  1. 触摸控制

    • 实现拖拽、缩放、旋转的触摸手势
    • 添加虚拟控制按钮
  2. 性能调整

    • 在移动设备上降低渲染质量
    • 减少后处理效果

八、测试与调试

8.1 性能监控

typescript 复制代码
// 添加性能监控
const addPerformanceMonitoring = () => {
  const stats = new Stats();
  document.body.appendChild(stats.dom);
  
  // 在渲染循环中更新
  const originalRenderFunction = renderer.render;
  renderer.render = function() {
    stats.begin();
    originalRenderFunction.apply(this, arguments);
    stats.end();
  };
};

8.2 调试工具

  1. Three.js调试工具

    • 使用three-debugger显示场景图
    • 使用lil-gui创建调试面板
  2. 浏览器开发工具

    • 使用Chrome Performance面板分析性能瓶颈
    • 使用WebGL Inspector检查WebGL调用

九、部署与优化

9.1 资源加载策略

  1. 资源分级加载

    • 先加载低分辨率资源,再加载高分辨率资源
    • 根据用户行为预加载资源
  2. 静态资源优化

    • 使用CDN加速资源加载
    • 实现资源的缓存策略

9.2 构建优化

  1. 代码分割

    • 使用动态导入拆分大型模块
    • 使用Vite的代码分割功能
  2. 资源压缩

    • 压缩模型和纹理
    • 使用gzip/brotli压缩传输数据

十、最佳实践与经验总结

10.1 性能与体验平衡

  • 在视觉效果和性能之间找到平衡点
  • 确保在中等配置设备上也能流畅运行

10.2 代码组织

  • 使用组合式API组织复杂逻辑
  • 将Three.js逻辑与Vue组件分离

10.3 未来优化方向

  • 支持WebXR实现VR/AR体验
  • 实现更复杂的物理交互
  • 添加声音效果增强沉浸感

十一、参考资源


通过上述实现过程,我们成功打造了一个基于Vue3和Three.js的3D数字展厅,它不仅具有良好的性能和用户体验,还有很强的可扩展性,可以根据不同需求进行定制和优化。

案例效果图

案例参考代码

ts 复制代码
import * as THREE from "three";
import { ref } from "vue";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";

// 定义场景加载选项接口
interface SceneLoadOptions {
  onProgress?: (progress: number) => void;
  onLoad?: () => void;
}

// 定义Three.js场景相关的状态和方法
export function useThree() {
  // Three.js核心对象
  let scene: THREE.Scene | null = null;
  let camera: THREE.PerspectiveCamera | null = null;
  let renderer: THREE.WebGLRenderer | null = null;
  let controls: any = null; // OrbitControls类型

  // 资源和对象
  let lights: THREE.Light[] = [];
  let meshes: THREE.Mesh[] = [];
  let models: THREE.Group[] = [];

  // 动画帧ID,用于清理
  let animationFrameId: number | null = null;

  // 性能监控
  const stats = ref({
    fps: 0,
    drawCalls: 0,
    triangles: 0,
    memory: 0,
  });

  // 初始化Three.js场景
  const initThreeScene = (): boolean => {
    try {
      // 初始化场景
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0x1a1a2e);

      // 初始化摄像机
      camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.position.set(0, 1.6, 5);

      // 初始化基础灯光
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
      scene.add(ambientLight);
      lights.push(ambientLight);

      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight.position.set(5, 10, 7);
      directionalLight.castShadow = true;
      directionalLight.shadow.mapSize.width = 1024;
      directionalLight.shadow.mapSize.height = 1024;
      scene.add(directionalLight);
      lights.push(directionalLight);

      console.log("Three.js scene initialized");
      return true;
    } catch (error) {
      console.error("Failed to initialize Three.js scene:", error);
      return false;
    }
  };

  // 创建基础场景
  const createBasicScene = () => {
    if (!scene) return;

    // 创建地板
    const floorGeometry = new THREE.PlaneGeometry(50, 50);
    const floorMaterial = new THREE.MeshStandardMaterial({
      color: 0x808080,
      roughness: 0.8,
      metalness: 0.2,
    });
    const floor = new THREE.Mesh(floorGeometry, floorMaterial);
    floor.rotation.x = -Math.PI / 2;
    floor.receiveShadow = true;
    scene.add(floor);
    meshes.push(floor);

    // 创建简单商品展示台
    const displayStandGeometry = new THREE.BoxGeometry(1.5, 0.1, 1.5);
    const displayStandMaterial = new THREE.MeshStandardMaterial({
      color: 0x404040,
      roughness: 0.2,
      metalness: 0.8,
    });

    // 创建多个展示台
    const displayPositions = [
      { x: -4, y: 0.05, z: -4 },
      { x: 0, y: 0.05, z: -4 },
      { x: 4, y: 0.05, z: -4 },
      { x: -4, y: 0.05, z: 0 },
      { x: 0, y: 0.05, z: 0 },
      { x: 4, y: 0.05, z: 0 },
      { x: -4, y: 0.05, z: 4 },
      { x: 0, y: 0.05, z: 4 },
      { x: 4, y: 0.05, z: 4 },
    ];

    displayPositions.forEach((position) => {
      const stand = new THREE.Mesh(displayStandGeometry, displayStandMaterial);
      stand.position.set(position.x, position.y, position.z);
      stand.castShadow = true;
      stand.receiveShadow = true;
      scene?.add(stand);
      meshes.push(stand);
    });

    // 创建示例商品 (简单几何体)
    const productGeometries = [
      new THREE.SphereGeometry(0.5, 32, 32),
      new THREE.BoxGeometry(0.8, 0.8, 0.8),
      new THREE.CylinderGeometry(0.3, 0.3, 1, 32),
      new THREE.TorusGeometry(0.4, 0.15, 16, 100),
      new THREE.TetrahedronGeometry(0.5),
      new THREE.OctahedronGeometry(0.5),
      new THREE.DodecahedronGeometry(0.5),
      new THREE.IcosahedronGeometry(0.5),
      new THREE.ConeGeometry(0.4, 1, 32),
    ];

    const productColors = [
      0xff5555, 0x55ff55, 0x5555ff, 0xffff55, 0xff55ff, 0x55ffff, 0xffaa55,
      0xaa55ff, 0x55ffaa,
    ];

    displayPositions.forEach((position, index) => {
      const geometry = productGeometries[index % productGeometries.length];
      const material = new THREE.MeshStandardMaterial({
        color: productColors[index % productColors.length],
        roughness: 0.4,
        metalness: 0.6,
      });
      const product = new THREE.Mesh(geometry, material);
      product.position.set(position.x, position.y + 0.6, position.z);
      product.castShadow = true;
      product.receiveShadow = true;

      // 添加旋转动画
      const speed = 0.005 + Math.random() * 0.005;
      const direction = Math.random() > 0.5 ? 1 : -1;
      (product as any).userData = { rotationSpeed: speed * direction };

      scene?.add(product);
      meshes.push(product);
    });

    // 添加周围环境
    createEnvironment();

    // 加载GLTF模型
    // loadExampleModel();
  };

  // 创建环境
  const createEnvironment = () => {
    if (!scene) return;

    // 创建墙壁
    const wallMaterial = new THREE.MeshStandardMaterial({
      color: 0xe0e0e0,
      roughness: 0.9,
      metalness: 0.1,
    });

    // 后墙
    const backWall = new THREE.Mesh(
      new THREE.PlaneGeometry(50, 15),
      wallMaterial
    );
    backWall.position.z = -25;
    backWall.position.y = 7.5;
    scene.add(backWall);
    meshes.push(backWall);

    // 左墙
    const leftWall = new THREE.Mesh(
      new THREE.PlaneGeometry(50, 15),
      wallMaterial
    );
    leftWall.position.x = -25;
    leftWall.position.y = 7.5;
    leftWall.rotation.y = Math.PI / 2;
    scene.add(leftWall);
    meshes.push(leftWall);

    // 右墙
    const rightWall = new THREE.Mesh(
      new THREE.PlaneGeometry(50, 15),
      wallMaterial
    );
    rightWall.position.x = 25;
    rightWall.position.y = 7.5;
    rightWall.rotation.y = -Math.PI / 2;
    scene.add(rightWall);
    meshes.push(rightWall);

    // 天花板
    const ceiling = new THREE.Mesh(
      new THREE.PlaneGeometry(50, 50),
      wallMaterial
    );
    ceiling.position.y = 15;
    ceiling.rotation.x = Math.PI / 2;
    scene.add(ceiling);
    meshes.push(ceiling);

    // 添加灯光阵列
    const lightPositions = [
      { x: -12, y: 14.5, z: -12 },
      { x: 0, y: 14.5, z: -12 },
      { x: 12, y: 14.5, z: -12 },
      { x: -12, y: 14.5, z: 0 },
      { x: 0, y: 14.5, z: 0 },
      { x: 12, y: 14.5, z: 0 },
      { x: -12, y: 14.5, z: 12 },
      { x: 0, y: 14.5, z: 12 },
      { x: 12, y: 14.5, z: 12 },
    ];

    lightPositions.forEach((position) => {
      // 灯光外壳
      const lightFixture = new THREE.Mesh(
        new THREE.CylinderGeometry(0.4, 0.4, 0.1, 32),
        new THREE.MeshStandardMaterial({
          color: 0x333333,
          roughness: 0.8,
          metalness: 0.5,
        })
      );
      lightFixture.position.set(position.x, position.y, position.z);
      lightFixture.rotation.x = Math.PI / 2;
      scene?.add(lightFixture);
      meshes.push(lightFixture);

      // 灯光
      const pointLight = new THREE.PointLight(0xffffff, 0.5, 15);
      pointLight.position.set(position.x, position.y - 0.1, position.z);
      pointLight.castShadow = true;
      scene?.add(pointLight);
      lights.push(pointLight);
    });
  };

  // 渲染电商展示厅
  const renderEcommerceShowroom = async (
    container: HTMLElement | null,
    options: SceneLoadOptions = {}
  ): Promise<boolean> => {
    if (!container) {
      console.error("Container element is required for Three.js");
      options.onLoad?.(); // 通知调用者,即使失败也需要结束加载状态
      return false;
    }

    try {
      // 确保场景已初始化
      if (!scene || !camera) {
        const initialized = initThreeScene();
        if (!initialized) {
          options.onLoad?.();
          return false;
        }
      }

      // 创建渲染器
      renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
      renderer.setSize(container.clientWidth, container.clientHeight);
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 限制像素比以提高性能
      renderer.shadowMap.enabled = true;
      renderer.shadowMap.type = THREE.PCFSoftShadowMap;
      container.innerHTML = "";
      container.appendChild(renderer.domElement);

      // 导入OrbitControls
      const { OrbitControls } = await import(
        "three/examples/jsm/controls/OrbitControls.js"
      );

      // 确保camera非空才能创建controls
      if (camera && renderer) {
        controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        controls.maxPolarAngle = Math.PI / 1.5; // 限制相机俯视角度
        controls.minDistance = 2;
        controls.maxDistance = 20;
      }

      // 初始化基础场景
      createBasicScene();

      // 模拟加载进度
      for (let i = 0; i <= 10; i++) {
        await new Promise((resolve) => setTimeout(resolve, 200));
        const progress = i / 10;
        options.onProgress?.(progress);
      }

      // 开始渲染循环
      startRenderLoop();

      // 添加窗口大小调整监听
      window.addEventListener("resize", handleResize);

      // 通知加载完成
      options.onLoad?.();

      console.log("Three.js ecommerce showroom initialized and rendered");
      return true;
    } catch (error) {
      console.error("Failed to render Three.js ecommerce showroom:", error);
      options.onLoad?.(); // 通知调用者,即使失败也需要结束加载状态
      return false;
    }
  };

  // 加载示例GLTF模型(简化版)
  const loadExampleModel = () => {
    if (!scene) return;

    // 创建DRACO加载器实例
    const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");

    // 创建GLTF加载器
    const gltfLoader = new GLTFLoader();
    gltfLoader.setDRACOLoader(dracoLoader);

    // 加载模型 (此处为示例,实际使用时需要替换为有效的模型URL)
    // gltfLoader.load(
    //   'path/to/model.glb',
    //   (gltf) => {
    //     const model = gltf.scene;
    //     model.position.set(0, 0, 0);
    //     model.scale.set(1, 1, 1);
    //     scene?.add(model);
    //     models.push(model);
    //   },
    //   (progress) => {
    //     console.log('Model loading progress:', (progress.loaded / progress.total) * 100, '%');
    //   },
    //   (error) => {
    //     console.error('Error loading model:', error);
    //   }
    // );
  };

  // 开始渲染循环
  const startRenderLoop = () => {
    if (!renderer || !scene || !camera) {
      console.error(
        "Cannot start render loop: Three.js renderer, scene or camera not initialized"
      );
      return;
    }

    // 更新物体旋转
    meshes.forEach((mesh) => {
      if (mesh.userData && mesh.userData.rotationSpeed) {
        mesh.rotation.y += mesh.userData.rotationSpeed;
      }
    });

    // 更新控制器
    if (controls) {
      controls.update();
    }

    // 渲染场景
    renderer.render(scene, camera);

    // 继续渲染循环
    animationFrameId = requestAnimationFrame(startRenderLoop);

    // 更新性能统计
    updatePerformanceStats();
  };

  // 更新性能统计
  const updatePerformanceStats = () => {
    if (!renderer) return;

    stats.value = {
      fps: Math.round(
        1000 /
          (performance.now() - (renderer as any)._lastRender ||
            performance.now())
      ),
      drawCalls: renderer.info.render.calls,
      triangles: renderer.info.render.triangles,
      memory: Math.round(
        renderer.info.memory.geometries + renderer.info.memory.textures
      ),
    };

    (renderer as any)._lastRender = performance.now();
  };

  // 处理窗口大小调整
  const handleResize = () => {
    if (!camera || !renderer) return;

    const container = renderer.domElement.parentElement;
    if (!container) return;

    const width = container.clientWidth;
    const height = container.clientHeight;

    camera.aspect = width / height;
    camera.updateProjectionMatrix();

    renderer.setSize(width, height);
  };

  // 清理场景资源
  const disposeThreeScene = () => {
    // 停止渲染循环
    if (animationFrameId !== null) {
      cancelAnimationFrame(animationFrameId);
      animationFrameId = null;
    }

    // 移除事件监听器
    window.removeEventListener("resize", handleResize);

    // 清理网格
    meshes.forEach((mesh) => {
      if (mesh.geometry) mesh.geometry.dispose();
      if (mesh.material) {
        if (Array.isArray(mesh.material)) {
          mesh.material.forEach((material) => material.dispose());
        } else {
          mesh.material.dispose();
        }
      }
    });
    meshes = [];

    // 清理模型
    models.forEach((model) => {
      scene?.remove(model);
    });
    models = [];

    // 清理灯光
    lights.forEach((light) => {
      scene?.remove(light);
    });
    lights = [];

    // 清理渲染器
    if (renderer) {
      renderer.dispose();
      renderer.forceContextLoss();
      renderer.domElement.remove();
      renderer = null;
    }

    // 清理控制器
    if (controls) {
      controls.dispose();
      controls = null;
    }

    // 清理场景和相机
    scene = null;
    camera = null;

    console.log("Three.js resources disposed");
  };

  // 返回公共方法和状态
  return {
    initThreeScene,
    renderEcommerceShowroom,
    disposeThreeScene,
    getScene: () => scene,
    getCamera: () => camera,
    getRenderer: () => renderer,
    getControls: () => controls,
    stats,
  };
}
ts 复制代码
import * as BABYLON from "@babylonjs/core";

// 定义场景加载选项接口
interface SceneLoadOptions {
  onProgress?: (progress: number) => void;
  onLoad?: () => void;
}

// 定义Babylon.js场景相关的状态和方法
export function useBabylon() {
  // Babylon.js核心对象
  let engine: BABYLON.Engine | null = null;
  let scene: BABYLON.Scene | null = null;
  let camera: BABYLON.ArcRotateCamera | null = null;

  // 资源和对象
  let lights: BABYLON.Light[] = [];
  let meshes: BABYLON.AbstractMesh[] = [];
  let materials: BABYLON.Material[] = [];

  // 动画帧ID,用于清理
  let animationFrameId: number | null = null;

  // 初始化Babylon.js场景
  const initBabylonScene = (): boolean => {
    try {
      // Babylon.js引擎会在渲染时进行初始化
      console.log("Babylon.js ready for initialization");
      return true;
    } catch (error) {
      console.error("Failed to prepare Babylon.js:", error);
      return false;
    }
  };

  // 渲染虚拟会议空间
  const renderMeetingSpace = async (
    container: HTMLElement | null,
    options: SceneLoadOptions = {}
  ): Promise<boolean> => {
    if (!container) {
      console.error("Container element is required for Babylon.js");
      options.onLoad?.(); // 通知调用者,即使失败也需要结束加载状态
      return false;
    }

    try {
      // 检查容器尺寸
      if (container.clientWidth === 0 || container.clientHeight === 0) {
        console.warn(
          "Babylon.js container has zero dimensions, this may cause rendering issues"
        );
      }

      // 创建画布
      const canvas = document.createElement("canvas");
      canvas.style.width = "100%";
      canvas.style.height = "100%";
      container.innerHTML = "";
      container.appendChild(canvas);

      // 创建Babylon引擎
      engine = new BABYLON.Engine(canvas, true, {
        preserveDrawingBuffer: true,
        stencil: true,
        disableWebGL2Support: false,
        doNotHandleContextLost: false,
        failIfMajorPerformanceCaveat: false, // 允许在性能较差的设备上运行
      });

      // 创建场景
      scene = new BABYLON.Scene(engine);

      // 设置场景背景色
      scene.clearColor = new BABYLON.Color4(0.02, 0.02, 0.05, 1.0);

      // 创建相机
      camera = new BABYLON.ArcRotateCamera(
        "camera",
        -Math.PI / 2, // alpha
        Math.PI / 2.5, // beta
        10, // radius
        new BABYLON.Vector3(0, 0, 0), // target
        scene
      );
      camera.attachControl(canvas, true);
      camera.lowerRadiusLimit = 5;
      camera.upperRadiusLimit = 20;

      // 添加基础光照
      setupLighting();

      // 模拟加载进度
      for (let i = 0; i <= 10; i++) {
        await new Promise((resolve) => setTimeout(resolve, 200));
        const progress = i / 10;
        options.onProgress?.(progress);
      }

      // 创建会议空间
      createMeetingRoom();

      // 添加虚拟会议参与者
      addParticipants();

      // 确保尺寸正确
      engine.resize();

      // 开始渲染循环
      startRenderLoop();

      // 添加窗口大小调整监听
      window.addEventListener("resize", handleResize);

      // 通知加载完成
      options.onLoad?.();

      console.log("Babylon.js scene initialized and rendered successfully");
      return true;
    } catch (error) {
      console.error("Failed to initialize or render Babylon.js scene:", error);
      options.onLoad?.(); // 通知调用者,即使失败也需要结束加载状态
      return false;
    }
  };

  // 添加基础光照
  const setupLighting = () => {
    if (!scene) {
      console.error("Cannot setup lighting: Babylon.js scene not initialized");
      return;
    }

    // 清理现有光源
    lights.forEach((light) => {
      scene?.removeLight(light);
    });
    lights = [];

    // 添加环境光
    const hemisphericLight = new BABYLON.HemisphericLight(
      "hemisphericLight",
      new BABYLON.Vector3(0, 1, 0),
      scene
    );
    hemisphericLight.intensity = 0.5;
    hemisphericLight.diffuse = new BABYLON.Color3(0.9, 0.9, 1.0);
    hemisphericLight.groundColor = new BABYLON.Color3(0.2, 0.2, 0.3);
    lights.push(hemisphericLight);

    // 添加方向光(主光源)
    const directionalLight = new BABYLON.DirectionalLight(
      "directionalLight",
      new BABYLON.Vector3(-1, -2, -1),
      scene
    );
    directionalLight.position = new BABYLON.Vector3(10, 20, 10);
    directionalLight.intensity = 0.7;

    // 为方向光添加阴影生成器
    const shadowGenerator = new BABYLON.ShadowGenerator(1024, directionalLight);
    shadowGenerator.useBlurExponentialShadowMap = true;
    shadowGenerator.blurKernel = 32;

    // 存储阴影生成器以供后续使用
    (directionalLight as any).shadowGenerator = shadowGenerator;

    lights.push(directionalLight);

    // 添加点光源作为房间内的灯光
    const pointLight = new BABYLON.PointLight(
      "pointLight",
      new BABYLON.Vector3(0, 4, 0),
      scene
    );
    pointLight.intensity = 0.6;
    pointLight.diffuse = new BABYLON.Color3(1, 0.9, 0.7);
    lights.push(pointLight);
  };

  // 创建会议室
  const createMeetingRoom = () => {
    if (!scene) {
      console.error(
        "Cannot create meeting room: Babylon.js scene not initialized"
      );
      return;
    }

    // 清理现有网格
    meshes.forEach((mesh) => {
      if (scene) scene.removeMesh(mesh);
    });
    meshes = [];

    // 清理现有材质
    materials.forEach((material) => {
      material.dispose();
    });
    materials = [];

    // 创建会议室地板
    const floorMaterial = new BABYLON.StandardMaterial("floorMaterial", scene);
    floorMaterial.diffuseColor = new BABYLON.Color3(0.2, 0.2, 0.2);
    floorMaterial.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
    materials.push(floorMaterial);

    const floor = BABYLON.MeshBuilder.CreateGround(
      "floor",
      { width: 20, height: 20, subdivisions: 2 },
      scene
    );
    floor.receiveShadows = true;
    floor.material = floorMaterial;
    meshes.push(floor);

    // 创建会议室墙壁
    const wallMaterial = new BABYLON.StandardMaterial("wallMaterial", scene);
    wallMaterial.diffuseColor = new BABYLON.Color3(0.8, 0.8, 0.9);
    materials.push(wallMaterial);

    // 后墙
    const backWall = BABYLON.MeshBuilder.CreatePlane(
      "backWall",
      { width: 20, height: 6 },
      scene
    );
    backWall.position = new BABYLON.Vector3(0, 3, -10);
    backWall.material = wallMaterial;
    meshes.push(backWall);

    // 左墙
    const leftWall = BABYLON.MeshBuilder.CreatePlane(
      "leftWall",
      { width: 20, height: 6 },
      scene
    );
    leftWall.position = new BABYLON.Vector3(-10, 3, 0);
    leftWall.rotation.y = Math.PI / 2;
    leftWall.material = wallMaterial;
    meshes.push(leftWall);

    // 右墙
    const rightWall = BABYLON.MeshBuilder.CreatePlane(
      "rightWall",
      { width: 20, height: 6 },
      scene
    );
    rightWall.position = new BABYLON.Vector3(10, 3, 0);
    rightWall.rotation.y = -Math.PI / 2;
    rightWall.material = wallMaterial;
    meshes.push(rightWall);

    // 前墙(带门)
    const frontWallLeft = BABYLON.MeshBuilder.CreatePlane(
      "frontWallLeft",
      { width: 8, height: 6 },
      scene
    );
    frontWallLeft.position = new BABYLON.Vector3(-6, 3, 10);
    frontWallLeft.rotation.y = Math.PI;
    frontWallLeft.material = wallMaterial;
    meshes.push(frontWallLeft);

    const frontWallRight = BABYLON.MeshBuilder.CreatePlane(
      "frontWallRight",
      { width: 8, height: 6 },
      scene
    );
    frontWallRight.position = new BABYLON.Vector3(6, 3, 10);
    frontWallRight.rotation.y = Math.PI;
    frontWallRight.material = wallMaterial;
    meshes.push(frontWallRight);

    const frontWallTop = BABYLON.MeshBuilder.CreatePlane(
      "frontWallTop",
      { width: 4, height: 2 },
      scene
    );
    frontWallTop.position = new BABYLON.Vector3(0, 5, 10);
    frontWallTop.rotation.y = Math.PI;
    frontWallTop.material = wallMaterial;
    meshes.push(frontWallTop);

    // 天花板
    const ceiling = BABYLON.MeshBuilder.CreatePlane(
      "ceiling",
      { width: 20, height: 20 },
      scene
    );
    ceiling.position = new BABYLON.Vector3(0, 6, 0);
    ceiling.rotation.x = Math.PI / 2;

    const ceilingMaterial = new BABYLON.StandardMaterial(
      "ceilingMaterial",
      scene
    );
    ceilingMaterial.diffuseColor = new BABYLON.Color3(0.9, 0.9, 0.9);
    ceiling.material = ceilingMaterial;
    materials.push(ceilingMaterial);
    meshes.push(ceiling);

    // 创建会议桌
    const tableTop = BABYLON.MeshBuilder.CreateBox(
      "tableTop",
      { width: 8, height: 0.2, depth: 3 },
      scene
    );
    tableTop.position = new BABYLON.Vector3(0, 1.1, 0);

    const tableMaterial = new BABYLON.StandardMaterial("tableMaterial", scene);
    tableMaterial.diffuseColor = new BABYLON.Color3(0.4, 0.3, 0.2);
    tableMaterial.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
    tableTop.material = tableMaterial;
    materials.push(tableMaterial);
    meshes.push(tableTop);

    // 桌腿
    const legPositions = [
      new BABYLON.Vector3(-3.8, 0.5, -1.4),
      new BABYLON.Vector3(3.8, 0.5, -1.4),
      new BABYLON.Vector3(-3.8, 0.5, 1.4),
      new BABYLON.Vector3(3.8, 0.5, 1.4),
    ];

    legPositions.forEach((position, index) => {
      const leg = BABYLON.MeshBuilder.CreateBox(
        `tableLeg${index}`,
        { width: 0.2, height: 1, depth: 0.2 },
        scene
      );
      leg.position = position;
      leg.material = tableMaterial;
      meshes.push(leg);
    });

    // 创建一个演示屏幕
    const screen = BABYLON.MeshBuilder.CreatePlane(
      "screen",
      { width: 6, height: 3 },
      scene
    );
    screen.position = new BABYLON.Vector3(0, 3, -9.5);

    const screenMaterial = new BABYLON.StandardMaterial(
      "screenMaterial",
      scene
    );
    screenMaterial.diffuseColor = new BABYLON.Color3(0.1, 0.1, 0.1);
    screenMaterial.emissiveColor = new BABYLON.Color3(0.2, 0.2, 0.5);
    screen.material = screenMaterial;
    materials.push(screenMaterial);
    meshes.push(screen);

    // 创建会议室灯
    const lampBase = BABYLON.MeshBuilder.CreateCylinder(
      "lampBase",
      { height: 0.2, diameter: 0.6 },
      scene
    );
    lampBase.position = new BABYLON.Vector3(0, 5.9, 0);

    const lampBaseMaterial = new BABYLON.StandardMaterial(
      "lampBaseMaterial",
      scene
    );
    lampBaseMaterial.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5);
    lampBase.material = lampBaseMaterial;
    materials.push(lampBaseMaterial);
    meshes.push(lampBase);

    const lampShade = BABYLON.MeshBuilder.CreateCylinder(
      "lampShade",
      { height: 0.8, diameterTop: 1.2, diameterBottom: 0.6 },
      scene
    );
    lampShade.position = new BABYLON.Vector3(0, 5.4, 0);

    const lampShadeMaterial = new BABYLON.StandardMaterial(
      "lampShadeMaterial",
      scene
    );
    lampShadeMaterial.diffuseColor = new BABYLON.Color3(0.9, 0.9, 0.7);
    lampShadeMaterial.emissiveColor = new BABYLON.Color3(0.5, 0.5, 0.3);
    lampShade.material = lampShadeMaterial;
    materials.push(lampShadeMaterial);
    meshes.push(lampShade);

    // 将所有网格添加到阴影生成器
    const mainLight = lights.find(
      (light) => light.name === "directionalLight"
    ) as BABYLON.DirectionalLight;
    if (mainLight && (mainLight as any).shadowGenerator) {
      meshes.forEach((mesh) => {
        (mainLight as any).shadowGenerator.addShadowCaster(mesh);
      });
    }
  };

  // 添加会议参与者
  const addParticipants = () => {
    if (!scene) {
      console.error(
        "Cannot add participants: Babylon.js scene not initialized"
      );
      return;
    }

    // 参与者位置(围绕会议桌)
    const participantPositions = [
      { pos: new BABYLON.Vector3(-3, 0, -2), rot: 0 },
      { pos: new BABYLON.Vector3(-1.5, 0, -2), rot: 0 },
      { pos: new BABYLON.Vector3(0, 0, -2), rot: 0 },
      { pos: new BABYLON.Vector3(1.5, 0, -2), rot: 0 },
      { pos: new BABYLON.Vector3(3, 0, -2), rot: 0 },
      { pos: new BABYLON.Vector3(-3, 0, 2), rot: Math.PI },
      { pos: new BABYLON.Vector3(-1.5, 0, 2), rot: Math.PI },
      { pos: new BABYLON.Vector3(0, 0, 2), rot: Math.PI },
      { pos: new BABYLON.Vector3(1.5, 0, 2), rot: Math.PI },
      { pos: new BABYLON.Vector3(3, 0, 2), rot: Math.PI },
    ];

    // 创建参与者(简化为人形几何体)
    participantPositions.forEach((posInfo, index) => {
      createSimpleParticipant(index, posInfo.pos, posInfo.rot);
    });
  };

  // 创建简单的参与者模型
  const createSimpleParticipant = (
    index: number,
    position: BABYLON.Vector3,
    rotation: number
  ) => {
    if (!scene) {
      console.error(
        "Cannot create participant: Babylon.js scene not initialized"
      );
      return;
    }

    // 创建参与者组
    const participant = new BABYLON.TransformNode(`participant${index}`, scene);
    participant.position = position;
    participant.rotation.y = rotation;

    // 创建椅子
    const chairSeat = BABYLON.MeshBuilder.CreateBox(
      `chairSeat${index}`,
      { width: 0.8, height: 0.1, depth: 0.8 },
      scene
    );
    chairSeat.position = new BABYLON.Vector3(0, 0.5, 0);
    chairSeat.parent = participant;

    const chairBack = BABYLON.MeshBuilder.CreateBox(
      `chairBack${index}`,
      { width: 0.8, height: 1, depth: 0.1 },
      scene
    );
    chairBack.position = new BABYLON.Vector3(0, 1, -0.4);
    chairBack.parent = participant;

    const chairMaterial = new BABYLON.StandardMaterial(
      `chairMaterial${index}`,
      scene
    );
    chairMaterial.diffuseColor = new BABYLON.Color3(0.2, 0.2, 0.4);
    chairSeat.material = chairMaterial;
    chairBack.material = chairMaterial;
    materials.push(chairMaterial);
    meshes.push(chairSeat, chairBack);

    // 只为一部分座位创建人物(模拟部分座位空缺)
    if (index % 3 !== 2) {
      // 创建简单的人形(只在一些座位上)
      const bodyColor = new BABYLON.Color3(
        0.1 + Math.random() * 0.3,
        0.1 + Math.random() * 0.3,
        0.1 + Math.random() * 0.3
      );

      // 身体(躯干)
      const body = BABYLON.MeshBuilder.CreateBox(
        `body${index}`,
        { width: 0.6, height: 0.8, depth: 0.3 },
        scene
      );
      body.position = new BABYLON.Vector3(0, 1.4, 0);
      body.parent = participant;

      const bodyMaterial = new BABYLON.StandardMaterial(
        `bodyMaterial${index}`,
        scene
      );
      bodyMaterial.diffuseColor = bodyColor;
      body.material = bodyMaterial;
      materials.push(bodyMaterial);
      meshes.push(body);

      // 头部
      const head = BABYLON.MeshBuilder.CreateSphere(
        `head${index}`,
        { diameter: 0.5, segments: 16 },
        scene
      );
      head.position = new BABYLON.Vector3(0, 2, 0);
      head.parent = participant;

      const headMaterial = new BABYLON.StandardMaterial(
        `headMaterial${index}`,
        scene
      );
      headMaterial.diffuseColor = new BABYLON.Color3(0.8, 0.7, 0.6);
      head.material = headMaterial;
      materials.push(headMaterial);
      meshes.push(head);

      // 添加一些动画
      const frameRate = 10;

      // 身体轻微摆动动画
      const bodyAnimation = new BABYLON.Animation(
        `bodyAnimation${index}`,
        "rotation.y",
        frameRate,
        BABYLON.Animation.ANIMATIONTYPE_FLOAT,
        BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
      );

      const bodyKeys = [];
      bodyKeys.push({
        frame: 0,
        value: -0.05,
      });
      bodyKeys.push({
        frame: frameRate,
        value: 0.05,
      });
      bodyKeys.push({
        frame: frameRate * 2,
        value: -0.05,
      });

      bodyAnimation.setKeys(bodyKeys);
      body.animations = [bodyAnimation];

      // 头部轻微点头动画
      const headAnimation = new BABYLON.Animation(
        `headAnimation${index}`,
        "rotation.x",
        frameRate,
        BABYLON.Animation.ANIMATIONTYPE_FLOAT,
        BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
      );

      const headKeys = [];
      headKeys.push({
        frame: 0,
        value: 0,
      });
      headKeys.push({
        frame: frameRate * 0.8,
        value: 0.1,
      });
      headKeys.push({
        frame: frameRate * 2,
        value: 0,
      });

      headAnimation.setKeys(headKeys);
      head.animations = [headAnimation];

      // 随机决定是否播放动画(让一些参与者静止)
      if (Math.random() > 0.4) {
        scene.beginAnimation(
          body,
          0,
          frameRate * 2,
          true,
          Math.random() * 0.5 + 0.5
        );
      }

      if (Math.random() > 0.3) {
        scene.beginAnimation(
          head,
          0,
          frameRate * 2,
          true,
          Math.random() * 0.5 + 0.5
        );
      }
    }

    // 将所有参与者网格添加到阴影生成器
    const mainLight = lights.find(
      (light) => light.name === "directionalLight"
    ) as BABYLON.DirectionalLight;
    if (mainLight && (mainLight as any).shadowGenerator) {
      meshes.forEach((mesh) => {
        (mainLight as any).shadowGenerator.addShadowCaster(mesh);
      });
    }
  };

  // 开始渲染循环
  const startRenderLoop = () => {
    if (!engine || !scene) {
      console.error(
        "Cannot start render loop: Babylon.js engine or scene not initialized"
      );
      return;
    }

    engine.runRenderLoop(() => {
      if (scene) scene.render();
    });
  };

  // 处理窗口大小调整
  const handleResize = () => {
    if (!engine) return;

    engine.resize();
  };

  // 清理场景资源
  const disposeBabylonScene = () => {
    // 停止渲染循环
    if (engine) {
      engine.stopRenderLoop();
    }

    // 移除事件监听器
    window.removeEventListener("resize", handleResize);

    // 清理资源
    materials.forEach((material) => {
      material.dispose();
    });
    materials = [];

    meshes = [];
    lights = [];

    // 清理场景
    if (scene) {
      scene.dispose();
      scene = null;
    }

    // 清理引擎
    if (engine) {
      engine.dispose();
      engine = null;
    }

    console.log("Babylon.js resources disposed");
  };

  return {
    initBabylonScene,
    renderMeetingSpace,
    disposeBabylonScene,
  };
}
html 复制代码
<!--
 * @File name: 
 * @Author: 1079161148@qq.com
 * @Version: V1.0
 * @Date: 2025-06-10 18:42:53
 * @Description: 
 * Copyright (C) 2024-{year} Tsing Micro Technology Inc All rights reserved.
-->
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import { useThree } from "../composables/useThree";
import { useBabylon } from "../composables/useBabylon";
import BaseButton from "../components/BaseButton.vue";

// 为 XR 添加简化的类型定义
interface NavigatorWithXR extends Navigator {
  xr?: any; // 使用any类型避免复杂的XRSystem类型兼容性问题
}

// 定义场景加载选项接口
interface SceneLoadOptions {
  onProgress: (progress: number) => void;
  onLoad: () => void;
}

// 当前活动的展示模式
const activeMode = ref<"ecommerce" | "meeting">("ecommerce");

// Three.js 场景容器引用
const threeContainerRef = ref<HTMLElement | null>(null);
// Babylon.js 场景容器引用
const babylonContainerRef = ref<HTMLElement | null>(null);

// 加载状态
const isLoading = ref(true);
const loadingProgress = ref(0);

// 性能统计
const fpsValue = ref(0);
const drawCallsValue = ref(0);
const trianglesValue = ref(0);
const memoryUsage = ref(0);

// 设备类型检测
const deviceType = ref("desktop");

// 使用自定义的Three.js和Babylon.js组合式函数
const { initThreeScene, disposeThreeScene, renderEcommerceShowroom } =
  useThree();
const { initBabylonScene, disposeBabylonScene, renderMeetingSpace } =
  useBabylon();

// 切换展示模式
const switchMode = (mode: "ecommerce" | "meeting") => {
  activeMode.value = mode;
  loadScene();
};

// 加载适当的场景
const loadScene = async () => {
  isLoading.value = true;
  loadingProgress.value = 0;

  // 清理现有场景
  disposeThreeScene();
  disposeBabylonScene();

  // 定义场景加载选项
  const loadOptions: SceneLoadOptions = {
    onProgress: (progress: number) => {
      loadingProgress.value = progress;
    },
    onLoad: () => {
      isLoading.value = false;
    },
  };

  // 根据当前模式加载相应场景
  if (activeMode.value === "ecommerce") {
    // 使用Three.js加载电商展示场景
    await renderEcommerceShowroom(threeContainerRef.value, loadOptions);
  } else {
    // 使用Babylon.js加载会议空间场景
    await renderMeetingSpace(babylonContainerRef.value, loadOptions);
  }
};

// 更新性能统计信息
const updatePerformanceStats = () => {
  // 这里通常会从Three.js或Babylon.js引擎中获取真实数据
  fpsValue.value = Math.floor(55 + Math.random() * 5);
  drawCallsValue.value = Math.floor(80 + Math.random() * 20);
  trianglesValue.value = Math.floor(50000 + Math.random() * 10000);
  memoryUsage.value = Math.floor(150 + Math.random() * 50);

  // 实际应用中应该使用真实的性能数据
  setTimeout(updatePerformanceStats, 1000);
};

// 检测设备类型
const detectDeviceType = () => {
  const ua = navigator.userAgent;
  if (
    /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua)
  ) {
    deviceType.value = "mobile";
    return;
  }

  // 检测VR/AR支持
  const navigatorXR = navigator as NavigatorWithXR;
  if (navigatorXR.xr) {
    deviceType.value = "xr-capable";
    return;
  }

  deviceType.value = "desktop";
};

// 组件挂载时初始化场景
onMounted(() => {
  detectDeviceType();

  // 不在这里直接初始化Three.js和Babylon.js场景
  // 而是在DOM完全渲染后再进行初始化和场景加载
  setTimeout(() => {
    if (threeContainerRef.value && babylonContainerRef.value) {
      const threeInitialized = initThreeScene();
      const babylonInitialized = initBabylonScene();

      console.log("Three.js initialized:", threeInitialized);
      console.log("Babylon.js initialized:", babylonInitialized);

      // 只有在初始化成功后再加载场景
      if (threeInitialized && babylonInitialized) {
        loadScene();
      } else {
        console.error("初始化3D引擎失败,正在重试...");
        // 再尝试一次初始化
        setTimeout(() => {
          const retryThree = initThreeScene();
          const retryBabylon = initBabylonScene();
          console.log(
            "重试初始化结果 - Three.js:",
            retryThree,
            "Babylon.js:",
            retryBabylon
          );
          loadScene();
        }, 500);
      }
    } else {
      console.error("3D容器元素不存在,无法初始化场景");
    }

    // 开始性能监控
    updatePerformanceStats();
  }, 300); // 增加延迟时间以确保DOM已完全渲染
});

// 组件卸载前清理资源
onBeforeUnmount(() => {
  disposeThreeScene();
  disposeBabylonScene();
});
</script>

<template>
  <div class="threeDShowroom">
    <header class="showroom-header">
      <h1>3D数字展厅</h1>
      <div class="mode-switcher">
        <BaseButton
          @click="switchMode('ecommerce')"
          :type="activeMode === 'ecommerce' ? 'primary' : 'secondary'"
        >
          WebXR电商导购
        </BaseButton>
        <BaseButton
          @click="switchMode('meeting')"
          :type="activeMode === 'meeting' ? 'primary' : 'secondary'"
        >
          虚拟会议空间
        </BaseButton>
      </div>
    </header>

    <!-- 加载进度覆盖层 -->
    <div class="loading-overlay" v-if="isLoading">
      <div class="loading-content">
        <div class="loading-spinner"></div>
        <div class="loading-progress">
          加载中... {{ Math.floor(loadingProgress * 100) }}%
        </div>
        <div class="loading-tip">
          <span v-if="activeMode === 'ecommerce'"
            >优化中: 正在应用纹理压缩和实例化渲染...</span
          >
          <span v-else>优化中: 正在预加载光照贴图和优化几何体...</span>
        </div>
      </div>
    </div>

    <main class="scene-container">
      <!-- Three.js 容器 (电商展示) -->
      <div
        ref="threeContainerRef"
        class="renderer-container"
        :class="{
          active: activeMode === 'ecommerce',
          hidden: activeMode !== 'ecommerce',
        }"
      ></div>

      <!-- Babylon.js 容器 (会议空间) -->
      <div
        ref="babylonContainerRef"
        class="renderer-container"
        :class="{
          active: activeMode === 'meeting',
          hidden: activeMode !== 'meeting',
        }"
      ></div>
    </main>

    <!-- 性能监控面板 -->
    <div class="performance-panel" v-if="!isLoading">
      <div class="stat-group">
        <div class="stat-item">
          <div class="stat-label">FPS</div>
          <div
            class="stat-value"
            :class="{
              good: fpsValue > 50,
              warning: fpsValue <= 50 && fpsValue > 30,
              bad: fpsValue <= 30,
            }"
          >
            {{ fpsValue }}
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.threeDShowroom {
  position: relative;
  width: 100%;
  height: 100vh;
  background-color: #000;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.showroom-header {
  background-color: rgba(0, 0, 0, 0.7);
  padding: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  z-index: 10;
  color: white;
}

.showroom-header h1 {
  margin: 0;
  font-size: 1.5rem;
}

.mode-switcher {
  display: flex;
  gap: 0.5rem;
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.7);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
}

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

.loading-spinner {
  width: 50px;
  height: 50px;
  border: 5px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  border-top-color: white;
  animation: spin 1s linear infinite;
  margin: 0 auto 1rem;
}

.loading-progress {
  font-size: 1.2rem;
  margin-bottom: 0.5rem;
}

.loading-tip {
  font-size: 0.9rem;
  opacity: 0.8;
}

.scene-container {
  flex: 1;
  position: relative;
  background-color: #000;
  width: 100%;
  height: 100%;
}

.renderer-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  transition: opacity 0.5s ease;
}

.renderer-container.hidden {
  opacity: 0;
  pointer-events: none;
}

.renderer-container.active {
  opacity: 1;
  pointer-events: auto;
}

.performance-panel {
  position: absolute;
  bottom: 1rem;
  left: 1rem;
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  border-radius: 4px;
  padding: 0.5rem;
  z-index: 20;
}

.stat-group {
  display: flex;
  gap: 1rem;
}

.stat-item {
  text-align: center;
}

.stat-label {
  font-size: 0.8rem;
  opacity: 0.8;
}

.stat-value {
  font-size: 1.1rem;
  font-weight: bold;
}

.stat-value.good {
  color: #48bb78;
}

.stat-value.warning {
  color: #f6ad55;
}

.stat-value.bad {
  color: #f56565;
}

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

@media (max-width: 768px) {
  .showroom-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
  }
}
</style>

到此为止,一个案例展厅即完成!!

相关推荐
Carlos_sam几秒前
OpenLayers:ol-wind之渲染风场图全解析
前端·javascript
拾光拾趣录9 分钟前
闭包:从“变量怎么还没死”到写出真正健壮的模块
前端·javascript
拾光拾趣录30 分钟前
for..in 和 Object.keys 的区别:从“遍历对象属性的坑”说起
前端·javascript
OpenTiny社区40 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠1 小时前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞1 小时前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路2 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9492 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8682 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie2 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端