基于Perlin Noise的Unity地形分块Mesh缺失及合并需求
地形分块Mesh对齐缺失修复与合并方案
一、先搞定分块Mesh的对齐问题
你的2D噪声图对齐没问题,但Mesh出现缺失/错位,问题根源在于每个Chunk的Mesh坐标没按它在大地图里的位置做偏移。原MeshGenerator.GenerateTerrainMesh是按当前Chunk尺寸计算局部坐标的,所有Chunk的Mesh原点都在自己中心,自然会在场景里重叠或错位。
改法如下:
- 给
MeshGenerator.GenerateTerrainMesh加两个参数:float worldOffsetX和float worldOffsetZ,用来传递当前Chunk在世界空间的偏移量。 - 调整顶点坐标计算,把局部坐标加上世界偏移:
修改后的MeshGenerator代码:
public static class MeshGenerator { // 新增世界偏移参数 public static MeshData GenerateTerrainMesh(float[,] heightMap, float heightMultiplier, AnimationCurve _heightCurve, int levelOfDetail, float worldOffsetX, float worldOffsetZ) { AnimationCurve heightCurve = new AnimationCurve(_heightCurve.keys); int width = heightMap.GetLength(0); int height = heightMap.GetLength(1); float topLeftX = (width - 1) / -2f; float topLeftZ = (height - 1) / 2f; int meshSimplificationIncrement = (levelOfDetail == 0) ? 1 : levelOfDetail * 2; int verticesPerLine = (width - 1) / meshSimplificationIncrement + 1; MeshData meshData = new MeshData(verticesPerLine, verticesPerLine); int vertexIndex = 0; for (int y = 0; y < height; y += meshSimplificationIncrement) { for (int x = 0; x < width; x += meshSimplificationIncrement) { // 局部坐标 + 世界偏移 = 最终世界坐标 float worldX = topLeftX + x + worldOffsetX; float worldZ = topLeftZ - y + worldOffsetZ; meshData.vertices[vertexIndex] = new Vector3(worldX, heightCurve.Evaluate(heightMap[x, y]) * heightMultiplier, worldZ); meshData.uvs[vertexIndex] = new Vector2(x / (float)width, y / (float)height); if (x < width - 1 && y < height - 1) { meshData.AddTriangle(vertexIndex, vertexIndex + verticesPerLine + 1, vertexIndex + verticesPerLine); meshData.AddTriangle(vertexIndex + verticesPerLine + 1, vertexIndex, vertexIndex + 1); } vertexIndex++; } } return meshData; } }
- 在
GenerateMap里给每个Chunk计算对应偏移,调用生成方法时传入:
// 循环生成Chunk时,根据索引计算偏移 for (var chunkIndex = 0; chunkIndex < noiseMapChunks.Count; chunkIndex++) { float offsetX = 0; float offsetZ = 0; // 按Chunk在大地图的位置计算偏移(大地图为2*2个Chunk) switch(chunkIndex) { case 0: // 左上 offsetX = -mapChunkSize; offsetZ = mapChunkSize; break; case 1: // 右上 offsetX = 0; offsetZ = mapChunkSize; break; case 2: // 左下 offsetX = -mapChunkSize; offsetZ = 0; break; case 3: // 右下 offsetX = 0; offsetZ = 0; break; } // 微调偏移让Chunk边缘完美对齐 float worldOffsetX = offsetX + (mapChunkSize - 1)/2f; float worldOffsetZ = offsetZ - (mapChunkSize - 1)/2f; Color[] colourMap = new Color[mapChunkSize * mapChunkSize]; // ... 原颜色图生成代码保持不变 ... // 调用修改后的生成方法,传入偏移量 var meshData = MeshGenerator.GenerateTerrainMesh(noiseMapChunks[chunkIndex], meshHeightMultiplier, meshHeightCurve, levelOfDetail, worldOffsetX, worldOffsetZ); display.DrawMesh(meshData.CreateMesh(), TextureGenerator.TextureFromColourMap(colourMap, mapChunkSize, mapChunkSize), chunkIndex); }
改完之后,四个Chunk的Mesh就能在场景里正确对齐,不会出现缺块问题。
二、合并多个Chunk Mesh的方案
如果确实需要合并Mesh,得注意Unity单个Mesh的顶点数限制——旧版Unity限制为65535个顶点,新版支持32位索引可扩展至数百万,但如果合并后超出限制,Mesh会创建失败。以下是合并方法:
方法:将多个MeshData合并为一个大MeshData
- 给
MeshGenerator添加合并静态方法:
public static MeshData MergeMeshDatas(List<MeshData> meshDatas, List<Vector3> chunkOffsets) { int totalVertices = 0; int totalTriangles = 0; // 先统计总顶点数和三角形数 foreach(var data in meshDatas) { totalVertices += data.vertices.Length; totalTriangles += data.triangles.Length; } // 创建合并后的MeshData MeshData mergedData = new MeshData(totalVertices, totalVertices); mergedData.vertices = new Vector3[totalVertices]; mergedData.uvs = new Vector2[totalVertices]; mergedData.triangles = new int[totalTriangles]; int vertexOffset = 0; int triangleOffset = 0; for(int i=0; i<meshDatas.Count; i++) { var data = meshDatas[i]; var offset = chunkOffsets[i]; // 复制顶点并添加Chunk偏移 for(int v=0; v<data.vertices.Length; v++) { mergedData.vertices[vertexOffset + v] = data.vertices[v] + offset; mergedData.uvs[vertexOffset + v] = data.uvs[v]; } // 复制三角形并调整索引偏移(因顶点总数变化) for(int t=0; t<data.triangles.Length; t++) { mergedData.triangles[triangleOffset + t] = data.triangles[t] + vertexOffset; } vertexOffset += data.vertices.Length; triangleOffset += data.triangles.Length; } return mergedData; }
- 在
GenerateMap里先收集所有Chunk的MeshData和偏移量,再执行合并:
public void GenerateMap() { // ... 原噪声图拆分代码保持不变 ... display.ClearMeshes(); List<MeshData> allMeshDatas = new List<MeshData>(); List<Vector3> allChunkOffsets = new List<Vector3>(); for (var chunkIndex = 0; chunkIndex < noiseMapChunks.Count; chunkIndex++) { // ... 原颜色图生成代码保持不变 ... float offsetX = 0; float offsetZ = 0; switch(chunkIndex) { case 0: offsetX = -mapChunkSize; offsetZ = mapChunkSize; break; case 1: offsetX = 0; offsetZ = mapChunkSize; break; case 2: offsetX = -mapChunkSize; offsetZ = 0; break; case 3: offsetX = 0; offsetZ = 0; break; } float worldOffsetX = offsetX + (mapChunkSize -1)/2f; float worldOffsetZ = offsetZ - (mapChunkSize -1)/2f; // 先不加偏移,合并时统一处理 var meshData = MeshGenerator.GenerateTerrainMesh(noiseMapChunks[chunkIndex], meshHeightMultiplier, meshHeightCurve, levelOfDetail, 0, 0); allMeshDatas.Add(meshData); allChunkOffsets.Add(new Vector3(worldOffsetX, 0, worldOffsetZ)); } // 合并MeshData MeshData mergedMeshData = MeshGenerator.MergeMeshDatas(allMeshDatas, allChunkOffsets); Mesh mergedMesh = mergedMeshData.CreateMesh(); // 纹理合并可选:可将四个Chunk的纹理拼接为一张大纹理,或用UV偏移复用单张纹理 display.DrawMesh(mergedMesh, null, 0); // 按你的Display方法调整 }
注意事项:
- 如果合并后顶点数超出Unity限制,Mesh会创建失败,这种情况下仍需保持分块。
- 纹理合并需额外处理,比如使用
Texture2D.SetPixels拼接四张纹理,或直接复用原Chunk纹理(UV坐标已正确对应)。
三、额外排查点
- 检查
display.DrawMesh方法是否正确设置了每个Chunk的Transform位置,若直接创建MeshFilter未设置位置,也会导致对齐错误。 - 原代码中
upperRight的try-catch块可删除,排查是否存在数组越界问题,这也可能是Mesh缺块的原因之一。
内容的提问来源于stack exchange,提问作者Michael Commons
相关产品推荐
相关产品推荐

