返回知识库

GIS

相机与拾取

相机与拾取 封面
GISengine-webgpu相机拾取

前情回顾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 物体。

有两种主流方案:

  1. 颜色编码 Picking(GPU-based)

    • 额外开一个离屏渲染通道(FBO)
    • 每个物体用唯一颜色渲染(不显示在屏幕上)
    • 点击时读取该像素颜色 → 映射到物体 ID
  2. 射线投射(CPU/GPU-based)

    • 从相机位置穿过点击像素,发射一条射线
    • 计算射线与场景中物体的交点
    • 取最近的交点 → 就是点中的物体

相机飞行:不是直线,是球面上的曲线

从地球一点飞到另一点,如果直线插值:

球心                    球心
  ●                      ●
   \                    /
    \  直线穿越地心    /   ← 问题:会穿过地球内部!
     \              /
      ●─────────●      ← 正确的路径应该沿着地表
    起点       终点

正确的做法是 球面线性插值(Slerp)

  • 把起点和终点看作单位球面上的两个点
  • 沿着大圆弧线插值
  • 保证插值点始终在球面上

原理与数学/机制

1. IEEE 754 浮点精度分析

1.1 float32 的精度极限

float32 的尾数有 23 位,意味着它可以精确表示的最小间隔(Unit in the Last Place, ULP)为:

ULP(x)x×223x×1.19×107ULP(x) \approx |x| \times 2^{-23} \approx |x| \times 1.19 \times 10^{-7}
坐标值范围ULP(最小可分辨间隔)对地形渲染的影响
0 ~ 1 m0.00000012 m可精确到微米级
1 ~ 1000 m0.00012 m可精确到 0.1 mm
1,000 ~ 10,000 m0.0012 m可精确到 1 mm
10,000 ~ 100,000 m0.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 核心公式

设:

  • Pworld\mathbf{P}_{world} = 物体在世界坐标系中的位置(float64)
  • Ccamera\mathbf{C}_{camera} = 相机在世界坐标系中的位置(float64)
  • Vview\mathbf{V}_{view} = 物体在视图坐标系中的位置(float32,传给 GPU)

RTE 公式:

Vview=PworldCcamera\mathbf{V}_{view} = \mathbf{P}_{world} - \mathbf{C}_{camera}

然后 GPU 的顶点着色器只需要:gl_Position=PMviewVview\mathbf{gl\_Position} = \mathbf{P} \cdot \mathbf{M}_{view} \cdot \mathbf{V}_{view}

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θ\theta垂直视野角度60°
Aspectaa宽高比16:9 = 1.78
Nearnn近裁剪面距离0.1 m
Farff远裁剪面距离1,000,000 m

3.2 投影矩阵(Perspective Projection Matrix)

P=(1ntan(θ/2)0000antan(θ/2)0000f+nfn2fnfn0010)\mathbf{P} = \begin{pmatrix} \dfrac{1}{n \tan(\theta/2)} & 0 & 0 & 0 \\[8pt] 0 & \dfrac{a}{n \tan(\theta/2)} & 0 & 0 \\[8pt] 0 & 0 & \dfrac{f + n}{f - n} & \dfrac{2fn}{f - n} \\[8pt] 0 & 0 & -1 & 0 \end{pmatrix}

NOTE

为什么 Picking 需要理解投影矩阵? 因为屏幕坐标 (x, y) 需要反投影回世界坐标 → 射线方向。这就要用到投影矩阵的逆矩阵。

4. Picking 技术详解

4.1 颜色编码 Picking(GPU Picking)

原理

  1. 创建一个与屏幕等大的 FBO(Frame Buffer Object)
  2. 给每个可点击物体分配一个唯一颜色(ID 编码)
  3. 额外渲染一帧(不显示到屏幕上),用 ID 颜色填充
  4. 用户点击时,读取该像素的颜色 → 解码为物体 ID
可见画面                    Picking 画面(FBO)
┌─────────────┐            ┌─────────────┐
│             │            │  RGB(1,0,0) │  ← 物体 A
│   ┌───┐     │            │             │
│   │ A │     │            │  RGB(0,1,0) │  ← 物体 B
│   └───┘     │            │             │
│      ┌──┐   │            │      ┌──┐   │
│      │ B │   │            │      │ B │   │
│      └──┘   │            │      └──┘   │
└─────────────┘            └─────────────┘
       ↑                          ↑
  用户看到                      系统存储

颜色编码 vs ID

  • 24-bit 颜色 = 16,777,21616{,}777{,}216 种唯一值
  • 可以编码 1600 万个不同物体
  • 解码:ID=R16    G8    BID = R \ll 16 \;|\; G \ll 8 \;|\; B

4.2 射线投射(Ray Casting)

原理

  1. 将屏幕坐标 (x, y) 归一化到 [-1, 1]
  2. 构建从近裁剪面到远裁剪面的射线
  3. 将射线转换到世界坐标系
  4. 计算射线与场景中所有物体的交点
  5. 取 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)

最简单的方式:

P(t)=(1t)Pstart+tPend,t[0,1]\mathbf{P}(t) = (1 - t)\mathbf{P}_{start} + t\mathbf{P}_{end}, \quad t \in [0, 1]

问题:在球面上直接 Lerp 会穿过地球内部。

5.2 球面线性插值(Slerp)

P(t)=sin((1t)θ)sinθPstart+sin(tθ)sinθPend\mathbf{P}(t) = \frac{\sin\bigl((1 - t)\theta\bigr)}{\sin\theta} \mathbf{P}_{start} + \frac{\sin(t\theta)}{\sin\theta} \mathbf{P}_{end}

其中 θ\thetaPstart\mathbf{P}_{start}Pend\mathbf{P}_{end} 之间的夹角:

θ=arccos(PstartPendPstartPend)\theta = \arccos\left(\frac{\mathbf{P}_{start} \cdot \mathbf{P}_{end}}{|\mathbf{P}_{start}| \cdot |\mathbf{P}_{end}|}\right)

特点

  • 插值点始终在单位球面上
  • 路径是大圆弧线(最短路径)
  • 匀速 rotation,但非匀速 translation

5.3 缓动函数

缓动类型公式视觉效果
Lineartt匀速
Ease-int2t^2慢→快
Ease-out1(1t)21 - (1 - t)^2快→慢
Ease-in-outt<0.5  ?  2t2  :  12(1t)2t < 0.5 \;?\; 2t^2 \;:\; 1 - 2(1 - t)^2慢→快→慢

可视化对比与动手实验

实验 1:float32 精度可视化

假设相机放在地球表面某点,观察近处 1 m 精度的地面细节:

场景坐标值float32 ULP能否表示 1m 细节?
地球表面6,371,000 m0.76 m❌ 不能
RTE 偏移0 ~ 100 m0.00001 m✅ 可以
缩小到城市10,000 m0.001 m✅ 可以
太空俯视10,000,000 m1.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) 或先投影到平面再插值。没有”唯一正确”的方案。

延伸阅读与自测

延伸阅读

  1. IEEE 754 标准https://en.wikipedia.org/wiki/IEEE_754
  2. RTE 高精度渲染https://help.agi.com/AGIComponents/html/Blog_RelativeToEyeRendering.htm
  3. WebGPU 与高精度坐标https://gpuweb.github.io/gpuweb/
  4. 3D Picking 技术比较https://www.songho.ca/opengl/gl_picking.html
  5. 球面插值(Slerp)https://en.wikipedia.org/wiki/Slerp
  6. 透视投影矩阵推导https://www.songho.ca/opengl/gl_projectionmatrix.html

自测题

  1. 计算题:float32 能精确表示的最大整数是多少?(提示:23 位尾数 + 1 位隐含 = 24 位有效位)

  2. 概念辨析:为什么 RTE 能解决精度问题?从数学角度解释 PworldCcamera\mathbf{P}_{world} - \mathbf{C}_{camera} 为什么比 Pworld\mathbf{P}_{world} 更适合 float32。

  3. 方案对比:在一个包含 100 万个三角面的复杂场景中,应该使用颜色编码 Picking 还是射线投射?为什么?

  4. 推导题:推导透视投影矩阵中 aspect 参数的作用。如果窗口从 16:9 变为 9:16(竖屏),投影矩阵应该如何调整?

  5. 设计题:设计一个相机飞行动画系统,要求:

    • 从地面一点飞到另一点(球面路径)
    • 飞行过程中保持 1000 米高度
    • 总飞行时间 3 秒
    • 起止点都有缓动(ease-in-out)

    你会如何设计插值函数?写出伪代码。