feat: add cesium traffic signal countdowns
This commit is contained in:
@@ -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 个数字 mesh(0-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 共享 |
|
||||
|
||||
## 第三方资产导入的源文件边界
|
||||
|
||||
|
||||
@@ -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`,不能依赖其他测试的导入顺序。
|
||||
|
||||
---
|
||||
|
||||
## 反模式
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. 范围与触发条件
|
||||
|
||||
@@ -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 坐标轴。
|
||||
|
||||
Reference in New Issue
Block a user