GIS
相机与拾取
前情回顾:02 坐标与投影 讲清了 WGS84 坐标系和 ECEF 变换,03 四叉树与 LOD 讲清了瓦片调度和视锥剔除。当用户与 3D 地图交互时,最直观的两个问题是:① 鼠标点击地图上的建筑,怎么知道点的是哪个?② 地球那么大,相机飞到近处怎么保证不抖动?本篇回答:Picking 原理和 RTE 高精度相机。
直觉问题
打开 Google Earth 或 Cesium,三个最直观的交互感受:
- 点击地图上的地标,信息弹窗立刻出现——这背后是什么技术?怎么从”屏幕上的像素”找到”地球上的物体”?
- 从太空缩放到地面,近处地面不抖动、不闪烁——但 float32 精度只有 7 位有效数字,地球半径 6371 km 已经占用了 6 位,剩下的小数部分只有 1 位精度?那怎么保证近处地面精度?
- 相机飞行动画很顺滑——从 A 点飞到 B 点,不是简单的直线,而是沿着球面的曲线。这个”飞行曲线”是怎么算的?
读完本篇,你能回答:“RTE 是什么?为什么能解决 float32 精度问题?”、“Picking 的两种主流方案是什么?各有什么优劣?”、“球面插值和线性插值有什么本质区别?“
核心概念白话讲
浮点精度:float32 的”大数吃小数”
IEEE 754 float32 只有 23 位尾数 + 8 位指数 + 1 位符号,总共 32 位。它的有效数字大约是 7 位十进制数:
float32 的存储结构
┌─────────┬───────────────────┬──────────────────────────────┐
│ sign │ exponent │ mantissa │
│ 1 bit │ 8 bits │ 23 bits │
└─────────┴───────────────────┴──────────────────────────────┘
有效位数:约 7 位十进制数
这意味着:
1000000.0 + 0.1=1000000.0(0.1 被”吃掉”了)6371000.0 + 0.0001=6371000.0(小数部分完全丢失)
RTE:把”绝对坐标”变成”相对坐标”
RTE(Relative To Eye) 的核心思想是:不直接传世界坐标给 GPU,而是传相对于相机位置的偏移量。
传统方案:
世界坐标 (1000000.5, 2000000.3, 3000000.1)
→ GPU 用 float32 计算 → 精度丢失
RTE 方案:
相机位置 (1000000.0, 2000000.0, 3000000.0)
相对偏移 (0.5, 0.3, 0.1)
→ CPU 用 float64 计算相机位置 → GPU 用 float32 处理偏移
→ 偏移量很小,float32 精度完全够用
Picking:从”像素”到”物体”
Picking 就是”点击检测”——用户点击屏幕上的一个像素,系统要知道点到了哪个 3D 物体。
有两种主流方案:
-
颜色编码 Picking(GPU-based):
- 额外开一个离屏渲染通道(FBO)
- 每个物体用唯一颜色渲染(不显示在屏幕上)
- 点击时读取该像素颜色 → 映射到物体 ID
-
射线投射(CPU/GPU-based):
- 从相机位置穿过点击像素,发射一条射线
- 计算射线与场景中物体的交点
- 取最近的交点 → 就是点中的物体
相机飞行:不是直线,是球面上的曲线
从地球一点飞到另一点,如果直线插值:
球心 球心
● ●
\ /
\ 直线穿越地心 / ← 问题:会穿过地球内部!
\ /
●─────────● ← 正确的路径应该沿着地表
起点 终点
正确的做法是 球面线性插值(Slerp):
- 把起点和终点看作单位球面上的两个点
- 沿着大圆弧线插值
- 保证插值点始终在球面上
原理与数学/机制
1. IEEE 754 浮点精度分析
1.1 float32 的精度极限
float32 的尾数有 23 位,意味着它可以精确表示的最小间隔(Unit in the Last Place, ULP)为:
| 坐标值范围 | ULP(最小可分辨间隔) | 对地形渲染的影响 |
|---|---|---|
| 0 ~ 1 m | 0.00000012 m | 可精确到微米级 |
| 1 ~ 1000 m | 0.00012 m | 可精确到 0.1 mm |
| 1,000 ~ 10,000 m | 0.0012 m | 可精确到 1 mm |
| 10,000 ~ 100,000 m | 0.012 m | 可精确到 1 cm |
| 1,000,000 m(地球尺度) | 0.12 m | 可精确到 10 cm |
| 6,371,000 m(地球半径) | 0.76 m | 近 1 米精度丢失 |
WARNING
关键结论:当坐标值达到地球半径量级(6371 km)时,float32 的最小可分辨间隔接近 0.76 米。这意味着小于 0.76 米的微小位移无法表示——地面上的细节会抖动或消失。
1.2 “大数吃小数”的数学原理
浮点加法需要先对齐指数:
1.0 × 10^6 = 1,000,000 (6 位有效数字)
+ 1.0 × 10^0 = 1 (1 位有效数字)
─────────────────────────────────────────
1.000001 × 10^6 (要求 7 位有效数字,但 float32 只有 7 位)
实际结果:1.000000 × 10^6 (1 被"吃掉"了)
2. RTE(Relative To Eye)高精度渲染
2.1 RTE 核心公式
设:
- = 物体在世界坐标系中的位置(float64)
- = 相机在世界坐标系中的位置(float64)
- = 物体在视图坐标系中的位置(float32,传给 GPU)
RTE 公式:
然后 GPU 的顶点着色器只需要:
2.2 RTE 管线架构
CPU 端(双精度 float64) GPU 端(单精度 float32)
┌─────────────────────────────────┐ ┌──────────────────────────┐
│ │ │ │
│ P_world (float64) │ │ V_view = │
│ ↓ │ │ P_world - C_camera │
│ C_camera (float64) │──►│ (float32) │
│ ↓ │ │ ↓ │
│ V_view = P_world - C_camera │ │ gl_Position = │
│ (float64) │ │ projection * V_view │
│ ↓ │ │ │
│ 传 float32 到 GPU │ │ │
└─────────────────────────────────┘ └──────────────────────────┘
IMPORTANT
RTE 的关键洞见:精度问题不是”GPU float32 不够用”,而是”传递大数给 GPU”。RTE 把大数(相机位置)留在 CPU 用 float64 处理,只传小数(相对偏移)给 GPU,float32 完全够用。
3. 视锥体(Frustum)数学
3.1 透视投影的参数
透视投影由 4 个参数定义:
| 参数 | 接待游客 | 含义 | 典型值 |
|---|---|---|---|
| FOV | 垂直视野角度 | 60° | |
| Aspect | 宽高比 | 16:9 = 1.78 | |
| Near | 近裁剪面距离 | 0.1 m | |
| Far | 远裁剪面距离 | 1,000,000 m |
3.2 投影矩阵(Perspective Projection Matrix)
NOTE
为什么 Picking 需要理解投影矩阵? 因为屏幕坐标 (x, y) 需要反投影回世界坐标 → 射线方向。这就要用到投影矩阵的逆矩阵。
4. Picking 技术详解
4.1 颜色编码 Picking(GPU Picking)
原理:
- 创建一个与屏幕等大的 FBO(Frame Buffer Object)
- 给每个可点击物体分配一个唯一颜色(ID 编码)
- 额外渲染一帧(不显示到屏幕上),用 ID 颜色填充
- 用户点击时,读取该像素的颜色 → 解码为物体 ID
可见画面 Picking 画面(FBO)
┌─────────────┐ ┌─────────────┐
│ │ │ RGB(1,0,0) │ ← 物体 A
│ ┌───┐ │ │ │
│ │ A │ │ │ RGB(0,1,0) │ ← 物体 B
│ └───┘ │ │ │
│ ┌──┐ │ │ ┌──┐ │
│ │ B │ │ │ │ B │ │
│ └──┘ │ │ └──┘ │
└─────────────┘ └─────────────┘
↑ ↑
用户看到 系统存储
颜色编码 vs ID:
- 24-bit 颜色 = 种唯一值
- 可以编码 1600 万个不同物体
- 解码:
4.2 射线投射(Ray Casting)
原理:
- 将屏幕坐标 (x, y) 归一化到 [-1, 1]
- 构建从近裁剪面到远裁剪面的射线
- 将射线转换到世界坐标系
- 计算射线与场景中所有物体的交点
- 取 t 值最小的交点 → 最近的物体
屏幕坐标 (x, y) NDC 坐标 (x', y') 世界坐标射线
│ │ │
├── 归一化 ──► ├── 逆投影 ──► ├── 逆视图 ──►
│ x' = 2x/W - 1 │ 近裁剪面上的点 │ 射线起点 + 方向
│ y' = 2y/H - 1 │ (x', y', -1, 1) │
│ │ │
└─────────────────────────────┴─────────────────────────────┘
两种方案对比:
| 特性 | 颜色编码 Picking | 射线投射 |
|---|---|---|
| 精度 | 像素级 | 取决于射线-物体求交算法 |
| 性能 | 需要额外渲染一帧 | CPU 端计算,无额外 GPU 开销 |
| 支持透明物体 | ❌ 需要特殊处理 | ✅ 可以处理 |
| 支持复杂形状 | ✅ 完美支持 | 需要实现物体-射线求交 |
| 多选支持 | 需要多渲染 | 天然支持(多条射线) |
| 适用场景 | 简单场景、大量物体 | 复杂几何、CPU 端已有数据 |
5. 相机飞行动画曲线
5.1 线性插值(Lerp)
最简单的方式:
问题:在球面上直接 Lerp 会穿过地球内部。
5.2 球面线性插值(Slerp)
其中 是 和 之间的夹角:
特点:
- 插值点始终在单位球面上
- 路径是大圆弧线(最短路径)
- 匀速 rotation,但非匀速 translation
5.3 缓动函数
| 缓动类型 | 公式 | 视觉效果 |
|---|---|---|
| Linear | 匀速 | |
| Ease-in | 慢→快 | |
| Ease-out | 快→慢 | |
| Ease-in-out | 慢→快→慢 |
可视化对比与动手实验
实验 1:float32 精度可视化
假设相机放在地球表面某点,观察近处 1 m 精度的地面细节:
| 场景 | 坐标值 | float32 ULP | 能否表示 1m 细节? |
|---|---|---|---|
| 地球表面 | 6,371,000 m | 0.76 m | ❌ 不能 |
| RTE 偏移 | 0 ~ 100 m | 0.00001 m | ✅ 可以 |
| 缩小到城市 | 10,000 m | 0.001 m | ✅ 可以 |
| 太空俯视 | 10,000,000 m | 1.19 m | ❌ 不能 |
实验 2:Picking 方案对比
在一个包含 10,000 个可见物体的场景中:
| 方案 | 额外渲染开销 | CPU 开销 | 点击延迟 | 适用场景 |
|---|---|---|---|---|
| 颜色编码 | 1 帧 | 极低 | 1ms | 简单物体、大量物体 |
| 射线投射 | 0 | 中等(遍历物体) | 5-20ms | 复杂几何、少而精 |
实验 3:插值曲线对比
从 (0°, 0°) 飞到 (30°, 30°):
地球仪视角(俯视图)
北极
●
│
│ ┌─── 直线 Lerp(穿地)
│ / ┌─── Slerp(大圆弧)
│ / /
│/ /
●────●
起点 终点
| 插值方式 | 路径长度 | 是否穿地 | 视觉自然度 |
|---|---|---|---|
| Lerp | 短 | ✅ 穿地 | ❌ 不自然 |
| Slerp | 较长 | ❌ 贴地 | ✅ 自然 |
| Slerp + 高度 | 中 | ❌ 贴地 | ✅ 最自然 |
常见误区
WARNING
误区 1:RTE 解决了所有精度问题 RTE 只解决了相机近处的精度问题。当物体离相机很远(如太空中的卫星)时,相对偏移仍然很大,float32 精度仍然不足。解决方案:层级剔除(LOD),远处物体用低精度渲染。
WARNING
误区 2:Picking 颜色编码只能编码 256 个物体 24-bit 颜色可以编码 16,777,216 个物体,足够绝大多数场景。如果需要更多,可以分通道编码(如 R=高 8 位,G=中 8 位,B=低 8 位)。
WARNING
误区 3:射线投射比颜色编码 Picking 慢 射线投射是 CPU 计算,颜色编码是 GPU 渲染。对于简单几何体(如球体、平面),CPU 计算很快;但对于复杂网格(百万三角面),GPU 渲染更快。不能一概而论。
WARNING
误区 4:Slerp 是唯一正确的插值方式 Slerp 适合球面路径,但如果相机飞行需要固定高度(如 1000 米),应该用 Squad(Spherical Quadrangle) 或先投影到平面再插值。没有”唯一正确”的方案。
延伸阅读与自测
延伸阅读
- IEEE 754 标准:https://en.wikipedia.org/wiki/IEEE_754
- RTE 高精度渲染:https://help.agi.com/AGIComponents/html/Blog_RelativeToEyeRendering.htm
- WebGPU 与高精度坐标:https://gpuweb.github.io/gpuweb/
- 3D Picking 技术比较:https://www.songho.ca/opengl/gl_picking.html
- 球面插值(Slerp):https://en.wikipedia.org/wiki/Slerp
- 透视投影矩阵推导:https://www.songho.ca/opengl/gl_projectionmatrix.html
自测题
-
计算题:float32 能精确表示的最大整数是多少?(提示:23 位尾数 + 1 位隐含 = 24 位有效位)
-
概念辨析:为什么 RTE 能解决精度问题?从数学角度解释 为什么比 更适合 float32。
-
方案对比:在一个包含 100 万个三角面的复杂场景中,应该使用颜色编码 Picking 还是射线投射?为什么?
-
推导题:推导透视投影矩阵中
aspect参数的作用。如果窗口从 16:9 变为 9:16(竖屏),投影矩阵应该如何调整? -
设计题:设计一个相机飞行动画系统,要求:
- 从地面一点飞到另一点(球面路径)
- 飞行过程中保持 1000 米高度
- 总飞行时间 3 秒
- 起止点都有缓动(ease-in-out)
你会如何设计插值函数?写出伪代码。