再加亿点点细节!Cocos 基于平面着色的 3D 水面渲染方案

一、开篇

自从上次写了《用实时反射 Shader 增强画面颜值》 后,不少开发者开始尝试用它来渲染水面,但效果都差强人意。

这是因为,水面除了反射,还有许多细节需要考虑。

在此之前,也有不少开发者提到过水面渲染的需求,也有不少开发者分享了一些关于水面渲染的 Shader,但更多集中在卡通着色方向。

水面渲染在 3D 项目中的需求是非常大的,毕竟地球表面水面占了约 70.8%,很难避开水面效果。

近期引擎团队的 youyou 大佬也在《Lake》中实现了水面渲染效果,包含平面反射、FSR、SSAO、TAA 等诸多实时渲染技术。

c52245e0964da633dc402dc66633c07f.png

但该项目基于 Cocos Creator 延迟渲染管线,对项目和设备要求较高,所以麒麟子专门准备了这个独立的水面效果分享,希望能够对大家有所帮助。

二、水面渲染流程

水面渲染技术非常多,不同段位的产品,对水面的要求不同。

毛星云的《真实感水体渲染技术总结》这篇文章中,通过对一些 3A 大作的水面渲染进行分析,列出了非常多的技术要点,有兴趣的朋友可以拜读。

水面渲染技术从简单到复杂来排序,可以分为以下三类:

  • 平面着色2fa1bd5a594fe239dcb570f7f50f2822.png

  • 顶点动画6aeda396a5940dee028082d6bfb9e6ed.gif

  • 流体模拟cb1546504a0650dceef336a0dfc9cee3.gif

本文实现的是基于平面着色的水面效果,虽然它并非高端效果,但却是大部分 3D 项目中采用的方案

基于平面着色的水面渲染主要涉及以下几个部分:

  • 反射

  • 折射

  • 水深效果

  • 水岸柔边

  • 动态天空盒

  • 法线图与光照

  • 岸边浪花

由于时间关系,法线图与光照岸边浪花暂未实现。

标准的渲染流程如下所示:

33a479ff43f097dffc0e3370b9363836.png

可以看出,如果要实现所有效果,至少需要绘制场景4次。

由于这里的深度图只是和折射搭配使用,8位精度足够用了,我们可以考虑借用折射图中的 Alpha 通道来存储深度信息。

优化后的流程图如下:

d2e29d47a9dc52249e04c7bb2e5a78ad.png

三、反射贴图渲染

麒麟子在《用实时反射 Shader 增强画面颜值》中已经完整地剖析了实时反射相关原理,在此就不再敷述,有需要了解的读者可直接点击查看。

这里主要讲一讲本 DEMO 中的实现步骤。

步骤1:使用代码新建一个 RenderTexture

步骤2:创建一个节点,添加摄像机组件,并将 clearFlagsclearColorvisibility 属性与主摄像机同步。

步骤3:设置反射摄像机的渲染优先级,确保比主摄像机先渲染。

步骤4:将新创建的 RenderTexture 赋值给此摄像机的 targetTexture 属性。

以上步骤的代码在 WaterPlane.ts 中,如下图所示:

bc3bf0af4d2d5a91871db45d24056b87.png

步骤5:lateUpdate 中同步主摄像机参数。

a2f5d1a009cff52b59be705c622e2a6d.png

步骤6:lateUpdate 中根据实时反射原理,动态计算摄像机关于主摄像机的镜像位置和旋转。

最终,渲染得到的 RenderTexture 如下:

990f6c00c603c3a322369dfa444e100f.jpeg

麒麟小贴士:
所有物体的材质,需要加入自定义裁剪面,裁剪掉水面以下的部分。
可以明显看到,上图中绿色物体的倒影,水面以下的部分是被裁剪掉了的。

四、折射贴图渲染

折射渲染的原理非常简单:

  • 渲染水平面以下的部分到 RenderTexture

  • 在水面渲染阶段使用噪声图进行扰动,以模拟出水面折射效果

折射渲染的流程与反射渲染大致相同,只有两个细小的差别:

  • 用于折射渲染的摄像机所有参数均与主摄像保持一致即可

  • 折射渲染阶段,物体被裁剪掉的是水面以上的部分

下面我们来看看,本 DEMO 中关于折射的实现步骤。

步骤1:使用代码新建一个 RenderTexture

步骤2:创建一个节点,添加摄像机组件,并将 clearFlagsclearColorvisibility 属性与主摄像机同步。

步骤3:设置反射摄像机的渲染优先级,确保比主摄像机先渲染。

步骤4:将新创建的 RenderTexture 赋值给此摄像机的 targetTexture 属性。

以上步骤的代码在 WaterPlane.ts 中,如下图所示:

21e4ee0930897814ebee6a4673d7aea1.png

麒麟小贴士: 注意红色线框部分,本 DEMO 中折射贴图的 Alpha 通道用于标记深度信息,所以需要确保 Alpha 通道的值为 255。

步骤5:lateUpdate 中同步主摄像机参数、位置、旋转等信息。

最终,渲染得到的 RenderTexture 如下:

24f8ec2a1315849989d1f0e7fe3c8a63.png

五、水面渲染

水面渲染主要利用了投影纹理技术,将顶点的投影坐标转化为 UV,对折射和反射贴图进行采样。

由于使用了折射贴图,我们的水面材质不需要开启 Alpha 混合。

折射渲染

步骤1:根据投影坐标计算出屏幕 UV。如下所示:

vec2 screenUV = v_screenPos.xy / v_screenPos.w * 0.5 + 0.5;

步骤2:采样折射贴图,可以得到如下渲染效果:

2599c0ca15ef339b344b80715ddd68b4.png

左边为正常渲染效果,右边为标记了折射内容的效果

步骤3:使用噪声图对折射进行扰动,可得到如下效果:

770e40e135bdf0f7263b083976a1edab.gif

反射渲染

步骤1:与折射渲染一样,根据投影坐标计算出屏幕 UV。

步骤2:采样反射贴图,可以得到如下渲染效果:

1469de2c656f6a73a59561a2a0720f60.png

步骤3:使用噪声图对反射进行扰动,可得到如下效果:

9841bab3bae69e1196206a015103eebb.gif

菲涅尔混合

菲涅尔的计算公式从玉兔的边缘光教程开始,到实时反射等场合,已经出现过很多次了。下面是核心代码:

51e483147f7f36f88998d8509dcd2054.png

折射可以视为水体本色,利用菲涅尔因子与反射内容混合,即可实现一个带折射和反射的水体效果。

伪代码如下:

finalColor = mix(refractionColor,reflectionColor,fresnel)

最终可以得到如下显示效果:

1a80e01700bcbc32e19651137c225236.gif

完整代码代码请查看项目中的 effect-water.effect 文件。

六、水深效果

从上面的动画中可以看出,虽然折射和反射效果都有了。但画风有些奇怪,完全没有水面的感觉。

这是水面没有深浅效果导致的。

我们来看看,如何获取深度信息,并根据深度信息实现水深效果。

获取深度信息

06e81bd46d6a4299bc3e7f48746b5516.png

从上图中,我们可以清晰地看到,靠近岸边的海水的颜色比远处海水的颜色透明得多。

产生这种现象的主要原因,就是基于视线方向的水体厚度不同。

什么叫基于视线方向的水体厚度,请看下图:55a927bbb81ae306e8660bfd7634b105.png

我们通常说的水体深度,是指在忽略视线因素的情况,水面到水底的高度差。

在不追究细节的情况下,我们可以简单地使用高度差来作为水的深度。

一种可能的伪代码如下:

depth = clamp((g_waterLevel - v_position.y) * depthScale,0.0,1.0);

其中 depthScale 是我们的深度缩放因子,可以用来调节比例尺问题,以及水体能见度线性衰减速率。

而基于视线方向的水体厚度,是指视线方向与水平面和水底交点的距离差。即图中 点 P1 到 点 P2 的距离。

下面我们来推导一下,使用基于视线方向水体的厚度来作为深度因子的公式。

许多朋友第一反应是解直线方程,但用空间向量的特性来求解会更容易。

为方便对照理解,再贴一次上面的图:83dc69c897d753e7dcaa75d98fd0aa20.png

设观察方向为 viewDir,厚度为 depth 则有:

P1 + viewDir * depth = P2

分拆为分量运算可得:

P1.x + viewDir.x * depth = P2.x

P1.y + viewDir.y * depth = P2.y

P1.z + viewDir.z * depth = P2.z

可推导出:

depth = (P2.y - P1.y) / viewDir.y

由此可得如下计算公式:

vec3 viewDir = normalize(v_position.xyz - cc_cameraPos.xyz);

float depth = (v_position.y - g_waterLevel) / viewDir.y

depth = clamp(depth * depthScale,0.0,1.0);

比起直接使用水体深度来说,多了一次求 viewDir 单位向量的运算,以及一次除以 viewDir.y 运算。

在非极端情况下,多出的这一点纯逻辑运算在 GPU 上是可以忽略不计的,可以放心使用。

将上述公式添加到渲染对象的 Shader 中,并在折射渲染阶段启用,将结果存入 Alpha 通道即可。

项目中的 Shader 代码如下图所示:

e4a40fd83ed25e921c6f2d3731ebfa72.png

最终得到的深度信息如下:

cbd3bc8c999ca648105e8377a1219a50.png

深度混合

有了上面的深度信息,我们只需要在计算出折射颜色后,再用深度信息与水底颜色混合即可。Shader 代码如下图所示:

2c345325cda7bb894cbcb9178aba5de1.png

由于水体的可见度是非线性的,所以对 diffDepth 使用了 pow 函数,这个 power 参数默认是 2.0。

最终可以得到如下效果:

3dc3574649ba35df8f4e1d7462196ea6.gif

六、水岸柔边

f267fc19475bf3f74267bdc24bd15477.png

当我们把摄像机拉近,观察水面与物体交接处的时候,可以明显看到一条清晰的边界。

这条边界在反射越强的时候越明显,使我们的水面效果大打折扣。

好在我们已经有了深度信息,可以根据深度来判断出哪里靠近岸边,并修改菲涅尔因子,使反射越靠近岸边的时候越弱即可。

核心代码如下:

b52aabd5531fcfb37a5506e847d5faeb.png

最终可以实现在全反射的情况下,水面与岸边依然平滑过渡。效果如下图所示:

a89a8f5ecc2f06bd7d023f88e91c46dd.png

再来一张远视角的图:

edd62a9cbaf754a03f887ddd5d34db10.png

七、动态天空盒

ff667b70f376262865433613621e2fe5.gif

为了增强氛围感,DEMO 中使用了动态天空盒。

这是一个特别简单的高效的动态天空盒方案,仅使用了一个双层纹理混合的半球模型,调节两张纹理的水平方向流动速度即可。

53e715aba90e4a294463544f7809354a.png 79dbdf361ef52132652325d3437d1f77.png

八、关于DEMO

所有效果参数均可调节,如下图所示:

71afd9e8b267f87be6a0f65498cd3052.png

点击文末【阅读原文】跳转至 Cocos Store 下载 DEMO,地址:https://store.cocos.com/app/detail/3645

本文作者麒麟子,关注麒麟子随笔,

获得更多图形渲染知识!

往期精彩

d44f31d5138cc5a9e86d3701c79958d4.png

1ee0e4f61888561005832895fad6be85.png

c0e15911fc6479bb2305f790a9f35b03.png

0d8ded4355c3c6bc74c3ddeaa6158442.gif

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Cocos Creator 中,你可以使用自定义材质来实现顶点着色渲染。顶点着色渲染是一种在渲染管线中对模型的每个顶点进行颜色计算的技术,通过改变顶点的颜色来实现特定效果。下面是一个简单的示例代码: ```javascript // 在节点上添加一个脚本组件 VertexColor.js cc.Class({ extends: cc.Component, properties: { color: cc.Color.WHITE, // 自定义颜色 }, // 在渲染组件的 updateMaterial 方法中设置材质的属性 updateMaterial: function() { var material = this.getComponent(cc.RenderComponent).getMaterial(0); // 获取渲染组件的材质 material.setProperty('u_color', this.color); // 设置自定义颜色属性 }, // 在 onLoad 方法中注册 updateMaterial 方法到渲染组件的 updateMaterial 方法中 onLoad: function() { this.getComponent(cc.RenderComponent).updateMaterial = this.updateMaterial.bind(this); }, }); ``` 在上述代码中,我们假设节点上有一个渲染组件(如 Sprite、Mesh 等),我们通过自定义脚本组件 `VertexColor.js` 来实现顶点着色渲染。脚本组件中定义了一个 `color` 属性,用于设置自定义颜色。在 `updateMaterial` 方法中,我们获取渲染组件的材质,并设置自定义颜色属性。最后,在 `onLoad` 方法中将 `updateMaterial` 方法注册到渲染组件的 `updateMaterial` 方法中,以便在每帧更新时调用。 然后,你可以在 Cocos Creator 编辑器中将该脚本组件 `VertexColor.js` 添加到需要进行顶点着色渲染的节点上。在脚本组件的属性面板中,可以设置自定义的颜色值。 需要注意的是,顶点着色渲染需要使用支持顶点着色的材质和渲染组件,例如使用自定义的 Shader 或在材质中设置相应的 Uniform 属性等。具体的实现方式和效果可以根据你的需求和场景进行自定义调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值