拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Cesium实体材质完全指南:从Material到动态效果与性能优化

做Cesium开发这几年我最深的一个感触是大多数看起来“花了心思”的三维效果本质都是在跟材质较劲。不管是给园区楼栋铺一层半透明高亮让河流动起来还是把监控画面贴到一面墙上、给轨迹线加上高德风格的箭头最终都会绕到同一个概念上——实体材质Entity Material。这篇文章不打算照官方文档目录讲而是按我做项目时的真实思路来梳理先拆清楚材质体系的运行机制再挨个过内置材质的用法接着讲动态材质和自定义着色器最后用热力图、河流、圆柱体这几个高频场景把知识点串起来。无论你是刚用Cesium拖了个地球、准备做业务图层的新手还是已经写了不少实体、但遇到自定义材质就发怵的开发者这篇应该都能帮上忙。1. 材质这层窗户纸Material和Property到底什么关系1.1 一个最小示例引发的疑问const viewer new Cesium.Viewer(cesiumContainer, { infoBox: false, selectionIndicator: false }) const polygon viewer.entities.add({ polygon: { hierarchy: Cesium.Cartesian3.fromDegreesArray([ 116.20, 39.55, 116.60, 39.55, 116.60, 39.90, 116.20, 39.90 ]), material: Cesium.Color.fromCssColorString(#2E86C1).withAlpha(0.6) } }) viewer.flyTo(polygon)很多新手第一次接触Cesium时最先学会的就是往material字段里填一个颜色然后多边形上色了。于是下意识把material当成“颜色”看待。但如果你去翻Cesium的API文档会发现这里真正接收的是一个叫MaterialProperty的对象而不是直接收Color。这可能让你觉得Cesium在过度设计。但是当你想做“呼吸灯”“闪烁告警”“渐变水面”这些效果时这个设计就显出价值了——因为MaterialProperty是一个会随时间变化的属性容器颜色只是其中最朴素的一种表达。1.2 Property体系材质为什么能“动”Cesium里几乎所有Entity属性位置、方向、高度、材质都用同一套Property机制驱动常见的包括ConstantProperty恒定值不随时间变化CallbackProperty回调函数每次求值都调用SampledProperty采样插值适合连续变化的量TimeIntervalCollectionProperty按时间段切换不同值CompositeProperty组合多个属性。材质属性的工作流程可以概括成两步getType(time)返回当前时刻的材质类型名例如Color、ImagegetValue(time, result)返回这个材质当前的参数颜色、图片地址、平铺次数等。渲染器拿到类型名后去材质缓存MaterialCache里找到对应的shader再把参数作为uniform传进去最终交给GPU绘制。用生活里的话说Property像剧本材质是演员。剧本只要有变化演员下一句台词就会变。CallbackProperty就是剧本里写得比较灵活的那一行每一帧都可以被重读一次。理解这一层之后再去看动态材质就不会觉得玄学了。2. 内置材质详解多数需求其实已经帮你做好了2.1 纯色材质ColorMaterialProperty的细节纯色材质是使用频率最高的写法也最自由。下面三种写法在效果上等价// 方式一直接在 material 里传 ColorCesium 内部会包装成 ColorMaterialProperty material: Cesium.Color.RED // 方式二显式创建 ColorMaterialProperty material: new Cesium.ColorMaterialProperty(Cesium.Color.RED) // 方式三给 ColorMaterialProperty 套一个 CallbackProperty为后续做动态效果留好口子 material: new Cesium.ColorMaterialProperty( new Cesium.CallbackProperty(() Cesium.Color.RED, true) )第三种写法的价值在于当你以后要把红色改成闪烁效果时只需要改动回调里的返回逻辑不用动整体结构。透明度的坑比较多我踩过一次。withAlpha返回的是一个新的Color对象不会修改原对象所以如果你在循环里反复调用color.withAlpha(0.3)去赋值一定要把返回值存下来。另外半透明材质默认走混合渲染管线多个半透明面叠加时会出现我后面第五章要讲的排序问题。2.2 图片贴图ImageMaterialProperty的实用参数贴图在项目里用得极多一个典型的写法是这样material: new Cesium.ImageMaterialProperty({ image: /assets/textures/floor.jpg, repeat: new Cesium.Cartesian2(4, 4), color: Cesium.Color.WHITE.withAlpha(0.8) })几个关键参数说清楚image不仅支持URL字符串还支持Base64数据、HTMLCanvasElement、HTMLVideoElement。这意味着动态生成的canvas比如热力图和视频画面都能直接当作材质贴图不需要额外转格式。repeat控制平铺次数横纵方向可以分别设比如new Cesium.Cartesian2(4, 2)表示横向平铺4次、纵向2次。这里一定要传Cartesian2我见过有人图省事写repeat: 4结果贴图变形排查半天才发现是类型问题。color是叠加色白色表示纹理原样显示其他颜色会和纹理像素做乘法。利用这一点可以给同一张贴图做不同色调省掉做美术资源的功夫。2.3 纹理不够用棋盘格、条纹、网格了解一下有时候我们只是想加一点辅助纹理没必要准备图片资源Cesium内置了三种纯算法生成的材质性能极好。// 棋盘格适合做透明网格辅助线、选中态标识 material: new Cesium.CheckerboardMaterialProperty({ evenColor: Cesium.Color.WHITE, oddColor: Cesium.Color.BLACK, repeat: new Cesium.Cartesian2(8, 4) }) // 条纹适合做警示带、水位线 material: new Cesium.StripeMaterialProperty({ evenColor: Cesium.Color.WHITE, oddColor: Cesium.Color.BLACK, repeat: 10, offset: 0.5, // 相位偏移让条纹“走”起来 orientation: Cesium.StripeOrientation.VERTICAL }) // 网格适合做规划图、路网底纹 material: new Cesium.GridMaterialProperty({ color: Cesium.Color.YELLOW, cellAlpha: 0.2, lineCount: new Cesium.Cartesian2(8, 6), lineThickness: new Cesium.Cartesian2(2, 2) })这三类材质不需要任何外部图片纹理全部由shader实时生成。StripeMaterialProperty的offset参数如果配合CallbackProperty使用可以做出流动的警戒线效果后面讲动态材质时会用到。2.4 线材质的特殊效果箭头、虚线、发光、描边线材质的应用场景很丰富尤其是PolylineArrowMaterialProperty几乎就是高德地图风格箭头在Cesium里的标准答案viewer.entities.add({ polyline: { positions: Cesium.Cartesian3.fromDegreesArray([ 116.30, 39.70, 116.40, 39.70, 116.45, 39.80 ]), width: 12, material: new Cesium.PolylineArrowMaterialProperty( Cesium.Color.fromCssColorString(#FF9800) ) } })其他几个线材质的参数我用表格列一下用的时候直接对号入座材质类参数典型用途PolylineArrowMaterialPropertycolor流向、路线、高德箭头PolylineDashMaterialPropertycolor, dashLength, dashPattern规划线、虚线边界PolylineGlowMaterialPropertycolor, glowPower, taperPower路径高亮、动态光带PolylineOutlineMaterialPropertycolor, outlineColor, outlineWidth描边边界、强调线条PolylineDashMaterialProperty的dashPattern是一个十六进制位掩码比如0x0F0F表示按“短划-间隔”排列调起来需要一点直觉建议先在本地小范围试效果再固化到配置里。3. 让材质动起来动态材质的三条技术路线3.1 CallbackProperty写一个呼吸灯效果动态材质最简单的入口就是CallbackProperty。它的第二个参数isConstant是个大坑传true表示系统认为这个属性不会随时间变化callback可能只执行一次要让它每帧都重新求值必须传false。来看一个多边形呼吸灯的完整写法const breatheMaterial new Cesium.ColorMaterialProperty( new Cesium.CallbackProperty(() { const t viewer.clock.currentTime.secondsOfDay * 2 const alpha 0.3 0.5 * (Math.sin(t) * 0.5 0.5) return Cesium.Color.fromCssColorString(#FF5722).withAlpha(alpha) }, false) ) viewer.entities.add({ polygon: { hierarchy: Cesium.Cartesian3.fromDegreesArray([...]), material: breatheMaterial } })这里我用Math.sin把透明度控制在 0.3 到 0.8 之间视觉上就是一段呼吸般的光晕。实际开发中闪烁告警、扫描高亮、时变着色都可以用同一套思路。3.2 Fabric自定义材质河流流动效果完整实现当内置材质满足不了需求时就要自己写材质了。Cesium定义材质用的是Fabric格式本质是一个JSON描述核心包含三块type材质类型名、uniformsshader参数表、components或source着色器逻辑。先注册一个自定义材质类型。注意这一步用的是Cesium材质注册表属于“文档没正式公开但社区一直在用”的API写法如下Cesium.Material._materialCache.addMaterial(RiverFlow, { fabric: { type: RiverFlow, uniforms: { u_tex: /textures/water.jpg, u_speed: 0.02, u_alpha: 1.0 }, source: uniform sampler2D u_tex; uniform float u_speed; uniform float u_alpha; czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material czm_getDefaultMaterial(materialInput); vec2 st materialInput.st; st.s fract(st.s czm_frameNumber * u_speed * 0.001); vec4 color texture2D(u_tex, st); material.diffuse color.rgb; material.alpha color.a * u_alpha; return material; } } })czm_frameNumber是Cesium提供的帧号全局变量会随着渲染自动递增用它做动画就能保证材质一直在动。采样坐标对s轴做了偏移视觉上就是纹理横向流动。接下来要给Entity用这个材质还需要一个自定义MaterialProperty类。因为Entity侧接收的是Property不是裸的Materialclass RiverMaterialProperty extends Cesium.MaterialProperty { constructor(options) { super() this._definitionChanged new Cesium.Event() this._speed options.speed this._alpha options.alpha } getType() { return RiverFlow } getValue(time, result) { if (!Cesium.defined(result)) { result {} } result.u_speed this._speed result.u_alpha this._alpha return result } equals(other) { return other instanceof RiverMaterialProperty this._speed other._speed } }挂到实体上viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(116.30, 39.60, 116.50, 39.70), material: new RiverMaterialProperty({ speed: 0.8, alpha: 0.9 }) } })如果你嫌定义Property类太绕还有一个更直接的方案用Primitive。Primitive直接接收Material实例不用做任何包装。const primitive new Cesium.Primitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: new Cesium.RectangleGeometry({ rectangle: Cesium.Rectangle.fromDegrees(116.30, 39.60, 116.50, 39.70) }) }), appearance: new Cesium.MaterialAppearance({ material: new Cesium.Material({ fabric: { type: RiverFlow, uniforms: { u_tex: /textures/water.jpg, u_speed: 0.02, u_alpha: 1.0 } } }), faceForward: true }) }) viewer.scene.primitives.add(primitive)3.3 视频贴图实时监控墙的三行代码视频贴图在智慧园区、安防项目中非常实用。原理很简单ImageMaterialProperty的image字段直接支持把video元素当作纹理源。const video document.createElement(video) video.src http://example.com/camera.mp4 video.autoplay true video.loop true video.muted true // 重要浏览器自动播放策略要求静音 viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(116.30, 39.60, 116.50, 39.70), material: new Cesium.ImageMaterialProperty({ image: video }) } })注意video必须真正处于播放状态纹理才会更新。如果画面不动多半是自动播放被拦截了检查muted是否设置。把多路视频贴到楼栋侧面就能拼出一面监控墙这个场景客户反馈一直很好。4. 高频场景拆解热力图、圆柱体与动态光照4.1 热力图canvas ImageMaterialPropertyCesium本身不带热力图组件但实现思路很成熟用前端热力图库生成一张带透明通道的canvas再把canvas作为贴图贴到目标区域上。以heatmap.js为例完整流程如下import h337 from heatmap.js // 1. 生成一个离屏容器渲染热力图 const heatContainer document.createElement(div) heatContainer.style.width 500px heatContainer.style.height 500px document.body.appendChild(heatContainer) // 必须先挂到DOM上heatmap.js才能正常绘制 const heatmap h337.create({ container: heatContainer, radius: 40 }) heatmap.setData({ max: 100, data: [ { x: 100, y: 100, value: 80 }, { x: 250, y: 200, value: 60 }, { x: 400, y: 150, value: 90 } ] }) // 2. 导出成图片地址 const dataUrl heatmap.getDataURL() document.body.removeChild(heatContainer) // 3. 贴到Cesium实体上 viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(116.30, 39.60, 116.50, 39.70), material: new Cesium.ImageMaterialProperty({ image: dataUrl }) } })这里有两个细节必须强调。第一heatContainer不挂到DOM上画不出来但是得到dataUrl后一定要移除否则会留下一个看不见的DOM节点长期占用内存。第二业务数据经纬度要和canvas像素坐标做线性映射比如经度范围[116.30, 116.50]映射到[0, 500]纬度范围映射到y轴这样热力点才能落在正确的位置。更进阶的做法是把CanvasRenderingContext2D直接画出来的渐变圆作为image连第三方库都不用引。原理一样只是省掉了一层依赖。4.2 圆柱体材质与空心圆柱的坑圆柱体是三维可视化里很常用的几何体参数不复杂viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.90, 0), cylinder: { length: 100, // 高度 topRadius: 30, // 顶面半径 bottomRadius: 30, // 底面半径 material: Cesium.Color.fromCssColorString(#00BFFF).withAlpha(0.6), outline: true, outlineColor: Cesium.Color.WHITE } })把topRadius和bottomRadius设成不同的值就能得到锥形或台形。很多人以为topRadius传0就是空心其实那是圆锥不是圆环管。Cesium的CylinderGeometry本身不支持真正意义上的空心圆柱圆环截面。我在项目里被这个需求卡过一次最终用“外柱内柱”叠加模拟外层半透明浅色大圆柱内层半透明深色小圆柱视觉上形成管壁厚度。这个方法应付普通场景够用但如果客户要求从顶上能看出“环”的结构就需要自己写Geometry做三角剖分了那属于进阶范围以后有机会单独写一篇。4.3 Primitive自定义Shader实现动态光照Entity的材质体系适合业务配置但要做复杂的逐像素光照建议直接上Primitive配合MaterialAppearance。Primitive可以拿到几何体的法线、UV等底层信息Entity不行。下面这个材质实现了一个简易的定向光漫反射效果const dynamicLightMaterial new Cesium.Material({ fabric: { type: DynamicLighting, uniforms: { u_lightDir: new Cesium.Cartesian3(0.5, 0.5, 1.0), u_baseColor: new Cesium.Color(0.2, 0.6, 0.9, 1.0) }, source: uniform vec3 u_lightDir; uniform vec4 u_baseColor; czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material czm_getDefaultMaterial(materialInput); vec3 dir normalize(u_lightDir); float ndl clamp(dot(materialInput.normalEC, dir), 0.0, 1.0); material.diffuse u_baseColor.rgb * (0.3 0.7 * ndl); material.alpha u_baseColor.a; return material; } } }) viewer.scene.primitives.add(new Cesium.Primitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: new Cesium.BoxGeometry({ dimensions: new Cesium.Cartesian3(1000, 1000, 1000) }) }), appearance: new Cesium.MaterialAppearance({ material: dynamicLightMaterial, faceForward: true }) }))materialInput.normalEC是眼睛坐标系下的法线dot计算法线和光照方向的夹角余弦0.3是环境光系数防止背光面纯黑。如果你想让光源位置随时间转动在更新循环里改material.uniforms.u_lightDir即可viewer.clock.onTick.addEventListener(function (clock) { const angle clock.currentTime.secondsOfDay * 0.1 const dir new Cesium.Cartesian3(Math.cos(angle), Math.sin(angle), 1.0) dynamicLightMaterial.uniforms.u_lightDir dir.normalize(dir) })5. 性能优化与踩坑记录5.1 动态材质帧率陷阱和requestRenderMode动态材质最隐蔽的性能杀手是CallbackProperty的isConstant: false。每个实体每一帧都会调用回调实体一多主线程瞬间被打满。我之前在园区项目里挂了三百多个带闪烁告警的实体帧率直接掉到个位数。几个优化方向用SampledProperty代替高频CallbackProperty预先生成好关键帧数据Cesium会做插值CPU压力小很多回调里避免创建新对象尽量复用结果变量大场景下不要给每个实体单独用动态材质把状态合并到一张纹理上用Shader去采样。还有一个容易被忽视的配置是viewer.scene.requestRenderMode true。一旦启用按需渲染Cesium默认不会每帧重绘动态材质的动画自然就“冻住”了。解决方式是在更新循环里手动调用viewer.scene.requestRender()。5.2 半透明材质的渲染顺序问题半透明材质的痛点是深度排序。两个半透明平面交叉时谁先绘制会影响最终混合结果经常出现“透过前面的物体看到后面的物体但顺序反了”的情况。我常用的处理手段面对量大面广的半透明面优先用GroundPrimitive 分类让Cesium把它当作贴地要素处理顺序问题少很多搭配terrain半透明时打开viewer.scene.globe.translucency.enable true否则半透明实体贴到地形上会出现奇怪的黑斑如果只是单层半透明叠加底图直接depthTestAgainstTerrain true简单粗暴但有效。5.3 常见问题速查表现象原因处理方式贴图不显示控制台报图片加载失败图片路径错误或跨域检查路径、给图片服务配CORS响应头贴图显示但位置歪斜repeat用了数字而非Cartesian2改成new Cesium.Cartesian2(x, y)动态材质不动CallbackProperty的isConstant传了true改为false视频贴图黑屏视频未播放或自动播放被拦截设置muted并调用video.play()Entity用自定义材质没效果Entity需要MaterialProperty而不是裸Material自定义类继承MaterialProperty或用Primitive半透明重叠区域闪烁透明排序问题改用GroundPrimitive或调整渲染模式多个GroundPrimitive更新材质不刷新内部缓存了材质状态重新赋值整个material对象或重建primitive动态材质导致帧率暴跌回调频率过高用SampledProperty或合并材质这里有两个点我再展开说说。GroundPrimitive更新材质不刷新这个问题我亲眼见同事排查了一下午最后是把primitive从场景里remove掉再重新add才生效。Cesium对GroundPrimitive做了深度缓存不要指望直接改appearance.material每次都能立刻看到变化项目里如果要做频繁的样式切换建议直接维护一个Entity列表“增删改”虽然土但稳定。另外一个经验是Cesium.Material._materialCache.addMaterial注册的材质类型在同一个Viewer生命周期内不会自动卸载。如果项目里动态创建很多种Fabric材质建议做一个统一注册模块启动时全部注册好避免运行时反复addMaterial造成不可预期的覆盖问题。我个人在实际项目里的习惯是把所有常用材质封装成工厂函数纯色、贴图、动态、自定义各写一个外部只传场景参数。这样业务层代码干净调材质就跟调接口一样后面再碰到性能问题也只需要集中到工厂函数里优化。三维可视化的技术栈会变但材质这层机制搞懂之后应付大多数场景都不会慌张。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门