Unity3D地形异常排查:TerrainData.SetHeights边缘悬垂问题
嘿,我仔细看了你的代码和问题描述,这个边缘悬垂的问题主要是因为地形尺寸、高度图顶点数和你的海拔数组之间的对应关系没理清楚,加上代码里几个逻辑小问题导致的。下面给你拆解问题和修正方案:
核心问题分析
1. 地形尺寸计算逻辑有漏洞
你在MapScale里用firstRun只取第一行的长度作为terrainWidth,如果数组里某行长度和第一行不一样(哪怕你预期是200x157),就会算错宽度。而且嵌套循环计算maxHeight也有点冗余。
2. 高度图分辨率设置错误
Unity的heightmapResolution是用来设置方形高度图的分辨率的,如果你需要200x157的非方形地形,直接设置这个属性会强制高度图变成方形(比如取宽高中的最大值+1),导致其中一个方向的顶点数远超需要,自然会出现边缘悬垂。
3. 高度图数组与地形尺寸不匹配
Unity地形的物理尺寸和高度图顶点数的关系是:物理尺寸 = (顶点数 - 1) × 顶点间隔(默认间隔是1单位)。你的海拔数组是200x157,但你创建的高度图数组是float[terrainHeight, terrainWidth],同时把地形size设为(terrainWidth, terrainDepth, terrainHeight),这就导致顶点数和物理尺寸不匹配,Unity会自动填充缺失的顶点高度(默认0),形成边缘的悬垂区域。
修正后的代码
1. 修正MapScale方法
把尺寸计算逻辑简化,确保准确获取数组的行列数,用LINQ快速计算最大高度:
private void MapScale(int[][] mapData) { // 先做参数校验,避免空数组报错 if (mapData == null || mapData.Length == 0 || mapData[0].Length == 0) { Debug.LogError("传入的地图数据无效!"); return; } terrainHeight = mapData.Length; // 对应地形z方向的顶点数 terrainWidth = mapData[0].Length; // 对应地形x方向的顶点数 terrainDepth = 10; // 用LINQ快速获取所有元素中的最大值 maxHeight = mapData.SelectMany(row => row).Max(); Debug.Log($"最大高度: {maxHeight}, 地形顶点尺寸: {terrainWidth}x{terrainHeight}"); }
2. 修正GenerateTerrain方法
放弃使用heightmapResolution,直接设置高度图的宽高,同时让地形物理尺寸和顶点数对应起来:
private TerrainData GenerateTerrain(TerrainData terrainData, int[][] mapData) { // 直接设置高度图的顶点数,和你的数组尺寸完全匹配 terrainData.heightmapWidth = terrainWidth; terrainData.heightmapHeight = terrainHeight; // 地形物理尺寸 = (顶点数 - 1) × 1单位(每个顶点间隔1米) // 如果你的数组是"块高度"(200x157块),那顶点数应该是201x158,这里根据你的需求调整 terrainData.size = new Vector3(terrainWidth - 1, terrainDepth, terrainHeight - 1); Debug.Log($"地形物理尺寸: {terrainData.size.x}x{terrainData.size.z}"); Debug.Log($"高度图维度: {terrainData.heightmapWidth}x{terrainData.heightmapHeight}"); // 传入完全匹配的高度数组 terrainData.SetHeights(0, 0, GenerateHeights(mapData)); return terrainData; }
3. 优化GenerateHeights方法
增加越界保护,避免数组行长度不一致时报错:
private float[,] GenerateHeights(int[][] mapData) { Debug.Log($"生成{terrainHeight}x{terrainWidth}的高度数据"); float[,] heights = new float[terrainHeight, terrainWidth]; for (int z = 0; z < terrainHeight; z++) { for (int x = 0; x < terrainWidth; x++) { // 防止某行长度不足导致索引越界 if (x >= mapData[z].Length) { heights[z, x] = 0; continue; } float value = mapData[z][x]; // 避免maxHeight为0时除以0的错误 heights[z, x] = maxHeight > 0 ? (value / maxHeight) : 0; } } return heights; }
4. 可选:创建全新的TerrainData
如果你的预制体自带默认高度图设置,可能会干扰你的修改,建议直接创建新的TerrainData:
public void CreateTerrain(int[][] mapData) { MapScale(mapData); // 创建全新的TerrainData,避免预制体默认设置的干扰 TerrainData terrainData = new TerrainData(); terrainData = GenerateTerrain(terrainData, mapData); // 生成Terrain游戏对象 Terrain.CreateTerrainGameObject(terrainData).transform.position = Vector3.zero; }
额外说明
如果你的需求是地形物理尺寸为200x157(而不是顶点数),那你的海拔数组应该是201x158的顶点高度数据(因为200单位宽需要201个顶点)。你需要调整MapScale里的尺寸计算,或者修改数组的生成逻辑,确保顶点数和物理尺寸对应。
内容的提问来源于stack exchange,提问作者Patrick Neumüller

