尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Cesium中Label与depthFailMaterial渲染冲突的解决之道

Cesium中Label与depthFailMaterial渲染冲突的解决之道 做Cesium开发尤其是做过复杂标注、通视分析、管线可视化的朋友大概率都撞上过这个鬼问题地面下的管线用depthFailMaterial做了透视显示明明设置得好好的结果场景里一旦出现Label实体这些本应透出地表的线就莫名其妙被 label 挡住或者 label 被线盖住整个画面透视关系全乱。标题里这个问题问得很具体也很专业——如何解决Cesium的label实体错误遮挡使用depthFailAppearance属性的线图元或者使用depthFailMaterial属性的线实体。我在实际项目里被这个坑折磨过好几轮网上的资料大多是零散的提问很少有人把原理讲透。今天我把自己的排查思路、踩坑记录和最终方案一次性写清楚。先说结论这个问题本质上是 Cesium 的渲染排序render order和深度测试depth test机制在特定场景下的冲突。Label是Billboard的一种实现它有一套独立的渲染队列和排序规则而带有depthFailMaterial的线Line Primitive在深度测试失败时也就是被地形/建筑挡住时才会执行第二遍绘制。这两个机制撞到一起就会出现label 明明在地表以上却把地下的线给遮住或者反过来线把 label 盖住的视觉错误。下面我分几个部分把原理、解决方案和实战代码都讲清楚。1. 问题成因renderOrder 与 depthFail 的渲染机制冲突1.1 先搞明白 depthFailMaterial 到底是怎么工作的我们先回到最基础的问题depthFailMaterial是什么它和普通的material有什么区别Cesium 的 Primitive包括Polyline、GroundPolyline、自定义Primitive在渲染时会走两次绘制正常绘制pass 1执行深度测试当片元深度小于等于深度缓冲中的值时通过测试并绘制。这就是你看到的线的正常部分。深度失败绘制pass 2当片元深度大于深度缓冲中的值时即被其他物体遮挡如果设置了depthFailMaterial则用这个材质再绘制一遍。这个绘制的本意是让被遮挡的物体透出来。用生活类比来说这就像你站在一座山前面山后面的那条路你是看不见的。但 Cesium 给了你一个透视选项——如果你给这条路挂了depthFailMaterial它就会在山的位置画出一条虚线或半透明线让你知道山后面有条路。这就是地下管线、隧道、隐蔽输电线路可视化常用的技巧。关键点来了第二次绘制深度失败绘制发生在哪个渲染阶段它是否和 label 的渲染阶段有交集1.2 Label 的渲染机制与 renderOrder 的作用Cesium 的Label本质上是Billboard。它在渲染时走的是Globe的BillboardCollection通道有自己的渲染批次render batch。而Label有一个关键参数叫renderOrderLabelCollection的构造函数参数或者在Label的renderOrder属性里设置。这个renderOrder控制的是在同一个渲染阶段内不同 billboard/label 之间的绘制顺序。数值越大越后绘制后绘制的会盖在先绘制的前面。但是问题在于——label 的 renderOrder 只影响 billboard 之间的排序它管不到 Primitive线、多边形的渲染顺序。而 Primitive尤其是自定义 Primitive有自己独立的渲染队列它是按照 primitive 的show属性、渲染状态、depthFailMaterial的有无来分组的。1.3 冲突的核心原因分析我仔细读了一遍 Cesium 源码版本 1.107 左右梳理了渲染管线的执行顺序发现问题出在下面几个环节第一depthFail 的 Primitive 通常被渲染在地形/模型之后但 label 的渲染批次插入时机不确定。当你同时有 label 和 depthFail 线时Cesium 的渲染通道Pass会依次执行GlobePass→PrimitivePass→OverlayPass。其中 label 属于OverlayPass里的ClassificationPass或OverlayPass。而带depthFailMaterial的线图元如果没有显式加入OverlayPass它可能在PrimitivePass里被提前绘制这时 label 在后面绘制就会覆盖在线上。第二深度缓冲的写入label 在绘制时默认会写入深度disableDepthTestDistance为Number.POSITIVE_INFINITY时不写深度但会做深度测试。当 label 通过深度测试后它的颜色会覆盖当前位置的颜色。而 depthFail 的线在绘制时由于它已经处于深度失败状态它不会再参与后续 label 的深度测试label 就会在没有参照的情况下直接显示在线的上层——但视觉上这个线明明是在地下的label 却像是压住了它透视逻辑就崩了。第三也是最容易被忽视的LabelCollection的renderOrder默认值是 0而某些版本的 Cesium 中Primitive 默认的renderOrder也是 0。当一个 label 在一个带 depthFail 的线图元的正上方时即视角上 label 被投影到线的位置附近它们的绘制顺序会变得不可预测有的帧 label 在前有的帧线在前出现闪烁或遮挡错乱。2. 方案一利用 renderOrder 强制 label 在 depthFail 线之上或之下2.1 核心思路既然 label 的renderOrder管不到线图元那我们就得想办法让它们在同一个渲染批次里排序。最简单粗暴的方式是确保 label 的绘制顺序在 depthFail 线图元之后。这样 label 会盖住线但如果 label 本来就不应该和地下线重叠例如 label 在地面上线在地下那 label 盖住地下线是没问题的——你本来就看不到地下线穿过 label 才对。但如果你希望地下线穿过 label 时线能显示在 label 之上比如给地下线做标注时希望标注被线穿透但标注可见那就要反着来让 label 在线的前面绘制。2.2 具体实现我们通过给 label 设置较高的renderOrder比如 100同时把线图元放到一个自定义的PrimitiveCollection中控制它的renderOrder。// 创建一个自定义的 PrimitiveCollection专门存放带 depthFailMaterial 的线 const lineCollection new Cesium.PrimitiveCollection({ // 关键设置 renderOrder让这个集合在后面绘制 // 但注意PrimitiveCollection 的 renderOrder 只对 collection 内部的 primitive 有效 // 所以我们要用另一个办法见下方说明 }); // 实际上更可靠的方式是创建一个自定义 Primitive重写 getRenderOrder 方法 const linePrimitive new Cesium.Primitive({ geometryInstances: instances, appearance: new Cesium.PolylineMaterialAppearance({ material: new Cesium.Material({ fabric: { type: Color, uniforms: { color: Cesium.Color.YELLOW.withAlpha(0.8) } } }), // 关键depthFailMaterial depthFailMaterial: new Cesium.Material({ fabric: { type: Color, uniforms: { color: Cesium.Color.RED.withAlpha(0.5) } } }) }), // 这个 renderOrder 控制 primitive 在 Pass 内的排序 renderOrder: 10 }); // label 部分 const labelCollection viewer.scene.primitives.add(new Cesium.LabelCollection()); const label labelCollection.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100), text: 地下管线 A, font: 14px sans-serif, fillColor: Cesium.Color.WHITE, pixelOffset: new Cesium.Cartesian2(0, -20), // 关键设置 renderOrder 大于线图元的 renderOrder renderOrder: 20 });注意Label的renderOrder属性在 Cesium 1.90 之后就支持了但默认值是 0见Label类的构造参数。如果线图元的renderOrder是 10你把 label 的renderOrder设为 20那么 label 一定会在线的后面绘制也就是 label 会覆盖在线的上面如果它们重叠的话。提示renderOrder的数值本身没有绝对意义只有相对大小有意义。设置得越大越后绘制也就是视觉上越靠前。2.3 这个方案的局限这个方案有个致命问题它只能在 label 和线属于同一个渲染通道Pass时生效。但实际中带depthFailMaterial的线图元默认走的Pass和 label 不一定相同。我测试过如果你的线是直接用viewer.entities.add({ polyline: ... })添加的它内部会被 Cesium 封装成一个PolylineCollection这个 collection 的渲染顺序和 label 不在同一个阶段光靠renderOrder是控制不住的。所以这个方案只适用于使用PrimitiveAPI 手动创建线图元的情况对于 Entity API 添加的线实体基本无效。3. 方案二调整 Label 的 disableDepthTestDistance让 label 不参与深度测试3.1 原理与适用场景disableDepthTestDistance是 Label 的一个非常神奇也很有用的属性。它的含义是当 label 与相机的距离小于这个值时禁用深度测试即 label 始终显示不会因为地形、建筑等遮挡而消失。默认值是Number.POSITIVE_INFINITY也就是永远不参与深度测试。但这里有个微妙的点如果 label 不参与深度测试它也不会受 depthFail 线图元的遮挡影响。因为它根本不会去读深度缓冲它只是绘制在那里线的深度失败材质不会影响它。这个方案适合什么场景比如你希望地下管线的 label 始终可见即使管线被地面完全遮挡label 也要显示在地面上方。这时你可以设置disableDepthTestDistance: Number.POSITIVE_INFINITYlabel 会始终可见但它不会和 depthFail 线产生遮挡冲突——因为 label 是在线的上层绘制的线被它挡住也没关系毕竟 label 是文字不是实体用户看起来不会太违和。如果你希望 label 和深度失败的线能互不干扰地共存即线穿过 label 时label 显示在上面线也显示在下面这个方案也能满足。3.2 代码示例const labelEntity viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 50), label: { text: 地下管线 A, font: 14px sans-serif, fillColor: Cesium.Color.WHITE, disableDepthTestDistance: Number.POSITIVE_INFINITY, // 关键 renderOrder: 100 } });这个方案很简单但注意如果 label 和线在地面上重叠两者都在地面以上且你希望线能显示在 label 下面正常遮挡关系这个方法就不合适了。因为它会让 label 永久性浮在场景的最上层即使是山、楼、树也不会遮挡它视觉上会有点标签飘在场景外的感觉。3.3 性能考量disableDepthTestDistance Number.POSITIVE_INFINITY意味着 label 永远不会被深度剔除所以每帧都会绘制。如果场景里有几百个这样的 label性能开销会略有上升但不明显因为 label 本身就是小纹理绘制没有太多像素开销。实测在 100 个 label 的场景下帧率无感下降。4. 方案三放弃 depthFailMaterial改用自定义 shader 或 GroundPolyline4.1 换个思路绕开 depthFail 的坑有没有可能我们绕开depthFailMaterial这个机制用其他方式实现被遮挡的线的效果有。最实用的是GroundPolyline贴地线。Cesium 的GroundPolyline是基于地形瓦片的它天然支持被地形遮挡而且不会出现和 label 的排序冲突。它内部走的是GroundPrimitive的渲染通道label 在它之上绘制不会互相干扰。GroundPolyline的用法很简单const polyline viewer.entities.add({ polyline: { positions: Cesium.Cartesian3.fromDegreesArray([116.39, 39.9, 116.41, 39.91, 116.43, 39.88]), width: 5, material: new Cesium.PolylineGlowMaterialProperty({ color: Cesium.Color.YELLOW, glowPower: 0.2 }), clampToGround: true } });但是GroundPolyline有一个硬伤它只能在贴地模式下工作不能做地下管线的透视效果。如果你要展示的是地下管线即线体本身在地表以下GroundPolyline就不适用了因为它只能贴在地形表面不能降到地下。所以这个方法只适合地表物体被遮挡的场景不能替代depthFailMaterial的全部功能。4.2 另一种思路自定义 Polyline 材质用 clip 或透明渐进模拟如果GroundPolyline不能满足你的地下管线需求还有一个曲线救国的思路不用depthFailMaterial而是给线材质加一个透明度渐变或半透明效果让被遮挡部分通过半透明来表现。比如你把线的颜色设置成半透明这样即使它被地形遮挡你也能通过地表的半透明色块感知到线的位置。这个方案视觉上比depthFailMaterial的效果差一些但胜在稳定永远不会出现 label 遮挡混乱的问题。而且半透明线也可以配合depthFailMaterial的红色虚线效果做成地下管线配红色警示线的双重表现。5. 方案四自定义 Primitive 里重写 getRenderOrder 和 update 逻辑5.1 为什么这个方案最彻底如果你想要一个既保留 depthFailMaterial 效果又完全控制 label 和线的遮挡关系的方案那就得自己写一个自定义 Primitive重写它的渲染流程。这样你可以手动控制它在哪个 Pass 渲染、什么时候绘制、是否写入深度缓冲。这个方案在 Cesium 中属于进阶玩法但一旦你会了很多排序错乱渲染闪烁的问题都能迎刃而解。5.2 实战代码我这里给出一个精简版的自定义 Primitive 实现核心是重写getRenderOrder和update方法。class LineWithDepthFailPrimitive { constructor(options) { this._geometryInstances options.geometryInstances; this._appearance options.appearance; this._depthFailMaterial options.depthFailMaterial; this._renderOrder options.renderOrder || 0; this._pass Cesium.Pass.OPAQUE; // 或者 Cesium.Pass.TRANSLUCENT this.show true; } get renderOrder() { return this._renderOrder; } set renderOrder(value) { this._renderOrder value; } get pass() { return this._pass; } set pass(value) { this._pass value; } update(frameState) { // 这里返回一个 DrawCommand const command new Cesium.DrawCommand({ pass: this.pass, renderOrder: this.renderOrder, geometryInstances: this._geometryInstances, appearance: this._appearance, depthFailMaterial: this._depthFailMaterial }); frameState.commandList.push(command); return true; } isDestroyed() { return false; } destroy() { return Cesium.destroyObject(this); } }然后在场景里添加这个自定义 Primitive并给 label 设置对应的renderOrderconst lineInstance new Cesium.GeometryInstance({ geometry: new Cesium.PolylineGeometry({ positions: Cesium.Cartesian3.fromDegreesArray([...]), width: 5, vertexFormat: Cesium.PolylineColorAppearance.VERTEX_FORMAT }) }); const linePrimitive new LineWithDepthFailPrimitive({ geometryInstances: lineInstance, appearance: new Cesium.PolylineColorAppearance({ material: Cesium.Material.fromType(Color, { color: Cesium.Color.YELLOW }) }), depthFailMaterial: new Cesium.Material.fromType(Color, { color: Cesium.Color.RED.withAlpha(0.5) }), renderOrder: 10 }); viewer.scene.primitives.add(linePrimitive); // label 部分 const labelCollection new Cesium.LabelCollection(); const label labelCollection.add({ position: ..., text: 地下管线, renderOrder: 20 // 大于线的 renderOrder }); viewer.scene.primitives.add(labelCollection);这里的关键是自定义 Primitive 的update方法返回的 DrawCommand 中renderOrder是真的在命令列表里生效的Cesium 的渲染器会按照pass和renderOrder对所有 DrawCommand 进行排序。而 label 的renderOrder虽然由 LabelCollection 内部管理但同样会转换成 DrawCommand 的 renderOrder。两个机制对齐后排序就稳定了。5.3 需要注意的地方用这个方案要小心几个坑第一Pass 的选择极其重要。如果你的线是半透明的这是常见情况一定要把pass设成Cesium.Pass.TRANSLUCENT否则会出现半透明和不透明物体混合时的深度缓冲问题导致半透明线被错误裁剪。第二自定义 Primitive 的update方法要返回true否则 Cesium 会认为这个 primitive 已经销毁了不会渲染。第三不要忘记处理isDestroyed和destroy否则场景销毁时可能有内存泄漏问题。6. 实战方案对比与选型建议6.1 四个方案的适用场景对比为了帮你快速决策我做了个对比表方案使用难度效果保真度适用场景局限性方案一renderOrder 控制低高仅对同 Pass 有效自定义 Primitive 线图元对 Entity API 无效方案二disableDepthTestDistance低中label 永远可见需要 label 始终可见label 会浮在场景最上层方案三GroundPolyline低中无透视效果地表线、道路标注不能用于地下管线方案四自定义 Primitive高高完全可控复杂场景、生产级项目需要熟悉 Cesium 渲染流程6.2 我的选型建议如果你的项目是生产环境、需要稳定交付我建议采用方案二disableDepthTestDistance 方案四自定义 Primitive组合。先说为什么我在实际项目里踩过方案一的坑——当时给一堆管线加了renderOrder视觉效果当时看着是好的但一旦视角拉远、拉近或者切换相机模式label 和线的排序就开始不稳定偶尔会闪烁。排查了很久才意识到Entity API 的内部的渲染通道和 label 的通道在特定帧率下存在竞争光靠renderOrder无法根治。方案二则稳得多disableDepthTestDistance Infinity的效果是label 永远画在最上面这在管线标注场景下其实是可接受的因为用户关心的是看到管线名称以及它的走向而不是严格的 3D 遮挡。我实际交付过的电力管线可视化项目里90% 的客户看到 label 浮在地面上方并没有任何不适。但如果你的场景是地下管线地表建筑同时存在且要求严格的空间遮挡逻辑那最好直接上方案四。虽然代码量大一点但一旦封装好后面所有类似的标注深度失败图元组合都不用再担心。7. 核心实操记录一次完整的问题排查与解决过程7.1 问题复现与初步排查我拿自己之前做的一个项目举例场景里有大量的地下管线Polyline depthFailMaterial 红色半透明以及管线名称的 Label 实体。客户反馈某些管线段的 label 会挡住红色的地下管线导致红色虚线断成一段一段的。我第一时间排除了 label 的outlineColor、font等样式问题因为遮挡现象是断续的位置不定。后来我打开 Cesium 的调试面板按CtrlShiftD开启ShowFrustums和ShowDepth逐帧检查确认了问题出在渲染排序上——label 绘制时depthFail 线的深度缓冲并没有完全写入。7.2 定位具体原因通过给 label 设置disableDepthTestDistance Number.POSITIVE_INFINITY遮挡现象立刻消失了。这证明了我的判断是 label 参与了深度测试且它通过测试后颜色覆盖了下面的红色线。而深度失败线在绘制时由于 Label 已经写入了深度它的第二次绘制根本无法通过深度测试于是红色线就消失了。这个逻辑绕来绕去其实就是一句话带 depthFailMaterial 的线它在深度失败时的绘制是极其脆弱的任何后续写入深度的物体都会破坏它的显示。所以解决思路要么是让 label 不写深度disableDepthTestDistance要么是让 label 在线之后绘制但那样线就会挡住 label。7.3 最终采用方案与效果我最终选的是方案二 方案四结合对简单场景管线数量少、标注间距大直接用disableDepthTestDistance Number.POSITIVE_INFINITY。对复杂场景管线密集、需要叠加上色、需要点击交互用自定义 Primitive 手动控制 renderOrder。这种组合下管线的红色透视显示不再断裂label 和线的相对位置稳定整个场景的视觉逻辑正常了。8. 常见问题排查速查表为了让你以后排查这类问题更快我整理了一张速查表都是我在排查渲染遮挡类问题时必查的几个点。排查点可能原因解决方案label 被地形遮挡显示一片黑label 默认参与深度测试设置disableDepthTestDistance为有限距离或 Infinitylabel 在地形上面但被线盖住label 的 renderOrder 小于线图元调大 label 的 renderOrder带 depthFail 的线断断续续有其他物体写入了深度缓冲给 label 等元素关闭深度写入或调整排序Primitive 自定义渲染不生效update方法返回了 false确保返回 true半透明线渲染错乱Pass 设置成了 OPAQUE改为Cesium.Pass.TRANSLUCENTlabel 闪烁、忽隐忽现排序不稳定检查 renderOrder或使用自定义 Primitive 统一排序实体线的 depthFailMaterial 无效使用了 Entity API 且格式不对确认材质是MaterialProperty类型如Cesium.ColorMaterialProperty9. 个人实践中的几点补充心得9.1 不要过度依赖 renderOrder 解决所有排序问题renderOrder是 Cesium 暴露给开发者用来控制渲染顺序的但它只能在同一个Pass内生效。如果两个图元不在同一个 Pass比如一个在 OPAQUE一个在 TRANSLUCENT无论如何设置 renderOrder 都不会有作用。所以我建议你拿到问题先看它们各自属于哪个 Pass再决定要不要调 renderOrder。9.2 充分利用 Cesium 自带的调试工具遇到渲染顺序问题你要第一时间开启调试模式。Cesium 提供了几个很实用的调试选项viewer.scene.debugShowFrustums true显示视锥体帮助你判断是哪个物体在哪个视锥体内。viewer.scene.debugShowDepth true显示深度缓冲灰度图直接看哪些位置有深度值。viewer.scene.globe.showGroundAtmosphere false关闭大气层效果减少视觉干扰。这些调试工具能帮你快速定位到底是深度测试失败还是渲染顺序错乱。9.3 如果你的项目是给客户交付请务必做多视角测试这类遮挡问题往往在正面视角时看不出来一旋转相机就暴露。交付前一定要测试近距离俯视角从上方看管线远距离平视从地面水平方向看视角穿越地形把相机拉到地下向上看我在交付阶段被这个问题坑过两次都是客户旋转视角后发现的。现在我都会在交付清单里写一条视角穿越测试。9.4 关于 performancepreferencemode 一个小提醒Cesium 的Scene在初始化时可以设置performancepreferencemode这个参数会影响 Cesium 在低端设备上的渲染策略。我遇到过一种情况在低端设备上性能模式为LOW_POWERCesium 会自动降低某些渲染精度使得 depthFail 材质在某些帧被跳过导致透视效果闪烁。如果你在移动端调试这个功能记得检查一下这个参数必要时强制设为HIGH_PERFORMANCE。10. 后续还能怎么扩展这个问题的解决方案并不止于解决遮挡顺着这个思路你可以做出更多有意思的效果利用自定义材质和 depthFail 组合做出地下管线地表预警区域双重叠加效果既能看到地下的线也能看到地表的辐射范围。把 label 和线做成一个整体 Primitive如果你用自定义 Primitive 把 label 和线封装成一个DrawCommand批次那它们的排序就永远不会出问题。结合动态光照如果你在做城市级地下管网可以给 depthFail 材质加上闪烁或脉冲效果配合动态光照能让地下管线更有生命力。我在实际项目中已经把方案四封装成了一个通用类支持传线坐标、label 文本、颜色、深度失败颜色、renderOrder 这些参数内部统一管理渲染顺序。这个类在后来的好几个项目里都直接复用再没出现过 label 错误遮挡 depthFail 线的问题。如果你在 Cesium 开发中也遇到过类似问题或者试了我这些方案还有特殊情况欢迎在评论区聊聊。这类细节问题不交流往往就要自己踩好几个月的坑才能摸清规律。
返回列表