文章目录
- [加载 .OBJ 文件](#加载 .OBJ 文件)
-
- [1. OBJ 是什么](#1. OBJ 是什么)
- [2. 如何引入 OBJLoader](#2. 如何引入 OBJLoader)
- [3. 基础加载 OBJ](#3. 基础加载 OBJ)
- [4. 只加载 OBJ 可能没有正确材质](#4. 只加载 OBJ 可能没有正确材质)
- [5. 如何引入 MTLLoader](#5. 如何引入 MTLLoader)
- [6. 正确流程:先加载 MTL,再加载 OBJ](#6. 正确流程:先加载 MTL,再加载 OBJ)
- [7. 设置资源路径](#7. 设置资源路径)
- [8. 材质可能需要双面显示](#8. 材质可能需要双面显示)
- [9. 加载进来的 root 是什么](#9. 加载进来的 root 是什么)
- [10. 可以手动替换材质](#10. 可以手动替换材质)
- [11. OBJ 和 glTF 的区别](#11. OBJ 和 glTF 的区别)
- [加载 .gltf 文件](#加载 .gltf 文件)
-
- [1. glTF 是什么](#1. glTF 是什么)
- [2. glTF优点](#2. glTF优点)
- [3. 如何引入 GLTFLoader](#3. 如何引入 GLTFLoader)
- [4. 基础加载写法](#4. 基础加载写法)
- [5. OBJ 加载和 glTF 加载的对比](#5. OBJ 加载和 glTF 加载的对比)
- [6. .gltf 和 .glb 的区别](#6. .gltf 和 .glb 的区别)
- [8. 查看 glTF 的场景图](#8. 查看 glTF 的场景图)
- [9. 通过名字查找模型中的节点](#9. 通过名字查找模型中的节点)
- [10. 让汽车沿路径运动](#10. 让汽车沿路径运动)
- [11. 开启阴影](#11. 开启阴影)
- [12. GLTFLoader 的完整常用模板](#12. GLTFLoader 的完整常用模板)
- [14. 总结](#14. 总结)
加载 .OBJ 文件
1. OBJ 是什么
.obj 是一种比较老、但很常见的 3D 模型格式。
它主要保存模型的几何信息,比如:
- 顶点位置
- 顶点法线
- UV 坐标
- 面信息
- 对象分组
- 材质名称引用
但 .obj 通常不完整保存材质细节,所以经常会配套一个 .mtl 文件。
简单理解:
javascript
.obj = 模型形状 / 顶点 / 面 / UV / 法线
.mtl = 材质 / 贴图 / 颜色参数
2. 如何引入 OBJLoader
在 Three.js 项目中,可以这样引入:
javascript
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
如果项目使用 three/examples/jsm 写法,也可以这样:
javascript
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
3. 基础加载 OBJ
最基础的加载方式:
javascript
const objLoader = new OBJLoader();
objLoader.load('resources/models/windmill/windmill.obj', (root) => {
scene.add(root);
});
这里的 root 通常是一个 Group,里面包含多个 Mesh。
可以理解成:
javascript
OBJLoader 读取 windmill.obj
解析出模型节点
把模型根对象 root 加入 scene
4. 只加载 OBJ 可能没有正确材质
.obj主要负责几何体.mtl才保存材质和贴图引用.obj文件里只是通过mtllib和usemtl引用材质
OBJ 文件里可能有类似内容:
javascript
mtllib windmill.mtl
usemtl windmill
意思是:
javascript
这个 OBJ 要使用 windmill.mtl 里的 windmill 材质
但如果不加载 .mtl,Three.js 就不知道具体材质和贴图该怎么设置。
5. 如何引入 MTLLoader
为了加载 .mtl 材质文件,需要再引入 MTLLoader:
javascript
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
当前项目里也可以这样写:
javascript
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';
6. 正确流程:先加载 MTL,再加载 OBJ
重点流程是:
javascript
const mtlLoader = new MTLLoader();
mtlLoader.load('resources/models/windmill/windmill.mtl', (mtl) => {
mtl.preload();
const objLoader = new OBJLoader();
objLoader.setMaterials(mtl);
objLoader.load('resources/models/windmill/windmill.obj', (root) => {
scene.add(root);
});
});
加载顺序很重要:
javascript
1. 用 MTLLoader 加载 windmill.mtl
2. 调用 mtl.preload()//preload 会把 MTL 里声明的材质实例化,OBJLoader 才能按材质名匹配。
3. 创建 OBJLoader
4. objLoader.setMaterials(mtl)
5. 再用 OBJLoader 加载 windmill.obj
6. scene.add(root)
其中:
javascript
mtl.preload()
会提前准备 .mtl 里声明的材质。
javascript
objLoader.setMaterials(mtl)
会告诉 OBJLoader:
javascript
加载 OBJ 时,如果遇到 usemtl,就去这个 MTL 材质库里找对应材质
7. 设置资源路径
如果 OBJ、MTL、贴图在同一个目录,通常可以设置路径:
javascript
const modelPath = 'resources/models/windmill/';
const mtlLoader = new MTLLoader();
mtlLoader.setPath(modelPath);
mtlLoader.setResourcePath(modelPath);
const objLoader = new OBJLoader();
objLoader.setPath(modelPath);
然后加载时只写文件名:
javascript
mtlLoader.load('windmill.mtl', (mtl) => {
mtl.preload();
objLoader.setMaterials(mtl);
objLoader.load('windmill.obj', (root) => {
scene.add(root);
});
});
这里:
javascript
setPath()
用于设置 .mtl 或 .obj 文件所在目录。
javascript
setResourcePath()
用于设置材质里贴图资源的查找目录。
8. 材质可能需要双面显示
材质默认只渲染正面。
对于薄片模型,比如:
- 风车叶片
- 树叶
- 纸片
- 布片
经常需要设置双面材质:
javascript
material.side = THREE.DoubleSide;
如果要给所有材质都设置双面:
javascript
for (const material of Object.values(mtl.materials)) {
material.side = THREE.DoubleSide;
}
但要注意:双面渲染比单面渲染更费性能。
9. 加载进来的 root 是什么
OBJLoader 加载出来的结果通常是:
javascript
THREE.Group
里面可能有多个子对象:
javascript
root
├─ Mesh
├─ Mesh
└─ Mesh
所以如果要统一处理模型里的所有网格,通常用:
javascript
root.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
10. 可以手动替换材质
如果 .mtl 解析结果不理想,也可以手动创建材质,然后给模型替换。
比如:
javascript
const materials = {
Material: new THREE.MeshPhongMaterial({ color: 'red' }),
windmill: new THREE.MeshPhongMaterial({ color: 'white' }),
};
root.traverse((node) => {
const material = materials[node.material?.name];
if (material) {
node.material = material;
}
});
这样可以绕过 .mtl 的不兼容问题,完全由代码控制材质效果。
11. OBJ 和 glTF 的区别
OBJ 的特点:
javascript
OBJ = 老格式,简单通用,主要保存几何
MTL = 附属材质文件
贴图 = 外部图片文件
glTF / GLB 的特点:
javascript
glTF / GLB = 更适合实时渲染和网页传输
可以包含材质、贴图、动画、场景层级
通常更容易直接在 Three.js 中使用
加载 .gltf 文件
相比 OBJ,glTF / GLB 更适合在网页和实时 3D 应用中加载模型。它不仅能保存模型几何,还能保存材质、贴图、层级结构、动画等信息,所以在 Three.js 里通常比 OBJ 更省心。
1. glTF 是什么
glTF 全称是 GL Transmission Format,可以理解成"面向 3D 图形传输和渲染的格式"。
3D 格式大概分成几类:
- 3D 编辑器格式:比如
.blend、.max、.mb - 交换格式:比如
.obj、.dae、.fbx - 应用程序格式:比如某些游戏内部格式
- 传输格式:比如
.gltf/.glb
glTF 更像是专门为网页和实时渲染准备的"传输格式"。
2. glTF优点
OBJ 比较老,主要保存几何数据,材质和贴图要靠 .mtl 配合,而且经常需要手动修路径、法线贴图、双面材质等问题。
glTF 的优点是:
- 更适合传输
- 通常体积更小
- 更适合直接渲染
- 可以包含场景层级
- 可以包含材质
- 可以包含贴图
- 可以包含动画
- 更接近 Three.js 需要的数据结构
文章里说,加载 glTF 经常是:
加载后纹理、材质等就直接工作了。
3. 如何引入 GLTFLoader
在 Three.js 项目中,需要从 examples/addons 里引入 GLTFLoader:
javascript
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
如果你的项目使用的是当前仓库这种写法,也可以这样:
javascript
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
这两种本质上都是引入 Three.js 提供的 glTF 加载器,具体路径取决于项目依赖和构建配置。
4. 基础加载写法
核心代码是:
javascript
const gltfLoader = new GLTFLoader();
const url = 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf';
gltfLoader.load(url, (gltf) => {
const root = gltf.scene;
scene.add(root);
});
这里的 gltf 对象里常见内容有:
javascript
gltf.scene // 默认场景,通常直接 scene.add(gltf.scene)
gltf.scenes // glTF 文件里的所有场景
gltf.animations // 动画剪辑数组
gltf.cameras // 文件里带的相机
gltf.asset // glTF 文件元信息
最常用的是:
javascript
scene.add(gltf.scene);
5. OBJ 加载和 glTF 加载的对比
OBJ 通常要这样:
javascript
const mtlLoader = new MTLLoader();
mtlLoader.load('windmill.mtl', (mtl) => {
mtl.preload();
const objLoader = new OBJLoader();
objLoader.setMaterials(mtl);
objLoader.load('windmill.obj', (root) => {
scene.add(root);
});
});
而 glTF 通常更简单:
javascript
const gltfLoader = new GLTFLoader();
gltfLoader.load('scene.gltf', (gltf) => {
scene.add(gltf.scene);
});
区别是:
- OBJ:几何、材质、贴图经常分开处理
- glTF:模型、材质、贴图、层级结构通常都在同一个资源体系里
6. .gltf 和 .glb 的区别
文章主要讲 .gltf,但实际开发中也常见 .glb。
简单理解:
javascript
.gltf = JSON 主文件 + 可能外带 .bin + 贴图
.glb = 二进制单文件,通常把模型、材质、贴图都打包进去
比如:
javascript
model.gltf
model.bin
texture.png
或者:
javascript
model.glb
实际项目中,.glb 更方便部署,因为通常只有一个文件。
8. 查看 glTF 的场景图
glTF 文件通常带有完整场景层级,比如城市、道路、汽车、建筑等分组。
dumpObject 函数,把场景图打印到控制台:
javascript
function dumpObject(obj, lines = [], isLast = true, prefix = '') {
const localPrefix = isLast ? '└─' : '├─';
lines.push(
`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`
);
const newPrefix = prefix + (isLast ? ' ' : '│ ');
const lastNdx = obj.children.length - 1;
obj.children.forEach((child, ndx) => {
const isLast = ndx === lastNdx;
dumpObject(child, lines, isLast, newPrefix);
});
return lines;
}
加载后调用:
javascript
gltfLoader.load('scene.gltf', (gltf) => {
const root = gltf.scene;
scene.add(root);
console.log(dumpObject(root).join('\n'));
});
这样可以看到模型里面有哪些节点,比如:
javascript
Scene
└─RootNode
├─Cars
├─Buildings
└─Roads
这对于后续查找某个对象很有用。
9. 通过名字查找模型中的节点
比如模型中所有汽车都在名为 Cars 的父节点下面,于是可以这样找:
javascript
const cars = root.getObjectByName('Cars');
然后可以操作它的子节点:
javascript
for (const car of cars.children) {
car.rotation.y = time;
}
10. 让汽车沿路径运动
可以让模型中的车子按模型中的道路运行
- 从 Blender 导出道路路径点。
- 用这些点创建
CatmullRomCurve3曲线。 - 每帧根据时间计算汽车在曲线上的位置。
- 用
lookAt让汽车朝向前方。
示意代码:
javascript
const curve = new THREE.CatmullRomCurve3(points, true);
const carPosition = new THREE.Vector3();
const carTarget = new THREE.Vector3();
function render(time) {
time *= 0.001;
const u = time * 0.01;
curve.getPointAt(u % 1, carPosition);
curve.getPointAt((u + 0.01) % 1, carTarget);
car.position.copy(carPosition);
car.lookAt(carTarget);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
这部分说明:glTF 加载出来的场景不是只能"看",也可以继续操作里面的节点做动画和交互。
11. 开启阴影
如果想让 glTF 场景有阴影,需要:
- renderer 开启阴影
- 灯光开启投射阴影
- 模型节点开启投射/接收阴影
- 调整阴影相机范围
示意代码:
javascript
renderer.shadowMap.enabled = true;
灯光:
javascript
const light = new THREE.DirectionalLight(0xffffff, 1);
light.castShadow = true;
scene.add(light);
模型:
javascript
root.traverse((obj) => {
if (obj.castShadow !== undefined) {
obj.castShadow = true;
obj.receiveShadow = true;
}
});
如果不设置这些,即使模型和灯光都加载了,也不会自动出现阴影。
12. GLTFLoader 的完整常用模板
实际项目中可以写成这样:
javascript
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const scene = new THREE.Scene();
const loader = new GLTFLoader();
loader.load(
'/assets/models/city/scene.gltf',
(gltf) => {
const root = gltf.scene;
scene.add(root);
root.traverse((obj) => {
if (obj.isMesh) {
obj.castShadow = true;
obj.receiveShadow = true;
}
});
},
(xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
},
(error) => {
console.error('GLTF 加载失败', error);
},
);
如果是 .glb,写法一样,只是 URL 换成 .glb:
javascript
loader.load('/assets/models/robot.glb', (gltf) => {
scene.add(gltf.scene);
});
14. 总结
这篇文章的核心不是只教一行 GLTFLoader.load(),而是讲完整思路:
- glTF 是更适合网页实时渲染的 3D 传输格式
- 相比 OBJ,glTF 更容易保留材质、贴图、层级和动画
- 在 Three.js 中用
GLTFLoader加载 - 加载结果通常通过
gltf.scene加入场景 - 可以打印 scene graph 查看模型内部结构
- 可以通过
getObjectByName()找到特定节点 - 加载后仍然可能需要自动取景、修正朝向、处理缩放
- 如果要动画,资源本身的层级和命名很重要
- 阴影需要 renderer、灯光、模型节点一起开启
一句话总结:
OBJ 更像"把模型几何导进来",glTF 更像"把一个准备好给实时渲染使用的 3D 场景导进来"。