Appearance
加载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格式的模型:
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);
});
gltf.scene
为整个模型部分,可以直接添加到当前场景scene中,也可以用于指定其位置(position),旋转方向(rotation),缩放大小(scale)操作。
2、调用模型动画
GLTF模型中可以存储3D建模软件中做好的动画。创建好的动画会存储在模型对象的animatinos
属性中,Three.js只需要调用播放动画即可。浏览器控制台打印加载的gltf对象,可以看到animations
中有4段动画:
我们选择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、加载压缩后的模型
加载使用上面的方法压缩后的模型时,需要用到DRACOLoader
。DRACOLoader
是 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();
});