|
上半年自学的时候翻译的文章,后面得知手机端无法使用就没深入研究.整个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(&#34;Bend Rotation Random&#34;, 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(&#34;Blade Width&#34;, Float) = 0.05
_BladeWidthRandom(&#34;Blade Width Random&#34;, Float) = 0.02
_BladeHeight(&#34;Blade Height&#34;, Float) = 0.5
_BladeHeightRandom(&#34;Blade Height Random&#34;, 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 &#34;Shaders/CustomTessellation.cginc&#34;如果你打开了CustomTessellation.cginc,你将会注意到他已经定义了vertexInput和vertexOutput的结构体,以及一个顶点shader,这没有必要在我们原来的shader中重新定义,所以直接删掉
观察CustomTessellation.cginc的顶点shader,它直接简单地将输入传到细分曲面阶段,创建vertexOutput结构体的工作由tessVert函数负责,在domain shader中被调用。
// 加入新属性
_TessellationUniform(&#34;Tessellation Uniform&#34;, Range(1, 64)) = 1
…
// 在Subshader Pass下添加
#pragma hull hull
#pragma domain domain
在外属性面板中调节plane的细分曲面属性,增加顶点数。
5.风力效果
采用扭曲纹理贴图的形式来实现风的效果。这张贴图类似于法线贴图,但它只有R、G两个通道,我们用这两个通道来作为风向的X和Y方向。
在采样之前,我们需要构建一个UV坐标,我们将会使用输入点的位置,而不是分配给网格的纹理坐标。用这种方式,如果场景中有多个草网格,这会造成都是受同一个风力系统影响的错觉效果(风场?)。同样我们也会使用自带的shader变量_Time在草地表面滚动我们的风力贴图。
// 添加新的属性
_WindDistortionMap(&#34;Wind Distortion Map&#34;, 2D) = &#34;white&#34; {}
_WindFrequency(&#34;Wind Frequency&#34;, 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(&#34;Wind Strength&#34;, 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(&#34;Blade Forward Amount&#34;, Float) = 0.38
_BladeCurve(&#34;Blade Curvature Amount&#34;, 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
{
&#34;LightMode&#34; = &#34;ShadowCaster&#34;
}
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&#34;collecting&#34;阴影在屏幕空间纹理。为了采样这个纹理,我们需要计算屏幕空间中顶点的位置,将他们传入片元着色器中。
// 加在几何着色器的结构体中
// 注意这里的 _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 &#34;Unlit/Geometryshader&#34;
{
Properties
{
_MainTex (&#34;Texture&#34;, 2D) = &#34;white&#34; {}
_TopColor (&#34;草叶顶部颜色&#34;,Color) = (1,1,1,1)
_BottomColor (&#34;草叶底部颜色&#34;,Color) = (1,1,1,1)
_BendRotationRandom (&#34;草叶随机的弯曲值&#34;, Range(0, 1)) = 0.2
_BladeWidth (&#34;草叶的宽度&#34;, Float) = 0.05
_BladeWidthRandom (&#34;草叶宽度,随机值&#34;, Float) = 0.02
_BladeHeight (&#34;草叶的高度&#34;, Float) = 0.5
_BladeHeightRandom (&#34;草叶高度,随机值&#34;, Float) = 0.3
_TessellationUniform (&#34;细分曲面强度&#34;, Range(1, 64)) = 1
_WindDistortionMap (&#34;风的方向贴图&#34;, 2D) = &#34;white&#34; {}
_WindFrequency (&#34;风频率&#34;, Vector) = (0.05, 0.05, 0, 0)
_WindStrength (&#34;风力强度&#34;, Float) = 1
_BladeForward (&#34;草叶的前向偏移数值(影响取得的随机数大小)&#34;, Float) = 0.38
_BladeCurve (&#34;草叶的曲率大小&#34;, Range(1, 4)) = 2
_TranslucentGain (&#34;_TranslucentGain&#34;,Range(0, 1)) = 0
}
CGINCLUDE
// 定义叶片的基础顶点数
#define BLADE_SEGMENTS 3
#define unityShadowCoord4 float4
#include &#34;UnityCG.cginc&#34;
#include &#34;Lighting.cginc&#34;
#include &#34;AutoLight.cginc&#34;
#include &#34;cginc/CustomTessellation.cginc&#34;
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 { &#34;RenderType&#34;=&#34;Opaque&#34; }
LOD 100
Cull off
Pass
{
Tags
{
&#34;LightMode&#34; = &#34;ForwardBase&#34;
}
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
{
&#34;LightMode&#34; = &#34;ShadowCaster&#34;
}
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(&#34;tri&#34;)]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology(&#34;triangle_cw&#34;)]
[UNITY_partitioning(&#34;integer&#34;)]
[UNITY_patchconstantfunc(&#34;patchConstantFunction&#34;)]
vertexInput hull (InputPatch<vertexInput, 3> patch, uint id : SV_OutputControlPointID)
{
return patch[id];
}
[UNITY_domain(&#34;tri&#34;)]
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);
} |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|