在Unity中实现基于粒子的水模拟(三:混合屏幕)
前言
经过了前面的纹理,我们就到了最后的混合到屏幕阶段了,这个阶段的逻辑不是很难,本来是要基于这篇文章的公式来往上套的,不过由于本人水平问题并没有看懂作者的具体含义,于是就大概的实现了一下,估计出入挺大的。
效果看图:
项目链接:注意是在其中的master分支中
一、着色算法介绍
我们将颜色分为两部分,也就是折射和反射。
1.折射
折射要有最好的效果应该是要根据当前像素执行SSR算法,也就是计算出折射法线后,在通过深度重建的世界坐标中,用最接近折射位置的那个像素的颜色值作为折射的颜色值,但是这种方式太昂贵,所以没有采用这种方式。
本文用的方式是最简单的折射采样方式,根据法线以及液体宽度指进行uv偏移来获取折射的颜色值(因为实在看不懂作者到底什么意思,所以就这样简单实现了 )。
2.反射
反射的计算比较简单,直接传递一个Cube贴图,然后根据反射方向进行纹理采样即可。
最后将两个颜色根据透光度进行混合就计算完成了。
二、准备纹理
1.获取纹理
在SRP中可以通过设置CommandBuffer的SetRenderTarget将宽度、法线、深度图专门用特定的ShaderTagId指定,这样可以渲染两次就得到所有3张纹理,因为法线可以和深度一次得到。
但是在Build-in中我没有找到指定深度图的方法,导致后面模糊时不能很好的将深度值提取出来模糊,不过因为我们这个计算方式只使用了xy的法线值,可以将深度值存储在法线纹理的b通道中。
裁剪空间获取深度的公式:
其中f是远裁剪面、n是近裁剪面、z是视角空间z值。
代码表示:
//获取深度的公式,这个深度值就是深度图中的深度值
//_ProjectionParams是Unity提供的数据,y是摄像机的近平面、z是远平面
//i.pos是裁剪空间坐标,其中的w值存储着视角空间的z值
depth = (_ProjectionParams.z * i.pos.w - _ProjectionParams.z * _ProjectionParams.y)/
((_ProjectionParams.z - _ProjectionParams.y) * i.pos.w)
//Unity会将深度图值转化,让近平面的精度更高,远平面精度可以低一点,
//因为浮点数在接近0时精度会上升
depth = 1- depth;
2.模糊纹理
模糊纹理使用的算法是双边滤波,这个算法可以让球体的边界混合起来,同时不会像高斯模糊一样让颜色值与周围混合,导致颜色不能准确对应该像素。
核心代码:
float CompareColor(float4 col1, float4 col2)
{
float l1 = LinearRgbToLuminance(col1.rgb);
float l2 = LinearRgbToLuminance(col2.rgb);
return smoothstep(_BilaterFilterFactor, 1.0, 1.0 - abs(l1 - l2));
}
float4 BilateralFilterFragment (Varyings input) : SV_TARGET{
float2 delta = _PostFxEffectSource_TexelSize.xy * _BlurRadius.xy;
//采集Normal的颜色值
float4 col = SAMPLE_TEXTURE2D(_PostFxEffectSource, sampler_linear_clamp, input.screenUV);
float4 col0a = SAMPLE_TEXTURE2D(_PostFxEffectSource, sampler_linear_clamp, input.screenUV - delta);
float4 col0b = SAMPLE_TEXTURE2D(_PostFxEffectSource, sampler_linear_clamp, input.screenUV + delta);
float4 col1a = SAMPLE_TEXTURE2D(_PostFxEffectSource, sampler_linear_clamp, input.screenUV - 2.0 * delta);
float4 col1b = SAMPLE_TEXTURE2D(_PostFxEffectSource, sampler_linear_clamp, input.screenUV + 2.0 * delta);
float4 col2a = SAMPLE_TEXTURE2D(_PostFxEffectSource, sampler_linear_clamp, input.screenUV - 3.0 * delta);
float4 col2b = SAMPLE_TEXTURE2D(_PostFxEffectSource, sampler_linear_clamp, input.screenUV + 3.0 * delta);
float w = 0.37004405286;
float w0a = CompareColor(col, col0a) * 0.31718061674;
float w0b = CompareColor(col, col0b) * 0.31718061674;
float w1a = CompareColor(col, col1a) * 0.19823788546;
float w1b = CompareColor(col, col1b) * 0.19823788546;
float w2a = CompareColor(col, col2a) * 0.11453744493;
float w2b = CompareColor(col, col2b) * 0.11453744493;
float3 result;
result = w * col.rgb;
result += w0a * col0a.rgb;
result += w0b * col0b.rgb;
result += w1a * col1a.rgb;
result += w1b * col1b.rgb;
result += w2a * col2a.rgb;
result += w2b * col2b.rgb;
result /= w + w0a + w0b + w1a + w1b + w2a + w2b;
return float4(result, 1);
}
双边滤波也需要像高斯模糊一样执行两次,对垂直以及水平进行模糊,具体实现可以参考这篇文章。
2.混合主纹理上
这部分就和正常的后处理流程一样了,具体后处理如何实现就不赘述了,这里只描写着色的核心代码。
首先第一步,进行深度对比,判断该像素是否需要进行液体着色,也就是是否被遮挡。
if(currentDepth >= waterDepth)
return float4(currentColor, 1); //返回原本像素颜色
获得折射以及反射的颜色值:
float3 viewDirection = normalize( _WorldSpaceCameraPos - worldPos );
float3 reflectDir = normalize(-viewDirection + 2 * waterNormal); //反射方向
//反射颜色
float3 specular =
SAMPLE_TEXTURECUBE_LOD( _WaterReflectCube,
sampler_WaterReflectCube, reflectDir, 0 ).rgb;
float2 ofssetUV = (-viewDirection -
0.2 * waterNormal).xy * waterWidth * 0.2 + input.screenUV;
//折射颜色
float3 refrColor =
SAMPLE_TEXTURE2D_LOD(_PostFxEffectSource,
sampler_linear_clamp, ofssetUV, 0).rgb;
//混合液体颜色,transLight是透光度
refrColor = lerp(refrColor * _WaterColor.rgb, refrColor, transLight);
按照大佬文章的公式:
其中R1和R2分别是水和空气的折射率。
代码:
float n_0 = pow( (_WaterData.y - _WaterData.x) / (_WaterData.y + _WaterData.x), 2 );
float fresnel =
( n_0 + (1 - n_0) * pow( 1 - dot(viewDirection, waterNormal), 5 ) )
* waterWidth;
float3 finalCol = refrColor * (1 - fresnel) + fresnel * specular;
总结
导致这个粒子水系列就正式完成了,我觉得这个实现的水不适合作为“水”,不过如果拿来做流体模拟的话还是可以的,因为有深度、法线图,用来实现牛奶等BSDF等渲染效果是很不错的,毕竟BSDF的一个难点是宽度计算,有了宽度其他计算就和BRDF没什么区别了。
1.现有问题
目前本场景的粒子并没有进行软粒子处理,如果有必要的话可以在渲染宽度时将深度图传入,在深度相近时进行透明,而且可以根据深度值进行Hi-z剔除,优化效果。
本项目的根据都是基于Unity的物理检测的,这个部分是损耗最大的部分,也是最容易出bug的部分,如果之后场景需要的话建议直接设置固定的流动方向,不进行真正的时时物理检测,刷新流动方向,这样CPU占用太大了。
理论上这些粒子着色都是要用ComputeShader写的,但是我在写这个系列时还不懂这些,在最近研究SRP时才知道有这个东西。
不过由于模拟时数据量太大了,这种直接将数据传递到顶点的方式说不定更适合物理模拟,不过之前实现的粒子系统可能就真的需要更新了,之后应该会更新Compute Shader进行剔除的粒子系统,更加全面的实现Unity新版粒子系统。