Unity Simple 2D Water Shader

效果

项目地址

在线演示

笔记

本来是要抄 帝国时代2HD 中的水面效果来的,后来折腾了好久. 发现搞不定😂, 弄出来的效果总是怪怪的. 没有原版的那种 高逼格的低调奢华.😑

网上有搜了许多,不过每个效果都弄的酷炫无比,没有个3A游戏的底子都不好意思用. 后来搜到了 则卷大明 的 Unity3D-移动平台简单水效果的实现 感觉思路很好. 就照着改了改. 以下是这段做水Shader中学到的心得.

制作思路

首先是 则卷大明 这版中的 直接用单张纹理来模拟 水面波动+光照. 这个思路很好. 比起扰动Normal的方法要省事许多. 不过他这版有几个地方我觉得不太适合我的项目.所以做了少许更改

  • 将光斑改为屏幕坐标系下.(同 AoE2 HD)
  • 分离水面纹理和光照纹理 : 这样方便调整效果,但是会增加一次Simpling.不过我底图纹理还加入了扰动包括后面底图还会做多张纹理的Blend.所以拆分并没有增加整体的性能损耗.
  • 底图加入扰动 : 同 AoE2 , 让水有一种很弱的流动感
  • 去除fresnel和高光 : 因为视角是斜45锁定视角,所以相关的效果都可以直接在光照纹理中模拟出来.

学到的东西

快速计算World Normal

原先是按 冯乐乐书中的构建矩阵的方式保留Normal变换矩阵,然后在frag中把Simpling到的切空间结果转一下 (P153 7.2 凹凸映射). 后来发现自己傻了.

1
2
float3 bump = UnpackNormal(tex2D(_BumpMap,i.uv.xy)); //取得Tangent空间下的Normal扰动值
float3 worldBump = float3(bump.xzy);

直接更换yz即可得到worldNormal的扰动值,又因为初始Normal肯定都是Vector3.Up, 所以连先使用UnityObjectToWorldNormal求得原始Normal

然后再线性插值传到frag中 *= worldBump 都可以省略了.

直接使用worldBump作为最终的Normal就行了.

当然后来我发现 其实我连Normal都不需要…😂

使用viewVector.y判断离相机远近

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v2f vert (appdata v)
{
v2f o;

o.vertex = UnityObjectToClipPos(v.vertex);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
o.viewVector = (_WorldSpaceCameraPos.xyz - worldPos);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
float value = normalize(i.viewVector).y * _Power;
return float4(value,value,value,1);
}

接近水平情况

接近垂直情况

使用NormalMap来制作扰动

Perlin Noise 产生的随机数区间是[0~1],经过转换后可变变为[-1~1]的区间. 但是对于单通道的Perlin Noise xy(rg)通道取得的值都一样. 也就是要么均为正 要么均为负.

所以如果直接使用Perlin Noise图来进行扰动的话会出现明显的图片偏移情况.

Perlin Noise Maker里面可以生成Color的纹理图. 我之前使用的就是这个方案. 也就是rgba通道对应四张纹理图.

这样Simpling时候xy的值不会同时为正或者同时为负. 不过可能是因为每个通道的Noise不同 最终效果并不好.

但是直接使用NormalMap就不会出现这种困扰.NormalMap在切空间内Z值朝上xy是在整个[-1,1]的空间内取值.

15123441065379

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
half4 PrintBump(float2 _bump)
{
if(_bump.x > 0 && _bump.y > 0)
{
return float4(0.0000, 0.5020, 0.2510,1); //墨绿=> 右上
}
else if(_bump.x < 0 && _bump.y < 0)
{
return float4(1.0000, 1.0000, 0.0000,1);//黄色 => 左下
}
else if(_bump.x > 0 && _bump.y < 0)
{
return float4(0.6000, 0.4000, 0.2000,1); //棕色 => 右下
}
else if(_bump.x < 0 && _bump.y > 0)
{
return float4(0.0000, 0.0000, 1.0000,1); //蓝色 => 左上
}
else
{
return float4(1,1,1,1);
}
}

直接输出NoiseMap:

输出NoiseMap转换后的NormalMap

单方向&双方向 Loop Noise

单方向Loop Noise有明显的朝某方向移动的感觉,双方向Loop Noise则是扰动的效果

1
2
3
4
5
float3 b1 = UnpackNormal(tex2D(_BumpTex,i.uv.zw + _Time.yy * _BumpDir.xy * _BumpSpeed1));
float3 b2 = UnpackNormal(tex2D(_BumpTex,i.uv.zw + _Time.yy * _BumpDir.zw * _BumpSpeed2));

fixed4 oneDir = tex2D(_SunTex,i.uv2 + b1 * _BumpPower);
fixed4 twoDir = tex2D(_SunTex,i.uv2 + (b1+b2) * _BumpPower);

对于单方向Loop 可以找到对应的Pa’点 使得其变化的值只和Pa点相差了△t个时间单位.

但是对于双方向的遍历Pb点,则找不出任何一个Pb’点 和其值一直相等只相差了△t个时间单位.

坚持原创技术分享,您的支持将鼓励我继续创作!