最近游戏《对马岛之魂》发售了。
其中的草地特动效十分先进:
(图片来自网络)
于是我也萌生了写一个动态草地的shader
草模型
现如今的许多手游甚至端游的草地考虑到硬件消耗,常常把草做成星形或者广告牌草地
(图片来自网络)
但是这样的草地很容易露出破绽,效果也不是特别好,于是这里我让每根草都拥有单独的模型。
实际上更好的方法是做多段的长方形,然后使用mask贴图指定草地形状,这样可以使草地形状更加圆润。
完成单个草模型后视情况调整、套用这篇文章中的Maya脚本制作出草丛簇。完成模型的制作。
草地动效实现
草地的动态效果初步分为两个部分:
1、采样一个白噪音贴图通过偏移UV的方式实现草地顶点的随机抖动
2、根据顶点位置实现草地的波浪
随机抖动
如上面所说,我们先搞一个四通道的白噪声贴图(RGBA通道均由白噪声随机生成,实际上我们只会任意使用其中两个通道,甚至一个)然后用顶点位置+_Time作为UV。_WindPow
为强度控制属性
float2 samplePos = worldPos.xz / _WindPow + _Time.x;
fixed4 noiseSample = tex2Dlod(_Noise,float4(samplePos,0,0));
顶点x、y轴的位移函数为
worldPos.x +=_WindPow *noiseSample.x*v.uv.y;
worldPos.z +=_WindPow *noiseSample.y*v.uv.y;
我们用两个采样出的噪声分别叠加进入x,y轴中,乘上uv.y
是为了
对不同高度的顶点区别处理,让草尖抖动大于草根。
由于抖动幅度小,这里暂时不处理顶点y轴。减少运算。
草地波浪
草地的波浪做了两个版本:
一个版本是使用正弦函数模拟草地波浪;
第二个版本采样一个波浪灰度图模拟草地波浪;
1、直接使用正弦函数模拟波浪
首先,引入一个_WindDir
属性,用于表示风方向,范围在0~2π(6.28)中,然后转为方向
//从方向转为xy值
half windX = cos(_WindDir);
half windY = sin(_WindDir);
新的顶点位移函数为:
//下面是无noise波浪方案
worldPos.x +=noisewindpow*noiseSample.x*_WindPow*v.uv.y/6
+(sin(6*_Time.y*_WindPow+(windX*worldPos.y+windY*worldPos.z))+1.3)*0.3f*_WindPow*windX*v.uv.y;
worldPos.z +=noisewindpow*noiseSample.y*_WindPow*v.uv.y/6
+(sin(6*_Time.y*_WindPow+(windX*worldPos.y+windY*worldPos.z))+1.3)*0.3f*_WindPow*windY*v.uv.y;
o.col = (sin(6*_Time.y*_WindPow+windX*worldPos.x+windY*worldPos.z)+1.0)*0.2f;
worldPos.y -= o.col*v.uv.y;
波浪函数增加在之前的随机飘动函数后(其中所有的数字都是可调的)
在sin函数内6*_Time.y*_WindPow
影响波浪速度,windX*worldPos.y+windY*worldPos.z
决定波浪方向。
sin函数外所有数值都只影响波浪强度和范围。
由于波浪的抖动幅度大,必须要考虑草的高度。这里也采用正弦函数模拟。方法与上面类似。这里同时为片面着色器传入了数据,可以为浪峰和浪谷增加色差。
下图是效果展示:
这样的做的波浪一旦处理大范围草地就会显得重复单调。所以引入使用一张灰度图处理波浪的办法。
2、采样灰度图模拟草地波浪
首先对灰度图进行采样:
float2 sampleNoise = worldPos.xz/50-float2(windX,windY)*(_Time.x/0.5+0.7)*_WindPow*4;
fixed noisewindpow = tex2Dlod(_WindNoise,float4(sampleNoise,0,0));
noisewindpow += _WindPow;
sampleNoise
中的_Time需要乘上之前算出的windX
和windY
以使采样偏移和风向相同。
采样出的值作为新的风强度来影响顶点。
//随机飘动+定向波浪(噪声纹理版本)
worldPos.x +=noisewindpow*noiseSample.x*v.uv.y/6
+windX*(noisewindpow+1)*v.uv.y/5;
worldPos.z +=noisewindpow*noiseSample.y*v.uv.y/6
+windY*(noisewindpow+1)*v.uv.y/5;
//大幅度位移过程中的高度变换
o.col = noisewindpow*0.5f;
worldPos.y -= o.col*v.uv.y;
方法与上一个波浪相似。甚至更加简单。
最后就是颜色、阴影接受等功能的实现,这里就不多说了。
方法需要改进的地方:
1、首先是之前提到的可以采用mask贴图绘制草
2、草在随机抖动过程中会变得扭曲,也许可以用法线对顶点移动方向进行一定的限制。
以下是shader源码:
Shader "sence/Grass"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("Color",Color) = (1,1,1,1)
_Noise ("Noise",2D) = "white"{}
//风
_WindNoise("WindNoise",2D) = "white"{}
_WindDir("GrassDirection",Range(0,6.28)) = 0
_WindPow("WindPower",Range(0,1)) = 1
}
SubShader
{
cull off
Tags { "RenderType"="Opaque" "ForceNoShadowCasting" = "True" "IgnoreProjector" = "True"}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
// make fog work
#pragma multi_compile_fog
#include "AutoLight.cginc"
#include "Lighting.cginc"
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
fixed4 col :COLOR;
UNITY_FOG_COORDS(1)
SHADOW_COORDS(2)
};
sampler2D _MainTex;
sampler2D _Noise;
sampler2D_float _WindNoise;
float4 _MainTex_ST;
half _WindDir;
half _WindPow;
fixed4 _Color;
v2f vert (appdata v)
{
v2f o;
float4 worldPos = mul(unity_ObjectToWorld,v.vertex);
float3 worldnormal = UnityObjectToWorldNormal(v.normal);
//从方向转为xy值
half windX = cos(_WindDir);
half windY = sin(_WindDir);
//噪声采样
float2 samplePos = worldPos.xz / _WindPow + _Time.x;
fixed4 noiseSample = tex2Dlod(_Noise,float4(samplePos,0,0));
float2 sampleNoise = worldPos.xz/50-float2(windX,windY)*(_Time.x/0.5+0.7)*_WindPow*4;
fixed noisewindpow = tex2Dlod(_WindNoise,float4(sampleNoise,0,0));
noisewindpow += _WindPow;
//随机飘动+定向波浪(噪声纹理版本)
worldPos.x +=noisewindpow*noiseSample.x*v.uv.y/6
+windX*(noisewindpow+1)*v.uv.y/5;
worldPos.z +=noisewindpow*noiseSample.y*v.uv.y/6
+windY*(noisewindpow+1)*v.uv.y/5;
//大幅度位移过程中的高度变换
o.col = noisewindpow*0.8f;
worldPos.y -= o.col*v.uv.y;
//下面是无noise波浪方案
// worldPos.x +=noisewindpow*noiseSample.x*_WindPow*v.uv.y/6
// +(sin(6*_Time.y*_WindPow+(windX*worldPos.y+windY*worldPos.z))+1.3)*0.3f*_WindPow*windX*v.uv.y;
// worldPos.z +=noisewindpow*noiseSample.y*_WindPow*v.uv.y/6
// +(sin(6*_Time.y*_WindPow+(windX*worldPos.y+windY*worldPos.z))+1.3)*0.3f*_WindPow*windY*v.uv.y;
// o.col = (sin(6*_Time.y*_WindPow+windX*worldPos.x+windY*worldPos.z)+1.0)*0.2f;
// worldPos.y -= o.col*v.uv.y;
o.pos = mul(UNITY_MATRIX_VP,worldPos);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.pos);
TRANSFER_SHADOW(o)
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 shadow = SHADOW_ATTENUATION(i);
col = float4(col.rgb,1)-i.col/2;
col *=shadow*_Color/*+_LightColor0.a/3*/;
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}