Introductory Graphics: Dynamic Terrain (1)

      To create an infinite dynamic terrain, I don't care about the functions related to the scene. The art colleagues can just make a plug-in, after all, it is a professional.
      However, there is a problem with the plug-in made by my colleague, there is a problem with the dynamic stitching of the terrain, and there is a memory leak after running for several hours, so the task fell on me, so I will record it.
      First determine the map index and coordinates of the dynamic terrain. Here I use the Jiugongge pattern, as follows:
insert image description here      First, we create the GroundChunk class, which is each map:

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
}

      Next build the block map grid class:

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;
    }
}

      Next is the important Jiugongge management class, as follows:

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);
    }
}

      The core here is based on the concept of Jiugongge, first generate the Jiugongge map blocks in the center, and when the target moves to a certain map block, rearrange the Jiugongge map to distinguish between fixed map blocks and map blocks that need to be moved. After sorting, reset the Jiugongge map.
      The effect is as follows:
insert image description here      Next, enter the core map block terrain generation. In general, we use random noise maps to generate high and low terrain grids.
      Perlin noise
      We use Perlin noise to generate terrain maps. Unity comes with it ( Unity Perlin Noise )
      to test and generate noise maps, as follows:

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();
    }
}

      The effect is as follows:
insert image description here      we can use the R value (0-1) of the noise map to identify the height of the terrain, of course, it can be processed by shader or c#, as follows:

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"
}

      Here I use shader for processing. After all, I am using it for the PC platform, and there is no extreme requirement in terms of performance. I can also use tessellation later. And if it is used on the mobile platform, it is recommended to directly process the c# grid according to the height map.
      The effect is as follows:
insert image description here      Add tessellation to see, as follows:

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"
}

      Add a distance-based subdivision function, the effect is as follows:
insert image description here      unity quick tessellation
      Because the technology covered here has been talked about before, so only the core part is briefly explained.
      OK, that's all for today, the next article will explain the processing of dynamic terrain normal maps and lighting.

Guess you like

Origin blog.csdn.net/yinhun2012/article/details/122211918