日期:2026-07-05
场景:许县·城门前(xu_gate)
方法:直尺实测 + 路的收敛速率 + 树的高度差
结论:2D 照片的每个 Y 坐标可映射到真实深度值
---
目标是从一张 9:16 的参考照片出发,程序化生成不依赖照片的场景渲染。
早期尝试用照片按行/列采样色值,再双线性插值还原——发现二维离散采样 + 透视投影存在根本矛盾:源数据中"相邻"的两个采样点,在照片里可能相距 10 像素也可能 100 像素,插值假设"平滑过渡"但实际可能是"突然出现一块石头"或"很远什么都没有"。
结论:照片采样只能找规则,不能直接当贴图用。 需要从照片里提取的是结构参数,不是像素。
---
| 维度 | 范围 | 方向 | 单位 |
|:---|:---|:---|:---|
| X | 0 → 9 | 左 → 右 | cm |
| Y | 0 → 16 | 底 → 顶 | cm |
照片实测 9:16,坐标系直接对应物理尺寸。
---
观测数据(直尺测量):
| 位置 | Y | 左边线 X | 右边线 X | 路宽 |
|:---|:---|:---|:---|:---|
| 近端 | 0 | 0 | 7 | 7.0 cm |
| 远端 | 8.3 | 3 | 3.3 | 0.3 cm |
推导:
路的两条边是直线,在投影平面上线性收敛:
左边线:x = 0 + (3 - 0) × t = 3t 右边线:x = 7 + (3.3 - 7) × t = 7 - 3.7t 其中 t = (Y - 0) / (8.3 - 0) = Y / 8.3
灭点 = 两线交点:
3t = 7 - 3.7t 6.7t = 7 t = 1.0448 vpX = 3 × 1.0448 = 3.13 vpY = 8.3 × 1.0448 = 8.67
灭点:(3.13, 8.67)
横向缩放函数:
width(Y) = 7 + (-3.7 - 3) × (Y / 8.3) = 7 - 6.7 × (Y / 8.3) scaleX(Y) = width(Y) / width(0) = 1 - 0.1154 × Y
含义:Y 每增加 1 个单位,横向缩放衰减 11.54%。
透视比:7 / 0.3 ≈ 23.3 倍(近端物体比远端大 23.3 倍)
---
关键修正:透视缩放未必等比。
横向缩放由路的收敛标定,但纵向缩放受以下因素影响,与横向不一定一致:
结论:不能用 scaleX(Y) 直接套高度,纵向需要独立标定。
标定方法: 用已知物理高度相同(或已知比例)的物体,在不同 Y 位置的视觉高度差反推。
visualHeight(Y) = physicalHeight × scaleY(Y) 两组参照物物理高度相同 H 时: scaleY(Y1) / scaleY(Y2) = V1 / V2 线性模型:scaleY(Y) = 1 + vSlope × (Y - Y_base)
参照物数据(已填入 lab-maps.json,physicalHeight 待补全):
| 参照物 | Y | 视觉高度 | 物理高度 |
|:---|:---|:---|:---|
| 山前树 | 8.75 | 0.5 | 待测量 |
| 中右树 | 9.1 | 2.3 | 待测量 |
---
推导链条:
横向缩放 scaleX(Y) ← 路的收敛,已标定
纵向缩放 scaleY(Y) ← 树的高度差,框架就位
↓
面积缩放 scaleArea(Y) = scaleX(Y) × scaleY(Y)
↓
透视投影中:视觉面积 ∝ 1 / 深度²
↓
深度(Y) ∝ 1 / √scaleArea(Y)
面积梯度不是"类似"景深变化——它就是景深变化的数学表达本身。
人眼看到"东西变小了",大脑判断"它变远了"。这个"变小"的具体量,就是面积梯度。
一旦纵向标定完成,这张 2D 照片里的每个 Y 坐标都可以映射到一个真实的深度值。这是摄影测量(photogrammetry)和运动恢复结构(structure from motion)的核心原理。
---
scaleX(Y) = 1 - 0.1154 × Y width(Y) = 7 × scaleX(Y)
scaleY(Y) = 1 + vSlope × (Y - Y_base) 其中 vSlope 和 Y_base 由 calibrateVertical(refs) 计算
scaleArea(Y) = scaleX(Y) × scaleY(Y)
depth(Y) ∝ 1 / √scaleArea(Y) 即:depth(Y) = K / √(scaleX(Y) × scaleY(Y)) 其中 K 是比例常数,由标定基准决定
vp = (3.13, 8.67)
---
透视标定模块,提供以下 API:
| 方法 | 说明 | 状态 |
|:---|:---|:---|
| `Perspective.calibrate(nearEnd, farEnd)` | 从路参数标定横向透视 | 完成 |
| `model.scaleAt(Y)` | 横向缩放比 | 完成 |
| `model.widthAt(Y)` | 该 Y 位置的路宽 | 完成 |
| `model.leftEdgeAt(Y)` | 路左边线 X 坐标 | 完成 |
| `model.rightEdgeAt(Y)` | 路右边线 X 坐标 | 完成 |
| `model.calibrateVertical(refs)` | 用参照物标定纵向缩放 | 完成(待数据) |
| `model.scaleYAt(Y)` | 纵向缩放比 | 完成(待数据) |
| `model.vanishingPoint` | 灭点坐标 | 完成 |
| `model.drawGrid(ctx, ...)` | 透视网格可视化 | 完成 |
地图绘制参数,`xu_gate` 地图包含:
画布测试台,"透视网格"按钮可可视化透视模型。
---
| 层 | 类型 | Y范围 | X范围 | 渲染方法 |
|:---|:---|:---|:---|:---|
| 1 | 天空 | 10 → 16 | 0 → 9 | 行锚点渐变 |
| 2 | 地平线过渡 | 9.8 → 10.2 | 0 → 9 | 混合 |
| 3 | 山体 | 9.3 → 10 | 0 → 9 | 多边形(双峰,硬边) |
| 4 | 草地 | 0 → 8.5 | 0 → 9 | 填充(95%黄绿 + 5%深绿) |
| 5 | 道路 | 0 → 8.3 | 0→7(近) → 3-3.3(远) | 梯形 |
| 6 | 树 | — | — | 剪影(两组) |
树的数据:
| 组 | 树根Y | 树梢Y | 位置 | 景深 |
|:---|:---|:---|:---|:---|
| 山前树 | 8.5 | 9 | 山脚前,路尽头 | 中 |
| 中右树 | 8-8.2 | 10-10.5 | 中右区 | 中远 |
---
结构对了,颜色是风格问题,不是精度问题。
---
1. 照片是"找规则"的参考,不是贴图来源 — 从照片提取参数(坐标、收敛率、高度差),然后用参数驱动程序化生成
2. 数据与算法分离 — `lab-maps.json` 承载绘制参数(位置/颜色/方法),JS 只承载算法
3. 结构先于细节 — 透视标定 → 区域填色 → 附属物 → 纹理,先有骨架再长皮肉
4. 相信数学 — 算法是数学,对了就是对了,不需要反复测试验证
---
---
单视角透视标定是验证手段,终态架构是球面投影:
球心 = 观测者(永远在中心) 球面内壁 = 投影屏(所有内容画在这里) 显示器 = 球面的一个窗口(只显示朝向相机的那一片) 分层: 近景投影区 — 高细节、scale 大 中景投影区 — 中细节 远景投影区 — 低细节、接近地平线 预加载层 — 视野之外的球面,提前生成
与天空盒(skybox)的区别:球面不是预渲染贴图,是规则参数驱动程序化生成。
已有参数与球面的对应关系:
| 参数 | 球面含义 |
|:---|:---|
| scaleX(Y) | 经度方向缩放 |
| scaleY(Y) | 纬度方向缩放 |
| 灭点 | 当前视角朝向的锚点 |
| 地平线高度 | 球面赤道偏移(相机俯仰) |
| 视频旋转采样 | 球面全周扫描,逐块标定 |
---
时间 = 球面相对于光源的旋转角度。光源固定在最远端。
0° 正对光源 → 正午 90° 侧面光源 → 黎明/黄昏 180° 背对光源 → 夜晚 270° 侧面光源 → 黄昏/黎明
三个基础量定义了场景渲染的全部输入:
| 基础量 | 参数 | 状态 |
|:---|:---|:---|
| 空间 | 球面投影 + scaleX/Y + 分层 | 框架就位 |
| 时间 | 球面旋转角度 | 概念明确 |
| 光 | 远端点源 + 旋转角度 | 概念明确 |
---
| 阶段 | 内容 | 状态 |
|:---|:---|:---|
| 1 | 文字类无画面纯交互 | 完成,可并行多条流程线 |
| 2 | 单向视角模拟出可视平面结构 | 透视标定完成,渲染器待接入 |
| 3 | 球面投影全局视角模拟 | 架构明确,待实现 |
显示器实际模拟的是物体在视网膜上的影子,数字内容只是在显示器可视化规则下的内容加工。地图场景无需大量素材,只需放置好初始的规则素材。
---
*从一把直尺、一条路的收敛、两组树的高度差开始,推导出三维空间的深度信息、球面投影架构和时间光源模型。*
—— 人与AI协作记录 ——