Appearance
高德车道级精模渲染:资源、锚点与比例尺
车道级精模不是「把模型摆到地图上」这么简单:资源要从 Nova 云配按城市圈选下发,经纬度要卡在高德 SDK 的 BeginDraw/EndDraw 之间转成三角化坐标,Unity 侧再用每帧浮动的锚点把千万米级的墨卡托坐标压回厘米级精度。
首批地标3 城 11 个
定位精度厘米级
比例尺范围Level 8–24
背景
6.0 一张图把车道级能力从百度 SDK 迁到高德 SDK,精模(车道级建筑物 + 城市地标)是视觉差异最大的一块。整条链路分四段:Nova 云配平台按城市发布精模资源包,车端下载校验后入库;运行时根据屏幕中心点圈选周边模型下发给 Unity;高德 SDK 负责把经纬度转成与底图对齐的三角化坐标;Unity Napa5 负责摆放和渲染。
首批地标覆盖 3 个城市 11 个点位:北京(CCTV 总部、中国尊、鸟巢、水立方)、上海(东方明珠、上海中心、环球金融中心、金茂大厦)、广州(广州塔、小鹏广州总部、猎德大桥),每个地标都要做日间(浅色)和夜间(深色)两套效果。
问题
| 问题 | 具体表现 | 不处理的后果 |
|---|---|---|
| 中心点拿不到 | 高德不透出屏幕中心点经纬度,只能靠接口轮询 GAMapCameraInfo 的 targetX/Y/Z | 圈选逻辑无从下手 |
| 坐标转换有时序约束 | 经纬度 → 三角化坐标必须卡在 SDK 的 BeginDraw/EndDraw 之间调用,且要每帧跟随 | 跨帧转换,精模和底图错位 |
| 浮点精度不够 | 墨卡托坐标千万米级,Unity float 在千万量级只剩米级精度 | 车道级导航要的厘米级定位直接丢光 |
| 资源治理复杂 | 城市索引、版本号、下载中状态、重复资源都要管理 | 重复下载、旧版本不更新、磁盘浪费 |
| 昼夜双模式 | 地标夜间要发光,贴图通道和日间完全不同 | 一套资源打天下,夜间效果糊掉 |
方案
mermaid
flowchart LR
A["Nova 云配<br>按城市发布精模包"] --> B["车端下载校验<br>MD5/AES 双 hash"]
B --> C["本地库按 hash 存储<br>重复资源只装一次"]
C --> D["屏幕中心点 1s 回调<br>换算经纬度 + cityCode"]
D --> E["2km 矩形圈选<br>范围内模型 diff 下发"]
E --> F["高德 SDK 三角化<br>BeginDraw/EndDraw 之间"]
F --> G["Unity 浮动锚点换算<br>墨卡托 - 锚点"]
G --> H["Napa5 渲染<br>昼夜双模式"]核心思路是四件事各自闭环:资源侧用 hash 和版本号做幂等;圈选侧用 cityCode + 距离矩形做最小下发集;坐标侧把 SDK 转换严格钉在渲染帧边界内;Unity 侧用浮动锚点规避 float 精度问题。
关键实现
1. 屏幕中心点:百度和高德两条路
百度链路是 Unity → floorcenter → Android 回传;高德不透出这个值,只能在 GblMapDataHandler.FetchOnce 里通过 GetAmapCameraInfo 主动轮询 GAMapCameraInfo.targetX/targetY/targetZ。所以信号层(ldScreenCenterSignal)和 Unity 侧(unityModelStateFlow)都要按 SDK 来源分支:中心点 1 秒回调一次,只在 LD 状态下轮询,中心点没变就跳过后续查询。
2. 城市圈选与资源去重
拿到中心点经纬度后:先换算 cityCode(跨城市边界要取多个城市),按 cityCode 查 Nova 索引拿城市精模列表;再以中心点画 2km(可配)矩形,用 isPointInRectangle 过滤出范围内模型;最后按 id + versionName 做 diff,只把增量下发给 Unity,避免每帧全量调用把接口打爆。
资源本体带 MD5(原始)和 AES(加密后)双 hash,本地按 hash 分目录存储——同一资源被多个发布单引用时只安装一次,代价是删除前要检查引用计数。
3. 三角化坐标:钉在渲染帧边界里
高德提供两个关键 C++ 接口(IMapView):
ConvertCoordToAnchorPoint:把经纬度列表转成三角化坐标(渲染可用);SetUnrenderedRegion:告诉底图哪些区域不渲染(精模盖住的地方底图让位,避免 z-fighting)。
硬约束:转换必须在 BeginDraw 和 EndDraw 之间完成,并跟随每帧数据传给 Unity。错过这个窗口,转出来的坐标对应的是上一帧的相机状态,精模就会和底图错位漂移。
4. 浮动锚点:厘米级精度的来源
墨卡托 X/Y 范围约 ±2003 万米(上海地区约 (13523660, -3632940)),float 在这个量级精度只剩米级。解法是让锚点每帧浮动:
csharp
// 锚点 = 地图中心 XY + 车辆高度 Z,每帧重算
Vector3d anchor = new Vector3d(MapCenter.x, MapCenter.y, CarPos.z);
// Unity 坐标 = 墨卡托 - 锚点(double 减完再转 float)
float x = (float)(inX - anchor.x);跟车状态下锚点就是车的位置,车永远在 Unity 原点,世界围着车转——视觉上「车在往前开」,实际上是建筑在相对后退。用户拖动地图时锚点切到屏幕中心,车偏离原点,松手后回正动画把 MapCenter 插值回车辆位置。所有物体坐标保持在 ±几百米内,厘米级精度就回来了。
5. 比例尺:一条对数公式管所有缩放
mermaid
flowchart TD
A["相机高度 cameraHeight"] --> B["FScaleLevel = 24 - log2(h / 35.6947)"]
B --> C["FScale = 1 / 2^(24 - Level)"]
C --> D["可视宽度 ≈ h × 0.669"]
D --> E{"瓦片数 = ⌈宽度/512⌉²"}
E -->|"≤ 4 个(2×2)"| F["Unity 3D 车道级"]
E -->|"需要 9 个"| G["切 Android 2D 地图"]基准值 minCenterDist = 35.6947m,由 2560×1440 分辨率、FOV_Y 37° 和校正系数 0.009330691929342804 推出,实测值与理论值零误差。关键级别:
| Level | 相机高度 | FScale | 语义 |
|---|---|---|---|
| 24 | 35.69m | 1.0 | 最近视角,元素 1:1 |
| 23 | 71.39m | 0.5 | 默认车道级导航视角 |
| 19 | 1142.23m | 0.031 | 4 瓦片上限(百度/高德单次最多发 4 个) |
| 18.5 | 1615.36m | 0.022 | Editor 下切 2D 的临界点(Release 为 17.5) |
瓦片限 4 个不是拍脑袋:单瓦片 21–62MB,4 个瓦片要在 1 秒内完成下载、解析、建网格、传 GPU,再叠加 4G 带宽和内存里 16 个瓦片(可见 4 + 预载 8–12)的占用,9 个瓦片车机吃不消,所以 Level 18.5 以下直接切轻量 2D。
6. 地标昼夜双模式
夜间地标不是把日间贴图调暗,而是单独一套四通道资源:Diffuse+transparent(tga)、AO(tga)、Emit 自发光(exr)、Lightmap+0Env(exr,增加采样)。每个地标按「实现效果 = 实现方式 + 所需资源」登记到资源表,上传 Nova 后用 adb 暗号命令在车端实时调效果。
指标
- 首批 3 城 11 地标完成视觉设计与实现,持续细节优化。
- minCenterDist 实测 35.6947m,对数缩放公式实测误差为 0。
- Level 19 可视宽度 764.1m,2×2 瓦片覆盖率有余量;Level 18.5 需 9 瓦片,触发切换。
- 圈选半径 2km 可配,中心点未变化时不重复查询;资源 diff 按
id + versionName三向比对。
复盘
- 第三方 SDK 接入,先把「它不给你什么」列清楚(屏幕中心点、帧边界约束),再设计自己的信号分支,比事后打补丁便宜得多。
- 浮点精度问题不要想着升级 double 硬扛,浮动锚点一次减法就把问题消掉了——车不动、世界动,视觉完全等效。
- 比例尺、瓦片数、切换阈值要从相机参数一路推到公式,实测值和理论值对得上,后面所有 LOD 和降级决策才有底气。
- 昼夜双模式这类视觉需求要落到「每个地标几张图、什么格式」的资源表上,否则联调阶段一定漏。