THREE.js中隐藏方块不可见面以优化渲染性能的方案咨询
THREE.js 大量方块场景性能优化方案(基于遮挡面剔除)
我正在开发一个包含大量方块的THREE.js项目,过多方块渲染会导致严重卡顿。目前已实现类似Minecraft的遮挡面剔除逻辑——检测相邻方块,不渲染被遮挡的面,但现有代码效率不足,希望得到更高效的优化方案。
现有检测代码如下:
let blRight = false let blLeft = false let blBack = false let blFront = false let blTop = false let blBottom = false for(let i = 0; i < blocks.coord.x.length; i++) { if(blocks.coord.x[i] == x + 10) { if(!blocks.coord.y[i] == y) { blRight = true } } } for(let i = 0; i < blocks.coord.x.length; i++) { if(blocks.coord.x[i] == x - 10) { if(!blocks.coord.y[i] == y) { blLeft = true } } } for(let i = 0; i < blocks.coord.y.length; i++) { if(blocks.coord.y[i] == y + 10) { blTop = false } } for(let i = 0; i < blocks.coord.y.length; i++) { if(blocks.coord.y[i] == y - 10) { blBottom = true } } for(let i = 0; i < blocks.coord.z.length; i++) { if(blocks.coord.z[i] == z + 10) { if(!blocks.coord.y[i] == y) { blFront = true } } } for(let i = 0; i < blocks.coord.z.length; i++) { if(blocks.coord.z[i] == x - 10) { if(!blocks.coord.y[i] == y) { blBack = true } } } if(blRight == false) { scene.add(right) } if(blLeft == false) { scene.add(left) } if(blTop == false) { scene.add(blockTop) } if(blBottom == false) { scene.add(bottom) } if(blFront == false) { scene.add(front) } if(blBack == false) { scene.add(back) } blocks.coord.x.push(x) blocks.coord.y.push(y) blocks.coord.z.push(z) blocks.coord.id.push(3)
注:blTop代表顶面,逻辑类似Minecraft的遮挡面剔除。
核心优化方案
1. 重构坐标存储,用哈希表实现O(1)查找
当前用三个独立数组存储x/y/z坐标,每次检测相邻方块都要遍历整个数组,时间复杂度O(n),方块越多越慢。改成**哈希表(Map)**存储,用x,y,z拼接成唯一键,直接查询是否存在相邻方块:
// 替换原blocks.coord结构 const blockMap = new Map(); // 检测相邻方块时(以右侧为例) const rightKey = `${x + 10},${y},${z}`; const blRight = blockMap.has(rightKey); // 添加新方块时 blockMap.set(`${x},${y},${z}`, { id: 3 });
这样每个方向的检测从O(n)降到O(1),性能提升显著。
2. 合并遍历逻辑,修正代码错误
把6次独立循环合并成1次遍历,一次完成所有方向的检测,同时修正原代码中忽略x/z坐标、变量误用等逻辑错误:
let blRight = false, blLeft = false, blBack = false, blFront = false, blTop = false, blBottom = false; const count = blocks.coord.x.length; for(let i = 0; i < count; i++) { const bx = blocks.coord.x[i]; const by = blocks.coord.y[i]; const bz = blocks.coord.z[i]; if(bx === x + 10 && by === y && bz === z) blRight = true; if(bx === x - 10 && by === y && bz === z) blLeft = true; if(by === y + 10 && bx === x && bz === z) blTop = true; if(by === y - 10 && bx === x && bz === z) blBottom = true; if(bz === z + 10 && bx === x && by === y) blFront = true; if(bz === z - 10 && bx === x && by === y) blBack = true; }
3. 合并几何体,减少Draw Call
THREE.js中每个添加到场景的物体都是一次Draw Call,大量独立平面会导致Draw Call暴增。优化方式:
- 改用
BoxGeometry创建方块,通过修改顶点索引剔除不需要的面; - 使用
BufferGeometryUtils.mergeBufferGeometries将所有可见面合并成一个大的BufferGeometry,再用一个Mesh渲染,把成百上千次Draw Call降到1次。
示例代码:
import { BufferGeometryUtils } from 'three/addons/utils/BufferGeometryUtils.js'; const visibleGeometries = []; // 收集所有可见的面的几何体 if(!blRight) visibleGeometries.push(right.geometry); if(!blLeft) visibleGeometries.push(left.geometry); // ...收集其他可见面 // 合并成单个几何体 const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(visibleGeometries); const mergedMesh = new Mesh(mergedGeometry, material); scene.add(mergedMesh);
4. 空间分区,缩小检测范围
当方块数量极大时,将场景划分为固定大小的区块(比如10x10x10个方块为一个区块),每个区块存储该区域内的方块。检测相邻方块时,只查询当前方块所在区块及上下左右前后的6个相邻区块,无需遍历所有方块:
const chunkSize = 100; // 每个区块的尺寸(单位) const chunkMap = new Map(); // 获取方块所在区块的键 function getChunkKey(x, y, z) { return `${Math.floor(x/chunkSize)},${Math.floor(y/chunkSize)},${Math.floor(z/chunkSize)}`; } // 添加方块时,存入对应区块 const chunkKey = getChunkKey(x, y, z); if(!chunkMap.has(chunkKey)) chunkMap.set(chunkKey, []); chunkMap.get(chunkKey).push({x, y, z, id:3}); // 检测相邻方块时,只查目标区块 const targetChunkKeys = [ getChunkKey(x+10, y, z), getChunkKey(x-10, y, z), // ...其他方向的区块键 ];
5. 视锥体剔除,跳过不可见方块
只渲染相机视锥体内的方块,用THREE.js的Frustum类检测方块是否在视野内,不在的话直接跳过遮挡检测和渲染:
const frustum = new THREE.Frustum(); const cameraViewProjectionMatrix = new THREE.Matrix4(); // 每帧更新视锥体 camera.updateMatrixWorld(); cameraViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); frustum.setFromProjectionMatrix(cameraViewProjectionMatrix); // 检测方块是否在视锥体内(用方块的包围盒) const box = new THREE.Box3().setFromCenterAndSize(new THREE.Vector3(x,y,z), new THREE.Vector3(10,10,10)); if(frustum.intersectsBox(box)) { // 进行遮挡检测和渲染 }
内容的提问来源于stack exchange,提问作者Owen Odroski
相关产品推荐
相关产品推荐

