Unity 动态编辑Terrain地形(二)地势

如果理解了HeightMap,对一块地形某一块区域的地势更改将会是一件很容易的事,但由于需要实现跨多块地图,四块地图之间的修改就会比较麻烦。从这一篇开始的几篇文章,会逐步完善一个地形编辑工具类TerrainUtility及其他相关扩展。代码我已经上传到了我的Github上,需要的话可以直接去下载https://github.com/xdedzl/RunTimeTerrainEditor,里面有一个TerrainModilfyDemo的场景,我做了一个简单的UI用来测试,工程版本目前使用的是2019.2,但2018.3之后的版本应该都没问题,但Unity貌似不支持从2019回滚到2018,需要新建工程后将资源复制过去。注意编译环境需要时.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之高度编辑

UnityEgine的命名空间下有一个Vector2Int类,可以用来存储高度图索引

直接上代码,主要的两个方法是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
}

 

### 回答1: Unity是一款强大的游戏引擎,可以用来搭建精美的森林场景。建议从以下几个方面入手: 第一步是收集素材。要建立一个森林场景,需要收集一些有关于森林的素材,比如树木、草地、岩石、动物等等。在Unity Asset Store上可以轻松找到大量这些素材。 第步是创建地形。在Unity中创建地形非常简单,只需要选择Terrain工具然后按照你的想法塑造山丘、沟壑以及普通地面等等就可以了。你还可以在地形上添加草、沙、石头和其他材料。 第三步是添加树木和植物。在地形上利用Tree Creator工具创建树木或覆盖草地,给人一种青葱的感觉,再配以适当的花朵和植物,气息更加清新。 第四步是添加动物。你可以在Unity Asset Store里找到大量的动物模型,并将它们放置在地形上。这样可以使场景更加真实和生动。 第五步是创建天气效果。你可以利用Unity的天气系统创建逼真的下雨、刮风等天气效果,为场景增添幻境的气息。 最后,如果你想要场景更加真实,可以添加一些特效,比如阳光透过树叶洒在地上或者环境光的变化等等。这些都可以通过Unity的Particle System实现。 Unity的优秀的操作体验简化了森林场景的开发过程,无论是新手还是经验丰富的开发者都可以从中收获良多。 ### 回答2: Unity是一款常用于游戏开发的引擎,它提供了强大的开发工具和工作流程,使开发者能够轻松搭建出想象中的场景和游戏。搭建森林场景是Unity开发中的常见任务之一,具体步骤如下。 首先,我们需要收集在森林场景中所需的资源。这些资源包括树、草、花、灌木、岩石等等。在Unity Asset Store或其他资源素材库中都可以找到丰富的资源包供我们使用。我们需要选择符合要求的资源,并导入到项目中。 接下来,我们可以开始搭建场景。我们可以在场景中放置树、岩石、草等地形元素,使用Terrain工具进行地形的塑造、修改地势高度、贴图等。在放置物体时,我们需要注意其数量、形状、大小等,以达到自然、真实的效果。 接着,我们可以添加物理效果,例如碰撞体、刚体等。我们可以为树、岩石等景物添加碰撞体,使玩家在行走时能够有更真实的体验。我们还可以为落叶、流水等特效添加简单的物理效果,增强游戏的真实感。 最后,我们需要为场景添加光照、天空盒等元素。我们可以选择不同的光照模式、调整亮度和颜色、添加雾效果等,使场景更加真实。同时,我们也可以设置适合场景的天空盒,让场景背景更加自然。 总之,unity搭建森林场景需要依次完成素材收集、搭建场景、添加物理效果和光照天空盒等步骤,才能打造出真实、自然的森林场景。大家可以根据自己的需要和想象,对场景进行不断的修改和优化,以达到最佳效果。 ### 回答3: Unity是一款强大的游戏引擎,它提供了丰富的功能和工具,可以快速搭建出各种类型的游戏场景。搭建森林场景是Unity中常见的场景搭建,本文将介绍如何在Unity中搭建森林场景。 首先,在Unity中新建一个场景,设置好场景的大小和摄像机的位置。在场景视图中,选择地形工具,创建一个地形,然后选择细节工具,添加细节纹理、草、岩石等地形细节。可以使用自定义细节纹理或使用Unity提供的资源。 接下来,添加树木。在Unity中,可以使用Unity自带的树木资源库或自行导入树木模型。在资源库中选择需要的树木,将其拖放到场景中即可。 再次,添加天空。在Unity中,可以使用Skybox或自定义天空盒子。可将自定义天空盒子拖放到场景中即可。 最后,添加灯光。在Unity中,灯光非常重要,它可以让场景变得更加逼真。可以添加日光、夜光等效果。可以在灯光设置中调整灯光的颜色和强度,让灯光效果更加逼真。 搭建森林场景需要实践和掌握一定的技巧,以上仅是其中的一些基本步骤。如果想要制作一个更加逼真的森林场景,可以继续深入学习Unity的相关技术,探究terrain工具的更多选项和灯光的高级设置,同时还可以学习Shader和材质的使用,以及观察自然环境的画面色彩、光线等特点。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值