feat: add cesium traffic signal countdowns

This commit is contained in:
2026-08-06 16:43:08 +08:00
parent 9fbc218e10
commit 043766b84e
22 changed files with 1022 additions and 170 deletions

View File

@@ -224,6 +224,29 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
`EXPORT_BASE_COLOR_OVERRIDES``EXPORT_EMISSION_OVERRIDES` 四张按材质名字符串匹配的表,
但它们只是旧 `.blend` 兼容回退。新材质不要只写旧表。
### 交通信号倒计时字体
`assets/fonts/7LED-1.ttf` 是项目纳入版本管理的倒计时字体。它的字形是反向轮廓:可见
的 LED 段是字体轮廓里的孔,而不是普通实心文字。因此 Blender 侧不能直接把文字曲线
转成普通填充面(会得到“发光背景+黑色数字”),也不能依赖曲线描边。正确做法是在
`blender/osmassets/traffic_signals.py` 中采样负 Bezier 轮廓,构造带前后盖面的挤出棱柱,
使 LED 段成为实心发光几何。数字 mesh 必须先在 Blender 中单独渲染确认,再进入 Cesium
导出导出器出现“Could not calculate tangents”只表示这些无 UV 的纯色网格没有切线,
不等同于倒计时集合为空或几何失败。
### 共享与拆分动态资产
倒计时数字按 phase group 共享 20 个数字 mesh0-19不要按信号灯复制网格。Cesium
阶段必须生成三个动态 GLB`traffic-signals-dynamic.glb` 只含灯珠,
`traffic-signals-countdown-0.glb``traffic-signals-countdown-1.glb` 分别含两个相位组的
倒计时节点。两个倒计时模型与灯珠模型使用同一个 `modelMatrix`,浏览器只切换当前数字
节点,并给整个倒计时模型设置 `color` + `ColorBlendMode.REPLACE`,从而让字色跟随当前
红/黄/绿相位且不增加每个灯的材质/几何副本。
导出器按完整材质名包含 `Countdown Group 0` / `Countdown Group 1` 判断分组;不能用
集合名的精确相等比较,否则实际材质名 `Traffic Signal Countdown Group 0` 会被误判为
空集合。
### 为什么新资产总是"发黑"
`export_cesium.py:38-54` 记录了这个反复出现的问题:
@@ -258,6 +281,8 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
| 加新资产不配 Cesium 调色 | Cesium 里显得发黑 |
| 靠调 `FOLIAGE_EMISSION` 提亮植被 | 用错了旋钮,该调 albedo gain |
| 在 `MATERIALS` 中间插入条目 | GLB 材质索引整体平移 |
| 直接用 Cesium `Model.getMaterial().setValue()` 改普通 glTF PBR 材质 | 运行时数字仍保持原色,不能实现相位字色 |
| 每个信号灯各自生成 0-19 全套倒计时 mesh | 节点和几何按信号数量线性膨胀;应按两个 phase group 共享 |
## 第三方资产导入的源文件边界

View File

@@ -165,6 +165,16 @@ def test_spacing_carries_across_segment_joins(self):
**推论**:能挪进纯 Python 层的逻辑就挪。一个函数只要不碰 `bpy`
放进 `geom.py` 就立刻获得测试覆盖的资格。
### 以 MeshBatch 为边界的静态设施测试
少数 bpy 要素模块的价值在于确定性地向 `MeshBatch` 追加顶点与面,而不是调用 bpy API
本身。对这类模块(例如 `osmassets/traffic_signals.py`),应在 `blender/tests/` 用假的
`osmassets.mesh.MeshBatch` 导入模块,断言有效输入的装配数量和关键几何方向。这样可覆盖
“校验函数意外返回空、所有要素被静默跳过”这一类错误,不必依赖可用的 Blender 进程。
测试必须在本文件列出的 `python3 -m unittest discover blender/tests` 命令下独立运行;测试
文件自己添加 `blender/``sys.path`,不能依赖其他测试的导入顺序。
---
## 反模式

View File

@@ -343,6 +343,84 @@ out.vehicleStopLines = crosswalkData.stopLines;
// 原生 lane_markings 停止线不得复制到输出。
```
## 信号锚点的跨阶段消费
### 1. Scope / Trigger
路口信号设施需要同时被 Blender 主 GLB 和 Cesium 预览消费时,使用
`<geojsonDir>/traffic_signals.json`。它是附属 intermediates 产物,而不是第十个
osm2streets/QGIS 图层。
### 2. Signatures
```bash
npm run build:area -- --config config/areas/<area>.json --stages intermediates,blender,cesium,preview
```
`normalizeAreaConfig()` 将默认路径归一化为:
```js
area.outputs.trafficSignals
// <areaDir>/osm2streets_web_out/traffic_signals.json
```
### 3. Contracts
- `build-area.js:writeTrafficSignals()` 是锚点 JSON 的生产者,调用
`traffic-signals.js:readTrafficSignals()`,输入为 `vehicle_stop_lines.geojson`
`intersection_surface.geojson`
- `intermediates``reimport` 都必须在其 GeoJSON 产物稳定后重写锚点,确保 QGIS
人工修补反导入后Blender 和 preview 仍使用同一事实。
- `blender``preview` 在启动前必须检查该文件存在;前者把静态设施写进 `05_Props`
后者只叠加动态灯珠、倒计时和车辆相位。
- `traffic_signals.json` 不得加入 `SCENE_LAYERS`、GeoPackage 或 QGIS 工程;这些层只能
继续包含九个道路场景图层。
- `layout.countdownLateralMeters` 等几何字段是 Blender/preview 的共同事实源;横向正值统一
表示相对来车方向的右侧。不得在任一消费方用独立的负号约定替代它。
- `layout.mastHeightMeters``layout.headCenterHeightMeters` 必须相等,表示横杆与灯壳的
中心对齐;`lensVerticalOffsetsMeters` 以灯壳中心为基准,正值向上、负值向下。当前倒计时牌
垂直偏移为 `0`,必须贴在横杆上而非悬挂。
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| `intermediates``reimport` 有合法停止线和路口面 | 写出 `version``signals` 数组,即使数组为空 |
| 直接运行 `blender` / `preview` 但锚点不存在 | 在启动外部工具前报 `Traffic signal anchors not found` |
| 单个停止线无法可靠关联路口 | 锚点生成器跳过该项,其他进口照常输出 |
| 用户仅修改 QGIS 后运行 `reimport` | 重新生成锚点,不沿用旧坐标 |
### 5. Good/Base/Bad Cases
- Good完整构建后GLB 的静态灯杆/灯壳和 Cesium 动态灯珠使用同一份 anchor。
- Base没有可用进口时写出空 `signals`Blender 继续生成其余场景。
- Bad在 Cesium 中再次推导灯杆位置,或把 anchors 导入 GeoPackage两者都会产生位置
漂移或污染人工 QGIS 工作流。
### 6. Tests Required
- `npm run test:budgets`:断言默认锚点路径位于 `osm2streets_web_out/`
- `npm run test:preview-assets`:断言预览配置仍传递相对锚点 URL。
- 目标区域完整构建:确认 `traffic_signals.json` 与 Blender/preview stage manifest 均存在。
- Blender 可运行环境:检查 `SCENE_DONE.traffic_signals`、主 GLB 的 `05_Props` 设施,
以及 Cesium 动态叠层与静态灯壳对齐。
### 7. Wrong vs Correct
错误:
```js
// preview 运行时再次从两份 GeoJSON 推导另一组锚点。
const signals = buildTrafficSignals(stopLines, intersections);
```
正确:
```js
// Blender 与 preview 都消费 intermediates 写出的同一份文件。
ensureFile(area.outputs.trafficSignals, "Traffic signal anchors");
```
## 区域诊断命令
### 1. Scope / Trigger

View File

@@ -201,6 +201,20 @@ GLB 停留在**局部 ENU 坐标系**X 东、Y 北、Z 上),靠伴生 JSO
`scenePlacement(metadata)``:131`)负责这一步。**改动导出侧的坐标约定必须同步改这里。**
## 交通信号动态覆盖层
metadata 的动态资产契约如下:
- `category="dynamic"`:灯珠节点,继续按相位切换红/黄/绿 lens 的 `show`
- `category="countdown"``phaseGroup``0``1`:对应相位组的倒计时模型;模型内
共享 20 个数字节点,不按每个信号复制数字。
三个模型必须使用完全相同的 `placement.modelMatrix`。倒计时颜色只能通过模型级
`model.color` 配合 `Cesium.ColorBlendMode.REPLACE` 设置;普通 glTF PBR 材质的
`getMaterial().setValue()` 在本项目验证中不能可靠修改运行时字色,禁止作为实现路径。
倒计时数字的显示逻辑只改变当前数字节点的 `show`,颜色由该 phase group 的当前灯色
统一设置。加载失败属于部分资产失败:应进入诊断而不清空主场景。
## 语义检查资产
### 1. 范围与触发条件

View File

@@ -89,3 +89,22 @@ edges.push(makeEdge(way, refs.reverse(), coords.reverse(), "backward"));
if (oneway !== "-1") edges.push(makeEdge(way, refs, coords, "forward"));
if (!isOneWay(oneway)) edges.push(makeEdge(way, [...refs].reverse(), [...coords].reverse(), "backward"));
```
## 信号动态 GLB 契约
`traffic_signals.json``pose.*` 是 Blender 静态设施、动态灯珠和倒计时共享的锚点。Blender
把发光灯珠导出为独立的 `*-traffic-signals-dynamic.glb`preview 必须使用与主 GLB 相同的
`scenePlacement(metadata).modelMatrix` 加载它Cesium 仅按命名灯珠节点切换 `show`。倒计时
例外:它由 Cesium Entity 从 `pose.countdown` 的 ENU 坐标与面向直接绘制,避免 glTF 轴变换
反转七段字形。
动态表面不能与静态镜片或倒计时外壳共面:镜片和数码管必须沿本地 `face` 轴前移
`(static_depth + dynamic_depth) / 2 + epsilon`。这是模型局部几何关系,不是经纬度修正;
否则静态网格会通过深度测试遮住发光状态,表现为灯不切换或数字不可见。
错误:在 Cesium 用 `fromDegrees`/Entity 重新计算动态设施,或将动态网格中心与静态表面中心
重合。
正确Blender 生成命名节点 `TrafficSignalDynamic_<signal-id>_<state>`;浏览器在同一 model
matrix 下加载该 GLB并只切换这些灯珠节点。倒计时 Entity 使用 `pose.countdown` 的经纬度、
高度、`faceHeadingDegrees` 生成与牌面相同的 ENU 坐标轴。

View File

@@ -1,10 +1,14 @@
# 设计Cesium 路口信号灯可视化
## 边界
## 分层边界
信号灯属于 Cesium 验证预览层,不进入 QGIS、GeoPackage、场景九图层、Blender 或主 GLB。
区域构建只额外写入一个轻量的信号锚点 JSON预览运行时读该 JSON 后以 Cesium 原生
Entity/Primitive 构成灯杆、灯头和发光灯珠。
信号锚点不是 QGIS 业务图层:它不进入 GeoPackage、场景九图层或 QGIS 工程。`intermediates`
`osm2streets_web_out/traffic_signals.json` 写出它Blender 和 preview 都消费同一份文件。
Blender 的 `05_Props` 负责所有静态设施:灯杆、横杆、灯头、熄灭灯珠和倒计时牌外壳。它们
`.blend` 和主 GLB 导出成为正式场景的一部分。Cesium 预览只负责动态覆盖层:当前相位的
发光灯珠、七段倒计时数字、Signals 显示开关,以及车辆在红黄灯前的等待。这样静态造型只有
一份,浏览器不再用临时 Entity 重复搭建设施。
## 锚点与几何
@@ -13,7 +17,14 @@ Entity/Primitive 构成灯杆、灯头和发光灯珠。
侧后方、道路外缘一侧,且朝向来车。没有可唯一关联的路口面、停止线过短或无法确定外侧时,
不输出锚点。
输出保存灯杆坐标、对应停止线坐标、朝向稳定 ID,避免浏览器重新解析 GeoJSON 或 OSM。
输出保存灯杆坐标、对应停止线坐标、朝向稳定 ID`layout` 几何契约,避免 Blender 或
浏览器重新解析 GeoJSON 或 OSM。`layout` 包含灯头、灯珠、横杆和倒计时牌的尺寸与偏移;
其中横向偏移以车辆行驶方向为基准,正值表示驾驶员右侧;`mastHeightMeters`
`headCenterHeightMeters` 是横杆和灯壳的共同中心高度。三颗灯珠相对灯壳中心排列,而倒计时牌
的垂直偏移为零、固定在横杆上。
Blender 使用
`Projector.xy((longitude, latitude))` 转成本地米制坐标,并以 `headingDegrees` 旋转Cesium
以同一字段派生地理位置与灯面朝向。
每个路口按相对进口方向分为两组对向相位;统一循环绿、黄、全红切换。
预览将每条路线按累计米数投影到信号停止线。只有距离阈值内且行驶方向与信号进口一致的
@@ -23,10 +34,11 @@ Entity/Primitive 构成灯杆、灯头和发光灯珠。
## 预览交互
预览加载锚点 JSON 失败时记录 warning场景、路线与车辆仍可用。加载成功时信号灯默认显示
并在现有 View 控件中提供独立 Signals 复选框。灯珠以 emissive 材质区分点亮和熄灭状态,
不依赖环境光;灯杆用低多边形几何,避免增加外部模型资产
并在现有 View 控件中提供独立 Signals 复选框。动态灯珠和数字以 emissive 材质区分点亮和
熄灭状态,不依赖环境光;静态部分由 GLB 的低多边形 MeshBatch 几何承载
## 风险与回退
信号灯是示意设施,不能视为 OSM 语义。复杂交叉口或人工修补后的不完整标线宁可跳过,也不
摆放到行车道中央。删除新锚点输出及预览加载逻辑即可完整回退,不影响既有主 GLB 或路线 JSON。
摆放到行车道中央。回退时删除附属锚点输出、`05_Props` 信号构件和 Cesium 动态覆盖层即可;
QGIS、道路/标线和既有路线 JSON 不受影响。

View File

@@ -1,21 +1,42 @@
# 实施计划Cesium 路口信号灯可视化
1. 从现有停止线和路口面建立稳定的信号锚点生成器,输出区域局部坐标、朝向、路口与相位组。
2. 将锚点 JSON 作为 preview 的可选支持文件写入区域输出与 preview manifest不触及主 GLB
3.Cesium 运行时加载可选锚点,构造立杆、灯头、红黄绿灯珠,并以统一相位钟更新状态。
4. 将路线投影到匹配停止线,在红/黄相位冻结累计里程、绿灯恢复推进;未匹配路线保持原行为
5. 添加 Signals 显示开关和诊断摘要,保持加载降级语义与既有 Controls 的布局。
6. 为锚点推导和预览配置/运行时编写针对性测试;构建目标区域并人工核对位置、相位与停车
1. 复用 `traffic-signals.js` 的锚点推导,在 `intermediates` 阶段把附属
`traffic_signals.json` 写入 `geojsonDir`;不修改 `SCENE_LAYERS`、GeoPackage 或 QGIS
2.`blender/osmassets/traffic_signals.py` 用共享低多边形 MeshBatch 几何装配静态信号设施,
并在 `generate_scene.py` 读取锚点、投影坐标、置入 `05_Props` 和写入计数
3. `catalog.MATERIALS` 末尾追加信号设施材质和 Cesium 导出补偿,保证新 GLB 在 Cesium
中不会发黑或材质索引漂移
4. 让 preview 直接读取 intermediates 的锚点文件;删除 Cesium 对立杆、横杆、灯头、熄灭灯珠
和外壳的构造,只保留精确对齐的动态灯珠、数字与既有相位/车辆等待。
5. 更新 Node 与纯 Python 测试,构建目标区域并用 parity 检查 GLB 差异只包含预期新增设施;
人工核对主 GLB 静态构件和 preview 动态覆盖层。
## 验证
```bash
npm run test:preview-assets
node --check scripts/lib/cesium-preview.js
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview
python3 -m unittest discover blender/tests
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates,blender,cesium,preview
git diff --check
```
## 已验证决策与故障记录
- `assets/fonts/7LED-1.ttf` 已纳入版本管理;该字体是反向 LED 轮廓。直接填充文字会得到
绿色背景/黑色字,曲线描边会得到空心描边字;当前实现改为采样负轮廓并构造实心挤出
棱柱,独立 Blender 渲染已确认数字形状正确。
- 为支持相位字色且避免复制几何,动态导出拆为灯珠 GLB + countdown group 0/1 两个 GLB。
预览三者共用同一 placement倒计时模型使用 `colorBlendMode=REPLACE` 做模型级换色。
- 失败方案Cesium `Model.getMaterial().setValue()` 修改普通 glTF PBR uniforms用户实测
数字仍为绿色,不能恢复使用。
- 失败陷阱:导出器的 `groups` 是完整材质名集合,不能检查精确字符串 `"Countdown Group 0"`
必须用 `"Countdown Group 0" in name` 的包含判断,否则导出阶段报
`Traffic countdown collection 0 is empty`
- glTF 的 `Could not calculate tangents` 警告来自无 UV 的纯色倒计时 mesh只要三个 GLB
均成功生成,它不是阻断错误。
## 回退
删除信号锚点的 preview 输出浏览器加载代码QGIS、GLB、车辆路线和原有预览功能不受影响。
删除静态信号 Blender 模块、附属锚点输出浏览器动态覆盖层QGIS、道路/标线和既有路线
JSON 不受影响。

View File

@@ -13,7 +13,10 @@
- 路线 JSON 包含连续的左、右、直转向曲线,但没有路口 ID 或信号相位字段。
- 现有停止线、斑马线和转向箭头已由区域构建确认,且用户要求暂不触及 QGIS 的人工修补
边界、道路生成与既有连续路线逻辑。
- Cesium 预览是验证层;改动它不应改变主 GLB、Blender 导出或 OSM/QGIS 产物。
- Cesium 当前同时绘制灯杆、横杆、灯头、熄灭灯珠、倒计时外壳,以及随相位变化的灯珠和
七段数字。这使静态设施只存在于验证层,难以随主场景维护。
- `05_Props` 集合已经进入主 `.blend` 和 Cesium GLB`traffic-signals.js` 已是停止线和
路口面推导信号锚点的唯一事实源。
## Requirements
@@ -26,16 +29,24 @@
- 等待逻辑只模拟单车对信号的响应,不做车辆间跟车距离、排队或碰撞避让。
- 首版以主要路口的程序化示意灯覆盖为准:从已生成的停止线和路口面推导进口,缺少可靠
几何锚点时跳过。它不宣称复刻 OSM 中逐节点标注的真实信号设施。
- 灯杆、横杆、灯壳、熄灭灯珠和倒计时牌外壳必须成为 `05_Props` 中的静态场景几何,随
主 GLB 导出Cesium 只保留与这些几何严格对齐的发光灯珠、七段倒计时数字、显示开关和
车辆相位等待。
- 信号锚点必须在 `intermediates` 阶段写入 `osm2streets_web_out/traffic_signals.json`,由
Blender 与 preview 共用;不得纳入 `SCENE_LAYERS`、GeoPackage 或 QGIS 工程。
## Acceptance Criteria
- [ ] 重建目标区域的 preview 后Cesium 中可见至少一组位于路口进口侧的信号灯,位置与停止线/
- [ ] 仅运行 `intermediates,blender,cesium` 后,主 GLB 已包含位于路口进口侧的灯杆、横杆、
灯头、熄灭灯珠和倒计时牌外壳;静态几何位置与停止线/
斑马线关系清楚,且不会漂浮在道路中央或遮挡车道箭头。
- [ ] 灯组以可见状态呈现红、黄、绿的相位切换;未点亮灯珠明显较暗。
- [ ] 页面提供独立的 Signals 显示开关,关闭后不影响场景、路线与车辆。
- [ ] 匹配到信号停止线的巡航车辆会在红/黄灯时停在线前,绿灯后连续通过;没有可靠匹配的
路线保持原有循环巡航,不因信号锚点缺失而卡住。
- [ ] 不修改 QGIS 工程、道路/标线生成或既有路线 JSON 的基本契约。
- [ ] preview 重建后,动态灯珠和数字与 GLB 中相应灯头、倒计时外壳对齐,且无 Cesium
重复的杆、横杆、灯壳或外壳实体。
## Notes