****
完整代码我已经上传到了我的Github上,需要的话可以直接去下载https://github.com/xdedzl/RunTimeTerrainEditor,里面有一个TerrainModilfyDemo的场景,我做了一个简单的UI用来测试,工程版本目前使用的是2019.2,但2018.3之后的版本应该都没问题,但Unity貌似不支持从2019回滚到2018,需要新建工程后将资源复制过去。注意编译环境需要是.net4.x,用3.5会报错。
高度编辑请参考https://blog.csdn.net/xdedzl/article/details/85268674
自定义笔刷参考https://blog.csdn.net/xdedzl/article/details/85546694
****
一、利用反射调用方法
本篇将介绍Unity 地形的植被编辑,由于需要调用Terrain类中的一个非public函数,我们需要用到c#的反射。首先还是进入在前面编辑过的Extern扩展类中,为object写一个通过反射调用非公共方法的函数。
#region Reflection
/// <summary>
/// 通过反射和函数名调用非公有方法
/// </summary>
/// <param name="obj">目标对象</param>
/// <param name="methodName">函数名</param>
/// <param name="objs">参数数组</param>
public static void Invoke(this object obj, string methodName, params object[] objs)
{
BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
Type type = obj.GetType();
MethodInfo m = type.GetMethod(methodName, flags);
m.Invoke(obj, objs);
}
#endregion
二、TerrainUtility之树木编辑
现在继续编辑TerrainUtility类,要创建树木,首先需要有树的模板TreePrototype,其主要成员就是一个树预制体,我们可以把我们想要动态加入的树的预制体放在Resources/Terrain/SpeedTree/Trees文件夹下。注意如果是在编辑器下运行,关闭后动态添加的树木模板不会被清空,所以InitTerrPrototype只需要调用一次就好。
TerrainData提供了添加树木的方法AddTreeInstance,但是没有提供移除树的方法,当然我们也可以获取到地形上所以树的实体然后利用距离判断是不是我们想要移除的树,然后删掉它,但是这样做并不友好。
查阅了Terrain和TerrainData的源码后发现,Terrain提供了一个删除一个点周围半径为r的圆内的所有树的方法RemoveTrees(这个方法在VS中F12定位到类中看不到,只能在github看源码才有),但是外界无法直接调用。还好c#的反射机制给了我们机会。利用前文写的扩展方法和函数名可以轻松调用。
#region 树木
/// <summary>
/// 初始化树木原型组
/// </summary>
private static void InitTreePrototype()
{
GameObject[] objs = Resources.LoadAll<GameObject>("Terrain/SpeedTree/Trees");
TreePrototype[] trees = new TreePrototype[objs.Length];
for (int i = 0, length = objs.Length; i < length; i++)
{
trees[i] = new TreePrototype();
trees[i].prefab = objs[i];
}
Terrain[] terrains = Terrain.activeTerrains;
for (int i = 0, length = terrains.Length; i < length; i++)
{
// 先读取原有的模板,然后整合后赋值
terrains[i].terrainData.treePrototypes = terrains[i].terrainData.treePrototypes.Concat(trees).ToArray();
}
}
/// <summary>
/// 创建树木
/// </summary>
/// <param name="terrain"></param>
/// <param name="pos"></param>
public static void CreatTree(Terrain terrain, Vector3 pos, int count, int radius, int index = 0)
{
TerrainData terrainData = terrain.terrainData;
Vector3 relativePosition;
Vector3 position;
for (int i = 0; i < count; i++)
{
// 获取世界坐标系的位置和相对位置
position = pos + new Vector3(UnityEngine.Random.Range(-radius, radius), 0, UnityEngine.Random.Range(-radius, radius));
relativePosition = position - terrain.GetPosition();
if (Mathf.Pow(pos.x - position.x, 2) + Mathf.Pow(pos.z - position.z, 2) > radius * radius)
{
i--; // 没有创建的数不计入
continue;
}
// 设置新添加的树的参数
TreeInstance instance = new TreeInstance();
instance.prototypeIndex = index;
instance.color = Color.white;
instance.lightmapColor = Color.white;
instance.widthScale = 1;
instance.heightScale = 1;
Vector3 p = new Vector3(relativePosition.x / terrainData.size.x, 0, relativePosition.z / terrainData.size.z);
if (p.x > 1 || p.z > 1)
{
if (p.x > 1)
p.x = p.x - 1;
if (p.z > 1)
p.z = p.z - 1;
instance.position = p;
GetTerrain(position)?.AddTreeInstance(instance);
}
else if (p.x < 0 || p.z < 0)
{
if (p.x < 0)
p.x = p.x + 1;
if (p.z < 0)
p.z = p.z + 1;
instance.position = p;
GetTerrain(position)?.AddTreeInstance(instance);
}
else
{
instance.position = p;
terrain.AddTreeInstance(instance);
}
}
}
/// <summary>
/// 移除地形上的树,没有做多地图的处理
/// </summary>
/// <param name="terrain">目标地形</param>
/// <param name="center">中心点</param>
/// <param name="radius">半径</param>
/// <param name="index">树模板的索引</param>
public static void RemoveTree(Terrain terrain, Vector3 center, float radius, int index = 0)
{
center -= terrain.GetPosition(); // 转为相对位置
Vector2 v2 = new Vector2(center.x, center.z);
v2.x /= Terrain.activeTerrain.terrainData.size.x;
v2.y /= Terrain.activeTerrain.terrainData.size.z;
terrain.Invoke("RemoveTrees", v2, radius / Terrain.activeTerrain.terrainData.size.x, index);
}
#endregion
三、TerrainUtility之草的编辑
1.初始化
和树木编辑一样,编辑草首先也是添加模板,其次我们还要提供一个和编辑高度时类似的一个获取索引的方法,不一样的时,高度编辑时获取的索引代表的是地形网格的顶点,这里的索引代表的是一个小网格块,另外高度网格和这里的网格并不是同一个。
草模板DetailPrototype必要元素是一张草的贴图,事实上,我们在引擎中看到的草就是一张贴图,只不过它始终是面朝摄像机的。注意如果是在编辑器下运行,关闭后动态添加的细节模板不会被清空,所以InitDetailPrototype同样只需要调用一次。
/// <summary>
/// 初始化细节原型组
/// </summary>
private static void InitDetailPrototype()
{
Texture2D[] textures = Resources.LoadAll<Texture2D>("Terrain/Details");
DetailPrototype[] details = new DetailPrototype[textures.Length];
for (int i = 0, length = details.Length; i < length; i++)
{
details[i] = new DetailPrototype();
details[i].prototypeTexture = textures[i];
details[i].minWidth = 1;
details[i].maxWidth = 2;
details[i].maxHeight = 1;
details[i].maxHeight = 2;
details[i].noiseSpread = 0.1f;
details[i].healthyColor = Color.green;
details[i].dryColor = Color.yellow;
details[i].renderMode = DetailRenderMode.GrassBillboard;
}
Terrain[] terrains = Terrain.activeTerrains;
for (int i = 0, length = terrains.Length; i < length; i++)
{
terrains[i].terrainData.detailPrototypes = terrains[i].terrainData.detailPrototypes.Concat(details).ToArray();
}
}
/// <summary>
/// 返回Terrain上某一点的DetialMap索引。
/// </summary>
/// <param name="terrain">Terrain</param>
/// <param name="point">Terrain上的某点</param>
/// <returns>该点在DetialMap中的位置索引</returns>
private static Vector2Int GetDetialMapIndex(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.detailWidth);
int z = (int)((point.z - terrain.GetPosition().z) / length * tData.detailHeight);
return new Vector2Int(x, z);
}
2.跨地图
草的编辑过程和高度编辑是类似的,我在这里对跨地图的处理也是同高度编辑一样 ,主要的两个方法一个是获取数据,一个是对detailLayer重新赋值,中间对数据的处理是可以自由发挥的。和GetHeightMap和SetHeightMap不一样的是,草的编辑多了一个参数Layer,它代表的是草模板的索引,一个小网格块是可以同时编辑多种草的。
和heightMap是float型的二维数组不一样的是,detailMap是一个int型的二维数组,它的数据代表的是对应索引的网格块中草的数量,所以它的取值范围应该大于等于0。
/// <summary>
/// 获取细节数据
/// </summary>
private static int[,] GetDetailLayer(Terrain terrain, int xBase = 0, int yBase = 0, int width = 0, int height = 0, int layer = 0)
{
if (xBase + yBase + width + height == 0)
{
width = height = terrain.terrainData.detailResolution;
return terrain.terrainData.GetDetailLayer(xBase, yBase, width, height, layer);
}
TerrainData terrainData = terrain.terrainData;
int differX = xBase + width - terrainData.detailResolution;
int differY = yBase + height - terrainData.detailResolution;
int[,] ret;
if (differX <= 0 && differY <= 0) // 无溢出
{
ret = terrain.terrainData.GetDetailLayer(xBase, yBase, width, height, layer);
}
else if (differX > 0 && differY <= 0) // 右边溢出
{
ret = terrain.terrainData.GetDetailLayer(xBase, yBase, width - differX, height, layer);
int[,] right = terrain.Right()?.terrainData.GetDetailLayer(0, yBase, differX, height, layer);
if (right != null)
ret = ret.Concat0(right);
}
else if (differX <= 0 && differY > 0) // 上边溢出
{
ret = terrain.terrainData.GetDetailLayer(xBase, yBase, width, height - differY, layer);
int[,] up = terrain.Top()?.terrainData.GetDetailLayer(xBase, 0, width, differY, layer);
if (up != null)
ret = ret.Concat1(up);
}
else // 上右均溢出
{
ret = terrain.terrainData.GetDetailLayer(xBase, yBase, width - differX, height - differY, layer);
int[,] right = terrain.Right()?.terrainData.GetDetailLayer(0, yBase, differX, height - differY, layer);
int[,] up = terrain.Top()?.terrainData.GetDetailLayer(xBase, 0, width - differX, differY, layer);
int[,] upRight = terrain.Right()?.Top()?.terrainData.GetDetailLayer(0, 0, differX, differY, layer);
if (right != null)
ret = ret.Concat0(right);
if (upRight != null)
ret = ret.Concat1(up.Concat0(upRight));
}
return ret;
}
/// <summary>
/// 设置细节数据
/// </summary>
/// <param name="terrain"></param>
/// <param name="detailMap"></param>
/// <param name="xBase"></param>
/// <param name="yBase"></param>
/// <param name="layer"></param>
private static void SetDetailLayer(Terrain terrain, int[,] detailMap, int xBase, int yBase, int layer)
{
TerrainData terrainData = terrain.terrainData;
int length_1 = detailMap.GetLength(1);
int length_0 = detailMap.GetLength(0);
int differX = xBase + length_1 - (terrainData.detailResolution);
int differY = yBase + length_0 - (terrainData.detailResolution);
if (differX <= 0 && differY <= 0) // 无溢出
{
terrain.terrainData.SetDetailLayer(xBase, yBase, layer, detailMap);
}
else if (differX > 0 && differY <= 0) // 右溢出
{
terrain.terrainData.SetDetailLayer(xBase, yBase, layer, detailMap.GetPart(0, 0, length_0, length_1 - differX));
terrain.Right()?.terrainData.SetDetailLayer(0, yBase, layer, detailMap.GetPart(0, length_1 - differX, length_0, differX));
}
else if (differX <= 0 && differY > 0) // 上溢出
{
terrain.terrainData.SetDetailLayer(xBase, yBase, layer, detailMap.GetPart(0, 0, length_0 - differY, length_1));
terrain.Top()?.terrainData.SetDetailLayer(xBase, 0, layer, detailMap.GetPart(length_0 - differY, 0, differY, length_1));
}
else // 右上均溢出
{
terrain.terrainData.SetDetailLayer(xBase, yBase, layer, detailMap.GetPart(0, 0, length_0 - differY, length_1 - differX));
terrain.Right()?.terrainData.SetDetailLayer(0, yBase, layer, detailMap.GetPart(0, length_1 - differX, length_0 - differY, differX));
terrain.Top()?.terrainData.SetDetailLayer(xBase, 0, layer, detailMap.GetPart(length_0 - differY, 0, differY, length_1 - differX));
terrain.Top()?.Right().terrainData.SetDetailLayer(0, 0, layer, detailMap.GetPart(length_0 - differY, length_1 - differX, differY, differX));
}
}
3.处理获取到的detailMap
数据的处理是自由的,这里提供一个圆形的植被添加
/// <summary>
/// 修改细节数据
/// </summary>
/// <param name="detailMap"></param>
/// <param name="count"></param>
private static void ChangeDetailMap(int[,] detailMap, int count)
{
int mapRadius = detailMap.GetLength(0) / 2;
// 修改数据
for (int i = 0, length_0 = detailMap.GetLength(0); i < length_0; i++)
{
for (int j = 0, length_1 = detailMap.GetLength(1); j < length_1; j++)
{
// 限定圆
if ((i - mapRadius) * (i - mapRadius) + (j - mapRadius) * (j - mapRadius) > mapRadius * mapRadius)
continue;
detailMap[i, j] = count;
}
}
}
/// <summary>
/// 可跨多块地形的细节修改
/// </summary>
/// <param name="terrain"></param>
/// <param name="center"></param>
/// <param name="radius"></param>
/// <param name="layer"></param>
/// <param name="count"></param>
public static void NewSetDetail(Vector3 center, float radius, int layer, int count)
{
Vector3 leftDown = new Vector3(center.x - radius, 0, center.z - radius);
Terrain terrain = Utility.SendRayDown(leftDown, LayerMask.GetMask("Terrain")).collider?.GetComponent<Terrain>();
if (terrain != null)
{
// 获取数据
TerrainData terrainData = terrain.terrainData;
int mapRadius = (int)(radius / terrainData.size.x * terrainData.detailResolution);
Vector2Int mapIndex = GetDetialMapIndex(terrain, leftDown);
int[,] detailMap = GetDetailLayer(terrain, mapIndex.x, mapIndex.y, 2 * mapRadius, 2 * mapRadius, layer);
// 修改数据
ChangeDetailMap(detailMap, count);
// 设置数据
SetDetailLayer(terrain, detailMap, mapIndex.x, mapIndex.y, layer);
}
}
4.删除草
草的删除实际上就是把草的数量设为0
/// <summary>
/// 移除细节
/// </summary>
/// <param name="terrain">目标地形</param>
/// <param name="center">目标中心点</param>
/// <param name="radius">半径</param>
/// <param name="layer">层级</param>
public static void RemoveDetial(Terrain terrain, Vector3 point, float radius, int layer = 0)
{
//SetDetail(terrain, point, radius, layer, 0);
NewSetDetail(point, radius, layer, 0);
}
植被的编辑到这里就结束了,下一篇将介绍地形的贴图。