第四章 Vue3 + Three.js 实战:GLTF 模型加载与交互完整方案
在 Web 3D 开发中,加载外部 GLTF/GLB 模型是核心需求之一,广泛应用于虚拟展厅、产品预览、游戏角色等场景。本文基于 Vue3 和 Three.js,提供一套完整的 GLTF 模型加载方案,包含加载进度反馈、错误处理、模型居中、相机适配、交互控制等功能,同时兼顾性能优化与用户体验,适合开发者直接集成到项目中。
一、效果预览与核心功能
最终实现效果
- 模型加载:支持 GLTF/GLB 格式,兼容 DRACO 压缩模型
- 状态反馈:加载时显示进度条,失败时提供重试按钮
- 交互控制:鼠标拖拽旋转、滚轮缩放、右键平移模型
- 智能适配:模型自动居中,相机自动调整视角以完整显示模型
- 响应式:窗口缩放时场景自动适配,无变形
- 资源管理:组件卸载时自动清理资源,避免内存泄漏
技术栈选型
技术 / 工具 | 版本 / 作用说明 |
---|---|
Vue3(<script setup> ) | 组件化开发,语法简洁高效 |
Three.js | 3D 场景构建核心库 |
GLTFLoader | Three.js 官方 GLTF 模型加载器 |
DRACOLoader | 支持 DRACO 压缩模型加载 |
OrbitControls | 相机交互控制器(旋转 / 缩放 / 平移) |
二、前置知识准备
在开始前,需掌握以下基础:
- Vue3 核心语法:
ref
响应式、生命周期钩子(onMounted
/onUnmounted
)、watch
监听 - Three.js 三要素:场景(Scene)、相机(Camera)、渲染器(Renderer)
- GLTF 模型基础:了解 GLTF/GLB 格式区别(GLB 是二进制单文件,推荐使用),DRACO 压缩原理(减少模型顶点数量,降低加载体积)
若对 Three.js 基础不熟悉,可先理解核心逻辑:场景是 “容器”,相机是 “视角”,渲染器是 “画布”,三者结合才能显示 3D 内容;GLTF 加载器则是将外部模型文件解析为 Three.js 可识别的网格对象。
三、完整实现步骤
1. 项目初始化与依赖准备
(1)创建 Vue3 项目(若未创建)
npm create vue@latest
# 选择:TypeScript(可选)、<script setup>、ESLint(可选)
cd 项目名
npm install
(2)安装 Three.js
npm install three
Three.js 已内置GLTFLoader
、DRACOLoader
、OrbitControls
,无需额外安装,直接从three/addons
目录导入即可。
(3)准备模型与 DRACO 解码器
- 模型文件:将 GLTF/GLB 模型放在
public/models
目录下(如example.glb
) - DRACO 解码器(可选,用于加载压缩模型):
- 从Three.js 官网下载 DRACO 解码器(
draco_decoder.js
等文件) - 在
public
目录下创建libs/draco
文件夹,将解码器文件放入其中
- 从Three.js 官网下载 DRACO 解码器(
2. 核心组件代码(GltfModelViewer.vue)
以下是完整的模型加载组件代码,包含详细注释,可直接复制使用:
<template><div class="model-viewer"><!-- 3D场景容器 --><div ref="container" class="model-container"></div><!-- 加载进度提示 --><div class="loading" v-if="isLoading"><div class="spinner"></div><p>加载中: {{ loadProgress.toFixed(0) }}%</p></div><!-- 错误提示与重试 --><div class="error-message" v-if="errorMessage"><p>加载失败: {{ errorMessage }}</p><button @click="reloadModel">重试</button></div></div>
</template><script setup>
// 1. 导入依赖
import { onMounted, ref, onUnmounted, watch } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // 相机交互
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // GLTF加载器
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; // DRACO压缩支持// 2. 响应式状态管理
const container = ref(null); // 3D场景容器DOM
const isLoading = ref(true); // 是否正在加载
const loadProgress = ref(0); // 加载进度(0~100)
const errorMessage = ref(''); // 错误信息
const modelPath = ref('/models/example.glb'); // 模型路径(默认)// 3. Three.js核心对象(全局声明,避免函数内重复创建)
let scene, camera, renderer, controls, model;
let animationId = null; // 动画循环ID,用于卸载时清理/*** 4. 初始化3D场景:创建“容器”,添加光照*/
const initScene = () => {scene = new THREE.Scene();scene.background = new THREE.Color(0xf5f5f5); // 浅灰色背景,避免模型过暗// ① 环境光:均匀照亮场景,避免局部漆黑const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);scene.add(ambientLight);// ② 方向光:模拟太阳光,产生明暗对比,增强模型立体感const directionalLight = new THREE.DirectionalLight(0xffffff, 1);directionalLight.position.set(5, 10, 7.5); // 光源位置(斜上方)directionalLight.castShadow = true; // 启用阴影,让模型投影更真实scene.add(directionalLight);
};/*** 5. 初始化相机:定义“视角”,决定能看到模型的范围*/
const initCamera = () => {const { clientWidth, clientHeight } = container.value;// 透视相机:模拟人眼视角,近大远小camera = new THREE.PerspectiveCamera(45, // 视野角度(FOV):单位度,值越小视角越窄clientWidth / clientHeight, // 宽高比:与容器一致,避免模型变形0.1, // 近裁剪面:距离相机小于此值的物体不渲染1000 // 远裁剪面:距离相机大于此值的物体不渲染);camera.position.set(5, 5, 10); // 初始相机位置(可根据需求调整)
};/*** 6. 初始化渲染器:将3D场景“画”到浏览器画布上*/
const initRenderer = () => {const { clientWidth, clientHeight } = container.value;// 创建WebGL渲染器,开启抗锯齿(让模型边缘更平滑)renderer = new THREE.WebGLRenderer({antialias: true, // 抗锯齿alpha: false // 不透明(与背景色一致)});renderer.setSize(clientWidth, clientHeight); // 渲染器尺寸与容器一致renderer.shadowMap.enabled = true; // 启用阴影渲染// 将渲染器生成的Canvas元素添加到容器中container.value.appendChild(renderer.domElement);
};/*** 7. 初始化相机控制器:实现模型交互(旋转/缩放/平移)*/
const initControls = () => {// 绑定相机与渲染器画布,监听鼠标事件controls = new OrbitControls(camera, renderer.domElement);// 核心交互配置controls.enableDamping = true; // 启用阻尼效果(操作后有惯性,更流畅)controls.dampingFactor = 0.05; // 阻尼系数:值越小惯性越明显controls.enableZoom = true; // 允许滚轮缩放controls.zoomSpeed = 0.7; // 缩放速度controls.enableRotate = true; // 允许拖拽旋转controls.rotateSpeed = 0.5; // 旋转速度controls.enablePan = true; // 允许右键平移controls.panSpeed = 0.5; // 平移速度
};/*** 8. 加载GLTF模型:核心功能,含进度反馈与错误处理* @param {string} path - 模型文件路径*/
const loadModel = (path) => {isLoading.value = true;errorMessage.value = '';// ① 初始化DRACO加载器(支持压缩模型,可选)const dracoLoader = new DRACOLoader();// DRACO解码器路径(指向public目录下的draco文件夹)dracoLoader.setDecoderPath('/libs/draco/');// ② 初始化GLTF加载器const loader = new GLTFLoader();loader.setDRACOLoader(dracoLoader); // 关联DRACO加载器(无压缩模型可省略)// ③ 加载模型loader.load(path,// 加载成功回调(gltf) => {// 清理之前的模型(避免切换模型时内存堆积)if (model) {scene.remove(model);// 递归释放旧模型的几何体和材质资源gltf.scene.traverse((child) => {if (child.isMesh) {child.geometry.dispose(); // 释放几何体child.material.dispose(); // 释放材质}});}// 处理新模型model = gltf.scene; // gltf.scene包含模型的所有子对象// 遍历模型,启用阴影(让模型产生投影)model.traverse((child) => {if (child.isMesh) {child.castShadow = true; // 模型投射阴影child.receiveShadow = true; // 模型接收其他物体的阴影}});// ④ 模型自动居中:计算模型边界,将模型中心移到原点const box = new THREE.Box3().setFromObject(model); // 获取模型包围盒const center = box.getCenter(new THREE.Vector3()); // 计算包围盒中心model.position.sub(center); // 模型位置 = 原位置 - 中心,实现居中// ⑤ 相机自动适配:调整相机位置,确保模型完整显示const size = box.getSize(new THREE.Vector3()); // 获取模型尺寸const maxDim = Math.max(size.x, size.y, size.z); // 模型最大维度(宽/高/深)const fov = camera.fov * (Math.PI / 180); // 将角度转为弧度// 计算相机所需距离(基于三角函数,确保模型完整显示)let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));cameraZ *= 1.5; // 增加1.5倍距离,避免模型紧贴边缘camera.position.z = cameraZ; // 更新相机Z轴位置camera.lookAt(0, 0, 0); // 相机看向模型中心(原点)// ⑥ 将模型添加到场景scene.add(model);// 加载完成isLoading.value = false;},// 加载进度回调(xhr) => {loadProgress.value = (xhr.loaded / xhr.total) * 100; // 计算进度百分比},// 加载错误回调(error) => {console.error('模型加载失败:', error);isLoading.value = false;errorMessage.value = error.message || '模型加载失败,请检查路径或文件完整性';});
};/*** 9. 动画循环:持续渲染场景,确保交互流畅*/
const animate = () => {animationId = requestAnimationFrame(animate); // 递归调用,与浏览器刷新率同步controls.update(); // 关键:启用阻尼后必须更新控制器,否则惯性无效renderer.render(scene, camera); // 渲染场景
};/*** 10. 窗口缩放处理:适配场景尺寸,避免变形*/
const handleResize = () => {if (!container.value) return;const { clientWidth, clientHeight } = container.value;// 更新相机宽高比camera.aspect = clientWidth / clientHeight;camera.updateProjectionMatrix(); // 必须更新投影矩阵,否则配置不生效// 更新渲染器尺寸renderer.setSize(clientWidth, clientHeight);
};/*** 11. 重新加载模型:错误时用户可重试*/
const reloadModel = () => {loadModel(modelPath.value);
};// 12. 监听模型路径变化:支持动态切换模型
watch(modelPath, (newPath) => {loadModel(newPath);
});/*** 13. 组件挂载:初始化场景并加载模型*/
onMounted(() => {initScene();initCamera();initRenderer();initControls();loadModel(modelPath.value);animate(); // 启动动画循环// 监听窗口缩放window.addEventListener('resize', handleResize);
});/*** 14. 组件卸载:清理资源,避免内存泄漏*/
onUnmounted(() => {// 停止动画循环if (animationId) {cancelAnimationFrame(animationId);}// 移除窗口监听window.removeEventListener('resize', handleResize);// 释放控制器资源if (controls) {controls.dispose();}// 释放渲染器资源if (renderer) {renderer.dispose();}// 清空场景if (scene) {scene.clear();}
});// 15. 暴露方法给父组件:支持动态切换模型
defineExpose({setModelPath: (path) => {modelPath.value = path;}
});
</script><style scoped>
/* 16. 样式:确保场景全屏,加载/错误提示居中 */
.model-viewer {position: relative;width: 100%;height: 100%;
}.model-container {width: 100vw; /* 占满屏幕宽度 */height: 100vh; /* 占满屏幕高度 */overflow: hidden; /* 隐藏溢出内容,避免滚动条 */
}/* 加载提示样式 */
.loading {position: absolute;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(255, 255, 255, 0.8);display: flex;flex-direction: column;justify-content: center;align-items: center;z-index: 100; /* 确保在场景上方显示 */
}/* 加载动画(旋转圆环) */
.spinner {width: 50px;height: 50px;border: 5px solid #f3f3f3;border-top: 5px solid #42b983; /* Vue绿,与生态风格统一 */border-radius: 50%;animation: spin 1s linear infinite;margin-bottom: 1rem;
}/* 错误提示样式 */
.error-message {position: absolute;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(255, 255, 255, 0.9);display: flex;flex-direction: column;justify-content: center;align-items: center;z-index: 100;padding: 2rem;text-align: center;color: #e53935; /* 错误色 */
}.error-message button {margin-top: 1rem;padding: 0.5rem 1rem;background-color: #42b983;color: white;border: none;border-radius: 4px;cursor: pointer;transition: background-color 0.3s;
}.error-message button:hover {background-color: #35956a; /* 按钮hover色 */
}/* 旋转动画关键帧 */
@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }
}
</style>
四、组件使用方法
1. 基础使用(直接加载默认模型)
在父组件中引入GltfModelViewer.vue
,即可自动加载默认路径的模型:
<template><div><h1>3D模型查看器</h1><GltfModelViewer /></div>
</template><script setup>
import GltfModelViewer from './components/GltfModelViewer.vue';
</script><style>
/* 确保页面无默认边距 */
body {margin: 0;padding: 0;
}
</style>
2. 动态切换模型
通过ref
调用组件暴露的setModelPath
方法,可动态切换模型:
<template><div><button @click="switchToCarModel">加载汽车模型</button><button @click="switchToCharacterModel">加载角色模型</button><GltfModelViewer ref="modelViewer" /></div>
</template><script setup>
import { ref } from 'vue';
import GltfModelViewer from './components/GltfModelViewer.vue';const modelViewer = ref(null);// 切换到汽车模型
const switchToCarModel = () => {modelViewer.value.setModelPath('/models/car.glb');
};// 切换到角色模型
const switchToCharacterModel = () => {modelViewer.value.setModelPath('/models/character.glb');
};
</script>
五、核心功能深度解析
1. 模型加载流程与 DRACO 压缩
(1)GLTF 与 GLB 的区别
- GLTF:文本格式,包含
.gltf
(JSON 结构)、.bin
(二进制数据)、.png
(纹理)等多个文件,适合需要修改模型结构的场景。 - GLB:二进制格式,将所有资源打包为单个文件,加载速度更快,适合生产环境,推荐优先使用。
(2)DRACO 压缩的作用
DRACO 是 Google 开发的 3D 模型压缩库,可将模型的顶点数据压缩 50%~90%,大幅减少加载体积。使用时需注意:
- 解码器路径必须正确(指向
public/libs/draco
); - 若模型未压缩,可删除
DRACOLoader
相关代码,减少不必要的资源加载。
2. 模型居中与相机适配
(1)模型居中原理
通过THREE.Box3
计算模型的包围盒(包含模型所有顶点的最小立方体),再将模型位置减去包围盒中心,实现模型在场景原点居中:
const box = new THREE.Box3().setFromObject(model); // 获取包围盒
const center = box.getCenter(new THREE.Vector3()); // 计算中心
model.position.sub(center); // 居中
(2)相机适配原理
根据模型最大维度和相机视野角度,通过三角函数计算相机所需距离,确保模型完整显示:
const maxDim = Math.max(size.x, size.y, size.z); // 模型最大维度
const fov = camera.fov * (Math.PI / 180); // 角度转弧度
const cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 1.5; // 计算距离并留有余量
3. 资源清理与内存泄漏防范
组件卸载时需清理以下资源,避免内存泄漏:
- 动画循环:通过
cancelAnimationFrame
停止requestAnimationFrame
递归; - 事件监听:移除窗口
resize
事件; - Three.js 资源:
controls.dispose()
:释放控制器监听的鼠标事件;renderer.dispose()
:释放 WebGL 渲染器占用的 GPU 资源;scene.clear()
:清空场景中的所有对象;- 模型切换时,递归释放旧模型的
geometry
和material
。
六、常见问题与解决方案
1. 模型不显示
-
原因 1:模型路径错误(如
public/models
目录下无对应文件); -
解决:检查
modelPath
是否正确,控制台查看是否有404
错误。 -
原因 2:模型格式不兼容(如非 GLTF/GLB,或 DRACO 版本不匹配);
-
解决:使用Three.js 官方示例测试模型是否正常,或重新导出模型。
-
原因 3:相机位置错误(模型在相机近裁剪面内或远裁剪面外);
-
解决:检查
camera.near
/camera.far
参数,或手动调整camera.position
。
2. 模型材质异常(如全黑、纹理丢失)
-
原因 1:未添加光源,或光源强度不足;
-
解决:确保
initScene
中添加了AmbientLight
和DirectionalLight
,适当提高光源强度(如ambientLight
强度设为1.0
)。 -
原因 2:模型纹理路径错误(GLTF 格式的纹理文件未放在正确目录);
-
解决:确保纹理文件与 GLTF 文件路径对应,或使用 GLB 格式(纹理打包在单个文件中)。
3. 加载速度慢
- 原因:模型体积过大(顶点数量多、纹理分辨率高);
- 解决:
- 使用 DRACO 压缩模型;
- 用 Blender 等工具简化模型顶点(降低多边形数量);
- 压缩纹理图片(如将
4096x4096
纹理压缩为1024x1024
)。
4. 交互卡顿
- 原因:模型顶点过多,导致每帧渲染耗时过长;
- 解决:
- 简化模型;
- 关闭不必要的阴影(如
directionalLight.castShadow = false
); - 启用
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
,限制像素比(避免高分辨率屏幕过度渲染)。
七、扩展与进阶方向
掌握基础加载功能后,可根据需求扩展以下进阶功能:
- 模型动画控制:若 GLTF 模型包含动画,可通过
gltf.animations
获取动画剪辑,用THREE.AnimationMixer
实现播放、暂停、进度控制; - 模型标注:在模型关键位置添加 2D/3D 标注(如产品参数、零件名称),通过
THREE.Sprite
或THREE.Mesh
实现; - 多模型交互:加载多个模型,实现模型之间的碰撞检测(
THREE.Raycaster
)或组合展示; - VR/AR 支持:集成
WebXR
API,实现 VR 模式(需 VR 设备)或 AR 模式(手机摄像头叠加模型); - 性能优化:
- 启用 LOD(细节层次):远距离显示低多边形模型,近距离显示高多边形模型;
- 使用
I