****

完整代码我已经上传到了我的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);
    }

自定义笔刷到这里就可以使用了,下一篇将介绍树木和草的动态编辑 

 

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐