大家好,我是鱼樱!!!
关注公众号【鱼樱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项目
- 安装依赖
4.2 实现Three.js核心功能
创建useThree.ts组合式函数,封装Three.js的核心功能:
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,
// 其他导出的方法...
};
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
4.3 展厅场景创建
在useThree.ts中添加创建展厅场景的方法:
// 创建基础场景
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();
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
4.4 添加展品
展品添加与管理功能:
// 添加展品
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);
});
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
4.5 添加交互功能
为展品添加交互功能:
// 交互状态
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);
}
});
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
4.6 展品详情展示
创建展品详情组件ExhibitDetail.vue:
<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>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
4.7 场景导航与控制
创建导航控制组件NavigationControls.vue:
<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>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.

最低0.47元/天 解锁文章
894

被折叠的 条评论
为什么被折叠?



