Skip to content

加载3D模型

我们可以在 Blender、Maya、3dsMax 等软件中创建模型,在 Three.js 中加载渲染。

一、模型格式

Three.js 支持多种模型格式,包括 GLTF、OBJ、FBX 等,每种格式都有其独特的特点和适用场景。其中 GLTF 格式是 Three.js 推荐的格式,它支持动画、材质、纹理等。以下两种格式是常见的模型文件格式:

1、GLTF/GLB

GLTF(GL Transmission Format)是一种开放的 3D 文件格式,专为 Web 应用设计。它支持丰富的材质、动画和压缩选项。 GLB 是 GLTF 的二进制版本,包含所有数据在一个文件中,便于传输。 GLTF/GLB 格式被广泛认为是 Web 上的“JPEG of 3D”,因为它的高效性和广泛支持。

GLTF和GLB本质上是相同的数据结构,但封装方式不同。以下是它们的详细区别:

.gltf.glb
文件类型文本格式(JSON)二进制格式(包含 JSON 和二进制数据)
可读性高(可手动查看和修改)低(需要工具解析)
资源封装方式分离:模型 .gltf + 数据 .bin + 贴图 .jpg/.png打包:模型数据、贴图等资源全部封装在一个二进制文件中
文件结构多文件结构单文件结构
贴图存储外部图片文件可内嵌为二进制
适合调试
加载速度稍慢(需加载多个文件)快(单文件加载)
体积大小略大(冗余 JSON 和外部文件)通常更小(压缩后更适合网络传输)
网络传输效率一般(多个 HTTP 请求)高(单一 HTTP 请求)
推荐使用场景调试、开发、需要可读性强的场合发布、网络传输、移动端场景

常见的3D建模软件都能导出以上格式的模型,以Blender软件为例,导出GLTF格式的模型:

image-20250712190448797

2、OBJ

OBJ 格式是一种简单的文本格式,广泛用于 3D 建模和渲染。 它支持几何形状和材质,但不支持动画。 由于其简单性,OBJ 文件通常较大,不适合复杂的场景。

二、加载模型

1、加载基础模型

这里用Three.sj官方示例中的模型为素材,加载的主要代码:

typescript
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loader = new GLTFLoader();
loader.load("/model/Soldier.glb", gltf => {
  const model = gltf.scene;
  // 设置模型位置为场景中心
  model.position.set(0, 0, 0);
  scene.add(model);
});

image-20250712195102856

gltf.scene为整个模型部分,可以直接添加到当前场景scene中,也可以用于指定其位置(position),旋转方向(rotation),缩放大小(scale)操作。

2、调用模型动画

GLTF模型中可以存储3D建模软件中做好的动画。创建好的动画会存储在模型对象的animatinos属性中,Three.js只需要调用播放动画即可。浏览器控制台打印加载的gltf对象,可以看到animations中有4段动画:

image-20250712223707050

我们选择name为Walk的动画进行播放:

typescript
let mixer: THREE.AnimationMixer;

// 加载模型
const loader = new GLTFLoader();
loader.load("/model/Soldier.glb", gltf => {
  const model = gltf.scene;

  // 设置模型位置为场景中心
  model.position.set(0, 0, 0);
  scene.add(model);

  // 设置动画
  const animations = gltf.animations;
    
  // 初始化动画混合器
  mixer = new THREE.AnimationMixer(model);

  // 播放动画(如果有)
  const walkAnimation = animations.find(animation => animation.name === "Walk");
  if (walkAnimation) {
    const walkAction = mixer.clipAction(walkAnimation);
    walkAction.play();
  }
});

animate中更新动画:

typescript
const clock = new THREE.Clock();
// 渲染循环
function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();
  mixer?.update(delta); // 每一帧都更新动画
  controls.update();
  renderer.render(scene, camera);
}

可以看到模型已经动起来了。接着换一个名称为Run的动画:

typescript
const runAnimation = animations.find(animation => animation.name === "Run");
if (runAnimation) {
    const runAction = mixer.clipAction(runAnimation);
    runAction.play();
}

模型已经跑起来了。

三、模型优化

当模型比较复杂时,模型的体积就会很大,加载会消耗较多的时间。此时可以通过压缩模型减小体积,优化加载体验。用gltf-pipeline可以实现将一个未压缩的gltf模型转换为用Draco算法压缩的模型。

Draco 是 Google 开源的一种 3D 网格压缩算法,专门用于压缩几何数据(如顶点、法线、UV 等),尤其适合 .gltf/.glb 模型中的 Mesh、Geometry、Animation 等数据。

1、下载工具

gltf-pipeline是一个npm包,可以全局安装:

bash
npm install -g gltf-pipeline

2、压缩模型

可以通过直接在命令行工具中运行压缩命令:

bash
gltf-pipeline -i Soldier.glb -o Soldier.draco.glb -d --draco.compressionLevel 9 --draco.quantizePositionBits 14
选项/参数含义
-i Soldier.glb指定输入文件,即原始模型 Soldier.glb
-o Soldier.draco.glb指定输出文件,压缩后的模型保存为 Soldier.draco.glb
-d--draco.compressMeshes开启 Draco 网格压缩
--draco.compressionLevel 9设置 Draco 的压缩等级(0–10),越高压缩越强但编码/解码更慢;这里使用了较高的 9,用于减小文件体积
--draco.quantizePositionBits 14设置顶点位置(Position)的量化精度为 14 位,值越小压缩率越高但模型精度越低;14 是一个常用的平衡值(默认是 14)

也可以写nodejs脚本调用改库实现压缩。设置不同的参数压缩后的体积也不尽相同,模型体积越大压缩的收益越大,关于gltf-pipeline的更多使用请参阅其官方使用说明。

3、加载压缩后的模型

加载使用上面的方法压缩后的模型时,需要用到DRACOLoaderDRACOLoader 是 Three.js 中用于加载 使用 Draco 压缩技术.glb/.gltf 模型的工具。

导入该工具:

typescript
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';

从本地的node_modules中找到Draco解码器文件,具体路径为:node_modules\three\examples\jsm\libs\draco

将draco文件夹拷贝到项目根目录下的public文件夹下,然后初始化DRACOLoader:

typescript
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
// 设置draco解码器文件所在位置的url
dracoLoader.setDecoderPath("/draco/");
loader.setDRACOLoader(dracoLoader);

loader.load("/model/Soldier.draco.glb", gltf => {
  // existed code
});

四、完整代码

以下是本文的主要源代码:

typescript
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";

// 场景对象
const scene = new THREE.Scene();

// 渲染器
const canvas = document.querySelector("#canvas") as HTMLCanvasElement;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);

// 相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = -2.5;
camera.position.y = 3;

// 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);

// 创建主光源
const mainLight = new THREE.DirectionalLight(0xffffff, 1);
mainLight.position.set(5, 8, -5);
mainLight.castShadow = true;
mainLight.intensity = 2;
scene.add(mainLight);

// 创建环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

let mixer: THREE.AnimationMixer;

// 加载模型
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/draco/");
loader.setDRACOLoader(dracoLoader);
loader.load("/model/Soldier.draco.glb", gltf => {
  console.log(gltf);

  const model = gltf.scene;

  // 设置模型位置为场景中心
  model.position.set(0, 0, 0);
  scene.add(model);

  // 设置动画
  const animations = gltf.animations;
  mixer = new THREE.AnimationMixer(model);

  const runAnimation = animations.find(animation => animation.name === "Run");
  if (runAnimation) {
    const runAction = mixer.clipAction(runAnimation);
    runAction.play();
  }
});

// 创建网格辅助线
const gridHelper = new THREE.GridHelper(10, 10, 0x313131, 0x313131);
scene.add(gridHelper);

const clock = new THREE.Clock();
// 渲染循环
function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();
  mixer?.update(delta);
  controls.update();
  renderer.render(scene, camera);
}
animate();

// 窗口大小改变时,更新渲染器和相机
window.addEventListener("resize", () => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
});