又开了一个新坑,这次聊一聊计算机图形学。

虽然这门课我十分喜欢,上课时也听得津津有味,但无奈知识点实在太多,有点难以记全,所以仔细思索后还是通过笔记的形式记录下来。这次的笔记就不像之前的XML与数据结构那么系统化了,想到啥就写啥吧。

接下来的内容会涉及很多 线性代数 的内容,大家务必带上线性代数的基本功再一同上车~


图形学中比较重要的一个概念就是变换了,不论是模型变换、视图变换还是投影变换,其基本思路都是矩阵运算的变换,所以接下来的内容都以矩阵变换为主,较为简单的我便一笔带过,偶尔几个比较麻烦的会稍微进行解释。

模型变换

在聊模型变换之前,我们先讲一讲坐标的表示,一般情况下我们是用三维的齐次坐标来表示一个点,如

[xyz]T\begin{bmatrix} x & y & z \end{bmatrix}^{T}

但在这里,我们需要将其扩展为四维的齐次坐标,变成 [xyz1]T\begin{bmatrix}x & y & z & 1\end{bmatrix}^{T}[xyz0]T\begin{bmatrix}x & y & z & 0\end{bmatrix}^{T} 。其中,第四维为1代表其为点,0则代表向量,至于为什么要这样做,看到后面就会明白了。

平移矩阵

要对向量 p 进行平移,我们需要一个平移矩阵 T ,使得 p'=Tp 。其中:

T(dx,dy,dz)=[100dx010dy001dz0001]T(d_x,d_y,d_z)=\begin{bmatrix} 1 & 0 & 0 & d_x \\ 0 & 1 & 0 & d_y \\ 0 & 0 & 1 & d_z \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}

去和四维齐次坐标相乘,就知道为什么要这样做了。

缩放矩阵

S(sx,sy,sz)=[sx0000sy0000sz00001]S(s_x,s_y,s_z)= \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

旋转矩阵

二维旋转矩阵

假设在 xOy 平面上进行绕z轴旋转 θ\theta 度,那么很容易得知:

{x=xcosθysinθy=xsinθ+ycosθz=z\begin{cases} x'=x\cos\theta-y\sin\theta \\ y'=x\sin\theta+y\cos\theta \\ z'=z \end{cases}

所以,绕z轴的旋转矩阵

Rz(θ)=[cosθsinθ00sinθcosθ0000100001]R_z(\theta)= \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}

同理:

Rx(θ)=[10000cosθsinθ00sinθcosθ00001]R_x(\theta)= \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta & 0 \\ 0 & \sin\theta & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}

Ry(θ)=[cosθ0sinθ00100sinθ0cosθ00001]R_y(\theta)= \begin{bmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}

绕任意轴旋转

绕任意轴旋转看似麻烦,但我们可以将其分解为几个小步骤:

  1. 将物体中心 p0p_0 移动到原点: T(p0)T(-p_0)
  2. 将旋转轴与z轴对齐,该步骤只需要分别经过绕x轴的旋转与绕y轴的旋转即可实现,具体原理不赘述,应该能理解
  3. 好了,现在物体中心在原点,而旋转轴就是z轴,此时只需进行一次 Rz(θ)R_z(\theta) 变换即可
  4. 先将物体绕x轴与y轴旋转回去,再将物体移动回原本的位置: T(p0)T(p_0)

具体计算较为复杂,感兴趣的朋友可以自己推算,此处只给出一个思路:

M=T(p0)Rx(θx)Ry(θy)Rz(θ)Ry(θy)Rx(θx)T(p0)M=T(p_0)R_x(-\theta_x)R_y(-\theta_y)R_z(\theta)R_y(\theta_y)R_x(\theta_x)T(-p_0)

视图变换

相机变换

在介绍相机变换之前,我们肯定都能想到与相机变换息息相关的函数:

void gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,GLdouble centerx,GLdouble centery,GLdouble centerz,GLdouble upx,GLdouble upy,GLdouble upz);

函数的相关参数就不再过多解释,但是我们需要从这些参数中提取出几个重要信息

  1. 相机位置 e=(eyex, eyey, eyez)
  2. 视线方向 g=(centerx-eyex, centery-eyey, centerz-eyez)
  3. 上方向量 t=(upx, upy, upz)

但是这些向量都是由用户给出,不能保证规范性,所以我们要先将其进行归一化:

w=ggw=-\frac{g}{\left|g\right|}

此处的w就是相机坐标系的z轴,而x轴,则只需通过t与w叉乘得到,然后将其归一化来保证规范性:

u=t×wt×wu=\frac{t\times w}{\left|t\times w\right|}

最后由这两个轴计算出y轴:

v=w×uv=w\times u

此时就可获得相机坐标系 (u, v, w) ,而坐标系变换矩阵,也很容易得出:

Mcam=[uvwe0001]1=[xuyuzu0xvyvzv0xwywzw00001][100xe010ye001ze0001]M_{cam}= {\begin{bmatrix} u & v & w & e \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}}^{-1} = \begin{bmatrix} x_u & y_u & z_u & 0 \\ x_v & y_v & z_v & 0 \\ x_w & y_w & z_w & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & -x_e \\ 0 & 1 & 0 & -y_e \\ 0 & 0 & 1 & -z_e \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}

投影变换

正交投影

glOrtho(left, right, bottom, top, near, far);

此处需要注意一下,near与far参数是相对于相机而言,所以-near需要映射到-1,-far需要映射到1。

Morth=S(2rightleft,2topbottom,2farnear)T(left+right2,bottom+top2,near+far2)=[2rightleft00right+leftrightleft02topbottom0top+bottomtopbottom002farnearfar+nearfarnear0001]M_{orth}=S(\frac{2}{right-left}, \frac{2}{top-bottom}, \frac{-2}{far-near})T(-\frac{left+right}{2}, -\frac{bottom+top}{2}, \frac{near+far}{2}) \\ = \begin{bmatrix} \frac{2}{right-left} & 0 & 0 & -\frac{right+left}{right-left} \\ 0 & \frac{2}{top-bottom} & 0 & -\frac{top+bottom}{top-bottom} \\ 0 & 0 & \frac{-2}{far-near} & -\frac{far+near}{far-near} \\ 0 & 0 & 0 & 1 \end{bmatrix}

透视投影

glFrustum(left, right, bottom, top, near, far);

Mfrus=[2nearrightleft0right+leftrightleft002neartopbottomtop+bottomtopbottom000far+nearfarnear2farnearfarnear0010]M_{frus}= \begin{bmatrix} \frac{2near}{right-left} & 0 & \frac{right+left}{right-left} & 0 \\ 0 & \frac{2near}{top-bottom} & \frac{top+bottom}{top-bottom} & 0 \\ 0 & 0 & -\frac{far+near}{far-near} & -\frac{2far*near}{far-near} \\ 0 & 0 & -1 & 0 \end{bmatrix}

了解就行,记不下来的。

视口变换

即把规范化视景体中的内容填入窗口中: glViewport(Ox, Oy, width, height);

由于规范化视景体坐标 (x, y, z) 的取值范围都在区间 [-1, 1] 中,而我们要将其填入 [Ox, Ox+width] 宽, [Oy, Oy+height] 高的窗口中,所以需要进行坐标的映射:

{WinX=Ox+x+12widthWinY=Oy+y+12heightWinZ=z+12\begin{cases} WinX=O_x+\frac{x+1}{2}*width \\ WinY=O_y+\frac{y+1}{2}*height \\ WinZ=\frac{z+1}{2} \end{cases}

稍作整理:

{WinX=(0.5width)x+(Ox+0.5width)WinY=(0,5height)y+(Oy+0.5height)WinZ=(0.5z)+(0.5)\begin{cases} WinX=(0.5*width)*x+(O_x+0.5*width) \\ WinY=(0,5*height)*y+(O_y+0.5*height) \\ WinZ=(0.5*z)+(0.5) \end{cases}

这样一来,我们就可以将该变换视为 缩放+平移 两步操作,对应的矩阵也就很容易计算出:

Mview=T(Ox+0.5width,Oy+0.5height,0.5)S(0.5width,0.5height,0.5)=[0.5width00Ox+0.5width00.5height0Oy+0.5height000.50.50001]M_{view}=T(O_x+0.5*width, O_y+0.5*height, 0.5)S(0.5*width, 0.5*height, 0.5) \\ = \begin{bmatrix} 0.5*width & 0 & 0 & O_x+0.5*width \\ 0 & 0.5*height & 0 & O_y+0.5*height \\ 0 & 0 & 0.5 & 0.5 \\ 0 & 0 & 0 & 1 \end{bmatrix}