如何拥有一个“私人泳池”

如何拥有一个“私人泳池”

【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目,帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果,并探索将其应用到自己项目的可行性。很多时候,我们并不知道自己想要什么,直到某一天我们遇到了它。

更多精彩内容请关注:lab.uwa4d.com


导读

水面效果是很多游戏场景中都会选择构建的地形,之前的博物纳新栏目中,我们介绍过海洋场景和水波效果的开源库:

而本文将介绍的Wave Equation这个项目中的水面效果,更像一个清澈的海岛无边泳池。接下来小编就来和大家一起学习如何搭建一个高效且华丽的“私人泳池”。

请输入图片描述
请输入图片描述

项目链接

Wave Equation
https://lab.uwa4d.com/lab/5b5a5467d7f10a201fe4bbfb

作者博客
http://www.lsngo.net/?tubwhs=nccuf1&kapgzy=qrdem2


项目介绍

该项目实现了诸多十分逼真的效果,小编节选其中水面波动效果、焦散和体积光效果重点介绍,其余效果如:水面反射、阴影和BlinnPhong模型通过截图进行展示,有兴趣的读者可以前往开源库下载和研究。

第一部分:水面网格及波动效果

请输入图片描述
水面波动效果

LiquidSimulator.cs用于控制生成水面网格,项目运行后会在当前GameObject中添加Mesh组件与水面材质,并添加一个摄像机用于实现反射效果(ReflectCamera)。

请输入图片描述
水面网格

LiquidSimulator的Inspector面板中输入参数:水体长度、宽度、深度、网络单元格大小、粘性阻尼大小、波速、力度系数。这些参数在经过波速、时间等约束条件检测后传递给LiquidSampleCamera.cs用于构建摄像机(LiquidSampleCamera)。

LiquidSampleCamera将水面作为近裁截面,生成名为[Cur]的RenderTexture作为当前摄像机的targetTexture。

当有物体(带有LiquidSampleObject组件)与水面交互时,调用DrawRenderer函数(如果是鼠标点击交互调用ForceDrawMesh)通过Force.shader,使用CommandBuffer绘制Mesh并保存在[Cur]RT中。

交互物体,例如正方体,没有完全浸入水面时,正面被摄像机裁剪,背面在Force.shader中通过第一个pass,根据浸入深度,填充R通道和G通道,从而模拟不同的波动力度。

请输入图片描述
方块上表面没有浸入水面时的 [Cur]RT

完全没入水面时,背面通过第一个Pass,正面通过第二个Pass。正面会将背面覆盖,形成黑色的纹理。
请输入图片描述
方块上表面浸入水面时的 [Cur]RT ,采用负色效果查看

当运行OnRenderImage时,将当前RT([Cur])、传入使用WaveEquationGen.shader的材质中运算并保存在名为[HeightMap]的RT中。

再将当前[HeightMap]传入使用NormalGen.shader的材质中运算并保存在名为[NormalMap]的RT中。

用一张名为[Pre]的RT保存当前RT。

运行OnPostRender,重置[Cur]为黑色。

水面网格的波动效果使用含粘性阻尼的二维波动方程的近似导数进行计算(公式推导可以参考《3D游戏与计算机图形学中的数学方法》第3版第15章第1节的内容)。公式如下:


含粘性阻尼的二维波动方程的近似导数

[Pre]为上一个时间段的位移RT、[Cur]为当前时间段的位移RT。

Shader中的计算如下:

 1float cur = _WaveParams.x*DecodeHeight(tex2D(_MainTex, i.uv));
 2
 3float rg = _WaveParams.z*(
 4  DecodeHeight(tex2D(_MainTex, i.uv + float2(_WaveParams.w, 0)))
 5  + DecodeHeight(tex2D(_MainTex, i.uv + float2(-_WaveParams.w, 0)))
 6  + DecodeHeight(tex2D(_MainTex, i.uv + float2(0, _WaveParams.w)))
 7  + DecodeHeight(tex2D(_MainTex, i.uv + float2(0, -_WaveParams.w))));
 8
 9float pre = _WaveParams.y*DecodeHeight(tex2D(_PreTex, i.uv));
10
11cur += rg + pre;

水面网格的材质Shader会对[HeightMap]、[NormalMap]、以及后文中提到的其他RT进行采样,计算顶点位置以及相应颜色。

请输入图片描述
[HeightMap]RT

请输入图片描述
[NormalMap]RT

第二部分:焦散

请输入图片描述
焦散效果图

CausticRenderer组件执行开始时创建一个摄像机,并新建一个顶点较多的网格。

在水体网格中计算得到的[NormalMap]RT,以及其他一些参数传递给使用Caustic.shader的材质进行运算。

该着色器的主要原理是根据法线扰动来计算顶点的偏移,再通过计算偏移后的每个三角面的面积和原始三角面的面积比值,如果面积变大说明各顶点离的更远了,则处于焦散的暗部,面积变小则说明顶点相距更近了,则处于焦散的亮部。

处于焦散亮部顶点更接近白色,从而得到一张名为[Caustic]的RT,用于后续参加水中物体BlinnPhong模型的绘制。

请输入图片描述

此刻的[NormalMap]RT(图左, 128✖️128)传递给Caustic.shader进行采样,经过运算后得到[Caustic]RT(图右, 512*512)

Caustic.shader部分计算如下:

 1v2f vert (appdata_full v)
 2{
 3  v2f o;
 4  float3 normal = UnpackNormal(tex2Dlod(_LiquidNormalMap,float4(v.texcoord.xy,0,0)));
 5  o.oldPos = v.vertex.xz;
 6  v.vertex.xz += normal.xy*_Refract;
 7  o.newPos = v.vertex.xz;
 8  o.vertex = UnityObjectToClipPos(v.vertex);
 9   return o;
10}
1fixed4 frag (v2f i) : SV_Target
2{
3  float oldArea = length(ddx(i.oldPos)) * length(ddy(i.oldPos));
4  float newArea = length(ddx(i.newPos)) * length(ddy(i.newPos));
5  //发生折射后与未折射时三角面的面积比
6  float area = (oldArea / newArea) * 0.5;
7  return float4(area, area, area, 1);
8}

第三部分:体积光

请输入图片描述
体积光效果图

通过添加水体Mesh,并使用WaterBody.shader进行渲染。其中关于光线追踪和体积光效果颜色的计算如下:

 1//传入水体包围盒信息,用于计算水底光线追踪的范围
 2Vector3 boundsMin = new Vector3(transform.position.x - width * 0.5f,         
 3  transform.position.y - depth,
 4  transform.position.z - length * 0.5f);
 5Vector3 boundsMax = new Vector3(transform.position.x + width * 0.5f, 
 6  transform.position.y,
 7  transform.position.z + length * 0.5f);
 8
 9//传入水体平面用于获取水体表面法线,以计算折射光线
10Vector4 plane = new Vector4(0, 1, 0, Vector3.Dot(new Vector3(0, 1, 0), transform.position));
 1float3 boundSize = _BoundsMax - _BoundsMin;
 2float delta = max(boundSize.x, max(boundSize.y, boundSize.z)) / RAY_STEP;
 3float coldelta = 1.0 / RAY_STEP * _LightIntensity;
 4float3 viewDir = normalize(-UnityWorldSpaceViewDir(i.worldPos));
 5float4 col = float4(0, 0, 0, 0);
 6for (float k = 0; k < RAY_STEP; k++) {
 7  float3 wp = i.worldPos + viewDir * k * delta;
 8  float atten = SampleShadow(wp);
 9  float clipv = ClipInBounds(wp);
10  float ndl = SampleWaterNormalDotLightDir(wp);
11  col += _FilterColor * coldelta * atten * clipv*ndl;
12}
13return col;

第四部分:其余效果

1.水面反射效果

请输入图片描述
开启水面反射效果图

请输入图片描述
关闭水面反射效果图

2.阴影效果

请输入图片描述
开启阴影效果图

请输入图片描述
关闭阴影效果图

3.BlinnPhong光照模型

项目构建了BlinnPhong光照模型用于水下物体着色,例如:处在更深的水面,则颜色会更蓝;可以作为焦散接受物体。
(关于这部分的内容,建议阅读《Unity Shader入门精要》第6章第5节,在 Unity Shader中实现高光反射光照模型 进行学习)

请输入图片描述
浅水区物体颜色效果图

请输入图片描述
深水区物体颜色效果图

请输入图片描述
绘制焦散效果图


性能测评

以上为对该项目的实现原理和效果的简单介绍,下面我们通过UWA GOT Online来对其在不同档次的移动设备上进行性能测试与分析。

请输入图片描述
小米5S测试数据

针对中端机型,选择小米5S机型进行测试,在关闭多线程渲染的情况下:

  • 开启全部效果时FPS均值19帧
  • 关闭体积光后FPS均值50帧
  • 关闭体积光关闭焦散后FPS均值55帧

请输入图片描述
小米8测试数据

针对高端机型,选择小米8机型进行测试,在关闭多线程渲染的情况下:

  • 开启全部效果时FPS均值37帧
  • 不开启体积光、开启焦散,FPS均值58帧

请输入图片描述
红米4X测试数据

针对低端机型,选择红米4X机型进行测试:

  • 关闭多线程渲染、不开启体积光、开启焦散FPS均值27帧
  • 开启多线程渲染后,开启全部效果FPS均值19帧
  • 开启多线程渲染后,关闭体积光FPS均值27帧

通过多组实验对照发现:开启体积光效果后,FPS帧率下降较大。我们选择小米8机型的实验数据,当开启体积光时,其中Graphics.PresentAndSync函数的CPU耗时较多,如下图所示:

请输入图片描述

Graphics.PresentAndSync在此处较高,说明当前设备上的GPU端性能压力较大,致使CPU端传输数据等待时间较长。所以,建议大家慎重选择在中低端配置的机器运行时是否要开启体积光效果。

除此之外的效果性能均比较优秀,推荐大家在实际开发项目根据项目需求使用。


快用UWA Lab合辑Mark好项目!

请输入图片描述

今天的推荐就到这儿啦,或者它可直接使用,或者它需要您的润色,或者它启发了您的思路......

请不要吝啬您的点赞和转发,让我们知道我们在做对的事。当然如果您可以留言给出宝贵的意见,我们会越做越好。