大家好,我是阿赵。
这里开始讲大面积草地渲染的第一个部分,一棵草的渲染。按照惯例,完整shader在最后。前面是原理的介绍。
一、准备的资源
这里我自己随便做了一个草的模型,主要是用几个面片搭建的一个简单模型。
然后我准备了一张草的贴图,带有透明通道的。
把贴图赋予给模型后,在3DsMax里面草是这个样子,只是简单的几个直草。
二、控制草的形态
把草的模型和贴图放到Unity引擎里面,写一个最简单的漫反射加上Cutout的shader,会看到草是这个样子:
这里我们需要根据草的顶点坐标,做2个范围的控制:
1.从底部到顶部的顶点渐变
由于我做模型的时候,模型的中心点就在草的根部,所以我可以拿模型中心点作为一个基准,然后计算其他顶点的坐标离根部的Y坐标差,算出一个渐变。上图是越高的地方颜色越白。
通过这个,我们在写Shader的时候,就可以根据顶点高度的不同,给草赋予不同的表现,比如颜色的变化,或者弯曲度的变化。
为了控制黑白渐变的范围,我一般会加一个smoothstep来控制。
2.以底部为圆心,求扩散的范围
这个计算和刚才的高度计算效果不一样,离圆心越近的点越黑,离得越远的点越白。这个渐变的范围,可以用于我们计算时,草离中心不一样,表现不一样。
同样的,为了控制黑白渐变的范围,我会加smoothstep控制。
有了以上2个渐变之后,下面就开始来计算草的外观了。
1、草的颜色变化
一棵植物,一般会在不同的生长部位表现出颜色不一样,比如一棵草,按道理可能会根部的颜色较深,而顶部的地方颜色较浅。
这里先实现一下这个效果,根据刚才的高度渐变计算结果,可以把草本身的贴图颜色再混合2个颜色:一个根部颜色和一个顶部颜色。
混合之后,结果如下:
2、草的弯曲度变化
之前在3DsMax里面做的草模型,是直挺挺的草,完全没有弯曲度的,但实际上的草,会因为受到重力的原因,而到了一定的长度和角度之后出现弯曲。
这里我用到了上面计算的高度渐变和圆心渐变,来做这个效果。
首先,根据实际情况,离圆心越远的叶子,由于他倾斜度会越大,所以受到重力之后他应该往下弯曲得更多,然后,究竟从哪个高度开始才应该弯曲呢?这里就可以用高度渐变来控制了。
最后得到的结果是这样:
三、关于阴影
由于我是用顶点片段程序来写这个shader的,所以阴影需要用单独一个pass来绘制。
怎样使用动态阴影,之前我有介绍过,要在shader里面加入影子三剑客。
不过这里由于使用了顶点变化的控制,所以在这个专门绘制影子的pass里面,我们需要把顶点程序也照抄一份。
还有一个需要注意的是,在顶点程序里面照抄完控制顶点的代码之后,不是给v2f结构体赋值转换到裁剪空间的顶点坐标,而是直接赋值给输入顶点程序的appdata结构体的vertex。这是比较特殊的地方。
四、完整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"
}