Unity 執行時動態編輯Terrain(二)地勢
如果理解了HeightMap,對一塊地形某一塊區域的地勢更改將會是一件很容易的事,但由於需要實現跨多塊地圖,四塊地圖之間的修改就會比較麻煩。從這一篇開始的幾篇文章,會逐步完善一個地形編輯工具類TerrainUtility及其他相關擴充套件。程式碼我已經上傳到了我的Github上,需要的話可以直接去下載https://github.com/xdedzl/xdedzl,裡面有一個TerrainModilfyDemo的場景,我做了一個簡單的UI用來測試,工程版本是2018.3。注意編譯環境需要時.net4.x,用3.5會報錯。
一、關鍵步驟
1.獲取地圖高度資料
public float[,] GetHeights(int xBase, int yBase, int width, int height);
這是TerrainData的獲取高度資料的函式,傳入起始索引和對應的寬高,返回一個高度資料
2.修改高度資料
根據需要修改返回的二維陣列,在這裡可以根據自己的演算法給地形以不同的形狀,後面會講到通過自定義筆刷繪製任意形狀
3.重新設定地形高度
public void SetHeights(int xBase, int yBase, float[,] heights);
這是TerrainData的設定地形高度的函式,傳入起始索引和包含高度資料的二維陣列設定高度
在這裡要注意的一點是設定和獲取的高度資料的二維陣列第一維對應Terrain的z軸,第二維對應的是x軸,不要想當然弄反了
二、獲取相鄰的地圖塊
由於涉及到多地圖塊之間的編輯,尋找相鄰地圖塊就必不可少了,Terrain本身提供了這種查詢,下面是Terrain類裡的四個屬性
public Terrain leftNeighbor { get; }
public Terrain rightNeighbor { get; }
public Terrain topNeighbor { get; }
public Terrain bottomNeighbor { get; }
注意,這是Unity2018中才有的,在Unity2017中我只看到了SetNieghbors的方法,Unity2018的Terrain系統相比2017強大了很多,可以在編輯器中跨地形編輯,可以直接建立地形的鄰居,2018.3為地形系統GPU例項渲染路徑,官方宣稱能減少50%的CPU消耗。為了方便編輯工具在2017中的使用,我給Terrain類寫了個擴充套件方法,用發射射線的方法獲取鄰居。
這裡我建立一個用於寫拓展方法的類ExternFun,後面還會其他的拓展方法會也會寫在這個類裡,利用預處理機制將Unity2018和其他版本區分,是Unity2018直接返回對應鄰居,不是的話則利用射線尋找,並且雖然這裡有上下左右四個鄰居,但實際上只用到了Top和Right兩個。
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
public static class ExtenFun
{
#region Terrain
/// <summary>
/// 右邊的地形塊
/// </summary>
/// <param name="terrain"></param>
/// <returns></returns>
public static Terrain Right(this Terrain terrain)
{
#if UNITY_2018
return terrain.rightNeighbor;
#else
Vector3 rayStart = terrain.GetPosition() + new Vector3(terrain.terrainData.size.x * 1.5f, 1000, terrain.terrainData.size.z * 0.5f);
RaycastHit hitInfo;
Physics.Raycast(rayStart, Vector3.down, out hitInfo, float.MaxValue, LayerMask.GetMask("Terrain"));
return hitInfo.collider?.GetComponent<Terrain>();
#endif
}
/// <summary>
/// 上邊的地形塊
/// </summary>
/// <param name="terrain"></param>
/// <returns></returns>
public static Terrain Up(this Terrain terrain)
{
#if UNITY_2018
return terrain.topNeighbor;
#else
Vector3 rayStart = terrain.GetPosition() + new Vector3(terrain.terrainData.size.x * 0.5f, 1000, terrain.terrainData.size.z * 1.5f);
RaycastHit hitInfo;
Physics.Raycast(rayStart, Vector3.down, out hitInfo, float.MaxValue, LayerMask.GetMask("Terrain"));
return hitInfo.collider?.GetComponent<Terrain>();
#endif
}
/// <summary>
/// 左邊的地形塊
/// </summary>
/// <param name="terrain"></param>
/// <returns></returns>
public static Terrain Left(this Terrain terrain)
{
#if UNITY_2018
return terrain.leftNeighbor;
#else
Vector3 rayStart = terrain.GetPosition() + new Vector3(-terrain.terrainData.size.x * 0.5f, 1000, terrain.terrainData.size.z * 0.5f);
RaycastHit hitInfo;
Physics.Raycast(rayStart, Vector3.down, out hitInfo, float.MaxValue, LayerMask.GetMask("Terrain"));
return hitInfo.collider?.GetComponent<Terrain>();
#endif
}
/// <summary>
/// 下邊的地形塊
/// </summary>
/// <param name="terrain"></param>
/// <returns></returns>
public static Terrain Down(this Terrain terrain)
{
#if UNITY_2018
return terrain.bottomNeighbor;
#else
Vector3 rayStart = terrain.GetPosition() + new Vector3(terrain.terrainData.size.x * 0.5f, 1000, -terrain.terrainData.size.z * 0.5f);
RaycastHit hitInfo;
Physics.Raycast(rayStart, Vector3.down, out hitInfo, float.MaxValue, LayerMask.GetMask("Terrain"));
return hitInfo.collider?.GetComponent<Terrain>();
#endif
}
#endregion
}
三、跨地圖
假設我們有一個由四塊Terrain組成的地圖,那麼跨地圖的編輯大致有四種情況
按照上面所說的三個步驟,我們只在第一步和第三步中對跨地圖的事情做處理,第二部只負責管理修改區域的形狀和高度,任務區分開來就好做了。當然了,除了上面四種情況,還有修改區域在整個地圖邊緣的情況也需要我們在程式碼中做處理。這樣一來,高度編輯的整個流程就清晰了。由上圖可知,在處理跨地形的問題時我們需要將多個地形的HeightMap進行融合,也需要將一個二維陣列拆分成多個HeightMap,所以,這裡再寫幾個拆分和融合的拓展函式加入到ExternFun類裡面。
#region Collection
public static T[,] Concat0<T>(this T[,] array_0, T[,] array_1)
{
if (array_0.GetLength(0) != array_1.GetLength(0))
{
Debug.LogError("兩個陣列第一維不一致");
return null;
}
T[,] ret = new T[array_0.GetLength(0), array_0.GetLength(1) + array_1.GetLength(1)];
for (int i = 0; i < array_0.GetLength(0); i++)
{
for (int j = 0; j < array_0.GetLength(1); j++)
{
ret[i, j] = array_0[i, j];
}
}
for (int i = 0; i < array_1.GetLength(0); i++)
{
for (int j = 0; j < array_1.GetLength(1); j++)
{
ret[i, j + array_0.GetLength(1)] = array_1[i, j];
}
}
return ret;
}
public static T[,] Concat1<T>(this T[,] array_0, T[,] array_1)
{
if (array_0.GetLength(1) != array_1.GetLength(1))
{
Debug.LogError("兩個陣列第二維不一致");
return null;
}
T[,] ret = new T[array_0.GetLength(0) + array_1.GetLength(0), array_0.GetLength(1)];
for (int i = 0; i < array_0.GetLength(0); i++)
{
for (int j = 0; j < array_0.GetLength(1); j++)
{
ret[i, j] = array_0[i, j];
}
}
for (int i = 0; i < array_1.GetLength(0); i++)
{
for (int j = 0; j < array_1.GetLength(1); j++)
{
ret[i + array_0.GetLength(0), j] = array_1[i, j];
}
}
return ret;
}
public static T[,] GetPart<T>(this T[,] array, int base_0, int base_1, int length_0, int length_1)
{
if (base_0 + length_0 > array.GetLength(0) || base_1 + length_1 > array.GetLength(1))
{
Debug.Log(base_0 + length_0 + ":" + array.GetLength(0));
Debug.Log(base_1 + length_1 + ":" + array.GetLength(1));
Debug.LogError("索引超出範圍");
return null;
}
T[,] ret = new T[length_0, length_1];
for (int i = 0; i < length_0; i++)
{
for (int j = 0; j < length_1; j++)
{
ret[i, j] = array[i + base_0, j + base_1];
}
}
return ret;
}
#endregion
四、射線及高斯模糊工具
射線是專案開發中比較常用的功能,這裡建立一個工具類Utility用來發射射線,然後再加上一個之前寫過的高斯模糊方法,高斯模糊用來對地形做平滑處理,相關類容可參考高斯模糊。高斯模糊的方法我在這裡用到了非同步。
// ==========================================
// 描述:
// 作者: HAK
// 時間: 2018-10-24 16:26:10
// 版本: V 1.0
// ==========================================
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;
using UnityEngine;
/// <summary>
/// 使用工具類
/// </summary>
public static class Utility
{
/// <summary>
/// 發射射線並返回RaycastInfo
/// </summary>
public static RaycastHit SendRay(int layer = -1)
{
RaycastHit hitInfo;
if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hitInfo, float.MaxValue, layer))
{
return hitInfo;
}
else
{
return default(RaycastHit);
}
}
public static RaycastHit SendRayDown(Vector3 start,int layer = -1)
{
RaycastHit hitInfo;
start.y += 10000;
if (Physics.Raycast(start,Vector3.down, out hitInfo, float.MaxValue, layer))
{
return hitInfo;
}
else
{
return default(RaycastHit);
}
}
/// <summary>
/// 對二維陣列做高斯模糊
/// </summary>
/// <param name="array">要處理的陣列</param>
/// <param name="dev"></param>
/// <param name="r">高斯核擴充套件半徑</param>
/// <param name="isCircle">改變形狀是否是圓</param>
public async static Task GaussianBlur(float[,] array, float dev, int r = 1,bool isCircle = true)
{
// 構造半徑為1的高斯核
int length = r * 2 + 1;
float[,] gaussianCore = new float[length, length];
float k = 1 / (2 * Mathf.PI * dev * dev);
for (int i = 0; i < length; i++)
{
for (int j = 0; j < length; j++)
{
float pow = -((j - r) * (j - r) + (i - r) * (i - r)) / (2 * dev * dev);
gaussianCore[i, j] = k * Mathf.Pow(2.71828f, pow);
}
}
// 使權值和為1
float sum = 0;
for (int i = 0; i < length; i++)
{
for (int j = 0; j < length; j++)
{
sum += gaussianCore[i, j];
}
}
for (int i = 0; i < length; i++)
{
for (int j = 0; j < length; j++)
{
gaussianCore[i, j] /= sum;
}
}
// 對二維陣列進行高斯模糊處理
int circleR = array.GetLength(0) / 2;
await Task.Run(async() =>
{
for (int i = r, length_0 = array.GetLength(0) - r; i < length_0; i++)
{
await Task.Run(() =>
{
for (int j = r, length_1 = array.GetLength(1) - r; j < length_1; j++)
{
if (isCircle && (i - circleR) * (i - circleR) + (j - circleR) * (j - circleR) > (circleR - r) * (circleR - r))
continue;
// 用高斯核處理一個值
float value = 0;
for (int u = 0; u < length; u++)
{
for (int v = 0; v < length; v++)
{
if ((i + u - r) >= array.GetLength(0) || (i + u - r) < 0 || (j + v - r) >= array.GetLength(1) || (j + v - r) < 0)
Debug.LogError("滴嘟滴嘟的報錯");
else
value += gaussianCore[u, v] * array[i + u - r, j + v - r];
}
}
array[i, j] = value;
}
});
}
});
}
}
Utility和ExternFun兩個會在後面繼續新增一些函式以方便TerrainUtility的呼叫,現在正式開始TerrainUtility類的開發
五、TerrainUtility之高度編輯
為了方便返回一個位置在高度圖上的索引,定義一個Vector2Int結構體,這個可以在直接定義在TerrainUtility類裡,外面暫時用不到
struct Vector2Int
{
public int x;
public int y;
public Vector2Int(int _x = 0, int _y = 0)
{
x = _x;
y = _y;
}
}
直接上程式碼,主要的兩個方法是GetHeightMap和SetHeightMap,在這兩步之間更改獲取到的HeightMap就可以了,ChangeHeight方法提供的是一個圓形的高度修改,後面會講到怎麼使用自定義筆刷修改地形。
using UnityEngine;
using System.Collections.Generic;
using System;
using System.Reflection;
using System.Linq;
using System.Threading.Tasks;
/**
* Terrain的HeightMap座標原點在左下角
* z
* ↑
* 0 → x
*/
/// <summary>
/// Terrain工具
/// terrainData.GetHeights和SetHeights的引數都是 值域為[0,1]的比例值
/// </summary>
public static class TerrainUtility
{
/// <summary>
/// 用於修改高度的單位高度
/// </summary>
private static float deltaHeight;
/// <summary>
/// 地形大小
/// </summary>
private static Vector3 terrainSize;
/// <summary>
/// 高度圖解析度
/// </summary>
private static int heightMapRes;
/// <summary>
/// 靜態建構函式
/// </summary>
static TerrainUtility()
{
deltaHeight = 1 / Terrain.activeTerrain.terrainData.size.y;
terrainSize = Terrain.activeTerrain.terrainData.size;
heightMapRes = Terrain.activeTerrain.terrainData.heightmapResolution;
}
#region 高度圖相關
/// <summary>
/// 返回Terrain上某一點的HeightMap索引。
/// </summary>
/// <param name="terrain">Terrain</param>
/// <param name="point">Terrain上的某點</param>
/// <returns>該點在HeightMap中的位置索引</returns>
private static Vector2Int 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 z = (int)((point.z - terrain.GetPosition().z) / length * tData.heightmapHeight);
return new Vector2Int(x, z);
}
/// <summary>
/// 返回地圖Index對應的世界座標系位置
/// </summary>
/// <param name="terrain"></param>
/// <param name="x"></param>
/// <param name="z"></param>
/// <returns></returns>
public static Vector3 GetIndexWorldPoint(Terrain terrain, int x, int z)
{
TerrainData data = terrain.terrainData;
float _x = data.size.x / (data.heightmapWidth - 1) * x;
float _z = data.size.z / (data.heightmapHeight - 1) * z;
float _y = GetPointHeight(terrain, new Vector3(_x, 0, _z));
return new Vector3(_x, _y, _z) + terrain.GetPosition();
}
/// <summary>
/// 返回GameObject在Terrain上的相對(於Terrain的)位置。
/// </summary>
/// <param name="terrain">Terrain</param>
/// <param name="go">GameObject</param>
/// <returns>相對位置</returns>
public static Vector3 GetRelativePosition(Terrain terrain, GameObject go)
{
return go.transform.position - terrain.GetPosition();
}
/// <summary>
/// 返回Terrain上指定點在世界座標系下的高度。
/// </summary>
/// <param name="terrain">Terrain</param>
/// <param name="point">Terrain上的某點</param>
/// <param name="vertex">true: 獲取最近頂點高度 false: 獲取實際高度</param>
/// <returns>點在世界座標系下的高度</returns>
public static float GetPointHeight(Terrain terrain, Vector3 point, bool vertex = false)
{
// 對於水平面上的點來說,vertex引數沒有影響
if (vertex)
{
// GetHeight得到的是離點最近的頂點的高度
Vector2Int index = GetHeightmapIndex(terrain, point);
return terrain.terrainData.GetHeight(index.x, index.y);
}
else
{
// SampleHeight得到的是點在斜面上的實際高度
return terrain.SampleHeight(point);
}
}
/// <summary>
/// 返回Terrain的HeightMap的一部分
/// 場景中有多塊地圖時不要直接呼叫terrainData.getheights
/// 這個方法會解決跨多塊地形的問題
/// </summary>
/// <param name="terrain">Terrain</param>
/// <param name="xBase">檢索HeightMap時的X索引起點</param>
/// <param name="yBase">檢索HeightMap時的Y索引起點</param>
/// <param name="width">在X軸上的檢索長度</param>
/// <param name="height">在Y軸上的檢索長度</param>
/// <returns></returns>
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);
}
TerrainData terrainData = terrain.terrainData;
int differX = xBase + width - (terrainData.heightmapResolution - 1); // 右溢位量級
int differY = yBase + height - (terrainData.heightmapResolution - 1); // 上溢位量級
float[,] ret;
if (differX <= 0 && differY <= 0) // 無溢位
{
ret = terrain.terrainData.GetHeights(xBase, yBase, width, height);
}
else if (differX > 0 && differY <= 0) // 右邊溢位
{
ret = terrain.terrainData.GetHeights(xBase, yBase, width - differX, height);
float[,] right = terrain.Right()?.terrainData.GetHeights(0, yBase, differX, height);
if (right != null)
ret = ret.Concat0(right);
}
else if (differX <= 0 && differY > 0) // 上邊溢位
{
ret = terrain.terrainData.GetHeights(xBase, yBase, width, height - differY);
float[,] up = terrain.Top()?.terrainData.GetHeights(xBase, 0, width, differY);
if (up != null)
ret = ret.Concat1(up);
}
else // 上右均溢位
{
ret = terrain.terrainData.GetHeights(xBase, yBase, width - differX, height - differY);
float[,] right = terrain.Right()?.terrainData.GetHeights(0, yBase, differX, height - differY);
float[,] up = terrain.Top()?.terrainData.GetHeights(xBase, 0, width - differX, differY);
float[,] upRight = terrain.Right()?.Top()?.terrainData.GetHeights(0, 0, differX, differY);
if (right != null)
ret = ret.Concat0(right);
if (upRight != null)
ret = ret.Concat1(up.Concat0(upRight));
}
return ret;
}
/// <summary>
/// 初始化地形高度圖編輯所需要的引數
/// 後四個引數需要在呼叫前定義
/// </summary>
/// <param name="center">目標中心</param>
/// <param name="radius">半徑</param>
/// <param name="mapIndex">起始修改點在高度圖上的索引</param>
/// <param name="heightMap">要修改的高度二維陣列</param>
/// <param name="mapRadius">修改半徑對應的索引半徑</param>
/// <param name="limit">限制高度</param>
/// <returns></returns>
private static Terrain InitHMArg(Vector3 center, float radius, ref Vector2Int mapIndex, ref float[,] heightMap, ref int mapRadius, ref int mapRadiusZ, ref float limit)
{
Vector3 leftDown = new Vector3(center.x - radius, 0, center.z - radius);
// 左下方Terrain
Terrain terrain = Utility.SendRayDown(leftDown, LayerMask.GetMask("Terrain")).collider?.GetComponent<Terrain>();
// 左下至少有一個方向沒有Terrain
if (terrain != null)
{
// 獲取相關引數
mapRadius = (int)(terrain.terrainData.heightmapResolution / terrain.terrainData.size.x * radius);
mapRadiusZ = (int)(terrain.terrainData.heightmapResolution / terrain.terrainData.size.z * radius);
mapRadius = mapRadius < 1 ? 1 : mapRadius;
mapRadiusZ = mapRadiusZ < 1 ? 1 : mapRadiusZ;
mapIndex = GetHeightmapIndex(terrain, leftDown);
heightMap = GetHeightMap(terrain, mapIndex.x, mapIndex.y, 2 * mapRadius, 2 * mapRadiusZ);
//limit = heightMap[mapRadius, mapRadius];
}
return terrain;
}
/// <summary>
/// 改變地形高度
/// </summary>
/// <param name="center"></param>
/// <param name="radius"></param>
/// <param name="opacity"></param>
/// <param name="amass"></param>
public static void ChangeHeight(Vector3 center, float radius, float opacity, bool isRise = true, bool amass = true)
{
int mapRadius = 0;
int mapRadiusZ = 0;
Vector2Int mapIndex = default(Vector2Int);
float[,] heightMap = null;
float limit = 0;
Terrain terrain = InitHMArg(center, radius, ref mapIndex, ref heightMap, ref mapRadius, ref mapRadiusZ, ref limit);
if (terrain == null) return;
if (!isRise) opacity = -opacity;
// 修改高度圖
for (int i = 0, length_0 = heightMap.GetLength(0); i < length_0; i++)
{
for (int j = 0, length_1 = heightMap.GetLength(1); j < length_1; j++)
{
// 限制範圍為一個圓
float rPow = (i - mapRadiusZ) * (i - mapRadiusZ) + (j - mapRadius) * (j - mapRadius);
if (rPow > mapRadius * mapRadiusZ)
continue;
float differ = 1 - rPow / (mapRadius * mapRadiusZ);
if (amass)
{
heightMap[i, j] += differ * deltaHeight * opacity;
}
else if (isRise)
{
heightMap[i, j] = heightMap[i, j] >= limit ? heightMap[i, j] : heightMap[i, j] + differ * deltaHeight * opacity;
}
else
{
heightMap[i, j] = heightMap[i, j] <= limit ? heightMap[i, j] : heightMap[i, j] + differ * deltaHeight * opacity;
}
}
}
// 重新設定高度圖
SetHeightMap(terrain, heightMap, mapIndex.x, mapIndex.y);
}
/// <summary>
/// 平滑地形
/// </summary>
/// <param name="center"></param>
/// <param name="radius"></param>
/// <param name="dev"></param>
/// <param name="level"></param>
public async static void Smooth(Vector3 center, float radius, float dev, int level = 1)
{
center.x -= terrainSize.x / (heightMapRes - 1) * level;
center.z -= terrainSize.z / (heightMapRes - 1) * level;
radius += terrainSize.x / (heightMapRes - 1) * level;
int mapRadius = 0;
int mapRadiusZ = 0;
Vector2Int mapIndex = default(Vector2Int);
float[,] heightMap = null;
float limit = 0;
Terrain terrain = InitHMArg(center, radius, ref mapIndex, ref heightMap, ref mapRadius, ref mapRadiusZ, ref limit);
if (terrain == null) return;
await Utility.GaussianBlur(heightMap, dev, level);
SetHeightMap(terrain, heightMap, mapIndex.x, mapIndex.y);
}
/// <summary>
/// 設定Terrain的HeightMap
/// 有不只一塊地形的場景不要直接呼叫terrainData.SetHeights
/// 這個方法會解決跨多塊地形的問題
/// </summary>
/// <param name="terrain">Terrain</param>
/// <param name="heights">HeightMap</param>
/// <param name="xBase">X起點</param>
/// <param name="yBase">Y起點</param>
public static void SetHeightMap(Terrain terrain, float[,] heights, int xBase = 0, int yBase = 0)
{
TerrainData terrainData = terrain.terrainData;
int length_1 = heights.GetLength(1);
int length_0 = heights.GetLength(0);
int differX = xBase + length_1 - (terrainData.heightmapResolution - 1);
int differY = yBase + length_0 - (terrainData.heightmapResolution - 1);
if (differX <= 0 && differY <= 0) // 無溢位
{
terrain.terrainData.SetHeights(xBase, yBase, heights);
}
else if (differX > 0 && differY <= 0) // 右溢位
{
terrain.terrainData.SetHeights(xBase, yBase, heights.GetPart(0, 0, length_0, length_1 - differX + 1)); // 最後的 +1是為了和右邊的地圖拼接
terrain.Right()?.terrainData.SetHeights(0, yBase, heights.GetPart(0, length_1 - differX, length_0, differX));
}
else if (differX <= 0 && differY > 0) // 上溢位
{
terrain.terrainData.SetHeights(xBase, yBase, heights.GetPart(0, 0, length_0 - differY + 1, length_1)); // 最後的 +1是為了和上邊的地圖拼接
terrain.Top()?.terrainData.SetHeights(xBase, 0, heights.GetPart(length_0 - differY, 0, differY, length_1));
}
else // 右上均溢位
{
terrain.terrainData.SetHeights(xBase, yBase, heights.GetPart(0, 0, length_0 - differY + 1, length_1 - differX + 1)); // 最後的 +1是為了和上邊及右邊的地圖拼接
terrain.Right()?.terrainData.SetHeights(0, yBase, heights.GetPart(0, length_1 - differX, length_0 - differY + 1, differX));
terrain.Top()?.terrainData.SetHeights(xBase, 0, heights.GetPart(length_0 - differY, 0, differY, length_1 - differX + 1));
terrain.Top()?.Right().terrainData.SetHeights(0, 0, heights.GetPart(length_0 - differY, length_1 - differX, differY, differX));
}
}
#endregion
}