4、坐标系与空间变换:世界坐标系、相机坐标系、屏幕坐标系及投影矩阵

好,我们进入第四章。坐标系与空间变换。

说实话,这是AR渲染里最绕、也最容易出bug的地方。我当年刚入行时,就因为搞混了坐标系,在项目里硬生生多熬了两个通宵。你想想看,一个虚拟的箭头,明明应该指向东南方,结果转了个圈指向了地底下——用户不骂你才怪。

这一章,我会把世界坐标系、相机坐标系、屏幕坐标系,以及它们之间的投影矩阵,掰开揉碎了讲清楚。

4.1 世界坐标系:一切物体的“绝对位置”

世界坐标系,说白了就是整个AR场景的“宇宙原点”。

所有虚拟物体,包括你的HUD界面、导航箭头、虚拟路标,都得有一个相对于这个原点的位置。比如,我把一个虚拟咖啡杯放在(2, 0, 3)这个位置,意思就是它距离原点往X轴方向2米,Z轴方向3米,Y轴高度为0(地面)。

我个人习惯把世界坐标系的原点放在用户启动AR应用时的位置。这样后续计算会方便很多。

关键点:世界坐标系是固定的,不随相机移动而改变。它是所有变换的“锚点”。

4.2 相机坐标系:以“眼睛”为原点

相机坐标系,就是以你的眼睛(或者手机摄像头)为原点建立的坐标系。

为什么需要它?因为渲染时,GPU只关心“从相机视角看出去,物体在什么位置”。世界坐标系里的(2, 0, 3),如果相机往后走了5米,那在相机坐标系里,这个物体的Z坐标就变成了-2(在相机后方)。

嗯,这里要注意:相机坐标系通常采用右手系。X轴朝右,Y轴朝上,Z轴朝后(即相机看向-Z方向)。

我的经验:我曾经在项目里把相机坐标系的Z轴方向搞反了,结果所有虚拟物体都跑到相机后面去了。排查了整整一下午,最后发现是视图矩阵的构造顺序错了。记住:视图矩阵 = 相机旋转矩阵的逆 × 相机平移矩阵的逆。

4.3 屏幕坐标系:最终显示的地方

屏幕坐标系,就是像素坐标。范围通常是(0, 0)到(屏幕宽度, 屏幕高度)。

从相机坐标系到屏幕坐标系,中间要经过两个步骤:

  1. 投影变换:把3D坐标映射到归一化设备坐标(NDC),范围是[-1, 1]。
  2. 视口变换:把NDC映射到实际的像素坐标。

你想想看,一个在相机前方10米的物体,投影到屏幕上可能只有几个像素大小。这就是投影矩阵干的事。

4.4 投影矩阵:从3D到2D的“魔法”

投影矩阵分两种:透视投影和正交投影。

AR HUD场景里,我们几乎只用透视投影。因为它能模拟人眼的“近大远小”效果。

透视投影矩阵的构造,需要四个参数:

  • FOV(视场角):决定了你能看到多宽的范围。我一般用45°到60°。
  • Aspect Ratio(宽高比):屏幕宽度除以高度。
  • Near Plane(近裁剪面):相机能看到的最近距离。通常设为0.1米。
  • Far Plane(远裁剪面):相机能看到的最远距离。AR场景我设为100米。

避坑指南:我曾经把近裁剪面设成了0.01米,结果导致深度精度严重不足,远处的物体闪烁抖动。后来改回0.1米,问题解决。近裁剪面不要设得太小,否则深度缓冲区的精度会被浪费在近处。

下面是一个典型的透视投影矩阵(OpenGL风格):

// 透视投影矩阵
// fov: 视场角(弧度), aspect: 宽高比, near: 近裁剪面, far: 远裁剪面
mat4 perspective(float fov, float aspect, float near, float far) {
    float tanHalfFov = tan(fov / 2.0f);
    mat4 result = mat4(0.0f);
    
    result[0][0] = 1.0f / (aspect * tanHalfFov);
    result[1][1] = 1.0f / tanHalfFov;
    result[2][2] = -(far + near) / (far - near);
    result[2][3] = -1.0f;
    result[3][2] = -(2.0f * far * near) / (far - near);
    
    return result;
}

这段代码里,result[2][3] = -1.0f 是关键。它把齐次坐标的w分量设为了-z,从而实现了“近大远小”的效果。

4.5 完整的变换流水线

一个虚拟物体从世界坐标到屏幕坐标,经历了以下步骤:

步骤 变换 说明
1 模型矩阵 把物体从局部坐标系变换到世界坐标系
2 视图矩阵 把物体从世界坐标系变换到相机坐标系
3 投影矩阵 把物体从相机坐标系变换到裁剪空间(NDC)
4 视口变换 把NDC映射到屏幕像素坐标

用代码表示就是:

// 顶点着色器中的变换
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);

这个顺序不能乱。我见过有人把viewMatrix和modelMatrix写反了,结果物体满屏幕乱飞。

4.6 AR HUD中的特殊处理

在AR HUD场景里,我们还需要考虑一个额外的问题:用户头部运动

HUD信息必须跟随用户的视角移动。比如,车速信息应该始终显示在视野的左下角。这就需要在视图矩阵的基础上,再叠加一个偏移矩阵

我的做法:我会在相机坐标系里定义一个“HUD锚点”,比如(-0.3, -0.2, -2.0)(左下方2米处)。然后所有HUD元素都相对于这个锚点进行布局。这样无论用户怎么转头,HUD始终在视野的固定位置。

另外,HUD元素通常不需要深度测试。因为它们应该始终显示在最前面。所以渲染HUD时,我会关闭深度写入,但保留深度测试(防止被其他物体遮挡)。

小技巧:如果你想让HUD元素有“呼吸感”,可以在投影矩阵里加一个微小的缩放因子。比如每帧乘以1.01再除以1.01,模拟轻微的脉动效果。用户不会注意到具体变化,但会觉得界面更“生动”。

4.7 总结

坐标系与空间变换,是AR渲染的基石。搞懂了世界、相机、屏幕这三个坐标系,以及它们之间的投影矩阵,你就掌握了AR渲染的“交通规则”。

我个人建议,在写代码之前,先在纸上画一遍变换流程。把每个矩阵的作用、输入输出都写清楚。这样能避免90%的坐标系错误。

下一章,我们会讨论光照与材质。到时候你会发现,光照计算其实也是在不同的坐标系之间来回切换。嗯,打好基础很重要。