大家好,我是鱼樱!!!
关注公众号【鱼樱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 技术选型理由
-
Vue 3:
- 更小的打包体积,提高加载速度
- Composition API提供更灵活的代码组织方式
- 更好的TypeScript支持
-
Three.js:
- WebGL的成熟封装,降低3D开发门槛
- 丰富的插件生态系统
- 活跃的社区和详尽的文档
-
其他考虑的选项:
- Babylon.js:功能更全面但学习曲线较陡
- PlayCanvas:商业项目需付费
四、实现过程
4.1 环境搭建
- 创建Vue3项目
bash
npm create vue@latest my-exhibition-hall
cd my-exhibition-hall
- 安装依赖
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 模型优化
-
使用合适的几何体
- 尽量使用低多边形模型
- 通过LOD (Level of Detail) 技术根据距离显示不同精度的模型
-
材质优化
- 尽量复用材质
- 使用纹理图谱(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 其他优化技巧
-
渲染优化
- 使用
renderer.setPixelRatio
限制像素比 - 在交互暂停时降低渲染频率
- 使用
-
资源管理
- 实现资源预加载
- 使用纹理压缩格式
-
场景管理
- 实现场景分区和视锥体剔除
- 使用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 用户体验优化
-
加载进度条
- 使用进度管理器显示加载进度
- 提供加载完成的回调
-
过渡动画
- 使用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 移动设备优化
-
触摸控制
- 实现拖拽、缩放、旋转的触摸手势
- 添加虚拟控制按钮
-
性能调整
- 在移动设备上降低渲染质量
- 减少后处理效果
八、测试与调试
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 调试工具
-
Three.js调试工具
- 使用
three-debugger
显示场景图 - 使用
lil-gui
创建调试面板
- 使用
-
浏览器开发工具
- 使用Chrome Performance面板分析性能瓶颈
- 使用WebGL Inspector检查WebGL调用
九、部署与优化
9.1 资源加载策略
-
资源分级加载
- 先加载低分辨率资源,再加载高分辨率资源
- 根据用户行为预加载资源
-
静态资源优化
- 使用CDN加速资源加载
- 实现资源的缓存策略
9.2 构建优化
-
代码分割
- 使用动态导入拆分大型模块
- 使用Vite的代码分割功能
-
资源压缩
- 压缩模型和纹理
- 使用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>
到此为止,一个案例展厅即完成!!