站内搜索

输入关键词开始搜索

Cesium 高度雾:从深度纹理恢复世界坐标

文章目录

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 和时钟监听已移除。

相关文章

Cesium 高度雾后处理:从深度纹理恢复世界坐标

使用 PostProcessStage 读取颜色和深度纹理,按像素高度、距离、噪声和光照计算动态高度雾

Cesium FFT 矩形水面:GPU 频谱海浪与随波船体

在 Cesium Primitive 中接入多级联 FFT 海浪,完成位移、法线、清澈度、LOD 网格和船体姿态采样