vue3+Ts 三维模型 可放大缩小 旋转 未加载时加载中动画
时间: 2025-07-31 07:43:06 AIGC 浏览: 26
<think>我们计划使用Vue3和TypeScript来构建一个三维模型展示组件。我们将使用Three.js库来实现3D渲染和交互功能。组件将支持以下特性:
1. 加载3D模型(支持glTF格式,这是Three.js推荐的格式)
2. 实现模型的缩放(通过鼠标滚轮)
3. 实现模型的旋转(通过鼠标拖拽)
4. 在模型加载期间显示加载动画(Loading动画)
步骤:
1. 创建Vue3项目(如果已有项目则跳过)
2. 安装Three.js和相关的类型定义
3. 创建3D模型展示组件
4. 在组件中初始化Three.js场景、相机、渲染器
5. 添加轨道控制器(OrbitControls)以实现旋转和缩放
6. 加载3D模型,并在加载过程中显示加载动画
7. 处理组件的销毁,释放资源
具体实现:
第一步:安装依赖
```bash
npm install three
npm install @types/three --save-dev
```
第二步:创建组件(例如:ModelViewer.vue)
在组件中,我们将使用`<canvas>`元素作为Three.js渲染器的输出目标,并在组件挂载时初始化3D场景。
同时,我们将使用一个状态(如`isLoading`)来控制加载动画的显示。
代码结构如下:
```vue
<template>
<div class="model-viewer">
<!-- 加载动画,当isLoading为true时显示 -->
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div>
</div>
<!-- 用于Three.js渲染的canvas -->
<canvas ref="canvas"></canvas>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
export default defineComponent({
name: 'ModelViewer',
props: {
modelPath: {
type: String,
required: true,
},
},
setup(props) {
const canvas = ref<HTMLCanvasElement | null>(null);
const isLoading = ref(true);
// 声明Three.js相关变量
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let controls: OrbitControls;
let model: THREE.Group;
// 初始化场景
const initScene = () => {
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// 创建相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// 创建渲染器
if (canvas.value) {
renderer = new THREE.WebGLRenderer({ canvas: canvas.value, antialias: true });
renderer.setSize(canvas.value.clientWidth, canvas.value.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
}
// 添加轨道控制器
if (renderer) {
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼(惯性)
controls.dampingFactor = 0.05;
}
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
};
// 加载模型
const loadModel = () => {
const loader = new GLTFLoader();
loader.load(
props.modelPath,
(gltf) => {
model = gltf.scene;
scene.add(model);
// 调整相机位置,使模型在视野中
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3()).length();
camera.position.copy(center);
camera.position.z = size * 1.5; // 调整相机位置,使其能够看到整个模型
camera.lookAt(center);
controls.target.copy(center); // 设置控制器的目标点为模型中心
isLoading.value = false; // 加载完成
},
undefined,
(error) => {
console.error('An error happened:', error);
isLoading.value = false; // 即使出错,也隐藏加载动画
}
);
};
// 动画循环
const animate = () => {
requestAnimationFrame(animate);
controls.update(); // 更新控制器(阻尼效果需要)
renderer.render(scene, camera);
};
// 处理窗口大小变化
const onWindowResize = () => {
if (canvas.value) {
camera.aspect = canvas.value.clientWidth / canvas.value.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(canvas.value.clientWidth, canvas.value.clientHeight);
}
};
onMounted(() => {
if (canvas.value) {
initScene();
loadModel();
animate();
window.addEventListener('resize', onWindowResize);
}
});
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize);
// 销毁渲染器、控制器等资源
if (renderer) {
renderer.dispose();
}
if (controls) {
controls.dispose();
}
});
return {
canvas,
isLoading,
};
},
});
</script>
<style scoped>
.model-viewer {
position: relative;
width: 100%;
height: 100%;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.7);
z-index: 10;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #09f;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
```
使用组件示例:
```vue
<template>
<div style="width: 100%; height: 500px;">
<ModelViewer modelPath="/path/to/your/model.gltf" />
</div>
</template>
```
注意事项:
1. 模型路径(modelPath)需要正确,可以是相对路径或绝对URL。
2. 加载动画会在模型加载过程中显示,加载完成后消失。
3. 我们使用了OrbitControls,它默认支持鼠标左键旋转、右键平移、滚轮缩放。你也可以通过修改OrbitControls的参数来调整。
4. 在组件销毁时,我们移除了事件监听器并释放了Three.js的资源,以避免内存泄漏。
相关问题:
1. 如何调整Three.js场景的背景色和光源设置?
2. 除了glTF格式,Three.js还支持哪些3D模型格式?如何加载它们?
3. 如何优化Three.js渲染性能,特别是在复杂场景中?
4. 如何为3D模型添加点击事件交互?</think># Vue3 + TypeScript 三维模型展示组件
我将为您创建一个功能完整的三维模型展示组件,支持缩放、旋转功能,并在加载过程中显示加载动画。
## 实现思路
1. 使用Three.js作为3D渲染引擎
2. 创建Vue3组件管理3D场景
3. 添加OrbitControls实现旋转和缩放功能
4. 实现加载动画和加载状态管理
5. 使用TypeScript进行类型安全开发
下面是完整的实现代码:
```vue
<template>
<div class="model-viewer-container">
<div class="loading-overlay" v-if="isLoading">
<div class="spinner">
<div class="double-bounce1"></div>
<div class="double-bounce2"></div>
</div>
<div class="loading-text">模型加载中... {{ progress }}%</div>
</div>
<div class="canvas-container" ref="container"></div>
<div class="controls">
<button @click="resetCamera">重置视图</button>
<button @click="toggleAutoRotate">
{{ autoRotate ? '停止旋转' : '自动旋转' }}
</button>
<div class="zoom-controls">
<button @click="zoomIn">放大</button>
<button @click="zoomOut">缩小</button>
</div>
</div>
<div class="instructions">
<p>鼠标操作指南:</p>
<ul>
<li>左键拖动:旋转模型</li>
<li>滚轮:缩放模型</li>
<li>右键拖动:平移视图</li>
</ul>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
export default defineComponent({
name: 'ModelViewer',
setup() {
// 场景元素引用
const container = ref<HTMLElement | null>(null);
// 状态管理
const isLoading = ref(true);
const progress = ref(0);
const autoRotate = ref(false);
// Three.js 对象
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let controls: OrbitControls;
let model: THREE.Group;
// 动画相关
let animationFrameId: number;
// 初始化场景
const initScene = () => {
if (!container.value) return;
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// 创建相机
camera = new THREE.PerspectiveCamera(
75,
container.value.clientWidth / container.value.clientHeight,
0.1,
1000
);
camera.position.set(0, 0, 5);
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.value.appendChild(renderer.domElement);
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加轨道控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.autoRotate = autoRotate.value;
controls.autoRotateSpeed = 2.0;
// 加载模型
loadModel();
};
// 加载3D模型
const loadModel = () => {
const loader = new GLTFLoader();
// 使用DRACO压缩加载器(可选)
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
loader.setDRACOLoader(dracoLoader);
// 这里使用一个示例模型URL,实际项目中替换为您的模型路径
const modelUrl = 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf';
loader.load(
modelUrl,
(gltf) => {
model = gltf.scene;
scene.add(model);
// 调整模型位置和大小
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3()).length();
model.position.sub(center);
camera.position.z = size * 1.5;
camera.lookAt(0, 0, 0);
isLoading.value = false;
},
(xhr) => {
// 加载进度更新
progress.value = Math.round((xhr.loaded / xhr.total) * 100);
},
(error) => {
console.error('模型加载失败:', error);
isLoading.value = false;
}
);
};
// 动画循环
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
if (controls) {
controls.update();
}
if (renderer && scene && camera) {
renderer.render(scene, camera);
}
};
// 重置相机位置
const resetCamera = () => {
if (controls && camera) {
controls.reset();
camera.position.set(0, 0, 5);
camera.lookAt(0, 0, 0);
}
};
// 切换自动旋转
const toggleAutoRotate = () => {
autoRotate.value = !autoRotate.value;
if (controls) {
controls.autoRotate = autoRotate.value;
}
};
// 放大视图
const zoomIn = () => {
if (camera) {
camera.zoom += 0.2;
camera.updateProjectionMatrix();
}
};
// 缩小视图
const zoomOut = () => {
if (camera && camera.zoom > 0.2) {
camera.zoom -= 0.2;
camera.updateProjectionMatrix();
}
};
// 响应窗口大小变化
const onWindowResize = () => {
if (container.value && camera && renderer) {
camera.aspect = container.value.clientWidth / container.value.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
}
};
// 生命周期钩子
onMounted(() => {
if (container.value) {
initScene();
animate();
window.addEventListener('resize', onWindowResize);
}
});
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize);
cancelAnimationFrame(animationFrameId);
// 清理资源
if (renderer) {
renderer.dispose();
}
if (controls) {
controls.dispose();
}
});
return {
container,
isLoading,
progress,
autoRotate,
resetCamera,
toggleAutoRotate,
zoomIn,
zoomOut
};
}
});
</script>
<style scoped>
.model-viewer-container {
position: relative;
width: 100%;
height: 70vh;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
}
.canvas-container {
width: 100%;
height: 100%;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.7);
z-index: 10;
color: white;
font-size: 1.2rem;
}
.spinner {
width: 80px;
height: 80px;
position: relative;
margin-bottom: 20px;
}
.double-bounce1, .double-bounce2 {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #42b883;
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
animation: sk-bounce 2.0s infinite ease-in-out;
}
.double-bounce2 {
animation-delay: -1.0s;
}
@keyframes sk-bounce {
0%, 100% {
transform: scale(0.0);
} 50% {
transform: scale(1.0);
}
}
.loading-text {
margin-top: 20px;
font-weight: 500;
text-align: center;
}
.controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
background: rgba(0, 0, 0, 0.5);
padding: 12px 20px;
border-radius: 30px;
backdrop-filter: blur(10px);
z-index: 5;
}
.controls button {
padding: 8px 16px;
background: #42b883;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.controls button:hover {
background: #349e6e;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.zoom-controls {
display: flex;
gap: 8px;
}
.instructions {
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 10px;
max-width: 200px;
font-size: 0.9rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
z-index: 5;
}
.instructions p {
font-weight: bold;
margin-bottom: 8px;
color: #2c3e50;
}
.instructions ul {
padding-left: 20px;
margin: 0;
}
.instructions li {
margin-bottom: 5px;
color: #555;
}
</style>
```
## 功能说明
1. **3D模型展示**:使用Three.js渲染3D模型
2. **交互控制**:
- 鼠标左键拖动旋转模型
- 鼠标滚轮缩放模型
- 鼠标右键拖动平移视图
3. **加载状态管理**:
- 加载过程中显示进度动画
- 显示加载百分比
4. **控制面板**:
- 重置视图按钮
- 自动旋转开关
- 放大/缩小按钮
5. **响应式设计**:自动适应容器大小变化
## 使用说明
1. 在Vue项目中安装Three.js:
```bash
npm install three @types/three
```
2. 将组件添加到您的Vue项目中
3. 替换模型路径为您的实际模型URL
4. 自定义加载动画、场景灯光等参数以满足您的需求
## 技术要点
- 使用Three.js的GLTFLoader加载glTF格式模型
- 使用OrbitControls实现模型的旋转和缩放
- 使用DRACOLoader优化压缩模型的加载
- 响应式设计确保在不同屏幕尺寸上正常显示
- 组件卸载时正确释放资源,避免内存泄漏
## 相关问题
1. 如何加载不同格式的3D模型(如OBJ、FBX等)?
2. 如何在Three.js中实现模型点击交互和事件处理?
3. 如何优化大型3D模型的加载性能?
4. 如何在Vue中管理多个3D模型组件的状态?
5. 如何为3D模型添加自定义材质和纹理?
您可以根据实际需求调整此组件,例如更改背景颜色、添加更多控制选项或优化加载动画效果。
阅读全文
相关推荐

















