跳到主要内容

模型加载与资源管理

在 3D 开发中,我们通常不会用代码手动创建所有物体,而是从 3D 建模软件导出模型,然后在 Three.js 中加载。Three.js 支持多种 3D 模型格式,并提供了丰富的加载器。本章将介绍如何加载和管理 3D 模型资源。

模型格式概述

Three.js 支持多种 3D 模型格式,每种格式有不同的特点:

格式文件扩展名特点适用场景
GLTF/GLB.gltf, .glbWeb 标准,支持 PBR、动画、骨骼推荐首选
FBX.fbx工业软件导出,支持动画和骨骼游戏模型
OBJ.obj, .mtl简单通用,不支持动画静态模型
STL.stl仅几何体,无材质3D 打印
PLY.ply点云数据扫描数据
USDZ.usdzApple 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 用于统一管理资源加载,显示加载进度
  • 内存管理 很重要,不再使用的模型要及时释放

选择合适的模型格式和加载策略,可以显著提升应用的加载速度和运行性能。