入门图形学:动态地形(一)

      要搞一个无限动态地形,本来这种和场景相关的功能我都是不管的,美术同事搞个插件即可,毕竟专业人员。
      不过同事搞的插件有问题,地形动态拼接有问题,运行几个小时还有内存泄露,于是任务落我头上了,所以记录一下。
      首先确定动态地形的地图索引和坐标,这里我使用的九宫格模式,如下:
在这里插入图片描述      首先我们创建GroundChunk类,也就是每一块地图:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GroundChunk : MonoBehaviour
{
    
    
    public int index;                       //块索引
    public Vector2Int position;             //块坐标
    public GroundPanelMesh mesh;            //平面网格

    public float chunkWidth;
    public float chunkLeft;
    public float chunkRight;
    public float chunkTop;
    public float chunkBottom;

    private void Awake()
    {
    
    
        mesh = GetComponent<GroundPanelMesh>();
    }

    void Start()
    {
    
    

    }

    public void Init(Vector2Int pos)
    {
    
    
        this.position = pos;
        this.index = pos.y * GroundMapController.ROW_CHUNK_COUNT + pos.x;
        gameObject.name = string.Format("chunk:{0}", index);
    }

    public void CreateMesh(int count, float width)
    {
    
    
        mesh.cellCount = count;
        mesh.cellWidth = width;
        mesh.CreateMesh();
    }

    public void SetParent(Transform parent)
    {
    
    
        transform.SetParent(parent);
    }

    public void SetWorldPos(Vector3 wpos)
    {
    
    
        transform.position = wpos;
    }

    public void SetActive(bool enab)
    {
    
    
        gameObject.SetActive(enab);
    }

    public void CalculateParameters(Vector3 wpos)
    {
    
    
        chunkWidth = mesh.cellCount * mesh.cellWidth;
        float hcwid = chunkWidth / 2f;
        chunkLeft = wpos.x - hcwid;
        chunkRight = wpos.x + hcwid;
        chunkTop = wpos.z + hcwid;
        chunkBottom = wpos.z - hcwid;
#if UNITY_EDITOR
        Debug.LogWarningFormat("GroundChunk wpos = {0} chunkLeft = {1} chunkRight = {2} chunkTop = {3} chunkBottom = {4}", wpos, chunkLeft, chunkRight, chunkTop, chunkBottom);
#endif
    }

    public Vector3 GetWorldPos()
    {
    
    
        return transform.position;
    }

    public bool CheckInBoundary(Vector3 wpos)
    {
    
    
        if (wpos.x >= chunkLeft
            && wpos.x <= chunkRight
            && wpos.z >= chunkBottom
            && wpos.z <= chunkTop)
        {
    
    
            return true;
        }
        return false;
    }

#if UNITY_EDITOR
    private void Update()
    {
    
    
        Vector3 lefttop = new Vector3(chunkLeft, 0, chunkTop);
        Vector3 leftbottom = new Vector3(chunkLeft, 0, chunkBottom);
        Vector3 rightbottom = new Vector3(chunkRight, 0, chunkBottom);
        Vector3 righttop = new Vector3(chunkRight, 0, chunkTop);
        Debug.DrawLine(lefttop, leftbottom, Color.white);
        Debug.DrawLine(leftbottom, rightbottom, Color.red);
        Debug.DrawLine(rightbottom, righttop, Color.yellow);
        Debug.DrawLine(righttop, lefttop, Color.blue);
    }
#endif
}

      接下来构建块地图网格类:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class GroundPanelMesh : MonoBehaviour
{
    
    
    public int cellCount = 50;
    public float cellWidth = 5f;

    private MeshRenderer meshRender;
    private MeshFilter meshFilter;
    private Mesh mesh;

    private void Awake()
    {
    
    
        meshRender = GetComponent<MeshRenderer>();
        meshFilter = GetComponent<MeshFilter>();
        mesh = new Mesh();
    }

    void Start()
    {
    
    

    }

    void Update()
    {
    
    

    }

    public void CreateMesh()
    {
    
    
        if (mesh != null)
        {
    
    
            mesh.Clear();
        }
        int cellVertexCount = cellCount + 1;
        Vector3[] vertices = new Vector3[cellVertexCount * cellVertexCount];
        Vector3[] normals = new Vector3[cellVertexCount * cellVertexCount];
        Vector2[] uvs = new Vector2[cellVertexCount * cellVertexCount];
        int[] triangles = new int[cellCount * cellCount * 6];
        int triindex = 0;
        Vector3 halfbias = new Vector3(cellCount * cellWidth / 2f, 0, cellCount * cellWidth / 2f);
        //逐行扫描
        //居中生成
        for (int y = 0; y <= cellCount; y++)
        {
    
    
            for (int x = 0; x <= cellCount; x++)
            {
    
    
                int index = cellVertexCount * y + x;
                vertices[index] = new Vector3(x * cellWidth, 0, y * cellWidth) - halfbias;
                normals[index] = new Vector3(0, 1, 0);
                uvs[index] = new Vector2((float)x / (float)cellCount, (float)y / (float)cellCount);
                if (x < cellCount && y < cellCount)
                {
    
    
                    int topindex = x + y * cellVertexCount;
                    int bottomindex = x + (y + 1) * cellVertexCount;
                    triangles[triindex + 5] = topindex;
                    triangles[triindex + 4] = topindex + 1;
                    triangles[triindex + 3] = bottomindex + 1;
                    triangles[triindex + 2] = topindex;
                    triangles[triindex + 1] = bottomindex + 1;
                    triangles[triindex] = bottomindex;
                    triindex += 6;
                }
            }
        }

        mesh.vertices = vertices;
        mesh.normals = normals;
        mesh.triangles = triangles;
        mesh.uv = uvs;

        meshFilter.sharedMesh = mesh;
    }
}

      接下来就是重要的九宫格管理类,如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class GroundMapController : MonoBehaviour
{
    
    
    public Transform mainTarget;                    //锚点
    [Range(0, -100f)]
    public float mapHeight = -10f;                  //地图生成y轴高低
    [Range(1, 100)]
    public int chunkCellCount = 50;                 //单块地图网格单元格数量
    [Range(0.1f, 10f)]
    public float chunkCellWidth = 5f;               //单元格宽度
    public GroundChunk chunkPrefab;
    public const int ROW_CHUNK_COUNT = 3;           //一行块数量,一共3*3=9

    private float chunkWidth;                       //块宽度

    private Queue<GroundChunk> chunkQueue = new Queue<GroundChunk>();           //块队列
    private List<GroundChunk> chunkMapList = new List<GroundChunk>();           //9块地图列表
    private Vector2Int lastChunkPos;                                            //mainActor九宫格坐标

    void Start()
    {
    
    
        InitChunkMap();
    }

    #region ///chunk factory

    private GroundChunk AllocChunk()
    {
    
    
        GroundChunk chunk;
        if (chunkQueue.Count > 0)
        {
    
    
            chunk = chunkQueue.Dequeue();
        }
        else
        {
    
    
            chunk = GameObject.Instantiate<GroundChunk>(chunkPrefab);
        }
        chunk.SetActive(true);
        return chunk;
    }

    private void RecycleChunk(GroundChunk chunk)
    {
    
    
        if (chunk != null)
        {
    
    
            chunk.SetActive(false);
            chunkQueue.Enqueue(chunk);
        }
    }

    #endregion
    /// <summary>
    /// 0 1 2      (0,0) (1,0) (2,0)
    /// 3 4 5  =>  (0,1) (1,1) (2,1)
    /// 6 7 8      (0,2) (1,2) (2,2)
    /// 初始化9宫格地图
    /// </summary>
    private void InitChunkMap()
    {
    
    
        chunkWidth = chunkCellCount * chunkCellWidth;
        for (int y = 0; y < ROW_CHUNK_COUNT; y++)
        {
    
    
            for (int x = 0; x < ROW_CHUNK_COUNT; x++)
            {
    
    
                Vector2Int pos = new Vector2Int(x, y);
                GroundChunk chunk = AllocChunk();
                chunk.Init(pos);
                chunk.CreateMesh(chunkCellCount, chunkCellWidth);
                chunk.SetParent(transform);
                Vector3 cwpos = GetChunkPos(pos, mainTarget.position);
                chunk.SetWorldPos(cwpos);
                chunk.CalculateParameters(cwpos);
                chunkMapList.Add(chunk);
            }
        }
        lastChunkPos = new Vector2Int(1, 1);
    }

    private Vector3 GetChunkPos(Vector2Int pos, Vector3 tpos)
    {
    
    
        int cindex = ROW_CHUNK_COUNT / 2;
        int left = pos.x - cindex;
        int top = cindex - pos.y;
        Vector3 ret = new Vector3(left * chunkWidth, mapHeight, top * chunkWidth) + new Vector3(tpos.x, 0, tpos.z);
        return ret;
    }

    /// <summary>
    /// 释放地图
    /// </summary>
    private void ReleaseChunkMap()
    {
    
    
        for (int i = 0; i < chunkMapList.Count; i++)
        {
    
    
            GroundChunk chunk = chunkMapList[i];
            RecycleChunk(chunk);
        }
        chunkMapList.Clear();
    }

    void Update()
    {
    
    
        bool reqinit = false;
        Vector2Int cpos = CheckInSudokuChunk(mainTarget.position, ref reqinit);
        //如果一次性移动超出9宫格地图范围
        //就需要重新初始化地图
        if (!reqinit)
        {
    
    
            if (lastChunkPos != cpos)
            {
    
    
                RankChunkMap(cpos);
                lastChunkPos = new Vector2Int(1, 1);
            }
        }
        else
        {
    
    
            ReleaseChunkMap();
            InitChunkMap();
        }
    }
    /// <summary>
    /// 起始index=4(1,1)为中心的棋盘格
    /// target一次移动一个格子
    /// </summary>
    /// <param name="index"></param>
    private void RankChunkMap(Vector2Int pos)
    {
    
    
        //区分需要固定块和移动块
        //target移动后的chunkindex相邻的chunk不需要移动
        //而不相邻的chunk需要重布局
        List<GroundChunk> fixedchunklist = new List<GroundChunk>();
        List<GroundChunk> movechunklist = new List<GroundChunk>();
        for (int i = 0; i < chunkMapList.Count; i++)
        {
    
    
            GroundChunk chunk = chunkMapList[i];
            if (CheckAroundChunk(pos, chunk.position))
            {
    
    
                fixedchunklist.Add(chunk);
            }
            else
            {
    
    
                movechunklist.Add(chunk);
            }
        }
        //9块地图
        List<Vector2Int> mapposlist = new List<Vector2Int>
        {
    
    
            new Vector2Int(0,0),
            new Vector2Int(1,0),
            new Vector2Int(2,0),
            new Vector2Int(0,1),
            new Vector2Int(1,1),
            new Vector2Int(2,1),
            new Vector2Int(0,2),
            new Vector2Int(1,2),
            new Vector2Int(2,2),
        };
        {
    
    
            //固定块不移动,但重写position
            Vector2Int bpos = lastChunkPos - pos;
            for (int i = 0; i < fixedchunklist.Count; i++)
            {
    
    
                GroundChunk chunk = fixedchunklist[i];
                Vector2Int cpos = chunk.position + bpos;
                chunk.Init(cpos);
                mapposlist.Remove(cpos);
            }
        }
        //中间块
        GroundChunk cchunk = fixedchunklist.Find(x => x.position == new Vector2Int(1, 1));
        {
    
    
            //移动块移动后,再重写position
            for (int i = 0; i < movechunklist.Count; i++)
            {
    
    
                GroundChunk chunk = movechunklist[i];
                Vector2Int mpos = mapposlist[i];
                Vector3 v3pos = GetChunkPos(mpos, cchunk.GetWorldPos());
                chunk.SetWorldPos(v3pos);
                chunk.CalculateParameters(v3pos);
                chunk.Init(mpos);
            }
        }
    }

    /// <summary>
    /// 判断i是否相邻(或相同)c
    /// 使用vector2int进行坐标判断(abs(xy)<=1)
    /// </summary>
    /// <param name="c"></param>
    /// <param name="i"></param>
    /// <returns></returns>
    private bool CheckAroundChunk(Vector2Int c, Vector2Int i)
    {
    
    
        //判断相同
        if (c == i)
        {
    
    
            return true;
        }
        Vector2Int ci = i - c;
        if (Mathf.Abs(ci.x) <= 1 && Mathf.Abs(ci.y) <= 1)
        {
    
    
            return true;
        }
        return false;
    }

    /// <summary>
    /// 检测target所处的块九宫格坐标
    /// </summary>
    /// <param name="wpos"></param>
    /// <param name="init"></param>
    /// <returns></returns>
    private Vector2Int CheckInSudokuChunk(Vector3 wpos, ref bool init)
    {
    
    
        for (int i = 0; i < chunkMapList.Count; i++)
        {
    
    
            GroundChunk chunk = chunkMapList[i];
            if (chunk.CheckInBoundary(wpos))
            {
    
    
                return chunk.position;
            }
        }
#if UNITY_EDITOR
        Debug.LogErrorFormat("GroundMapController CheckInSudokuChunk Need ReInitMap");
#endif
        init = true;
        return new Vector2Int(0, 0);
    }
}

      这里核心就是根据九宫格的概念,首先居中生成九宫格地图块,target移动到某一块地图块,就重新排列九宫格地图,区分固定不变的地图块和需要移动的地图块,排序完毕后,重置九宫格地图。
      效果如下:
在这里插入图片描述      接下来进入核心的地图块地形生成,一般情况,我们使用随机噪声贴图生成高低起伏的地形网格。
      perlin noise
      我们使用柏林噪声生成地形贴图,unity自带即可(Unity Perlin Noise
      测试生成噪声图,如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;

public class EditorPerlinNoiseTextureGenerator : EditorWindow
{
    
    
    private string texWidString;
    private int texWidth;
    private string texHeiString;
    private int texHeight;
    private string texScaleString;
    private float texScale;

    private string texName;

    private Texture2D pnTex;
    private Color[] pix;

    [MenuItem("GameTools/InfinityGround/GeneratePerlinNoiseTexture")]
    static void execute()
    {
    
    
        EditorPerlinNoiseTextureGenerator win = (EditorPerlinNoiseTextureGenerator)EditorWindow.GetWindow(typeof(EditorPerlinNoiseTextureGenerator), false, "GeneratePerlinNoiseTexture");
        win.Show();
    }

    private void OnGUI()
    {
    
    
        EditorGUILayout.LabelField("输入Texture宽度");
        texWidString = EditorGUILayout.TextField("int类型:", texWidString);
        EditorGUILayout.LabelField("输入Texture高度");
        texHeiString = EditorGUILayout.TextField("int类型:", texHeiString);
        EditorGUILayout.LabelField("输入Texture缩放");
        texScaleString = EditorGUILayout.TextField("float类型:", texScaleString);
        EditorGUILayout.LabelField("输入Texture名称");
        texName = EditorGUILayout.TextField("string类型:", texName);
        if (GUILayout.Button("生成贴图"))
        {
    
    
            if (!int.TryParse(texWidString, out texWidth) || !int.TryParse(texHeiString, out texHeight) || !float.TryParse(texScaleString, out texScale))
            {
    
    
                this.ShowNotification(new GUIContent("请输入int类型长宽,float类型缩放"));
                return;
            }
            Generate(texWidth, texHeight, texScale);
        }
    }

    private void Generate(int wid, int hei, float sca)
    {
    
    
        pix = new Color[wid * hei];

        int y = 0;
        while (y < hei)
        {
    
    
            int x = 0;
            while (x < wid)
            {
    
    
                float xcoord = (float)x / (float)wid * sca;
                float ycoord = (float)y / (float)hei * sca;
                float samp = Mathf.PerlinNoise(xcoord, ycoord);
                pix[y * wid + x] = new Color(samp, samp, samp);
                x++;
            }
            y++;
        }

        pnTex = new Texture2D(wid, hei);
        pnTex.SetPixels(pix);
        pnTex.Apply();

        byte[] buffer = pnTex.EncodeToJPG();
        string filepath = Application.dataPath + "/InfinityGround/Texture/" + texName + ".jpg";
        File.WriteAllBytes(filepath, buffer);
        AssetDatabase.Refresh();
    }
}

      效果如下:
在这里插入图片描述      我们可以通过noise贴图的R值(0-1)标识地形高度,当然使用shader或者c#处理都行,如下:

Shader "InfinityGround/GroundChunkUnTesselSurfaceShader"
{
    
    
    Properties
    {
    
    
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {
    
    }
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _NoiseTex("Noise Texture",2D) = "white" {
    
    }
        _HeightInten("Height Intensity",Range(1,100)) = 1
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows vertex:vert

        #include "UnityCG.cginc"

        #pragma target 4.0

        sampler2D _MainTex;

        struct Input
        {
    
    
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        sampler2D _NoiseTex;            //perlin高度图
        float _HeightInten;             //高度强度

        void vert(inout appdata_full v,out Input o)
        {
    
    
            UNITY_INITIALIZE_OUTPUT(Input,o);
            float3 normal = UnityObjectToWorldNormal(v.normal);
            float r = tex2Dlod(_NoiseTex,v.texcoord).r;
            v.vertex+=float4(normal*_HeightInten*r,0);
        }

        UNITY_INSTANCING_BUFFER_START(Props)
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
    
    
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

      这里我使用shader处理,毕竟我这是为了pc平台使用,性能方面没有极致要求,后面我还可以使用tessellation。而如果在手机平台使用,建议直接c#网格按照高度图处理完成。
      效果如下:
在这里插入图片描述      添加tessellation看看,如下:

Shader "InfinityGround/GroundChunkTesselSurfaceShader"
{
    
    
    Properties
    {
    
    
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {
    
    }
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _TesselMin("Tessellation Min Distance",Range(0,200)) = 1
        _TesselMax("Tessellation Max Distance",Range(0,400)) = 1
        _TesselFactor("Tessellation Factor",Range(1,20)) = 5
        _NoiseTex("Noise Texture",2D) = "white" {
    
    }
        _HeightInten("Height Intensity",Range(1,100)) = 10
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM

        #pragma surface surf Standard fullforwardshadows vertex:vert tessellate:tess

        #pragma target 5.0

        #include "Tessellation.cginc"

        sampler2D _MainTex;

        struct Input
        {
    
    
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        float _TesselMin;
        float _TesselMax;
        int _TesselFactor;
        sampler2D _NoiseTex;
        float _HeightInten;

        float4 tess(appdata_tan v0,appdata_tan v1,appdata_tan v2)
        {
    
    
            float4 v = UnityDistanceBasedTess(v0.vertex,v1.vertex, v2.vertex,_TesselMin,_TesselMax,_TesselFactor);
            return v;
        }

        void vert(inout appdata_tan v)
        {
    
    
            float3 normal = UnityObjectToWorldNormal(v.normal);
            float r = tex2Dlod(_NoiseTex,v.texcoord).r;
            v.vertex+=float4(normal*_HeightInten*r,0);
        }

        UNITY_INSTANCING_BUFFER_START(Props)
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
    
    
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

      添加一个基于距离细分的功能,效果如下:
在这里插入图片描述      unity quick tessellation
      因为这里涵盖的技术以前都聊过,所以只简洁说明核心部分。
      OK,今天暂时就到这里,下一篇讲解关于动态地形法线贴图和光照的处理。

猜你喜欢

转载自blog.csdn.net/yinhun2012/article/details/122211918