模型加载与资源管理
在 3D 开发中,我们通常不会用代码手动创建所有物体,而是从 3D 建模软件导出模型,然后在 Three.js 中加载。Three.js 支持多种 3D 模型格式,并提供了丰富的加载器。本章将介绍如何加载和管理 3D 模型资源。
模型格式概述
Three.js 支持多种 3D 模型格式,每种格式有不同的特点:
| 格式 | 文件扩展名 | 特点 | 适用场景 |
|---|---|---|---|
| GLTF/GLB | .gltf, .glb | Web 标准,支持 PBR、动画、骨骼 | 推荐首选 |
| FBX | .fbx | 工业软件导出,支持动画和骨骼 | 游戏模型 |
| OBJ | .obj, .mtl | 简单通用,不支持动画 | 静态模型 |
| STL | .stl | 仅几何体,无材质 | 3D 打印 |
| PLY | .ply | 点云数据 | 扫描数据 |
| USDZ | .usdz | Apple AR 格式 | AR 应用 |
GLTF/GLB 格式详解
GLTF(GL Transmission Format)是 Khronos 制定的开放标准,专门为 Web 传输优化:
GLTF(.gltf):基于 JSON 的文本格式,可能引用外部资源(纹理、二进制数据) GLB(.glb):二进制格式,所有资源打包在一个文件中
推荐使用 GLB 格式,因为:
- 单文件,便于管理和分发
- 加载更快
- 所有资源(纹理、动画)都包含在内
GLTFLoader
GLTFLoader 是加载 GLTF/GLB 模型的标准方式:
基本使用
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load(
'models/character.glb',
(gltf) => {
// 加载成功
const model = gltf.scene;
scene.add(model);
},
(progress) => {
// 加载进度
console.log(`加载中: ${(progress.loaded / progress.total * 100).toFixed(0)}%`);
},
(error) => {
// 加载错误
console.error('加载失败:', error);
}
);
GLTF 对象结构
加载成功后返回的 GLTF 对象包含多个属性:
loader.load('model.glb', (gltf) => {
const { scene, scenes, cameras, animations, asset } = gltf;
// scene: 默认场景(最常用)
// scenes: 所有场景数组(GLTF 可包含多个场景)
// cameras: 模型中的相机数组
// animations: 动画剪辑数组
// asset: 元数据(版本、生成器等)
console.log('模型信息:', asset);
});
处理模型
加载模型后,通常需要进行一些处理:
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
// 调整比例
model.scale.set(0.5, 0.5, 0.5);
// 调整位置
model.position.set(0, 0, 0);
// 遍历所有子对象
model.traverse((child) => {
if (child.isMesh) {
// 设置阴影
child.castShadow = true;
child.receiveShadow = true;
// 修改材质
if (child.material) {
child.material.metalness = 0.5;
child.material.roughness = 0.5;
}
}
});
scene.add(model);
});
模型中心对齐
有时模型的中心点不在原点,需要重新调整:
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
// 计算包围盒
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 = 2 / maxDim; // 让最大边长为 2
model.scale.setScalar(scale);
scene.add(model);
});
加载动画
如果模型包含动画,需要使用 AnimationMixer 播放:
let mixer;
const clock = new THREE.Clock();
loader.load('character.glb', (gltf) => {
const model = gltf.scene;
scene.add(model);
// 创建动画混合器
mixer = new THREE.AnimationMixer(model);
// 播放所有动画
gltf.animations.forEach((clip) => {
const action = mixer.clipAction(clip);
action.play();
});
});
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
if (mixer) {
mixer.update(delta);
}
renderer.render(scene, camera);
}
控制特定动画
let mixer;
let actions = {};
loader.load('character.glb', (gltf) => {
const model = gltf.scene;
scene.add(model);
mixer = new THREE.AnimationMixer(model);
// 存储所有动画动作
gltf.animations.forEach((clip) => {
actions[clip.name] = mixer.clipAction(clip);
});
// 播放默认动画
if (actions['Idle']) {
actions['Idle'].play();
}
});
// 切换动画
function playAnimation(name) {
// 停止当前动画
Object.values(actions).forEach(action => action.stop());
// 播放新动画
if (actions[name]) {
actions[name].reset().play();
}
}
// 混合动画
function crossFade(fromName, toName, duration = 0.5) {
const from = actions[fromName];
const to = actions[toName];
from.fadeOut(duration);
to.reset().fadeIn(duration).play();
}
DRACO 压缩模型
DRACO 是 Google 开发的 3D 模型压缩库,可以大幅减小模型体积:
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('jsm/libs/draco/'); // 解码器路径
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load('compressed.glb', (gltf) => {
scene.add(gltf.scene);
});
DRACO 压缩可以减少 50-90% 的文件体积,但会增加解压时间。适合大型模型或需要快速加载的场景。
其他加载器
OBJLoader + MTLLoader
OBJ 是传统的模型格式,简单但不支持动画:
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
// 先加载材质
const mtlLoader = new MTLLoader();
mtlLoader.load('model.mtl', (materials) => {
materials.preload();
// 再加载模型
const objLoader = new OBJLoader();
objLoader.setMaterials(materials);
objLoader.load('model.obj', (object) => {
scene.add(object);
});
});
FBXLoader
FBX 格式常用于游戏开发,支持完整的动画和骨骼系统:
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
const loader = new FBXLoader();
loader.load('model.fbx', (object) => {
// 处理动画
if (object.animations.length > 0) {
const mixer = new THREE.AnimationMixer(object);
mixer.clipAction(object.animations[0]).play();
mixers.push(mixer);
}
scene.add(object);
});
STLLoader
STL 格式仅包含几何体,常用于 3D 打印:
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
const loader = new STLLoader();
loader.load('model.stl', (geometry) => {
const material = new THREE.MeshStandardMaterial({
color: 0x00ff00,
metalness: 0.3,
roughness: 0.6
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
});
PLYLoader
PLY 格式常用于点云数据:
import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
const loader = new PLYLoader();
loader.load('pointcloud.ply', (geometry) => {
// 作为点云渲染
const material = new THREE.PointsMaterial({
size: 0.01,
vertexColors: geometry.hasAttribute('color')
});
const points = new THREE.Points(geometry, material);
scene.add(points);
// 或作为网格渲染
// const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
// scene.add(mesh);
});
SVGLoader
加载 SVG 作为 3D 形状:
import { SVGLoader } from 'three/addons/loaders/SVGLoader.js';
const loader = new SVGLoader();
loader.load('shape.svg', (data) => {
const paths = data.paths;
paths.forEach((path) => {
// 创建形状
const shapes = path.toShapes(true);
shapes.forEach((shape) => {
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: 0.5,
bevelEnabled: false
});
const material = new THREE.MeshStandardMaterial({
color: path.color
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
});
});
});
资源管理
LoadingManager
使用 LoadingManager 统一管理所有资源加载:
const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => {
console.log(`开始加载 ${url}`);
};
manager.onProgress = (url, loaded, total) => {
const progress = (loaded / total * 100).toFixed(0);
console.log(`总进度: ${progress}%`);
// 更新进度条 UI
progressBar.style.width = `${progress}%`;
};
manager.onLoad = () => {
console.log('所有资源加载完成');
// 隐藏加载界面
loadingScreen.style.display = 'none';
};
manager.onError = (url) => {
console.error(`加载失败: ${url}`);
};
// 使用同一个 manager 创建各种加载器
const textureLoader = new THREE.TextureLoader(manager);
const gltfLoader = new GLTFLoader(manager);
const audioLoader = new THREE.AudioLoader(manager);
加载状态显示
创建一个加载界面:
const manager = new THREE.LoadingManager();
const loadingManager = {
itemsTotal: 0,
itemsLoaded: 0
};
manager.onStart = (url, loaded, total) => {
loadingManager.itemsTotal = total;
document.getElementById('loading').style.display = 'flex';
};
manager.onProgress = (url, loaded, total) => {
loadingManager.itemsLoaded = loaded;
const percent = (loaded / total * 100).toFixed(0);
document.getElementById('progress').textContent = `${percent}%`;
document.getElementById('progress-bar').style.width = `${percent}%`;
};
manager.onLoad = () => {
// 延迟隐藏,让用户看到 100%
setTimeout(() => {
document.getElementById('loading').style.opacity = '0';
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
}, 500);
}, 500);
};
预加载资源
对于需要即时响应的应用,可以预加载资源:
const resources = {
models: {},
textures: {},
audio: {}
};
async function preloadResources() {
const gltfLoader = new GLTFLoader();
const textureLoader = new THREE.TextureLoader();
// 并行加载所有资源
const [model1, texture1] = await Promise.all([
gltfLoader.loadAsync('models/character.glb'),
textureLoader.loadAsync('textures/diffuse.jpg')
]);
resources.models.character = model1;
resources.textures.diffuse = texture1;
console.log('预加载完成');
}
// 使用资源
function createCharacter() {
const model = resources.models.character.scene.clone();
scene.add(model);
}
// 启动
preloadResources().then(() => {
animate();
createCharacter();
});
模型导出
Three.js 也支持将场景导出为各种格式:
GLTFExporter
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
const exporter = new GLTFExporter();
// 导出为 GLTF
exporter.parse(
scene,
(gltf) => {
const output = JSON.stringify(gltf, null, 2);
const blob = new Blob([output], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// 触发下载
const link = document.createElement('a');
link.href = url;
link.download = 'scene.gltf';
link.click();
URL.revokeObjectURL(url);
},
(error) => {
console.error('导出失败:', error);
},
{ binary: false } // true 导出为 GLB
);
// 导出为 GLB(二进制)
exporter.parse(
scene,
(glb) => {
const blob = new Blob([glb], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'scene.glb';
link.click();
URL.revokeObjectURL(url);
},
(error) => console.error(error),
{ binary: true }
);
OBJExporter
import { OBJExporter } from 'three/addons/exporters/OBJExporter.js';
const exporter = new OBJExporter();
const result = exporter.parse(scene);
const blob = new Blob([result], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'model.obj';
link.click();
URL.revokeObjectURL(url);
内存管理
加载大量模型时,需要注意内存管理:
释放资源
// 释放单个对象
function disposeObject(object) {
object.traverse((child) => {
if (child.geometry) {
child.geometry.dispose();
}
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(material => disposeMaterial(material));
} else {
disposeMaterial(child.material);
}
}
});
}
function disposeMaterial(material) {
// 释放所有纹理
Object.keys(material).forEach((key) => {
if (material[key] && typeof material[key].dispose === 'function') {
material[key].dispose();
}
});
material.dispose();
}
// 使用示例
scene.remove(model);
disposeObject(model);
模型缓存
对于频繁使用的模型,可以缓存克隆:
const modelCache = new Map();
async function loadModel(url) {
if (modelCache.has(url)) {
// 返回缓存的克隆
return modelCache.get(url).clone();
}
const loader = new GLTFLoader();
const gltf = await loader.loadAsync(url);
// 缓存原始模型
modelCache.set(url, gltf.scene);
// 返回克隆
return gltf.scene.clone();
}
// 清理缓存
function clearCache() {
modelCache.forEach((model) => {
disposeObject(model);
});
modelCache.clear();
}
完整示例
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();
scene.background = new THREE.Color(0x1a1a2e);
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.set(5, 5, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.set(2048, 2048);
scene.add(directionalLight);
// 加载管理
const manager = new THREE.LoadingManager();
const loadingOverlay = document.getElementById('loading');
manager.onProgress = (url, loaded, total) => {
const percent = (loaded / total * 100).toFixed(0);
console.log(`加载进度: ${percent}%`);
};
manager.onLoad = () => {
console.log('所有资源加载完成');
};
// 配置 DRACO
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
// 创建加载器
const gltfLoader = new GLTFLoader(manager);
gltfLoader.setDRACOLoader(dracoLoader);
// 存储动画
const mixers = [];
const clock = new THREE.Clock();
// 加载模型(示例 URL,实际使用时替换)
async function loadModel() {
try {
const gltf = await gltfLoader.loadAsync('https://threejs.org/examples/models/gltf/Horse.glb');
const model = gltf.scene;
// 调整模型
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
// 计算缩放比例
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 2 / maxDim;
model.scale.setScalar(scale);
// 居中
model.position.sub(center.multiplyScalar(scale));
model.position.y = 0;
// 设置阴影
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(model);
// 处理动画
if (gltf.animations.length > 0) {
const mixer = new THREE.AnimationMixer(model);
mixer.clipAction(gltf.animations[0]).play();
mixers.push(mixer);
}
} catch (error) {
console.error('模型加载失败:', error);
// 加载失败时显示默认立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff88 });
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true;
scene.add(cube);
}
}
// 地面
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x333344,
roughness: 0.8
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// 加载模型
loadModel();
// 动画循环
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
// 更新动画混合器
mixers.forEach((mixer) => mixer.update(delta));
controls.update();
renderer.render(scene, camera);
}
animate();
// 响应窗口变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
小结
模型加载是 3D 应用开发的核心环节:
- GLTF/GLB 是推荐的模型格式,支持完整的 PBR 材质、动画和骨骼
- GLTFLoader 是加载 GLTF 模型的标准方式,配合 DRACOLoader 可以加载压缩模型
- 动画处理 需要使用 AnimationMixer,支持多动画切换和混合
- LoadingManager 用于统一管理资源加载,显示加载进度
- 内存管理 很重要,不再使用的模型要及时释放
选择合适的模型格式和加载策略,可以显著提升应用的加载速度和运行性能。