引言
在使用 three.js 构建大型 3D 场景时,比如虚拟城市、大型工业模型或者复杂的游戏世界,你可能会遇到性能瓶颈。场景中的海量数据会导致加载时间过长,内存占用过高,甚至引发浏览器崩溃。数据分块与懒加载技术正是解决这些问题的有效手段。
数据分块
什么是数据分块?
数据分块就是把一个庞大的 3D 场景按照特定规则,划分成多个较小的、独立的部分,也就是数据块。这样一来,就不用一次性加载整个场景了。
分块策略
1. 基于空间的分块(网格划分)
这种策略是将 3D 空间划分成大小相同的网格单元,每一个单元就是一个数据块。在城市模型中,我们可以按照街区来划分场景。
下面是一个简单的网格分块实现示例:
// 场景分块函数
function createSpatialChunks(sceneSize, chunkSize) {
const chunks = [];
const chunkCount = Math.floor(sceneSize / chunkSize);
for (let x = 0; x < chunkCount; x++) {
for (let y = 0; y < chunkCount; y++) {
for (let z = 0; z < chunkCount; z++) {
// 定义每个块的边界
const chunk = {
position: new THREE.Vector3(x * chunkSize, y * chunkSize, z * chunkSize),
size: chunkSize,
objects: [] // 存储属于该块的3D对象
};
chunks.push(chunk);
}
}
}
return chunks;
}
2. 基于主题的分块
按照功能或者类型对场景进行划分。在一个工厂模型里,可以把机器、管道、建筑等分别划分为不同的数据块。
实现分块加载
当完成场景分块后,我们还需要对数据进行预处理,把每个块保存成单独的文件。以下是一个处理单个块数据导出的示例:
// 导出单个块的数据
function exportChunk(chunk, filename) {
// 创建一个临时场景来存储当前块的对象
const tempScene = new THREE.Scene();
chunk.objects.forEach(object => {
tempScene.add(object.clone());
});
// 使用THREE.GLTFExporter导出为GLTF格式
const exporter = new THREE.GLTFExporter();
exporter.parse(tempScene, (gltf) => {
const data = JSON.stringify(gltf);
const blob = new Blob([data], {type: 'application/json'});
const url = URL.createObjectURL(blob);
// 下载文件
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
}, {binary: false});
}
懒加载技术
什么是懒加载?
懒加载,也就是延迟加载,指的是在用户需要某个数据块的时候才去加载它,而不是在一开始就把所有数据都加载好。在 3D 场景中,通常是当数据块进入视锥体或者靠近相机时,才进行加载。
实现懒加载的关键技术
1. 视锥体剔除检测
视锥体剔除是判断物体是否在相机视场范围内的一种技术。借助它,我们能够确定哪些数据块需要被加载。
// 检查块是否在视锥体内
function isChunkInFrustum(chunk, camera) {
const frustum = new THREE.Frustum();
const cameraViewProjectionMatrix = new THREE.Matrix4();
camera.updateMatrixWorld();
camera.updateProjectionMatrix();
cameraViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
frustum.setFromProjectionMatrix(cameraViewProjectionMatrix);
// 为块创建一个包围盒
const box = new THREE.Box3(
new THREE.Vector3(
chunk.position.x,
chunk.position.y,
chunk.position.z
),
new THREE.Vector3(
chunk.position.x + chunk.size,
chunk.position.y + chunk.size,
chunk.position.z + chunk.size
)
);
return frustum.intersectsBox(box);
}
2. 距离检测
除了视锥体剔除,我们还可以根据数据块与相机的距离来决定是否加载。离相机远的数据块可以延迟加载或者降低细节程度。
// 计算块与相机的距离
function getDistanceFromCamera(chunk, camera) {
const chunkCenter = new THREE.Vector3(
chunk.position.x + chunk.size / 2,
chunk.position.y + chunk.size / 2,
chunk.position.z + chunk.size / 2
);
return chunkCenter.distanceTo(camera.position);
}
懒加载管理器
下面是一个简单的懒加载管理器实现,它能够根据相机位置动态加载和卸载数据块:
class ChunkLoader {
constructor(scene, camera, chunkSize, loadDistance, unloadDistance) {
this.scene = scene;
this.camera = camera;
this.chunkSize = chunkSize;
this.loadDistance = loadDistance;
this.unloadDistance = unloadDistance;
this.loadedChunks = new Map(); // 已加载的块
this.chunks = []; // 所有块的元数据
this.loadingQueue = []; // 加载队列
this.exporter = new THREE.GLTFExporter();
this.loader = new THREE.GLTFLoader();
}
// 初始化块
initChunks(sceneSize) {
this.chunks = createSpatialChunks(sceneSize, this.chunkSize);
}
// 更新加载状态
update() {
// 检查是否有需要加载的块
this.chunks.forEach(chunk => {
const distance = getDistanceFromCamera(chunk, this.camera);
// 如果块在加载距离内且未加载
if (distance < this.loadDistance && !this.loadedChunks.has(chunk.id)) {
this.queueChunkForLoading(chunk);
}
// 如果块在卸载距离外且已加载
else if (distance > this.unloadDistance && this.loadedChunks.has(chunk.id)) {
this.unloadChunk(chunk);
}
});
// 处理加载队列
this.processLoadingQueue();
}
// 将块加入加载队列
queueChunkForLoading(chunk) {
if (!this.loadingQueue.includes(chunk)) {
this.loadingQueue.push(chunk);
}
}
// 处理加载队列
processLoadingQueue() {
// 一次只加载一个块,避免性能峰值
if (this.loadingQueue.length > 0) {
const chunk = this.loadingQueue.shift();
this.loadChunk(chunk);
}
}
// 加载块
loadChunk(chunk) {
const filename = `chunk_${chunk.position.x}_${chunk.position.y}_${chunk.position.z}.gltf`;
this.loader.load(
filename,
(gltf) => {
// 设置块的位置
gltf.scene.position.copy(chunk.position);
// 将模型添加到场景
this.scene.add(gltf.scene);
// 记录已加载的块
this.loadedChunks.set(chunk.id, gltf.scene);
// 可以在这里触发加载完成事件
},
(progress) => {
// 加载进度回调
},
(error) => {
console.error('加载块出错:', error);
}
);
}
// 卸载块
unloadChunk(chunk) {
if (this.loadedChunks.has(chunk.id)) {
const chunkObject = this.loadedChunks.get(chunk.id);
this.scene.remove(chunkObject);
this.loadedChunks.delete(chunk.id);
}
}
}
优化建议
- 预加载策略:可以提前加载相机移动方向前方的数据块,以此减少用户等待时间。
- 多级细节(LOD) :对于远距离的数据块,可以加载低精度版本,从而节省资源。
- 缓存机制:已经加载过的数据块可以进行缓存,这样在用户返回时就无需重新加载。
- 渐进式加载:先加载低细节模型,然后再逐步加载更高细节的内容。
总结
通过数据分块和懒加载技术,我们能够显著提升大型 three.js 应用的性能,减少初始加载时间,降低内存占用。这种技术特别适合虚拟游览、大型数据可视化以及 3D 游戏等场景。
希望这篇文章能帮助你掌握 three.js 中的数据分块与懒加载技术!如果有任何问题,欢迎留言讨论。