news 2026/9/3 19:42:03

Cesium全球动态光照体积云渲染:Ray Marching实战与性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cesium全球动态光照体积云渲染:Ray Marching实战与性能优化

简介:一套基于体渲染的Cesium全球体积云实现代码,面向需要在大范围三维地球场景中模拟连续云层效果的前端GIS、WebGL开发者。方案结合shadertoy与three.js社区思路,使用Perlin噪声与Worley噪声叠加生成云形,并通过分形布朗运动增强细节层次;云层体数据先由CPU计算,后改为GPU渲染到纹理加速生成,再以光线步进完成渲染,整体不依赖外部噪声纹理,实现门槛较低。包内共4个文件,含两个JavaScript脚本、一个HTML页面及一个inscode工程配置,分别对应体数据生成、云层渲染与页面演示入口,压缩包仅9KB,结构清晰便于阅读。目前已有436人学习下载,适合想快速上手体积云渲染或为Cesium扩展天气效果的中高级开发者。代码虽精简,但完整呈现了从噪声生成到光照步进的主干流程,可直接运行并修改参数观察云层变化;对飞行模拟、智慧城市可视化等真实感要求较高的场景也有直接参考价值。 做Cesium项目的时候,一旦涉及“全球”这两个字,很多在局部场景里特别成熟的做法就都不好用了。体积云渲染就是这样。我前后试过贴图云、粒子云、Sprite云和Cesium地表材质方案,要么从太空看是平的,要么转到全球视角就穿帮,要么性能直接崩掉。折腾了几个月后,最终沉淀下来一套能真正覆盖全球、支持动态光照的Ray Marching体积云方案。这篇文章不聊PPT式的架构图,直接把我实测过的实现路径、关键代码、参数选择和踩坑记录写出来。

这套方案适合谁?如果你正在做Cesium的三维数字孪生项目、全球气象可视化、飞行模拟,或者是想给WebGIS场景加一层“电影感”天空效果,那么这篇文章可以帮你直接省掉一大轮弯路。接下来我会从渲染原理讲起,一步步拆到Cesium集成、性能调优和问题排查。

1. 方案选型:全球体积云为什么必须走Ray Marching

1.1 为什么贴图云、粒子云在全球尺度下都不靠谱

Cesium是一个地球级别的GIS平台,相机可以无缝从地表拉到卫星轨道。在这样的场景下,云不是一个“道具”,而是必须符合地球曲率的半透明体积层。贴图云本质是一张带透明通道的平面图片,贴在球体上后,从侧面看就是一片薄纸,完全穿帮;粒子云更考验性能,全球范围几百米尺度的粒子数量不会是一个小数目,还要处理粒子与地球曲率的关系,计算量本身就失控;Sprite云只能在局部区域摆几个广告牌,它们适合做“大气层的一朵点缀”,但根本撑不起“全球云图”这种需求。

1.2 后处理Stage要比Cesium的材质系统更适合做全球云层

这里有一个关键判断:云层体积应该“悬浮”在地球上方,而不是像地表纹理一样贴在地形上。Cesium的CustomShader、Fabric材质,天生是给地形、模型或billboard做表面效果用的,要做真实的云层光照和体积遮挡,绕来绕去非常别扭。

我的选择是PostProcessStage后处理。思路很直接:在每一帧渲染完成后,拿到的其实是一张包含场景颜色的纹理。我在后处理片元着色器里,根据每个像素的屏幕坐标反算出视线方向,再用视线方向去和“云层包围壳”求交,沿视线做步进采样,累积颜色和透明度,最后叠加回场景。这样云层天然就是全球的,跟相机位置无关,也不需要额外的几何网格。

很多人在这一步会犹豫:Cesium的PostProcessStage是作用于全屏的,会不会跟UI文字、billboard冲突?其实不会,因为后处理拿到的colorTexture就是场景本身,我们只是在场景颜色上叠加云层,最终结果再与场景自动合成。只要把云层alpha计算正确,不会破坏原有的实体渲染。

1.3 动态光照:云的颜色不是“贴图颜色”,是散射和吸收的结果

体积云和普通模型的Phong光照完全不是一回事。普通光照算的是一个表面,体积云是半透明的体积,光在云层内部被散射、吸收,还会穿过云体形成阴影。所以体积云的颜色计算实际上是一个“体光照”问题。

在Cesium里,我直接用Simon1994PlanetaryPositions计算太阳在ECEF下的方向,把它作为uniform传到后处理着色器里。这样云层的光影会随着场景时间动态变化,早晨和黄昏的云会带出暖色调,正午则更白亮。这个动态光照是整个方案里观感提升最大的一项,也是我标题里为什么特意强调“动态光照”的原因。

2. 全球体积云核心实现:包围壳、噪声与密度场

2.1 云层包围壳:用内外两个椭球限定云的“活动空间”

要渲染全球云层,我不能在世界里凭空造一个云层实体。更通用的做法是给云层定义一个“包围壳”。在Cesium的ECEF坐标系下,我直接用WGS84椭球的两个同心高度面来构造:内壳约在海拔1500米,外壳约在海拔8000米。相机任意位置,我都可以在着色器里用射线与这两个椭球求交,得到光线进入云层和离开云层的两个距离值。

这一步看起来很数学,但对精度影响很大。如果只用球体做包围壳,在近地视角下,云层底部会和真实地形高度产生肉眼可见的误差,尤其是高原和山区。用椭球能直接复用Cesium的Ellipsoid参数,把云层约束在一个贴合地球形状的厚度带中。

在实际代码里,射线与椭圆面的求交可以用解析公式做,也可以迭代逼近。我建议解析求交,因为迭代在远距离视角下性能和精度都不稳定。Cesium自己也内置了很多椭球计算相关的数学方法,可以直接参考。

2.2 3D噪声纹理:云的“形状DNA”

体积云形状的核心是3D噪声。我用了两层噪声:低频Perlin噪声决定云的整体聚散形态,高频Worley噪声负责给云层边缘增加蓬松细节。光靠单次噪声是不够的,世界上的云有明显的“絮状感”,所以我会把多个倍频程的噪声叠加起来,这个技术叫FBM(Fractal Brownian Motion)。

具体实现上,我在初始化阶段预生成一张3D纹理,比如64×64×64,把3到4个倍频程的噪声结果打包进纹理的RGBA通道。运行时只需采样这张3D纹理,再用少量ALU做FBM叠加,就能获得不错的云形态。这么做的好处是效率高,缺点是纹理分辨率有限,靠近看会有重复感。所以我建议采样时对坐标乘一个缩放因子,让噪声在空间上不产生明显周期。

2.3 云密度函数:高度衰减、覆盖度与边缘软化

有了噪声形状后,不是所有噪声值都能成为云。我通过一个密度函数来“雕刻”云朵:

  • 高度因子:在1500米到8000米的区间内,云层中部密度最大,底部和顶部逐渐衰减。这个衰减曲线我习惯用平滑的梯形或正弦曲线模拟。
  • 覆盖度(Coverage):用一个0到1的参数控制全球云的总体覆盖率。cover越大,云连成片;越接近0,云越稀疏破碎。
  • 边缘软化:对采样得到的噪声值做一个阈值映射,让云层边缘具有渐变的半透明过渡,而不是硬边。

这个密度函数我调了很多版本,最终发现一个经验值:Coverage在0.5附近时,云层在视觉上既有大片的云区,也有零散的碎云,对整个地球的观感最自然。过于拉高Coverage后云层容易糊成一大片,反而看不出体积感。

2.4 代码示例:噪声采样与密度合成

这里直接给一段在Cesium后处理片元着色器里用到的密度函数GLSL核心逻辑:

float sampleCloudDensity(vec3 p, float time) { vec2 uv = geographicUV(p); // 低频大形 float base = texture3D(noiseTex, p * 0.0004 + vec3(0.0, 0.0, time * 0.02)).r; // 高频细节 float detail = texture3D(noiseTex, p * 0.002 + vec3(time * 0.03)).g; // 高度衰减,innerAlt和outerAlt需要外部传入 float altFactor = smoothstep(innerAlt, midAlt, height) * (1.0 - smoothstep(midAlt, outerAlt, height)); // 覆盖度裁剪 float density = max(0.0, base - (1.0 - coverage)); density *= detail * altFactor; return max(0.0, density); }

实际工程里我会把p的坐标从ECEF转到局部空间再采样,主要是为了规避全球坐标数值过大带来浮点精度问题,这个在下面的排查章节细讲。

3. 与Cesium集成:后处理、视线步进与光照合成

3.1 创建后处理Stage并注入动态uniform

Cesium里创建一个自定义后处理效果并不复杂,关键是要把“每帧会变化”的uniform注入进去。以相机位置和太阳方向为例:

const cloudStage = viewer.postProcessStages.add( new Cesium.PostProcessStage({ fragmentShader: cloudFragShader, uniforms: { cameraPosition: new Cesium.Cartesian3(), sunDirection: new Cesium.Cartesian3(), cloudCoverage: 0.5, }, }) ); viewer.scene.postProcessStages.add(cloudStage); // 每个渲染帧更新uniform viewer.scene.postUpdate.addEventListener(() => { cloudStage.uniforms.cameraPosition = viewer.camera.positionWC; cloudStage.uniforms.sunDirection = Cesium.Simon1994PlanetaryPositions.computeSunDirectionInWorld( viewer.clock.currentTime, Cesium.Matrix3.IDENTITY ); });

注意uniform里不要放Cesium的Matrix4对象直接当vec3用,PostProcessStage会自动做类型映射,但是为了保险,我都是从Cartesian3对象取分量传入,避免因为对象引用问题导致更新不生效。

3.2 Ray Marching主循环的GLSL实现

后处理片元着色器里,我首先要反算视线方向。通过varying拿到屏幕UV,再配合相机逆视图、逆投影矩阵,可以重建出视线方向。在Cesium的PostProcessStage中,它会自动注入一些uniform,比如czm_inverseProjectionczm_inverseView,直接乘就能从NDC坐标得到世界坐标方向。

然后执行射线与椭球的求交,进入ray marching主循环。下面是伪代码级别的核心流程:

vec3 rayDir = normalize(reconstructWorldDir(uv)); vec2 t = intersectEllipsoidShell(cameraPos, rayDir); // 与全球云层壳求交 vec3 acc = vec3(0.0); float transmittance = 1.0; for (float tCurrent = t.x; tCurrent < t.y; ) { vec3 pos = cameraPos + rayDir * tCurrent; float density = sampleCloudDensity(pos, time); if (density > 0.002) { float light = computeLighting(pos, rayDir, sunDir); vec3 color = light * density * cloudColor; acc += color * transmittance * stepSize; transmittance *= exp(-density * stepSize * absorbCoef); } if (transmittance < 0.02) break; // 提前终止 tCurrent += stepSize; } gl_FragColor = vec4(acc, 1.0 - transmittance);

这里computeLighting函数计算当前采样点被太阳照亮后的散射光和透射衰减。实际编写中有很多优化空间:比如步长不固定、远处步长短近处步长长;再比如云层很薄的时候可以直接跳过大步长段。

3.3 光照与相位函数:让云层有体积感

体积云的光照计算我参照了Schneider和Vos的做法,在单次散射假设下,点p处的亮度可以写成:

L(p) = I_sun * T(p, sunDir) * sigma(p) * phase(angle) + 环境光

其中T(p, sunDir)代表从该点到太阳方向的透射率,sigma是散射系数,phase函数用Henyey-Greenstein相位函数来模拟云层的前向散射。云在逆光时边缘会呈现“银边”效果,很大程度就是相位函数的功劳。

我用一个近似技巧提升效率:主步进循环里对每个采样点做一次向太阳方向的光线采样,开销太大。我退而求其次,用同一个采样点的噪声密度值去估算光照遮蔽,再用一个pow函数模拟多重散射的补光。简单说就是:

float light = exp(-densityAlongSun * absorbCoef); light = pow(light, 0.6); // 模拟多重散射

这一行的效果非常明显,实测能显著提升云层的通透感,尤其在太阳方向侧。

3.4 合成到场景:注意与深度、透明件的顺序

后处理拿到的是整个场景的colorTexture。如果直接把云叠上去,会出现云层被地表物体遮挡的错误。我的做法是读depthTexture,把云层的深度和场景深度做比较:云在场景后面,就不渲染。这个逻辑写起来不复杂,但能避免云层穿山体的尴尬。

另外有个细节,Cesium中后处理Stage输出的颜色是覆盖整个场景的。要把半透明的云层正确混入场景,我一般输出RGBA,其中Alpha代表云的遮挡度,然后在最终合成时手动做一次mix:

vec3 finalColor = mix(sceneColor.rgb, cloudColor, cloudAlpha);

如果你的自定义Stage里还包含其他效果,比如大气散射、体积光,要留意执行顺序。我通常把云层Stage放在大气后、常规后期色调映射之前。

4. 性能优化:从“能看”到“能跑”

4.1 自适应步长与提前终止

Ray Marching的步长是最大性能瓶颈。固定步长在4000米厚的云层里会逼死GPU。我采用两层策略:

  • 第一层:根据采样点距离相机的距离调整步长。离相机近的云层,视觉权重高,用细步长;远距离云层,用粗步长。
  • 第二层:当transmittance降到某一个阈值以下,直接终止循环。因为此时继续往前走的光线几乎都被挡住了,再算下去意义不大。

这两招组合起来,通常能把循环次数压缩掉四成以上。我的线上版本最大步数控制在48步以内,再高就要考虑降级。

4.2 低分辨率渲染与上采样

后处理Stage的渲染尺寸是可以自定义的。我的做法是单独开一个小尺寸RenderTarget保存云层结果,再在合成阶段把低分辨率云纹理上采样混合回场景。云属于低频信息,半分辨率下肉眼几乎看不出差异,但计算量直接少了四分之三。实测在2K屏上,全分辨率步进大约耗时3-4ms,半分辨率能压到1.5ms左右。

低分辨率渲染时,云层边缘会产生锯齿,建议在上采样时做一次柔和的高斯模糊或使用bilinear纹理过滤缓解。如果你的团队对画面要求特别高,也可以把云单独用动态分辨率策略:相机快速运动时降分辨率,静态观察时逐步恢复到全分辨率。

4.3 深度提前截止与相机高度分级

相机在海平面附近时,云层被地表挡住的部分其实没必要计算。我利用scene.depthTexture,在进入ray marching之前先判断当前像素对应的场景深度,如果场景深度小于云层内壳距离,那就说明这个像素被地形或建筑遮挡,直接discard或者输出原色,不做云计算。这个优化在峡谷、城市等场景里收益极其明显。

另一个经验是相机高度分级:相机在近地面时,视线能穿过的云层厚度有限,步进深度不需要太大;相机在太空时,全球云层变成一整片视觉信息,步数可以进一步降低。我在uniform里根据相机高度动态调整最大步数和步长。

4.4 与动态水面、地形等效果的协同

很多项目不止要做体积云,还要做高逼真动态水面、全球海洋效果等。有一个容易被忽略的点:后处理Stage会消耗场景的颜色和深度纹理,如果多个后处理Stage互相叠加,GPU带宽会很紧张。

我的经验是尽量把同类型的全球视觉特效合并在一个Stage里。比如全局海洋着色、云层、大气,可以放到同一个片元着色器里,按视线方向分支处理。这样能减少render-to-texture的次数。Cesium的多Effect链固然方便,但每个Stage都是一次全屏pass,叠加多了性能会非常难看。

5. 排查实录:几个让我头疼到失眠的Bug

5.1 云层在太空视角疯狂闪烁

症状:把相机拉高到几百公里后,云层出现大量闪烁噪点,像老电视雪花。

原因非常典型:我在噪声采样时直接把ECEF世界坐标作为纹理坐标,ECEF坐标动辄几百万的量级,而GLSL的float在高精度下也只有7位有效小数,坐标一膨胀,噪声采样就出现了严重的数值抖动。闪烁的本质是浮点精度不够导致采样坐标上下跳动。

解决办法是把采样坐标从世界坐标改为“以相机为原点的局部坐标”,也就是在进入ray marching前,先把pos减去cameraPos,再做噪声采样。这样坐标值在几十到几千的范围内,精度完全够用。这是一个很小的改动,但把闪烁根治了。

5.2 云层在南北极区域变形开裂

症状:云在赤道附近看起来正常,一旦转到北极圈附近,云层出现明显的拉伸和错位。

原因是早期版本图省事,直接用经纬度来做云层UV。WGS84的经度在极点处会聚成一个点,导致纹理采样在极点附近被无限放大,形成类似“万花筒”的畸变。

解决思路是放弃经纬度UV,改成用归一化的ECEF方向向量去采样3D噪声。三维方向在球面上是均匀的,不存在极点奇异性。你只需要对方向向量做坐标变换,映射到一个局部坐标系里再采样。这个方案在南北极区域表现稳定,就是实现成本稍高一点。

5.3 帧率在低端机直接掉到个位数

症状:RTX 3060级别的显卡上运行流畅,换到集成显卡或低端移动端,只能两位数徘徊。

排查结果是三处叠加:全程高分辨率步进、最大步数设置过大、每帧重复计算噪声的FBM倍频程太多。最终我把云层后处理降到半分辨率,最大步数从96降到48,并把噪声纹理从R16格式改成单通道RGBA打包格式,减少纹理采样次数。三管齐下后,集成显卡也能稳定30帧左右。记住,云层是视觉重点,但不是全部场景的主角,性能预算一定要卡死。

5.4 太阳方向对不齐:黄昏效果很假

症状:云层明明有光照,但亮面和暗面的角度和场景里的其他光影完全对不上。

后面发现Cesium的Simon1994PlanetaryPositions.computeSunDirectionInWorld返回的是时间相关的太阳方向,但我没有把相位函数参数和太阳高度对应起来。解决方式是确保uniform更新时使用同一个时间源,并且在着色器里对光照系数做太阳高度的梯度插值。比如太阳低于地平线时,云的base color不应该还是白色,要混合暗蓝紫色。

6. 扩展方向:从“云”到“天气系统”

6.1 接入真实天气数据

体积云方案稳定后,可以尝试把真实的气象数据接入云层覆盖率。核心思路是:不从Uniform里写死一个全局Coverage,而是把气象数据(如云量格点)解码成纹理,映射到全球经纬度。云层的密度函数里,用这张纹理替换或调制全局Coverage参数,就能实现“哪里下雨哪里多云”的真实全球云图。

这里有个坑:气象数据纹理通常都是经纬度平面投影,直接采样会重复我在5.2节提到的极点问题。建议把气象纹理映射到cube map上,再用方向向量采样。这个工作量不大,但能彻底规避极点畸变。另外,如果项目要求整体离线部署,地形和影像瓦片也别忘了本地化,体积云本身不依赖任何在线服务,这点反而是它的优势。

6.2 与three.js共享GL上下文的另一种思路

团队里如果有比较强的three.js渲染功底,也可以选择在Cesium的GL上下文中挂载一个three.js渲染器,用three的体积云插件渲染云层相机,再合帧。这个思路对熟悉three的团队可能更快,但缺点是麻烦在于相机同步和深度合成,容易出现云层不跟地形遮挡的问题。我两种方案都试过,最终保留Cesium原生后处理实现,因为它更可控,依赖更少。

6.3 模块化:把云层参数做成可配置项

我在工程里把云层的所有视觉参数都抽成了一个单独的json配置文件,覆盖度、高度区间、步长、太阳光颜色、云层密度等全部可调。项目方改需求时,美术同学可以直接在面板里调数,不需要动着色器代码。这一步看起来不起眼,但实际交付项目中非常关键,省掉了我大量“帮忙改个参数”的沟通成本。

最后再讲一个实际的小技巧。我用这套方案跑了几个真实项目后,最大的收获是:调试体积云时,先不要在完整场景里调试,直接建一个空场景,只留体积云和太阳。这样每次改参数,肉眼能立刻看到云层形态和光照的变化。等云层形态调满意了,再放回真实场景去调遮挡和性能。这样做至少帮你省掉一半的调试时间。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 8:20:36

基于微信小程序的青少年街舞培训中心系统设计与实现源码+文档

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/9/4 7:14:11

AI编程时代:用确定性工具链约束智能体,守护代码质量与架构

如果你是一位经验丰富的开发者&#xff0c;最近可能被各种 AI 编程助手&#xff08;如 Cursor、GitHub Copilot&#xff09;的“魔法”所震撼。它们能生成代码、修复 Bug&#xff0c;甚至重构整个模块&#xff0c;效率的提升肉眼可见。但兴奋之余&#xff0c;你是否也隐隐感到一…

作者头像 李华
网站建设 2026/9/4 7:35:10

收藏!小白程序员轻松入门大模型,带你掌握未来趋势!

本文分享了作者从后端开发转向AI Agent开发的心路历程&#xff0c;对比了两个方向的学习难度、求职前景和个人感受。作者认为AI Agent作为新兴领域&#xff0c;学习曲线相对平缓&#xff0c;发展前景广阔&#xff0c;适合想进入AI浪潮但无暇学习复杂后端技术的程序员。同时&…

作者头像 李华
网站建设 2026/9/3 20:01:29

SparsePR稀疏注意力:无训练加速视频生成与世界模型推理

视频生成和世界模型在推理阶段会生成很长的序列&#xff0c;SparsePR 是为这类场景设计的一种无需训练、基于稀疏注意力的大模型优化框架。公开材料称&#xff0c;在合适的稀疏度配置下&#xff0c;视频生成和世界模型推理速度最高可以提升 2.6 倍。这类方案对已经训练好的模型…

作者头像 李华
网站建设 2026/9/4 15:24:06

华为AI岗面试全攻略:机试准备、技术栈与项目实战指南

1. 先把这个标题读明白&#xff1a;到底在准备什么如果你最近也在刷各种招聘信息&#xff0c;应该见过类似“2026年-华为-06月03号AI岗”这种标题。第一次看到的人可能会愣一下&#xff1a;这到底是个岗位名称&#xff0c;还是一场考试通知&#xff1f;实际上&#xff0c;这种命…

作者头像 李华
网站建设 2026/9/2 21:05:40

AI Agent警惕开源赏金蜜罐:避免白嫖劳动的筛查指南

我第一次看到“Some GitHub bounty repos are honeypots that farm free work from AI agents”这句话时&#xff0c;下意识以为是安全领域里常见的蜜罐。后来认真想了想&#xff0c;才发现它真正描述的&#xff0c;是开源协作里正在出现的新情况&#xff1a;有些仓库把 bounty…

作者头像 李华