以下类实现了在Unity中动态的修改Terrain的功能,可以在运行时升高、降低以及平滑地形高度。在Unity的Play Mode修改地形后退出Play Mode仍然会保留修改;当游戏打包成独立的可执行文件后退出游戏则不能保留对地形的修改,需要手动将地形数据序列化保存,下次启动时重新赋值。
using UnityEngine;
public class TerrainUtil
{
/**
* Terrain的HeightMap坐标原点在左下角
* y
* ↑
* 0 → x
*/
///
/// 返回Terrain上某一点的HeightMap索引。
///
/// Terrain
/// Terrain上的某点
/// 该点在HeightMap中的位置索引
public static int[] GetHeightmapIndex(Terrain terrain, Vector3 point)
{
TerrainData tData = terrain.terrainData;
float width = tData.size.x;
float length = tData.size.z;
// 根据相对位置计算索引
int x = (int)((point.x - terrain.GetPosition().x) / width * tData.heightmapWidth);
int y = (int)((point.z - terrain.GetPosition().z) / length * tData.heightmapHeight);
return new int[2] { x, y };
}
///
/// 返回GameObject在Terrain上的相对(于Terrain的)位置。
///
/// Terrain
/// GameObject
/// 相对位置
public static Vector3 GetRelativePosition(Terrain terrain, GameObject go)
{
return go.transform.position - terrain.GetPosition();
}
///
/// 返回Terrain上指定点在世界坐标系下的高度。
///
/// Terrain
/// Terrain上的某点
/// true: 获取最近顶点高度 false: 获取实际高度
/// 点在世界坐标系下的高度
public static float GetPointHeight(Terrain terrain, Vector3 point, bool vertex = false)
{
// 对于水平面上的点来说,vertex参数没有影响
if (vertex)
{
// GetHeight得到的是离点最近的顶点的高度
int[] index = GetHeightmapIndex(terrain, point);
return terrain.terrainData.GetHeight(index[0], index[1]);
}
else
{
// SampleHeight得到的是点在斜面上的实际高度
return terrain.SampleHeight(point);
}
}
///
/// 返回Terrain的HeightMap,这是一个 height*width 大小的二维数组,并且值介于 [0.0f,1.0f] 之间。
///
/// Terrain
/// 检索HeightMap时的X索引起点
/// 检索HeightMap时的Y索引起点
/// 在X轴上的检索长度
/// 在Y轴上的检索长度
///
public static float[,] GetHeightMap(Terrain terrain, int xBase = 0, int yBase = 0, int width = 0, int height = 0)
{
if (xBase + yBase + width + height == 0)
{
width = terrain.terrainData.heightmapWidth;
height = terrain.terrainData.heightmapHeight;
}
return terrain.terrainData.GetHeights(xBase, yBase, width, height);
}
///
/// 升高Terrain上某点的高度。
///
/// Terrain
/// Terrain上的点
/// 升高的高度
/// 笔刷大小
/// 当笔刷范围内其他点的高度已经高于笔刷中心点时是否同时提高其他点的高度
public static void Rise(Terrain terrain, Vector3 point, float opacity, int size, bool amass = true)
{
int[] index = GetHeightmapIndex(terrain, point);
Rise(terrain, index, opacity, size, amass);
}
///
/// 升高Terrain上的某点。
///
/// Terrain
/// HeightMap索引
/// 升高的高度
/// 笔刷大小
/// 当笔刷范围内其他点的高度已经高于笔刷中心点时是否同时提高其他点的高度
public static void Rise(Terrain terrain, int[] index, float opacity, int size, bool amass = true)
{
TerrainData tData = terrain.terrainData;
int bound = size / 2;
int xBase = index[0] - bound >= 0 ? index[0] - bound : 0;
int yBase = index[1] - bound >= 0 ? index[1] - bound : 0;
int width = xBase + size <= tData.heightmapWidth ? size : tData.heightmapWidth - xBase;
int height = yBase + size <= tData.heightmapHeight ? size : tData.heightmapHeight - yBase;
float[,] heights = tData.GetHeights(xBase, yBase, width, height);
float initHeight = tData.GetHeight(index[0], index[1]) / tData.size.y;
float deltaHeight = opacity / tData.size.y;
// 得到的heights数组维度是[height,width],索引为[y,x]
ExpandBrush(heights, deltaHeight, initHeight, height, width, amass);
tData.SetHeights(xBase, yBase, heights);
}
///
/// 降低Terrain上某点的高度。
///
/// Terrain
/// Terrain上的点
/// 降低的高度
/// 笔刷大小
/// 当笔刷范围内其他点的高度已经低于笔刷中心点时是否同时降低其他点的高度
public static void Sink(Terrain terrain, Vector3 point, float opacity, int size, bool amass = true)
{
int[] index = GetHeightmapIndex(terrain, point);
Sink(terrain, index, opacity, size, amass);
}
///
/// 降低Terrain上某点的高度。
///
/// Terrain
/// HeightMap索引
/// 降低的高度
/// 笔刷大小
/// 当笔刷范围内其他点的高度已经低于笔刷中心点时是否同时降低其他点的高度
public static void Sink(Terrain terrain, int[] index, float opacity, int size, bool amass = true)
{
TerrainData tData = terrain.terrainData;
int bound = size / 2;
int xBase = index[0] - bound >= 0 ? index[0] - bound : 0;
int yBase = index[1] - bound >= 0 ? index[1] - bound : 0;
int width = xBase + size <= tData.heightmapWidth ? size : tData.heightmapWidth - xBase;
int height = yBase + size <= tData.heightmapHeight ? size : tData.heightmapHeight - yBase;
float[,] heights = tData.GetHeights(xBase, yBase, width, height);
float initHeight = tData.GetHeight(index[0], index[1]) / tData.size.y;
float deltaHeight = -opacity / tData.size.y; // 注意负号
// 得到的heights数组维度是[height,width],索引为[y,x]
ExpandBrush(heights, deltaHeight, initHeight, height, width, amass);
tData.SetHeights(xBase, yBase, heights);
}
///
/// 根据笔刷四角的高度来平滑Terrain,该方法不会改变笔刷边界处的Terrain高度。
///
/// Terrain
/// Terrain上的点
/// 平滑灵敏度,值介于 [0.05,1] 之间
/// 笔刷大小
public static void Smooth(Terrain terrain, Vector3 point, float opacity, int size)
{
int[] index = GetHeightmapIndex(terrain, point);
Smooth(terrain, index, opacity, size);
}
///
/// 根据笔刷四角的高度来平滑Terrain,该方法不会改变笔刷边界处的Terrain高度。
///
/// Terrain
/// HeightMap索引
/// 平滑灵敏度,值介于 [0.05,1] 之间
/// 笔刷大小
public static void Smooth(Terrain terrain, int[] index, float opacity, int size)
{
TerrainData tData = terrain.terrainData;
if (opacity > 1 || opacity <= 0)
{
opacity = Mathf.Clamp(opacity, 0.05f, 1);
Debug.LogError("Smooth方法中的opacity参数的值应该介于 [0.05,1] 之间,强制将其设为:" + opacity);
}
// 取出笔刷范围内的HeightMap数据数组
int bound = size / 2;
int xBase = index[0] - bound >= 0 ? index[0] - bound : 0;
int yBase = index[1] - bound >= 0 ? index[1] - bound : 0;
int width = xBase + size <= tData.heightmapWidth ? size : tData.heightmapWidth - xBase;
int height = yBase + size <= tData.heightmapHeight ? size : tData.heightmapHeight - yBase;
float[,] heights = tData.GetHeights(xBase, yBase, width, height);
// 利用笔刷4角的高度来计算平均高度
float avgHeight = (heights[0, 0] + heights[0, width - 1] + heights[height - 1, 0] + heights[height - 1, width - 1]) / 4;
Vector2 center = new Vector2((float)(height - 1) / 2, (float)(width - 1) / 2);
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
// 点到矩阵中心点的距离
float toCenter = Vector2.Distance(center, new Vector2(i, j));
float diff = avgHeight - heights[i, j];
// 判断点在4个三角形区块上的位置
// 利用相似三角形求出点到矩阵中心点与该点连线的延长线与边界交点的距离
float d = 0;
if (i == height / 2 && j == width / 2) // 中心点
{
d = 1;
toCenter = 0;
}
else if (i >= j && i <= size - j) // 左三角区
{
// j/((float)width / 2) = d/(d+toCenter),求出距离d,其他同理
d = toCenter * j / ((float)width / 2 - j);
}
else if (i <= j && i <= size - j) // 上三角区
{
d = toCenter * i / ((float)height / 2 - i);
}
else if (i <= j && i >= size - j) // 右三角区
{
d = toCenter * (size - j) / ((float)width / 2 - (size - j));
}
else if (i >= j && i >= size - j) // 下三角区
{
d = toCenter * (size - i) / ((float)height / 2 - (size - i));
}
// 进行平滑时对点进行升降的比例
float ratio = d / (d + toCenter);
heights[i, j] += diff * ratio * opacity;
}
}
tData.SetHeights(xBase, yBase, heights);
}
///
/// 压平Terrain并提升到指定高度。
///
/// Terrain
/// 高度
public static void Flatten(Terrain terrain, float height)
{
TerrainData tData = terrain.terrainData;
float scaledHeight = height / tData.size.y;
float[,] heights = new float[tData.heightmapWidth, tData.heightmapHeight];
for (int i = 0; i < tData.heightmapWidth; i++)
{
for (int j = 0; j < tData.heightmapHeight; j++)
{
heights[i, j] = scaledHeight;
}
}
tData.SetHeights(0, 0, heights);
}
///
/// 设置Terrain的HeightMap。
///
/// Terrain
/// HeightMap
/// X起点
/// Y起点
public static void SetHeights(Terrain terrain, float[,] heights, int xBase = 0, int yBase = 0)
{
terrain.terrainData.SetHeights(xBase, yBase, heights);
}
// TODO
// public static void SaveHeightmapData(Terrain terrain, string path) {}
///
/// 扩大笔刷作用范围。
///
/// HeightMap
/// 高度变化量[-1,1]
/// 笔刷中心点的初始高度
/// HeightMap行数
/// HeightMap列数
/// 当笔刷范围内其他点的高度已经高于笔刷中心点时是否同时提高其他点的高度
private static void ExpandBrush(float[,] heights, float deltaHeight, float initHeight, int row, int column, bool amass)
{
// 高度限制
float limit = initHeight + deltaHeight;
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
if (amass) { heights[i, j] += deltaHeight; }
else // 不累加高度时
{
if (deltaHeight > 0) // 升高地形
{
heights[i, j] = heights[i, j] >= limit ? heights[i, j] : heights[i, j] + deltaHeight;
}
else // 降低地形
{
heights[i, j] = heights[i, j] <= limit ? heights[i, j] : heights[i, j] + deltaHeight;
}
}
}
}
}
#region 弃用的旧方法
/*public static*/
[System.Obsolete]
void Rise_Old(Terrain terrain, int[] index, float opacity, int size, bool amass = true)
{
if (index.Length != 2)
{
Debug.LogError("参数错误!");
return;
}
TerrainData tData = terrain.terrainData;
// heights中存储的是顶点高度,不是斜面的准确高度
float[,] heights = tData.GetHeights(0, 0, tData.heightmapWidth, tData.heightmapHeight);
float deltaHeight = opacity / tData.size.y;
ExpandBrush_Old(heights, index, deltaHeight, size, amass, tData.heightmapWidth, tData.heightmapHeight);
tData.SetHeights(0, 0, heights);
}
/*private static*/
[System.Obsolete]
void ExpandBrush_Old(float[,] heights, int[] index, float deltaHeight, int size, bool amass, int xMax, int yMax)
{
float limit = heights[index[0], index[1]] + deltaHeight;
int bound = size / 2;
for (int offsetX = -bound; offsetX <= bound; offsetX++)
{
int x = index[0] + offsetX;
if (x < 0 || x > xMax) continue;
for (int offsetY = -bound; offsetY <= bound; offsetY++)
{
int y = index[1] + offsetY;
if (y < 0 || y > yMax) continue;
if (amass)
{
heights[x, y] += deltaHeight;
}
else
{
if (deltaHeight > 0)
{
// 升高地形
heights[x, y] = heights[x, y] >= limit ? heights[x, y] : heights[x, y] + deltaHeight;
}
else
{
// 降低地形
heights[x, y] = heights[x, y] <= limit ? heights[x, y] : heights[x, y] + deltaHeight;
}
}
}
}
// 平滑方程:y = (cos(x) + 1) / 2;
//float rad = 180.0f * (smooth / 9) * Mathf.Deg2Rad;
//float height = (Mathf.Cos(rad) + 1) / 2;
}
#endregion
}