山海鲸可视化

GIS融合之路(七)-Cesium实现夜空月亮星星渲染

系列传送门:
山海鲸可视化:GIS 融合之路(一)技术选型 CesiumJS/loaders.gl/iTowns?
山海鲸可视化:GIS 融合之路(二)CesiumJS 和 ThreeJS 深度缓冲区整合
山海鲸可视化:GIS 融合之路(三)CesiumJS 和 ThreeJS 相机同步
山海鲸可视化:GIS 融合之路(四)如何用 CesiumJS 做出 Cesium For Unreal 的效果
山海鲸可视化:GIS 融合之路(五)给 CesiumJS 加上体积云(Volumetric Cloud)和高度雾(Height Fog)
山海鲸可视化:GIS 融合之路(六)-Cesium 的雨雪风雷电效果

本来没打算写到第七篇,但最近山海鲸的 GIS 夜景被同事们吐槽的厉害,说 GIS 白天很不错,但是夜景效果很一般。人嘛,一定要知耻而后勇。在严词拒绝了同事们升级夜空的诉求之后,便默默地开始了夜空渲染的研究。

开始之前我们还是有必要先回顾一下山海鲸中 Cesium 整合天空各个元素的效果:
白天效果.webp
白天效果

夕阳效果.webp
夕阳效果

要知道,山海鲸是完全基于 WebGL 的渲染引擎,特效全靠手搓,而目前白天完全可以比肩游戏引擎的视觉能力了(这里必须吐槽一下那些套壳 UE 的同行,明明底层就用 UE,竟然好意思说自己比 UE 好)。

好了,咱们正式进入正文,话说在之前的技术调研中,写白天大气散射和体积云渲染的文章可以说是多如牛毛,具体可以参考我前面的系列文章,但写夜空渲染的文章竟然一个没有。同时 UE 和 Unity 似乎也对夜空渲染不感兴趣,在官方的天空中都没有整合夜空的逻辑。UE 新版大气散射的介绍视频中倒是提了一句“如果要做夜空只需要整合一张星空贴图即可”,Unity 的官方 Roadmap 中也有提到说很难用基于物理的方式来实现夜空,因此打算提供一些艺术家向的设置来实现夜空的渲染。真正有意义的也只有几篇关于夜空渲染的论文,却也没有见到什么像样的代码实现。

在开始之前,我们先讨论一个问题,为什么夜空的渲染文章这么少呢?我想主要有以下几个方面的原因:

  1. 需求不够大。对于传统游戏来说,日景要比夜景更常用,毕竟晚上都不太看得清,只能作为辅助玩法,不能作为主线,夜晚的天空盒子也就够用了;
  2. 基于物理渲染的夜景大概率是全黑的,除了星星和月亮,没有太多细节;
  3. 目前的夜空拿白天的天空改改艺术向的一些设置就能实现了。

也就是说费劲巴拉的真的按照物理方式去渲染夜空可能还不如改改艺术向设置让人觉得真实,还更卡。这里也可见计算机图形学第一定律在发挥作用了:如果它看上去是对的,那么它就是对的。

那山海鲸还做不做夜空渲染呢?那肯定还是要做的。我们的产品理念是要帮用户把复杂的技术细节屏蔽掉,我们的用户和游戏引擎的用户不一样,游戏引擎可以要求用户去改亮度,改大气散射粒子浓度,而山海鲸则希望客户什么都不改就能用。因此我们还是得从目前仅有的论文开始看起:

对于渲染来说,最重要的是光源。那么我们先看下夜晚的光源都有些什么,这里我们贴一下论文中的天空光源强度和光源类型:
光源类型.webp
earth.webp
光.webp
从左至右分别是黄道光、星光和气辉

可以看到,满月相对其他夜空光源来说是具有统治地位的,而黄道光、星光和气辉逐步降低,再往后我都不知道是啥了。这里我们要发挥一下图形学神功的精髓——大胆而无耻的假设,除了月光,其他就先当做不存在。

好了做完的这个假设,问题变的简单了。夜空和日空是很类似的,单光源结合大气散射即可,是不是可以直接复用日空的代码,把亮度调小 10 的六次方即可呢?显然这里还有两个细节问题值得探讨:

  1. 夜空亮度调小 10 的六次方,那么基本上就是纯黑了,那为什么人类夜晚能看到夜空呢?这里必须引入一个新的概念就是人眼的自动适应,本质是人眼在暗场景中的自动调高曝光的功能——这个功能在 UE 和我们山海鲸中都有实现。但如果客户没有开自动曝光,有必要主动调亮夜空亮度,做一个艺术向的处理,不能直接用 10 的六次方的差异。
  2. 太阳和月亮的尺寸差异还有距离地球的远近差异极大。太阳光我们可以假设成为平行光,但月亮假设为平行光就有些不妥了。因此太阳光在大气层内每一个位置上都可以认为是方向不变的,而月亮光是不一样的。但这个显然大幅增加的 Shader 的适配成本,相当于要为月亮再单独写一套逻辑,这里我们要再次发挥一下发挥一下图形学神功的精髓——假设月光也是平行光。

做了这么多无耻的假设,我们终于可以用极小的成本适配夜空了。但目前做的本质上和艺术向的设置没有区别,只是调暗主光源光强即可,当然我们需要同步适配大气散射和高度雾的光强。调完之后,我们看下现在的效果:
夜空.webp
看着还算可以接受夜空效果

我们把体积云也调整好光强后叠加上来:
体积云夜空.webp
加上体积云的夜空

现在的夜空基本已经到了可以勉强接受的范围,但没有月亮和星星的夜空是没有灵魂的,那么我们先参照 UE 视频中的说法:在夜空的基础上叠加星空 Cubemap,同时我们用一个remap 函数来控制星空的稠密程度:

1
nightLuminance += smoothstep(vec3(1. - starry),vec3(1.),starLuminance);

叠加后的效果如下:
叠加星空.webp
叠加星空后的效果

最后我们再实现月亮,月光的光照其实就是一个简单的球体,如下:
球体.webp

我们这里为了整合在天空中一起渲染,因此不能单独用一个球来贴图。我们通过RayIntercept 函数来处理球体的渲染:具体是先计算好 Ray 和月球的交点后,再用交点计算当前点位的法线,最后通过控制 incoming light 的方向来实现月相效果。为了能让大家更好的观察月亮,我们还提供了一个放大月球的功能,效果如下:
月亮升起.webp
月亮升起

月相变化.webp
月相变化

环形山.webp
月球放大,可以看到通过法线实现的环形山效果

到这一步,夜空基本就已经实现完了。最后还有一个小细节,那就是实际上在夕阳的时候,我们就已经能够看到星空了,不是一定要等到晚上才有。因此我们在太阳接近落山的时候,就选择开始叠加星空贴图:
星空贴图.webp

这样可以进一步增强清晨和黄昏的视觉层次感。到此,整个山海鲸的夜空实现就已经全部完成了,而空气透视和 Cesium 系统已经自动叠加,因此不需要再做更多的处理就可以很好的和GIS 系统融合在了一起。

当然还有很多工作值得进一步来做,例如:

  1. 取消月光是平行光的假设;
  2. 气辉和极光。其实山海鲸第一版的动态天空就有极光,不过确实对于数字孪生来说不是特别适合,毕竟北极不做数字孪生,气辉倒是后续可以加上进行设置;
  3. 添加夜景中城市灯光和空气污染的影响,这样在城市场景中会让整个画面更加统一。