map-3d · diff
git:20261009.8aa7a53 to git:20261009.683f73a
27 added, 1 removed. Audit A to A.
---
name: map-3d
- description: "3D 地形地图视频:真实立体山体(卫星贴图 + 高程)上做轨迹/路线/地点动画,竖屏 MP4。卫星地球俯冲 → 整条路线 3D 鸟瞰 → 镜头贴着人/车沿路线走(走过的线变黄、里程、海拔、累计爬升、小地图、底部海拔剖面、沿途弹照片)→ 拉回全景出总数据;或单个地点俯冲后绕着转一圈(地名、经纬度、海拔)。支持徒步/登山(GPX 或 OSM 小路连线)、自驾、骑行、步行(高德路线规划)、景点/雪山环绕。用 map-motion 的引擎(MapLibre 渲染)。当用户说「3D 地图」「立体地图」「3D 地形」「像 Google Earth 那样」「两步路/六只脚那种 3D 轨迹回放」「3D 徒步轨迹」「自驾路线 3D」「山体立体」「环绕雪山」「3D 版本」,或拿来 GPX/路线要做有山体起伏的地图视频时使用。平面风格(手账、霓虹、高德标准等)、多镜头组合、飞线、行政区仍用 map-motion 主 skill。"
+ description: "3D 地形地图视频:真实立体山体(卫星贴图 + 高程)上做轨迹/路线/地点动画,竖屏 MP4。卫星地球俯冲 → 整条路线 3D 鸟瞰 → 镜头贴着人/车沿路线走(走过的线变黄、里程、海拔、累计爬升、小地图、底部海拔剖面、沿途弹照片)→ 拉回全景出总数据;或单个地点俯冲后绕着转一圈(地名、经纬度、海拔);或光线推演:某天日出/日落时太阳照到哪座山、几点日照金山、机位几点见光(按真实太阳位置 + 山体遮挡算)。支持徒步/登山(GPX 或 OSM 小路连线)、自驾、骑行、步行(高德路线规划)、景点/雪山环绕。用 map-motion 的引擎(MapLibre 渲染)。当用户说「3D 地图」「立体地图」「3D 地形」「像 Google Earth 那样」「两步路/六只脚那种 3D 轨迹回放」「3D 徒步轨迹」「自驾路线 3D」「山体立体」「环绕雪山」「3D 版本」「日照金山几点」「日出/日落机位」「光线推演」「几点见光」,或拿来 GPX/路线要做有山体起伏的地图视频时使用。平面风格(手账、霓虹、高德标准等)、多镜头组合、飞线、行政区仍用 map-motion 主 skill。"
---
# 3D 地形 map-3d
一份 spec(`"mode": "3d"`)→ 一段竖屏 3D 片子。渲染引擎是 map-motion(`scripts/terrain3d.py` 算镜头,`scripts/runtime/t3d.js` + MapLibre GL 出画面),本 skill 管流程:选路线来源 → 写 spec → 连续小段检查 → 出片。
| 片型 | 节奏(缺省) | 用在 |
|---|---|---|
| **路线**(有 `track`) | 0–6s 地球俯冲 → 6–8.6s 整条路线鸟瞰、白线画出、标题 → 2.4s 压低贴到起点 → 跟随 10–26s(按长度)→ 6s 拉回全景、绕一点、总里程/爬升/最高海拔 | 徒步、登山、自驾、骑行、跑步、步道 |
+ | **光线**(`"mode": "sun"`) | 0–6s 俯冲到山峰 → 从机位方向看山,14s 时间快进(关键时刻附近慢放):山体明暗跟真实太阳方位走、天空变色、太阳方向光线、顶部时钟+罗盘、底部关键时刻表到点打勾 → 拉远看机位和视线 | 日照金山、日出日落机位规划、「几点有光」 |
| **单点**(只有 `place`) | 0–6s 俯冲 → 8s 绕着转(缺省 90°),底部地名卡+经纬度+海拔滚动 | 雪山、景点、营地、「我在这里」 |
+ ### 光线片 spec
+ ```json
+ {"mode": "sun", "peak": [98.6833, 28.4367], "name": "卡瓦格博", "ele": 6740,
+ "spot": {"name": "飞来寺观景台", "at": [98.9177, 28.4114]}, "date": "2026-10-15", "event": "sunrise"}
+ ```
+ | 字段 | 说明 |
+ |---|---|
+ | `peak` `name` `ele` | 要拍的山(坐标或地名);`ele` 写公开海拔(峰顶受光时刻按它算) |
+ | `spot` | 机位,可省(省了就背对太阳看受光面,没有「机位见光」一行) |
+ | `date` `event` `tz` | 日期、`sunrise`/`sunset`、时区(缺省 8) |
+ | `zoom` `pitch` `bearing` `lapse` | 看山镜头(缺省 12.6 / 70° / 机位看山的方向)、快进秒数(14) |
+ 编译时打印关键时刻(天文日出、山峰受光=日照金山开始、金色褪去=太阳高度 5°、机位见光),**交付时把这几行原样告诉用户**,并说明:按 30 m 高程推算,云和雾算不了,误差约 ±3 分钟;机位见光受身边小山坡影响大,坐标要准。
+
## 准备
- map-motion 引擎(同一仓库根目录 `scripts/`)、uv、ffmpeg、Chromium(`uv run --with playwright playwright install chromium`)。高德 key 只在用地名或高德路线时需要(见 map-motion 主 skill)。
- `X=~/.claude/skills/map-3d/scripts/m3d.sh`
- `$X check spec.json` 在每个关键时刻前渲 0.6 秒连续小段并**自动质检**(出画、文字互压、黑块),输出文字结论;拼图照样生成(5–6 分钟)
- `$X make spec.json 成片.mp4` 整片 + 预览 GIF(约 1 秒一帧:30 秒片子 15–25 分钟,**放后台跑**);边渲边每 0.5 秒抽检,结束只出几行摘要
- `$X status` 一行看进度(别反复读日志)
## 流程
### 1. 定路线来源(按可信度)
| 用户给了什么 | `track` 写法 | 说明 |
|---|---|---|
| GPX / KML 转的 GeoJSON(两步路、六只脚、Strava、Keep、手表) | `"track": "xx.gpx"` | 最准,就是真实走过的线。WGS-84,不用转 |
| 只说了起终点,开车/骑车/城里走 | `{"mode": "driving" \| "bicycling" \| "walking", "from": "地名", "to": "地名", "via": [...]}` | 高德路线规划(GCJ → 自动转 WGS)。景区台阶路高德步行也常有(泰山红门→南天门实测可用) |
| 山野徒步,没有 GPX | `{"via": [途经点…], "margin": 0.02}` | 沿 OpenStreetMap 小路取最短路连起来。山里 OSM 常断头:报「不连通」就加中间点、缩短,或请用户给 GPX |
| 一串坐标 | `[[lng, lat], …]` | 直接用 |
| 只有一个地方 | 不写 `track`,写 `"place"` | 单点环绕 |
地名一律经高德解析,编译会打印「地点 X → 解析结果」——**每行核对**(同名 POI 会解析错)。山峰 POI 可能偏几公里,主峰直接写公开资料的 WGS 坐标。
### 2. 写 spec
从 `examples/` 挑最像的复制改:
| 例子 | 内容 |
|---|---|
| `hike_osm_yubeng.json` | 雨崩:南争垭口 → 上村 → 下村 → 神瀑,OSM 小路连线,9.8 km |
| `walk_amap_taishan.json` | 泰山红门 → 中天门 → 南天门,高德步行,6.1 km 爬升 1181 m |
| `drive_amap_shangrila.json` | 香格里拉 → 飞来寺,高德驾车 171 km(长路线自动拉远镜头) |
| `place_orbit_meili.json` | 卡瓦格博单点俯冲 + 环绕 |
+ | `sun_meili_sunrise.json` | 10 月 15 日飞来寺看卡瓦格博日照金山:07:21 受光,07:52 褪色,飞来寺 09:54 才见光 |
| 字段 | 说明 |
|---|---|
| `title` / `sub` | 鸟瞰和片尾标题;长标题自动缩字号 |
| `marks` | 地名小标:`{"name", "at"}`,`at` 可写地名;名字里可以带海拔(「卡瓦格博 6740m」) |
| `photos` | 沿途照片:`{"image", "at" 或 "km", "label"}`;走到那里停 `pause`(2.2s)弹大图,之后缩成小牌子留在原地。有人像的照片提醒用户这是要公开的 |
| `gcj` | `true` = spec 里的数字坐标是高德坐标;缺省按 WGS-84 |
| `zoom` / `pitch` | 跟随镜头,缺省按长度自动(10 km 内 15.1 级;170 km 约 11.7 级)/ 62° |
| `exaggeration` | 地形夸张,缺省 1.35;平原路线可加到 2 |
| `follow` `pause` `dive` `overview` `swoop` `end` | 各段秒数 |
| `color` | 走过的线和数字颜色,缺省 `#f2ee3a` |
| 单点:`place` `name` `ele` `zoom` `pitch` `bearing` `turn` `orbit` | `ele` 写已知海拔(高程数据 30 m 网格会削峰:卡瓦格博 DEM 只有 6469 m) |
### 3. 检查(必做,用连续小段,不要只看单帧)
```sh
cd 项目目录 && $X check spec.json
```
- 「✓ 质检通过」:机器能查的都没问题,**不用读拼图**;只有要判断构图好不好看时才读一次。
- 「✗ 质检 N 项」:按报的时刻和内容改,必要时读拼图对照。
机器查不了、要人看的:
- 俯冲末/鸟瞰:整条线在画面里、标题不压线;**没有黑块、没有直边断口**(远处地形缺瓦片的样子)
- 跟随:人在画面中部、前方能看到白线;顶部里程和小地图、底部海拔剖面清楚
- 片尾:整条黄线都在画面内,总数据卡不压线
- 不对就改 `zoom`/`pitch`/`exaggeration` 或 marks,再 check
### 4. 出片(后台)
```sh
$X make spec.json 名字.mp4 # 同时出 名字.gif(270px 预览)
```
首次跑会下载几千张卫星/高程瓦片(缓存在 `~/.cache/map-motion/tiles/`,重渲很快)。
### 5. 交付
说清:文件位置、时长;路线来自哪(GPX / 高德规划 / OSM 小路推测——推测的要明说「不是你实际走的线」);里程、爬升是按 AWS 高程数据算的(和手表数字会差 5–15%);影像 Esri、地形 Mapzen/AWS 已在画面右下角署名。
## 踩过的坑
- 单帧 `--stills` 一切正常,连续出片却露黑块/直边断口——MapLibre 连续换相机时远处瓦片会漏。所以检查一律用 `check`(连续小段)。黑块判定只算纯黑像素(RGB < 3),山体阴影不会误报。引擎已处理:12.2 级后换平面投影、天空 2D 自画、全景俯角 40–44°。
- MapLibre 5.6 在地球↔地形切换时抛 `clearFadeHold`,已锁 5.24。
- 国内也用 Esri 卫星(WGS-84):高德卫星是 GCJ,和地形、GPX 对不上。所以 3D 片里所有高德来的点都转回 WGS。
- 雪山上白字看不清 → HUD 上下有压暗渐变;顶部 290px 内的地名小标会自动淡出,避免压住里程。
- 软件 WebGL(SwiftShader)慢且吃 CPU,别同时跑两个整片。
+
+ ## 公共组件(加新片型时复用,别重写)
+ | 件 | 管什么 |
+ |---|---|
+ | `scripts/geo.py` | 距离、方位、沿方位走 N km、墨卡托、缓动、`fit_cam`(把一堆点装进画面)、`dive`(地球俯冲)、`blend`(两镜头过渡)、`frame`、`resolve`(地名/坐标→WGS) |
+ | `scripts/dem.py` / `sun.py` | 任意点海拔;太阳方位高度、地平线遮挡、过零时刻 |
+ | `scripts/runtime/hud.js` | 字体、描影文字、斜体大数字、压暗渐变、底板、照片牌、脉冲落点、度分秒、标题、署名 |
+ | `render.py` / `qa.py` / `status.py` | 按 `runtime` 字段加载 `runtime/<名>.html`,逐帧出片、自动质检、进度 |
+ | `m3d.py check` | 读时间轴里的 `checks`(关键时刻),不用为新片型改 |
+
+ 新片型=`scripts/<模块>.py` 的 `compile(SPEC, sp, out, W, H)`(在 compile.py 的 `MODES` 登记)出时间轴,带 `checks`;画面能用 t3d.js 就在里面加一块(如 `SUN`),否则新写 `runtime/<名>.html` 实现 `prepare(t)` / `renderFrame(t)` / `__canvas`。