资讯动态

Cesium进阶实战:把ShaderToy特效搬进数字孪生场景

发布时间:2026/10/3 17:25:07 来源:尧图企业网站定制
前不久一个做智慧园区项目的朋友打来电话问我在Cesium里怎么让湖面水波看起来有流动感。我反问他Cesium自带了Water材质你没试过他说试了那水面看起来像一层蓝绿色的果冻客户看了直摇头。我听完笑了笑跟他讲你要是想靠调参数调出真实的水那大概率要失望了。真正要迈过去的坎是着色器。这篇是Cesium进阶学习系列的第四篇主题是ShaderToy风格的着色器在Cesium里的落地。我会从Cesium的材质系统讲起带你看懂Fabric语法再讲怎么把ShaderToy上的特效移植到Cesium中最后给三个可以直接放进项目的实战案例以及我调试着色器时积累的经验。适合那些已经会用Entity、Primitive往地球上放东西但总觉得效果不够炸裂想进一步深入渲染底层的开发者。1. 为什么Cesium进阶路上着色器是一道绕不过去的坎1.1 从一次“做水面效果”的需求说起数字孪生和智慧城市类的项目这两年我接触得很多。客户的需求翻来覆去就是那几类园区楼栋要高亮、河面水面要有流动感、重点区域要做雷达扫描、地下管线的颜色要跟着数据动态变化。这些需求有个共同特点Cesium自带的基础材质做不出细节或者说做得出来但特别“塑料感”。我那位朋友就是把Cesium的Water材质参数来回调了一下午Frequency、AnimationSpeed、NormalMap换了个遍最后水还是像一层油。为什么因为Water材质本质上是一套固定的着色器程序它只暴露了有限的几个uniform参数给你。你想让水波有多个尺度叠加的层次感想在水面上加上视觉误差的法线扰动想在岸边做菲涅尔渐变这些逻辑都会涉及重写着色器源码而不是简单调参能解决的。所以做Cesium进阶第一关就是破除对“材质参数”的依赖学会直接写GLSL。水、光、高亮、扫描、热力这些特效的答案最后都落在着色器里。1.2 Cesium自带的“材质”不是什么玄学很多人用Entity设material的时候都把它当黑盒。其实Cesium的材质体系非常透明你在new Cesium.Material({...})里传入的fabric配置最终都会被编译成一个片元着色器交给GPU执行。Cesium之所以叫“智能地球底座”渲染这块做了大量封装但封装不等于黑盒。任何一个Entity上的PolygonGeometry最终都会走Primitive渲染管线。Primitive由Geometry和Appearance组成Appearance内部持有MaterialMaterial内部持有ShaderSource。这个链条捋清楚之后你就知道改材质就是在改片元着色器的一句话。更直接一点在浏览器里打开Cesium源码目录Source/Shaders/Materials/你能看到每个内置材质的GLSL源码比如Water.glsl、BumpMap.glsl、PolylineGlow.glsl。这些文件有挂满贴图的复杂版本也有几行就搞定一个基础效果的简单版本。看一遍源码比你上十节网课都管用。1.3 ShaderToy与Cesium的同频学习法ShaderToy是shader圈子里的“GitHub”上面大量实时渲染的着色器效果从水面、火焰、云层到各种抽象视觉全是片元着色器写的。你不需要建模、不需要光照设置打开网页就能跑起来。对Cesium开发者来说ShaderToy最大的价值是算法验证。你可以在ShaderToy上快速验证一个噪声函数、一套水波算法、一个UV动画的效果确认视觉OK了再把它移植到Cesium里。这个工作流能帮你省掉大量反复编译调试的时间。有人问Cesium的着色器和ShaderToy是完全一样的语言吗答案是都是GLSL但环境不同。ShaderToy的入口是mainImageCesium的材质入口是czm_getMaterialShaderToy拿到的是像素坐标Cesium拿到的是纹理坐标和法线等几何信息。所以两者的结合点在于“算法”而不是“代码”。下一章我就把Cesium这套材质系统的运行机制拆开讲清楚。2. Fabric材质语法Cesium把GLSL包起来的那层“糖衣”2.1 一个最小自定义材质长什么样要理解Cesium材质系统直接看代码最快。下面这段代码创建了一个最基础的自定义材质效果是让一个矩形表面呈现出左右两种不同颜色透明度90%。const material new Cesium.Material({ fabric: { type: MyTwoColor, uniforms: { leftColor: Cesium.Color.fromCssColorString(#ff6600), rightColor: Cesium.Color.fromCssColorString(#0066ff) }, source: czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material czm_getDefaultMaterial(materialInput); if (materialInput.st.x 0.5) { material.diffuse leftColor.rgb; material.alpha leftColor.a; } else { material.diffuse rightColor.rgb; material.alpha rightColor.a; } return material; } } });这段代码里有三个核心要素。type是材质名相当于你在Cesium内部注册了一个新材质可以在别处通过Cesium.Material.fromType(MyTwoColor)直接复用uniforms是外部传入的变量相当于给着色器传参source是真正的片元着色器源码里面必须实现czm_getMaterial函数。理解了这个结构再看Cesium内置材质就一目了然了。比如自带的Color材质本质上就是返回一个固定diffuse的着色器Stripe材质就是根据纹理坐标做条纹渐变。所有材质都是这套模板的变体。2.2 czm_materialInput片元着色器能拿到哪些“料”在czm_getMaterial里你一定会用到materialInput这个结构体。它就像GPU塞给你的一个信封里面装满了当前片元在场景中的各种信息。我常用的几个字段是st/uv纹理坐标。st范围是0到1uv是以0.5为中心归一化到-0.5到0.5的版本。做大多数UV动画、条纹、SDF计算都从这两个出发。positionToEyeEC当前片元指向相机位置的向量。做菲涅尔、边缘光、水面反射的时候必须用到它。normalEC当前片元的法线在眼睛坐标系下。注意这里的法线是几何法线如果你想用法线贴图需要用纹理采样出的法线结合tangentEC和binormalEC做切线空间变换。longitudeLatitude当前片元的经纬度适合做区域渐变、行政边界相关特效。position3D世界坐标。像雷达扫描按距离衰减这类效果用世界坐标比用纹理坐标方便得多。理解了这些字段再看ShaderToy就有一个很自然的坐标映射思路ShaderToy里的fragCoord是屏幕像素坐标Cesium里的materialInput.st是对象的局部纹理坐标。要把ShaderToy的算法搬过来第一步就是搞清楚它依赖的是哪个坐标。2.3 czm_material的五个输出字段怎么理解czm_getMaterial的返回值是一个czm_material结构体有很多人刚接触时不太理解为什么要返回这么多字段。其实这五个字段对应的是光照模型里的几个关键概念diffuse物体本身的固有色即“它是什么颜色”。光线打到上面后这个颜色会被反射出来。specular镜面反射强度控制高光的明显程度。数值越大物体表面越像塑料或镜子。shininess高光锐度控制高光斑的大小。数值越小高光越柔和越大越尖锐。emission自发光颜色。这个颜色不依赖光源直接叠加到最终颜色上。做扫描特效、发光体基本都用它。alpha透明度。1.0为完全不透明0.0为完全透明。实际写材质时你要做的核心工作就是根据materialInput输入的各种坐标和向量用算法算出这五个字段的值。比如水面材质就是根据波浪高度和法线扰动动态改变diffuse和specular雷达扫描特效就是让emission随距离和时间变化。2.4 材质与自定义Primitive的分工线Cesium里实现自定义渲染效果有两条路。一条是上面说的Material路线它被设计用来给现成的几何体“换肤”比如给PolygonGeometry、RectangleGeometry、PolylineGeometry附上动态材质。另一条是完全自定义Primitive你可以自己定义顶点数据、顶点着色器、片元着色器甚至自己管理绘制状态。什么时候走哪条路我的经验是如果效果是作用在某个已有的几何表面比如给楼层立面换动态贴图、给地面加扫描圈走Material就够了开发量小代码也好维护。如果你要往地球上画一个全新的几何体比如上万粒子的流动线条、体积云、自己算顶点位置的变形体那就必须自定义Primitive。自定义Primitive的核心是重写Primitive.update创建DrawCommand挂上自己的ShaderProgram。这条路普通人不需要上来就学但理解它的存在你就能在工作中判断“该不该用shader”以及“该用哪个级别的shader”。3. 把ShaderToy作品搬进Cesium从像素坐标到材质输入3.1 入口函数换成czm_getMaterialShaderToy里的每一个效果都有同一个入口void mainImage(out vec4 fragColor, in vec2 fragCoord)fragCoord是当前片元的像素坐标你需要自己除iResolution把它归一化到0到1之间。Cesium材质的世界里入口变成了czm_material czm_getMaterial(czm_materialInput materialInput)返回值不是一个vec4颜色而是一个czm_material结构体。移植时最直观的做法就是把ShaderToy里算出的颜色写到material.diffuse和material.alpha其他字段用czm_getDefaultMaterial填充。举个例子ShaderToy里一个最简单的渐变void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv fragCoord / iResolution.xy; fragColor vec4(uv, 0.0, 1.0); }移植到Cesium就变成czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material czm_getDefaultMaterial(materialInput); vec2 uv materialInput.st; material.diffuse vec3(uv, 0.0); material.alpha 1.0; return material; }逻辑完全一一对应只是换了壳。这样写你就能在Cesium里看到红绿渐变的矩形。3.2 iResolution、iTime、iMouse怎么落进Cesium这是移植过程中最容易卡住的地方。你翻开的ShaderToy代码里十有八九会有这么三个uniformuniform vec3 iResolution; uniform float iTime; uniform vec4 iMouse;iResolution在Cesium里没有直接对应物。如果你的算法需要像素分辨率来做缩放可以在uniforms里声明一个vec2 uResolution然后在JS侧传入当前的canvas画布尺寸。不过大多数效果用materialInput.st就够不需要知道具体像素。iTime在Cesium里有两种接法。一种是用内置的czm_frameNumber / 60.0当作秒数但精度和可控制性一般而且全局帧号是共用的不同材质的动画时间会同步。更推荐在uniforms里声明一个float uTime然后在渲染循环里更新它material.uniforms.uTime performance.now() / 1000.0;这样不同材质可以有不同的时间轴暂停、加速都很容易控制。iMouse一般用于交互特效。Cesium材质里没有全局鼠标状态的统一传参需要自己做。监听screenSpaceEventHandler的MOUSE_MOVE事件把鼠标在地图上的经纬度或画布像素坐标写进uniform。Cesium默认不开启持续渲染如果你不操作鼠标画面是静止的。要让材质动起来需要把viewer.scene.requestRenderMode设为true的对应逻辑处理好或直接调用scene.requestRender()。3.3 坐标系与精度差异最容易翻车的地方ShaderToy里很多效果直接用像素坐标当世界坐标比如画一个以屏幕中心为圆心的圆环。但Cesium的材质作用在三维几何体上纹理坐标st是物体局部坐标而且它的范围取决于你的几何体展开方式。两个最容易翻车的地方我单独拿出来说。一个是UV方向。Cesium中部分几何体的st原点在左下角部分在左上角不同几何体不一样。你从ShaderToy搬运一个上下渐变的效果如果方向反了第一时间不要怀疑算法先试试把st.y改成1.0 - st.y。另一个是精度问题。ShaderToy是全屏Quad一个像素对应很小的物理尺寸坐标数值范围不大。但Cesium的材质可能贴在跨度几十公里的地面上直接拿经纬度或世界坐标算很容易因为浮点数精度不足出现“顶点抖动”“画面闪烁”。这种情况要从需求出发看是否需要把坐标中心移到片元附近或者用地图形学里常见的“相对坐标”技巧。3.4 一份可复用的移植步骤我每次从ShaderToy往Cesium搬代码都会按固定流程走这里整理出来供参考先看ShaderToy的效果是什么纯颜色变化、UV动画、依赖时间、依赖鼠标还是依赖某些纹理输入。把mainImage里的核心算法拆出来先不管fragColor只看它用了哪些变量。把fragCoord / iResolution.xy替换成materialInput.st把iTime替换成自定义的uTime。把最后写进fragColor的颜色按需求写进material.diffuse、material.emission、material.alpha。在Cesium里挂到一个简单的多边形上看效果先做静态验证再加时间动画。效果确认后再考虑性能优化和参数抽取。这套流程走几十遍之后你再看ShaderToy的代码脑子里会自动浮现“它在Cesium里会是什么样的”这种翻译能力就练出来了。4. 三个能直接用进项目的着色器案例4.1 案例一可调参数的水面材质用一个简化版的水面效果来说明完整流程。这个材质基于多层正弦波叠加虽然不如复杂的水面渲染算法精致但比Cesium自带Water材质的“果冻感”好得多关键是你能完全控制它。const waterMaterial new Cesium.Material({ fabric: { type: RiverWater, uniforms: { uTime: 0.0, uBaseColor: Cesium.Color.fromCssColorString(#1a6f8a), uDeepColor: Cesium.Color.fromCssColorString(#0d3b4a), uWaveStrength: 0.08 }, source: czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material czm_getDefaultMaterial(materialInput); vec2 st materialInput.st; float t uTime; // 多层正弦波叠加产生不规则的涟漪 float wave1 sin(st.x * 10.0 t * 1.2) * cos(st.y * 8.0 t * 0.8); float wave2 sin((st.x st.y) * 16.0 t * 2.0) * 0.6; float wave wave1 * 0.7 wave2 * 0.3; // 用波浪值扰动纹理坐标模拟水面的扭曲感 vec2 distortedSt st wave * uWaveStrength; // 根据波浪值在浅色和深色之间过渡 float mixFactor 0.5 0.5 * sin(distortedSt.x * 20.0 distortedSt.y * 15.0 t); vec3 waterColor mix(uBaseColor.rgb, uDeepColor.rgb, mixFactor); material.diffuse waterColor; material.specular 0.6; material.shininess 12.0; // 菲涅尔近似视线越倾斜反射越强这里简化处理 vec3 toEye normalize(-materialInput.positionToEyeEC); float fresnel pow(1.0 - abs(toEye.z), 2.0); material.emission vec3(0.05, 0.1, 0.12) * fresnel; material.alpha 0.95; return material; } } });挂载的时候如果你要给一个河面多边形用const riverEntity viewer.entities.add({ polygon: { hierarchy: Cesium.Cartesian3.fromDegreesArray([...]), material: waterMaterial } });然后在viewer.scene.preUpdate事件里更新时间viewer.scene.preUpdate.addEventListener(() { waterMaterial.uniforms.uTime performance.now() / 1000.0; });这个案例想说明的重点是materialInput.positionToEyeEC可以用来做菲涅尔st可以经过算法扰动之后再用于采样和计算。掌握了这两个思路你可以把ShaderToy里大量水面算法都搬过来。4.2 案例二3DTiles动态光扫效果3DTiles模型的高亮、扫光、单体化是Cesium进阶绕不开的高频需求。Cesium新版提供了CustomShader专门用来给3D Tiles模型自定义顶点和片元着色非常强大。const customShader new Cesium.CustomShader({ uniforms: { uTime: { type: Cesium.UniformType.FLOAT, value: 0.0 }, uScanColor: { type: Cesium.UniformType.VEC3, value: new Cesium.Cartesian3(0.0, 0.8, 1.0) } }, fragmentShaderText: void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { // 通过模型坐标的Y方向做从上到下的扫光 vec3 modelPos fsInput.attributes.positionMC.xyz; float scan sin(modelPos.y * 2.0 uTime * 2.5) * 0.5 0.5; material.diffuse mix(material.diffuse, uScanColor, scan * 0.4); material.emission uScanColor * scan * 0.6; } }); tileset.customShader customShader;然后在渲染循环里更新viewer.scene.preUpdate.addEventListener(() { customShader.setUniformValue(uTime, performance.now() / 1000.0); });这个效果非常适合用在重点建筑、区域标注、单栋楼宇高亮等场景。需要注意两点第一CustomShader是Cesium 1.89之后的正式API更早的版本里是ModelExperimental模式接口略有差异第二如果模型自身带有PBR材质你的material.diffuse修改的是基础色想保留纹理细节就多写几行拿到material.baseColor再叠加不要整个覆盖。4.3 案例三多边形雷达扫描特效雷达扫描也是数字孪生项目里点的比较多的效果。我们先在Cesium里画一个圆形多边形再给它附上一个带时间动画的径向渐变材质就能模拟出扫描圈。const scanMaterial new Cesium.Material({ fabric: { type: RadarScan, uniforms: { uTime: 0.0, uColor: Cesium.Color.fromCssColorString(#00eaff), uRadius: 500.0 }, source: czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material czm_getDefaultMaterial(materialInput); vec2 st materialInput.st - vec2(0.5); // 计算片元到圆心的距离并映射为0-1 float dist length(st) * 2.0; float angle atan(st.y, st.x); // 旋转的扫描臂角度随时间变化 float sweepAngle mod(uTime * 0.8, 6.2831853); float angleDiff abs(mod(angle - sweepAngle 3.14159265, 6.2831853) - 3.14159265); // 扫描臂是一条高亮窄带后方是渐隐尾巴 float beam exp(-angleDiff * 8.0); float rangeFade 1.0 - dist; vec3 color uColor.rgb * beam * rangeFade; material.emission color * 2.0; material.diffuse vec3(0.0); material.alpha beam * rangeFade * 0.8; return material; } } }); viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(120.12, 30.28), ellipse: { semiMajorAxis: 500.0, semiMinorAxis: 500.0, material: scanMaterial } });里面用了atan算角度、用st - vec2(0.5)把圆心移到多边形中心这些思路在ShaderToy的极坐标类效果里非常常见。做这类效果时如果发现扫描圈和几何体对不齐先检查st的中心化是否正确再看纹理坐标的V方向是否反了。5. 调试与性能着色器开发真正见功夫的地方5.1 着色器调试三板斧颜色编码、增量简化、单测写着色器最痛苦的就是看不见中间量。GLSL没有断点没有console.log出了错只能黑屏或者糊成一团。我调试Cesium着色器主要靠三板斧。第一板斧是颜色编码。想确认某个值到底算出来是什么范围直接把它映射到颜色上输出。比如material.diffuse vec3(materialInput.st, 0.0)就能看到纹理坐标的分布是否和预期一致。fragCoord在不同区域显示不同的RGB一看便知。这个方法尤其适合排查UV方向、坐标中心化这类问题。第二板斧是增量简化。ShaderToy上很多效果代码动辄上百行不要直接全量搬进来。把特效拆成几个阶段比如先画静态渐变再叠加噪声再加时间最后做光照。每加一个阶段就验证一次。绝大多数着色器问题都是因为一次引入太多变量导致的。第三板斧是单测。Cesium材质本质是函数你可以把czm_getMaterial里那段核心算法单独抽出来放到ShaderToy里跑一遍用同样的输入验证输出把环境差异和算法错误彻底分开。很多看起来很诡异的Bug其实是Cesium的坐标系和ShaderToy的屏幕坐标打架跟算法本身没关系。5.2 性能优化片元着色器里哪些操作会悄悄吃掉帧率移动端和低端机的表现就不说了就算在桌面端一个全屏特效如果写得不好也能把帧率从60拉到10。我见过的最典型性能杀手有这么几类。第一类是过度采样。很多人直接搬ShaderToy的SDF场景里面动不动就是几十次的for循环采样每次采样还有复杂的pow和sqrt。片元着色器是逐像素执行的对于覆盖半个屏幕的多边形来说这就是几百万次计算。务必先用低分辨率、低循环次数跑确认效果之后再慢慢提升质量。第二类是动态分支。在片元着色器里写if (dist 0.5)这种量级的分支GPU的并行度会下降因为同一批次执行的线程都得把所有分支走一遍。能用mix、smoothstep、step这类函数替代的分支尽量替代这也是习惯问题。第三类是又要实时计算又要在CPU侧做逻辑。有些效果完全可以在CPU上预计算成纹理比如热力图的权重场你把数据算成一张低分辨率纹理传给shader比在shader里逐像素计算要高效得多。这就是为什么很多成熟的Cesium热力图方案会选择“先画到离屏纹理再贴到地球上”的原因。Cesium里还有个细节默认模式下场景不会每帧都重绘但如果你启用了这种持续渲染模式一个shader效果一直跑GPU始终满载。做项目交付时务必评估这个效果是不是需要一直动如果只需要在特定事件期间动事件结束就停掉时间更新让场景回到静默模式。5.3 常见坑位与排查思路整理几个我踩过多次的坑给后来人当路标。第一个坑是uniform更新时机问题。很多人直接写material.uniforms.uTime performance.now() / 1000.0但忘了Cesium的preUpdate事件触发顺序。如果你在scene.postUpdate里更新uniform某些渲染批次可能已经提交画面会滞后一帧。建议统一在preUpdate里更新动画相关uniform。第二个坑是材质复用冲突。同一个Material实例如果被多个Entity共用你更新uniform就会影响所有Entity。如果只想让某一个Entity出现扫描动画要给这个Entity单独创建一个Material实例。第三个坑是几何体大小对效果的影响。同一个水面材质贴在一个100米宽的池塘上和贴在一个10公里宽的湖泊上波浪密度会完全不同。纹理坐标是0到1的几何体越大单位纹理覆盖的实际距离就越大。实际项目里一定不要用固定频率要按经纬度跨度或者世界坐标来动态换算频率参数。第四个坑是不同Cesium版本的API差异。旧版的ModelExperimental和新版的CustomShader接口不完全相同网上很多教程跑的代码在新版本里已经废弃。遇到编译报错不要慌先确认你的Cesium版本再看文档里对应的API变更记录。我见过不少朋友把老代码直接抄进新版本报错后以为是自己写错了折腾半天。还有一个容易被忽略的Cesium的着色器在WebGL2模式下和WebGL1模式下一些GLSL写法会有差异。如果你的项目启用了WebGL2注意检查texture2D这类旧写法是否需要替换成texture。这类问题在浏览器控制台会有明确的报错提示按照提示逐行改就行。最后再分享一点个人体会从我接触Cesium到现在最大的感受是不要把着色器当成一个“高级功能”去回避。它其实就是一段用GLSL写的小程序而Cesium已经给了你非常友好的接入方式。你不需要成为图形学专家只需要理解纹理坐标、时间、颜色这几个核心概念就能做出比自带材质好得多的效果。平时浏览ShaderToy的时候可以多留意那些短小精悍的效果十个以内的代码就能驱动出让人惊艳的画面。这些就是天然的练习素材每翻译一个你对坐标、时间、光照的直觉就会更准一点。下次面试官再问“Cesium怎么给模型加一个动态高亮效果”你就能从材质讲到CustomShader再讲到性能优化一套组合拳打下来对方就知道你是真的做过。如果你已经在项目里实现了一些有趣的Cesium着色器效果欢迎拿出来交流。持续积累把翻译ShaderToy变成肌肉记忆你的Cesium进阶之路会顺畅很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑