skip to content

Cesium
使用 CustomShader 为 Cesium 3D Tileset 修改材质

在 Cesium 开发中,我们经常需要对加载的 3D Tileset 模型进行材质的自定义修改。本文将介绍如何使用 CustomShader 来替换模型的默认材质,并实现一个简单的动态呼吸效果。

该方案不依赖模型的具体几何特征(如法线方向),而是通过全局的材质重写,确保在不同类型的模型上都能呈现一致的效果。

需求分析

我们的目标是修改模型的材质表现:

  1. 修改颜色:统一使用青色(Cyan)基调。
  2. 动态效果:材质亮度随时间进行律动(呼吸效果)。
  3. 兼容性:不依赖模型坐标系原点或法线方向,避免不同数据源导致的效果差异。
  4. LOD 优化:解决远距离模型消失的问题。

核心代码实现

我们使用 CustomShader 的 PBR(基于物理的渲染)模式,并利用 Cesium 内置变量实现动画。

Shader 代码

const roofShader = new cesium.CustomShader({
  // 1. 设置光照模式:PBR 模式
  lightingModel: cesium.LightingModel.PBR,
  
  // 2. 定义 Uniforms:传递参数
  uniforms: {
    u_roofColor: {
      value: new cesium.Cartesian3(0.0, 1.0, 1.0),
      type: cesium.UniformType.VEC3
    }
  },
  
  // 3. Fragment Shader:核心逻辑
  fragmentShaderText: `
    void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
      // 定义基础色调
      vec3 baseColor = vec3(0.0, 1.0, 1.0);
      
      // --- 动态效果:呼吸 ---
      // 使用内置变量 czm_frameNumber (每帧+1) 替代 u_time,无需 CPU 每帧更新 Uniform
      // 系数推导:0.1 * 60FPS = 6.0,2π/6 ≈ 1秒,对应约 60 BPM 的频率
      float breathe = 0.6 + 0.4 * sin(czm_frameNumber * 0.1);
      
      // --- 材质重写 ---
      
      // 1. 漫反射 (Diffuse):降低亮度
      material.diffuse = baseColor * 0.2;
      
      // 2. 自发光 (Emissive):应用呼吸效果
      material.emissive = baseColor * breathe;
      
      // 3. 高光与粗糙度 (Specular & Roughness)
      material.specular = vec3(1.0); 
      material.roughness = 0.3;
    }
  `
});

技术要点

使用 czm_frameNumber 实现动画

实现动画通常需要传入 u_time。Cesium 内置了 czm_frameNumber 全局 Uniform。虽然它代表帧数而非绝对时间,但在帧率相对稳定的场景下,直接使用它乘以一个系数来驱动 sin 函数,既能获得平滑的动画,又省去了 JavaScript 与 WebGL 之间的通信开销。


解决 LOD 导致的模型消失

应用 Shader 后,可能会遇到**“相机拉远后模型突然消失”**的问题。这是 Cesium 的 SSE (Screen Space Error) 优化机制导致的。

需要在加载 Tileset 时覆盖以下配置:

const tilesetOptions = {
  // ...应用我们的 Shader
  customShader: roofShader,
  
  // 关键配置 1:最大屏幕空间误差设为 16(默认可能为 64)
  // 强制 Cesium 在远距离也加载并渲染模型,而不是将其剔除
  maximumScreenSpaceError: 16,
  
  // 关键配置 2:禁止动态 SSE 调整
  // 防止在帧率波动时,Cesium 自动降低画质导致远处的建筑闪烁或消失
  dynamicScreenSpaceError: false
};

// 加载模型
const tileset = await cesium.Cesium3DTileset.fromUrl(url, tilesetOptions);

// 双重保险:显式赋值(解决部分 Tileset 初始化时配置未生效的偶发问题)
tileset.customShader = roofShader;
mapViewer.scene.primitives.add(tileset);

总结

通过 CustomShader,我们以较低的成本实现了自定义的材质效果。

  • 设计上:简化逻辑,不依赖纹理和坐标判断。
  • 性能上:利用内置变量和 PBR 属性,计算成本低。
  • 工程上:通过合理的 LOD 设置,保证了模型在不同距离下的可见性。