CesiumJS 地图主题系统:用着色器打造实时滤镜

大家好,今天来拆解一个 CesiumJS 实战项目 ------ 地图图层主题系统。通过两套着色器方案叠加,实现实时颜色滤镜、深度遮罩和参数化调参,最终用 lil-gui 搭建出一个可交互的调色面板。

完整 HTML 源码在文末,单文件即可运行。

环境搭建

项目依赖三个库,全部走 CDN 无需本地构建:

  • Vue 3:驱动 UI 响应式,这里用 Options API 管理 lil-gui 面板状态
  • CesiumJS:三维地球引擎,负责底图渲染和着色器管线
  • lil-gui:轻量调参面板,绑定到 Cesium 图层属性和自定义着色器的 uniform
html 复制代码
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.13/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cesium@1.133.0/Build/Cesium/Cesium.js"></script>
<script type="module">
  import GUI from "https://cdn.jsdelivr.net/npm/lil-gui@0.19.1/dist/lil-gui.esm.js";
</script>

底图与相机

使用高德地图瓦片作为底图,通过 UrlTemplateImageryProvider 加载。相机初始定位在中国中部上空约 14,000 公里处,俯视视角。

javascript 复制代码
let imageryLayer = viewer.imageryLayers.addImageryProvider(
  new Cesium.UrlTemplateImageryProvider({
    url: "https://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=2&style=8&x={x}&y={y}&z={z}",
    maximumLevel: 18,
  }),
);

注意两个关键设置:关闭 FXAA 避免干扰深度纹理采样,启用 depthTestAgainstTerrain 确保地形遮挡正确。

javascript 复制代码
viewer.scene.postProcessStages.fxaa.enabled = false;
viewer.scene.globe.depthTestAgainstTerrain = true;

核心一:PostProcessStage 色彩滤镜

这是整个系统的核心。Cesium 的 PostProcessStage 可以注入自定义片段着色器,在全屏四边形上执行后处理。

着色器接收两个纹理:colorTexture(当前帧的颜色缓冲)和 depthTexture(深度缓冲)。关键技巧在于用深度值区分天空和地面

glsl 复制代码
uniform sampler2D colorTexture;
uniform sampler2D depthTexture;
uniform vec3 filterColor;
uniform bool enabled;

void main() {
    vec4 color = texture(colorTexture, v_textureCoordinates);
    float depth = texture(depthTexture, v_textureCoordinates).r;

    if (enabled && depth < 0.999) {
        vec3 inverted = vec3(1.0) - color.rgb;
        vec3 filtered = inverted * filterColor;
        out_FragColor = vec4(filtered, color.a);
    } else {
        out_FragColor = color;
    }
}

深度阈值 0.999 是关键:Cesium 中天空盒渲染到远平面,深度值接近 1.0。小于 0.999 意味着像素属于地球表面或地形 ------ 只对这些区域做反色 + 滤镜。天空保持原样,避免出现怪异色块。

算法分两步:

  1. 反色vec3(1.0) - color.rgb,把浅色底图变成深色基底
  2. 滤镜乘法inverted * filterColor,每个通道独立缩放,实现任意色调

通过 JS 侧更新 filterColor uniform,就能在运行时切换主题色:

javascript 复制代码
colorFilterStage.uniforms.filterColor = new Cesium.Cartesian3(r, g, b);
colorFilterStage.uniforms.enabled = true;

核心二:Globe 表面着色器注入

PostProcessStage 作用于全屏,无法精确控制地球表面内部的光照计算。为此项目同时修改了 Cesium 内部的 Globe 片段着色器。

Cesium 的 _surfaceShaderSet.baseFragmentShaderSource.sources 是一个字符串数组,包含 Globe 表面的 GLSL 源码。代码在 czm_saturation 调用之后注入自定义逻辑:

javascript 复制代码
const baseFragShader =
  viewer.scene.globe._surfaceShaderSet.baseFragmentShaderSource.sources;

for (let i = 0; i < baseFragShader.length; i++) {
  const strS = "color = czm_saturation(color, textureSaturation);\n#endif\n";
  let strT = strS + `
    color.r = 1.0 - color.r;
    color.g = 1.0 - color.g;
    color.b = 1.0 - color.b;
    color.r = color.r * ${filterR} / 255.0;
    color.g = color.g * ${filterG} / 255.0;
    color.b = color.b * ${filterB} / 255.0;
  `;
  baseFragShader[i] = baseFragShader[i].replace(strS, strT);
}

⚠️ 这是 Cesium 内部 API,未在文档中公开。版本升级可能导致 _surfaceShaderSet 结构变化,建议锁定 Cesium 版本号。

为什么两套着色器都要?

  • PostProcessStage 覆盖全屏(含地形、3D Tiles、矢量),但无法区分地球表面内部的光照阶段
  • Globe 着色器注入仅在 Globe 表面生效,但受 Cesium 批次管理(多个 shader variant 对应不同光照条件)
  • 两者叠加才能保证:无论 Day/Night 切换、地形开关、视角变化,滤镜效果一致

核心三:主题切换系统

switchTheme() 将两套着色器的参数打包成一个主题对象,一次调用完成全局切换:

javascript 复制代码
switchTheme(theme) {
  if (theme.isOriginal) {
    colorFilterStage.uniforms.enabled = false;
    baseLayer.brightness = 1;
    baseLayer.contrast = 1;
    baseLayer.gamma = 1;
    baseLayer.hue = 0;
    baseLayer.saturation = 1;
  } else {
    colorFilterStage.uniforms.enabled = true;
    colorFilterStage.uniforms.filterColor = new Cesium.Cartesian3(
      theme.filterRGB[0], theme.filterRGB[1], theme.filterRGB[2]
    );
    baseLayer.brightness = theme.brightness ?? 0.6;
    baseLayer.contrast = theme.contrast ?? 1.8;
    baseLayer.gamma = theme.gamma ?? 0.3;
    baseLayer.hue = 0;
    baseLayer.saturation = 0;
  }
  viewer.scene.requestRender();
}

五个图层属性各有含义:

属性 作用 典型值
brightness 亮度,值越大越亮 0.6
contrast 对比度,增强明暗差异 1.8
gamma 伽马校正,压暗或提亮中间调 0.3
hue 色相偏移 0
saturation 饱和度,0 = 灰度 0

饱和度设为 0 是关键 ------ 去掉底图原有色彩后,着色器的 filterColor 乘法才能精确控制最终色调,不会被原图颜色干扰。

lil-gui 调参面板

lil-gui 使用 ESM 引入,通过链式 API 绑定控件:

javascript 复制代码
let params = {
  colorArray: [255, 0, 0],
  brightness: 1.0,
  contrast: 1.0,
  saturation: 1.0,
  hue: 0.0,
  gamma: 1.0,
  reset: () => this.switchTheme({ isOriginal: true }),
};

myLilGui
  .addColor(params, "colorArray").name("颜色")
  .onChange((value) => this.switchTheme({ filterRGB: value }));

myLilGui
  .add(params, "brightness", 0.1, 10.0, 0.1).name("亮度")
  .onChange(() => { this.imageryLayer.brightness = params.brightness; });

颜色控件走 switchTheme() 触发完整主题更新(着色器 + 图层属性),而亮度/对比度等滑块直接修改 imageryLayer 属性。这样设计的原因是:颜色变化需要着色器重新计算,而亮度等属性是 Cesium 内置的图层参数,直接赋值即可实时生效。

重置按钮传入 { isOriginal: true } 一次性还原所有参数,比逐个恢复更简洁。

避坑笔记

  1. FXAA 与深度纹理 :开启 FXAA 会影响深度纹理采样精度,导致 depth < 0.999 判断失效,必须在初始化时关闭
  2. ESM 与 global 混用 :Vue 走 <script> 全局加载,lil-gui 走 import 模块加载,Cesium 是 UMD。三者共存的关键是 Cesium 用 <script> 加载且 window.CESIUM_BASE_URL 必须在 import 之前设置
  3. saturation = 0 是故意的 :不是 bug。去饱和后让着色器的 filterColor 乘法成为唯一色彩来源,保证主题色一致
  4. Globe 着色器是数组baseFragmentShaderSource.sources 是数组而非单个字符串 ------ Cesium 为不同光照条件生成了多个 shader variant,必须全部替换

完整代码

见原文:https://mp.weixin.qq.com/s/Tkwbxhf8eeL7ZB4YyqnitA

总结

本文拆解的地图主题系统,核心思路是两套着色器叠加 + 参数化面板

  • PostProcessStage 用深度纹理做天空/地面分割,只在非天空区域做反色和滤镜乘法
  • Globe 着色器注入在表面渲染阶段做同样的色彩变换,确保所有 shader variant 覆盖
  • lil-gui 将颜色、亮度、对比度、伽马等参数暴露为交互控件,实时调参
  • switchTheme() 把离散的参数组合打包为主题对象,一键切换

这个架构可以扩展为预设多套主题、接入后端配色方案,甚至做成可视化主题编辑器。把单文件 HTML 部署到任何静态服务器就能运行,无需构建工具。


相关推荐
用户298698530141 小时前
在 React 中通过 JavaScript 实现 Excel 单元格、行与列的锁定保护
javascript·react.js·excel
এ慕ོ冬℘゜1 小时前
深入理解 JavaScript 事件对象:从 target 到 preventDefault 的实战指南
开发语言·前端·javascript
Listen·Rain1 小时前
Vue概述
前端·javascript·vue.js
小雪_Snow2 小时前
JS 数组的遍历 —— 两种方式详解
前端·javascript
小二·2 小时前
WebGPU 浏览器端跑大模型:让AI在网页里跑起来(WebLLM/Transformers.js实战)
开发语言·javascript·人工智能
心中有国也有家2 小时前
AtomGit Flutter 鸿蒙客户端:零外部资源的应用打包策略
android·javascript·flutter·华为·harmonyos
胡萝卜术11 小时前
从API调用到手写LRU:力扣146「LRU缓存」的哈希表+双向链表终极进化之路
前端·javascript·面试
科技道人11 小时前
记录 默认置灰/禁用 app ‘Search Engine Selector‘ 的disable按钮
开发语言·前端·javascript
shuaijie051817 小时前
强制修改调用接口的api地址。
javascript·vue.js·ecmascript