0. 前言
笔者最近阅读学习了知乎大神@陈嘉栋 所写的这篇文章:《利用GPU实现无尽草地的实时渲染》,这篇文章写得非常好,给出了实时生成一片草地的核心思路和基本流程,非常清晰……但可惜的是,如果读者没有一定Shader基础(尤其是关于GeometryShader)的话,在读了这篇文章后也很难直接着手去做,许多内容需要自己去下工程来解读。
笔者在下载工程并较为完整地学习/制作,且在此基础上做了一点小创新之后,认为这是一次不错的学习经验,值得拿出来与各位分享,所以才着手书写此篇文章,也打算比陈大神的文章讲解地更细致一点,希望能够对后来者有所帮助。
建议读者先去阅读陈大神的文章后再来读这篇,当然就算不读那篇也不会有太大问题,但是那篇确实写得很好,能够体验到要完成一个效果时所产生的思路流程,同样的实现思路笔者也不会在文章中过多赘述。
草地的大致制作流程如下:
- 用Script控制生成一个地形
- 生成地形之后在地形上生成大量的顶点(点云)作为之后要生成的草的根
- 以点云为基础,利用Geometry Shader生成草地
- 给草地添加模拟风吹的顶点运动
- 解决从草片的侧面观看时产生的artifact
- 实现陈在文章中所提到的LOD
- 美化草地
*加粗的后三点为陈大神工程中所未涉及的内容,属于笔者个人的一点微创新。其中,笔者个人对LOD部分的代码相当不满意,详细内容下文会有所提及。
*笔者使用unity版本为2018.4.16f
1. 用Script控制生成一个地形
*虽然流程1,2并不是本文想要讲解的重点,但笔者也会在必要的范围内进行讲解。
众所周知,当我们需要生成一个有高低变化的地形的时候,最方便的方法就是使用HeightMap来作为一个平面的顶点高低变化的依据,使其顶点的位置发生y轴上的偏移,我们这一步就是要做这件事。
那么首先,我们需要创建一个脚本(本工程也只需要这么一个脚本)用来生成地形,将其命名为GrassSpawner(直译为“生草者”,虽然这一步还没生草www) ,并将其挂载在Main Camera上。
GrassSpawner的具体内容请看以下代码:
public Texture2D heightMap; //我们指定的高度图
[Range(0, 70f)] //Range作为一个Attribute,可以方便我们在Inspector面板上进行
//拖拽更改变量的值,并对其大小做一个限制
public float terrainHeight = 10;
[Range(0, 250)]
public int terrainSize = 64; //我们所生成的地形的长宽
public Material terrainMat; //赋予所生成地形的材质
void Start()
{
GenerateTerrain(); //生成地形
}
private void GenerateTerrain()
{
//要生成一个平面,我们需要自定义其顶点和网格数据
List<Vector3> vertexs = new List<Vector3>();
List<int> tris = new List<int>();
//进行循环,生成一个基本的平面
for (int i = 0; i < terrainSize; i++)
for (int j = 0; j < terrainSize; j++)
{
//使用GetPixel读取高度图的灰度,计算所生成点的高度
vertexs.Add(new Vector3(i, heightMap.GetPixel(i, j).grayscale * terrainHeight, j));
//非坐标轴的顶点
if (i == 0 || j == 0)
continue;
//给tris添加vertex的索引,可以理解为把每三个顶点“相互连起来”,生成三角形
tris.Add(terrainSize * i + j);
tris.Add(terrainSize * i + j - 1);
tris.Add(terrainSize * (i - 1) + j - 1);
tris.Add(terrainSize * (i - 1) + j - 1);
tris.Add(terrainSize * (i - 1) + j);
tris.Add(terrainSize * i + j);
}
//计算uv
Vector2[] uvs = new Vector2[vertexs.Count];
for (var i = 0; i < uvs.Length; i++)
uvs[i] = new Vector2(vertexs[i].x, vertexs[i].z);
//创建一个名为Terrain的GameObject,并赋予其材质
GameObject plane = new GameObject("Terrain");
plane.AddComponent<MeshFilter>();
MeshRenderer renderer = plane.AddComponent<MeshRenderer>();
renderer.sharedMaterial = terrainMat;
//创建一个mesh来承载我们的网格数据,并将该mesh赋予生成的Terrain
Mesh groundMesh = new Mesh();
groundMesh.vertices = vertexs.ToArray();
groundMesh.uv = uvs;
groundMesh.triangles = tris.ToArray();
//重新计算法线
groundMesh.RecalculateNormals();
plane.GetComponent<MeshFilter>().mesh = groundMesh;
}
其中有部分代码为
//非坐标轴的顶点
if (i == 0 || j == 0)
continue;
这是什么意思呢?请看下图
笔者使用的地形材质和高度图(以及之后的草的贴图)均借用了陈大神在工程中使用的,关于Terrain的材质如下图,可见他也只是给Unity默认的材质加了个贴图而已,毕竟这部分并不是重点。
高度图(其实高度图并不需要借用大神的,只是笔者所持有的几张图凹凸效果并没有陈大神的这张那么好……):
此时完成效果大致是这样
2. 生成大量的顶点
于是我们接下来就要在地形的表面生成大量顶点,来作为草的根。
首先看代码:
//“草根集”的行列数,“草根集”是什么下文会提及
public int grassRowCount = 30;
public int grassCountPerPatch = 50;
//public Material grassMat; //之后会指定给点云的材质
private List<Vector3> verts;
void Start()
{
//新建一个保存“草根”的List
verts = new List<Vector3>();
GenerateTerrain();
//生成草的函数
GenerateGrassArea(grassRowCount, grassCountPerPatch);
}
private void GenerateTerrain()
{
......
//清空verts中的数据
verts.Clear();
}
private void GenerateGrassArea(int rowCount, int countPerPatch)
{
List<int> indices = new List<int>();
//Unity网格顶点上限65535
for (int i = 0; i < 65000; i++)
{
indices.Add(i);
}
//设置循环起始位置
var startPosition = new Vector3(0, 0, 0);
//计算每次循环后位置的偏移量,即“步幅”
var patchSize = new Vector3(terrainSize / rowCount, 0, terrainSize / rowCount);
for (int x = 0; x < rowCount; x++)
{
for (int y = 0; y < rowCount; y++)
{
//调用另一个函数来在startPosition的周围生成更多的随机分布的点,这些点即为上文提到的“草根集”
this.GenerateGrass(startPosition, patchSize, countPerPatch);
startPosition.x += patchSize.x;
}
startPosition.x = 0;
startPosition.z += patchSize.z;
}
Mesh mesh;
GameObject grassLayer;
MeshFilter meshFilter;
MeshRenderer renderer;
int a = 0;
//当要生成的顶点超过65000时
while (verts.Count > 65000)
{
mesh = new Mesh();
mesh.vertices = verts.GetRange(0, 65000).ToArray();
//设置子网格的索引缓冲区,相关官方文档:https://docs.unity3d.com/ScriptReference/Mesh.SetIndices.html
mesh.SetIndices(indices.ToArray(), MeshTopology.Points, 0);
//创建一个GameObject来承载这些点
grassLayer = new GameObject("grassLayer " + a++);
meshFilter = grassLayer.AddComponent<MeshFilter>();
renderer = grassLayer.AddComponent<MeshRenderer>();
//renderer.sharedMaterial = grassMat;
meshFilter.mesh = mesh;
verts.RemoveRange(0, 65000);
}
grassLayer = new GameObject("grassLayer" + a);
mesh = new Mesh();
mesh.vertices = verts.ToArray();
mesh.SetIndices(indices.GetRange(0, verts.Count).ToArray(), MeshTopology.Points, 0);
meshFilter = grassLayer.AddComponent<MeshFilter>();
renderer = grassLayer.AddComponent<MeshRenderer>();
meshFilter.mesh = mesh;
//renderer.sharedMaterial = grassMat;
}
private void GenerateGrass(