大家好,我是阿赵。
这里开始讲大面积草地渲染的第一个部分,一棵草的渲染。按照惯例,完整shader在最后。前面是原理的介绍。

一、准备的资源

unity3d如何添加草地 unity怎么添加草地效果_游戏引擎

这里我自己随便做了一个草的模型,主要是用几个面片搭建的一个简单模型。

unity3d如何添加草地 unity怎么添加草地效果_游戏引擎_02


unity3d如何添加草地 unity怎么添加草地效果_草渲染_03

然后我准备了一张草的贴图,带有透明通道的。

unity3d如何添加草地 unity怎么添加草地效果_草渲染_04

把贴图赋予给模型后,在3DsMax里面草是这个样子,只是简单的几个直草。

二、控制草的形态

把草的模型和贴图放到Unity引擎里面,写一个最简单的漫反射加上Cutout的shader,会看到草是这个样子:

unity3d如何添加草地 unity怎么添加草地效果_#pragma_05

这里我们需要根据草的顶点坐标,做2个范围的控制:

1.从底部到顶部的顶点渐变

unity3d如何添加草地 unity怎么添加草地效果_unity_06

由于我做模型的时候,模型的中心点就在草的根部,所以我可以拿模型中心点作为一个基准,然后计算其他顶点的坐标离根部的Y坐标差,算出一个渐变。上图是越高的地方颜色越白。

通过这个,我们在写Shader的时候,就可以根据顶点高度的不同,给草赋予不同的表现,比如颜色的变化,或者弯曲度的变化。

为了控制黑白渐变的范围,我一般会加一个smoothstep来控制。

2.以底部为圆心,求扩散的范围

unity3d如何添加草地 unity怎么添加草地效果_草渲染_07

这个计算和刚才的高度计算效果不一样,离圆心越近的点越黑,离得越远的点越白。这个渐变的范围,可以用于我们计算时,草离中心不一样,表现不一样。
同样的,为了控制黑白渐变的范围,我会加smoothstep控制。

有了以上2个渐变之后,下面就开始来计算草的外观了。

1、草的颜色变化

一棵植物,一般会在不同的生长部位表现出颜色不一样,比如一棵草,按道理可能会根部的颜色较深,而顶部的地方颜色较浅。

这里先实现一下这个效果,根据刚才的高度渐变计算结果,可以把草本身的贴图颜色再混合2个颜色:一个根部颜色和一个顶部颜色。

混合之后,结果如下:

unity3d如何添加草地 unity怎么添加草地效果_游戏引擎_08

2、草的弯曲度变化

之前在3DsMax里面做的草模型,是直挺挺的草,完全没有弯曲度的,但实际上的草,会因为受到重力的原因,而到了一定的长度和角度之后出现弯曲。

这里我用到了上面计算的高度渐变和圆心渐变,来做这个效果。

首先,根据实际情况,离圆心越远的叶子,由于他倾斜度会越大,所以受到重力之后他应该往下弯曲得更多,然后,究竟从哪个高度开始才应该弯曲呢?这里就可以用高度渐变来控制了。

最后得到的结果是这样:

unity3d如何添加草地 unity怎么添加草地效果_unity3d如何添加草地_09

三、关于阴影

由于我是用顶点片段程序来写这个shader的,所以阴影需要用单独一个pass来绘制。
怎样使用动态阴影,之前我有介绍过,要在shader里面加入影子三剑客。
不过这里由于使用了顶点变化的控制,所以在这个专门绘制影子的pass里面,我们需要把顶点程序也照抄一份。
还有一个需要注意的是,在顶点程序里面照抄完控制顶点的代码之后,不是给v2f结构体赋值转换到裁剪空间的顶点坐标,而是直接赋值给输入顶点程序的appdata结构体的vertex。这是比较特殊的地方。

其实也可以不用影子三剑客来做,在不要#pragma multi_compile_shadowcaster、TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)和SHADOW_CASTER_FRAGMENT(i)的情况下,
为了让影子的顶点正常的动画,所以顶点程序直接复制正常pass。
然后片段程序改成最简单的clip透明度,也可以做到镂空的效果。

half4 frag(v2f i) : SV_Target
	{
		half4 col = tex2D(_MainTex, i.uv);
		clip(col.a - 0.5);
		return col;
	}

不过产生影子的pass必须要加上标签

Tags { "LightMode" = "ShadowCaster" }

四、完整shader:

Shader "azhao/GrassBsse"
{
    Properties
    {
		_MainTex("MainTex", 2D) = "white" {}
		_hmin("hmin", Range(0 , 1)) = 0
		_hmax("hmax", Range(0 , 1)) = 1
		_hOffset("hOffset", Range(-1 , 1)) = 0
		_vmin("vmin", Range(0 , 1)) = 0
		_vmax("vmax", Range(0 , 1)) = 1
		_vOffset("vOffset", Range(-5 , 5)) = 0
		_topCol("topCol", Color) = (0,1,0,0)
		_bottomCol("bottomCol", Color) = (0,0,0,0)

    }
    SubShader
    {
		Tags{"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout" }
		Cull Off

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag


			#include "UnityShaderVariables.cginc"
			#pragma target 3.0
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {                
                float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 centerPos : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
				float3 hvVal : TEXCOORD3;

            };

			uniform float _hmin;
			uniform float _hmax;
			uniform float _vmin;
			uniform float _vmax;
			uniform float _vOffset;

			uniform float _hOffset;

			uniform sampler2D _MainTex;
			uniform float4 _MainTex_ST;
			uniform float4 _topCol;
			uniform float4 _bottomCol;
			SamplerState sampler_MainTex;

            v2f vert (appdata v)
            {
                v2f o;
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.centerPos = mul(unity_ObjectToWorld, float4(float3(0, 0, 0), 1)).xyz;
				o.worldPos = mul(unity_ObjectToWorld, v.vertex);
				float hVal = smoothstep(_hmin, _hmax, o.worldPos.y - o.centerPos.y);
				float vVal = smoothstep(_vmin, _vmax, distance(o.worldPos.xz, o.centerPos.xz));
				float hvVal = hVal * vVal;
				o.hvVal = float3(hVal, vVal, hvVal);
				float hVertexOffset = hvVal * _hOffset;
				float2 vVertexOffset = (o.worldPos.xz - o.centerPos.xz)*hvVal*_vOffset;

				o.pos = UnityObjectToClipPos(v.vertex+float3(vVertexOffset.x, hVertexOffset, vVertexOffset.y));
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                // sample the texture
                half4 col = tex2D(_MainTex, i.uv);
				half3 finalCol = col.rgb * _topCol.rgb*i.hvVal.z + col.rgb;
				finalCol = clamp(finalCol*i.hvVal.x + _bottomCol * (1 - i.hvVal.x)*finalCol,  half3(0, 0, 0), half3(1, 1, 1));
				half alpha = col.a;
				clip(alpha - 0.5);
                return half4(finalCol,alpha);
            }
            ENDCG
        }
		
		//为了产生影子,加多一个pass
		Pass {
			Name "ShadowCaster"
			Tags { "LightMode" = "ShadowCaster" }

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 2.0
			#pragma multi_compile_shadowcaster
			#include "UnityCG.cginc"

			struct v2f {
				V2F_SHADOW_CASTER;
			};
			uniform float _hmin;
			uniform float _hmax;
			uniform float _vmin;
			uniform float _vmax;
			uniform float _vOffset;

			uniform float _hOffset;
			uniform sampler2D _MainTex;
			uniform float4 _MainTex_ST;
			uniform float4 _topCol;
			uniform float4 _bottomCol;
			SamplerState sampler_MainTex;
			v2f vert(appdata_base v)
			{
				v2f o;
				float3 centerPos = mul(unity_ObjectToWorld, float4(float3(0, 0, 0), 1)).xyz;
				float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
				float hVal = smoothstep(_hmin, _hmax, worldPos.y - centerPos.y);
				float vVal = smoothstep(_vmin, _vmax, distance(worldPos.xz, centerPos.xz));
				float hvVal = hVal * vVal;
				float hVertexOffset = hvVal * _hOffset;
				float2 vVertexOffset = (worldPos.xz - centerPos.xz)*hvVal*_vOffset;

				v.vertex = v.vertex + float4(vVertexOffset.x, hVertexOffset, vVertexOffset.y,1);
				TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
				return o;
			}

			float4 frag(v2f i) : SV_Target
			{
				SHADOW_CASTER_FRAGMENT(i)
			}
			ENDCG

		}

    }
	FallBack "VertexLit"
}