要搞一个无限动态地形,本来这种和场景相关的功能我都是不管的,美术同事搞个插件即可,毕竟专业人员。
不过同事搞的插件有问题,地形动态拼接有问题,运行几个小时还有内存泄露,于是任务落我头上了,所以记录一下。
首先确定动态地形的地图索引和坐标,这里我使用的九宫格模式,如下:
首先我们创建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,今天暂时就到这里,下一篇讲解关于动态地形法线贴图和光照的处理。