07 加载 3D 模型
学习在 Three.js 中加载 GLTF/GLB、FBX、OBJ 等 3D 模型
为什么需要加载模型
手写代码创建复杂 3D 物体非常困难,实际项目通常用 Blender、Maya、3ds Max 等工具制作模型,然后导入 Three.js。
常用模型格式
| 格式 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| GLTF/GLB | 加载快、支持动画、压缩性好 | 兼容性略差 | Web 首选 |
| FBX | 支持动画、通用性好 | 体积大、需转换 | 影视级 |
| OBJ | 简单通用 | 不支持动画 | 静态模型 |
| PLY | 点云数据 | 体积大 | 3D 扫描 |
| USD | Pixar 主推 | 生态不成熟 | 影视 |
推荐使用 GLTF/GLB 格式,这是专为 Web 设计的。
GLTFLoader 加载 GLTF/GLB
基本用法
bash
pnpm add three1
js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
const loader = new GLTFLoader()
loader.load(
'/models/scene.glb', // 模型路径
(gltf) => {
const model = gltf.scene
scene.add(model)
console.log('加载成功', gltf)
},
(progress) => {
const percent = (progress.loaded / progress.total * 100).toFixed(0)
console.log(`加载进度: ${percent}%`)
},
(error) => {
console.error('加载失败:', error)
}
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GLTF 结构解析
js
loader.load('/models/scene.glb', (gltf) => {
const model = gltf.scene
// 遍历所有网格
model.traverse((child) => {
if (child.isMesh) {
// 设置阴影
child.castShadow = true
child.receiveShadow = true
// 材质优化
child.material.side = THREE.DoubleSide
}
})
// 调整模型大小和位置
model.scale.set(0.01, 0.01, 0.01) // 常见于 Blender 导出的模型
scene.add(model)
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
加载进度条
js
function loadGLTF(url) {
return new Promise((resolve, reject) => {
const loader = new GLTFLoader()
loader.load(
url,
(gltf) => resolve(gltf),
(progress) => {
const percent = (progress.loaded / progress.total * 100).toFixed(0)
updateProgressBar(percent) // 更新UI进度条
},
(error) => reject(error)
)
})
}
loadGLTF('/models/scene.glb').then((gltf) => {
scene.add(gltf.scene)
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
加载动画模型
js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { AnimationMixer } from 'three'
let mixer = null
loader.load('/models/robot.glb', (gltf) => {
const model = gltf.scene
scene.add(model)
// 创建动画混合器
mixer = new AnimationMixer(model)
// 获取所有动画剪辑
const animations = gltf.animations
if (animations && animations.length > 0) {
// 播放第一个动画
const action = mixer.clipAction(animations[0])
action.play()
}
})
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
// 更新动画
if (mixer) {
const delta = clock.getDelta()
mixer.update(delta)
}
controls.update()
renderer.render(scene, camera)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
动画切换
js
let currentAction = null
const animations = gltf.animations
function playAnimation(index) {
const newAction = mixer.clipAction(animations[index])
// 切换动画(淡入淡出)
if (currentAction) {
currentAction.fadeOut(0.3)
}
newAction.reset().fadeIn(0.3).play()
currentAction = newAction
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
DRACOLoader(解压压缩模型)
GLTF/GLB 模型经常用 Draco 算法压缩,需要先用 DRACOLoader 解压:
js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
// 创建 DRACO 解压加载器
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/')
// 设置给 GLTFLoader
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
// 加载
loader.load('/models/compressed.glb', (gltf) => {
scene.add(gltf.scene)
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
USDLoader(USD/ZAX 格式)
js
import { USDLoader } from 'three/addons/loaders/USDLoader.js'
const loader = new USDLoader()
loader.load('/models/model.usdz', (object) => {
scene.add(object)
})1
2
3
4
5
6
2
3
4
5
6
OBJLoader(OBJ 格式)
OBJ 是最简单的模型格式:
js
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'
const loader = new OBJLoader()
loader.load('/models/mesh.obj', (object) => {
// OBJ 没有材质,需要手动添加
object.traverse((child) => {
if (child.isMesh) {
child.material = new THREE.MeshStandardMaterial({ color: 0xffffff })
}
})
scene.add(object)
})1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
OBJ + MTL(带材质)
js
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js'
const mtlLoader = new MTLLoader()
mtlLoader.load('/models/mesh.mtl', (materials) => {
materials.preload()
const objLoader = new OBJLoader()
objLoader.setMaterials(materials)
objLoader.load('/models/mesh.obj', (object) => {
scene.add(object)
})
})1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
FBXLoader(FBX 格式)
bash
pnpm add three1
js
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js'
const loader = new FBXLoader()
loader.load('/models/character.fbx', (object) => {
object.traverse((child) => {
if (child.isMesh) {
child.castShadow = true
child.receiveShadow = true
}
})
scene.add(object)
})1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
实用技巧
1. 自动调整模型大小
js
function normalizeModel(model) {
const box = new THREE.Box3().setFromObject(model)
const center = box.getCenter(new THREE.Vector3())
const size = box.getSize(new THREE.Vector3())
// 将模型中心移到原点
model.position.sub(center)
// 缩放到合适大小
const maxDim = Math.max(size.x, size.y, size.z)
const scale = 5 / maxDim // 最大尺寸 5 个单位
model.scale.set(scale, scale, scale)
}
loader.load('/models/scene.glb', (gltf) => {
normalizeModel(gltf.scene)
scene.add(gltf.scene)
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2. 替换材质
js
loader.load('/models/scene.glb', (gltf) => {
gltf.scene.traverse((child) => {
if (child.isMesh) {
// 保存原纹理
const originalMap = child.material.map
// 替换为新材质
child.material = new THREE.MeshStandardMaterial({
color: 0xffffff,
map: originalMap, // 保留原纹理
metalness: 0.5,
roughness: 0.5
})
}
})
scene.add(gltf.scene)
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
3. 模型拖拽(拖放到页面)
js
const dropZone = document.getElementById('dropZone')
dropZone.addEventListener('dragover', (e) => {
e.preventDefault()
})
dropZone.addEventListener('drop', (e) => {
e.preventDefault()
const file = e.dataTransfer.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (event) => {
const contents = event.target.result
// 解析文件内容加载模型
// ...
}
reader.readAsArrayBuffer(file)
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4. 异步加载多个模型
js
async function loadModels() {
const [character, environment, props] = await Promise.all([
loadGLTF('/models/character.glb'),
loadGLTF('/models/environment.glb'),
loadGLTF('/models/props.glb')
])
scene.add(character.scene)
scene.add(environment.scene)
scene.add(props.scene)
}
loadModels()1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
模型资源网站
| 网站 | 说明 |
|---|---|
| Sketchfab | 高质量 3D 模型 |
| Three.js examples | 官方示例模型 |
| Poly Pizza | 免费低多边形模型 |
| Kenney.nl | 免费游戏资源 |
| Clara.io | 在线 3D 编辑器 |