三维渲染数学基础:从相机坐标系到投影变换

一、视图矩阵(将相机放置到原点)

在计算机图形学中,相机不仅有位置,还有自己的朝向。为了从相机的视角观察三维场景,我们需要将世界坐标系中的点转换到相机坐标系中。

这个过程叫作视图变换(View Transformation)。

视图变换可以分为两个部分:

  1. 旋转变换:将世界坐标系转换到相机坐标系的方向。
  2. 平移变换:将相机的位置移动到坐标原点。

在之前的文章里面,对于某一个点的旋转是不方便的,所以我们一般会将该点移动到原点,在进行旋转。

一、相机的四个基本符号

首先固定以下符号,避免混淆。

  • e:相机在世界坐标系中的位置。
  • g:相机的视线方向,从相机指向观察目标。
  • t:相机的上方向参考向量。
  • r:相机的右方向向量。

其中,e 决定相机的位置,而 g、t、r 用来确定相机的朝向。

假设相机位于:

复制代码
e = (0, 0, 5)

相机观察世界原点:

复制代码
target = (0, 0, 0)

那么,视线方向为:

复制代码
g = target - e
  = (0, 0, -5)

对向量进行归一化,只保留方向:

复制代码
g = normalize(target - e)
  = (0, 0, -1)

接下来,给出一个参考上方向:

复制代码
t = (0, 1, 0)

这个向量表示相机的上方大致朝向世界坐标系的 Y 轴正方向,这个 t 也就相当于固定了照相机的角度。

注意:初始的 t 不一定与 g 垂直,因此后面还需要修正。

二、通过叉乘计算相机的右方向

已知视线方向 g 和上方向参考向量 t,可以通过叉乘计算相机的右方向:

复制代码
r = g × t

叉乘顺序非常重要:

复制代码
g × t = -(t × g)

如果将顺序颠倒,结果方向就会相反。

设:

复制代码
g = (gx, gy, gz)
t = (tx, ty, tz)

那么叉乘公式为:

复制代码
r = (
    gy * tz - gz * ty,
    gz * tx - gx * tz,
    gx * ty - gy * tx
)

代入前面的例子:

复制代码
g = (0, 0, -1)
t = (0, 1, 0)

计算得到:

复制代码
r = g × t
  = (1, 0, 0)

因此,相机的右方向就是世界坐标系的 X 轴正方向。

接下来,重新计算上方向:

复制代码
t = r × g

这样可以让三个方向互相垂直。

实际计算时,应保证三个方向都是单位向量,并且两两正交。如果初始上方向与视线方向平行,叉乘会得到零向量,需要更换参考上方向。

最终得到三个相机方向:

复制代码
r  = 相机右方向
t  = 相机上方向
-g = 相机后方向

三、为什么相机的 Z 轴是 -g?

使用的相机坐标系约定中:

复制代码
X 轴正方向:相机右方
Y 轴正方向:相机上方
Z 轴正方向:相机后方

相机实际观察的方向是局部负 Z 轴方向。

但是,g 表示的是从相机指向目标的方向,也就是相机前方。

因此:

复制代码
相机 Z 轴正方向 = -g

这里的负号来自相机坐标系的约定,而不是叉乘运算本身。

如果其他图形系统采用相机朝正 Z 轴观察的约定,相关矩阵的符号也可能不同。因此,推导矩阵之前必须先明确坐标系约定。

四、根据相机的三个方向向量构造旋转矩阵

前面已经求出了相机的三个方向向量:

复制代码
r = 相机右方向
t = 相机上方向
g = 相机视线方向

由于使用相机朝局部负 Z 轴观察的约定,因此相机坐标系的三个正方向分别为:

复制代码
X 轴: r
Y 轴: t
Z 轴:-g

接下来,我们要根据这三个方向向量构造旋转矩阵。

1. 先构造世界坐标到相机坐标的矩阵

我们的目标是将世界坐标中的向量转换为相机坐标中的向量。

在列向量左乘的约定下,世界坐标中的向量可以写成:

复制代码
v_world = (x, y, z)^T

转换后的相机坐标为:

复制代码
v_camera = R * v_world

矩阵的每一行,决定了输入向量在相应相机坐标轴上的投影分量。

例如,相机坐标的 X 分量,就是世界空间向量在相机右方向 r 上的投影:

复制代码
x_camera = dot(r, v_world)

同理:

复制代码
y_camera = dot(t, v_world)

z_camera = dot(-g, v_world)

因此,世界坐标到相机坐标的旋转矩阵为:

复制代码
R_world_to_camera =

[ rx    ry    rz  ]
[ tx    ty    tz  ]
[-gx   -gy   -gz  ]

这里,r、t 和 -g 分别构成矩阵的三行。

将矩阵乘法展开,就能得到:

复制代码
x_camera = rx*x + ry*y + rz*z

y_camera = tx*x + ty*y + tz*z

z_camera = -gx*x - gy*y - gz*z

这三个公式的含义非常直观:分别计算世界空间中的点在相机右方向、上方向和后方向上的坐标分量。

2. 再通过求逆得到相机到世界坐标的矩阵

如果已经知道世界坐标到相机坐标的矩阵:

复制代码
v_camera = R_world_to_camera * v_world

那么反过来,将相机坐标转换回世界坐标,就需要对矩阵求逆:

复制代码
v_world = inverse(R_world_to_camera) * v_camera

因此:

复制代码
R_camera_to_world = inverse(R_world_to_camera)

由于 r、t 和 -g 是互相正交的单位向量,旋转矩阵属于正交矩阵,所以满足:

复制代码
inverse(R) = transpose(R)

于是:

复制代码
R_camera_to_world = transpose(R_world_to_camera)

展开后得到:

复制代码
R_camera_to_world =

[ rx    tx   -gx ]
[ ry    ty   -gy ]
[ rz    tz   -gz ]

这就是相机坐标到世界坐标的旋转矩阵。

需要注意:这里不是随意交换矩阵元素,而是利用正交矩阵的性质,将矩阵求逆转换成矩阵转置。

总结一下:

复制代码
世界坐标 -> 相机坐标:

R_world_to_camera =

[ rx    ry    rz  ]
[ tx    ty    tz  ]
[-gx   -gy   -gz  ]


相机坐标 -> 世界坐标:

R_camera_to_world =

[ rx    tx   -gx ]
[ ry    ty   -gy ]
[ rz    tz   -gz ]

两者互为逆矩阵。由于旋转矩阵是正交矩阵,因此它们也互为转置矩阵。

二、投影

在三维图形学中,我们通常在三维世界中定义物体,但最终需要将它们显示在二维屏幕上。因此,我们需要将三维空间中的点映射到一个用于显示的投影平面上,这个过程称为投影(Projection)。

常见的投影方式有两种:

  • 正交投影(Orthographic Projection):投影线彼此平行。
  • 透视投影(Perspective Projection):投影线汇聚于同一个投影中心,具有近大远小的效果。

一、正交投影

正交投影的核心特点是:物体的投影大小不会因为它距离相机的远近而发生变化。

例如,一个长度为 2 的立方体,无论放在相机前方较近的位置,还是较远的位置,只要没有进行其他缩放,它在投影平面上的大小就不会因为距离变化而改变。

正交投影可以理解为:从三维空间中沿着固定方向发出一组互相平行的投影线,将物体投射到投影平面上。

为了便于计算,我们通常先确定一个长方体作为观察范围。这个长方体由以下六个边界确定:

  • 左边界:l
  • 右边界:r
  • 下边界:b
  • 上边界:t
  • 近裁剪面距离:n
  • 远裁剪面距离:f

这里采用常见的相机约定:相机朝局部 -Z 方向观察,因此近裁剪面和远裁剪面的相机空间坐标分别为 z = -n 和 z = -f,其中 n、f 都是正数,且 f > n。

我们希望将这个观察长方体变换到一个标准立方体中,也就是规范设备坐标(NDC)所使用的范围:

  • x 方向:-1, 1
  • y 方向:-1, 1
  • z 方向:-1, 1

这个过程就是正交投影矩阵要完成的工作。

一、正交投影的变换过程

将观察长方体映射到标准立方体,可以拆分为两个步骤:

  1. 平移:将观察长方体的中心移动到原点。
  2. 缩放:将平移后的长方体缩放到 -1, 1 范围内。

这样做的好处是,可以将一个复杂的空间映射问题拆解成我们熟悉的平移和缩放变换。

1. 平移变换

观察长方体在 x、y 方向的中心坐标分别为:

复制代码
center_x = (l + r) / 2
center_y = (b + t) / 2

由于相机空间中的 z 范围是 -f, -n,因此长方体在 z 方向的中心坐标为:

复制代码
center_z = (-f + (-n)) / 2
         = -(f + n) / 2

为了将长方体中心移动到原点,需要沿三个方向分别平移:

复制代码
x: -(l + r) / 2
y: -(b + t) / 2
z:  (f + n) / 2

采用列向量左乘的约定,平移矩阵为:

复制代码
T =
[ 1  0  0  -(l+r)/2 ]
[ 0  1  0  -(b+t)/2 ]
[ 0  0  1   (f+n)/2 ]
[ 0  0  0      1    ]

经过这一步,观察长方体的中心就移动到了原点。

此时,各轴方向的坐标范围变为:

复制代码
x: [-(r-l)/2, (r-l)/2]
y: [-(t-b)/2, (t-b)/2]
z: [-(f-n)/2, (f-n)/2]
2. 缩放变换

平移完成后,我们还需要将长方体的三个方向分别缩放,使每个方向的范围都变成 -1, 1。

对于 x 方向,平移后的范围是:

复制代码
[-(r-l)/2, (r-l)/2]

要将这个范围映射到 -1, 1,需要乘以:

复制代码
scale_x = 2 / (r-l)

同理,y 方向的缩放系数为:

复制代码
scale_y = 2 / (t-b)

z 方向稍有不同。由于我们采用相机朝 -Z 方向观察的约定,并希望近裁剪面 z = -n 映射到 -1、远裁剪面 z = -f 映射到 1,因此 z 方向需要进行反向缩放:

复制代码
scale_z = -2 / (f-n)

于是,缩放矩阵为:

复制代码
S =
[ 2/(r-l)      0          0       0 ]
[     0     2/(t-b)       0       0 ]
[     0        0      -2/(f-n)    0 ]
[     0        0          0       1 ]

注意:z 方向出现负号,并不是所有正交投影都必然如此,而是由这里采用的相机坐标约定和深度映射方向决定的。

二、推导正交投影矩阵

现在我们已经得到了两个变换矩阵:

  • T:将观察长方体的中心平移到原点。
  • S:将平移后的长方体缩放到标准立方体。

由于使用列向量左乘,矩阵乘法的顺序与实际执行顺序相反:先执行平移,再执行缩放,因此总变换矩阵为:

复制代码
M_ortho = S * T

将两个矩阵相乘,可以得到:

复制代码
M_ortho =
[ 2/(r-l)      0          0       -(r+l)/(r-l) ]
[     0     2/(t-b)       0       -(t+b)/(t-b) ]
[     0        0      -2/(f-n)    -(f+n)/(f-n) ]
[     0        0          0              1     ]

这就是在上述坐标约定下的正交投影矩阵。

它将相机空间中指定的观察长方体映射到标准立方体,使三个方向的坐标都落入 -1, 1 范围。

需要注意,正交投影矩阵的作用是将观察范围映射到标准立方体,并不意味着所有位于长方体之外的物体都会自动被删除。实际渲染时,超出裁剪范围的部分还需要由图形管线进行裁剪。

二、透视投影

在三维图形学中,我们需要将三维世界中的物体映射到二维屏幕上。上一节介绍的正交投影,可以将观察范围映射到标准立方体,但它有一个明显的特点:物体不会因为距离相机更远而自然变小。

而在现实世界中,我们通常会观察到近大远小的现象。例如:

  • 近处的汽车看起来比较大,远处的汽车看起来比较小。
  • 铁轨两侧的边缘会随着距离增加逐渐靠近。
  • 同样大小的物体,距离相机越远,在视野中通常占据越小的范围。

为了模拟这种视觉效果,我们需要使用透视投影(Perspective Projection)。

透视投影的核心思想是:将三维空间中的点沿着经过相机投影中心的射线,映射到投影平面上。

与正交投影不同,透视投影的投影线并不是互相平行的,而是汇聚于同一个投影中心,因此能够产生近大远小的效果。

一、透视投影的基本原理

其实,透视投影的原理并不复杂,可以想象一下:同样大小的东西,离我们越远,看起来就越小。

比如,站在铁轨上往远处看,两条铁轨好像慢慢靠到了一起。实际上,铁轨并没有变窄,只是因为距离越来越远,它们在我们眼中占据的范围越来越小。

在计算机图形学中,也是同样的道理。

我们先看相机坐标系中的一个点:

复制代码
P = (x, y, z)

在 GAMES101 中,我们约定相机朝着 -Z 方向看,所以物体在相机前方时,z 通常是负数。

假设我们在相机前方放了一块投影平面,位置是:

复制代码
z = -n

现在,想象从相机原点出发,穿过点 P,继续延伸这条线,直到它碰到投影平面。碰到的位置,就是点 P 的投影位置。

根据相似三角形,可以得到:

复制代码
x_projected = n * x / (-z)
y_projected = n * y / (-z)

这两个公式看起来有点绕,其实只需要抓住一点:投影后的坐标,要除以点距离相机的深度。

举个例子,假设投影平面距离相机 1 个单位,两个点的 x 坐标都是 2,只是距离相机的远近不同。

近处的点:

复制代码
x = 2
z = -2

x_projected = 1 * 2 / 2 = 1

远处的点:

复制代码
x = 2
z = -4

x_projected = 1 * 2 / 4 = 0.5

可以看到,两个点原本的 x 坐标相同,但远处的点投影后,x 坐标只有近处点的一半。

这就是因为距离越远,除数越大,最终得到的投影坐标就越小。

对于物体来说,它的各个点都会受到这种深度缩放的影响,所以距离相机越远,投影到屏幕上的大小通常也就越小。

这就是透视投影产生近大远小效果的原因。

我们可以将透视投影理解为两个步骤:

  1. 将透视投影的视锥体变换为一个长方体。
  2. 再使用正交投影,将这个长方体映射到标准立方体。
一、构建矩阵,转化为长方形

n面为近裁剪面,z面为远裁剪面,n到z的对方也就是相机可以看到的范围,我们首先就要将这个棱台压缩到长方体,根据相似三角形性质,所以每一个点的x,y都可以使用x' ,y'来表示。

x' = (n / z) * x

y' = (n / z) * y

当投影平面距离 n 固定时,投影坐标与深度 z 成反比。

  • 当 z 较小时,n / z 较大,投影后的坐标偏移更大。

  • 当 z 较大时,n / z 较小,投影后的坐标偏移更小。

这里要注意,压缩后的x和y可以通过最近面的相似三角形性质来推导出来,除了近裁剪面和远裁剪面的z是不变的,中间的z坐标是会随着距离的增加而受到不同程度的压缩,是无法通过相似三角形表示出来的。

因此,同样大小的物体距离相机越远,投影到屏幕上的尺寸就越小,这就是透视投影中的近大远小现象。

也就是说某一个点(x,y,z,1)要应用某一个矩阵后要变成(nx,ny,unknown(不知道),z)的形式,所以我们就可以先推出来矩阵的大概为:

因为z坐标是不知道的,所以第三行的值暂时无法推导出来。

这个时候可以利用到近裁剪面和远裁剪面上面的点的z坐标是不会发生变化的。

也就是近裁剪面上点(x,y,n,1)的值经过经过矩阵变化后为(nx,ny,

n²,n)

推导出A*n+B=n²。

在利用远裁剪面的z坐标也不会发送改变。

这里我们设远裁剪面的距离为f。、

也就可以推导出另一个公式。

A*n+B=f²。

得出A值为n+f

得出B值为-nf

所以压缩为长方体的矩阵就为

复制代码
M_persp =
| n   0     0       0 |
| 0   n     0       0 |
| 0   0   f+n      -f*n |
| 0   0    1       0 |
二、使用正交投影

之后将这个矩阵再进行一次正交投影,t,b,l,r分别为上边界,下边界,左边界,右边界。

① 挤压矩阵(把棱台压成长方体)

复制代码
M_persp_to_ortho =

| n   0    0     0  |
| 0   n    0     0  |
| 0   0   n+f   -nf |
| 0   0    1     0  |

② 正交投影矩阵(代入对称条件 l = -r,b = -t)

一般正交投影矩阵:

复制代码
M_ortho =

| 2/(r-l)     0          0          0 |
|    0      2/(t-b)      0          0 |
|    0         0       2/(n-f)      0 |
|    0         0          0          1 |

代入对称条件 l = -r,b = -t,得到:

复制代码
M_ortho =

| 1/r    0       0            0       |
| 0      1/t     0            0       |
| 0      0       2/(n-f)      0       |
| 0      0       0            1       |

如果需要将深度从 -n, -f 映射到 -1, 1,还需要包含 Z 方向的平移项。对应的正交投影矩阵应为:

复制代码
M_ortho =

| 1/r    0       0                0       |
| 0      1/t     0                0       |
| 0      0       2/(n-f)    -(n+f)/(n-f)   |
| 0      0       0                1       |

③ 两者相乘,得到最终透视投影矩阵

复制代码
M_persp = M_ortho * M_persp_to_ortho

最终结果:

复制代码
M_persp =

| n/r    0       0                0       |
| 0      n/t     0                0       |
| 0      0       (n+f)/(n-f)   -2*n*f/(n-f)|
| 0      0       1                0       |

最后我们还可以使用含有fovY(视场角),aspect(宽高比)的版本

最后的公式可以表示为:

代入得到用视场角(FOV)表示的透视投影矩阵:

复制代码
M_persp =

| 1 / (aspect * tan(fovY / 2))    0                 0                  0          |
| 0                                1 / tan(fovY / 2) 0                  0          |
| 0                                0                 (n+f) / (n-f)     -2*n*f/(n-f) |
| 0                                0                 1                  0          |
相关推荐
田里的水稻7 小时前
EP_ROS2的相机视频流传递说明
数码相机·机器学习
田里的水稻9 小时前
EP_ROS框架---相机视频流交互二
人工智能·深度学习·数码相机·机器学习·迁移学习
光电的一只菜鸡10 小时前
相机光学(五十六)——DCIAF
数码相机
Zldaisy3d10 小时前
北京理工大学 l 金属激光增材制造在线监测技术研究进展
数码相机·3d·制造
江汉似年12 小时前
针孔相机成像模型
数码相机
波力海苔夹心脆67518 小时前
C# 机器视觉实战:单相机引导机械手精准贴合,从拍照、偏差计算(平移+旋转)到坐标补偿全解
开发语言·经验分享·数码相机·c#·视觉检测·.net
光电的一只菜鸡20 小时前
摄影里面的 APEX 系统指的是什么
数码相机
数智工坊1 天前
视觉SLAM第2讲|初识SLAM:经典框架、传感器选型与工程环境全梳理
人工智能·深度学习·数码相机·机器人
博图光电1 天前
Libra 27105相关技术参数
人工智能·数码相机