找回密码
 立即注册
查看: 459|回复: 0

[笔记] Grass Shader翻译及实现(草地渲染)

[复制链接]
发表于 2021-12-2 15:02 | 显示全部楼层 |阅读模式
上半年自学的时候翻译的文章,后面得知手机端无法使用就没深入研究.整个DEMO采用Built-in管线
有一些自己画的思路图和觉得相关的引用
最后有代码实现
萌新时期可能会存在一些翻译的纰漏
原文地址:
<hr/>Unity Grass Geometry Shader Tutorial at Roystan
<hr/>这篇文章将会一步一步地描述如何去写一个grass shader(Unity)。这个shader将会使用网格模型的每个顶点去生成草的叶子,通过使用几何着色器。为了创建生动真实的场景,这些草叶将会随机有着随机的大小和旋转角度,并且会受到风力的影响。为了控制草叶的密度,将会对网格进行细分曲边的操作。这些草叶也有能力去投射和接受阴影。
1.几何着色器

Geometry shader 在渲染管线中是一个可选的部分,在vertex shader之后(如果细分曲面着色器打开的话,就在tessellation后执行),在片元着色器之前执行。



Direct3D 11 graphics pipeline.Note that in this diagram,the fragment shader is referred to as the pixel shader.image sourced from here

几何着色器的输入是一个图元(如点或三角形)的一组顶点,能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。
我们将通过写一个几何着色器去让一个顶点作为输入,然后输出简单的三角形去绘制一片草叶。
//加在CGINCLUDE块中
struct geometryOutput
{
        float4 pos : SV_POSITION;
};

[maxvertexcount(3)]
void geo(triangle float4 IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream)
{
}



// Add inside the SubShader Pass, just below the #pragma fragment frag line.
#pragma geometry geo上面声明了一个名为geo的几何着色器,它包含了两个参数。
第一个参数 triangle float4 IN[3],表示我们将接受一个三角形(由三个顶点构成)作为输入。
第二个参数TriangleStream,我们的shader会去输出一个三角形流,每个顶点都用geometryOutput结构体来包含自身数据。
除此之外,我们在函数前的方括号内加了一个maxvertexcount(3)变量 ,这告诉GPU我们将至多输出三个顶点,通过声明它在Pass中也会确保我们的SubShader使用了几何着色器.

到目前为止我们的几何着色器还没有做任何事,接下来我们将在GS中加入下面的代码去输出一个三角形
geometryOutput o;

o.pos = float4(0.5, 0, 0, 1);
triStream.Append(o);

o.pos = float4(-0.5, 0, 0, 1);
triStream.Append(o);

o.pos = float4(0, 1, 0, 1);
triStream.Append(o);


这里是创建了两个plane并使其中一个作为地面,白色的为我们材质添加的对象(也就是草)

产生了一些奇怪的结果——移动相机可以看到三角形是在屏幕空间进行渲染的。这是因为GS发生在顶点阶段之前,它接管了顶点着色器的责任确保了顶点输出的结果在裁剪空间上,我们可以通过修改代码去解决它。
// 更新顶点着色器中的返回值
// return UnityObjectToClipPos(vertex);
return vertex;



// Update each assignment of o.pos in the geometry shader.
o.pos = UnityObjectToClipPos(float4(0.5, 0, 0, 1));



o.pos = UnityObjectToClipPos(float4(-0.5, 0, 0, 1));



o.pos = UnityObjectToClipPos(float4(0, 1, 0, 1));

现在三角形可以正确渲染在世界空间上。但是现在只有一个。
实际上,我们网格中的每个顶点现在都绘制了一个三角形,但我们为这个三角形顶点分配的位置是固定的——他们不会因每个输入的顶点位置不同而改变——相当于网格顶点所构建的所有三角形都出现在了同一个位置。
我们将通过更新我们的输出顶点的位置来纠正这个问题,让他们与输入点的偏移量相等。
// Add to the top of the geometry shader.
float3 pos = IN[0];



// Update each assignment of o.pos.
o.pos = UnityObjectToClipPos(pos + float3(0.5, 0, 0));



o.pos = UnityObjectToClipPos(pos + float3(-0.5, 0, 0));



o.pos = UnityObjectToClipPos(pos + float3(0, 1, 0));

现在三角形在正确的位置上被绘制。
在继续之前,我们先把两个Plane关掉,创建一个球体,测试在球形对象上的渲染方式,以应对斜坡之类地形的渲染。


可以看见,现在所有三角形都朝着同一个方向进行输出,而不是沿着球体表面方向,为了解决这个问题,我们需要在切线空间构建草叶。
2.切线空间(Tangent Space)

在理想的情况下,我们想要构建我们的草叶的参数——包含随机的宽度,高度,曲率和旋转角度——而不需要考虑草叶在不同表面的角度(斜坡或者凹凸不平的地面)。用更简单的术语来讲,我们将在切线空间中定义正确的叶片朝向然后转换到世界空间中。



在切线空间中,XYZ轴是由曲面的法线和位置进行定义的(在这个例子中,是顶点)

类似其他任意一种空间,我们的顶点可以在切线空间中用三个向量来定义:右,前,上。利用这些向量,我们可以构建一个矩阵旋转我们的叶片(从切线空间到模型空间)。
我们能够通过添加一些新的顶点输入来访问向右和向上的向量。
// 在CGINCLUDE中.
struct vertexInput
{
        float4 vertex : POSITION;
        float3 normal : NORMAL;
        float4 tangent : TANGENT;
};

struct vertexOutput
{
        float4 vertex : SV_POSITION;
        float3 normal : NORMAL;
        float4 tangent : TANGENT;
};



// 修改顶点shader
vertexOutput vert(vertexInput v)
{
        vertexOutput o;
        o.vertex = v.vertex;
        o.normal = v.normal;
        o.tangent = v.tangent;
        return o;
}



// 修改GS的输入,将SV_POSITION语义移除
void geo(triangle vertexOutput IN[3], inout TriangleStream<geometryOutput> triStream)



// Modify the existing line declaring pos.
float3 pos = IN[0].vertex;第三个向量能通过叉乘由其他两个向量(normal、tangent)叉乘得到,叉乘(Cross)会返回一个垂直于两个输入参数的向量。
// 在GS中计算
float3 vNormal = IN[0].normal;
float4 vTangent = IN[0].tangent;
float3 vBinormal = cross(vNormal, vTangent) * vTangent.w;为什么叉乘的结果要乘以tangent.w?
是为了决定副切线的方向性。
用这三个向量,我们能构建一个矩阵进行切线空间和模型空间的转换。在顶点被传入裁剪空间(untiyObjectToClipPos)前,草叶的每个顶点都乘以TBN矩阵来转换到模型空间。
//构建TBN矩阵
float3x3 tangentToLocal = float3x3(
        vTangent.x, vBinormal.x, vNormal.x,
        vTangent.y, vBinormal.y, vNormal.y,
        vTangent.z, vBinormal.z, vNormal.z
        );在使用这个矩阵前,我们将移动我们的顶点输出代码到函数中,以避免重复编写相同的代码行。
// 加在CGINCLUDE代码块中
geometryOutput VertexOutput(float3 pos)
{
        geometryOutput o;
        o.pos = UnityObjectToClipPos(pos);
        return o;
}



// 移除GS中下面代码
// geometryOutput o;

// o.pos = UnityObjectToClipPos(pos + float3(0.5, 0, 0));
// triStream.Append(o);

// o.pos = UnityObjectToClipPos(pos + float3(-0.5, 0, 0));
// triStream.Append(o);

// o.pos = UnityObjectToClipPos(pos + float3(0, 1, 0));
// triStream.Append(o);

// 用函数替代
triStream.Append(VertexOutput(pos + float3(0.5, 0, 0)));
triStream.Append(VertexOutput(pos + float3(-0.5, 0, 0)));
triStream.Append(VertexOutput(pos + float3(0, 1, 0)));最后,我们将输出顶点乘以tangentToLocal矩阵。正确地将它们与输入点的法线对齐。
triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0.5, 0, 0))));
triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(-0.5, 0, 0))));
triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 1, 0))));

接近了我们想要的结果,但是仍然不完全正确,这里的问题是,我们最初定义向上方向Up在Y轴上,然而,在切线空间中,规定的向上方向Up是沿着Z轴的,所以现在要做一些改变。
// 在输出前修改第三个顶点位置
triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 0, 1))));

3.草地样式

为了让我们的三角形看起来更像草叶,我们需要去加一些颜色信息和变化,我们将开始加一个梯度颜色来改变草叶从顶部到底端的颜色。
3.1 渐变颜色

我们的目标是让美术可以去定义两种颜色——顶端颜色和底部颜色——并用这两种颜色对草叶进行一个从上到下的颜色渐变,这两个颜色在shader中定义为_TopColor和_BottomColor。为了采样正确,我们需要提供片元着色器的UV坐标。
// 加在几何输出的结构体中
float2 uv : TEXCOORD0;



// 修改顶点输出函数
geometryOutput VertexOutput(float3 pos, float2 uv)



// 加在顶点输出函数中,放在o.pos下一行就行
o.uv = uv;



// 在GS中修改参数
triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0.5, 0, 0)), float2(0, 0)));
triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(-0.5, 0, 0)), float2(1, 0)));
triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 0, 1)), float2(0.5, 1)));为叶片构建一个三角形的UV。它带有两个基本的顶点,一个在左下一个在右下,然后一个尖端顶点位于中上方。



这个UV坐标由叶片的三个顶点构成,这种方式允许我们用简单的梯度进行叶片的上色

我们现在可以使用该UV在片元着色器下采样顶部和底部的颜色,然后用Lerp在它们直接进行插值计算。我们也需要修改片元着色器的参数,用来接收geometryOutput的值作为输入,而不仅仅是设立一个float4的位置坐标。
// 修改片元着色器函数的接口签名
float4 frag (geometryOutput i, fixed facing : VFACE) : SV_Target



// 用lerp得到的值代替返回值
// return float4(1, 1, 1, 1);

return lerp(_BottomColor, _TopColor, i.uv.y);

3.2 随机面朝方向

为了创建多样性并让草地看起来更加自然,我们下一步要让每一个草叶面朝一个随机方向,为了完成它,我们需要构建一个围绕着叶片Up轴旋转的随机矩阵。
shader中创建两个函数:rand和AngleAxis3x3
rand,可以从一个三维输入中生成一个随机值
AngleAxis3x3,它取一个角度(弧度值为单位)并返回一个围绕给定旋转轴的矩阵。
AngleAxis3x3的运作方式和C#函数中的四元数Quaternion.AngleAxis类似(但是AngleAxis3x3返回的是一个矩阵而不是四元数)
// 数学函数
// 根据一个顶点的三维坐标生成随机数
float rand(float3 seed)
{
  float f = sin(dot(seed, float3(127.1, 337.1, 256.2)));
  f = -1 + 2 * frac(f * 43785.5453123);
  return f;
}
// 取一个角度,返回一个围绕给定旋转轴的矩阵

float3x3 AngleAxis3x3(float angle, float3 axis)
{
  float s, c;
  sincos(angle, s, c);
  float x = axis.x;
  float y = axis.y;
  float z = axis.z;
  return float3x3(
    x * x + (y * y + z * z) * c, x * y * (1 - c) - z * s, x * z * (1 - c) - y * s,
    x * y * (1 - c) + z * s, y * y + (x * x + z * z) * c, y * z * (1 - c) - x * s,
    x * z * (1 - c) - y * s, y * z * (1 - c) + x * s, z * z + (x * x + y * y) * c
    );
}rand函数返回0到1之间的数,我们将把他乘以2π来得到角度值的整个范围
// 在声明tangentToLacl矩阵的行后添加
float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0, 0, 1));我们用输入的顶点位置信息pos作为旋转的随机种子。用这种方式,每个叶片都会得到一个不同的旋转值,但它将会在帧之间保持一致。
旋转可以与现有的tangentToLocal矩阵相乘然后运用在叶片上,注意矩阵的乘法应用是不可交换的,操作顺序很重要。
// 在facingRotationMatrix下添加,进行转换和随机旋转的矩阵乘法
float3x3 transformationMatrix = mul(tangentToLocal, facingRotationMatrix);



// 用transformationMatrix代替tangentToLocal
triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0.5, 0, 0)), float2(0, 0)));
triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(-0.5, 0, 0)), float2(1, 0)));
triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0, 0, 1)), float2(0.5, 1)));

3.3 随机向前的弯曲

如果所有的草叶都站的非常直,他们看起来就非常一致,这对于精心照料的草坪是可取的,但是对于野外的杂草来说,却是不准确的。我们将建立一个新的矩阵来沿着草叶的x轴进行旋转,以及一个属性来控制它的弯曲值。
// 添加一个新的参数
_BendRotationRandom("Bend Rotation Random", Range(0, 1)) = 0.2



// 放入CGINCLUDE中
float _BendRotationRandom;



// 加在GS中,放在facingRotationMatrix下面
float3x3 bendRotationMatrix = AngleAxis3x3(rand(pos.zzx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1, 0, 0));再次使用顶点位置作为随机种子,这次用它来创建一个独特的种子,我们需要乘以0.5π,这给了我们一个0到90°的角度。
将这个矩阵添加在transformationMatrix中,注意用正确的顺序。
mul(mul(切线到局部坐标,面随机朝向矩阵),随机向前弯曲矩阵)
float3x3 transformationMatrix = mul(mul(tangentToLocal, facingRotationMatrix), bendRotationMatrix);

3.4 宽度和高度

现在草叶的尺寸在1单位宽,1单位高,我们将添加一些属性来进行控制,以及一些属性来添加一些随机的变化
// 添加新的属性
_BladeWidth("Blade Width", Float) = 0.05
_BladeWidthRandom("Blade Width Random", Float) = 0.02
_BladeHeight("Blade Height", Float) = 0.5
_BladeHeightRandom("Blade Height Random", Float) = 0.3



// 声明
float _BladeHeight;
float _BladeHeightRandom;       
float _BladeWidth;
float _BladeWidthRandom;



// 添加到GS中,在triStream.Append前调用
float height = (rand(pos.zyx) * 2 - 1) * _BladeHeightRandom + _BladeHeight;
float width = (rand(pos.xzy) * 2 - 1) * _BladeWidthRandom + _BladeWidth;



// 用新的宽度和高度修改原来的位置
triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(width, 0, 0)), float2(0, 0)));
triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(-width, 0, 0)), float2(1, 0)));
triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0, 0, height)), float2(0.5, 1)));

现在这些三角形更像草叶了,但太稀疏了,在网格中没有足够的顶点来创建一个密集的草坪。
一个解决方案是创建一个新的、更密集的网格,要么使用C#,要么用3D建模的形式。虽然这都可以达到要求,但是都不能达到动态控制草地密度的目的,所以,我们采用细分曲面来增加网格的顶点数目。
4.细分曲面Tessellation

细分曲面是渲染管线中在顶点着色器之后,几何着色器之前的可选择管线。它的作用是将一个输入面细分为多个图元,细分曲面通过两个可编程的阶段实施:hull shader 和 domain shader
对于surface着色器,unity有一套内置的细分曲面进行实施。但是,我们这里没有使用surface着色器,所以这需要去实现自定义的hull和domain shader。这里将不会深入具体的细节。
CustomTessellation.cginc 文件。这个文件改编自Catlike Coding的一篇文章,这是一篇很好的参考文章。
如果我们在场景中启用了TessellationExample 对象,我们能看见它已经应用了一个细分曲面材质,修改Tessellation Uniform属性将演示细分效果。



通过修改Tessellation Uniform值来完成细分效果


我们将在我们的草地shader中加入细分曲面着色器,达到控制平面顶点密度进而控制草叶数量的目的。首先我们需要加入CustomTessellation.cginc这个内置文件。我们将引用它的相对路径在着色器中。
// 将该库引用
#include "Shaders/CustomTessellation.cginc"如果你打开了CustomTessellation.cginc,你将会注意到他已经定义了vertexInput和vertexOutput的结构体,以及一个顶点shader,这没有必要在我们原来的shader中重新定义,所以直接删掉




观察CustomTessellation.cginc的顶点shader,它直接简单地将输入传到细分曲面阶段,创建vertexOutput结构体的工作由tessVert函数负责,在domain shader中被调用。
// 加入新属性                       
_TessellationUniform("Tessellation Uniform", Range(1, 64)) = 1



// 在Subshader Pass下添加
#pragma hull hull
#pragma domain domain

在外属性面板中调节plane的细分曲面属性,增加顶点数。


5.风力效果

采用扭曲纹理贴图的形式来实现风的效果。这张贴图类似于法线贴图,但它只有R、G两个通道,我们用这两个通道来作为风向的X和Y方向。


在采样之前,我们需要构建一个UV坐标,我们将会使用输入点的位置,而不是分配给网格的纹理坐标。用这种方式,如果场景中有多个草网格,这会造成都是受同一个风力系统影响的错觉效果(风场?)。同样我们也会使用自带的shader变量_Time在草地表面滚动我们的风力贴图。
// 添加新的属性
_WindDistortionMap("Wind Distortion Map", 2D) = "white" {}
_WindFrequency("Wind Frequency", Vector) = (0.05, 0.05, 0, 0)



// 声明
sampler2D _WindDistortionMap;
float4 _WindDistortionMap_ST;

float2 _WindFrequency;



// 加在GS中,在transformationMatrix上方
float2 uv = pos.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y;应用_WindDistortionMap的缩放和偏移在顶点位置,接着通过_Time.y进一步偏移,用_WindFrequency进行缩放,现在使用这个UV去采样贴图,并创建一个新的属性来控制风强。
// 添加属性
_WindStrength("Wind Strength", Float) = 1



// 声明
float _WindStrength;



// 在构建的uv下采样
float2 windSample = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindStrength;注意我们现在将纹理的采样值从(0,1)映射到了(-1,1),然后我们需要构造了一个表示风向的归一化变量
// 定义一个风的旋转向量
float3x3 windRotation = AngleAxis3x3(UNITY_PI * windSample, wind);



// 重写transformationMatrix
float3x3 transformationMatrix = mul(mul(mul(tangentToLocal, windRotation), facingRotationMatrix), bendRotationMatrix);最后,在material上,加上Wind贴图,然后设置tiling值为(0.01,0.01)



会随风力贴图的风浪进行摆动,需要细分曲面增大、

现在的结果从远处看是正确的,但如果我们近距离观察草叶,会注意到整个叶片都在旋转,导致底部不再固定在plane上。





草叶的底部没有固定在地面上,并且还有的会漂浮在空中。

我们需要定义第二个变换矩阵来来纠正这个问题,让他只作用在两个底部的顶点。
// 在transformationMatrix下增加
float3x3 transformationMatrixFacing = mul(tangentToLocal, facingRotationMatrix);



// 修改输出的底顶点
triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(width, 0, 0)), float2(0, 0)));
triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(-width, 0, 0)), float2(1, 0)));6.草叶的曲率(前向弯曲)

现在,我们的草叶是一个简单的三角片面,在远处看其实没什么问题,但是靠近了就会看起来过于的刚性和几何性,这不是生动的草叶。我们将通过用几个三角形重新构建我们的叶片,并且让他们沿着一条曲线进行弯曲。

每一片草叶都被分成若干节,每个部分的形状都是由两个三角形组成的矩形(不包括尖端部分)——尖端部分是代表草叶上尖的三角形。

到目前为止,我们只输出了三个顶点,用来构建一个简单的三角形——那么如果输出更多的顶点呢,几何着色器是如何知道哪些顶点应该互相连接来形成三角形呢?这个答案就在三角形条带的数据结构中。和之前一样,前三个顶点被连接成一个三角形,每个附加的顶点与前两个三角形构成一个三角形。(1、2、3,构成三角形,添加一个节点4,4与2、3构成三角形,这样以此下去)




用三角形带来表示细分的草叶,一次构建一个顶点,在初始的三个顶点后,每个额外的顶点和前面两个顶点连接构成一个新的三角形。

这不仅提高了内存的效率,而且能很快速地用代码来构建三角形序列。如果我们希望有多个三角形带,我们能调用TriangleStream中的RestartStrip函数
在我们给几何着色器输出更多的顶点之前,我们需要增加maxvertexcount(最大顶点数目)的值,我们用#define语句来允许着色器去控制片段的数目,并计算出输出的顶点数目。
// 草叶段数,基础为3
#define BLADE_SEGMENTS 3



// 设置草叶的最大段数为7
[maxvertexcount(BLADE_SEGMENTS * 2 + 1)]我们最开始去定义我们的段数为3,接着在maxvertexcount中去重新计算基于初始段数的顶点数目,进行更新。

为了创造我们多个片段的草叶,我们需要利用for循环,每一次循环迭代都会增加两个顶点,一个左顶点,一个右顶点,在顶部的基顶点完成之后,我们将最后增加一个位于草叶顶端的顶点。


在我们操作前,我们需要移动计算顶点位置的代码到一个函数中,因为我们需要在循环内多次调用相同的代码段。将下面的函数添加
geometryOutput GenerateGrassVertex(float3 vertexPosition, float width, float height, float2 uv, float3x3 transformMatrix)
{
        float3 tangentPoint = float3(width, 0, height);

        float3 localPosition = vertexPosition + mul(transformMatrix, tangentPoint);
        return VertexOutput(localPosition, uv);
}这个函数与我们目前传递给VertexOutput以生成草叶顶点的参数有相同的责任。它通过获取当前顶点的位置,宽度和高度,根据正确的矩阵来进行顶点的转换,并且分配给它了一个UV坐标,我们将用这个函数来更新现有的代码并确保它能运作。
观察它的输出,就是调用VertexOutput函数。
// 更新现有的代码
triStream.Append(GenerateGrassVertex(pos, width, 0, float2(0, 0), transformationMatrixFacing));
triStream.Append(GenerateGrassVertex(pos, -width, 0, float2(1, 0), transformationMatrixFacing));
triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix));确保更新后能正常工作以后,我们需要把我们的顶点生成代码放入for循环中,在width声明下添加下列代码。
for (int i = 0; i < BLADE_SEGMENTS; i++)
{
        float t = i / (float)BLADE_SEGMENTS;
}这是一个循环,它表示了我们将运行草叶的段数次,在循环中,我们添加了一个变量t,这个变量将存储(0,1)的值。我们将用这个值来计算每次循环中每次迭代的段的宽度和高度。


// 加在循环中
float segmentHeight = height * t;
float segmentWidth = width * (1 - t);当我们向上移动草叶,高度增加,宽度减少(逐渐向内),我们现在在循环中加入GenerateGrassVertex函数进行调用。我们也会做一个简单的GenerateGrassVertex函数调用在循环外,为了在草叶的尖端添加最后一个顶点。
// 在循环体中加入
float3x3 transformMatrix = i == 0 ? transformationMatrixFacing : transformationMatrix;

triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, float2(0, t), transformMatrix));
triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, float2(1, t), transformMatrix));



// 添加在循环后,插入最后一个尖端的顶点
triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix));



// 移除
// triStream.Append(GenerateGrassVertex(pos, width, 0, float2(0, 0), transformationMatrixFacing));
// triStream.Append(GenerateGrassVertex(pos, -width, 0, float2(1, 0), transformationMatrixFacing));
// triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix));注意我们使用了一个判断语句,判断如果i==0的时候,是最下面的两个顶点,这两个顶点要使用transformationMatrixFacing矩阵,这是为了避免它发生错误的偏移。其他的五个顶点都直接用transformationMatrix。


现在草叶被细分成了多个片段,但是它的表面还是平的——因为新添加的三角形还没有被使用。我们将通过偏移顶点的Y轴的值给叶片添加一些曲率。首先,需要修改我们的GenerateGrassVertex函数,给他一个Y轴的偏移值forward。
// 更新函数,添加了一个forward偏移值
geometryOutput GenerateGrassVertex(float3 vertexPosition, float width, float height, float forward, float2 uv, float3x3 transformMatrix)



// 用forward的值重写顶点Y轴的值
float3 tangentPoint = float3(width, forward, height);为了计算每个顶点forward的偏移值,我们需要利用t,将t带入pow函数,通过取t的次幂来影响前向偏移值,由于是用pow函数进行计算,所以他的偏移影响是非线性的,这能达到我们将草叶塑造成曲线的目的。
// 添加新的属性
_BladeForward("Blade Forward Amount", Float) = 0.38
_BladeCurve("Blade Curvature Amount", Range(1, 4)) = 2



// 声明
float _BladeForward;
float _BladeCurve;



// 添加在GS中,设置宽度的下方
// forward是一个随机数(0,1)*_BladeForward
float forward = rand(pos.yyz) * _BladeForward;



// 添加在循环体中,segmentWidth下方
// 用pow值来求顶点的Y轴方向的偏移值,用pow是为了做出草叶向前的曲线效果,非线性
float segmentForward = pow(t, _BladeCurve) * forward;



// 修改传入参数 加上了segmentForward
triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix));
triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix));



// 修改循环外 尖端顶点的传入参数
triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix));通过参数_BladeForward和_BladeCurve的调节可以进行前向偏移值的大小和弯曲的曲率。


7. 光照和阴影

终于到了grass shader的最后一步了。我们将加入草叶投射和接受阴影的能力,最后加入一个主光源
7.1 投射阴影

为了能在Untiy引擎中投射阴影,我们需要添加第二个Pass,这个Pass将被用作场景中的阴影投射灯作用,渲染出草地的深度信息到shadow map中,这意味着我们的GS也需要在阴影Pass中运行,以确保草叶来进行阴影的投射。

因为我们的GS是写在CGINCLUDE块中,我们就有能力在任意的Pass中进行调用,我们将创建第二个Pass,这个Pass可以利用我们所拥有的所有着色器,除了片元着色器——我们将定义一个新的,它由宏进行填充并为我们进行输出。
// 第二个Pass
Pass
{
        Tags
        {
                "LightMode" = "ShadowCaster"
        }

        CGPROGRAM
        #pragma vertex vert
        #pragma geometry geo
        #pragma fragment frag
        #pragma hull hull
        #pragma domain domain
        #pragma target 4.6
        #pragma multi_compile_shadowcaster

        float4 frag(geometryOutput i) : SV_Target
        {
                SHADOW_CASTER_FRAGMENT(i)
        }

        ENDCG
}除了有一个新的片元着色器,我们将这个Pass的标签设为了ShadowCaster,而不是ForwardBase——这向Unity传达了我们需要利用这个Pass来将对象渲染成阴影贴图,我们还要设置预处理命令multi_compile_shadowcaster,这确保了着色器为阴影投射编译时的所有所需的变体。

将Fence对象拉进Scence中,用来接收草叶投射的阴影。


7.2 接收阴影

在Untiy从阴影投射光中渲染了一张阴影贴图后,他将运行一个Pass"collecting"阴影在屏幕空间纹理。为了采样这个纹理,我们需要计算屏幕空间中顶点的位置,将他们传入片元着色器中。
// 加在几何着色器的结构体中
// 注意这里的 _ShadowCoord为float4类型
// #define unityShadowCoord4 float4
unityShadowCoord4 _ShadowCoord : TEXCOORD1;



// 加在VertexOutput函数中,最后的返回值上方
o._ShadowCoord = ComputeScreenPos(o.pos);在ForwardBase Pass的片元着色器里,我们用一个宏来检索一个浮点值float,用来表示是否这个表面处于阴影中,这个值的范围为【0,1】,0为完全在阴影处,1为完全照明
为什么将屏幕空间的UV坐标称为_ShadowCoord?这不符合以往的惯例
Shader入门精要P200-203
// 在ForwardBase Pass的片元着色器中,获取返回值
return SHADOW_ATTENUATION(i);

//return lerp(_BottomColor, _TopColor, i.uv.y);最后,我们需要确保我们的shader能正确配置以接受阴影,为了做到这点,我们将添加一个预处理器在ForwardBase的Pass中,用来编译所有必要的着色器变体
#pragma multi_compile_fwdbase



放大以后,我们可以看到草叶上有错误的阴影;这是由于他们自身的投射阴影造成的自遮挡现象,我们可以通过应用线性的偏移值来进行纠正,将顶点在裁剪空间的位置添加一个小的偏移值来远离屏幕(Z深度偏移),可以使用一个Untiy的宏命令,并将其包含在#if语句中,以确保操作只在阴影信息的传递中运行。
// 添加到VertexOutput函数的末尾,就在返回调用的上方。
#if UNITY_PASS_SHADOWCASTER
        // 应用偏移值可以防止阴影出现在表面上。
        o.pos = UnityApplyLinearShadowBias(o.pos);
#endif


在应用linear shadow偏移之后,效果得到了改善

为什么会发生这种情况呢?
自遮挡关系,可看Games202 PCSS,总而言之就是精确度不够
7.3 光照

我们将用兰伯特光照模型来进行草叶的光照计算


现在,我们草叶上的顶点没有分配法线信息。就像我们对顶点的位置所做的一样,我们需要先计算在切线空间的法线,然后将它转到模型空间。

当Blade Curvature Amount设置为1的时候,所有的草叶在切线空间中都面朝同一个方向:指向Y轴的反方向,我们解决的第一步,就是计算没有曲率时的法线方向。
// GenerateGrassVertex函数中添加
float3 tangentNormal = float3(0, -1, 0);
float3 localNormal = mul(transformMatrix, tangentNormal);给tangentNormal直接定义了Y轴的负方向,用转换切线空间点到对象空间点一样的矩阵进行法线的转换。可以通过它传递给VertexOutput函数,接着传到geometryOutput结构体中。
// 修改VertexOutput的返回值
return VertexOutput(localPosition, uv, localNormal);



// 在几何输出的结构体中添加normal信息
float3 normal : NORMAL;



// 添加输入参数
geometryOutput VertexOutput(float3 pos, float2 uv, float3 normal)



// 通过VertexOutput函数将法线输出到片元着色器中
o.normal = UnityObjectToWorldNormal(normal);注意我们在输出前需要把法线从对象空间转换到世界空间,这样便于进行兰伯特光照计算。

我们现在可以在ForwardBase Pass中的片元着色器中进行可视化操作。
// 添加在片元着色器中
float3 normal = facing > 0 ? i.normal : -i.normal;

return float4(normal * 0.5 + 0.5, 1);

// 先移除
// return SHADOW_ATTENUATION(i);

当Blade Curvature Amount的值大于1的时候,每个顶点都将会有他自己在切线空间Z轴的偏移值,因为forward偏移输入了GenerateGrassVertex函数,我们将用这个偏移值来进行Z轴法线的缩放。
float3 tangentNormal = normalize(float3(0, -1, forward));最后,我们在片元着色器中整合一下代码。添加阴影,方向光和环境光。
// Add to the ForwardBase fragment shader, below the line declaring float3 normal.
float shadow = SHADOW_ATTENUATION(i);
float NdotL = saturate(saturate(dot(normal, _WorldSpaceLightPos0)) + _TranslucentGain) * shadow;

float3 ambient = ShadeSH9(float4(normal, 1));
float4 lightIntensity = NdotL * _LightColor0 + float4(ambient, 1);
float4 col = lerp(_BottomColor, _TopColor * lightIntensity, i.uv.y);

return col;

// 移除
// return float4(normal * 0.5 + 0.5, 1);

Code

Shader "Unlit/Geometryshader"
{
    Properties
    {
        _MainTex                ("Texture", 2D) = "white" {}
        _TopColor               ("草叶顶部颜色",Color) = (1,1,1,1)
        _BottomColor            ("草叶底部颜色",Color) = (1,1,1,1)
        _BendRotationRandom     ("草叶随机的弯曲值", Range(0, 1)) = 0.2
        _BladeWidth             ("草叶的宽度", Float) = 0.05
        _BladeWidthRandom       ("草叶宽度,随机值", Float) = 0.02
        _BladeHeight            ("草叶的高度", Float) = 0.5
        _BladeHeightRandom      ("草叶高度,随机值", Float) = 0.3
        _TessellationUniform    ("细分曲面强度", Range(1, 64)) = 1
        _WindDistortionMap      ("风的方向贴图", 2D) = "white" {}
        _WindFrequency          ("风频率", Vector) = (0.05, 0.05, 0, 0)
        _WindStrength           ("风力强度", Float) = 1
        _BladeForward           ("草叶的前向偏移数值(影响取得的随机数大小)", Float) = 0.38
        _BladeCurve             ("草叶的曲率大小", Range(1, 4)) = 2
        _TranslucentGain        ("_TranslucentGain",Range(0, 1)) = 0
    }
    CGINCLUDE

    //  定义叶片的基础顶点数
    #define BLADE_SEGMENTS 3
    #define unityShadowCoord4 float4

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    #include "AutoLight.cginc"
    #include "cginc/CustomTessellation.cginc"

    sampler2D _MainTex;
    float4 _MainTex_ST;
    float4 _TopColor;
    float4 _BottomColor;
    float _BendRotationRandom;
    float _BladeHeight;
    float _BladeHeightRandom;       
    float _BladeWidth;
    float _BladeWidthRandom;
    sampler2D _WindDistortionMap;
    float4 _WindDistortionMap_ST;
    float2 _WindFrequency;
    float _WindStrength;
    float _BladeForward;
    float _BladeCurve;
    float _TranslucentGain;

    // 数学函数
    // 根据一个顶点的三维坐标生成随机数
    float rand(float3 seed)
    {
        float f = sin(dot(seed, float3(127.1, 337.1, 256.2)));
        f = -1 + 2 * frac(f * 43785.5453123);
        return f;
    }
   
    // 取一个角度,返回一个围绕给定旋转轴的矩阵
    float3x3 AngleAxis3x3(float angle, float3 axis)
    {
        float s, c;
        sincos(angle, s, c);
        float x = axis.x;
        float y = axis.y;
        float z = axis.z;
        return float3x3(
            x * x + (y * y + z * z) * c, x * y * (1 - c) - z * s, x * z * (1 - c) - y * s,
            x * y * (1 - c) + z * s, y * y + (x * x + z * z) * c, y * z * (1 - c) - x * s,
            x * z * (1 - c) - y * s, y * z * (1 - c) + x * s, z * z + (x * x + y * y) * c
        );
    }

    struct g2f
    {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
        float3 normal : NORMAL;
        unityShadowCoord4 _ShadowCoord : TEXCOORD1;
    };

    //用于GS中的函数
    g2f VertexOutput(float3 pos, float2 uv, float3 normal)
    {
        g2f o;
            o.pos = UnityObjectToClipPos(pos);
            o.normal = UnityObjectToWorldNormal(normal);
            o.uv = uv;
            o._ShadowCoord = ComputeScreenPos(o.pos);
            //处理自遮挡关系
            #if UNITY_PASS_SHADOWCASTER
            o.pos = UnityApplyLinearShadowBias(o.pos);
            #endif
        return o;
    }
    g2f GenerateGrassVertex(float3 vertexPosition, float width, float height, float forward, float2 uv, float3x3 transformMatrix)
    {
        float3 tangentPoint = float3(width, forward, height);

        // 曲率为1时,切线空间的法线方向
        // float3 tangentNormal = float3(0,-1,0);
        float3 tangentNormal = normalize(float3(0,-1,forward));
        float3 localNormal = mul(transformMatrix, tangentNormal);

        float3 localPosition = vertexPosition + mul(transformMatrix, tangentPoint);
        return VertexOutput(localPosition, uv, localNormal);
    }


    //单个调用最大顶点数
    [maxvertexcount(BLADE_SEGMENTS * 2 + 1)]
    //以一个三角形为单位进行输入(每次同时输入三个顶点)
    //图元输入:point line lineadj triangle triangleadj
    //以线的形式进行输出
    //图元输出:LineStream PointStream TriangleStream
    void geo(triangle vertexOutput IN[3], inout TriangleStream<g2f> triStream)
    {
        //g2f o;
        float3 pos = IN[0].vertex;

        float3 vNormal = IN[0].normal;
        float4 vTangent = IN[0].tangent;
        float3 vBinormal = cross(vNormal, vTangent) * vTangent.w;

        // 切线空间转换到模型空间 草叶的生长朝向
        float3x3 tangentToLocal = float3x3(
        vTangent.x, vBinormal.x, vNormal.x,
        vTangent.y, vBinormal.y, vNormal.y,
        vTangent.z, vBinormal.z, vNormal.z
        );
        // 构建随机的旋转矩阵,为了实现不同叶片的面朝方向
        float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0,0,1));
        // 实现叶片不同角度的向前弯曲值
        float3x3 bendRotationMatrix = AngleAxis3x3(rand(pos.zzx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1, 0, 0));
   
        // 构建UV坐标,根据顶点位置构建 用作风力图
        float2 uv = pos.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y;
        float2 windSample = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindStrength;
        float3 wind = normalize(float3(windSample.x, windSample.y, 0));
        float3x3 windRotation = AngleAxis3x3(UNITY_PI * windSample, wind);       //影响草叶朝风向旋转的矩阵

        float3x3 transformationMatrix = mul(mul(mul(tangentToLocal, facingRotationMatrix),bendRotationMatrix), windRotation);
        float3x3 transformationMatrixFacing = mul(tangentToLocal, facingRotationMatrix);

        // 设置叶片新的随机高度和宽度
        float height = (rand(pos.zyx)* 2 - 1)* _BladeHeightRandom + _BladeHeight;
        float width = (rand(pos.xzy) * 2 - 1)* _BladeWidthRandom + _BladeWidth;
        float forward = rand(pos.yyz) * _BladeForward;   //前向的随机偏移值

        // 用循环来构建多个片段组成的草叶
        for (int i = 0; i < BLADE_SEGMENTS; i++)
        {
            float t = i / (float)BLADE_SEGMENTS;

            float segmentHeight = height * t;
            float segmentWidth = width * (1 - t);

            // 用pow值来求顶点的Y轴方向的偏移值,用pow是为了做出草叶向前的曲线效果,非线性
            // forward是一个随机数(0,1)*_BladeForward
            float segmentForward = pow(t, _BladeCurve) * forward;

            //判断是否是基顶点,避免基顶点发生错误的偏移
            float3x3 transformMatrix = i == 0 ? transformationMatrixFacing : transformationMatrix;

            triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix));
            triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix));
        }
        //添加最后一个顶点,位于草叶的尖端
        triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix));
    }

    ENDCG

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Cull off

        Pass
        {
            Tags
            {
                "LightMode" = "ForwardBase"
            }
            CGPROGRAM

            #pragma vertex vert
            #pragma hull hull
            #pragma domain domain
            #pragma geometry geo
            #pragma fragment frag
            #pragma multi_compile_fwdbase

            float4 frag (g2f i,fixed facing : VFACE) : SV_Target
            {
                float3 normal = facing > 0 ? i.normal : -i.normal;
               
                float shadow = SHADOW_ATTENUATION(i);
                float NdotL = saturate(saturate(dot(normal, _WorldSpaceLightPos0)) + _TranslucentGain)*shadow;

                float3 ambient = ShadeSH9(float4(normal,1));
                float4 lightIntensity = NdotL * _LightColor0 + float4(ambient,1);
                float4 col =lerp(_BottomColor,_TopColor * lightIntensity, i.uv.y);

                return col;
            }
            ENDCG
        }
        Pass
        {
            Tags
            {
                "LightMode" = "ShadowCaster"
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geo
            #pragma fragment frag
            #pragma hull hull
            #pragma domain domain
            #pragma target 4.6
            #pragma multi_compile_shadowcaster

            float4 frag(g2f i) : SV_Target
            {
                SHADOW_CASTER_FRAGMENT(i)
            }

            ENDCG
        }
    }
}
Tessellation.cginc
struct vertexInput
{
        float4 vertex : POSITION;
        float3 normal : NORMAL;
        float4 tangent : TANGENT;
};

struct vertexOutput
{
        float4 vertex : SV_POSITION;
        float3 normal : NORMAL;
        float4 tangent : TANGENT;
};

struct TessellationFactors
{
        float edge[3] : SV_TessFactor;
        float inside : SV_InsideTessFactor;
};

vertexInput vert(vertexInput v)
{
        return v;
}

vertexOutput tessVert(vertexInput v)
{
        vertexOutput o;
        o.vertex = v.vertex;
        o.normal = v.normal;
        o.tangent = v.tangent;
        return o;
}

float _TessellationUniform;

TessellationFactors patchConstantFunction (InputPatch<vertexInput, 3> patch)
{
        TessellationFactors f;
        f.edge[0] = _TessellationUniform;
        f.edge[1] = _TessellationUniform;
        f.edge[2] = _TessellationUniform;
        f.inside = _TessellationUniform;
        return f;
}

[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("integer")]
[UNITY_patchconstantfunc("patchConstantFunction")]
vertexInput hull (InputPatch<vertexInput, 3> patch, uint id : SV_OutputControlPointID)
{
        return patch[id];
}

[UNITY_domain("tri")]
vertexOutput domain(TessellationFactors factors, OutputPatch<vertexInput, 3> patch, float3 barycentricCoordinates : SV_DomainLocation)
{
        vertexInput v;

        #define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) v.fieldName = \
                patch[0].fieldName * barycentricCoordinates.x + \
                patch[1].fieldName * barycentricCoordinates.y + \
                patch[2].fieldName * barycentricCoordinates.z;

        MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
        MY_DOMAIN_PROGRAM_INTERPOLATE(normal)
        MY_DOMAIN_PROGRAM_INTERPOLATE(tangent)

        return tessVert(v);
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-10 10:25 , Processed in 0.104057 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表