Unity動態編輯Terrain地形(四)植被編輯
****
完整程式碼我已經上傳到了我的Github上,需要的話可以直接去下載https://github.com/xdedzl/xdedzl,裡面有一個TerrainModilfyDemo的場景,我做了一個簡單的UI用來測試,工程版本是2018.3。注意編譯環境需要是.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);
}
植被的編輯到這裡就結束了,下一篇將介紹地形的貼圖。