# Earth Vessel Rendering Performance Plan ## 背景 Earth 船只图层已经形成了一套较好的视觉语言: - 航行船只使用三角形标记 - 标记按航向旋转 - 停泊或低速船只使用圆点 - 不同船型使用不同颜色 - hover / locked 状态有放大、透明度和聚焦反馈 - 标记带有轻微 glow / soft edge,和 Earth HUD 的观感一致 当前性能问题不应通过降级成普通 `Points` 来解决。目标是在保留现有观赏性的前提下,把底层从“每艘船一个 Sprite 对象”优化为批量绘制和轻量交互。 ## 当前问题判断 卫星图层能承载几万个对象,是因为它主要走 `THREE.Points` / `BufferGeometry` / instanced trail 路径。船只图层目前每艘船创建一个 `THREE.Sprite` 和独立 `SpriteMaterial`,这会带来: - draw call 随船只数量增长 - 透明 sprite 排序和 overdraw 成本上升 - 每帧遍历所有船只更新 opacity / scale / visible - pointer move 时对船只 sprite 做对象级 raycast - hover reset 时全量遍历 marker 因此,即使免费 BarentsWatch AIS 只开放挪威周边数据,前端仍可能因为对象级 sprite、raycast 和每帧全量更新出现地球拖动卡顿。 ## 目标 1. 保留当前船只标记的视觉质量。 2. 保留 hover tooltip、点击详情、lock、轨迹等交互。 3. 显著降低 draw call、每帧 JS 遍历和 pointer picking 成本。 4. 为后续全球 AIS 或更高船只数量预留扩展空间。 ## 非目标 - 不把船只降级为普通无方向 `Points`。 - 不取消船型颜色、航向三角和停泊圆点。 - 不为了短期性能直接删除 hover / click 交互。 ## Phase 1:交互路径止血 这一阶段不改视觉,只减少 pointer move 和 hover 状态开销。 ### 1. 拖动和惯性期间跳过船只 picking 地球拖动时用户主要关注视角变化,不需要每个 pointer move 都命中船只。 处理方式: - `isDragging === true` 时跳过船只 hover picking。 - 惯性旋转期间也跳过船只 hover picking。 - 拖动结束后再恢复 hover 检测。 ### 2. vessel hover picking 节流 对船只 hover 命中增加节流,例如 `80ms ~ 120ms` 一次。鼠标高速移动时复用上一次 hover 状态,不在每个 pointer event 上都做 raycast。 ### 3. hover reset 从全量遍历改为增量更新 当前 `resetTransientVesselStates()` 会遍历所有船只。改为记录: - `hoveredVessel` - `lockedObject` 当 hover 目标变化时,只更新旧 hover 和新 hover。 ### 4. 点击路径只在 click 时做一次精确 picking 点击仍保留精确命中,但只在 click 事件里执行,不参与拖动和高频 pointer move。 ## Phase 2:每帧更新减负 这一阶段仍保留 `Sprite` 外观,但减少每帧对全部 marker 的写操作。 ### 1. `updateVesselVisualState()` 增量化 当前每帧都会遍历船只并写: - `marker.material.opacity` - `marker.scale` - `marker.visible` 优化方向: - 图层关闭时直接 return。 - 没有船只时直接 return。 - 只有以下状态变化时才更新 marker: - show/hide 变化 - hover 变化 - locked 变化 - camera zoom / distance scale 变化超过阈值 - focus dim 状态变化 ### 2. 缓存 distance scale `getDistanceScale(camera)` 可以按 camera distance 或 zoom 阈值缓存。缩放没有明显变化时,不必每帧重设所有船只 scale。 ### 3. 降低透明 overdraw 在不破坏视觉的前提下微调: - marker 基础尺寸 - glow blur 半径 - 最大 size stabilization 目标是减少屏幕空间重叠面积,而不是改变符号设计。 ## Phase 3:保留视觉的批量渲染 正式方案是把每艘船的视觉从 `THREE.Sprite` 迁移为 instanced sprite batch。 ### 1. 使用 instanced quad 每艘船仍然显示为带贴图/软边的 billboard,但底层使用: - `THREE.InstancedBufferGeometry` - 每类船只一个或少量 material - per-instance attributes 可按形状和船型拆 batch: - moving cargo - moving tanker - moving passenger - moving fishing - moving military - moving other - anchored / slow dot 这样 draw call 从“每艘船一个”变为“每类船只一个”。 ### 2. per-instance attributes 每个 instance 存: - position - color - rotation - scale - opacity - state - mmsi / data index hover、locked、dimmed 通过更新少量 instance attribute 实现,不再逐个修改 material。 ### 3. 复刻当前视觉 视觉上继续使用当前 canvas texture 或等效 shader: - moving 使用三角形纹理 - anchored 使用圆点纹理 - 保留 soft glow - 保留航向 rotation - 保留 hover / locked 放大 因此用户看到的效果应与当前船只图层基本一致。 ## Phase 4:picking 改造 批量渲染后不再适合对所有 sprite object 做 `raycaster.intersectObjects()`。 ### 1. 屏幕空间 picking 参考卫星 picking: 1. 过滤背面船只。 2. 将候选船只世界坐标投影到屏幕。 3. 用鼠标位置计算距离。 4. 取距离最近且小于半径阈值的船只。 ### 2. 可选空间索引 如果后续船只数量明显上升,可增加轻量空间索引: - 经纬度网格 bucket - 屏幕空间 bucket - viewport bbox 过滤 第一阶段不必引入复杂索引。 ## Phase 5:数据层和 LOD 当接入全球 AIS 或船只数量显著增加时,再做数据层优化。 ### 1. 请求视口范围 前端请求 `/api/v1/visualization/geo/vessels` 时带上当前视口 `bbox`,减少无关船只。 ### 2. 后端排序策略 从单纯 `received_at desc` 改为综合排序: - 数据新鲜度 - 船型优先级 - 当前视口相关性 - 是否正在航行 ### 3. 远景聚合 远景可显示聚合或 top N,近景展开单船。 ## 验收指标 1. 船只视觉效果保持当前质量:三角、圆点、颜色、航向、hover、lock 都保留。 2. 开启船只图层后拖动地球不应明显掉帧。 3. pointer move 不应因为船只 hover 导致卡顿。 4. 船只数量达到 `1000` 级别时仍可顺畅旋转地球。 5. `renderer.info.render.calls` 相比 Sprite 版本显著下降。 6. hover / click 命中体验不低于当前版本。 ## 建议落地顺序 1. 先做 Phase 1,快速恢复地球拖动手感。 2. 再做 Phase 2,减少每帧 JS 写操作。 3. 最后做 Phase 3 和 Phase 4,把船只迁移到 instanced sprite batch。 4. Phase 5 等全球船只数据或数量压力出现后再推进。