Ticket: 02 — 调研:设备状态特效在 Cesium 的可用手段与开销预算
Date: 2026-09-18
Scope: 只查事实,不做实现。未改 src/,未装依赖,未跑 build/dev/serve(仓库规则 AGENTS.md)。
引擎版本(本地实测): cesium@1.145.0(node_modules/cesium/package.json:3)。包布局与 06 笔记一致:cesium 包是空壳,真实源码在 node_modules/@cesium/engine/Source/(node_modules/cesium/Source/Cesium.js:8 起全部 export { … } from '@cesium/engine')。本笔记的「路径:行号」在没有特别注明时都指 node_modules/@cesium/engine/Source/…。
BillboardCollection(Entity.billboard 底层就是它):整个 collection 是一次 draw call(官方博客原话「render the entire billboard collection with a single drawElements call」,Billboard Instancing),每个 billboard 可按 scale/color 逐帧单独改,改哪个只重传哪个(_billboardsToUpdate 脏列表,BillboardCollection.js:152-153, 655-657, 1466-1507)。可拾取(可关)。→ 10/50/200 个脉冲点用「同一张扩散圈图片 + scale/color 动画」是低风险、量级安全的做法。Entity + CallbackProperty 是所有路线里最贵的一条:isConstant=false 的属性会让 EllipseGeometryUpdater._isDynamic 判真(EllipseGeometryUpdater.js:222-239),进而走 DynamicGeometryBatch(每个实体一个 updater,DynamicGeometryBatch.js:14-22),而 DynamicGeometryUpdater.update() 每帧先 removeAndDestroy 再 new Primitive({asynchronous:false}) 重建(DynamicGeometryUpdater.js:76-85, 146-153)。社区实测(2020 年、用户报告、非受控基准):25 个 CallbackProperty 圆柱 ≈8 FPS、200 个 ≈0 FPS(论坛帖)。→ 200 个动画 ellipse 不可行;「按需只给 1–3 个当前聚焦设备上动效」才成立。Primitive + N 个 GeometryInstance + 同一个 Appearance:几何实例先合并成一个几何,再建 batch table(Primitive.js:538-543, 1160-1161),draw command 数 = _va.length(每个顶点数组一条,Primitive.js:1868),逐实例数据(颜色/show/自定义属性)走 batch table 纹理在顶点着色器里按 batchId 取(Primitive.js:887, 942-963)。200 个实例 = 1 条命令。BufferPointCollection(1.145 新增,@experimental):显式「N 个点 = 1 次 DrawCommand」,count: collection.primitiveCount 一次画完(renderBufferPointCollection.js:278-294, 307)。但它只支持屏幕像素尺寸的点 + 颜色/描边(BufferPointMaterialOptions 只有 color/outlineColor/outlineWidth/size,Cesium.d.ts:28094-28105),没有时间 uniform、也没有自定义顶点属性入口,做不了「设计感」的扩散圈。且 BufferPointCollection / BufferPrimitiveCollection 是 @experimental,「not final and subject to change without Cesium's standard deprecation policy」(BufferPointCollection.js:56-57)——本驾驶舱按 ADR 纪律不宜押注在它上面。scene.postProcessStages 可以挂任意多个自建 stage(PostProcessStageCollection.add(),无上限、无 sort 参数)。执行顺序 = 加入顺序:AO → Bloom → 色调映射 → 按 _stages 数组顺序跑自建 stage(每个吃前一个的输出)→ FXAA(PostProcessStageCollection.js:799-834;过滤在 :583-593;无插入索引 API,_index = stages.length 在 :466)。两者互不干扰的关键约束:每个 stage 拿到的 depthTexture 都是「场景渲染输出的那一张」(execute() 的 depthTexture 参数一路原样传递,:801-827),所以雾气 stage 与特效 stage 读的是同一份深度,谁先谁后都能读;但颜色是链式的,后跑的 stage 会把前一个的输出当输入。→ 顺序必须显式对齐:先特效、后雾气(特效属于「被雾遮蔽的远处内容」),否则特效会盖在雾上面。Context.depthTexture 更正沿用 06 结论:它是布尔能力标志(Context.js:714-725),不是纹理。设备特效若只想做「不穿模的点位特效」,根本不必自己读深度:用 Primitive / billboard 的正常深度测试即可。若确实要读深度(例如特效要按距离淡出),正确路径就是 PostProcessStage 默认提供的 sampler2D depthTexture uniform(PostProcessStage.js:526-531)+ czm_readDepth(depthTexture, v_textureCoordinates),由 Cesium 自动注入 LOG_DEPTH 并解码(06 笔记 §2 第 4 条已核实)。tokens.css 是 CSS 文件,WebGL 渲染路径读不到 CSS 变量。所以凡是引擎侧要的颜色,都必须像 markerSvg() 一样由服务层从计算样式读出、烘焙成具体色值再传下去(现成实现:services/mapService.js:639-651 的 toneColors() → :670-676)。引擎侧要 Cesium.Color / 数字 RGBA 的 API 有:billboard.image(data-URI SVG,色值必须烘进图片)、Billboard.color、Material uniform(Material.js:1255-1278 的 uniform 类型判定,Color 对象而非 CSS 串)、PerInstanceColorAppearance 的 ColorGeometryInstanceAttribute、BufferPointMaterial.color、PostProcessStage 的 uniforms。形状仍只来自 pointStatus.js 的 SHAPES(pointStatus.js:42-59)。derivedCommands.picking(由 command.pickId 派生)或 derivedCommands.depth 的命令才会画(Scene.js:2379-2404,即 executeIdCommand);没有 pickId 的实体命令在拾取通道里被无条件跳过。因此:Primitive + allowPicking:false(Primitive.js:76, 2055-2059)、BufferPointCollection 默认 allowPicking=false(BufferPointCollection.js:159)天然不吃点击;反过来说,只要给特效挂了 pickId,它就会挡住背后监测点/设备的拾取(Scene.pick 只回最靠近相机的一个,靠 Scene.drillPick 才能穿透,而 drillPick 是「拾一个藏一个再拾」的多次拾取,Picking.js:895-941)。判定口径统一为四问:能否按实体定位 / 能否只影响一个点 / 能否被拾取 / 是否每帧重算。
Entity + CallbackProperty 驱动的动画 billboard / ellipse / cylinder| 原语 | 按实体定位 | 只影响一个点 | 可拾取 | 每帧重算 |
|---|---|---|---|---|
Entity.billboard |
是(position) |
是 | 是(canvas 内非透明像素,Billboard.getPickId Billboard.js:1104-1114) |
否(除非属性非常量) |
Entity.ellipse(常量属性) |
是 | 是 | 是 | 否 |
Entity.ellipse(CallbackProperty 属性) |
是 | 是 | 是 | 是,且是「销毁 + 重建 Primitive」 |
Entity.cylinder(同上) |
是 | 是 | 是 | 是 |
依据:
CallbackProperty 的语义:new CallbackProperty(callback, isConstant),isConstant 必须显式给(CallbackProperty.js:12-19, 30-33, 72-86);getValue(time) 惰性求值(:59-63)。isConstant=false 意味着「每帧都要重新求值」,也就是每帧都要造新值。GeometryVisualizer 按「geometry 类型 + appearance + shadowMode」分组进 StaticGeometryColorBatch / StaticGeometryPerMaterialBatch / StaticOutlineGeometryBatch(GeometryVisualizer.js:56-120),多个实体可以合批。EllipseGeometryUpdater._isDynamic() 只要求 !ellipse.semiMajorAxis.isConstant 等任一条件成立(EllipseGeometryUpdater.js:222-239),就走 DynamicGeometryBatch,而它是 this._dynamicUpdaters.set(updater.id, updater.createDynamicUpdater(...))——每个实体一个 updater(DynamicGeometryBatch.js:14-22)。DynamicGeometryUpdater.prototype.update 先 primitives.removeAndDestroy(this._primitive),再 new Primitive({ geometryInstances: createFillGeometryInstance(time), asynchronous: false }) 重新 add(DynamicGeometryUpdater.js:76-85, 139-153)。asynchronous:false 还意味着几何在主线程生成。requestRenderMode 的按需渲染。semiMajorAxis 是几何输入,不是着色器 uniform。想做出「世界尺度上不断扩大的圈」,就必须动几何 → 只能走动态路径。若只做「整体透明度/大小的呼吸」,不要用 ellipse,用 billboard 的 scale/color(见 1.5)。结论:Entity + CallbackProperty 适合「同时只有 1–3 个设备在播特效」的场景;绝不适合 200 个点常驻动画。
CustomDataSource 与 Primitive + GeometryInstance + AppearanceCustomDataSource 只是「一串 Entity + 一个名字」的组织容器,自己不产生任何渲染原语。仓库已有用例:点位标记数据源 POINT_MARKER_SOURCE(src/modules/cockpit/services/cesiumEngine.js:267, 286-301),并且该文件 :272-283 记录了 DataSourceCollection.add() 异步注册的坑(同 tick 先写后读会建出第二个空数据源)。→ 设备特效若走 Entity/DataSource 路线,必须沿用同一份「WeakMap 缓存实例」的写法,否则会重演「图例有计数、地图 0 个标记」。Primitive 的批量能力(这是「一次 draw call 画 N 个」的正统路径):
geometryInstances 可以是数组(Primitive.js:67)。Primitive.js:1160-1161, 1195-1214 把实例分片喂给 worker 后合并),再为它们建一张 batch table(createBatchTable,Primitive.js:538-543, 628-663)。primitive._va.length(每个顶点数组一条,Primitive.js:1868)——合并后通常就是 1 条。color / show / pickColor / distanceDisplayCondition / 自定义属性)不是顶点属性,而是 batch table 纹理,在顶点着色器里按 batchId 查(Primitive.js:817-963:czm_batchTable_show、czm_batchTable_color、czm_batchTable_pickColor、czm_batchTable_distanceDisplayCondition 等)。primitive.getGeometryInstanceAttributes(id) 返回带 getter/setter 的对象(Primitive.js:2379-2420);setter 会走 createSetFunction → batchTable.setBatchedAttribute(Primitive.js:2277-2304)。Appearance 是可行的:MaterialAppearance 的 vertexFormat 由 materialSupport 决定(MaterialAppearance.js:55, 94, 197-199,MaterialSupport.TEXTURED / ALL 分别给 POSITION_NORMAL_AND_ST / VertexFormat.ALL,:288-323),Appearance 也接受自定义 fragmentShaderSource(Appearance.js:125-153)。→ 想「shader 里按 attribute 驱动」是可行的,但逐实例的那个 attribute 只能走 batch table 纹理这条路(czm_batchTable_*),不是真正的 instanced vertex attribute。allowPicking 是构造参数,默认 true(Primitive.js:76, 243),false 时不创建 pickId 且不写 command.pickId(Primitive.js:650-653, 2055-2059),文档原话「GPU memory is saved」。GeometryInstance 放在该设备的 ECEF 位置(Transforms.eastNorthUpToFixedFrame,仓库已有用法 cesiumEngine.js:452-455)。PostProcessStage / PostProcessStageCollectionPostProcessStage.js:592 的 context.createViewportQuadCommand),它无法知道「哪个像素属于哪个设备」。→ 不能只影响一个点,除非你在 shader 里自己做「世界坐标 → 屏幕」的判定(要自己给 uniforms 传设备点位的屏幕坐标或世界坐标),或者用下面这条:
stage.selected = [features] + shader 里 czm_selected():这是 Cesium 官方给「只影响被选中的对象」的机制(PostProcessStage.js:71-94 的示例、:355-395 的 selected、:548-567 注入的 czm_selected)。但它只对参与 id 通道的对象(3D Tiles feature 等)有效,普通 Entity/Primitive 不在其列——所以它救不了「只影响某一个设备图标」。对「只影响一个点」的需求,不要选后处理。uniforms 支持每帧函数:属性值可以是常量,也可以是函数,函数每帧执行前被调用(PostProcessStage.js:237-245, 458-496)。→ 「脉冲相位」这类每帧量可以这样喂。PostProcessStage.prototype._isSupported 用正则检查片元着色器里有没有 uniform sampler2D depthTexture,有则要求 context.depthTexture 为真,否则整级不执行(PostProcessStage.js:397-404)。注意它是文本正则匹配(/uniform\s+sampler2D\s+depthTexture/g),声明写法不同会影响判定。Scene.pick 看不到它)。→ 「特效不该吃点击」这条需求,后处理天然满足;反问是它也无法承载「点特效」。enabled 开关是廉价的(PostProcessStage.js:178-182)。ParticleSystem(全局,不是按实体)Scene.primitives 上:官方教程与 API 的用法就是 viewer.scene.primitives.add(particleSystem)(Introduction to Particle Systems、ParticleSystem API);源码里它内部自建一个 BillboardCollection 来画粒子(ParticleSystem.js:710),每次 update 都会 this._billboardCollection.update(frameState)(ParticleSystem.js:807)。→ 一个 ParticleSystem ≈ 一个 billboard collection 的开销;N 个点 = N 个 ParticleSystem = N 份粒子模拟 + N 次 billboard collection 更新。modelMatrix(把粒子系统从模型坐标变到世界坐标,ParticleSystem.js:28, 97)+ emitterModelMatrix(把发射器放到系统局部坐标里,:29, 98-99, 213-223)+ emitter(默认 CircleEmitter(0.5),:27, 89-93)。没有「position」参数,也没有 entity 关联:它是场景级 primitive,要自己算 ECEF 矩阵。ParticleSystem 表达多个不同颜色的点位特效(startColor/endColor 是系统级,不能逐粒子按状态区分)。ParticleSystem 没有 pickId / allowPicking 相关成员(构造函数 ParticleSystem.js:58-100 与成员列表里都没有),它画出的粒子 billboard 也没有建立 pick 身份(Particle.js 里只有位置/速度/尺寸/颜色等)。→ 不参与拾取。这一条对「特效不该吃点击」是有利的,但换来的是「无法与设备点做拾取联动」。emissionRate 默认 5/秒、particleLife 默认 5 秒(见 API 文档默认值表)。→ 开销随 emissionRate × particleLife 的粒子数增长。BillboardCollection(本票结论里的首选,独立列出)BillboardCollection.js:152-153(_billboardsToUpdate)、:655-657(setter 里 push 脏索引)、:1466-1507(更新时只处理 _billboardsToUpdateIndex 个)。→ 每个特效点一套自己的相位是免费的,不必重建整个 collection。Billboard.getPickId,Billboard.js:1104-1114);Entity.billboard 的 properties.pointCode 正是仓库现有拾取判据(cesiumEngine.js:417-431)。新增特效 billboard 时必须让它没有 pointCode,或者干脆不放拾取数据源里,否则会污染 pickPointMarkerAt()。| 路子 | 一次 draw call 能否画 N 个 | 每帧成本发生在哪 | 判定 |
|---|---|---|---|
BillboardCollection + 每点 scale/color 动画 |
能(一个 collection = 一次 drawElements) | CPU 侧改 N 个脏 billboard 的顶点属性(BillboardCollection.js:1466-1507) |
推荐:换来的表现力足以做「呼吸/脉冲/扩散」;N=200 属安全区 |
自定义 Primitive(N 个 GeometryInstance + 同一 Appearance) |
能(command 数 = _va.length,通常 1) |
逐实例数据走 batch table 纹理(Primitive.js:817-963);改单个实例走 getGeometryInstanceAttributes() setter |
可行;表现力最强(自建 GLSL),但也是最需要自己写 GLSL 的一条 |
BufferPointCollection(1.145 新,@experimental) |
能(显式 count: collection.primitiveCount,renderBufferPointCollection.js:290) |
只更新脏区间(_dirtyCount,:109-164) |
不建议押注:@experimental(BufferPointCollection.js:56-57)且只能改 size/color,做不出设计感 |
Entity + CallbackProperty 椭圆/圆柱 |
否(每个实体一个 updater、每帧重建 Primitive) | 每帧几何在主线程重建(DynamicGeometryUpdater.js:139-153) |
只用于 1–3 个聚焦设备 |
ParticleSystem |
每个系统自己一次 billboard collection update | 粒子模拟 + 粒子数(emissionRate × particleLife) |
适合「单个重点设备的一处烟雾/火花」,不适合 N 点 |
先说清性质:下面是「依据绘制结构 + 时间预算 + 可引用的实测」做出的推断,不是在本机实测的数据。本次调研按仓库规则没有跑任何 build/dev/浏览器测量。
CallbackProperty 椭圆路线在这个量级也只剩「每帧 10 次几何重建」的噪声级成本(社区实测 25 个才掉到 8 FPS 那种量级的机器上)。BillboardCollection 与自定义 Primitive 都在安全区(对照:官方 10,000 个静态 billboard 266 FPS,即约 0.0037 ms/个 的整帧摊派,50 个 ≈0.19 ms);CallbackProperty 椭圆路线开始危险(社区实测同机 25 个 ≈8 FPS = 125 ms/帧)。BillboardCollection 仍在安全区(官方实测 10,000 个动画 billboard 138 FPS ≈ 0.0007 ms/个 摊派,200 个 ≈0.15 ms);自定义 Primitive 亦在同量级(1 条 draw command,成本主要是 batch table 纹理 200 个 texel);CallbackProperty 椭圆路线不可行(社区同机 200 个 ≈0 FPS)。Color/Cartesian3/图片 data-URI),GC 压力会盖过绘制成本。官方对 BufferPrimitiveCollection 明确写了这条纪律:「be careful to avoid allocating "N" instances of any related JavaScript object … using the flyweight pattern」(Cesium.d.ts:28686-28691)。同理,禁止每帧重新生成 SVG data-URI 当图片(markerSvg() 是建图片用的,不是每帧调用的)。CallbackProperty(isConstant=false) 会持续请求新帧(issue #11315 引官方维护者原话),也就是说「常驻动画特效」会让按需渲染失效——这条对「低性能消耗」是硬约束,凡是常驻动效都要按 07 的门控来决定何时挂上。PostProcessStageCollection 里再挂一个设备特效级 —— 可行this._stages = [](PostProcessStageCollection.js:76),add(stage) 只要求名字唯一并把 _index 设为当前长度(:436-466)。_index 只有 :420(移除后重排)、:466(加入时赋值)、:499/:502/:517(移除)。→ 顺序 = 加入顺序,唯一「插到中间」的办法是移除后按新顺序重新 add。stage.ready && stage.enabled && stage._isSupported(context)(:583-593)。execute 的顺序语义(源码级)PostProcessStageCollection.prototype.update 收集活跃级(:583-593),execute 按固定骨架跑(:768-835):
initialTexture = colorTexture // 场景渲染结果
if (ao) { execute(ao, initialTexture, depthTexture, idTexture); initialTexture = ao.output }
if (bloom) { … }
if (autoExposure){ … }
if (tonemapping) { execute(…); initialTexture = tonemapping.output }
// —— 自建 stage 按加入顺序,链式传色 ——
execute(activeStages[0], initialTexture, depthTexture, idTexture)
for i in 1..length-1:
execute(activeStages[i], getOutputTexture(activeStages[i-1]), depthTexture, idTexture)
if (fxaa) { execute(fxaa, lastTexture, depthTexture, idTexture) }
三条必须记住的语义:
:799-834;类文档也这么写::25-30)。→ 后处理级的位置不可能比内置级更早。colorTexture 是第 i−1 个的输出(:819-828)。→ 特效级与雾气级的先后会直接改变观感:谁后跑谁就在最上层。depthTexture 参数从 execute 入口一路原样传给每一级(:801-827 每处都传同一个 depthTexture)。→ 两级读到的深度完全一致,谁先谁后都不影响读深度。.scratch/cockpit-primary-area-guardrails/issues/07-fog-implementation-spec.md:20-24)。设备特效必须排在雾气之前才能被这份「遮盖」覆盖;否则侧视时会出现「雾把县城盖住了,但设备特效还亮在雾上面」。update 里原子地对齐:enabled 是逐级的独立开关(PostProcessStage.js:178-182),集合自己不做联动。→ 「07 里显式对齐顺序与开关」这句(map.md:19、map.md:57)在实现上就是:同一处创建、同一处 add、按固定顺序 add、按开关一起 enable/disable。Context.depthTexture 是布尔能力标志(Context.js:714-725),不是纹理(该更正沿用,research/three-fog-shader-reuse.md 及其票面已核实)。PostProcessStage 默认提供的 depthTexture uniform:PostProcessStage.js:32-33 的文档写明默认 uniform 就是 sampler2D colorTexture + sampler2D depthTexture + vec2 v_textureCoordinates;uniform 注入在 :526-531(depthTexture、depthTextureDimensions)与 :588-592(ShaderSource)。读深度用 czm_readDepth(depthTexture, v_textureCoordinates)(Shaders/Builtin/Functions/readDepth.glsl:1-4,LOG_DEPTH 下自动反解,见 06 笔记 §2 第 4 条)。Primitive / billboard 走正常深度测试就能「被建筑挡住」,Entity.billboard 现有的 disableDepthTestDistance: POSITIVE_INFINITY(cesiumEngine.js:346)是刻意「永不被挡」的选择。只有「按距离淡出」这类需求才需要读深度。PostProcessStage.js:397-404),片元着色器里必须写成 uniform sampler2D depthTexture 才能被识别;② depthTexture 的原始内容是 D24S8 打包纹理(DEPTH_STENCIL + UNSIGNED_INT_24_8,Renderer/FramebufferManager.js:189-196,06 已核实),必须经 czm_readDepth 解码,不能当普通颜色采样。styles/tokens.css 声明一份:--lv1: #ff4d5e; --lv2: #ffa63d; --lv3: #4fd8ff; --lv4: #35e39a; --neutral: #6d92ae;(src/modules/cockpit/styles/tokens.css:31-38,挂在 .cockpit-shell 上而不是 :root,理由写在 :9-13)。同一文件里 --lv3 与主色 --cyan 同值(:28, :36)。src/modules/cockpit/domain/pointStatus.js:78-85(POINT_STATUS_VISUALS,六态各带 tone/shape/label/priority)、:88(UNKNOWN_VISUAL)、:91(UNASSOCIATED_VISUAL)、:94-96(visualForStatus)。形状片段只写 currentColor、不含任何色值(:42-59)。domain/statusTones.js:14(TONES = ['lv1'..'neutral'])、:32-38(TONE_DESCRIPTIONS)。services/mapService.js:631-651(TONE_FALLBACK + toneColors() 从 .cockpit-shell 的 getComputedStyle 读 --lv1..--neutral),:670-676 把它交给引擎(colors[marker.tone] → markerSvg(shape, color, 18))。markerSvg() 为什么必须烘焙色值:注释写得很直白——「color 属性让片段里的 currentColor 有值:SVG 作为图片加载时读不到页面 CSS 变量」(pointStatus.js:218-222);mapService.js:626-629 重复了同一句。syncPointMarkers(viewer, markers, colorOf, imageOf) 只收 shape 与 color(cesiumEngine.js:303-319)。→ 特效若新增「动画形态」,应沿用同一模式:domain 出「形态键 + 色键」,服务层出「色值 + 素材」,引擎层不新增第二张业务映射表。CSS 自定义属性只在 DOM/CSS 世界解析,WebGL 渲染路径(着色器 uniform、顶点属性、纹理)一律读不到。逐项列出:
| 引擎 API | 为什么读不到 CSS 变量 | 服务层该传什么 |
|---|---|---|
BillboardCollection#add({ image }) / Entity.billboard.image |
图片是 data-URI/URL,SVG 作为图片加载时不继承页面 CSS(pointStatus.js:218 注释) |
markerSvg(shape, color) 这样把色值写进 SVG 的 color 属性 |
Billboard#color / Entity.billboard.color |
要 Cesium.Color(数值 RGBA) |
由 toneColors() 的 #rrggbb 转 Color.fromCssColorString() |
Material 的 uniform |
要求 number / boolean / Color / Cartesian2..4 / 纹理,字符串被当作图片或阶段输出名(Material.js:1255-1278) |
Color 对象 |
ColorGeometryInstanceAttribute(PerInstanceColorAppearance 的逐实例颜色) |
数值 Uint8 RGBA | 同上 |
BufferPointMaterial#color / #outlineColor |
Color 对象(Cesium.d.ts:28094-28105) |
同上 |
PostProcessStage 的 uniforms |
常量值或每帧函数(PostProcessStage.js:237-245),函数返回值是 GLSL 类型 |
由服务层闭包捕获烘焙好的 Color 再返回 |
| 自建 GLSL 里的颜色常量 | 着色器源码是纯文本,不经过 CSS | 走 uniforms,不要硬编码 #rrggbb 进 shader |
结论:单一来源可以维持,但入口只有一个——「服务层调一次 toneColors(),把色值往下传」。任何在引擎层或 shader 里写死色值的做法都会长出第二份定义。形状同理:SHAPES → markerSvg() → data-URI 是唯一通路;若要给特效做「扩散圈」素材,素材本身应由 shape 派生或与 shape 一一对应,不要在引擎里按状态名分支。
Scene.pick 与 Scene.drillPick 在多层叠加时的行为Scene.pick(windowPosition, width, height) 走 this._picking.pick(...)(Scene.js:4770),只返回最靠近相机的一个对象。Scene.drillPick 的实现是反复拾取:drillPick(pickCallback, limit) 先拾一次,把拾到的对象 show = false 藏起来,再拾下一次,直到拾满或拾不到,最后统一恢复 show = true(Picking.js:895-941)。→ 多层叠加时 drillPick 能全部拿到,但代价是 N 次 pick pass;而且它靠改 show 实现,若对象本身 show 归别人管,会互相打架。Scene.pick 用的是 _picking 的专用 framebuffer(Scene.js:3104-3106 附近给 getPickDepth 喂深度),所以拾取开销与「屏幕上画了多少东西」无关,而与「拾了几次」成正比。→ 走查里 pickPointMarkerAt() 每次点击一次 scene.pick(cesiumEngine.js:417-431),是廉价的。判据是「这条命令在拾取通道里有没有 pick 身份」,与「是不是半透明」无关:
performIdPass(...)(Scene.js:3147-3149:CESIUM_3D_TILE / OPAQUE / TRANSLUCENT 三个 pass 都调它),每条命令进 executeIdCommand。executeIdCommand(Scene.js:2379-2404)只做两件事:有 derivedCommands.pickingMetadata / picking 就画对应的 pick 命令,否则若只有 depth 派生命令就只画深度,两者都没有就直接 return(什么都不画)。derivedCommands.picking 只在 command.pickId 有定义时才创建(Scene.js:1810-1817;DerivedCommand.createPickDerivedCommand 在 DerivedCommand.js:315-334)。Primitive + allowPicking:false(Primitive.js:2055-2059)与 BufferPointCollection 默认 allowPicking:false(BufferPointCollection.js:159)的效果。Scene.pick 在它盖住的像素上返回它而不是后面的监测点。半透明的圈/粒子只要挂了 pickId 就会吃点击。Scene.pickTranslucentDepth 默认 false(Scene.js:539),文档说明开了它才会把 pickPosition 的深度算上半透明对象(Scene.js:4999-5030)——这是 pickPosition 的语义,不影响 pick 返回哪个对象。Primitive({ allowPicking: false, … })(Primitive.js:76)、BufferPointCollection({ allowPicking: false })(默认就是 false)、ParticleSystem(本身没有拾取路径)、PostProcessStage(不是 primitive)。→ 零额外代码。BillboardCollection(不是 POINT_MARKER_SOURCE 数据源),并且不要给它 pointCode 属性。仓库现有拾取口径正是「属性里有 pointCode 才算点位标记」(cesiumEngine.js:405-431),所以这条能立刻生效。depthOnly 派生命令(Scene.js:2400-2403 的 else if (defined(depth))),也就是说一个不透明的特效面片会在拾取通道里写深度,挡住后面的设备点。→ 特效面片必须走半透明(Appearance.translucent = true / Material.isTranslucent(),Appearance.js:152-153),否则它既没有 pickId 又会挡住拾取。这是最容易踩的一条。drillPick 兜底(不推荐):若某天特效确实要可拾取(例如点特效弹菜单),用 drillPick 穿透取列表再筛——但它多次拾取、并且靠改 show(Picking.js:923-934),与「特效按状态独立开关」会互相干扰。AGENTS.md 禁止),所以 §2.2 的 10/50/200 量级判断是推断,依据已在正文逐条列出(绘制结构 + 官方 10,000 billboard 的 2015 年对照 + 社区 200 个 CallbackProperty 的用户实测)。要拿到真数,得在实现票里用 scene.debugShowFramesPerSecond 或 FrameRateMonitor 实测。CallbackProperty 椭圆」在本仓库目标机型上的具体帧率:查不到。社区那组数字是 2020 年、提问者本机、Sandcastle 环境,只可作为量级参考。BufferPrimitiveCollection 的正式文档页(cesium.com ref-doc):抓取 ParticleSystem.html 时的侧边栏里已出现 BufferPointCollection 等条目,但本票没有抓到它们的官方参考页正文,结论全部来自随包 Cesium.d.ts:28040-28820 与源码 Renderer/…、Scene/BufferPointCollection.js、Scene/renderBufferPointCollection.js。其 @experimental 标注即来自源码 JSDoc(BufferPointCollection.js:25, 56-57)。ParticleSystem 能否被 Scene.pick 拾取:源码里没有任何 pickId / allowPicking 痕迹(构造参数表 ParticleSystem.js:58-100、成员表、内部 BillboardCollection 用法),但没有找到官方明文写「不可拾取」。判定为「机制上不参与拾取」,依据是源码缺失而非文档声明。.scratch/cockpit-primary-area-guardrails 07 的对齐范围。Entity.cylinder 的 _isDynamic 具体判据:本票只核了 EllipseGeometryUpdater(EllipseGeometryUpdater.js:222-239)。CylinderGeometryUpdater 未逐行读,但其 length 属性非常量时走同一套 DynamicGeometryBatch / DynamicGeometryUpdater 机制(DynamicGeometryBatch.js:14-22、DynamicGeometryUpdater.js:68-85),结论不变。随包 CesiumJS 1.145.0(node_modules/@cesium/engine/Source/,行号见正文)
Scene/PostProcessStageCollection.js —— stage 集合、add、removeStages、_index、update 的活跃级过滤、execute 的固定骨架与链式传色Scene/PostProcessStage.js —— 默认 uniform(colorTexture / depthTexture / v_textureCoordinates)、uniforms 每帧函数、enabled、selected + czm_selected、_isSupported 的 depthTexture 正则Scene/BillboardCollection.js、Scene/Billboard.js —— 脏列表更新、getPickIdScene/Primitive.js —— allowPicking、batch table(createBatchTable / czm_batchTable_*)、_va.length 与 draw command 数、getGeometryInstanceAttributesScene/BufferPrimitiveCollection.js、Scene/BufferPointCollection.js、Scene/renderBufferPointCollection.js、Shaders/BufferPointMaterialFS.js —— 单 DrawCommand 画 N 点、allowPicking=false 默认、@experimentalScene/ParticleSystem.js —— 内部自建 BillboardCollection、modelMatrix / emitterModelMatrix、无位置参数Scene/Appearance.js、Scene/MaterialAppearance.js —— vertexFormat / materialSupport / translucentScene/Material.js —— uniform 类型判定(Color 而非 CSS 串)Scene/Scene.js —— 拾取通道 performIdPass、executeIdCommand、pickId → derivedCommands.picking、pickTranslucentDepthScene/Picking.js —— drillPick 的「拾一个藏一个」实现Scene/DerivedCommand.js、Scene/DynamicGeometryBatch.js、Scene/DynamicGeometryUpdater.js、Scene/EllipseGeometryUpdater.js、DataSources/GeometryVisualizer.js —— 动态几何的每帧重建链Scene/CallbackProperty.js —— isConstant 语义Renderer/FramebufferManager.js —— D24S8 深度纹理node_modules/cesium/package.json:3(版本)、node_modules/cesium/Source/Cesium.d.ts:28040-28820(BufferPointCollection / BufferPrimitiveMaterial / BufferPrimitiveCollectionOptions)仓库内文件
src/modules/cockpit/services/cesiumEngine.js(点位标记数据源、syncPointMarkers、pickPointMarkerAt、frameAt)src/modules/cockpit/services/mapService.js(TONE_FALLBACK / toneColors() / syncPointMarkersForSummary)src/modules/cockpit/domain/pointStatus.js(SHAPES / POINT_STATUS_VISUALS / markerSvg)src/modules/cockpit/domain/statusTones.js、src/modules/cockpit/styles/tokens.css.scratch/cockpit-primary-area-guardrails/research/three-fog-shader-reuse.md、该图 issues/06、issues/07.scratch/cockpit-point-locate-and-device-link/map.md(Notes 现状事实 1–8、跨图耦合)官方文档 / 官方博客 / 社区