我用AI把高质量的体积云搬进了Cesium

前言

作为一名webGIS开发者,CesiumJS简陋的场景特效一直被诟病,想要实现类似于UE5那样的高质量体积云和大气散射,在AI时代纯靠自己手搓不现实。

我前前后后经历了多轮迭代,自认为算是成功移植takram-design-engineering/three-geospatial的场景特效,性能和视觉效果也达到了平衡

  • 体积云;
  • 大气散射;
  • 地表云影;
  • 体积光;
  • 透镜炫光(Lens Flare);
  • 云层运动和时间重建

先看效果

image-20260804102525111

日落

image-20260804102733819

云底

image-20260804102749341

云上

image-20260804102843854

体积光

image-20260804102936963

透镜炫光

一些基础概念

Ray Marching

体积云没有固定的几何表面,不能像普通模型那样直接画三角形。

程序需要从相机发射一条射线,然后沿着射线一步一步向前检查:

第 1 步:这里有没有云?
第 2 步:这里的云有多浓?
第 3 步:太阳能照到这里吗?
第 4 步:累计颜色和透明度
……

这种方式叫 Ray Marching,可以翻译为“射线步进”。

步数越多:

  • 云越细腻;
  • 光照越准确;
  • 性能消耗越高。

所以体积云的开发始终是在画质和性能之间寻找平衡。

HDR 和 Tone Mapping

现实中的太阳非常亮,但普通屏幕只能显示有限的亮度。

HDR 渲染允许程序先保存超出屏幕范围的亮度,比如:

普通地面亮度:1
白云亮度:5
太阳亮度:100

最后再通过 Tone Mapping,把这些亮度压缩到屏幕可以显示的范围。

可以把 Tone Mapping 理解成相机拍照后的“显影过程”。

一个重要原则是:

云、大气、太阳和 Lens Flare 应该先在 HDR 中合成,最后只进行一次 Tone Mapping。

如果中途已经把颜色处理过一次,最后 Cesium 又处理一次,就可能出现:

  • 场景发白;
  • 云偏灰;
  • 高光丢失;
  • 调曝光也无法恢复正常。

TAAU

TAAU 是整个项目里很重要、也比较容易出问题的一部分。

它的全称可以理解为:

利用连续多帧画面,把低分辨率结果恢复成高分辨率结果。

1. 为什么不直接全分辨率画云?

假设屏幕分辨率是:

1920 × 1080

这意味着每一帧大约有两百万个像素。

如果每个像素需要走几百次 Ray Marching,计算量会非常大。

所以原版会先用较低分辨率计算云,例如宽和高都缩小到四分之一:

完整分辨率:1920 × 1080
云分辨率:  480 × 270

像素数量会大幅下降,性能也会明显提高。

2. 低分辨率的云为什么不会很模糊?

因为原版不会简单地把低分辨率图片直接放大。

它使用一个 4×4 的采样顺序,每一帧只计算其中一部分像素:

第 1 帧:计算第 1 组像素
第 2 帧:计算第 2 组像素
……
第 16 帧:计算第 16 组像素

经过 16 帧后,完整屏幕上的像素都被覆盖一次。

然后把当前帧和前面几帧的结果组合起来,逐渐得到稳定的高分辨率云。

这就是项目中提到的:

4×4 Bayer
16 帧覆盖
TAAU

3. TAAU 为什么会出现拖影和闪烁?

问题在于相机和场景会移动。

例如上一帧某个像素看到的是云:

上一帧:这个位置有云

但相机移动后,当前帧同一个屏幕位置可能已经变成山体:

当前帧:这个位置是山

如果程序继续使用上一帧的云,就会出现:

  • 丝状闪烁;
  • 黑色细线;
  • 云拖在山体上;
  • 云边缘抖动;
  • 地形加载时画面撕裂。

因此,TAAU 不能只保存上一帧的颜色,还要保存:

  • 云距离相机多远;
  • 地形距离相机多远;
  • 这份历史是否有效;
  • 当前像素移动到了哪里。

当这些数据发生明显变化时,就要放弃旧历史,重新计算。

CSM 和 Beer Shadow Map

Shadow Map 可以理解为“从太阳的位置看场景,记录哪些地方被挡住”。

由于摄像机附近和远处需要的阴影精度不同,CSM 会把视野分成多个距离区间,每个区间使用一张阴影贴图。这样既能保证近处阴影清晰,又能覆盖较远的区域。

普通阴影通常只关心“被挡住”或“没有被挡住”,但云是半透明的,不能简单地得到一块纯黑阴影。Beer Shadow Map 还需要记录光在云中穿过了多远、被吸收了多少,最后得到有层次的云影透射率。

three-geospatial渲染管线

three-geospatial的完整渲染管线代码量很大,靠自己来阅读不太现实,还不一定看得懂,且阅读源码时发现并未封装相关API(不得不吐槽小日子的代码写的乱糟糟,注释也没几个)

借助AI可以高效的重新梳理一遍渲染管线

1. 云的形状从哪里来?

原版不是手工建模每一朵云,而是使用多张纹理共同生成云。

主要包括:

纹理 作用 容易理解的比喻
Weather 决定哪里有云 天气预报图
Shape 决定云的大轮廓 云的毛坯
Shape Detail 增加小尺度细节 雕刻云边缘
Turbulence 扭曲云的形状 风吹动云
STBN 帮助降低噪点 更均匀的随机采样表

最终的云密度大致通过下面的过程产生:

天气分布
  → 云的大形状
  → 小细节侵蚀
  → 覆盖率调整
  → 最终云密度

每个云层还有自己的:

  • 云底高度;
  • 云层厚度;
  • 覆盖率;
  • 密度;
  • 风速;
  • 使用的纹理通道。

原版最多支持四层云,默认启用低、中、高三层。


2. 云的亮面和暗面是怎么来的?

当程序沿着相机射线发现云之后,还需要向太阳方向再发射一条射线。

它会检查:

当前位置到太阳之间,还有多少云?

如果中间没有其他云,当前位置就比较亮。

如果中间有很多云,太阳光会被吸收,当前位置就比较暗。

但是云的暗面不能直接变成黑色,因为现实中的光会在云内部多次反射。

原版还计算了:

  • 云内部多次散射;
  • 地面的反射光;
  • 云对太阳的遮挡;
  • 地球本身对太阳的遮挡;
  • 云边缘的银边效果。

这些计算共同决定了云是否有立体感。

原版完整渲染顺序

原版的核心思路可以简化为:

1. 画地形、建筑和模型
2. 记录场景深度
3. 计算云的太阳阴影
4. 低分辨率计算当前帧云
5. 使用 TAAU 恢复完整分辨率
6. 把云影应用到地面
7. 计算天空和地表的大气散射
8. 把云覆盖到场景上
9. 添加太阳圆盘和 Lens Flare
10. 进行 Tone Mapping
11. 输出最终画面

更接近实际工程的版本是:

HDR Scene Color
       ↓
CSM / Beer Shadow Map
       ↓
Cloud Current Pass
       ↓
TAA / TAAU
       ↓
Scene Cloud Shadow
       ↓
Aerial Perspective
       ↓
Cloud Overlay
       ↓
Sun Disc
       ↓
Lens Flare
       ↓
Tone Mapping / FXAA

我给 Push AI 的全部任务是怎样逐步变化的

我的提示词并不是一次写完的,而是根据每次运行结果不断调整。(个人感受:gpt更能懂人话,GLM需要详细解释步骤和方法)

阶段 我给 Push AI 的主要任务 得到的结果
1 对比当前项目和 three-geospatial,检查哪些功能没实现 得到缺失功能清单
2 制定完整复刻原版的实施计划 确定 Cesium 1.139、WebGL2、HDR、TAAU、BSM 等方案
3 按照计划开始实现 建立 Vite、TypeScript、Cesium 后端和云管线
4 继续完成完整计划 补齐云层、纹理、阴影和时间重建
5 新管线中云、地形和大气遮挡错误 重新把“深度契约”设为最高优先级
6 大气、云影和体积光没有出现 重排为“深度→大气→云影→体积光”
7 TAAU 开启后出现丝状闪烁 增加历史深度、有效性和拒绝规则
8 云和大气偏灰暗 对比原版光度单位和 HDR 合成顺序
9 调曝光仍无法解决发白问题 确认问题不只是曝光,而是重复颜色处理
10 Cesium 报 Tone Mapper 值无效 改用 Cesium 支持的 Tone Mapper 枚举
11 大气缩放到全球时变成小圆 检查大气壳、地球半径和世界坐标
12 云影过黑 只衰减太阳直射,并增加 GUI 强度控制
13 缺少太阳、Lens Flare 和云动画 增加太阳圆盘、HDR Lens Flare 和风速动画
14 再次出现 Tone Mapper 错误 检查依赖版本和初始化顺序

一个很重要的事情

AI 开发并不是给出一个大提示词,然后等待它一次完成。真正有效的是根据运行结果不断缩小问题范围。

AI 发挥了什么作用?

1. 阅读和对比大量源码

它可以同时搜索:

  • 原版 TypeScript;
  • 原版 Shader;
  • Cesium Shader;
  • 当前移植代码;
  • Cesium 私有渲染流程;
  • 测试和资源引用。

这比人工逐个文件查找快很多。

2. 把大任务拆成小任务

例如“修复体积云”太宽泛。

更有效的提示词是:

只检查场景深度读取。
不要调整云颜色。
不要修改大气参数。
找出 Terrain 未加载时云消失的原因。
增加对应的数值测试。

任务范围越明确,AI 越不容易同时修改无关模块。

3. 建立可以重复执行的验证

这次移植没有只依赖“肉眼看起来差不多”。每次修改后都会执行类型检查、构建、单元测试和 GPU 中间纹理检查。

在不使用截图验证的情况下,也可以直接读取云颜色、场景深度、Beer Shadow Map、大气和 Lens Flare 等中间结果,检查是否出现全零、NaN、WebGL error 或 Shader 编译错误。

AI 最大的价值之一,是把一次性的人工排查整理成以后还能重复运行的测试。

一些个人感受

AI每天都在进步的时代,个人知识储备的速度已经完全无法相提并论了,以前可能重要的是会多少门开发语言,会多少技术栈,现在在AI面前完全是降维打击。

这很容易带来职业焦虑,但在真实项目中,AI 仍然需要人来确定目标、划定边界并判断结果。它可以很快生成 GLSL,也可以同时修改几十个文件,但它并不知道最终画面是否符合需求,更不会自动知道某次“修复”是不是用调暗参数掩盖了深度错误。

对我来说,这次项目最大的感受是:

具体语法可以随时让 AI 帮忙,但基础原理、问题拆分和结果判断反而比以前更重要。

我不一定需要记住所有 GLSL 函数,却需要知道:

  • 这个问题可能出在深度、历史还是光照;
  • Tone Mapping 应该在哪个阶段执行;
  • TAAU 为什么会产生拖影;
  • 云影应该影响整个场景,还是只影响太阳直射;
  • AI 给出的结果是真正修复了问题,还是只是让问题暂时不明显。

AI 没有让图形学基础变得不重要。恰恰相反,它让“理解原理并提出正确问题”变得更有价值。

Push AI 大幅降低了阅读源码、生成测试和反复修改的成本,但整个项目能够推进,依靠的仍然是一个循环:

先理解问题
  → 给出明确边界
  → 让 AI 实现或分析
  → 用测试和画面验证
  → 根据结果继续缩小范围

对 WebGIS 开发者来说,我们不必一开始就掌握所有图形学公式。先知道每个阶段“在画面里负责什么”,再逐步理解深度、Ray Marching、TAAU、阴影和大气散射,就已经可以开始尝试很多过去看起来门槛很高的效果了。