水面模拟2.0
之前根据Minionsart的教程学习过一个卡通风格的水面效果。这次趁着作业打算学习一下稍微复杂一点的水面渲染。星云大大的文章总结得已经很全面了,之前写的那个shader大概就是正弦波和顶点高度位移图了,这次想试试Gertner波实现。
水面波浪 Gerstner波
Gerstner波因为其计算量可控,性价比高,在游戏水体渲染领域应用较为广泛,比之前的正弦波而言,Gerstner波可以生成更为尖锐的波浪,波谷更为宽广,可以模拟粗犷的海面。关于波形的计算可以看Catlike Coding-Wave和GPU Gems-Chap1。
根据GPU Gems,Gerstner波的顶点计算公式可以表达为(看起来一大串其实都是加减乘除啦):
其中,
$Q = waveSteepness$,当$Q_i = 0$,即为普通sin波,要注意$Q_i$不得过大,否则波峰之上会出现诡异的面片交叉。
$D$为方向向量,jiD等于向量的$(x,y)$分量。
$A = waveAmplitude$,为波峰高度。
$w = \sqrt{\frac{2\pi}{waveLength}}$,为频率。
$\phi = \frac{waveSpeed}{waveLength}$,为相位。
法线部分的计算如下:
其中,
根据以上公式得知,每个Gersner波需要输入的参数有——波纹方向、陡峭程度、波峰高度、波长、波纹速度。Shaderlab实现代码为:
float3 GerstnerWave(float4 wave, float3 p, float height, float steepness, inout float3 tangent, inout float3 binormal) {
float wavelength = wave.z;
float wavespeed = wave.w;
float w = 1 / wavelength;
float phase = wavespeed / wavelength;
float2 d = normalize(wave.xy);
float f = w * (dot(d, p.xz) + phase * _Time.y);
steepness = clamp(0, wavelength / height, steepness);
float qa = steepness * height;
float wa = w * height;
float qwa = w * qa;
float3 displacement;
displacement.x = d.x * qa * cos(f);
displacement.z = d.y * qa * cos(f);
displacement.y = height * sin(f);
tangent.x += -d.x * d.y * qwa * sin(f);
tangent.y += d.y * wa * cos(f);
tangent.z += -d.y * d.y * qwa * sin(f);
binormal.x += -d.x * d.x * qwa * sin(f);
binormal.y += d.x * wa * cos(f);
binormal.z += -d.x * d.y * qwa * sin(f);
return displacement;
}
单个Gertner波的动画如下:
通常会用多个Gerstner波叠加来模拟水面波纹。下图是使用三个Gerstner波生成的波浪。
先凑合看下,这个波浪调起来挺麻烦的…
水面波光 Flow Map
之前做水面波光效果一般都是用一张或者几张法线叠加在一起。这次因为之后想做河流,所以添加了Flow Map来做这块。主要参考了CatlikeCoding的教程。
流型图(Flow Map),也常被称为矢量场图(Vector Field Map),本质上是一种基于矢量场平移法线贴图的着色技术,或者可以理解为一种UV动画,由VALVE的Vlachos在SIGGRAPH 2010上的talk Water Flow in Portal 2被大众所熟知。
以这张螺旋图作Flow Map为例。最终期望达到的效果是在主贴图基础上实现类似传送门的波纹动画。
首先,借助从Flow Map中读取的颜色值作为扭曲的方向向量(只需要xz平面的二维向量)。根据方向向量对MainTex的uv进行位移,形成扭曲效果。这个循环是通过对时间取小数点部分形成的——frac(_Time.y)
。
可以看见上图的循环基本呈“闪现”状态,要让过渡更加自然一些,可以加入一个平滑参数,对于这张图片而言,可以使用其Alpha通道达成此目的。
上图中,每个循环间的间隙依然很大,可以通过在时间错开并叠加两次来达成无缝衔接的目的。(呜呜,换了个颜色,是人鱼姬!
做到这里就可以实现近似波光粼粼的效果了,想要的波纹形状和高光可以通过参数和贴图调整。
水下颜色 Fog & Absorption
由于水会散射和吸收部分光的缘故,一般自然场景中越往深处水的颜色越深,浅处则更为透明。
这里我是根据Lux Water的启发,添加了Underwater Fog和Light Absorption两项。
Underwater Fog影响了水下可视度(透明度);Light Absorption则控制水对不同颜色光的吸收程度,换句话说决定了水下的颜色,通常水呈现偏蓝色,就是因为蓝色光被吸收得最少。
Light Absorption
水下颜色计算代码:
//underwater fog & color absorption
sampler2D _CameraDepthTexture, _WaterBackground; //_WaterBackground为GrabPass Texture
float4 _CameraDepthTexture_TexelSize;
float3 _WaterFogColor, _AbsorptionColor;
float _WaterFogDensity, _AbsorptionStrength, _AbsorptionDepth, _AbsorptionColorStrength;
float _Test;
float3 ColorBelowWater(float4 screenPos) {
float2 uv = (((screenPos.xy) ) / screenPos.w);
float backgroundDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
float surfaceDepth = UNITY_Z_0_FAR_FROM_CLIPSPACE(screenPos.z);
float depthDifference = backgroundDepth - surfaceDepth;
backgroundDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
depthDifference = backgroundDepth - surfaceDepth;
float3 backgroundColor = tex2D(_WaterBackground, uv).rgb;
float fogFactor = exp2(-_WaterFogDensity * depthDifference);
float3 ColorAbsorption = float3(1,1,1) - _AbsorptionColor;
float d = exp2(-depthDifference * _AbsorptionDepth);
d = lerp(1, d, _AbsorptionStrength);
ColorAbsorption = lerp(d, -ColorAbsorption, _AbsorptionColorStrength * (1.0 - d));
return ColorAbsorption * lerp(_WaterFogColor, backgroundColor, fogFactor);
}
焦散 Caustics
之前用过Unity内置组件projector
实现焦散,不过感觉在范围控制上并不是很方便,最好还是能跟水面shader放到一起嘛。
Alan Zucconi的这个[教程]给出了添加焦散贴图的方法,使用一张焦散贴图,在sample时用两套uv,uv的位移速度和tiling不同,然后叠加。另外还通过分离RGB通道形成虹光的效果,挺有趣的。
但是很显然焦散不该是一张浮在水面上的贴图,它应该投射(project)到水底,还会受水面波动的影响(需要考虑法线)。
获取最近surface depth
float backgroundDepth = Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uvscreen));
``
```c++
//caustics
float3 _SplitRGB;
sampler2D _CausticsTex;
float4 _Caustics1_ST, _Caustics2_ST;
float _Caustics1_Speed, _Caustics2_Speed, _CausticsTiling;
float3 CausticsColor(float2 mainuv) {
//uv sets
float2 uv = mainuv * _Caustics1_ST.xy + _Caustics1_ST.zw;
float2 uv2 = mainuv * _Caustics2_ST.xy + _Caustics2_ST.zw;
uv += _Caustics1_Speed * _Time.y;
uv2 += _Caustics2_Speed * _Time.y;
// RGB split
float s1 = _SplitRGB;
float r1 = tex2D(_CausticsTex, uv + float2(+s1, +s1)).r;
float g1 = tex2D(_CausticsTex, uv + float2(+s1, -s1)).g;
float b1 = tex2D(_CausticsTex, uv + float2(-s1, -s1)).b;
float s2 = _SplitRGB;
float r2 = tex2D(_CausticsTex, uv2 + float2(+s2, +s2)).r;
float g2 = tex2D(_CausticsTex, uv2 + float2(+s2, -s2)).g;
float b2 = tex2D(_CausticsTex, uv2 + float2(-s2, -s2)).b;
float3 caustics1 = float3(r1, g1, b1);
float3 caustics2 = float3(r2, g2, b2);
float3 col = min(caustics1, caustics2);
return col;
}