Cesium实现积雪

在 Cesium 中实现逼真的积雪效果:基于法线的着色器技术

在三维可视化应用中,积雪效果是增强场景真实感和季节变化的重要元素。特别是在城市规划、景观设计和数字孪生等领域,逼真的积雪效果可以大大提升用户体验。本文将深入探讨如何使用基于法线的着色器技术在 Cesium 中实现高质量的积雪效果,并解析其背后的图形学原理。

在现实世界中,雪的积累有其特定规律:

  • 顶面和向上表面积雪最多
  • 垂直表面(如墙壁)积雪很少或没有
  • 悬挑底面基本没有积雪
  • 积雪在边缘和转角处有平滑过渡

实现逼真的积雪效果,关键在于模拟这些自然规律,而法线向量正是解决这个问题的理想工具。

基于法线的积雪原理

在 3D 图形学中,法线(normal)是垂直于表面的单位向量,它指明了表面的朝向。通过计算表面法线与雪的降落方向(通常是向下的向量)之间的关系,我们可以确定表面应该积累多少雪。

具体来说:

  • 当表面法线与雪方向相同时(如水平屋顶),该表面会积累最多的雪
  • 当表面法线与雪方向垂直时(如垂直墙面),该表面几乎不会积雪
  • 当表面法线与雪方向相反时(如悬挑底面),该表面不会有雪

GLSL 着色器实现

下面是实现积雪效果的关键 GLSL 着色器代码:

1
2
3
4
5
6
7
8
9
if(u_snow){ // 如果启用了雪效果
vec3 normalEC = fsInput.attributes.normalEC; // 获取眼睛坐标系中的法线
vec3 normalMC = czm_inverseNormal * normalEC; // 将法线从眼睛坐标系转换到模型坐标系
vec3 color = material.diffuse; // 获取原始的漫反射颜色
vec3 white = vec3(1.0,1.0,1.0); // 定义白色(雪的颜色)
float m = dot(normalMC, u_snowDirection); // 计算法线和雪方向的点积
m = pow(m,5.0); // 对点积结果进行5次幂运算,使效果更加集中
material.diffuse = mix(color, white, clamp(m,0.0,1.0) * u_snowAlpha); // 混合原始颜色和白色
}

这段代码的核心是通过法线和雪方向的点积来计算积雪程度,然后使用混合函数将原始材质与雪的白色进行混合。

代码详解

让我们逐步分析这段代码的工作原理:

1. 坐标系转换

1
2
vec3 normalEC = fsInput.attributes.normalEC; // 获取眼睛坐标系中的法线
vec3 normalMC = czm_inverseNormal * normalEC; // 将法线从眼睛坐标系转换到模型坐标系

在 Cesium 中,片元着色器通常接收的是眼睛坐标系(Eye Coordinates, EC)中的法线。但为了正确计算积雪效果,我们需要在模型坐标系(Model Coordinates, MC)中工作,因此需要进行坐标转换。czm_inverseNormal是 Cesium 提供的转换矩阵。

2. 点积计算

1
float m = dot(normalMC, u_snowDirection); // 计算法线和雪方向的点积

点积操作dot(normalMC, u_snowDirection)是这个效果的核心,它计算了表面法线与雪方向之间的余弦值:

  • 当两个向量方向一致时,点积为 1
  • 当两个向量垂直时,点积为 0
  • 当两个向量方向相反时,点积为-1

这正好符合我们对积雪分布的期望:朝上的表面积雪最多,垂直表面几乎没有,朝下的表面完全没有。

3. 非线性增强

1
m = pow(m,5.0); // 对点积结果进行5次幂运算,使效果更加集中

点积的结果是线性的,但现实中的积雪分布并非线性。通过应用幂函数pow(m,5.0),我们可以非线性地增强效果:

  • 接近 1 的值(如 0.9)经过 5 次幂后仍然很大(约 0.59)
  • 中等的值(如 0.5)经过 5 次幂后变得很小(约 0.03)
  • 小于 0 的值在 clamp 后会被截断为 0

这使得积雪效果更加集中在朝向雪方向的表面上,创造更逼真的视觉效果。

4. 颜色混合

1
material.diffuse = mix(color, white, clamp(m,0.0,1.0) * u_snowAlpha); // 混合原始颜色和白色

最后,我们使用 GLSL 的mix函数将原始颜色与白色(代表雪)进行混合。混合系数由两部分组成:

  • clamp(m,0.0,1.0)确保混合因子在 0 到 1 之间
  • u_snowAlpha控制整体雪的透明度,可以用来调整雪的厚度

另一种实现方式

以下是一个稍微不同的实现方式,同样基于法线计算,但有一些细微的差异:

1
2
3
4
5
6
// 计算表面法线与方向向量(u_tilesetNormal)的点积
float angle = dot(normal, u_tilesetNormal);
// 将这个值进行幂运算,创造更加锐利的过渡效果
angle = pow(angle, 5.0);
// 生成一个接近白色的颜色,透明度由计算出的角度值决定
vec4 snowColor = vec4(0.8, 0.8, 0.8, angle);

这个版本使用了稍浅一点的白色(0.8,0.8,0.8)而非纯白色,可能在某些场景下看起来更自然。

在 Cesium 中应用

要在 Cesium 中应用这个着色器效果,我们需要创建自定义着色器并应用到 3D Tileset 或模型上。以下是一个简化的实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 定义积雪着色器
const snowShader = new Cesium.CustomShader({
uniforms: {
u_snow: {
type: Cesium.UniformType.BOOL,
value: true,
},
u_snowDirection: {
type: Cesium.UniformType.VEC3,
value: new Cesium.Cartesian3(0.0, 0.0, 1.0), // 向上的方向
},
u_snowAlpha: {
type: Cesium.UniformType.FLOAT,
value: 0.5, // 雪的强度/透明度
},
},
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
if(u_snow) {
vec3 normalEC = fsInput.attributes.normalEC;
vec3 normalMC = czm_inverseNormal * normalEC;
vec3 color = material.diffuse;
vec3 white = vec3(1.0,1.0,1.0);
float m = dot(normalMC, u_snowDirection);
m = pow(m,5.0);
material.diffuse = mix(color, white, clamp(m,0.0,1.0) * u_snowAlpha);
}
}
`,
});

// 应用到3D Tileset
const tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: "path/to/your/tileset",
customShader: snowShader,
})
);
作者

wangyawei

发布于

2025-03-21

更新于

2025-03-21

许可协议