本文参考自该文章 实现思路:
1、几何着色器实现草 (三角形),原理是将几何着色器的三角片元输入重新构造成草的样式
2、让草可以在各种地形朝上生长(切线空间)
3、曲面细分可以动态改变模型定点数,进而改变草的密度
4、扰动图实现风吹草低的效果
5、使用Unity标准着色器自带的阴影计算实现“草的阴影投射和阴影接收”
几个要点:
1、为了让草坪能在各种地形因素下朝正确的方向长出来,我们需要用切线空间下进行计算
2、增加随机变量控制草的:高度、宽度、朝向等,让草坪整体看起来更真实
3、在利用几何着色器创建草的时候,为了实现草的弯曲效果,我们需要将草设计成多节(后面会看到)
1、几何着色器实现草 (三角形)
[maxvertexcount(3)]
void geom(triangle t2g input[3], inout TriangleStream<g2f> outStream) {
g2f o1;
g2f o2;
g2f o3;
//着重理解一下 为什么要同一个input 以及 float3(0.5, 0, 0) 是怎么设置顶点的 ,比如最后一个float3(0, 1, 0)改变1可以改变高度
o1.pos = UnityObjectToClipPos(input[0].pos + float3(0.5, 0, 0));
outStream.Append(o1);
o2.pos = UnityObjectToClipPos(input[0].pos + float3(-0.5, 0, 0));
outStream.Append(o2);
o3.pos = UnityObjectToClipPos(input[0].pos + float3(0, 1, 0));
outStream.Append(o3);
}
1、上述代码中 input可以是三角片面任意一个点,分别对应如下图所示,就会从对应的顶点上长出 “草”
分别对应如下三图
2、上述代码中 float3(0.5, 0, 0)、float3(-0.5, 0, 0)、float3(0, 1, 0) 其实就是相对于input[i]的相对位置,input[i]为原点
下面三图分别对应
① float3(0.5, 0, 0)、float3(-0.5, 0, 0)、float3(0, 1, 0)
② float3(0.25, 0, 0)、float3(-0.25, 0, 0)、float3(0, 1, 0)
③ float3(0.25, 0, 0)、float3(-0.25, 0, 0)、float3(0, 2, 0)
2、让草适应任意地形
引入:之前在UnityShader法线纹理的应用(在切线空间下计算)这篇文章中有描述过切线空间可以让模型产生凹凸现象,同理,这里也是要让草在不同的凹凸地形中都可以正确生长,因此我们使用切线空间计算来使 草能适应任意地形
我们这里以一颗球为代表测试草是否生长正确,因为球每个三角面的朝向都是不一样的
//计算切线空间三轴
float3 normal = input[ALIGN_POINT].normal;
float4 tangent = input[ALIGN_POINT].tangent;
float3 binormal = cross(normal, tangent) * tangent.w;
//切线空间矩阵
float3x3 obect2TangentMatrix = float3x3(
tangent.x, binormal.x, normal.x,
tangent.y, binormal.y, normal.y,
tangent.z, binormal.z, normal.z
);
//这里我们定义一个叫做转化矩阵,后面会对这个转化矩阵不断扩充,实现不同的效果
float3x3 transformationMatrix = obect2TangentMatrix;
//将顶点全部转移到切线空间下计算
o1.pos = UnityObjectToClipPos(input[0].pos + mul(transformationMatrix, float3(0.5, 0, 0)));
outStream.Append(o1);
o2.pos = UnityObjectToClipPos(input[0].pos + mul(transformationMatrix, float3(-0.5, 0, 0)));
outStream.Append(o2);
//特别注意!
o3.pos = UnityObjectToClipPos(input[0].pos + mul(transformationMatrix, float3(0, 0, 1)));
outStream.Append(o3);
特别注意! 在最后一个顶点我们不再使用(0,1,0)作为草末端,而是使用(0,0,1),因为在切线空间里 朝上的方向为Z轴方向!
从上面可以看出,从一个球体上长出的“草”都是垂直顶点长出来的!
3、给草上色
Properties
{
_TopColor("Top Color", Color) = (1,1,1,1)
_BottomColor("Bottom Color", Color) = (1,1,1,1)
}
[maxvertexcount(3)]
void geom(triangle t2g input[3], inout TriangleStream<g2f> outStream)
{
.....
o1.pos = UnityObjectToClipPos(input[0].pos + mul(transformationMatrix, float3(0.5, 0, 0)));
o1.uv = float2(0, 0);
outStream.Append(o1);
o2.pos = UnityObjectToClipPos(input[0].pos + mul(transformationMatrix, float3(-0.5, 0, 0)));
o2.uv = float2(1, 0);
outStream.Append(o2);
o3.pos = UnityObjectToClipPos(input[0].pos+ mul(transformationMatrix, float3(0, 0, 1)));
o3.uv = float2(0.5, 1);
outStream.Append(o3);
}
float4 frag(g2f i) : SV_Target
{
return lerp(_BottomColor, _TopColor, i.uv.y);
}
uv是如何设定的?看下面的图就知道
然后我们在片元着色器中利用插值得到如下图所示的草色
此时我们的草就有颜色了!
4、增加草的随机性,使草变得真实
包括以下几个方面:草宽、草高、草的转向
①宽、高随机,随机的方式你可以自己定,这里是直接根据位置随机
Properties
{
_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
}
[maxvertexcount(3)]
void geom(triangle t2g input[3], inout TriangleStream<g2f> outStream)
{
.....
float height = (rand(pos) * 2 - 1) * _BladeHeightRandom + _BladeHeight;
float width = (rand(pos) * 2 - 1) * _BladeWidthRandom + _BladeWidth;
o1.pos = UnityObjectToClipPos(input[0].pos + mul(transformationMatrix, float3(width, 0, 0)));
.....
o2.pos = UnityObjectToClipPos(input[0].pos + mul(transformationMatrix, float3(-width, 0, 0)));
.....
o3.pos = UnityObjectToClipPos(input[0].pos+ mul(transformationMatrix, float3(0, 0, height)));
.....
}
② 朝向随机
float3x3 AngleAxis3x3(float angle, float3 axis)
{
float c, s;
sincos(angle, s, c);
float t = 1 - c;
float x = axis.x;
float y = axis.y;
float z = axis.z;
return float3x3(
t * x * x + c, t * x * y - s * z, t * x * z + s * y,
t * x * y + s * z, t * y * y + c, t * y * z - s * x,
t * x * z - s * y, t * y * z + s * x, t * z * z + c
);
}
[maxvertexcount(3)]
void geom(triangle t2g input[3], inout TriangleStream<g2f> outStream)
{
.....
float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0, 0, 1));
float3x3 transformationMatrix = mul(obect2TangentMatrix, facingRotationMatrix);
.....
}
其实只是替换了 transformationMatrix 这个矩阵,下图的转向不是很明显,红框那些就是转向90,所以看到的只有一条缝
**
5、给草增加密度,现在太稀疏了不好看!
核心思想: 我们草的生成是利用平面自带的三角片元的顶点,理论上来说,我们只要换一个三角片元较为密集的平面就能让草变得密集,但是这样不够灵活,因此我们使用曲面细分的方式来增加网格密度。
之前的雪地效果就是使用的曲面细分,也就是这篇文章 曲面细分的使用方式和上面这篇文章大同小异,下面直接给出代码
//在 (minDist,maxDist)区间内细分会不断变化
float4 tessDistance(appdata v0, appdata v1, appdata v2)
{
float minDist = MIN_DISTANCE; //细分最小距离,小于细分不在增加
float maxDist = MAX_DISTANCE; //细分最远距离,超出不在细分
//这个函数计算每个顶点到相机的距离,得出最终的tessellation 因子。
return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Density);
}
//曲面细分相关
UnityTessellationFactors hsconst_surf(InputPatch<a2t, 3> v)
{
UnityTessellationFactors o;
float4 tf;
appdata vi[3];
vi[0].vertex = v[0].pos;
vi[0].tangent = v[0].tangent;
vi[0].normal = v[0].normal;
vi[1].vertex = v[1].pos;
vi[1].tangent = v[1].tangent;
vi[1].normal = v[1].normal;
vi[2].vertex = v[2].pos;
vi[2].tangent = v[2].tangent;
vi[2].normal = v[2].normal;
tf = tessDistance(vi[0], vi[1], vi[2]);
o.edge[0] = tf.x; o.edge[1] = tf.y; o.edge[2] = tf.z; o.inside = tf.w;
return o;
}
//指明输入进hull shader的图元是三角形。
[UNITY_domain("tri")]
//决定舍入规则,fractional_odd意为factor截断在[1,max]范围内,然后取整到小于此数的最大奇数整数值。
[UNITY_partitioning("fractional_odd")]
//决定图元的朝向,由组成三角形的三个顶点的顺序所产生的方向决定,cw为clockwise顺时针,ccw为counter clockwise逆时针。
[UNITY_outputtopology("triangle_cw")]
//指明计算factor的方法
[UNITY_patchconstantfunc("hsconst_surf")]
//hull shader输出的outputpatch中的顶点数量。
[UNITY_outputcontrolpoints(3)]
//给出控制点在path中的ID,与outputcontrolpoints对应,例如outputcontrolpoints为4,那么i的取值就是[0,4)的整数。
a2t hs_surf(InputPatch<a2t, 3> v, uint id : SV_OutputControlPointID) {
return v[id];
}
[UNITY_domain("tri")]
t2g ds_surf(UnityTessellationFactors tessFactors, const OutputPatch<a2t, 3> vi, float3 bary : SV_DomainLocation) {
appdata v;
UNITY_INITIALIZE_OUTPUT(appdata, v);
v.vertex = vi[0].pos * bary.x + vi[1].pos * bary.y + vi[2].pos * bary.z;
v.tangent = vi[0].tangent * bary.x + vi[1].tangent * bary.y + vi[2].tangent * bary.z;
v.normal = vi[0].normal * bary.x + vi[1].normal * bary.y + vi[2].normal * bary.z;
t2g o;
o.pos = v.vertex;
o.tangent = v.tangent;
o.normal = v.normal;
return o;
}
是不是开始有点那味了!
曲面细分这次再次使用的时候发现有些疑点,就去查了下官方文档,在前面两行有说到一个
Unity曲面细分官方文档 说vertex:FunctionName是在曲面细分之后调用,然后做了如下的猜测和总结
// 曲面细分的感觉是:原本没有曲面细分,我们都是在顶点着色器中处理顶点数据
// 现在有曲面细分,顶点着色器只充当一个初始数据传输的作用
// 原顶点着色器中的数据处理转移到曲面细分生成顶点之后进行处理
// 所以才有Unity官方文档中说得:vert是在曲面细分之后
// 实际上应该是说 顶点内的数据处理要在曲面细分之后
6、给草上风!动起来!
核心思想: 首先就是风吹草低!,所以草浪就是草弯腰了!所以我们只要能让草弯腰,在利用一张扰动图,让这张扰动图循环播放,实现风一阵一阵的感觉。
Properties
{
// 风扰动图
_WindDistortionMap("Wind Distortion Map", 2D) = "white" {}
// 风频
_WindFrequency("Wind Frequency", Vector) = (0.05, 0.05, 0, 0)
// 风强
_WindStrength("Wind Strength", Float) = 1
}
[maxvertexcount(3)]
void geom(triangle t2g input[3], inout TriangleStream<g2f> outStream)
{
.....
// 风扰动
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 bendRotationMatrix = AngleAxis3x3(rand(pos.zzx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1, 0, 0));
float3x3 transformationMatrix = mul(mul(mul(obect2TangentMatrix, windRotation), facingRotationMatrix), bendRotationMatrix);
.....
}
这感觉对了!非常棒!
但还不对劲!草怎么就一节!看上去太僵硬!太丑了!给他多几节!让草本身带有一定的曲率
7、给草多长几节,让草本身变弯
草变弯的原理图如下,总结为以下几点:
1、给草增加顶点(越往上层,每节草宽度按照一定比例缩小,这样就能保证是一个三角形)
2、让每一层(2个点)产生一定的水平偏移,展示出来的效果就是草 “弯了”
1、下面优化一下代码结构,之前在添加三个顶点的时候,每个顶点的添加其实是重复的,提炼一下,变成下面这个函数 GenerateGrassVertex,并且让每棵草的定点数增加
g2f GenerateGrassVertex(float3 vertexPosition, float width, float height, float2 uv, float3x3 transformMatrix)
{
g2f o;
float3 tangentPoint = float3(width, 0, height);
float3 pos = vertexPosition + mul(transformMatrix, tangentPoint);
o.pos = UnityObjectToClipPos(pos);
o.uv = uv;
return o;
}
[maxvertexcount(3)]
void geom(triangle t2g input[3], inout TriangleStream<g2f> outStream)
{
.....
for (int i = 0; i < BLADE_SEGMENTS; i++)
{
float t = i / (float)BLADE_SEGMENTS;
float segmentHeight = height * t;
float segmentWidth = width * (1 - t);
outStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, float2(0, t), transformationMatrix));
outStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, float2(1, t), transformationMatrix));
}
// 添加最后一个顶点
outStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix));
.....
}
看上面的草是不是变成3节了!
2、让每一层的顶点水平发生偏移使草变弯
g2f GenerateGrassVertex(float3 vertexPosition, float width, float height, float forward, float2 uv, float3x3 transformMatrix)
{
g2f o;
float3 tangentPoint = float3(width, forward, height);
......
}
[maxvertexcount(3)]
void geom(triangle t2g input[3], inout TriangleStream<g2f> outStream)
{
.....
for (int i = 0; i < BLADE_SEGMENTS; i++)
{
......
float segmentForward = pow(t, _BladeCurve) * forward;
outStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformationMatrix));
outStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformationMatrix));
}
outStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix));
.....
}
此时的效果如下
8、投射阴影
原理大致同UnityShader 阴影投射接收的理解及简单应用这篇文章
直接上代码,多一个用于处理投射的pass
Pass
{
Tags
{
"LightMode" = "ShadowCaster"
}
CGPROGRAM
#pragma vertex vert
#pragma hull hs_surf
#pragma domain ds_surf
#pragma geometry geom
#pragma fragment frag
#pragma target 4.6
#pragma multi_compile_shadowcaster
float4 frag(g2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
可见栏杆上的投影
此时草本身是没有阴影接收的,所以看不到草与草之间的阴影投射,因此进行最后一步,让草接收阴影
9、接收阴影,并给草表面增加漫反射
//几何着色器传到片元着色器的数据
struct g2f
{
float4 pos : SV_POSITION;
#if UNITY_PASS_FORWARDBASE
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
//阴影纹理图
unityShadowCoord4 _ShadowCoord : TEXCOORD1;
#endif
};
g2f GenerateGrassVertex(float3 vertexPosition, float width, float height, float forward, float2 uv, float3x3 transformMatrix)
{
g2f o;
float3 tangentPoint = float3(width, forward, height);
float3 pos = vertexPosition + mul(transformMatrix, tangentPoint);
o.pos = UnityObjectToClipPos(pos);
#if UNITY_PASS_FORWARDBASE
float3 tangentNormal = normalize(float3(0, -1, forward));
float3 normal = mul(transformMatrix, tangentNormal);
o.normal = UnityObjectToWorldNormal(normal);
o.uv = uv;
o._ShadowCoord = ComputeScreenPos(o.pos);
#elif UNITY_PASS_SHADOWCASTER
//这一行的作用在下面会有描述
o.pos = UnityApplyLinearShadowBias(o.pos);
#endif
return o;
}
float4 frag(g2f i, fixed facing : VFACE) : SV_Target
{
// 判断内外表面
float3 normal = facing > 0 ? i.normal : -i.normal;
// 获取阴影
float shadow = SHADOW_ATTENUATION(i);
//半罗伯特反射
float diffuse = (1 + dot(_WorldSpaceLightPos0, normal) + _TranslucentGain)/ 2 * shadow;
//详看链接文章
float3 ambient = ShadeSH9(float4(normal, 1));
float4 lightIntensity = diffuse * _LightColor0 +float4(ambient, 1);
float4 col = lerp(_BottomColor, _TopColor * lightIntensity, i.uv.y);
return col;
}
1、fixed facing : VFACE的作用:Unity让我们用于判断内外表面的,>0表示外面,<0表示背面
2、关于ShadeSH9可查看该文章。
3、关于UnityApplyLinearShadowBias函数的作用
如果不加这个函数判断,会产生如下现象,草的阴影投射回有一条一条的线性阴影
添加后线性阴影变得柔和
这篇文章中有讲述到对UnityApplyLinearShadowBias函数的解释
👆成片的草坪上面有草之间阴影的投射,也有栏杆的阴影
对应shader下载地址
PS.后面会写一篇文章,解决草坪跟物体碰撞交互的问题(就是实现草坪可以被踩的效果)以及对草坪的一些效果提升
学历、大学从来都不是限制你成功的因素,当有一天你真正明白自己想要的是什么,成功的路就已经向你敞开,而你与成功的这段距离,需要靠努力来缩短,我一直都坚信着并且这么做着。