Deep Read

【空间变换】基本变换

有了 【空间变换】齐次坐标【空间变换】齐次坐标    空间变换这一系列笔记,讲的是一个顶点从建模软件里的局部坐标,一路变换到屏幕像素的完整过程,也就是渲染管线里常说的 MVP 变换加上后续的屏幕映射。而这一切的数学地基,是齐次坐标。所以先从它讲起。 为什么需要齐次坐标 顶点变换无非三种:平移、旋转、缩放。旋转和缩放都能写成一个矩阵乘以坐标: $$P' = M \cdot P$$ 但平移不行。在三维笛卡尔坐标下,平移是把每个分量各加一个偏移量: $$(x, y, z) \rightarrow (x + t_x,\ y + t_y,\ z + t_z)$$ 这是一次加法,没法用一个 $3\times3$ 矩阵乘出来。于是就出现了一个别扭的局面:旋转缩放是乘法,平移是加法,两类操作凑不到一起。如果一个顶点既要旋转又要平移,就得写成 $P' = M \cdot P + t$,既有乘又有加。当变换一多,这种混合写法会变得非常难组合。 齐次坐标就是为了解决这件事:给坐标加上第四个分量 $w$,把平移也塞进矩阵里,让平移、旋转、缩放全都变成统一的矩阵乘法。 一旦所有变换都是矩阵,它们就能通过矩阵连乘预先合并成一个矩阵 打底,就可以来看最基础的三种变换:平移、缩放、旋转。它们是模型变换(Model Transformation)的三块积木,把物体从局部空间摆放到世界空间里的正确位置、朝向和大小,靠的就是这三者的组合。

因为统一用了齐次坐标,三种变换都写成 4×44\times4 矩阵,可以直接连乘合并,最后对每个顶点只做一次矩阵乘法。下面逐个推导,并附上在着色器里的写法。

平移变换

平移就是给每个坐标分量加上一个偏移量,借助齐次坐标的第四列实现:

[100x010y001z0001][PxPyPz1]=(Px+x, Py+y, Pz+z, 1)\begin{bmatrix} 1 & 0 & 0 & x \\ 0 & 1 & 0 & y \\ 0 & 0 & 1 & z \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} P_x \\ P_y \\ P_z \\ 1 \end{bmatrix} = (P_x + x,\ P_y + y,\ P_z + z,\ 1)

对应的着色器代码:

float4x4 T = float4x4(
  1, 0, 0, _TransForm.x,
  0, 1, 0, _TransForm.y,
  0, 0, 1, _TransForm.z,
  0, 0, 0, 1
);
v.positionOS = mul(T, float4(v.positionOS.xyz, 1.0));

缩放变换

缩放是把各个轴上的坐标各自乘以一个缩放系数,所以缩放系数落在矩阵的对角线上:

[Sx0000Sy0000Sz00001][PxPyPz1]=(PxSx, PySy, PzSz, 1)\begin{bmatrix} S_x & 0 & 0 & 0 \\ 0 & S_y & 0 & 0 \\ 0 & 0 & S_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} P_x \\ P_y \\ P_z \\ 1 \end{bmatrix} = (P_x S_x,\ P_y S_y,\ P_z S_z,\ 1)

代码里额外用 _Scale.w 做了一个整体的统一缩放系数:

float4x4 S = float4x4(
  _Scale.x * _Scale.w, 0, 0, 0,
  0, _Scale.y * _Scale.w, 0, 0,
  0, 0, _Scale.z * _Scale.w, 0,
  0, 0, 0, 1
);
v.positionOS = mul(S, float4(v.positionOS.xyz, 1.0));

旋转变换

旋转比前两者复杂,先从二维推起,再推广到三维。

二维旋转

image.png

设想有两个坐标系:(x1,y1)(x_1, y_1) 和把它旋转了 θ\theta 角后的 (x2,y2)(x_2, y_2)。已知点 PP 在第一个坐标系下的坐标 (Px1,Py1)(P_{x_1}, P_{y_1}),要求它在第二个坐标系下的坐标 (Px2,Py2)(P_{x_2}, P_{y_2})。分别推导两个分量。

推导 Px2P_{x_2}

image.png

OC=OB+BCOC = OB + BC

cosθ=OBOA    OB=cosθOA\cos\theta = \frac{OB}{OA} \implies OB = \cos\theta \cdot OA

sinθ=ADAP    AD=BC=sinθAP\sin\theta = \frac{AD}{AP} \implies AD = BC = \sin\theta \cdot AP

OC=cosθOA+sinθAPOC = \cos\theta \cdot OA + \sin\theta \cdot AP

Px2=cosθPx1+sinθPy1P_{x_2} = \cos\theta \cdot P_{x_1} + \sin\theta \cdot P_{y_1}

推导 Py2P_{y_2}

image.png

OA=OCACOA = OC - AC

cosθ=OCOB    OC=cosθOB\cos\theta = \frac{OC}{OB} \implies OC = \cos\theta \cdot OB

sinθ=BDBP    AC=BD=sinθBP\sin\theta = \frac{BD}{BP} \implies AC = BD = \sin\theta \cdot BP

OA=cosθOBsinθBPOA = \cos\theta \cdot OB - \sin\theta \cdot BP

Py2=cosθPy1sinθPx1P_{y_2} = \cos\theta \cdot P_{y_1} - \sin\theta \cdot P_{x_1}

于是点 PP 在第二个坐标系下的坐标为:

P2=(cosθPx1+sinθPy1, cosθPy1sinθPx1)P_2 = (\cos\theta \cdot P_{x_1} + \sin\theta \cdot P_{y_1},\ \cos\theta \cdot P_{y_1} - \sin\theta \cdot P_{x_1})

写成矩阵形式:

[cosθsinθsinθcosθ][Px1Py1]=[cosθPx1+sinθPy1sinθPx1+cosθPy1]\begin{bmatrix} \cos\theta & \sin\theta \\ -\sin\theta & \cos\theta \end{bmatrix} \begin{bmatrix} P_{x_1} \\ P_{y_1} \end{bmatrix} = \begin{bmatrix} \cos\theta \cdot P_{x_1} + \sin\theta \cdot P_{y_1} \\ -\sin\theta \cdot P_{x_1} + \cos\theta \cdot P_{y_1} \end{bmatrix}

需要留意的是,这里推的是"坐标系旋转 θ\theta、点不动"的情形,等价于在固定坐标系里把点反向旋转。后面三维的三个轴,都沿用这套同样的符号约定,保持一致即可。

三维旋转

三维旋转的前提是先定住一根轴,绕这根轴旋转。绕某根轴旋转时,该轴上的坐标分量保持不变,另外两个分量按二维旋转的规律变化。

绕 X 轴旋转: xx 分量不变,yyzz 分量参与旋转。

[10000cosθsinθ00sinθcosθ00001][PxPyPz1]=[PxcosθPy+sinθPzsinθPy+cosθPz1]\begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & \sin\theta & 0 \\ 0 & -\sin\theta & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} P_x \\ P_y \\ P_z \\ 1 \end{bmatrix} = \begin{bmatrix} P_x \\ \cos\theta \cdot P_y + \sin\theta \cdot P_z \\ -\sin\theta \cdot P_y + \cos\theta \cdot P_z \\ 1 \end{bmatrix}

绕 Y 轴旋转: yy 分量不变。

[cosθ0sinθ00100sinθ0cosθ00001][PxPyPz1]=[cosθPx+sinθPzPysinθPx+cosθPz1]\begin{bmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} P_x \\ P_y \\ P_z \\ 1 \end{bmatrix} = \begin{bmatrix} \cos\theta \cdot P_x + \sin\theta \cdot P_z \\ P_y \\ -\sin\theta \cdot P_x + \cos\theta \cdot P_z \\ 1 \end{bmatrix}

绕 Z 轴旋转: zz 分量不变。

[cosθsinθ00sinθcosθ0000100001][PxPyPz1]=[cosθPx+sinθPysinθPx+cosθPyPz1]\begin{bmatrix} \cos\theta & \sin\theta & 0 & 0 \\ -\sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} P_x \\ P_y \\ P_z \\ 1 \end{bmatrix} = \begin{bmatrix} \cos\theta \cdot P_x + \sin\theta \cdot P_y \\ -\sin\theta \cdot P_x + \cos\theta \cdot P_y \\ P_z \\ 1 \end{bmatrix}

对应的着色器代码:

// 绕 X 轴旋转
float4x4 R_X = float4x4(
  1, 0, 0, 0,
  0, cos(_Angle.x), sin(_Angle.x), 0,
  0, -sin(_Angle.x), cos(_Angle.x), 0,
  0, 0, 0, 1
);
v.positionOS = mul(R_X, float4(v.positionOS.xyz, 1.0));

// 绕 Y 轴旋转
float4x4 R_Y = float4x4(
  cos(_Angle.y), 0, sin(_Angle.y), 0,
  0, 1, 0, 0,
  -sin(_Angle.y), 0, cos(_Angle.y), 0,
  0, 0, 0, 1
);
v.positionOS = mul(R_Y, float4(v.positionOS.xyz, 1.0));

// 绕 Z 轴旋转
float4x4 R_Z = float4x4(
  cos(_Angle.z), sin(_Angle.z), 0, 0,
  -sin(_Angle.z), cos(_Angle.z), 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1
);
v.positionOS = mul(R_Z, float4(v.positionOS.xyz, 1.0));

任意方向的旋转,都可以拆成绕三根轴旋转的组合(即欧拉角),把这三个矩阵按顺序连乘即可。把平移、缩放、旋转三者合并成一个矩阵,就得到完整的模型变换。接下来是把物体从世界空间搬到相机视角下的 【空间变换】观察变换【空间变换】观察变换经过 [[【空间变换】基本变换]] 的模型变换,物体已经摆放在世界空间里。接下来要换个视角看世界——把相机放到合适的位置和角度,这一步叫观察变换(View Transformation),也叫视图变换。 打个拍照的比方:先把要拍的东西摆放好,这是模型变换;再挑一个角度架好相机,这是观察变换;最后按下快门把三维场景拍成二维照片,那是后面的 [[【空间变换】投影变换]]。 观察空间 观察空间(View Space)就是以相机原点为坐标原点建立起来的坐标系。它的三个基向量在世界空间里的取值,描述的正是相机的朝向。 按照惯例,观察空间是右手坐标系,相机看向 $-Z$ 方向。也就是说,相机的视线方向对应的是观察空间的 $-Z$ 轴。如果记相机位置为 $ViewPos$、注视目标为 $ViewTarget$,那么视线方向(gaze)指向目标,而观察空间的 $+Z$ 轴与视线相反: $$Z_{view} = -(ViewTarget - ViewPos) = ViewPos - ViewTarget$$ 要唯一确定一个相机,需要三个量: 1. 相机的位置。 2. 相机看向的方向(gaz

参考