《Fundamentals of Computer Graphics》8. Viewing

之前的章节中,矩阵变换是在三维或二维世界中排列对象的工具,矩阵变换的另一个重要用途是在“三维世界的二维视角”中的位置和三维世界中的位置进行转换

3D 到 2D 的映射叫做视图变换(viewing transformation),视口变换在对象顺序渲染中很重要,因为我们要频繁地查找每个对象在 2D 中的位置

第四章中,学习了不同类型的透视、正交视图以及如何生成观察射线。

本章是第四章的逆过程,本章将阐述如何使用矩阵变换来描述“平行视图”和“透视视图”,本章中的变换会将 3D 场景中的点投影到图像中的 2D 点,并且会将给定像素的观察射线上的任意点投影回像素的位置

把三维世界中的点投影到二维,仅仅能够做到线框(wireframe)的渲染,即物体边缘。距离摄像机更近的面不会遮挡更远的面

image-20241026212420960

也就是这一章仅仅是线框的渲染,之后的章节会讨论对象表面的渲染

视图变换(Viewing Transformations)

视图变换的任务是将 3D 坐标 (x,y,z) 映射到图像中的坐标(以像素为单位)

这是个复杂的过程,取决于摄像机的位置和方向、投影类型、视野、图像的分辨率等

与所有复杂的变换一样,最好将其分解为几个更简单的变换的乘积。大多数图形系统通过使用三个变换序列来实现这一点:

  1. 相机变换(camera transformation)或视变换(eye transformation):将相机放置在原点,并且朝向方便计算的方向(例如 x 轴正方向?)。这一步仅取决于相机的位置、方向或者叫姿态(pose)
  2. 投影变换(projection transformation):将三维场景中的点映射到相机空间(camera space),所有点将在 xy 方向上都落到 11 的范围内。这一步仅取决于投影类型
  3. 视口变换(viewport transformation)或窗口变换(windowing transformation):将单位图像矩形映射到像素坐标系的矩形。这一步仅取决于输出图像的大小和位置

image-20241026213712509

摄像机坐标系(camera coordinates)、摄像机空间(camera space)

规范视空间(canonical view volume):(x,y,z)[1,1]3 的一个正方体,x=1x=1 代表屏幕的最左侧和最右侧,y=1y=1 分别代表屏幕的最下边和最上边

image-20241026214817726

视口变换(Viewport Transformation)

从规范视空间映射到屏幕空间(screen space),即将 [1,1]2 映射到 [0.5,nx0.5]×[0.5,ny0.5]

假设现在所有线段都在规范视空间内部,之后在讨论剪切(clipping)时,会放松这个限制

上一章有一个例子,提供了一个公式,如下:

x=nxx+120.5y=nyy+120.5

写成矩阵形式:

[xscreenyscreen1]=[nx20nx120ny2ny12001][xcanonicalycanonical1]

我们把 z 坐标加进来(之后的章节会进行深度测试),得到视口(变换)矩阵(viewport matrix):

Mvp=[nx200nx120ny20ny1200100001]

注意,这里好像有点问题:屏幕像素大小是 nx×ny,那么整数表示的屏幕空间应该是 [0,nx1]×[0,ny1],所以屏幕空间应该是 [0.5,nx1.5]×[0.5,ny1.5]

所以视口变换应该是:

Mvp=[nx200nx320ny20ny3200100001]

正交投影矩阵

这里的正交投影应该指的是平行正交投影,最简单的一种,只需要垂直移动到图像上就可以了

首先,将视线方向的方向记作 z 方向(与左手系、右手系有关),并且 y 轴朝上

然后,框一个长方体 [l,r]×[b,t]×[f,n]

构成的空间被称为正交视图空间(orthographic view volume)

image-20241026222242052

从正交视图空间到规范视空间的变换很简单:

x=xl+r2rl2=2x(r+l)rl

写成矩阵就是:

Morth=[2rl00r+lrl02tb0t+btb002nfn+fnf0001]

从正交视图空间到屏幕空间变换就是把上面得到的两个矩阵相乘,即:

[xscreenyscreenzcanonical1]=MvpMorth[xyz1]

z 轴现在在 [1,1] 区间内,之后使用 z-buffer 进行深度测试时需要

画线段的伪代码:

construct M_vp
construct M_orth
M = M_vp M_orth
for each line-segment (a_i, b_i) do
    p = M a_i
    q = M b_i
    drawline(x_p, y_p, x_q, y_q)

相机变换(Camera Transformation)

相机变换就是世界空间到相机空间的变换

有许多约定用于指定相机的位置和方向。我们将使用以下一种:

  • 眼睛的位置 e
  • 注视方向(gazing direction)g
  • 相机的向上向量 t

通过这三个向量来构建一个坐标系

首先构建一个正交基:

w=ggu=t×wt×wv=w×u

然后加上位移 e,根据上一章的坐标系变换,我们可以得到变换矩阵:

Mcam=[uvwe0001]1=[xuxvxwxeyuyvywyezuzvzwze0001]1

上一章中说,仿射变换可以分解成线性变换和平移变换,它可以写成:

Mcam=([100xe010ye001ze0001][xuxvxw0yuyvyw0zuzvzw00001])1

其中,我们知道左上的坐标系变换是正交矩阵(因为它就是由三个正交向量组成),所以后面矩阵的逆可以很轻松地求出来

前面的矩阵是平移变换,所以根据数学意义也可以轻松求出

所以:

Mcam=[xuyuzu0xvyvzv0xwywzw00001][100xe010ye001ze0001]

画线段的伪代码:

construct M_vp
construct M_orth
construct M_cam
M = M_vp M_orth M_cam
for each line-segment (a_i, b_i) do
    p = M a_i
    q = M b_i
    drawline(x_p, y_p, x_q, y_q)

投影变换(Projective Transformations)

对于透视投影,我们要把空间中的点映射到规范视空间中,我们会用到 z 坐标,并且 z 坐标在分母上,如下(仅仅是举例子,并不是真的是这样):

ys=dzy

image-20241027121102657

传统的矩阵变换做不到这样(除法)

我们使用一个简单的方法推广齐次坐标的机制来进行除法:

对于之前的齐次坐标,(x,y,z) 表示为 [xyz1]T,多出来的一维永远是 1,且仿射变换矩阵中的第四行永远是 [0001]

现在,我们让第四维是 w[xyzw]T 用来表示坐标 (x/w,y/w,z/w),这对之前没有影响(因为除以 1 没什么意义)

线性变换允许我们计算:

x=a1x+b1y+c1zy=a2x+b2y+c2zz=a3x+b3y+c3z

仿射变换允许我们计算:

x=a1x+b1y+c1z+d1y=a2x+b2y+c2z+d2z=a3x+b3y+c3z+d3

w 作为分母允许我们计算:

x=a1x+b1y+c1z+d1ex+fy+gz+hy=a2x+b2y+c2z+d2ex+fy+gz+hz=a3x+b3y+c3z+d3ex+fy+gz+h

但是,还有一个限制,就是 xyz 的除数相同

表示成矩阵形式如下:

[x~y~z~w~]=[a1b1c1d1a2b2c2d2a3b3c3d3efgh][xyz1]

并且:

(x,y,z)=(x~/w~,y~/w~,z~/w~)

这样的变换称为投影变换(projective transformation)或者单应变换(homography)

有一种更优雅的方式来解释 w 坐标:3D 投影变换视作 4D 线性变换,并且额外规定:

xαx,α0

这表示两个齐次坐标 xαxα0)是同一个三维空间中的点

这些点构成了一条直线

image-20241027124319834

在需要笛卡尔坐标时,只需要找到它和 w=1 这个面上的交点就可以了

其实就是说给齐次向量乘上一个倍数不影响它表示的坐标

透视投影

使用投影变换后,前面的 ys 表达式可以写成:

[ys1][d00010][yz1]

根据惯例,在相机空间中,相机朝向 z 方向,近屏幕和远平面为 nf(都是负数),使用这样的变换:

x=nxzy=nyzz=(n+f)zfnz=n+ffnz

为什么 z 坐标不能直接使用呢?因为实现不了 z=z2/z 的操作,只能通过相似的变换来实现。

规范视空间是 [1,1]3,为什么 z 坐标没有映射到 [1,1] 上?因为这不是到规范视空间的变换,还要再乘上一个前面得到的 Morth

上面是最常用的一种变换,它保证了当 z=nz=f 时有 z=z=nz=z=f

写成矩阵形式为:

P=[n0000n0000n+ffn0010]

这个矩阵被称为透视矩阵(perspective matrix)

变换为:

P[xyz1]=[nxny(n+f)zfnz][nx/zny/zn+ffn/z1]

我们看一下 P 的逆:

P1=[1n00001n000001001fnn+ffn]

由于乘上一个系数没有影响,我们乘上一个 fn,它的逆可以写成:

P1=[f0000f00000fn001n+f]

我们乘上 Morth 之后就是透视投影矩阵(perspective projection matrix),注意是左乘:

Mper=MorthP

一个问题是如何确定 Morth 所需的 lrbt,我们知道透视矩阵变换后,在 z=n 面上 xy 坐标不会改变,所以可以在这里确定 lrbt

相乘之后 Mper 长这样:

Mper=[2nrl0l+rlr002ntbb+tbt000f+nfn2fnfn0010]

在 OpenGL 中,它会让我们指定 |n||f|

其他的图像 API 中,通常将 n 设置为 0,将 f 设置为 1

规范视空间有时还会设置为 [0,1]3

上面的决策都会改变投影矩阵

变换线段的伪代码:

construct M_vp
construct M_per
construct M_cam
M = M_vp M_per M_cam
for each line-segment (a_i, b_i) do
    p = M a_i
    q = M b_i
    drawline(x_p, y_p, x_q, y_q)

透视变换的一些性质

直线性

一个重要性质是将线转换为线,将平面转换为平面,另外将线段转换为线段,例如,我们有这样的线段:

f(t)=q+t(Qq)

对它进行透视变换:

g(t)=Mper(q+t(Qq))=Mperq+t(MperQMperq)r+t(Rr)

如果没有同质化,那么肯定存在线性关系,且 t 不变,所以有:

wg=wr+t(wrwr)

经过同质化后,g(t)rR 变成了:

g(t)r+t(Rr)wr+t(wrwr)rrwrRRwR

其中,我们如果可以找到一个线性关系:

r+t(Rr)wr+t(wrwr)=rwr+t(RwRrwr)

我们就可以知道:原来是一条直线上的点,经过透视变换并同质化后仍然在一条直线上

进行一些操作:

r+t(Rr)wr+t(wRwr)=(1t)r+tR(1t)wr+twR=(1t)wr(1t)wr+twRrwr+twR(1t)wr+twRRwR=[1twR(1t)wr+twR]rwr+twR(1t)wr+twRRwR=rwr+twR(1t)wr+twR(RwRrwr)rwr+t(RwRrwr)

如果我们表示成 gh(t)rhRh,那么总结一下,有:

f(t)=q+t(Qq)

经过透视变换后:

g(t)=r+t(Rr)

然后经过同质化后:

gh(t)=rh+t(Rhrh)

其中:

t=twR(1t)wr+twR

我们发现确实是这样,而且,t=f(t) 是个连续函数,确保不会被“撕裂(torn)”

视野(Field-of-View)

虽然我们可以使用 lrbt 来指定窗口,但是有一个更简单的方法:从窗口的中心观察,即 l=rb=t

这样的话只需要两个自由度就能表示这个窗口

如果我们添加了这样一个约束:

nxny=rt

来表示“图像的性质没有失真”,即二者比例相同。

一旦二者比值被指定,那么还需要一个自由度就可以表示这个窗口了,通常使用视野来指定:

tanθ2=t|n|

image-20241027141920640

在某些系统中,n 被硬编码成一个合理的定值,那么我们只需要知道 θ 就可以计算出 t

问题和练习

在透视矩阵中:

z=n+fnfz

当有一个线段,它跨越了 z=0 时会发生撕裂,即 a/0=a/0=,这通常可以使用剪切来避免,但是撕裂现象使剪切本身变得更加复杂,之后会讨论

在透视变换之后,需要把 w 坐标变成 1

  1. 构建视口矩阵,其中像素坐标从图像顶部向下计数:

    x=nx1+x20.5=nxx+nx12y=ny1y20.5=nyy+ny12

    那么矩阵为:

    Mvp=[nx200nx120ny20ny1200100001]
  2. 视口矩阵与正交矩阵相乘的意义是:一个长方体映射到正方体,再将 xy 坐标映射到屏幕空间。两个操作合在一起就是将 [l,r]×[b,t]×[n,f] 映射到 [0.5,nx0.5]×[0.5,ny0.5]×[1,1]

  3. 从代数角度证明透视矩阵保留了视场内的 z 值顺序。略

  4. 使用前面的办法构建正交基即可?

  5. 根据 f(t) 很容易证