本文参考自该文章 实现思路:
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可以是三角片面任意一个点,分别对应如下图所示,就会从对应的顶点上长出 “草”

unity刷草地在Game中不显示 unity草地怎么做_shader


分别对应如下三图

unity刷草地在Game中不显示 unity草地怎么做_Blade_02


unity刷草地在Game中不显示 unity草地怎么做_unity刷草地在Game中不显示_03


unity刷草地在Game中不显示 unity草地怎么做_Blade_04


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)

unity刷草地在Game中不显示 unity草地怎么做_unity刷草地在Game中不显示_05

unity刷草地在Game中不显示 unity草地怎么做_Blade_06


unity刷草地在Game中不显示 unity草地怎么做_shader_07

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轴方向!

unity刷草地在Game中不显示 unity草地怎么做_unity刷草地在Game中不显示_08


从上面可以看出,从一个球体上长出的“草”都是垂直顶点长出来的!

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是如何设定的?看下面的图就知道

unity刷草地在Game中不显示 unity草地怎么做_unity刷草地在Game中不显示_09


然后我们在片元着色器中利用插值得到如下图所示的草色

unity刷草地在Game中不显示 unity草地怎么做_shader_10

此时我们的草就有颜色了!

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)));
		.....
}

unity刷草地在Game中不显示 unity草地怎么做_Blade_11

② 朝向随机

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,所以看到的只有一条缝

unity刷草地在Game中不显示 unity草地怎么做_Blade_12


**

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刷草地在Game中不显示 unity草地怎么做_着色器_13


是不是开始有点那味了!

曲面细分这次再次使用的时候发现有些疑点,就去查了下官方文档,在前面两行有说到一个

unity刷草地在Game中不显示 unity草地怎么做_unity_14


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);
		.....
}

unity刷草地在Game中不显示 unity草地怎么做_shader_15


这感觉对了!非常棒!

但还不对劲!草怎么就一节!看上去太僵硬!太丑了!给他多几节!让草本身带有一定的曲率

7、给草多长几节,让草本身变弯

草变弯的原理图如下,总结为以下几点:

1、给草增加顶点(越往上层,每节草宽度按照一定比例缩小,这样就能保证是一个三角形)

2、让每一层(2个点)产生一定的水平偏移,展示出来的效果就是草 “弯了”

unity刷草地在Game中不显示 unity草地怎么做_着色器_16


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));
		.....
}

unity刷草地在Game中不显示 unity草地怎么做_unity刷草地在Game中不显示_17


看上面的草是不是变成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));
		.....
}

unity刷草地在Game中不显示 unity草地怎么做_shader_18


此时的效果如下

unity刷草地在Game中不显示 unity草地怎么做_unity刷草地在Game中不显示_19

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
}

unity刷草地在Game中不显示 unity草地怎么做_Blade_20


可见栏杆上的投影

此时草本身是没有阴影接收的,所以看不到草与草之间的阴影投射,因此进行最后一步,让草接收阴影

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函数的作用

如果不加这个函数判断,会产生如下现象,草的阴影投射回有一条一条的线性阴影

unity刷草地在Game中不显示 unity草地怎么做_着色器_21


添加后线性阴影变得柔和

unity刷草地在Game中不显示 unity草地怎么做_unity刷草地在Game中不显示_22


这篇文章中有讲述到对UnityApplyLinearShadowBias函数的解释

unity刷草地在Game中不显示 unity草地怎么做_shader_23


👆成片的草坪上面有草之间阴影的投射,也有栏杆的阴影

对应shader下载地址

PS.后面会写一篇文章,解决草坪跟物体碰撞交互的问题(就是实现草坪可以被踩的效果)以及对草坪的一些效果提升

学历、大学从来都不是限制你成功的因素,当有一天你真正明白自己想要的是什么,成功的路就已经向你敞开,而你与成功的这段距离,需要靠努力来缩短,我一直都坚信着并且这么做着。