尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Cesium Billboard全解析:Entity配置、穿地形排查与性能优化
做三维地球相关的前端项目几乎绕不开 Cesium 这套 Entity API而Billboard是所有实体类型里出场频率最高的一个——比Model轻、比Point好看、比Label更自由几乎所有 POI 标注、设备点位、报警闪烁图标、车辆状态标识最后落到屏幕上都是它。但恰恰是这个看起来最简单的类型藏着不少容易翻车的细节图标陷进地形、贴着地球背面显示、明明 64×64 的图糊成一片、上千个点之后帧率掉到个位数。这篇内容就围绕 Cesium 的 Entity 体系里的 Billboard 展开把它的本质、最小可跑配置、参数取舍、故障排查、性能优化和交互拾取逐层拆开讲。不管你是刚接触 Cesium 想搞清楚verticalOrigin到底是怎么对位的还是已经写了半年、被贴地图标穿地形搞得头皮发麻下面的内容应该都能对得上。1. Billboard 在 Cesium 里到底是个什么东西1.1 永远面向相机的屏幕空间四边形先把概念钉死Billboard 不是三维模型它是渲染管线里的一个四边形quad贴上一张纹理然后在顶点着色器阶段被强制旋转到与相机视线垂直的方向。这意味着你绕地球怎么转这个图标永远正对着你不会出现侧着看变成一条线的情况。这一点和ModelGraphics有本质区别——模型是真实的三维几何体Billboard 本质上是屏幕空间的贴纸。理解这一点后面很多参数就顺了。比如sizeInMeters为什么默认是false因为屏幕空间的贴纸天然以像素为单位衡量才符合直觉。再比如rotation为什么是屏幕空间逆时针的弧度而不是绕某个世界轴因为它本来就不在世界空间里。再比如alignedAxis这个参数——它允许你把图标旋转轴对齐到某个世界方向比如地球表面的法线这在做图标随地形倾斜的时候有用但如果你的需求只是单纯的 POI 标注直接用默认值就行改了反而容易出歪。我这几年接触到的情况里大部分把 Billboard 用错的人都是脑子里把它当成三维物体了于是想当然地去调 height、调 position、调各种三维属性结果一圈下来发现最该调的verticalOrigin和pixelOffset没动。所以第一步认知很关键它在屏幕上是一张图在地球上只是一个锚点。1.2 Billboard 与 Label、PointGraphics、ModelGraphics 的选型边界同样是标一个点四种实体类型各有各的适用面选错了后期会很难受。我整理了一张表实际项目里按这个判断基本不会错类型渲染开销适用场景主要短板Billboard低POI 图标、状态灯、自定义形状标注不是真三维斜视角下没有体积感Label中SDF 字体光栅化名称、数值、编号大量文本时纹理切换频繁PointGraphics极低海量散点万级以上样式单一只能画圆点ModelGraphics高需要体积感的设备、建筑、车辆加载慢数量一多必卡绝大多数图标 文字的标注场景正确做法是Billboard 管图形、Label 管文字两者共用一个 position各自配好偏移量。很多人图省事想用一张带文字的图片代替两者短期看省事长期看是灾难——文字大小不随缩放变化、分辨率受限、改文案要重新出图、多语言没法切换。分开之后Label 用系统字体渲染永远是清晰的Billboard 只管图形部分各司其职。至于PointGraphics如果你的场景是十几万个点那就别犹豫直接上 Point甚至考虑PointPrimitiveCollection。Billboard 的纹理切换成本在那儿摆着数量级上去之后是撑不住的。1.3 Entity 方式和 BillboardCollection 方式的关系Cesium 有两套 API 画图标高层的Entity数据驱动属性可以是常量也可以是CallbackProperty底层的BillboardCollection直接管Billboard实例。很多新手以为这是两个完全平行的东西其实不是——Entity 的 Billboard 在底层会被DataSourceDisplay里的BillboardVisualizer翻译成一堆BillboardCollection里的Billboard只是这个 Collection 由 Visualizer 自己持有你从viewer.scene.primitives里是拿不到的。这个内部机制解释了一个常见现象为什么改entity.billboard.scale之后画面变化有明显的延迟感因为改的是Property需要等下一帧时钟 tick 时由 Visualizer 求值再同步到底层 Billboard 上。而直接改BillboardCollection里 Billboard 的scale是立刻生效的。数据量小的时候感知不到上千个之后差距就出来了。结论性判断需要动态绑定、需要和业务数据模型一一对应、数量在几百以内用 Entity纯展示、数量上千、或者需要精细控制渲染顺序直接上 BillboardCollection。2. 让第一枚图标稳稳落在地表最小可运行配置2.1 一个能直接粘贴跑的初始化骨架与其一上来就讲参数不如先给一段能跑的代码把后面的讨论都挂在这段代码上。下面这个骨架我用了很多次改改就能用const viewer new Cesium.Viewer(cesiumContainer, { terrainProvider: await Cesium.createWorldTerrainAsync(), baseLayerPicker: false, geocoder: false, animation: false, timeline: false }); viewer.entities.add({ id: poi-001, position: Cesium.Cartesian3.fromDegrees(108.9, 34.2, 0), billboard: { image: /assets/icons/poi-default.png, width: 32, height: 32, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, disableDepthTestDistance: 5000 } }); viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(108.9, 34.2, 12000) });这段代码里有三个点值得单独拎出来说。第一width/height直接给定像素尺寸比用scale更可控——因为不同设计师给的图尺寸五花八门统一在配置层处理掉别让scale的值散落在业务代码里。第二verticalOrigin: BOTTOM是 POI 场景的默认正确选择这一点下一章细讲。第三disableDepthTestDistance给的是5000而不是无穷大这是个刻意为之的选择第 4 章会解释为什么。2.2 坐标高度那点事fromDegrees 的第三个参数不是摆设Cesium.Cartesian3.fromDegrees(lon, lat, height)的第三个参数很多人习惯性写 0 或者干脆不写在纯椭球底图上确实没问题但一旦加载了地形就会出问题——因为椭球面和真实地形之间可能差几百米写 0 意味着你的图标锚定在椭球面实际地形如果在椭球面之上图标就被埋了。heightReference: Cesium.HeightReference.CLAMP_TO_GROUND就是干这个的它会异步去采样该经纬度下的地形高度或者 3D Tiles 表面高度把 position 的 z 值改成贴合地面的值。注意异步两个字——刚 add 进去的那一帧图标可能还在原来的高度上一两帧之后才落下去。所以如果你的业务逻辑依赖添加后立刻获取图标实际高度会拿到没 clamp 之前的值这是个坑。Cesium 提供了scene.sampleHeight和scene.clampToHeight这类同步方法需要scene.globe.depthTestAgainstTerrain true并且地形加载完成如果业务确实需要立即拿到高度可以自己先 clamp 再传进去绕开异步问题。还有一点CLAMP_TO_GROUND对 3D Tiles 的贴合能力受scene.globe.depthTestAgainstTerrain和模型的精细程度影响倾斜摄影数据上经常会出现贴在半空的情况因为模型表面有缝隙或者精度不够。遇到这种情况RELATIVE_TO_GROUND配合手动的少量偏移值往往更实在。3. BillboardGraphics 的构造参数里真正影响成败的就这八项BillboardGraphics的属性有二十多个但实际项目里高频使用的就那么几个。剩下的要么是特殊场景才用imageSubRegion、alignedAxis要么是几乎不用pixelOffsetScaleByDistance。这一章只讲真正决定成败的那部分。3.1 imageURL、Canvas、DataURL 三条路的取舍image支持的类型比很多人想的多URL 字符串、HTMLCanvasElement、HTMLImageElement、HTMLVideoElement甚至是返回这些的Property。三条主流的路径各有适用场景URL 路径是最常规的/assets/icons/poi.png这种。Cesium 内部有资源缓存同一个 URL 多次引用不会重复发请求这点可以放心。但要注意跨域——如果图片和页面不同源且服务端没配 CORS 头某些浏览器环境下纹理会加载失败控制台报安全错误。这个坑在把图标放 CDN 上时特别容易踩症状是图标位置正确但显示为纯色方块或者干脆不显示。// 服务端需要返回Access-Control-Allow-Origin: * // 或者用 crossOrigin 参数交给浏览器处理部分场景有效 billboard: { image: https://your-cdn.example.com/icons/poi.png }Canvas 路径适合动态生成的图标比如带数字角标的告警点、带进度环的设备状态。做法是先document.createElement(canvas)画好再把 canvas 对象直接传给image。这里有个实测出来的坑改了 canvas 内容之后Cesium 不会自动重新上传纹理因为 image 属性的引用没变。要么每次生成一个新的 canvas 对象要么走下面第三条路。DataURL 路径是动态图标最稳的方案canvas.toDataURL(image/png)出来的字符串每次都是新的Property 求值时能感知到变化。代价是字符串比较和内存占用几百个动态图标还扛得住上千个就得想办法了。const canvas document.createElement(canvas); canvas.width 64; canvas.height 64; const ctx canvas.getContext(2d); ctx.fillStyle #e74c3c; ctx.beginPath(); ctx.arc(32, 32, 28, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle #fff; ctx.font bold 30px sans-serif; ctx.textAlign center; ctx.textBaseline middle; ctx.fillText(3, 32, 34); entity.billboard.image canvas.toDataURL(image/png);另外提一下 Cesium 自带的PinBuilder它能在运行时生成那种水滴形的 pin 图标还能带文字做点位标记的时候省得找设计要图。缺点是生成的样式固定且内部依赖 canvas 字体渲染图标在不同系统上可能有细微差异。3.2 verticalOrigin 与 horizontalOrigin让针尖对准真坐标这是 Billboard 里最容易被忽略、又最容易出问题的一对参数。默认值都是CENTER意味着图标的几何中心对齐到坐标点。对于圆形图标比如状态灯这是对的但对于水滴形 pin 就是灾难——针尖应该指向坐标结果中心点指过去了视觉上整个图标偏了半个高度。正确的组合是verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 图标底部对齐坐标 horizontalOrigin: Cesium.HorizontalOrigin.CENTER // 水平居中这样水滴形的针尖正好戳在坐标点上。但如果你给的是正方形图标BOTTOM就意味着图标完全在坐标点上方视觉重心会偏上这时候要么改用CENTER要么配合pixelOffset往上挪半个高度。pixelOffset是Cartesian2单位是像素而且是屏幕空间的——正 y 向下因为屏幕坐标系 y 轴朝下。这一点非常反直觉很多人第一次调的时候往正方向挪想往上走结果图标往下跑了。实测下来想往上挪new Cesium.Cartesian2(0, -20)。两者的配合逻辑我用一句话总结verticalOrigin负责锚点在图标的哪个位置pixelOffset负责在这个基础上再挪多少像素。前者是结构性的后者是微调性的。业务代码里最好把这两个量集中管理别散落在各个 add 调用里不然后期改图标尺寸的时候会疯。3.3 scale、sizeInMeters 与像素尺寸的三种表达控制图标大小有三种方式同时用会互相打架必须选一种主用width/height直接指定像素尺寸最直观推荐作为主用方式scale相对原始图片尺寸的缩放倍数适合做 hover 放大这类动态效果sizeInMeters: true配合scale图标以米为单位随距离缩放做真实尺寸标注才用sizeInMeters这个参数要特别小心。它设成true之后图标会随相机距离自动缩放近大远小看起来更三维。但在标注场景里这是灾难——用户拉远之后图标小得看不见拉近之后大得糊屏。除非你做的就是在地球上摆真实尺寸的广告牌这种需求否则保持false。scale在我的实践里主要干两件事一是 hover 高亮时1.0 → 1.2的轻微放大二是配合scaleByDistance做距离衰减billboard: { scaleByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.4) }这行的意思是相机距离 1000 米时缩放 1.0距离 50 万米时缩放 0.4中间线性插值。做全国尺度的点位展示时这个非常有用不然拉远之后满屏都是大图标叠在一起什么都看不清。同类参数还有translucentByDistance远处变淡和distanceDisplayCondition远处直接隐藏配合使用效果最好。3.4 color 与 alpha染色和调透明度不要混着来billboard.color是对纹理的乘法染色不是覆盖。默认是白色Color.WHITE意味着纹理原色输出设成Color.RED之后纹理中所有非黑像素都会被乘上红色分量白色部分变成红色深色部分基本不变。这个特性在做同一套图标不同状态色的时候特别香——准备一张白色的图标运行时按状态给 color红黄绿随便切不用出三套图。但要注意如果原图本身是彩色的染色结果会很脏所以做这套方案时图标素材必须是纯白或者纯灰度的。alpha单独设的话是整体透明度 0~1。这里有个细节color的 alpha 分量和billboard.alpha实际是相乘关系两个都设了非 1 的值时要留意叠加效果。我一般只用color的 alphabillboard.alpha保持不设避免两处配置打架。做闪烁告警效果的时候CallbackProperty驱动 alpha 是最常见的写法billboard: { image: /assets/icons/alarm.png, color: new Cesium.CallbackProperty((time) { const t Cesium.JulianDate.secondsOfDay(time) % 2; return Cesium.Color.RED.withAlpha(0.4 0.6 * Math.abs(Math.sin(t * Math.PI))); }, false) }false这个参数是isConstant必须显式传false否则 Cesium 会当成常量只求值一次。这个参数漏传导致动画不动的情况我见过太多次了。4. 图标穿地形、被建筑吃、跑到地球背面三条高频故障的排查链路这一章讲的是实际项目里反馈最多的三个现象我把排查过程按顺序写出来遇到问题可以照着走一遍。4.1 现象复现与第一层判断三个现象描述起来分别是穿地形相机从侧面看图标本应被山体挡住结果直接浮在山前面被建筑吃城市白模或者倾斜摄影上图标被楼体遮挡转到某些角度就看不见了跑到地球背面把地球转到另一面本该看不见的图标还在屏幕上显示这三个现象看着不一样根子上是同一组参数的两种极端配置。先做一个判断打开浏览器控制台把disableDepthTestDistance临时改成0刷新看现象是否消失。如果消失了说明确实是深度测试被关掉导致的如果没消失问题在别处比如 position 的高度值本身就不对图标飘在半空那不是遮挡问题。这个第一步很关键因为很多人一上来就去调heightReference方向就错了。heightReference管的是position 的 z 值怎么算disableDepthTestDistance管的是画的时候要不要跟深度缓冲比大小两个完全不同的东西混着调只会越来越乱。4.2 heightReference 与 disableDepthTestDistance 的互斥关系这两个参数的关系是全篇最需要记住的一条heightReference: CLAMP_TO_GROUND和disableDepthTestDistance: Number.POSITIVE_INFINITY是互斥的同时用必然出现跑地球背面的现象。原因不复杂。CLAMP_TO_GROUND让 position 贴地这没错但disableDepthTestDistance设成无穷大意味着渲染时完全跳过深度测试不管前面有什么都直接画。地球是个球体背面的图标虽然被球体挡住但因为不测深度照样被画出来。这就是为什么转到背面还能看见。正确的做法是二选一需求配置方案代价图标必须永远可见包括被山、楼挡住时disableDepthTestDistance给一个有限值比如 5000~50000heightReference不要设或设 NONE超出这个距离后仍会被遮挡图标必须严格贴地、允许被遮挡heightReference: CLAMP_TO_GROUNDdisableDepthTestDistance保持 0山谷、楼后的图标会被挡住图标贴地但想减少被挡CLAMP_TO_GROUND 有限值的disableDepthTestDistancedistanceDisplayCondition限制可见范围需要调参没有万能值disableDepthTestDistance的语义是当相机到图标的距离小于这个值时不做深度测试。所以它本质上是一个近距离强制可见的开关。给5000意味着 5 公里以内不管前面有什么都画出来超过 5 公里恢复正常遮挡逻辑。这个值怎么定看你的场景尺度——城市级 POI 给 3000~8000 比较合适省级展示给 50000 也行但千万别给无穷大。有个例外情况如果heightReference是NONE且位置本身在椭球面以上足够高disableDepthTestDistance给无穷大也不会穿地球因为图标本来就在球体外侧。所以这条互斥结论的前提是图标贴地。4.3 distanceDisplayCondition 兜底与 eyeOffset 的取舍distanceDisplayCondition是另一个常被忽略的参数但它能解决很多看起来无解的问题distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 200000)含义是只在相机距离 0~200 公里之间显示。这个参数做两件事一是性能优化远处的小图标根本看不清不如不画二是视觉兜底——即便disableDepthTestDistance配得不太对把可见范围限死之后跑地球背面的概率也大幅降低因为地球背面的点距离相机通常都超过了两万公里。eyeOffset是另一个思路在相机空间eye coordinates里对图标做偏移单位是米。它的经典用途是把图标往相机方向推一点避免和地形表面发生深度冲突z-fighting表现为图标边缘闪烁。但这个参数调起来很玄学偏移量给大了图标会明显脱离地面给小了不起作用。我的建议是能用disableDepthTestDistance解决的就别碰eyeOffset后者留给确实需要精细控制的场景。顺带说一句scene.globe.depthTestAgainstTerrain true这个全局开关。它控制地形是否参与深度测试默认是false。不打开的话地形不写深度缓冲所有贴地图标都不会被山体遮挡——这是图标穿山最容易被忽略的根因之一。开启它会带来一点性能开销但做地形相关的标注是必须的。5. 图标发虚、锯齿、错位跟分辨率与渲染管线有关的细节5.1 2 倍图与 resolutionScale 的配合设计师给的图标素材越来越多是 2 倍图比如 128×128 的图期望显示成 64×64这在 Cesium 里需要手动处理它不会像 CSS 那样自动按设备像素比缩放。两种处理方式方式一width: 64, height: 64图片是 128×128。Cesium 会把纹理缩采样到 64×64 显示。在高 DPI 屏幕上效果刚好在普通屏上是标准清晰度。这是最省事的做法。方式二width: 128, height: 128配合scale: 0.5。效果类似但 scale 会影响scaleByDistance的计算基准后期做距离衰减时要多算一层不如方式一干净。还有一个全局参数viewer.resolutionScale默认 1.0设成window.devicePixelRatio可以让整个场景按设备像素比渲染。注意这个参数影响的是整个画布的渲染分辨率开销是平方级增长的——resolutionScale 2意味着像素数量翻 4 倍。除非你的场景很简单否则不建议无脑开。做高清截图viewer.render()导出的时候临时调高截完再调回去这是比较稳妥的用法。如果图标还是发虚检查一下是不是图片本身的尺寸和width/height比例不匹配。比如 100×80 的图你强行设成 64×64就会变形拉伸。Cesium 不会报错就是难看。这种情况要么改图要么只设width让height按比例自适应Billboard 支持只给一个维度。5.2 Canvas 动态图标好用但别滥用用 canvas 动态生成图标数字角标、进度环、实时状态确实方便但有三个实际问题要注意。第一是性能。每次toDataURL都是完整的 PNG 编码几十毫秒起步。如果你有 500 个设备状态图标每秒刷新一次那就是每秒 500 次编码主线程直接卡死。正确的做法是把动态内容收敛——比如数字角标只允许 0~99 这 100 种预先生成 100 张缓存起来运行时按 key 取。第二是缓存失效。前面提过传 Canvas 对象本身不会触发纹理更新。而传 DataURL 虽然能更新但 Cesium 的图片缓存是按字符串 key 的每次都生成新的 DataURL 意味着缓存永远不命中内存会缓慢增长。实测下来几百个动态图标跑一两个小时内存涨个几十兆是常事。第三是清晰度。canvas 默认按 1 倍尺寸画在高分屏上会模糊。生成时把 canvas 尺寸设成目标尺寸的 2 倍然后width/height设成目标值就清晰了const size 64, dpr 2; const canvas document.createElement(canvas); canvas.width size * dpr; canvas.height size * dpr; const ctx canvas.getContext(2d); ctx.scale(dpr, dpr); // 之后按 64×64 的逻辑坐标画我的一般原则是能静态化的绝不动态化能预生成的绝不运行时算。图标这东西天生适合静态资源动态只在确实必要时用。6. 上千个图标怎么不卡从 Entity 到 BillboardCollection 的迁移6.1 先量化Entity 的属性系统开销在哪说Entity 慢之前得先搞清楚慢在哪不然优化就是瞎猜。Entity 的每个属性billboard.image、billboard.scale、position……都会被包装成一个Property对象。时钟每 tick 一次DataSourceDisplay就会遍历所有 Entity对每个 Property 调用getValue(time)求值再把结果同步到底层的 Billboard 实例上。即使你用的是常量ConstantProperty这个求值和同步的流程也不会跳过。所以Entity 的开销大致是 O(n) 的遍历 每个属性一次函数调用 一次赋值。单个 Entity 的 billboard 大概有 5~10 个有效属性1000 个 Entity 就是每帧 5000~10000 次函数调用加上等量的属性赋值。现代浏览器扛得住但会明显占用主线程时间在低端机或者和别的逻辑叠加时就容易掉帧。我做过一个粗略的对比测试同一台机器同一个视角只看帧率方式100 个1000 个5000 个Entity billboard60fps55fps30fps 以下明显卡顿BillboardCollection60fps60fps50fps 左右数字只是量级参考不同机器差异很大但趋势是明确的500 个以下 Entity 完全够用1000 个是分水岭3000 以上必须迁移。6.2 迁移到 BillboardCollection 的具体改法迁移本身不难难的是改造业务代码的数据绑定逻辑。Entity 那套CallbackProperty的响应式很方便换成 BillboardCollection 之后得自己维护同步。基本写法const billboards viewer.scene.primitives.add(new Cesium.BillboardCollection()); // 添加 const b billboards.add({ position: Cesium.Cartesian3.fromDegrees(108.9, 34.2, 100), image: /assets/icons/poi.png, width: 32, height: 32, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, disableDepthTestDistance: 5000, id: { type: poi, bizId: poi-001 } // 自定义数据挂在这 }); // 更新 b.scale 1.2; // 立刻生效不需要等 tick b.show false; // 删除 billboards.remove(b);几个迁移时的注意点id字段在这里是自由字段可以挂任何业务对象拾取时通过picked.id拿回来。这点比 Entity 灵活——Entity 的id只能是字符串。BillboardCollection不支持heightReference的自动 clamp严格说Billboard实例上有这个属性但行为和 Entity 那套不完全一致实测容易出问题。所以贴地需求得自己算好高度用scene.clampToHeight或者sampleHeight预先处理。这是迁移的主要痛点。BillboardCollection里的 Billboard 有position但没有完整的PositionProperty体系动画得自己在preRender事件里改。虽然麻烦但反过来也意味着完全可控不会被 Property 求值流程拖累。6.3 图集与按需加载迁移到 Collection 之后下一个瓶颈往往是纹理切换。每个不同image的 Billboard 都对应一次 draw call 或至少一次纹理绑定几百种不同图标的时候开销很明显。解法是图集sprite sheet把多个小图标拼成一张大图用imageSubRegion指定每个图标在大图上的矩形区域billboards.add({ position: pos, image: /assets/icons/sprite-atlas.png, imageSubRegion: new Cesium.BoundingRectangle(0, 0, 32, 32), width: 32, height: 32 });BoundingRectangle的坐标是像素原点在图片左下角Cesium 内部纹理坐标是 y 轴向上这一点和很多图集工具生成的 JSON通常是左上角原点不一致需要转换y_cesium atlasHeight - y_tool - height。这个偏移量算错会导致图标全花我第一次接图集的时候就被这个坑了两个小时。按需加载是另一个维度。如果图标种类很多但每个视角下只显示一小部分可以用distanceDisplayCondition或者按需 add/remove别一次性把所有 Billboard 都塞进 Collection。几千个不显示的 Billboard 依然参与包围盒计算和渲染遍历。7. 交互拾取、hover 高亮与 tooltip 的落地写法7.1 scene.pick 拿到的是 Billboard取业务数据走 picked.id拾取的标准写法const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (Cesium.defined(picked)) { if (picked.id instanceof Cesium.Entity) { // Entity 方式创建 console.log(entity id:, picked.id.id); } else { // BillboardCollection 方式创建id 是自己挂的对象 console.log(biz data:, picked.id); } } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);这里有个容易混淆的层级picked是PickedObjectpicked.primitive是实际渲染的Billboard实例picked.id是那个Billboard的id属性。Entity 方式下Visualizer 会把 Entity 自己挂到 Billboard 的 id 上所以你拿到的picked.id就是那个 Entity。Collection 方式下你 add 时id传什么就拿到什么。有个实践细节拾取时最好先判断picked.primitive instanceof Cesium.Billboard因为点击可能打在 Label、Model、地形上不判断的话后续代码可能报错。另外scene.pick在每帧只能调用有限次数高频调用比如 mousemove要做好节流。7.2 hover 高亮改 scale 还是改 color以及为什么要节流hover 高亮的实现有两种常见方式各有代价改 scale是最直观的billboard.scale 1.2鼠标移出还原成 1.0。问题是 scale 大了之后图标会有点模糊纹理被放大采样而且如果图标本来就有scaleByDistance两个 scale 会互相覆盖得在代码里自己算乘积。改 color更干净比如 hover 时给Color.YELLOW的染色移出还原Color.WHITE。缺点是如果原图是彩色的染色效果会偏如果是纯白图标推荐做法效果很正。我这边的标准做法是图标素材统一用纯白hover 时改 color同时轻微放大 scale。两者叠加的视觉效果最好。关键在节流。mousemove每秒触发几十次每次都调scene.pick加上属性赋值负担不小。我一般是自己维护一个hoveredId只在 id 发生变化时才做还原和高亮let lastHovered null; handler.setInputAction((movement) { const picked viewer.scene.pick(movement.endPosition); const current Cesium.defined(picked) ? picked.id : null; if (current lastHovered) return; if (lastHovered) { lastHovered.billboard.color Cesium.Color.WHITE; lastHovered.billboard.scale 1.0; } if (current current.billboard) { current.billboard.color Cesium.Color.YELLOW; current.billboard.scale 1.15; } lastHovered current; viewer.scene.canvas.style.cursor current ? pointer : default; }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);这个写法的核心是那个if (current lastHovered) return;。少了这一行鼠标在图标上稍微动一下就会触发几十次属性赋值在 Entity 体系下每次赋值都要等下一帧求值实际表现就是鼠标移动时有轻微卡顿感。无缝衔接到这里其实差不多把 Billboard 从概念到运维的整条链路走完了。最后分享一个我踩过好几次的小经验图标素材的命名一定要带尺寸和状态比如poi-alarm-322x.png别用icon1.png这种。项目跑到三个月之后你会感谢当初命名规范的自己——因为那时候你根本想不起来icon1到底是哪个场景的哪个状态。
RELATED

相关推荐

AI工程四语言协同:Python原型、Rust交付、TS契约、Julia计算

AI工程四语言协同:Python原型、Rust交付、TS契约、Julia计算

1. 从零开始构建AI工程能力:不是学框架,而是重建“AI系统思维”你有没有发现,现在打开招聘网站搜“AI工程师”,岗位要求里清一色写着“熟悉PyTorch/TensorFlow”“有LLM微调经验”“掌握LangChain/RAG流程”。但真正坐到工位上第一…

📅 2026/11/2 5:38:09
C++格式化输出三套体系:printf、cout与std::format深度对比

C++格式化输出三套体系:printf、cout与std::format深度对比

1. 项目概述:为什么“C 占位符”突然成了高频学习关键词?最近在几个技术社区和初学者交流群中,频繁看到“C 占位符”被单独拎出来讨论——不是作为某本教材里的一个冷门小节,而是作为独立的学习主题被反复提问:“C里到…

📅 2026/11/2 5:33:09
以太坊三棵树设计解析:MPT与状态存储优化

以太坊三棵树设计解析:MPT与状态存储优化

1. 以太坊三棵树的核心设计理念以太坊区块链系统中状态树、交易树和收据树的设计,本质上是为了解决区块链三元悖论中的状态存储难题。传统区块链系统如比特币采用UTXO模型,交易本身就是状态,而以太坊作为图灵完备的智能合约平台,需…

📅 2026/11/2 5:33:09
MORE NEWS

更多资讯

📰

超级鹰版免登录实战(一):Chaojiying.py + DrissionPage 实现【古诗文网图片验证码自动识别免登录】保姆级实战教程

一、 前置回顾(承接上篇文章)承接上篇:超级鹰账号注册、充值、获取 softid 💡 核心重点: 1. 使用现成 Chaojiying_Client 超级鹰打码 2. DrissionPage 操控浏览器,元素截图提取验证码 3. 题型编码 1004&…

📰

专其利 AI V3.4.0 更新说明

专其利 AI V3.4.0 升级说明 AI 对话,更懂你在写什么 输入框支持 快捷引用——直接关联当前专利、上传文件或图片,AI 自动理解上下文,无需反复粘贴内容图片直接上传:支持拖拽或粘贴图片,AI 自动识别图中技术内容并纳入…

📰

RVC 开源音色转换实战:Retrieval-based Voice Conversion 与 WebUI 从安装到调参

RVC 开源音色转换实战:Retrieval-based Voice Conversion 与 WebUI 从安装到调参 RVC(Retrieval-based Voice Conversion,社区常用实现与 WebUI 多托管于 GitHub 相关仓库,检索关键词 RVC-Project / Retrieval-based-Voice-Conver…

📰

深度学习毕业设计选题推荐:基于YOLO11的交通标志检测系统设计与实现(Python+Django+Vue3+对接deepseek)

🔥作者:雨晨源码🔥 💖简介:java、微信小程序、安卓;定制开发,远程调试 代码讲解,文档指导,ppt制作💖 精彩专栏推荐订阅:在下方专栏👇&…

📰

听故事学语言 · 数据结构篇 · 第一章 当千万条数据肩并肩

本书是《听故事学语言》第二册。零基础的建议从序章三读起。 听故事学语言 数据结构篇 第一章 当千万条数据肩并肩 本章讲述:数据是什么,结构是什么,一张演出单如何立起来,第一场舞如何跳;以及九年前两份手写程序,如何在九年后当庭对质。第一册结尾那场集体舞,这一册…

📰

你的迭代器为何总在“第二次”失灵?——Python iter() 与 next() 的协议真相与完全驾驭手册

你的迭代器为何总在“第二次”失灵?——Python iter() 与 next() 的协议真相与完全驾驭手册 在 Python 中,for 循环几乎无处不在,但支撑它运转的底层机制——iter() 和 next()——却常常被开发者忽略。你可能已经习惯写 for x in data:&#…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬