02-research-device-status-effect-mechanisms.md 9.2 KB

调研 —— 设备状态特效在 Cesium 的可用手段与开销预算

Type: research Status: resolved Labels: wayfinder:research Blocked by: None

进度:100%

已完成:Cesium 1.145.0 上设备状态特效的五项事实全部核实完毕(可用原语的能力边界、批量做法与 10/50/200 的开销量级、与限距雾后处理的同集合共存与执行顺序、色与形的单一来源在引擎侧的可行边界、拾取与层级),产物落在 research/device-status-effect-mechanisms.md,每条结论带随包 d.ts/源码路径与行号或官方文档链接;查不到的项已在产物里显式列出。只读调研:未改 src/、未装依赖、未跑 build/dev/截图。

Question

需求⑤要求「定位到设备时,设备点位周围产生对应的特效(对应设备状态:报警、预警、工单、正常、离线等)」。本票只查事实:Cesium 1.145.0 上这类特效怎么做、代价多大、与既定的雾气后处理是否打架。

  1. 可用原语与能力边界:逐项给出「能否按实体定位、能否只影响一个点、能否被拾取、是否每帧重算」——
    • Entity + CallbackProperty 驱动的动画 billboard / ellipse / cylinder;
    • CustomDataSource 与 Primitive + GeometryInstance + Appearance;
    • PostProcessStage / PostProcessStageCollection;
    • 粒子系统(ParticleSystem,注意它挂在 Scene.primitives 上,是全局而非按实体)。
  2. 批量做法与开销:有没有「一次 draw call 画 N 个脉冲」的路子(自定义 Primitive + Appearance 的 instancing、BillboardCollection 的 scale 动画、shader 里按 attribute 驱动)?给同屏 10 / 50 / 200 个特效点的量级判断(有实测数据最好,没有就写清是推断并说明依据)。
  3. 与限距雾气的共存(关键):.scratch/cockpit-primary-area-guardrails 的 07 已定「自建 Cesium depth 后处理级视距雾(路线 B)」,产物在本地图 research/three-fog-shader-reuse.md。请核对:同一 PostProcessStageCollection 里再挂设备特效级是否可行?PostProcessStage 的 execute 顺序语义是什么?该图 06 已更正「Context.depthTexture 是布尔能力标志而非纹理对象」——设备特效若也要读深度,路径是什么?
  4. 色与形的单一来源:特效的颜色/形状怎么仍然只从 styles/tokens.css 的语义色(--lv1..--lv4/--neutral)与 domain/pointStatus.js 那张表来(票 06/09 的纪律)。引擎侧有哪些地方读不到 CSS 变量、必须像 markerSvg() 那样由服务层烘焙(列出具体 API)。
  5. 拾取与层级:半透明面片/粒子/扩散圈会不会挡住监测点与设备的拾取?Scene.pick 与 Scene.drillPick 在多个 primitive 叠加时的行为,以及「特效不该吃点击」有哪些做法。

产物写到 research/device-status-effect-mechanisms.md,每条结论带来源(随包 d.ts 路径 + 行号、官方文档 URL、或仓库内文件)。

Answer

结论:状态特效走「BillboardCollection 逐点 scale/color 动画」为主、「单个自定义 Primitive(N 个 GeometryInstance + 同一 Appearance + batch table)一次 draw call 画 N 个」为升级路径;Entity + CallbackProperty 只留给 1–3 个聚焦设备;与限距雾同处一个 PostProcessStageCollection 可行,但顺序必须显式定为「先特效、后雾」。 全文与逐条证据:research/device-status-effect-mechanisms.md。

  1. 原语边界(问题 1):Entity + CallbackProperty 能做「按实体定位、只影响一点、可拾取」,但每帧重算且代价极高——非常量属性让 EllipseGeometryUpdater._isDynamic 判真(EllipseGeometryUpdater.js:222-239),走「每实体一个 updater」的 DynamicGeometryBatch(DynamicGeometryBatch.js:14-22),而 DynamicGeometryUpdater.update 每帧 removeAndDestroy 再 new Primitive({asynchronous:false}) 重建(DynamicGeometryUpdater.js:76-85, 139-153);社区实测 25 个 CallbackProperty 圆柱 ≈8 FPS、200 个 ≈0 FPS。CustomDataSource 只是容器,不产生渲染原语。Primitive + GeometryInstance + Appearance 是「一次 draw call 画 N 个」的正统路径(N 个实例合并成一个几何,draw command 数 = _va.length,逐实例数据走 batch table 纹理按 batchId 查,Primitive.js:538-543, 1868, 887)。PostProcessStage 是全屏后处理,无法只影响一个点(除非用只对 3D Tiles feature 有效的 selected + czm_selected,对点位图标无效)。ParticleSystem 挂在 Scene.primitives 上、内部自建一个 BillboardCollection(ParticleSystem.js:710, 807),靠 modelMatrix/emitterModelMatrix 定位、没有位置参数、没有 pick 路径,N 个点就是 N 份粒子模拟。
  2. 批量与开销(问题 2):「一次 draw call 画 N 个」成立,两条实证路径:① 自定义 Primitive(逐实例数据走 batch table 纹理,Primitive.js:817-963);② 1.145 的 BufferPointCollection 显式一次 DrawCommand 画完(renderBufferPointCollection.js:278-294),但它 @experimental(BufferPointCollection.js:56-57)且只能改 size/color,不建议押注。10/50/200 的量级判断:BillboardCollection(整集合一次 drawElements,逐点脏更新 BillboardCollection.js:1466-1507)在 200 个点仍属安全区——官方 10,000 个静态 billboard 266 FPS、动画 138 FPS(Billboard Instancing);CallbackProperty 椭圆路线 200 个不可行。本票未在本机实测,10/50/200 是依据绘制结构 + 上述可引实测的推断,真数需在实现票实测。
  3. 与雾气共存(问题 3):同一 scene.postProcessStages 可以再挂一个设备特效级(add() 无上限、只有名字唯一约束,PostProcessStageCollection.js:436-466)。execute 顺序 = 加入顺序(无插入/排序 API,_index = stages.length),骨架固定为「AO → Bloom → 色调映射 → 自建 stage 按序链式传色 → FXAA」(:799-834);颜色链式、深度不链式——每级拿到的 depthTexture 都是场景那一张(:801-827),所以两级读同一份深度,但谁后跑谁在最上层。→ 顺序应定为先特效、后雾气,否则侧视时会出现「雾盖住了县城、设备特效却亮在雾上面」,违反该图 07 的门控诉求(07-fog-implementation-spec.md:20-24);两级开关要一次原子对齐(enabled 是逐级独立的,PostProcessStage.js:178-182)。读深度的正确路径 = PostProcessStage 默认提供的 sampler2D depthTexture(PostProcessStage.js:32-33, 526-531)+ czm_readDepth;但点位特效通常不需要读深度,正常深度测试即可。注意能力探测是文本正则(PostProcessStage.js:397-404),着色器里必须写成 uniform sampler2D depthTexture。
  4. 色与形的单一来源(问题 4):CSS 变量在 WebGL 路径一律读不到,所以任何引擎侧颜色都必须像 markerSvg() 一样由服务层烘焙(现成入口 mapService.js:631-651 的 toneColors() → :670-676)。必须烘焙的具体 API:billboard.image(SVG 作为图片加载时读不到页面 CSS,pointStatus.js:218)、Billboard#color、Material 的 uniform(Material.js:1255-1278 要求 number/Color,字符串会被当作图片或 stage 输出名)、ColorGeometryInstanceAttribute、BufferPointMaterial.color/outlineColor、PostProcessStage.uniforms、以及自建 GLSL 里的颜色常量(只能走 uniforms)。形状仍只来自 pointStatus.js 的 SHAPES(:42-59),引擎层不新增第二张业务映射表(syncPointMarkers 只认 shape/color 是同一条纪律,cesiumEngine.js:303-319)。
  5. 拾取与层级(问题 5):特效吃不吃点击只取决于有没有 pickId,与「半透明」无关。拾取通道只在命令有 derivedCommands.picking(由 command.pickId 派生,Scene.js:1810-1817)或 depth 时才画(executeIdCommand,Scene.js:2379-2404),没有 pickId 的实体命令被整条跳过。做法:① 首选 Primitive({ allowPicking: false })(Primitive.js:76, 2055-2059)或 BufferPointCollection 默认 allowPicking=false(BufferPointCollection.js:159),ParticleSystem / PostProcessStage 本就无拾取路径;② 特效放独立 collection 且不带 pointCode 属性(现有拾取口径就是「有 pointCode 才算点位」,cesiumEngine.js:417-431);③ 做成空心环(billboard 拾取看非透明像素)。最易踩的坑:不透明特效即使没有 pickId,也有 depthOnly 派生命令(Scene.js:2400-2403),会在拾取通道写深度挡住背后的设备点——特效面片必须走半透明 pass(Appearance.js:152-153)。Scene.pick 只回最靠近相机的一个;Scene.drillPick 是「拾一个藏一个再拾」的多次拾取(Picking.js:895-941),可穿透但代价是 N 次 pick pass,且靠改 show 实现,与特效独立开关会互相干扰,不推荐。