THREE.js中基于平面的区块化无限世界生成问题排查
解决Minecraft式无限区块生成的重复与性能问题
区块重复问题修复
你的代码中所有区块看起来完全一致,核心原因是修改顶点后未通知Three.js更新几何体数据:Three.js会缓存几何体的顶点信息,修改顶点数组后必须手动标记更新,否则渲染器会继续使用旧数据。
临时修复(不改动整体架构)
在修改完区块顶点后,添加以下关键代码:
// Setup Terrain for(let y2 = 0; y2 < 101;y2++) { for(let x2 = 0; x2 < 101; x2++) { g.geometry.vertices[(y2 * 101) + x2].z = currentChunk[x2 + ',' + y2] } } // 通知Three.js顶点数据已更新 g.geometry.verticesNeedUpdate = true; // 若使用光照材质,必须重新计算法线才能正确显示光影 g.geometry.computeVertexNormals();
同时检查坐标计算逻辑:确保startX和startY为每个区块生成了唯一的噪声偏移,避免不同区块使用相同的噪声输入导致地形重复。
性能优化方案(解决重复创建几何体的开销)
每次循环创建独立Mesh会产生大量Draw Call和内存开销,推荐两种优化思路:
1. 合并几何体(大幅减少Draw Call)
使用Three.js的BufferGeometryUtils.mergeBufferGeometries将所有区块的几何体合并为一个,仅需一个Mesh即可渲染所有区块,性能提升显著。
实现步骤:
- 初始化时提前创建基础BufferGeometry(Three.js推荐用BufferGeometry替代旧版Geometry):
// 仅创建一次,不要在updateChunk中重复生成 const basePlane = new THREE.PlaneGeometry(1000, 1000, 100, 100); basePlane.rotateX(-Math.PI / 2); // 提前旋转平面,避免每个区块重复执行旋转操作 const baseBufferGeo = basePlane.toBufferGeometry();
- 修改
updateChunk函数,合并所有区块几何体:
function updateChunk() { // 清理旧的合并网格 if (ground.children.length > 0) { ground.children[0].geometry.dispose(); ground.remove(ground.children[0]); } let size = (game.chunk.render * 2) + 1; const playerChunkX = Math.floor(plane.position.x / 1000); const playerChunkZ = Math.floor(plane.position.z / 1000); const startChunkX = playerChunkX - Math.floor(size / 2); const startChunkZ = playerChunkZ - Math.floor(size / 2); const chunkGeometries = []; for(let z = 0; z < size; z++) { for(let x = 0; x < size; x++) { const chunkX = startChunkX + x; const chunkZ = startChunkZ + z; const startX = chunkX * 100; const startY = chunkZ * 100; const currentChunk = createChunk(startX, startY); // 克隆基础BufferGeometry const chunkGeo = baseBufferGeo.clone(); const positions = chunkGeo.attributes.position.array; // 更新顶点高度 for(let y2 = 0; y2 < 101; y2++) { for(let x2 = 0; x2 < 101; x2++) { // BufferGeometry的位置数组格式为[x,y,z,x,y,z,...],z分量索引为 (y2*101 + x2)*3 + 2 const index = (y2 * 101 + x2) * 3 + 2; positions[index] = currentChunk[y2 * 101 + x2]; } } chunkGeo.attributes.position.needsUpdate = true; chunkGeo.computeVertexNormals(); // 平移几何体到对应区块位置 chunkGeo.translate(x * 1000, 0, z * 1000); chunkGeometries.push(chunkGeo); } } // 合并所有区块几何体 const mergedGeo = THREE.BufferGeometryUtils.mergeBufferGeometries(chunkGeometries); const mergedMesh = new THREE.Mesh(mergedGeo, material); ground.add(mergedMesh); }
- 优化
createChunk函数,用数组索引代替字符串键,提升运行效率:
function createChunk(startX, startY) { const chunk = new Array(101 * 101); for(let y = 0; y < 101; y++) { for(let x = 0; x < 101; x++) { const index = y * 101 + x; chunk[index] = Math.abs(noise.perlin2((x + startX) / 100, (y + startY) / 100)) * 200; } } return chunk; }
2. 区块复用(避免频繁销毁重建)
维护一个已加载区块的Map,仅移除不在视野内的区块,添加新进入视野的区块,减少不必要的几何体创建和销毁操作:
// 全局维护已加载的区块,键为区块坐标字符串,值为对应Mesh const loadedChunks = new Map(); function updateChunk() { let size = (game.chunk.render * 2) + 1; const playerChunkX = Math.floor(plane.position.x / 1000); const playerChunkZ = Math.floor(plane.position.z / 1000); const startChunkX = playerChunkX - Math.floor(size / 2); const startChunkZ = playerChunkZ - Math.floor(size / 2); // 记录当前需要保留的区块键 const neededChunks = new Set(); // 创建/更新需要的区块 for(let z = 0; z < size; z++) { for(let x = 0; x < size; x++) { const chunkX = startChunkX + x; const chunkZ = startChunkZ + z; const chunkKey = `${chunkX},${chunkZ}`; neededChunks.add(chunkKey); if (!loadedChunks.has(chunkKey)) { // 创建新区块Mesh const startX = chunkX * 100; const startY = chunkZ * 100; const currentChunk = createChunk(startX, startY); const g = new THREE.Mesh(baseBufferGeo.clone(), material); const positions = g.geometry.attributes.position.array; for(let y2 = 0; y2 < 101; y2++) { for(let x2 = 0; x2 < 101; x2++) { const index = (y2 * 101 + x2) * 3 + 2; positions[index] = currentChunk[y2 * 101 + x2]; } } g.geometry.attributes.position.needsUpdate = true; g.geometry.computeVertexNormals(); g.position.set(chunkX * 1000, 0, chunkZ * 1000); ground.add(g); loadedChunks.set(chunkKey, g); } } } // 移除不需要的区块 for (const [key, mesh] of loadedChunks.entries()) { if (!neededChunks.has(key)) { mesh.geometry.dispose(); ground.remove(mesh); loadedChunks.delete(key); } } }
内容的提问来源于stack exchange,提问作者Owen Odroski
相关产品推荐
相关产品推荐

