****
完整代码我已经上传到了我的Github上,需要的话可以直接去下载https://github.com/xdedzl/RunTimeTerrainEditor,里面有一个TerrainModilfyDemo的场景,我做了一个简单的UI用来测试,工程版本目前使用的是2019.2,但2018.3之后的版本应该都没问题,但Unity貌似不支持从2019回滚到2018,需要新建工程后将资源复制过去。注意编译环境需要是.net4.x,用3.5会报错。
这一篇用到了上一篇的函数Unity动态编辑Terrain(二)地势
****
上一遍文章介绍了Terrain运行时编辑高度图,这一篇还是编辑高度,只是方式发生了改变,之前的编辑是在取到数据之后自己写算法决定笔刷的形状,这一次使用自定义笔刷,利用一张图片的透明度信息来决定笔刷的形状。
一、Unity编辑器中的自定义笔刷
Unity在编辑器模式下提供了自定义笔刷的功能,做以下几步即可
1.在根目录下创建一个名为Gizmos的文件夹
2.创建一个带透明通道的图片(推荐png格式,大小为256 * 256)并将图片命名为brush_0,brush_1,brush_2等,数字不可以有间隔
3.把图片放入Gizmos文件夹下,重启Unity
三步之后,就可以在Terrain的编辑界面看到我们的自定义笔刷了,unity在这里识别的是图片的Alpha通道,RGB的值对其不产生影响,所以我们做的图片只需要包含透明通道就可以了。
为了和Unity保持一致,我们在TerrainUtility中做的自定义笔刷功能利用Alpha通道计算,当然利用RGB当中的任一值都是可以的。
二、TerrainUtility自定义笔刷
1.图片的导入
在为Unity编辑器下的自定义笔刷导入图片时,我们不需要在导入后对图片做任何修改,但在自己的开发中,由于需要读写图片的数据,所以需要将图片ImportSettings中的 Read/Write Enables 选项勾上,如下图所示
2.双线性插值
我们在上一篇创建的Utility工具类中创建一个异步静态函数用于插值,关于双线性插值的具体类容可参考https://blog.csdn.net/xdedzl/article/details/85414427
/// <summary>
/// 对二维数组进行双线性插值
/// </summary>
/// <param name="array"></param>
/// <param name="length_0"></param>
/// <param name="length_1"></param>
/// <returns></returns>
public static async Task<float[,]> BilinearInterp(float[,] array, int length_0, int length_1)
{
float[,] _out = new float[length_0, length_1];
int original_0 = array.GetLength(0);
int original_1 = array.GetLength(1);
float ReScale_0 = original_0 / ((float)length_0); // 倍数的倒数
float ReScale_1 = original_1 / ((float)length_1);
float index_0;
float index_1;
int inde_0;
int inde_1;
float s_leftUp;
float s_rightUp;
float s_rightDown;
float s_leftDown;
await Task.Run(async () =>
{
for (int i = 0; i < length_0; i++)
{
await Task.Run(() =>
{
for (int j = 0; j < length_1; j++)
{
index_0 = i * ReScale_0;
index_1 = j * ReScale_1;
inde_0 = Mathf.FloorToInt(index_0);
inde_1 = Mathf.FloorToInt(index_1);
s_leftUp = (index_0 - inde_0) * (index_1 - inde_1);
s_rightUp = (inde_0 + 1 - index_0) * (index_1 - inde_1);
s_rightDown = (inde_0 + 1 - index_0) * (inde_1 + 1 - index_1);
s_leftDown = (index_0 - inde_0) * (inde_1 + 1 - index_1);
_out[i, j] = array[inde_0, inde_1] * s_rightDown +
array[inde_0 + 1, inde_1] * s_leftDown +
array[inde_0 + 1, inde_1 + 1] * s_leftUp + array[inde_0, inde_1 + 1] * s_rightUp;
}
});
}
});
return _out;
}
3. TerrainUtility之自定义笔刷
上一篇文章中的地形数据读取和写入搞清楚之后锕,自定义笔刷其实就很简单了。现在接着上一篇编写TerrainUtility,在代码中讲解。
首先为TerrainUtility添加一个静态变量,用于存储图片的透明通道信息,不需要没一次修改地形时还要读一遍图片数据
/// <summary>
/// 用于记录要修改的Terrain目标数据,修改后统一刷新
/// </summary>
private static Dictionary<int, float[,]> brushDic = new Dictionary<int, float[,]>();
然后创建一个初始化笔刷的静态函数并在静态构造函数中调用它 。
存储图片数据的二维数组大小先和图片的分辨率保持一致,在之后修改地形时再利用双线性插值做处理
我的自定义笔刷图片是保存在Resources/Terrain/Brushs下的(brush的复数形式是brushes)
/// <summary>
/// 初始化笔刷
/// </summary>
private static void InitBrushs()
{
Texture2D[] textures = Resources.LoadAll<Texture2D>("Terrain/Brushs");
for (int i = 0, length = textures.Length; i < length; i++)
{
// 获取图片颜色ARGB信息
Color[] colors = textures[i].GetPixels();
// terrainData.GetHeightMap得到的二维数组是[y,x]
float[,] alphas = new float[textures[i].height, textures[i].width];
for (int j = 0, length0 = textures[i].height, index = 0; j < length0; j++)
{
for (int k = 0, length1 = textures[i].width; k < length1; k++)
{
alphas[j, k] = colors[index].a;
index++;
}
}
brushDic.Add(i, alphas);
}
}
/// <summary>
/// 静态构造函数
/// </summary>
static TerrainUtility()
{
deltaHeight = 1 / Terrain.activeTerrain.terrainData.size.y;
terrainSize = Terrain.activeTerrain.terrainData.size;
heightMapRes = Terrain.activeTerrain.terrainData.heightmapResolution;
InitBrushs();
}
最后是供外部调用的利用自定义笔刷编辑地形的方法
/// <summary>
/// 通过自定义笔刷编辑地形
/// </summary>
/// <param name="terrain"></param>
public static async void ChangeHeightWithBrush(Vector3 center, float radius, float opacity, int brushIndex = 0, bool isRise = 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;
//修改高度图
await Task.Run(async () =>
{
float[,] deltaMap = await Utility.BilinearInterp(brushDic[brushIndex], 2 * mapRadius, 2 * mapRadius);
//float[,] deltaMap = await Utility.ZoomBilinearInterpAsync(brushDic[brushIndex], 2 * mapRadius, 2 * mapRadius);
for (int i = 0; i < 2 * mapRadius; i++)
{
for (int j = 0; j < 2 * mapRadius; j++)
{
heightMap[i, j] += deltaMap[i, j] * deltaHeight * opacity;
}
}
});
// 重新设置高度图
SetHeightMap(terrain, heightMap, mapIndex.x, mapIndex.y);
}
自定义笔刷到这里就可以使用了,下一篇将介绍树木和草的动态编辑