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: [email protected]
 * @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>

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

相关推荐
dualven_in_csdn1 小时前
搞了两天的win7批处理脚本问题
java·linux·前端
你的人类朋友2 小时前
✍️【Node.js程序员】的数据库【索引优化】指南
前端·javascript·后端
小超爱编程2 小时前
纯前端做图片压缩
开发语言·前端·javascript
银色的白2 小时前
工作记录:人物对话功能开发与集成
vue.js·学习·前端框架
应巅3 小时前
echarts 数据大屏(无UI设计 极简洁版)
前端·ui·echarts
萌萌哒草头将军3 小时前
🚀🚀🚀什么?浏览器也能修改项目源文件了?Chrome 团队开源的超强 Vite 插件!🚀🚀🚀
vue.js·react.js·vite
Jimmy3 小时前
CSS 实现描边文字效果
前端·css·html
islandzzzz4 小时前
HMTL+CSS+JS-新手小白循序渐进案例入门
前端·javascript·css·html
Senar4 小时前
网页中如何判断用户是否处于闲置状态
前端·javascript
很甜的西瓜4 小时前
typescript软渲染实现类似canvas的2d矢量图形引擎
前端·javascript·typescript·图形渲染·canvas