three.js地图视口瓦片(二):如何形成完整视口多边形

地面 footprint:四条射线如何形成完整视口多边形

屏幕角点射线给出 r(t)=o+t dr(t)=o+t\,d 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+t dy r_y(t)=o_y+t\,d_y ry(t)=oy+tdy

ry(t)=0 r_y(t)=0 ry(t)=0:
t=− oydy t=-\frac{o_y}{d_y} t=−dyoy

判断顺序必须明确:

  1. ∣dy∣<ε |d_y|<\varepsilon ∣dy∣<ε,射线与地面平行,失败;
  2. 计算 tt t;
  3. tt t 非有限或 t<0t<0 t<0,交点不在前向射线上,失败;
  4. 否则命中点为 h=o+t dh=o+t\,d 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+ hxworldSize x_m=origin_x+\frac{h_x}{worldSize} xm=originx+worldSizehx
ym=originy+ hzworldSize y_m=origin_y+\frac{h_z}{worldSize} ym=originy+worldSizehz

这里 Three.js +Z 对应 Mercator 向南的 +y,所以第二式没有负号。

不要在这里把 xm x_m 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. 本章建立的不变量

  1. 普通二维 source 只与零高程平面求交;
  2. 交点参数为 t=−oy/dy t=-o_y/d_y t=−oy/dy,且必须有限、非负;
  3. Mercator x 不因 world wrap 被 clamp;
  4. polygon 保持 TL → TR → BR → BL 的稳定顺序;
  5. 成功结果至少有三个唯一点并且完整冻结;
  6. 任一失败返回 invalid、incomplete、empty points 与确定性 reason;
  7. footprint 不读取 request、record、cache、GPU 或相机 settling 状态。

相关推荐
用户4771626979726 分钟前
GeoJSON 在线预览编辑器开源:一个纯前端实现的拆解
gis
mONESY1 小时前
从零到上线:全栈项目部署全流程实战
javascript
honkun61 小时前
vue 表格组件 vxe-table 实实现专业记账凭证编辑表格与自动汇总
前端·javascript·vue.js
蛋先生DX1 小时前
明明都是源码到CPU,各语言中间原理咋不是一个套路?
java·javascript·go
zzzzzz3103 小时前
react-bits:酷炫动效之外,React 组件库还能提供什么
javascript·react.js·动效
web打印社区10 小时前
网页静默打印热敏小票:从 HTML 到出纸的完整指南
前端·javascript·vue.js·electron·pdf·html
专业抄代码选手17 小时前
08|Fiber 上的 `useState`:状态终于属于具体组件
前端·javascript·react.js
默_笙17 小时前
😭 Vibe Coding 翻车实录:AI 编程为什么必须先写"剧本"
前端·javascript