效果预览图

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);}//计算uvVector2[] 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赋予生成的TerrainMesh 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(){//新建一个保存“草根”的Listverts = 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网格顶点上限65535for (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.htmlmesh.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(Vector3 pos, Vector3 patchSize, int grassCountPerPatch){//循环以生成“草根集”for (int i = 0; i < grassCountPerPatch; i++){//Random.value范围[0,1]之间的一个随机浮点数,将其乘以步幅大小var randomX = Random.value * patchSize.x;var randomZ = Random.value * patchSize.z;int indexX = (int)(pos.x + randomX);int indexZ = (int)(pos.z + randomZ);//防止种草种出地形if (indexX >= terrainSize){indexX = (int)terrainSize - 1;}if (indexZ >= terrainSize){indexZ = (int)terrainSize - 1;}//添加此次循环生成的点的位置Vector3 currentPos = new Vector3(pos.x + randomX, heightMap.GetPixel(indexX, indexZ).grayscale * terrainHeight, pos.z + randomZ);verts.Add(currentPos);}}

此时我们运行程序,可以看到以下效果,粉色的一堆点就是我们已经生成的点云,接下来,我们就要赋予这些点以材质,让它们成功生草ww


3. 利用Geometry Shader生成草地

*此步骤我们将涉及Geometry Shader的相关内容,如果你对Geometry Shader不甚了解,或者想去了解更多关于它的内容的话,笔者推荐以下几篇入门文章:
【风宇冲】Shader:二十七GeometryShaders
Unity之Geometry Shader
几何着色器-LearnOpenGL

——————————
那么我们经过了前两步,已经拥有了一个地形,和一堆在地形上的点,那么我们怎么通过点来生成草呢?通过陈大神的文章我们可知,能够通过几何着色器,即Geometry Shader来以点为“根”,以此为底边中点生成草的网格,从而构建一根草

那么正式动手。
我们新建一个Unlit Shader,将其命名为“GrassShader”
同样的,笔者假设你已经拥有一定的入门Shader编程基础,只会在代码之间进行必要的注释讲解

Properties{_MainTex ("Texture", 2D) = "white" {}_AlphaTex("Alpha (A)", 2D) = "white" {}_Height("Grass Height", float) = 3_Width("Grass Width", range(0, 0.1)) = 0.05}
SubShader{//设置Alpha-to-coverage,相关文档:https://docs.unity3d.com/Manual/SL-Blend.htmlTags { "Queue"="AlphaTest" "RenderType" = "TransparentCutout" "IgnoreProjector" = "True"}LOD 100Cull OffPass{Cull OFFTags{ "LightMode" = "ForwardBase" }AlphaToMask OnCGPROGRAM#pragma vertex vert#pragma fragment frag#pragma geometry geom       //这个就是我们指定的Geometry Shader#include "UnityCG.cginc"#include "UnityLightingCommon.cginc"#pragma target 4.0struct v2g{float4 pos : SV_POSITION;float2 uv : TEXCOORD0;float3 normal : NORMAL;};//Geometry Shader的输出结构体,会传入Fragment Shaderstruct g2f{float2 uv : TEXCOORD0;float4 pos : SV_POSITION;float3 normal : NORMAL;};sampler2D _MainTex;sampler2D _AlphaTex;float4 _MainTex_ST;float _Width;float _Height;//没对传入数据做任何变更(甚至不需要转换顶点到裁剪空间),直接传入GeometryShader中v2g vert (appdata_full v){v2g o;o.pos = v.vertex;o.normal = v.normal;o.uv = v.texcoord;return o;}//一个用于初始化g2f结构体的函数g2f createGSOut() {g2f output;output.pos = float4(0, 0, 0, 0);output.normal = float3(0, 0, 0);output.uv= float2(0, 0);return output;}//设置输出最大顶点数,此处将网格中三角形共用的重复顶点也计算在内,因为我们一共有10个三角形,所以最大输出顶点数为30[maxvertexcount(30)]void geom (point v2g points[1], inout TriangleStream<g2f> triStream){float4 root = points[0].pos;//我们将使用12个顶点来作为每根草的网格顶点const int vertexCount = 12;//生成一个伪随机数,相关文档:https://blog.csdn.net/qq_23030843/article/details/104353754float random = sin(UNITY_HALF_PI * frac(root.x) + UNITY_HALF_PI * frac(root.z));//给每根草的长宽加上这个随机值,我们希望草的宽度不要太宽_Width = _Width + (random / 50);_Height = _Height +(random / 5);//初始化g2f数组g2f v[vertexCount] = {createGSOut(), createGSOut(), createGSOut(), createGSOut(),createGSOut(), createGSOut(), createGSOut(), createGSOut(),createGSOut(), createGSOut(), createGSOut(), createGSOut()};//竖直方向上的起始值float currentV = 0;float offsetV = 1.0 / (vertexCount/2 - 1);float currentHeightOffset = 0;float currentVertexHeight = 0;float windCoEff = 0;for (uint i = 0; i < vertexCount; i++){//简单起见,所有草的法线方向都设置为(0, 0, 1)v[i].normal = fixed3(0, 0, 1);//判断顶点应当处在哪个位置,算法思想在陈大神的文章中有叙述if(fmod(i, 2) == 0){ v[i].pos = float4(root.x - _Width, root.y + currentVertexHeight, root.z, 1);v[i].uv = fixed2(0, currentV);}else{v[i].pos = float4(root.x + _Width, root.y + currentVertexHeight, root.z, 1);v[i].uv = fixed2(1, currentV);currentV += offsetV;currentVertexHeight = currentV * _Height;}  if (fmod(i, 2) == 1) {windCoEff += offsetV;}//将顶点位置转换至裁剪空间v[i].pos = UnityObjectToClipPos(v[i].pos);}//在三角形输出流中将顶点加入其中,自动构建三角形for (int p = 0; p < (vertexCount - 2); p++) {triStream.Append(v[p]);triStream.Append(v[p + 2]);triStream.Append(v[p + 1]);}}//使用简单的Blinn-Phong光照模型进行着色,这部分比较基础,没什么特别好讲的fixed4 frag (g2f i) : SV_Target{// sample the texturefixed3 col = tex2D(_MainTex, i.uv);fixed4 alpha = tex2D(_AlphaTex, i.uv);fixed3 light;half3 worldNormal = UnityObjectToWorldNormal(i.normal);//ambientfixed3 ambient = ShadeSH9(half4(worldNormal, 1));//diffusefixed3 diffuseLight = saturate(dot(worldNormal, UnityWorldSpaceLightDir(i.pos))) * _LightColor0;//specular Blinn-Phong fixed3 halfVector = normalize(UnityWorldSpaceLightDir(i.pos) + WorldSpaceViewDir(i.pos));fixed3 specularLight = pow(saturate(dot(worldNormal, halfVector)), 15) * _LightColor0;light = ambient + diffuseLight + specularLight;return fixed4(col * light, alpha.r);}ENDCG}}

生成的每根草的网格如图(取自陈大神文章)
记得写完Shader后将其赋予一个材质,
_MainTex所用的Texture为,模拟了草尖到草根的颜色变化
而_AlphaTex所用Texture为,模拟了草的形状。

将之前的GrassSpawner脚本中关于grassMat的注释去掉,然后将我们生成的材质在Inspector面板中赋予grassMat变量。

此时运行程序,我们可以得到如下结果

可以看到我们成功生成了一大片草,但是这些草不会动而且一致朝天长,显得非常单调而不真实。


4. 给草地添加模拟风吹的顶点运动

我们用sin函数来模拟风的吹动。由于一阵风吹来后,草地并不是直接整体往一个方向倒,而是像多米诺骨牌一样,先被吹倒的草会压向后面的草,呈现出一个波浪形。
不过在那之前,我们先让草没被风吹的时候就长得自然一点,不要全部朝天冲。

                 else{v[i].pos = float4(root.x + _Width, root.y + currentVertexHeight, root.z, 1);v[i].uv = fixed2(1, currentV);currentV += offsetV;currentVertexHeight = currentV * _Height;}  //我们在这一步先在Shader这个位置添加这两行float2 randomDir = float2(sin((random*15)), sin((random*10)));v[i].pos.xz += (sin((root.x * 10 + root.z / 5) * random)* windCoEff + randomDir * sin((random*15)))* windCoEff;if (fmod(i, 2) == 1) {windCoEff += offsetV;}

此时我们添加了两行代码,其实就是给顶点在x和z方向上添加了一个伪随机的位移量,不过这个伪随机的量笔者也是凭感觉瞎写的,读者完全可以自己写一个类似的随机偏移,无论如何,只要实现让自己感到满意的效果就可以了。
此时运行程序的话,效果是这样的,可以看到效果已经自然很多了。

那么我们接下来要模拟风的吹拂了
我们先设定风的方向变量windDir,然后创建一个wind变量来计算风的大小变化,其中,我们用sin来模拟风吹,并在sin中添加根的位置来作为自变量。
其中我们添加了_WindSpeed来控制风的速度(或者说sin的频率),因此我们要在Properties中声明它,顺便声明一个控制风力的_WindForce。记得在Pass中再定义一下同名变量来使用它们。

其实这种将风力和风速完全分离开来模拟风的方法是不符合自然规律的,但是根据图形学第一定律,看上去像是对那就是对的~

    Properties{_MainTex ("Texture", 2D) = "white" {}_AlphaTex("Alpha (A)", 2D) = "white" {}_Height("Grass Height", float) = 3_Width("Grass Width", range(0, 0.1)) = 0.05//新添加以下两行_WindSpeed("WindSpeed", float) = 5_WindForce("WindForce",float) = 1}......Pass{.......float _WindSpeed;float _WindForce;......void geom (point v2g points[1], inout TriangleStream<g2f> triStream){.......//这四行添加在刚刚添加的代码之后float2 windDir = fixed2(1, 1);float2 wind = windDir * sin(_Time.x * UNITY_PI * _WindSpeed * (root.x * windDir.x + root.z * windDir.y)/100);v[i].pos.xz += wind *_WindForce * windCoEff;v[i].pos.y -= length(wind *_WindForce * windCoEff);

_WindSpeed和_WindForce的默认值设置得相对较大,是为了出个明显的效果,大家完全可以自己调小试试。

由于CSDN无法上传超过5M的动图,笔者这里就不传gif了,大家运行工程自己看一下效果吧~


5. 解决从草片的侧面观看时产生的artifact

我所说的artifact(在图形学中泛指各种我们不愿意见到的瑕疵)是指什么呢,如下图,

当我们从草片的侧面看过去的时候,会看见一条像小路一样的空隙,感觉挺神奇的,但其实只是因为我们的草片比较薄,而且所有面片都与x轴平行,而我们的视线也正好与x轴基本平行……说到这里,解决方法也很明确了,那就是让草片沿y轴随机旋转!
让草片旋转,具体来说,也就是让我们生成的网格点沿根绕y轴旋转,那么,我们要如何让点沿它旋转呢?有一定图形学基础的读者肯定知道,那就得靠矩阵。
*如果读者对矩阵变换的内容不甚了解,可以阅读这篇文章:旋转变换(一)旋转矩阵

想要让一点p1绕另外特定一点p2旋转,我们可以将这个变换拆分为三个部分:
1. 将目标点p2转换至原点位置(p1也进行同样的平移变换)
2. 让p1进行“绕原点的旋转”
3. 将p1平移回去

在二维空间,这个变换可以写成这样的矩阵相乘,使用时,将这个矩阵M左乘p1

于是正式动手,我们先声明并初始化矩阵变量,因为使用的是齐次坐标系,所以矩阵应当是4x4大小。
然后在我们进行上一步所写的顶点偏移运动之前,先把顶点进行旋转。

             ......//可以写在geom的开头,计算root之后//生成一个随机的角度fixed randomAngle = frac(sin(root.x)*10000.0) * UNITY_HALF_PI;//平移矩阵1float4x4 firstransfromMat = float4x4(1.0, 0.0, 0.0, -root.x,0.0, 1.0, 0.0, -root.y,0.0, 0.0, 1.0, -root.z,0.0, 0.0, 0.0, 1.0);//旋转矩阵float4x4 transformationMatrix = float4x4(cos(randomAngle), 0, sin(randomAngle),0,0, 1, 0, 0,-sin(randomAngle), 0, cos(randomAngle),0,0, 0, 0, 1);//平移矩阵2float4x4 lasttransformat = float4x4(1.0, 0.0, 0.0, root.x,0.0, 1.0, 0.0, root.y,0.0, 0.0, 1.0, root.z,0.0, 0.0, 0.0, 1.0);......//在我们进行模拟风吹之前,先让点旋转v[i].pos = mul(lasttransformat,mul(transformationMatrix,mul(firstransfromMat, v[i].pos)));......

好,运行程序,现在我们无论从什么角度看,都看不到那种artifact了


6. 实现LOD

陈大神在文章中有写到判断每个根顶点距离摄像机的距离大小,根据其距离大小进行LOD,“分别需要1个quad(远)、3个quad(中)以及5个quad(近)”。
这个效果的实现也很简单,思路就是,在v2g(顶点着色器传往几何着色器的结构体)中加入一个摄像机空间的坐标,根据其z值判断远近,并设置两个距离阈值(因为我们有三种草片,将它们两两隔开需要两个阈值,在代码中,为_LODDistance1与_LODDistance2),以if-else实现分支
在每个分支中,我们只需要修改vertexCount与g2f v[vertexCount]这个数组的初始化内容即可,按理说后面的内容完全可以共用,但是笔者能力有限,各个分支中重复的内容以个人能力难以重构(笔者想得到的各种优化写法都会产生某些编译错误),个人也相当不满意这一点,如果路过的大神肯不吝赐教,就再好不过了。

当完成此内容时,我们的Shader内容是这样的

    Properties{_MainTex ("Texture", 2D) = "white" {}_AlphaTex("Alpha (A)", 2D) = "white" {}_Height("Grass Height", float) = 3_Width("Grass Width", range(0, 0.1)) = 0.05_WindSpeed("WindSpeed", float) = 5_WindForce("WindForce",float) = 1//LOD的两个分割点_LODDistance1("LODDistance_near(第一个分割点)",float) = 30_LODDistance2("LODDistance_far(第二个分割点)",float) = 50}SubShader{Tags { "Queue"="AlphaTest" "RenderType" = "TransparentCutout" "IgnoreProjector" = "True"}LOD 100Cull OffPass{Cull OFFTags{ "LightMode" = "ForwardBase" }AlphaToMask OnCGPROGRAM#pragma vertex vert#pragma fragment frag#pragma geometry geom#include "UnityCG.cginc"#include "UnityLightingCommon.cginc"#pragma target 4.0struct v2g{float4 pos : SV_POSITION;float2 uv : TEXCOORD0;float3 normal : NORMAL;//这一步所添加的观察空间坐标float3 viewPos : TEXCOORD1;};struct g2f{float2 uv : TEXCOORD0;float4 pos : SV_POSITION;float3 normal : NORMAL;};sampler2D _MainTex;sampler2D _AlphaTex;float4 _MainTex_ST;float _Width;float _Height;float _WindSpeed;float _WindForce;float _LODDistance1;float _LODDistance2;v2g vert (appdata_full v){v2g o;o.pos = v.vertex;o.normal = v.normal;o.uv = v.texcoord;//进行观察坐标计算o.viewPos = mul(UNITY_MATRIX_MV, v.vertex);return o;}g2f createGSOut() {g2f output;output.pos = float4(0, 0, 0, 0);output.normal = float3(0, 0, 0);output.uv= float2(0, 0);return output;}[maxvertexcount(30)]void geom (point v2g points[1], inout TriangleStream<g2f> triStream){float4 root = points[0].pos;float random = sin(UNITY_HALF_PI * frac(root.x) + UNITY_HALF_PI * frac(root.z));_Width = _Width + (random / 50);_Height = _Height +(random / 5);fixed randomAngle = frac(sin(root.x)*10000.0) * UNITY_HALF_PI;float4x4 firstransfromMat = float4x4(1.0, 0.0, 0.0, -root.x,0.0, 1.0, 0.0, -root.y,0.0, 0.0, 1.0, -root.z,0.0, 0.0, 0.0, 1.0);float4x4 transformationMatrix = float4x4(cos(randomAngle), 0, sin(randomAngle),0,0, 1, 0, 0,-sin(randomAngle), 0, cos(randomAngle),0,0, 0, 0, 1);float4x4 lasttransformat = float4x4(1.0, 0.0, 0.0, root.x,0.0, 1.0, 0.0, root.y,0.0, 0.0, 1.0, root.z,0.0, 0.0, 0.0, 1.0);//进行距离判断if(points[0].viewPos.z > -_LODDistance1){const int vertexCount = 12;g2f v[vertexCount] = {createGSOut(), createGSOut(), createGSOut(), createGSOut(),createGSOut(), createGSOut(), createGSOut(), createGSOut(),createGSOut(), createGSOut(), createGSOut(), createGSOut()};float currentV = 0;float offsetV = 1.0 / (vertexCount/2 - 1);float currentHeightOffset = 0;float currentVertexHeight = 0;float windCoEff = 0;for (uint i = 0; i < vertexCount; i++){v[i].normal = fixed3(0, 0, 1);if(fmod(i, 2) == 0){ v[i].pos = float4(root.x - _Width, root.y + currentVertexHeight, root.z, 1);v[i].uv = fixed2(0, currentV);}else{v[i].pos = float4(root.x + _Width, root.y + currentVertexHeight, root.z, 1);v[i].uv = fixed2(1, currentV);currentV += offsetV;currentVertexHeight = currentV * _Height;}  v[i].pos = mul(lasttransformat,mul(transformationMatrix,mul(firstransfromMat, v[i].pos)));float2 randomDir = float2(sin((random*15)), sin((random*10)));v[i].pos.xz += (sin((root.x * 10 + root.z / 5) * random)* windCoEff + randomDir * sin((random*15)))* windCoEff;float2 windDir = fixed2(1, 1);float2 wind = windDir * sin(_Time.x * UNITY_PI * _WindSpeed * (root.x * windDir.x + root.z * windDir.y)/100);v[i].pos.xz += wind *_WindForce * windCoEff;v[i].pos.y -= length(wind *_WindForce * windCoEff);if (fmod(i, 2) == 1) {windCoEff += offsetV;}v[i].pos = UnityObjectToClipPos(v[i].pos);}for (int p = 0; p < (vertexCount - 2); p++) {triStream.Append(v[p]);triStream.Append(v[p + 2]);triStream.Append(v[p + 1]);}}else if(points[0].viewPos.z > -_LODDistance2){const int vertexCount = 8;g2f v[vertexCount] = {createGSOut(), createGSOut(), createGSOut(), createGSOut(),createGSOut(), createGSOut(), createGSOut(), createGSOut(),};float currentV = 0;float offsetV = 1.0 / (vertexCount/2 - 1);float currentHeightOffset = 0;float currentVertexHeight = 0;float windCoEff = 0;for (uint i = 0; i < vertexCount; i++){v[i].normal = fixed3(0, 0, 1);if(fmod(i, 2) == 0){ v[i].pos = float4(root.x - _Width, root.y + currentVertexHeight, root.z, 1);v[i].uv = fixed2(0, currentV);}else{v[i].pos = float4(root.x + _Width, root.y + currentVertexHeight, root.z, 1);v[i].uv = fixed2(1, currentV);currentV += offsetV;currentVertexHeight = currentV * _Height;}  v[i].pos = mul(lasttransformat,mul(transformationMatrix,mul(firstransfromMat, v[i].pos)));float2 randomDir = float2(sin((random*15)), sin((random*10)));v[i].pos.xz += (sin((root.x * 10 + root.z / 5) * random)* windCoEff + randomDir * sin((random*15)))* windCoEff;float2 windDir = fixed2(1, 1);float2 wind = windDir * sin(_Time.x * UNITY_PI * _WindSpeed * (root.x * windDir.x + root.z * windDir.y)/100);v[i].pos.xz += wind *_WindForce * windCoEff;v[i].pos.y -= length(wind *_WindForce * windCoEff);if (fmod(i, 2) == 1) {windCoEff += offsetV;}v[i].pos = UnityObjectToClipPos(v[i].pos);}for (int p = 0; p < (vertexCount - 2); p++) {triStream.Append(v[p]);triStream.Append(v[p + 2]);triStream.Append(v[p + 1]);}}else{const int vertexCount = 4;g2f v[vertexCount] = {createGSOut(), createGSOut(), createGSOut(), createGSOut(),};float currentV = 0;float offsetV = 1.0 / (vertexCount/2 - 1);float currentHeightOffset = 0;float currentVertexHeight = 0;float windCoEff = 0;for (uint i = 0; i < vertexCount; i++){v[i].normal = fixed3(0, 0, 1);if(fmod(i, 2) == 0){ v[i].pos = float4(root.x - _Width, root.y + currentVertexHeight, root.z, 1);v[i].uv = fixed2(0, currentV);}else{v[i].pos = float4(root.x + _Width, root.y + currentVertexHeight, root.z, 1);v[i].uv = fixed2(1, currentV);currentV += offsetV;currentVertexHeight = currentV * _Height;}  v[i].pos = mul(lasttransformat,mul(transformationMatrix,mul(firstransfromMat, v[i].pos)));float2 randomDir = float2(sin((random*15)), sin((random*10)));v[i].pos.xz += (sin((root.x * 10 + root.z / 5) * random)* windCoEff + randomDir * sin((random*15)))* windCoEff;float2 windDir = fixed2(1, 1);float2 wind = windDir * sin(_Time.x * UNITY_PI * _WindSpeed * (root.x * windDir.x + root.z * windDir.y)/100);v[i].pos.xz += wind *_WindForce * windCoEff;v[i].pos.y -= length(wind *_WindForce * windCoEff);if (fmod(i, 2) == 1) {windCoEff += offsetV;}v[i].pos = UnityObjectToClipPos(v[i].pos);}for (int p = 0; p < (vertexCount - 2); p++) {triStream.Append(v[p]);triStream.Append(v[p + 2]);triStream.Append(v[p + 1]);}}}fixed4 frag (g2f i) : SV_Target{// sample the texturefixed3 col = tex2D(_MainTex, i.uv);fixed4 alpha = tex2D(_AlphaTex, i.uv);fixed3 light;half3 worldNormal = UnityObjectToWorldNormal(i.normal);//ambientfixed3 ambient = ShadeSH9(half4(worldNormal, 1));//diffusefixed3 diffuseLight = saturate(dot(worldNormal, UnityWorldSpaceLightDir(i.pos))) * _LightColor0;//specular Blinn-Phong fixed3 halfVector = normalize(UnityWorldSpaceLightDir(i.pos) + WorldSpaceViewDir(i.pos));fixed3 specularLight = pow(saturate(dot(worldNormal, halfVector)), 15) * _LightColor0;light = ambient + diffuseLight + specularLight;return fixed4(col * light, alpha.r);}ENDCG}}

要注意的是,在摄像机空间,z轴是从前指向后的,这也是OpenGL的传统,它在摄像机空间使用右手坐标系。也就是说,如果一个在我们前面的物体距离我们10个单位长度,则它在z轴上的值为-10。可推知,若我们要表示“在摄像机前面且距离摄像机在10个单位长度以内”则可以写作"viewPos.z > -10",代码中也以此作为分支判断。
*如果读者对这部分内容或者之前所提到的矩阵变换感兴趣,推荐去阅读学习冯乐乐所著的《Unity Shader 入门精要》第四章。

我们可以通过调整两个LODDistance的值来对比全部为最简草片和全部为最复杂草片时的草地情况。

全部为最复杂草片(上图)

全部为最简草片(上图)

乍一看区别并不大,但是细看可以发现,第二张图的草似乎变成了一根不会弯曲的小钢片,无论怎样,总是直的,而复杂草片则会明显有些弯曲。


7. 美化草地

那么总算到了最后,也是最简单的一步。
可以看见,我们的草虽然样子比较真实了,但是颜色不是很好看(贴图颜色画得比较深,显得没什么活力),我们在此基础上将其乘以一个亮色,让草地变得美观一些。
我们只需要在Properties中声明一个颜色(笔者这里用了HDR,只是个人喜好而已,并不必要):
[HDR]_ExtraColor(“Color”,Color) = (1, 1, 1, 1)
然后记得在Pass中引用它,并在最后frag输出颜色时,写成
return fixed4(col * light * _ExtraColor, alpha.r); 即可。

调整ExtraColor的值,笔者的设置如下

然后笔者又额外使用了PostProcess(相关学习文档),使用了TAA(Temporal Anti-Aliasing, 可译作“时域反锯齿”),加了一点Vignette、Bloom以及Depth of Field(景深),

运行效果便如下图与本文头图所示

处理之后的草地

个人觉得这样美观不少,不知你认为如何呢?


8. 总结

那么我们就把本文的案例草地做完了,但是想直接用在游戏中的话还是需要做一些修改的,比如实现“人物会把草踩偏”之类的交互效果,其实原理也不难,这部分就交给读者自己去思考吧~
说点题外话,笔者在学习之前觉得这个草地的效果应该蛮复杂的,但真正学习之后发现原理其实比较易懂,比较陌生的也只有几何着色器而已,在理解了原理后许多问题都迎刃而解了。让笔者懂得了世上无难事,只要肯去学,23333.

如果我的文章对你有所帮助,或者你觉得写得不错,不妨点个赞支持一下~

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

  1. 利用GPU实现无尽草地的实时渲染

    利用GPU实现无尽草地的实时渲染 0x00 前言 在游戏中展现一个写实的田园场景时,草地的渲染是必不可少的,而一提到高效率的渲染草地,很多人都会想起GPU Gems第七章 <Chapter 7. ...

  2. unity描边发光shader_unity shader实例#1 轮廓渲染-描边

    本文主要来讲几种描边的实现方法 1.法线外扩 一般期望的描边效果,就是在模型外面有一圈选边,因此我们可以把模型扩大一点点,利用这个扩大的边缘来实现描边效果.可以看出,扩大的方向其实就是法线的方向,边缘 ...

  3. 在3dmax里仿unity的Unlit/Texture shader并渲染

    在3dmax里实现unity的texture shader并渲染 3dsmax版本:2019 首先感谢这位大哥的 Unity与3DMax效果同步--3DMaxDX Shader:https://zhu ...

  4. OpenGL 几何着色器Geometry Shader

    OpenGL几何着色器Geometry Shader 几何着色器Geometry Shader简介 使用几何着色器 造几个房子 爆破物体 法向量可视化 几何着色器Geometry Shader简介 在 ...

  5. unity 2020 怎么写shader使其接受光照?_如何在Unity中造一个PBR Shader轮子

    之前有业界大佬建议我去了解下Unity的PBR.说来惭愧,我查找了下资料才发现自己在这方面的知识居然是一片空白.经过几周的学习与尝试我对这一块算是有了初步的了解,于是写了这篇文章,一方面对自己学到的东 ...

  6. 在Unity 3D中,shader是何时编译的,在何时加载入显存中的?

    在Unity 3D中,shader是何时编译的,在何时加载入显存中的? 是某一对象在实例化时,加载其相关的material与shader还是游戏开始时? 添加评论 分享 按时间排序按投票排序 4 个回 ...

  7. unity 2020 怎么写shader使其接受光照?_用Unity实现半条命Alyx中的液体物理效果

    干了两个月客户端的活终于能闲下来几天,有点空写个玩具了. 前段时间被半条命Alyx里的酒瓶刷屏了,这酒瓶里液体的的物理效果仅仅看录屏都能感受到十分棒,同时据说瓶子中液体的逻辑全部在shader的一个单 ...

  8. unity 全息和xRay shader

    unity 全息和xRay shader 这个是网上的效果,科幻的感觉是不是很强烈. 下面是我们去实现的效果. 先看下效果图,左边的是Xray的效果,右边是全息的效果.都有着异曲同工的妙处. 全息的效 ...

  9. unity URP内置shader lit解析

    unity内置的pbr渲染shader Properties为shader相关属性 两个SubShader里面为相应的渲染内容,第二个为降级处理渲染,如果第一个SubShader不兼容,才会渲染第二个 ...

最新文章

  1. 深度学习在目标视觉检测中的应用进展与展望
  2. nlp中的经典模型(三)
  3. 【数字逻辑设计】Logisim构建四位行波进位加法器
  4. ubuntu14.04安装arm-linux-gcc,Ubuntu14.04下arm-linux-gcc交叉编译环境搭建
  5. java设计思想和设计模式,快来收藏!
  6. [ZJOI2010]排列计数
  7. 3ds max 旋转及角度
  8. 微信小程序商城有发展机会吗?
  9. 政府安全资讯精选 2017年第四期:聚焦美国网络安全新动态
  10. 游戏地图主要功能实现
  11. 虚拟现实大作业——太阳系
  12. 夜天之书 #19 The ZeroMQ Community
  13. OSPF详解 一看就会奥!
  14. 如何通过Matplotlib画圆
  15. 微信小程序开发之mpVue
  16. vim autoformat php,Linux Vim代码格式化/美化插件vim-autoformat安装
  17. 四步修改Linux ip地址
  18. ORACLE表有逻辑坏块时EXPDP导出报错排查
  19. CSI笔记【8】:基于MUSIC Algorithm的DoA/AoA估计以及MATLAB实现
  20. Win10企业版激活(亲测有效)

热门文章

  1. js对ul列表中的项进行删除
  2. ldd命令 ubuntu_Linux命令:ldd
  3. 6级听力词汇与习语2
  4. 登录秒领2个月PPTV会员秒到 无需复杂步骤
  5. 揭秘抖音春节爆款 AR 道具背后的“秘密”
  6. ios系统不兼容的php命令,苹果固件不兼容怎么办 苹果固件不兼容解决方法【详解】...
  7. 数电学习(七、半导体存储器)
  8. RestHighLevelClient使用updateByQuery批量更新操作
  9. 喜欢捣蛋的无符号类型
  10. ResultSet相关ResultSetMetaData详细