跳到主要内容

二维空间变换

1. 平移(Translation)​

齐次坐标

给二维空间坐标引入第 3 个分量 ww,使平移变换也能表示为矩阵乘法,从而将所有几何变换统一为矩阵运算。

平移变换将点 (x,y)(x, y) 移动到 (x+tx,y+ty)(x + t_x, y + t_y):

x′=x+txy′=y+ty⇒[x′y′1]=[10tx01ty001][xy1]\begin{aligned} x' &= x + t_x \\ y' &= y + t_y \end{aligned} \quad \Rightarrow \quad \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
平移矩阵
T(tx,ty)=[10tx01ty001]T(t_x, t_y) = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}

2. 缩放(Scale)​

缩放变换将点 (x,y)(x, y) 缩放到 (sx⋅x,sy⋅y)(s_x \cdot x, s_y \cdot y):

x′=sx⋅xy′=sy⋅y⇒[x′y′1]=[sx000sy0001][xy1]\begin{aligned} x' &= s_x \cdot x \\ y' &= s_y \cdot y \end{aligned} \quad \Rightarrow \quad \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
缩放矩阵
S(sx,sy)=[sx000sy0001]S(s_x, s_y) = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix}

2.1 反射(Reflection)​

反射是缩放的特例。当 sxs_x 或 sys_y 取 −1-1 时,产生关于坐标轴的镜像:

  • 水平翻转(关于 Y 轴):sx=−1, sy=1s_x = -1,\ s_y = 1
S(−1,1)=[−100010001]S(-1, 1) = \begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}
  • 竖直翻转(关于 X 轴):sx=1, sy=−1s_x = 1,\ s_y = -1
S(1,−1)=[1000−10001]S(1, -1) = \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix}

3. 旋转(Rotation)​

绕原点逆时针旋转 θ\theta:

x′=xcos⁡θ−ysin⁡θy′=xsin⁡θ+ycos⁡θ⇒[x′y′1]=[cos⁡θ−sin⁡θ0sin⁡θcos⁡θ0001][xy1]\begin{aligned} x' &= x \cos\theta - y \sin\theta \\ y' &= x \sin\theta + y \cos\theta \end{aligned} \quad \Rightarrow \quad \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
旋转矩阵
R(θ)=[cos⁡θ−sin⁡θ0sin⁡θcos⁡θ0001]R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}

旋转矩阵是正交矩阵,满足 R−1(θ)=RT(θ)=R(−θ)R^{-1}(\theta) = R^T(\theta) = R(-\theta)。

4. 错切(Shear)​

错切变换使图形沿某一坐标轴方向发生"倾斜",保持面积不变。

  • 水平错切:沿 X 轴方向偏移,偏移量与 y 成正比
Hx(k)=[1k0010001]⇒{x′=x+k⋅yy′=yH_x(k) = \begin{bmatrix} 1 & k & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \quad \Rightarrow \quad \begin{cases} x' = x + k \cdot y \\ y' = y \end{cases}
  • 竖直错切:沿 Y 轴方向偏移,偏移量与 x 成正比
Hy(k)=[100k10001]⇒{x′=xy′=y+k⋅xH_y(k) = \begin{bmatrix} 1 & 0 & 0 \\ k & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \quad \Rightarrow \quad \begin{cases} x' = x \\ y' = y + k \cdot x \end{cases}

5. 复合变换​

多个变换可以通过矩阵乘法组合为一个变换:

v′=Tn⋯T2T1v\bm{v}' = T_n \cdots T_2 T_1 \bm{v}
注意顺序

矩阵乘法从右向左依次作用。即先应用 T1T_1,再应用 T2T_2,最后应用 TnT_n。

例如,先缩放再旋转再平移(TRS 组合)是图形学中最常见的复合变换:

M=T(tx,ty)⋅R(θ)⋅S(sx,sy)M = T(t_x, t_y) \cdot R(\theta) \cdot S(s_x, s_y)

6. 翻转变换​

flip

  • UI坐标:原点在左上角,X 轴向右,Y轴向下,如UIKit View坐标,图像像素等
  • 笛卡尔坐标:原点在左下角,X 轴向右,Y轴向上,如OpenGL、Metal、Quartz 2D等
Y轴翻转变换
[x′y′1]=[1000−1h001][xy1]\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & h \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
变换的本质

点的空间位置不变,只是换了一个坐标系来描述它,即移动旧坐标系的基,使其与新坐标系对齐,这里先将 Y 轴反向,再将原点移到左下角。

注意,变换的每一步都是相对原坐标系进行的:旋转和缩放始终以原点为基准,且原点位置始终不变(这里是左上角)。因此先向上平移 T(0,−h)T(0, -h) 再翻转也是可以的,结果同样是 y′=−y+hy' = -y + h。

M=T(0,h)⋅S(1,−1)=[10001h001][1000−10001]=[1000−1h001]M = T(0, h) \cdot S(1, -1) = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & h \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & h \\ 0 & 0 & 1 \end{bmatrix}

反过来:

M−1=S(1,−1)⋅T(0,−h)=[1000−10001][10001−h001]=[1000−1h001]M^{-1} = S(1, -1) \cdot T(0, -h) = \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & -h \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & h \\ 0 & 0 & 1 \end{bmatrix}
有趣的事实

两个方向的变换矩阵恰好相同,而这个矩阵恰好是自逆的(M2=IM^2 = I)。

7. 实际应用​

7.1 Quartz绘图时的坐标变换​

在iOS开发时,通常使用 Core Graphics(Quartz)进行绘图操作,需要使用CGAffineTransform处理坐标变换。

struct CGAffineTransform {
CGFloat a, b, c, d;
CGFloat tx, ty;
};

CGAffineTransform 本质上就是 3×33 \times 3 齐次坐标矩阵的前两列(因为第三列恒为 (0,0,1)(0, 0, 1)):

M=[ab0cd0txty1]M = \begin{bmatrix} a & b & 0 \\ c & d & 0 \\ t_x & t_y & 1 \end{bmatrix}
提示
  1. Core Graphics使用行向量表示坐标,即 P′=PMP' = P M
  2. CGAffineTransformConcat(M1M_1, M2M_2)的效果是M′=M1∗M2M' = M_1 * M_2,表示的是矩阵的乘法
  3. CGAffineTransformTranslate/Scale/Rotate考虑了作用顺序的问题,例如Translate的效果是 M′=T(tx,ty)∗MinM' = T(t_x, t_y) * M_{in},相当于Concat(T(tx,ty),Min)\text{Concat}(T(t_x, t_y), M_{in})
\(x...
\(y...
\(y...
\(y...
\(x...
\(x...
Text is not SVG - cannot display

该图分几步绘制而成,实现逻辑如下:

override func draw(_ rect: CGRect) {
let w = rect.width
let h = rect.height
// (300.0, 300.0)

UIColor(white: 0.9, alpha: 1.0).setFill()
UIGraphicsGetCurrentContext()?.fill(rect)

// 1: default coordinate axes
guard let ctx = UIGraphicsGetCurrentContext() else { return }
//CTM: [3, 0, 0, -3, 0, 900]
drawAxes(in: ctx, color: .yellow)

// 2: translate
ctx.translateBy(x: floor(w / 4), y: floor(h / 4))

//CTM: [3, 0, 0, -3, 225, 675]
drawAxes(in: ctx, color: .red)

// 3: rotate -> translate -> scale
ctx.rotate(by: .pi / 6.0)
ctx.translateBy(x: 115, y: 0.0)
ctx.scaleBy(x: 0.5, y: 0.5)

//CTM: [1.299038105676658, -0.74999999999999989,
// -0.74999999999999989, -1.299038105676658,
// 523.77876430563128, 502.5]
drawAxes(in: ctx, color: .green.withAlphaComponent(0.5))
}

假设屏幕scale是3,当前view的大小是(300,300)(300, 300)。

  • 绘制黄色坐标轴时的CTM
M1=[1000−1003001]×[300030001]=[3000−3009001]\begin{aligned} M_1 &= \begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 300 & 1 \end{bmatrix} \times \begin{bmatrix} 3 & 0 & 0 \\ 0 & 3 & 0 \\ 0 & 0 & 1 \end{bmatrix} \\ &= \begin{bmatrix} 3 & 0 &0 \\ 0 & -3 &0 \\ 0 &900 &1 \end{bmatrix} \end{aligned}

在调用drawRect前,UIKit会对CTM进行如下处理:

  1. 点到像素的缩放变换
  2. Y轴翻转变换
  1. 绘制红色坐标轴时的CTM
M2=[10001075751]×[3000−3009001]=[3000−302256751]\begin{aligned} M_{2} & = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 75 & 75 & 1 \end{bmatrix} \times \begin{bmatrix} 3 & 0 &0\\ 0 & -3 &0\\ 0 &900 &1 \end{bmatrix} \\ & = \begin{bmatrix}3 & 0 &0\\0 & -3 &0\\225 &675 &1\end{bmatrix} \\ \end{aligned}

在前一步的基本上对CTM进行一个平移变换

  1. 绘制绿色坐标轴时的CTM
M3=[0.50000.50001]×[10001011501]×[cos⁡π6sin⁡π60−sin⁡π6cos⁡π60001]×M2=[1.299038−0.750−0.75−1.2990380523.7788502.51]\begin{aligned} M_{3} &= \begin{bmatrix} 0.5 & 0 & 0 \\ 0 & 0.5 & 0 \\ 0 & 0 & 1 \end{bmatrix} \times \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 115 & 0 & 1 \end{bmatrix} \times \begin{bmatrix} \cos\frac{\pi}{6} & \sin\frac{\pi}{6} & 0 \\ -\sin\frac{\pi}{6} & \cos\frac{\pi}{6} & 0 \\ 0 & 0 & 1 \end{bmatrix} \times M_2 \\ &= \begin{bmatrix} 1.299038 & -0.75 & 0 \\ -0.75 & -1.299038 & 0 \\ 523.7788 & 502.5 & 1 \end{bmatrix} \end{aligned}

在前一步的基础上进行旋转、平移、缩放

提示

每一次对CTM的操作,都可以看到对绘图的局部坐标系进行了变换。

7.2 UIView/CALayer的坐标变换​

UIView 与坐标变换有关的属性:

  • transform :类型是 CGAffineTransform,对应二维仿射变换,是三维变换的子集
  • transform3D:类型是 CATransform3D,对应三维变换
  • anchorPoint:类型是 CGPoint,变换的基准点

CALayer与坐标变换有关的属性:

  • affineTransform:类型是CGAffineTransform,对应二维仿射变换,是三维变换的子集
  • transform类型是 CATransform3D,对应三维变换
  • anchorPoint:类型是 CGPoint,变换的基准点
提示

UIKit坐标体系中的Y轴是向下的,即相对于笛卡尔坐标系存在一个Y的翻转,因此旋转是顺时针的。