【Unity Shader】使用Geometry Shader进行大片草地的实时渲染

在这里插入图片描述

效果预览图

0. 前言

笔者最近阅读学习了知乎大神@陈嘉栋 所写的这篇文章:《利用GPU实现无尽草地的实时渲染》,这篇文章写得非常好,给出了实时生成一片草地的核心思路和基本流程,非常清晰……但可惜的是,如果读者没有一定Shader基础(尤其是关于GeometryShader)的话,在读了这篇文章后也很难直接着手去做,许多内容需要自己去下工程来解读。
笔者在下载工程并较为完整地学习/制作,且在此基础上做了一点小创新之后,认为这是一次不错的学习经验,值得拿出来与各位分享,所以才着手书写此篇文章,也打算比陈大神的文章讲解地更细致一点,希望能够对后来者有所帮助。
建议读者先去阅读陈大神的文章后再来读这篇,当然就算不读那篇也不会有太大问题,但是那篇确实写得很好,能够体验到要完成一个效果时所产生的思路流程,同样的实现思路笔者也不会在文章中过多赘述。

草地的大致制作流程如下:

  1. 用Script控制生成一个地形
  2. 生成地形之后在地形上生成大量的顶点(点云)作为之后要生成的草的根
  3. 以点云为基础,利用Geometry Shader生成草地
  4. 给草地添加模拟风吹的顶点运动
  5. 解决从草片的侧面观看时产生的artifact
  6. 实现陈在文章中所提到的LOD
  7. 美化草地

*加粗的后三点为陈大神工程中所未涉及的内容,属于笔者个人的一点微创新。其中,笔者个人对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;

这是什么意思呢?请看下图
在这里插入图片描述

假设我们所需生成一个3x3的网格
其中,红点为满足i = 0或 j = 0的点,当我们循环到了一个非红点的点的时候,才能够接着运行下面的tris.add()的内容来构建三角形,否则就会报错。

笔者使用的地形材质和高度图(以及之后的草的贴图)均借用了陈大神在工程中使用的,关于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(
  • 22
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值