You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 07:54:59