前言

草地效果有多种实现方法,我所了解的比较常见的有Billborad,星型结构,还有就是用GPU实时渲染,也就是用Geometry Shader生成面片双面渲染。前两种方法虽然效率比较高但是实现的效果有限,第三种方法则可以根据我们的需要实现更细致的效果。在这不得不提一句,我第一次看到Geometry Shader实现的效果时,我不禁感叹这真的是一个狂拽酷炫的东西,作为一个处于顶点着色器和片元着色器之间的着色器,它相比较顶点着色器具有更高的灵活性,因为我们可以方便的使用GPU添加和删除图元,相比较于片元着色器它又更加直观。我们可以用几何着色器实现非常多花里胡哨的功能,其中之一就是模拟草地。但是效果酷炫带来的代价就是它的效率不是很高,它不像vertex和fragment shader可以高度并行运算,而且低版本的图形库是不支持这个功能的。不过好消息是根据我的调查,从两年多前的骁龙835处理器的Areno 540开始就支持OpenGL ES3.2了,而这个版本的一个新特性就是支持Geometry Shader!!!所以随着硬件的发展,在移动端以及非旗舰性能的PC端大量使用GS应该也不是一个久远的事情了。

具体实现

我的学习和实现参考的是Unity Grass Shader这篇文章,文章是英文的,但讲的很详细,想要了解更多的可以去看一看,我在这里就不非常细致的讲解了,后续就讲讲几个关键点和我做出的修改。首先贴出完整代码。

Shader "MyShader/GrassShader"
{
	Properties
	{
		[Header(Wind)]
		[Space]
		_WindDistortionMap("Wind Distortion Map", 2D) = "white" {}
		_WindFrequency("Wind Frequency", Vector) = (0.05, 0.05, 0, 0)
		_WindStrength("Wind Strength", Range(0, 2)) = 1
		[Header(Density)]
		[Space]
		_Density("Density", Range(1, 30)) = 5
		[Header(Color of grass)]
		[Space]
		_TopColor("Top Color", Color) = (1,1,1,1)
		_BottomColor("Bottom Color", Color) = (1,1,1,1)
		[Header(Shape of blade)]
		[Space]
		_BendRotationRandom("Bend Rotation Random", Range(0, 1)) = 0.2
		_BladeWidth("Blade Width", Range(0, 0.2)) = 0.1
		_BladeWidthRandom("Blade Width Random", Range(0, 0.1)) = 0.02
		_BladeHeight("Blade Height", Range(0, 1)) = 0.5
		_BladeHeightRandom("Blade Height Random", Range(0, 1)) = 0.3
		_BladeForward("Blade Forward Amount", Range(0, 1)) = 0.38
		_BladeCurve("Blade Curvature Amount", Range(1, 4)) = 2
	}
		CGINCLUDE
		#include "UnityCG.cginc"
		#include "AutoLight.cginc"

		#define ADD_TANSPCVERT(v, matrix) \
				o.pos = UnityObjectToClipPos(pos + mul(matrix, v)); \
				o.uv = float2(v.x - 0.5, v.y); \
				triStream.Append(o); 

		#define BLADE_SEGMENTS 3

		struct a2v
		{
			float4 vertex : POSITION;
			float3 normal : NORMAL;
			float4 tangent : TANGENT;
		};

		struct v2g
		{
			float4 vertex : POSITION;
			float3 normal : NORMAL;
			float4 tangent : TANGENT;
		};

		struct g2f
		{
			float4 pos : SV_POSITION;
			float2 uv : TEXCOORD0;
		};

		float _BendRotationRandom;
		float _BladeHeight;
		float _BladeHeightRandom;
		float _BladeWidth;
		float _BladeWidthRandom;
		float _Density;
		sampler2D _WindDistortionMap;
		float4 _WindDistortionMap_ST;
		float2 _WindFrequency;
		float _WindStrength;
		float _BladeForward;
		float _BladeCurve;

		v2g vert(a2v v)
		{
			v2g o;
			o.vertex = v.vertex;
			o.normal = v.normal;
			o.tangent = v.tangent;
			return o;
		}

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

		float2 randto2D(float2 seed)
		{
			float2 f = sin(float2(dot(seed, float2(127.1, 337.1)), dot(seed, float2(269.5, 183.3))));
			f = 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
			);
		}

		void addVert(float3 pos, float3x3 tangentToObject, inout TriangleStream<g2f> triStream)
		{
			float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos.xyz) * UNITY_TWO_PI, float3(0, 0, 1));
			float3x3 bendRotationMatrix = AngleAxis3x3(rand(pos.zyx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1, 0, 0));
			float2 uv = pos.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y;
			float2 windSample = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * max(_WindStrength, 0.0001);
			float3 wind = normalize(float3(windSample.x, windSample.y, 0));
			float3x3 windRotation = AngleAxis3x3(UNITY_PI * windSample, wind);
			float3x3 transformationMatrixFacing = mul(tangentToObject, facingRotationMatrix);
			tangentToObject = mul(mul(mul(tangentToObject, windRotation), facingRotationMatrix), bendRotationMatrix);

			float height = max(rand(pos.xzy) * _BladeHeightRandom + _BladeHeight, 0.1);
			float width = max(rand(pos.yzx) * _BladeWidthRandom + _BladeWidth, 0.01);
			float forward = rand(pos.yyz) * _BladeForward;
			g2f o;
			for (int i = 0; i < BLADE_SEGMENTS; i++)
			{
				float t = i / (float)BLADE_SEGMENTS;
				float segmentHeight = height * t;
				float segmentWidth = width * (1 - t);
				float segmentForward = pow(t, _BladeCurve) * forward;
				float3x3 transformationMatrix = i == 0 ? transformationMatrixFacing : tangentToObject;
				ADD_TANSPCVERT(float3(segmentWidth / 2, segmentForward, segmentHeight), transformationMatrix);
				ADD_TANSPCVERT(float3(-segmentWidth / 2, segmentForward, segmentHeight), transformationMatrix);
			}
			ADD_TANSPCVERT(float3(0, forward, height), tangentToObject);
			triStream.RestartStrip();
		}

		[maxvertexcount((BLADE_SEGMENTS * 2 + 1) * 24)]
		void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
		{
			float3 normal = IN[0].normal;
			float4 tangent = IN[0].tangent;
			float3 biNormal = cross(normal, tangent) * tangent.w;
			float3x3 tangentToObject = float3x3(
				tangent.x, biNormal.x, normal.x,
				tangent.y, biNormal.y, normal.y,
				tangent.z, biNormal.z, normal.z
			);
			float4 center = (IN[0].vertex + IN[1].vertex + IN[2].vertex) / 3;//center of quad
			float3 pos = center.xyz;
			for (int i = 0; i < _Density; i++)
			{
				float2 offset = randto2D(pos.xz);
				pos = IN[0].vertex;
				pos += (IN[1].vertex - pos) * offset.x;
				pos += (IN[2].vertex - pos) * offset.y;
				addVert(pos.xyz, tangentToObject, triStream);
			}
		}
		ENDCG
    SubShader
    {
        Tags { "RenderType"="Opaque" }
		Pass
		{
			Tags{"LightMode" = "ForwardBase"}
			Cull Off
			CGPROGRAM
			#pragma target 4.0
			#pragma vertex vert
			#pragma fragment frag
			#pragma geometry geom
			
			fixed4 _TopColor;
			fixed4 _BottomColor;
			
			fixed4 frag(g2f i) : SV_Target
			{
				fixed4 color = lerp(_BottomColor, _TopColor, i.uv.y);
				return color;
			}
			ENDCG
		}      
		Pass
		{
			Tags
			{
				"LightMode" = "ShadowCaster"
			}

			CGPROGRAM
			#pragma vertex vert
			#pragma geometry geom
			#pragma fragment frag
			#pragma target 4.0
			#pragma multi_compile_shadowcaster

			float4 frag(g2f i) : SV_Target
			{
				SHADOW_CASTER_FRAGMENT(i)
			}
			ENDCG
		}
    }
    FallBack "Diffuse"
}

关键点0(修改点):密度变换

因为是根据mesh中原有顶点来生成小草,所以为了有更大密度的草地需要更多顶点的mesh,原文中采取的是曲面细分着色器,这个我还没有去接触和学习,所以我用在三角面片中随机添加顶点的方式来增加密度。这个方法有一点坏处就是每输入一个三角面片就要输出大量的顶点。

for (int i = 0; i < _Density; i++)
			{
				float2 offset = randto2D(pos.xz);//生成2维向量
				pos = IN[0].vertex;//从一个顶点出发
				pos += (IN[1].vertex - pos) * offset.x;//向第二个顶点偏移
				pos += (IN[2].vertex - pos) * offset.y;//向第三个顶点偏移以得到最终的随机位置
				addVert(pos.xyz, tangentToObject, triStream);
			}

关键点1:顶点变换

因为考虑到要让grass不仅能在plane上生成,还可以在cube,sphere等各种顶点法线朝向不统一的物体上,所以需要我们在顶点的切线空间内计算需要生成点的位置,这就涉及到顶点的坐标变换。在这一层变换之外为了模拟草叶的不同朝向,风吹草动等效果还要涉及顶点的旋转变换。

float3 normal = IN[0].normal;
float4 tangent = IN[0].tangent;
float3 biNormal = cross(normal, tangent) * tangent.w;
float3x3 tangentToObject = float3x3(
	tangent.x, biNormal.x, normal.x,
	tangent.y, biNormal.y, normal.y,
	tangent.z, biNormal.z, normal.z
);

......
......

tangentToObject = mul(mul(mul(tangentToObject, windRotation), facingRotationMatrix), bendRotationMatrix);

需要注意的一点是,关于旋转变化的矩阵是如何得出的

float3x3 AngleAxis3x3(float angle, float3 axis)

这个函数传入的旋转轴需要进行归一化处理,我代码中的这个函数不需要。

关键点2:风的模拟

风的模拟采取的方法是根据时间和顶点坐标对一张噪声贴图(应该是)采样,需要注意的是调节纹理采样的缩放值,也就是材质面板中的Tiling到一个比较小的值,这样小草“被风吹动导致”的倾斜度才有一个比较连续的状态,更贴近自然中风吹草动的状况。不理解的可以看看下图,如果不调节这个值会出现下面这种情况

unity terrian 添加花草 the brush is read only unity如何添加草地_unity3d

关键点3:阴影

我们需要用一个单独的Pass来处理阴影投射。这个Pass中的片元着色器只进行阴影投射,不会输出任何颜色,但是为了让阴影能正确生成阴影图我们同样需要在这个Pass中再做一次几何着色器中对图元的处理,所以可以把两个Pass中通用的代码写在CGINCLUDE里来避免代码的重复。

未实现点:接收阴影和光照处理

由于时间原因我暂时还没有做这两个点,但是实际上都是一些很基础的东西,如果阅读这篇博客的人真有需要可以自己尝试实现以下或者是参考原文链接中给出的代码。

最终效果展示

unity terrian 添加花草 the brush is read only unity如何添加草地_unity3d_02