地面 footprint:四条射线如何形成完整视口多边形
屏幕角点射线给出 r(t)=o+td。对于普通二维瓦片源,本书按零高程教学平面 Y=0 求交,把四个命中点转换为 normalized Mercator,得到 cover 的唯一地面输入。

1. 为什么地面固定为 Y=0
当前手册只讨论 planar / Web Mercator 普通 source。没有 terrain 或显式 elevation envelope 时,瓦片 geometry 的空间支持面就是零高程平面。
不能用完整三维视锥 AABB 代替地面求交。那样会把位于视锥上方、但不与地图地面相交的 tile 纳入 ideal cover,污染成员关系和请求优先级。地基契约 F2 因此要求 flat source 采用零高程交点。
2. 射线与平面求交
射线 y 分量为:
ry(t)=oy+tdy
令 ry(t)=0:
t=−dyoy
判断顺序必须明确:
- 若 ∣dy∣<ε,射线与地面平行,失败;
- 计算 t;
- 若 t 非有限或 t<0,交点不在前向射线上,失败;
- 否则命中点为 h=o+td。
教学实现使用 1e-12 作为方向平行阈值。它只用于识别数值上无法可靠求交的射线,不用于裁剪 cover 或限制地面距离。
3. 从 render-local 回到 normalized Mercator
h=(h_x,0,h_z) 位于当前 render-local 坐标。snapshot origin 记录地图中心的 Mercator 位置,worldSize 把一个完整世界换算成当前 world units:
xm=originx+worldSizehx
ym=originy+worldSizehz
这里 Three.js +Z 对应 Mercator 向南的 +y,所以第二式没有负号。
不要在这里把 xm clamp 到 [0,1]。超出范围代表视口看到了相邻水平世界,后续会把连续 unwrappedX 拆成 canonical x 与 wrap。垂直 Mercator 边界则由投影与 tile 枚举规则处理。
4. 四个点为何按顺时针排列
角点固定为:
text
1 top-left
2 top-right
3 bottom-right
4 bottom-left
在 screen 与 normalized Mercator 的"y 向下/向南"约定中,这个顺序是顺时针。顶视时,z2、512×512 视口、中心 (0°,0°) 得到:
text
1 (0.375, 0.375)
2 (0.625, 0.375)
3 (0.625, 0.625)
4 (0.375, 0.625)
pitch 与 bearing 会把正方形变成拉伸、旋转的凸四边形,但中心投影不会任意交换角点。保留输入顺序有三项价值:
- polygon clipping 不需要重新计算凸包;
- 边
i → i+1始终对应屏幕同一条边; - 相同 snapshot 的序列化输入稳定,可形成可恢复遍历 identity。
5. 为什么成功 footprint 是凸的
屏幕矩形是凸集。所有角点射线从同一个相机中心出发,并与同一平面在相机前方相交;这种中心投影把屏幕矩形映射成平面上的凸四边形,只要整个矩形都处在可求交半空间内。
这项凸性让下一阶段可以使用确定性的 convex polygon clipping:
text
viewport footprint ∩ tile rectangle
若投影退化到少于三个唯一 Mercator 点,就没有有面积的 polygon。resolver 返回 degenerate-ground-footprint,而不是交给 clipping 猜测。
6. all-or-none 完整性
四条射线按顺序计算,但中间数组不是公共结果。只有四个点全部成功并通过退化检查时,才原子发布:
js
Object.freeze({
valid: true,
coverageComplete: true,
points: Object.freeze(points),
reason: null,
});
若第 4 条射线失败,第 1--3 条虽然在局部变量中存在,返回值仍是:
js
{
valid: false,
coverageComplete: false,
points: [],
reason: "viewport-ray-misses-ground",
}
这不是保守程度的偏好,而是 ideal cover 的形成条件:只有 valid=true && coverageComplete=true 才能产生目标 keys。部分 polygon 会漏掉可见区域,违反 F3。
7. 四类 fail-closed reason
| reason | 触发条件 | 对外结果 |
|---|---|---|
invalid-transform-snapshot |
snapshot 缺字段、矩阵长度错误或存在非有限数 | invalid、empty points |
invalid-homogeneous-coordinate |
逆投影得到不可除的 w 或非有限坐标 | invalid、empty points |
viewport-ray-misses-ground |
射线平行、朝天或交点在相机背后 | invalid、empty points |
degenerate-ground-footprint |
四次命中少于三个唯一 Mercator 点 | invalid、empty points |
reason code 描述几何失败,不描述网络、内容或 GPU 状态。相同 snapshot 必须得到相同 reason。
8. GroundFootprint 的不可变结构
成功 point 的结构是:
js
{
ndc: { x, y, name },
renderLocal: { x, y, z },
mercator: { x, y },
distance,
}
顶层对象、points 数组、每个 point 及其三个嵌套对象全部冻结。这样 cover、overlay 和 inspector 可以共享同一结果,却不能相互改写。
同时保存三个空间不是冗余:
ndc说明点来自哪个屏幕角;renderLocal直接用于 Three.js 调试线;mercator用于 tile clipping;distance用于稳定优先级与诊断。
生产算法不应从渲染对象 userData 反推这些事实。
9. 与单点拾取的边界
PlanarTransform.screenPointToGround(x,y) 服务 cursor 拾取:某个点 miss 时返回 null,其他 screen point 仍可单独查询。
resolveGroundFootprint(snapshot) 服务视口完整性:任一角 miss 时整个结果 invalid。
二者可以复用相同数学,但不能共享错误语义。为避免 live camera 读取,getGroundFootprint() 直接委托 snapshot resolver;单点拾取函数保持浏览器交互 API 不变。
10. 本章建立的不变量
- 普通二维 source 只与零高程平面求交;
- 交点参数为 t=−oy/dy,且必须有限、非负;
- Mercator x 不因 world wrap 被 clamp;
- polygon 保持 TL → TR → BR → BL 的稳定顺序;
- 成功结果至少有三个唯一点并且完整冻结;
- 任一失败返回 invalid、incomplete、empty points 与确定性 reason;
- footprint 不读取 request、record、cache、GPU 或相机 settling 状态。