Unity Terrain移除建筑下方草地绘制地面层实现方案
原代码失效原因
你写的逻辑出黑色斑块是几个硬伤导致的:
- 新建的alphamap数组没有读取地形原有数据,C#里新建的float数组默认值全是0,相当于所有地形层权重都是0,自然渲染出纯黑色
- 你初始化的
element数组只给[0,0]这一个像素点赋了值,其余所有像素权重全是0,调用SetAlphamaps时会把覆盖区域除了单个点之外的地方全清成黑块 - 坐标计算完全不对:只取了建筑坐标对应的单个像素点,没有覆盖建筑整个占地面积,也没算周边预留的清理范围
- 没有做单位转换:直接把建筑的世界尺寸(单位米)当成alphamap像素数用,实际不同地形尺寸下单个alphamap像素对应的世界大小完全不一样
- 没做边界校验,坐标超出地形范围时会直接抛错或者写入异常
- 没有保证单像素所有层权重和为1,渲染必然出异常
正确实现代码
以下代码默认约定:alphamap第0层为草地,第1层为需要铺设的硬地面层,周边额外清理半径默认1米,可按需传参调整
/// <summary> /// 清除建筑本体及周边范围的草地,铺设指定地面层 /// </summary> /// <param name="buildingPos">建筑的世界坐标位置</param> /// <param name="buildingSize">建筑占地的世界尺寸,x、z对应水平方向长宽</param> /// <param name="extraClearRadius">建筑周边额外清除草地的半径,单位:米</param> /// <param name="groundLayerIndex">待铺设地面层在alphamap中的索引,默认1</param> /// <param name="grassLayerIndex">草地层在alphamap中的索引,默认0</param> private void SetGroundOnPosition(Vector3 buildingPos, Vector3 buildingSize, float extraClearRadius = 1f, int groundLayerIndex = 1, int grassLayerIndex = 0) { // 缓存地形组件和基础数据 Terrain targetTerrain = GameObject.Find("Terrain").GetComponent<Terrain>(); TerrainData terrainData = targetTerrain.terrainData; Vector3 terrainOriginPos = targetTerrain.transform.position; // 计算单个alphamap像素对应的世界空间尺寸 float pixelWorldWidth = terrainData.size.x / terrainData.alphamapWidth; float pixelWorldDepth = terrainData.size.z / terrainData.alphamapHeight; // 计算需要修改的总范围(建筑尺寸+两侧额外清理半径),转成alphamap像素尺寸 float totalWorldWidth = buildingSize.x + extraClearRadius * 2; float totalWorldDepth = buildingSize.z + extraClearRadius * 2; int pixelWidth = Mathf.CeilToInt(totalWorldWidth / pixelWorldWidth); int pixelDepth = Mathf.CeilToInt(totalWorldDepth / pixelWorldDepth); // 计算修改区域的起点像素坐标,以建筑中心为基准对齐 int startPixelX = Mathf.FloorToInt( ((buildingPos.x - totalWorldWidth / 2f) - terrainOriginPos.x) / terrainData.size.x * terrainData.alphamapWidth ); int startPixelZ = Mathf.FloorToInt( ((buildingPos.z - totalWorldDepth / 2f) - terrainOriginPos.z) / terrainData.size.z * terrainData.alphamapHeight ); // 边界钳制,避免坐标超出地形范围引发报错或写入异常 startPixelX = Mathf.Clamp(startPixelX, 0, terrainData.alphamapWidth - pixelWidth); startPixelZ = Mathf.Clamp(startPixelZ, 0, terrainData.alphamapHeight - pixelDepth); pixelWidth = Mathf.Min(pixelWidth, terrainData.alphamapWidth - startPixelX); pixelDepth = Mathf.Min(pixelDepth, terrainData.alphamapHeight - startPixelZ); // 读取目标区域原有alphamap数据,禁止直接新建空数组覆盖 float[,,] alphaData = terrainData.GetAlphamaps(startPixelX, startPixelZ, pixelWidth, pixelDepth); // 遍历区域内所有像素修改权重 for (int z = 0; z < pixelDepth; z++) { for (int x = 0; x < pixelWidth; x++) { // 计算当前像素对应的世界坐标 float currentWorldX = terrainOriginPos.x + (startPixelX + x) * pixelWorldWidth; float currentWorldZ = terrainOriginPos.z + (startPixelZ + z) * pixelWorldDepth; // 判断当前像素是否在清理范围内 float dx = Mathf.Abs(currentWorldX - buildingPos.x); float dz = Mathf.Abs(currentWorldZ - buildingPos.z); bool needClear = dx <= buildingSize.x / 2f + extraClearRadius && dz <= buildingSize.z / 2f + extraClearRadius; if (needClear) { // 范围内清空草地权重,全权重切换到目标地面层 alphaData[z, x, grassLayerIndex] = 0f; alphaData[z, x, groundLayerIndex] = 1f; // 其余无关层权重清零,保证单像素所有层权重和为1 for (int layer = 2; layer < terrainData.alphamapLayers; layer++) { alphaData[z, x, layer] = 0f; } } } } // 写回修改后的数据刷新地形 terrainData.SetAlphamaps(startPixelX, startPixelZ, alphaData); targetTerrain.Flush(); }
使用注意事项
- 调用前确认自身地形的alphamap层顺序,如果草地、硬地面的索引和默认约定不一致,传入对应索引参数即可
- 地形数据修改有一定性能开销,不要在
Update等高频生命周期里调用,建筑放置完成时调用一次即可 - 如果需要草地和硬地面的过渡更自然,可以在范围边缘加权重渐变逻辑,不要硬切0和1的权重值,能消除明显接缝
- 如果场景是多地形分片的结构,不要用
GameObject.Find硬找地形对象,要根据建筑世界坐标定位所属的Terrain组件再修改,避免写错地形数据
内容的提问来源于stack exchange,提问作者Daniil999
相关产品推荐
相关产品推荐

