第四章 Vue3 + Three.js 实战:GLTF 模型加载与交互完整方案

发布于:2025-08-31 ⋅ 阅读:(19) ⋅ 点赞:(0)

在 Web 3D 开发中,加载外部 GLTF/GLB 模型是核心需求之一,广泛应用于虚拟展厅、产品预览、游戏角色等场景。本文基于 Vue3 和 Three.js,提供一套完整的 GLTF 模型加载方案,包含加载进度反馈、错误处理、模型居中、相机适配、交互控制等功能,同时兼顾性能优化与用户体验,适合开发者直接集成到项目中。

一、效果预览与核心功能

最终实现效果

  • 模型加载:支持 GLTF/GLB 格式,兼容 DRACO 压缩模型
  • 状态反馈:加载时显示进度条,失败时提供重试按钮
  • 交互控制:鼠标拖拽旋转、滚轮缩放、右键平移模型
  • 智能适配:模型自动居中,相机自动调整视角以完整显示模型
  • 响应式:窗口缩放时场景自动适配,无变形
  • 资源管理:组件卸载时自动清理资源,避免内存泄漏

技术栈选型

技术 / 工具 版本 / 作用说明
Vue3(<script setup> 组件化开发,语法简洁高效
Three.js 3D 场景构建核心库
GLTFLoader Three.js 官方 GLTF 模型加载器
DRACOLoader 支持 DRACO 压缩模型加载
OrbitControls 相机交互控制器(旋转 / 缩放 / 平移)

二、前置知识准备

在开始前,需掌握以下基础:

  1. Vue3 核心语法:ref响应式、生命周期钩子(onMounted/onUnmounted)、watch监听
  2. Three.js 三要素:场景(Scene)、相机(Camera)、渲染器(Renderer)
  3. 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 已内置GLTFLoaderDRACOLoaderOrbitControls,无需额外安装,直接从three/addons目录导入即可。

(3)准备模型与 DRACO 解码器
  • 模型文件:将 GLTF/GLB 模型放在public/models目录下(如example.glb
  • DRACO 解码器(可选,用于加载压缩模型):
    1. Three.js 官网下载 DRACO 解码器(draco_decoder.js等文件)
    2. public目录下创建libs/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():清空场景中的所有对象;
    • 模型切换时,递归释放旧模型的geometrymaterial

六、常见问题与解决方案

1. 模型不显示

  • 原因 1:模型路径错误(如public/models目录下无对应文件);

  • 解决:检查modelPath是否正确,控制台查看是否有404错误。

  • 原因 2:模型格式不兼容(如非 GLTF/GLB,或 DRACO 版本不匹配);

  • 解决:使用Three.js 官方示例测试模型是否正常,或重新导出模型。

  • 原因 3:相机位置错误(模型在相机近裁剪面内或远裁剪面外);

  • 解决:检查camera.near/camera.far参数,或手动调整camera.position

2. 模型材质异常(如全黑、纹理丢失)

  • 原因 1:未添加光源,或光源强度不足;

  • 解决:确保initScene中添加了AmbientLightDirectionalLight,适当提高光源强度(如ambientLight强度设为1.0)。

  • 原因 2:模型纹理路径错误(GLTF 格式的纹理文件未放在正确目录);

  • 解决:确保纹理文件与 GLTF 文件路径对应,或使用 GLB 格式(纹理打包在单个文件中)。

3. 加载速度慢

  • 原因:模型体积过大(顶点数量多、纹理分辨率高);
  • 解决
    1. 使用 DRACO 压缩模型;
    2. 用 Blender 等工具简化模型顶点(降低多边形数量);
    3. 压缩纹理图片(如将4096x4096纹理压缩为1024x1024)。

4. 交互卡顿

  • 原因:模型顶点过多,导致每帧渲染耗时过长;
  • 解决
    1. 简化模型;
    2. 关闭不必要的阴影(如directionalLight.castShadow = false);
    3. 启用renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)),限制像素比(避免高分辨率屏幕过度渲染)。

七、扩展与进阶方向

掌握基础加载功能后,可根据需求扩展以下进阶功能:

  1. 模型动画控制:若 GLTF 模型包含动画,可通过gltf.animations获取动画剪辑,用THREE.AnimationMixer实现播放、暂停、进度控制;
  2. 模型标注:在模型关键位置添加 2D/3D 标注(如产品参数、零件名称),通过THREE.SpriteTHREE.Mesh实现;
  3. 多模型交互:加载多个模型,实现模型之间的碰撞检测(THREE.Raycaster)或组合展示;
  4. VR/AR 支持:集成WebXR API,实现 VR 模式(需 VR 设备)或 AR 模式(手机摄像头叠加模型);
  5. 性能优化
    • 启用 LOD(细节层次):远距离显示低多边形模型,近距离显示高多边形模型;
    • 使用I

网站公告

今日签到

点亮在社区的每一天
去签到