文章目录
Cesium 内置雾更偏向远景大气和性能优化。这个案例使用后处理阶段,为每个屏幕像素计算“低处浓、高处淡”的高度雾。

实现思路导图
flowchart TD
ROOT[高度雾后处理]
ROOT --> INPUT[场景输入]
INPUT --> COLOR[颜色纹理]
INPUT --> DEPTH[深度纹理]
ROOT --> POSITION[重建世界位置与高度]
ROOT --> DENSITY[雾密度]
DENSITY --> HEIGHT[高度因子]
DENSITY --> DISTANCE[距离因子]
DENSITY --> NOISE[FBM 流动扰动]
ROOT --> COMPOSE[指数透射与颜色合成]
页面与效果对象分离
页面创建 Viewer 后实例化 HeightFog,面板只调用参数接口:
fogEffect = new HeightFog(viewer, {
fogHeight: controls.fogHeight,
globalDensity: controls.globalDensity,
fogColor: controls.fogColor,
scatterStrength: controls.scatterStrength,
});
普通参数直接更新 uniform,不需要销毁和重建 PostProcessStage。
深度先恢复成世界位置
后处理输入只有颜色和深度。Shader 先解包深度,再从窗口坐标恢复眼坐标,最后乘逆视图矩阵:
float depth = czm_unpackDepth(texture(depthTexture, uv));
vec4 eye = czm_windowToEyeCoordinates(gl_FragCoord.xy, depth);
vec4 positionWC = czm_inverseView * (eye / eye.w);
天空没有有效场景深度,应返回原颜色。否则天空会被当成离相机无限远的实体,出现整屏雾化。
高度和距离共同决定密度
像素高度低于雾顶时,根据高度衰减得到 heightFactor,再用指数消光计算随距离增加的雾量:
float density = u_globalDensity * heightFactor;
float fogAlpha = 1.0 - exp(-distanceToCamera * density);
fogStartDistance 保留相机近处清晰区域。它可以避免控制面板、近景模型和地面贴近相机时被一层灰色覆盖。
噪声与散射让雾不再均匀
多频率噪声使用世界位置和时间作为输入,改变局部密度。风速决定噪声域移动速度,湍流参数决定均匀密度与噪声密度之间的混合比例。
散射项根据视线与光照方向夹角增加暖色高光。它是视觉近似,不是完整的体积光传输。
生命周期
动态时间通过 preRender 或 uniform 回调更新。销毁时必须同时移除后处理 Stage 和时间监听:
fogEffect?.destroy();
viewer?.destroy();
只把 enabled 设为 false 是临时停用,不等于释放资源。
这个案例的主线是:深度恢复世界坐标 → 估算高度和距离 → 叠加噪声与散射 → 与原场景颜色混合。
截图中的雾为何只堆在山谷
截图使用山间云雾参数。近处山脊仍清晰,远处低谷逐渐变白,说明密度同时受高度和相机距离控制。天空保持原色,因为没有有效深度的像素会提前返回。
如果只根据屏幕 y 坐标加白色渐变,移动相机后雾层会粘在屏幕上;这里先恢复世界位置,所以山谷位置随相机变化仍保持稳定。
世界高度是怎样近似的
案例用世界位置长度减去当地地球半径估算海拔:
float pixelHeight = length(positionWC.xyz) - u_localEarthRadius;
u_localEarthRadius 由相机世界坐标长度减相机海拔得到。这对局部山地足够稳定,但 WGS84 是椭球,跨大纬度或全球视角会产生误差。严格做法是把世界坐标转换为椭球经纬高,代价是 Shader 运算更多。
密度公式分成四个因子
最终密度 = 全局密度
× 高度衰减
× 距离生效
× 噪声扰动
高度衰减用 smoothstep 在地面到雾顶之间过渡。距离因子在起始距离前为零,之后进入指数消光。噪声只在已有密度上扰动,不能让雾顶以上凭空出现云块。
指数消光使用:
alpha = 1 - exp(-density × distance)
它在近处增长缓慢,远处渐近 1,比线性乘距离更自然,也不会无限超过透明度范围。
FBM 为什么使用多层噪声
单层噪声只有一个空间尺度,看起来像均匀斑点。FBM 叠加多组频率和振幅:低频决定大片雾团,高频打破边缘。
时间只平移噪声采样域,不改变 Cesium 世界位置。windSpeed 控制平移速度,turbulence 控制噪声结果对基础密度的影响比例。
当前实现直接使用 ECEF 坐标的两个分量。局部区域可以工作,但噪声方向不等于当地东、北方向。若业务要求明确风向,应先在 CPU 计算世界到 ENU 的矩阵并传给 Shader。
后处理顺序会改变结果
高度雾读取前一阶段的颜色与深度。如果 Bloom 在雾之前,亮部先扩散再被雾混合;如果 Bloom 在雾之后,雾中的太阳散射也可能产生辉光。FXAA 通常放在接近最终输出的位置。
因此 PostProcessStage 加入集合的顺序是画面设计的一部分,不能把所有 Stage 当作可交换的滤镜。
从零复现:高度雾教程
案例源码位于 src/demos/cesiumdemos/fog/。实现顺序是先证明深度重建正确,再加入高度密度,最后才叠加噪声和流动。
第 1 步:创建最小后处理 Stage
创建 PostProcessStage,先让片元 Shader 原样返回 colorTexture。确认开关、添加和移除 Stage 正常,再读取深度。
const stage = new Cesium.PostProcessStage({
name: "height-fog",
fragmentShader,
uniforms,
});
viewer.scene.postProcessStages.add(stage);
场景必须支持深度纹理。若 depthTexture 不可用,应禁用效果并给出提示,而不是让 Shader 输出整屏雾色。
第 2 步:从深度重建眼坐标
读取当前像素深度,使用 Cesium 提供的深度解包和窗口坐标转换函数得到眼坐标,再乘逆视图矩阵还原世界位置。
float depth = czm_readDepth(depthTexture, v_textureCoordinates);
vec4 eye = czm_windowToEyeCoordinates(gl_FragCoord.xy, depth);
vec3 world = (czm_inverseView * vec4(eye.xyz / eye.w, 1.0)).xyz;
先把世界位置或线性距离映射成灰度调试图。天空深度通常接近远平面,需要单独跳过,否则天空会被当成极远地形。
第 3 步:计算像素高度
快速方案用世界位置长度减椭球半径估算高度;精确方案在 CPU 或专用数学路径中转 Cartographic。后处理需要每像素执行,通常选择椭球近似。
float height = length(world) - u_ellipsoidRadius;
完成标志:把 smoothstep(minHeight, maxHeight, height) 显示为黑白图时,低谷与高山分层方向正确。
第 4 步:组合高度和距离密度
低于雾顶时高度因子较大,超过雾顶后平滑降为零。距离因子随相机到像素距离增长,避免近处也完全被雾覆盖。
float heightFactor = 1.0 - smoothstep(u_baseHeight, u_topHeight, height);
float distanceFactor = 1.0 - exp(-distanceToCamera * u_distanceDensity);
float density = u_density * heightFactor * distanceFactor;
用指数透射率 T = exp(-density) 合成,比直接线性 alpha 在远距离更稳定。
第 5 步:加入空间噪声与时间流动
用世界位置缩放后采样 FBM,时间只平移噪声域。噪声负责打破均匀边界,不应把密度变成负数。
vec3 noisePosition = world * u_noiseScale + u_wind * u_time;
float variation = mix(1.0, fbm(noisePosition), u_noiseStrength);
density *= max(variation, 0.0);
先将噪声强度设为 0 验证高度雾,再逐步增加。否则深度错误和噪声图案会混在一起难以定位。
第 6 步:合成雾色
vec3 sceneColor = texture(colorTexture, v_textureCoordinates).rgb;
vec3 finalColor = mix(u_fogColor, sceneColor, transmittance);
out_FragColor = vec4(finalColor, 1.0);
雾色可以根据太阳方向、视线角度或高度轻微变化,但这些属于光照增强,不应改变核心透射逻辑。
第 7 步:连接面板与生命周期
密度、颜色、雾顶、噪声和风直接更新 uniform。Stage 只创建一次;页面卸载时从 postProcessStages 移除并销毁,同时停止时间更新回调。
验收与排错
- 整个屏幕变成雾色:检查天空深度和深度解包。
- 雾层跟着相机移动:噪声必须采样世界位置而不是屏幕 UV。
- 山顶比山谷更浓:检查高度因子是否取反。
- 切换页面后帧率仍下降:确认 Stage 和时钟监听已移除。