矩阵的几何应用-二维变换

矩阵的几何应用-二维变换

1 基本概念

在几何应用中,矩阵是一个非常强大的工具,广泛用于各种变换和操作。以下是矩阵在几何应用中的主要知识点:

1.1 点和向量

在几何中,点和向量可以用矩阵表示。
例如,二维点 (x, y) 可以表示为列向量:
$$\begin{bmatrix} x \\ y \end{bmatrix}$$

1.2 齐次坐标(Homogeneous Coordinate)

1.2.1 定义

齐次坐标是通过在普通坐标后面添加一个额外的维度来表示的,即将一个原本是n维的向量用一个n+1维向量来表示。

例如:二维点 $(x,y)$ 对应的齐次坐标表示为:
$$\begin{bmatrix} x \\ y \\ 1 \end{bmatrix}$$

可以定义成: $(x_h,y_h,h)$
$$\Biggl\{ \begin{matrix}x_h=h\cdot x\\ y_h=h\cdot y\\ h \neq 0 \end{matrix}$$

1.2.2 归一化

齐次坐标 $(x_h,y_h,h)$ 可以通过除以 h 转换回普通坐标 $(x,y)$

一个向量的齐次表示并不是唯一的,齐次坐标的h取不同的值都表示的是同一个点,比如齐次坐标(8,4,2)、(4,2,1)表示的都是二维点(4,2);

1.2.3 齐次坐标的优势

使用齐次坐标可以将平移变换表示为矩阵乘法,从而统一各种几何变换的表示形式。
若将二维点 (x,y) 分别在X和Y轴上平移 (1.5,2.5) 单位,则表示为:
$$\begin{bmatrix}1&0&1.5\\ 0&1&2.5\\ 0&0&1 \end{bmatrix} \cdot \begin{bmatrix}x\\ y\\ 1 \end{bmatrix}$$

为什么需要齐次坐标?
直接使用各个形式不统一的矩阵,会带来很多的不便:

  1. 由于形式不统一,进行多种变换时,计算量会很大;
  2. 同时,运算的形式也不统一,平移为相加,旋转缩放为相乘,使用齐次坐标可以将所有变换运算形式保持一致;
  3. 齐次坐标可以表示无穷远点,这对于投影变换特别重要;

2 基本变换

import matplotlib.pyplot as plt
import numpy as np
import copy
import math
sin = math.sin
cos = math.cos

from matplotlib.gridspec import GridSpec

# 黑暗模式
plt.style.use('dark_background')

# F 图形的齐次坐标
F = np.array([
    [  0,   0, 1],
    [  0,   2, 1],
    [  1,   2, 1],
    [  1, 1.5, 1],
    [0.5, 1.5, 1],
    [0.5,   1, 1],
    [  1,   1, 1],
    [  1, 0.5, 1],
    [0.5, 0.5, 1],
    [0.5,   0, 1],
    [  0,   0, 1]])

# 对 shape 执行变换
def apply(shape,*transforms):
    result = shape.tolist()
    for transform in transforms:
        # 对每一个点(齐次坐标)进行变换
        tmp = []
        for p in result:
            tmp.append(transform @ p)
        result = np.array(tmp)
    # 转化为 np.array 对象
    return np.array(result)

# 展示接口 datas[] = { ax, title, x, y, xlable, ylable, shapes:[] }
def show(*datas):
    for data in datas:
        # 参数
        ax = data['ax']
        x = data['x']
        y = data['y']
        # 设置标题和标签
        if 'title' in data:
            ax.set_title(data['title'])
        if 'xlable' in data:
            ax.set_xlabel(data['xlable'])
        if 'xlable' in data:
            ax.set_ylabel(data['ylable'])
        # 设置x轴和y轴的刻度
        ax.set_xticks(np.arange(math.floor(x[0]),math.ceil(x[1]),x[2]))
        ax.set_yticks(np.arange(math.floor(y[0]),math.ceil(y[1]),y[2]))
        # 设置坐标轴范围
        ax.set_xlim(x[0],x[1])
        ax.set_ylim(y[0],y[1])
        # 绘制x轴和y轴
        ax.axhline(y=0, linewidth=0.6)  # y=0 轴
        ax.axvline(x=0, linewidth=0.6)  # x=0 轴
        # 设置网格
        ax.grid(which='both',linewidth=0.3)
        # 设置坐标轴的纵横比为1,使x轴和y轴的长度一致
        ax.set_aspect('equal', adjustable='box')
        # 绘制
        for shape in data['shapes']:
            # 颜色
            if 'color' in shape:
                color = shape['color']
            else:
                color = 'gray'
            # 线类型
            if 'line' in shape:
                line = shape['line']
            else:
                line = '-'
            # 标签
            if 'lable' in shape:
                lable = shape['lable']
                pos = shape['shape'][lable[0]]
                ax.text(pos[0], pos[1]+0.1, lable[1], color='white', fontsize=lable[2])
            # 绘制
            if 'fill' in shape and shape['fill'] == True:
                ax.fill(
                    shape['shape'][:, 0],
                    shape['shape'][:, 1],
                    color=color)
            else:
                ax.plot(
                    shape['shape'][:, 0],
                    shape['shape'][:, 1],
                    color=color,
                    linestyle=line)
            # 锚点
            if ('noanchor' not in shape) or (shape['noanchor'] != True):
                anchor = shape['shape'][0]
                ax.plot(anchor[0], anchor[1], 'ro')

2.1 平移 (Translation)

将坐标点$(x,y)$平移到$(x+t_x,y+t_y)$,对应的平移矩阵为:
$$\mathbf{T} = \mathbf{T(t_x,t_y)} = \begin{bmatrix}1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1\end{bmatrix}$$

其中 $t_x$ 和 $t_y$ 是平移量。

逆平移变换矩阵为:
$$\mathbf{T^{-1}} = \mathbf{T(-t_x,-t_y)} = \begin{bmatrix}1&0&-t_x\\ 0&1&-t_y\\ 0&0&1 \end{bmatrix}$$
$$\mathbf{T·T^{-1}}=\begin{bmatrix}1&0&t_x-t_x\\ 0&1&t_y-t_y\\ 0&0&1 \end{bmatrix}=\begin{bmatrix}1&0&0\\ 0&1&0\\ 0&0&1 \end{bmatrix}$$

平移变换具有可加性:
$$\mathbf{T(t_{x2},t_{y2})·T(t_{x1},t_{y1})=T(t_{x1}+t_{x2},t_{y1}+t_{y2})}$$

如果对坐标点 $(x,y)$ 平移 $(t_x,t_y)$,则表示如下:
$$\mathbf{T(t_x,t_y)}\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}=\begin{bmatrix}x+t_x\\ y+t_y\\ 1 \end{bmatrix}$$

fig, ax = plt.subplots(figsize=(6,6))

# 定义一个平移矩阵(x轴上右移0.4,y轴上下移-1.1)
tx,ty = 0.4,-1.1
translation = np.array([
    [1,0,tx],
    [0,1,ty],
    [0,0, 1]])

# 应用变换,并显示
S = apply(F, translation)
show({
    'ax':ax,
    'x':(-2.5, 2.5, 1),
    'y':(-2.5, 2.5, 0.5),
    'xlable':'x',
    'ylable':'y',
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
下图为一个在 $(0,0)$ 点的 F 形状,使用平移矩阵相乘,即平移 $(0.4, -1.1)$ 后的表现:
png

2.2 缩放 (Scaling)

将坐标点$(x,y)$缩放到$(s_xx,s_yy)$,对应的缩放矩阵为:
$$\mathbf{S} = \mathbf{S(s_x,s_y)} = \begin{bmatrix}s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1\end{bmatrix}$$

其中 $s_x$ 和 $s_y$ 是缩放因子。

逆缩放变换矩阵为:
$$\mathbf{S^{-1}} = \mathbf{S(\frac{1}{s_x},\frac{1}{s_y})} = \begin{bmatrix}\frac{1}{s_x}&0&0\\ 0&\frac{1}{s_y}&0\\ 0&0&1 \end{bmatrix}$$
$$\mathbf{S·S^{-1}} = \begin{bmatrix}\frac{s_x}{s_x}&0&0\ 0&\frac{s_y}{s_y}&0\\ 0&0&1 \end{bmatrix}=\begin{bmatrix}1&0&0\\ 0&1&0\\ 0&0&1 \end{bmatrix}$$

放缩变换具有可乘性:
$$\mathbf{S(s_{x2},s_{y2})·S(s_{x1},s_{y1})=S(s_{x1}s_{x2},s_{y1}s_{y2})}$$

如果对坐标点 $(x,y)$ 缩放 $(s_x,s_y)$,则表示如下:
$$\mathbf{S_(s_x,s_y)}\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}=\begin{bmatrix}s_xx\\ s_yy\\ 1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))

# 定义一个缩放矩阵
sx,sy = 1.5,0.7
scaling = np.array([
    [sx, 0, 0],
    [ 0,sy, 0],
    [ 0, 0, 1]])

# 应用变换,并显示
LF = S
LS = apply(LF, scaling)
S = apply(F, scaling)
show({
    'ax':ax[0],
    'x':(-2.5, 2.5, 1),
    'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':LF,'color':'white','line':'--'},
        {'shape':LS,'fill':True},
    ]},{
    'ax':ax[1],
    'x':(-2.5, 2.5, 1),
    'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
左图为一个在坐标 $(0.4,-1.1)$ 的F图形,直接应用缩放矩阵 $scaling(sx,sy)$ 后的表现;
右图为一个在原点 $(0,0)$ 的F图形,应用缩放矩阵 $scaling(sx,sy)$ 后的表现。
png

2.3 旋转 (Rotation)

将坐标点$(x,y)$围绕原点 $(0,0)$ 旋转到 $\theta$ 角度,对应的旋转矩阵为:
$$\mathbf{R} = \mathbf{R(\theta)} = \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1\end{bmatrix}$$

旋转后的点为: $(x·cos\theta-y·sin\theta, x·sin\theta+y·cos\theta)$

逆旋转变换矩阵为:
$$\mathbf{R^{-1}} = \mathbf{R(-\theta)} = \begin{bmatrix}cos\theta&sin\theta&0\\ -sin\theta&cos\theta&0\\ 0&0&1 \end{bmatrix}$$
$$\mathbf{RR^{-1}}=\begin{bmatrix}cos^2\theta+sin^2\theta&cos\theta sin\theta-sin\theta cos\theta&0\\ sin\theta cos\theta-cos\theta sin\theta&sin^2\theta+cos^2\theta&0\\ 0&0&1 \end{bmatrix}=\begin{bmatrix}1&0&0\\ 0&1&0\\ 0&0&1\end{bmatrix}$$

平移和旋转变换具有可加性:
$$\mathbf{R(\theta_2)·R(\theta_1)=R(\theta_1+\theta_2)}$$

如果对坐标点 $(x,y)$ 以$(0,0)$为中心,旋转 $\theta$,则表示如下:
$$\mathbf{R(\theta)}\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}w\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}x·cos\theta-y·sin\theta\\ x·sin\theta+y·cos\theta\\ 1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))

# 定义一个旋转矩阵
θ = math.radians(-45)
roatation = np.array([
    [cos(θ),-sin(θ), 0],
    [sin(θ), cos(θ), 0],
    [     0,      0, 1]])

# 应用变换,并显示
LF = S
LS = apply(LF, roatation)
S = apply(F, roatation)
show({
    'ax':ax[0],
    'x':(-2.5, 2.5, 1), 'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':LF,'color':'white','line':'--'},
        {'shape':LS,'fill':True},
    ]},{
    'ax':ax[1],
    'x':(-2.5, 2.5, 1), 'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
下图为一个在  $(0,0)$ 点的 F 形状,使用旋转矩阵相乘,即旋转 -45 度后的表现:
png

2.4 反射 (Reflection)

反射是一种将点映射到对称轴或对称平面另一侧的变换。
反射矩阵是一种特殊的线性变换矩阵,用于将点反射到某个轴或平面上。
反射矩阵的形式取决于反射的轴或平面。

反射矩阵在计算机图形学、物理学和工程学中有广泛的应用。

例如,在计算机图形学中,反射矩阵用于生成镜像效果;在物理学中,反射矩阵用于描述光线或波的反射行为。

通过使用这些反射矩阵,可以方便地对几何图形进行反射变换,从而实现各种复杂的几何操作。
以下是关于矩阵在几何反射中的应用的详细解释:

2.4.1 相对于 x 轴的反射

反射关于x轴的矩阵将点 $(x, y)$ 映射到 $(x, -y)$。
对应的反射矩阵为:
$$\mathbf{R_x} = \begin{bmatrix}1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix}$$

如果对坐标点 $(x,y)$ 相对于X轴反射,则表示如下:
$$\mathbf{R_x}\begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}1&0&0\\ 0&-1&0\\ 0&0&1 \end{bmatrix} \begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}x\\ -y\\ 1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))

# 定义一个相对于X轴的反射矩阵
reflectionX = np.array([
    [1, 0, 0],
    [0,-1, 0],
    [0, 0, 1]])

# 应用变换,并显示
LF = S
LS = apply(LF, reflectionX)
S = apply(F, reflectionX)
show({
    'ax':ax[0],
    'x':(-2.5, 2.5, 1), 'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':LF,'color':'white','line':'--'},
        {'shape':LS,'fill':True},
    ]},{
    'ax':ax[1],
    'x':(-2.5, 2.5, 1), 'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
png

2.4.2 反射关于y轴

反射关于y轴的矩阵将点 $(x, y)$ 映射到 $(-x, y)$。
对应的反射矩阵为:
$$\mathbf{R_y} = \begin{bmatrix}-1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}$$

如果对坐标点 $(x,y)$ 相对于X轴反射,则表示如下:
$$\mathbf{R_y}\begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}-1&0&0\\ 0&1&0\\ 0&0&1 \end{bmatrix} \begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}-x\\ y\\ 1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))

# 定义一个相对于Y轴的反射矩阵
reflectionY = np.array([
    [-1, 0, 0],
    [ 0, 1, 0],
    [ 0, 0, 1]])

# 应用变换,并显示
LF = S
LS = apply(LF, reflectionY)
S = apply(F, reflectionY)
show({
    'ax':ax[0],
    'x':(-2.5, 2.5, 1),
    'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':LF,'color':'white','line':'--'},
        {'shape':LS,'fill':True},
    ]},{
    'ax':ax[1],
    'x':(-2.5, 2.5, 1),
    'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
png

2.4.3 反射关于原点

反射关于原点的矩阵将点 $(x, y)$ 映射到 $(-x, -y)$。
对应的反射矩阵为:
$$\mathbf{R_o} = \begin{bmatrix}-1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix}$$

如果对坐标点 $(x,y)$ 相对于原点反射,则表示如下:
$$\mathbf{R_o}\begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}-1&0&0\\ 0&-1&0\\ 0&0&1 \end{bmatrix} \begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}-x\\ -y\\ 1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))

# 定义一个相对于原点的反射矩阵
reflectionO = np.array([
    [-1, 0, 0],
    [ 0,-1, 0],
    [ 0, 0, 1]])

# 应用变换,并显示
LF = S
LS = apply(LF, reflectionO)
S = apply(F, reflectionO)
show({
    'ax':ax[0],
    'x':(-2.5, 2.5, 1),
    'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':LF,'color':'white','line':'--'},
        {'shape':LS,'fill':True},
    ]},{
    'ax':ax[1],
    'x':(-2.5, 2.5, 1),
    'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
png

2.4.4 反射关于y = x

反射关于直线 $y = x$ 的矩阵将点 $(x, y)$ 映射到 $(y, x)$。
对应的反射矩阵为:
$$\mathbf{R_{y=x}} = \begin{bmatrix}0 & 1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1\end{bmatrix}$$

如果对坐标点 $(x,y)$ 相对于$x=y$反射,则表示如下:
$$\mathbf{R_{y=x}}\begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}0&1&0\\ 1&0&0\\ 0&0&1 \end{bmatrix} \begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}y\\ x\\ 1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))

# 定义一个相对于y=x的反射矩阵
reflectionYX = np.array([
    [ 0, 1, 0],
    [ 1, 0, 0],
    [ 0, 0, 1]])

# 应用变换,并显示
LF = S
LS = apply(LF, reflectionYX)
S = apply(F, reflectionYX)
show({
    'ax':ax[0],
    'x':(-2.5, 2.5, 1),
    'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':LF,'color':'white','line':'--'},
        {'shape':LS,'fill':True},
    ]},{
    'ax':ax[1],
    'x':(-2.5, 2.5, 1),
    'y':(-2.5, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
png

2.4.5 反射关于任意直线

对于反射关于任意直线 $y = mx + c$,我们需要进行以下步骤:

  1. 平移:将直线平移到通过原点的位置。
    $$\mathbf{T} = \mathbf{T(0,-c)} = \begin{bmatrix}1&0&0 \\ 0&1&-c \\ 0&0&1 \end{bmatrix}$$

  2. 旋转:将直线旋转到与x轴对齐的位置。

假设直线的斜率为 $m$,则旋转角度 $\theta$ 满足 $\tan(\theta) = m$。

$$\mathbf{R} = \mathbf{R(-\theta)} = \begin{bmatrix} \cos(-\theta) & -\sin(-\theta) & 0\\ \sin(-\theta) & \cos(-\theta) & 0\\ 0&0&1\end{bmatrix} = \begin{bmatrix}\cos(\theta) & \sin(\theta) & 0\\ -\sin(\theta) & \cos(\theta) & 0\\ 0&0&1\end{bmatrix}$$

  1. 反射:使用反射关于x轴的矩阵。
    $$\mathbf{R_x} = \begin{bmatrix}1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1\end{bmatrix}$$

  2. 逆旋转:将直线旋转回原来的角度。
    $$\mathbf{R^{-1}} = \mathbf{R(\theta)} = \begin{bmatrix}\cos(\theta) & -\sin(\theta) & 0 \\ \sin(\theta) & \cos(\theta) & 0 \\ 0 & 0 & 1 \end{bmatrix}$$

  3. 逆平移:将直线平移回原来的位置。
    $$\mathbf{T^{-1}} = \mathbf{T(0,c)} = \begin{bmatrix}1&0&0 \\ 0&1&c \\ 0&0&1\end{bmatrix}$$

反射关于任意直线的矩阵 $R_{line}$ 可以表示为:
$$\mathbf{R_{line}} = \mathbf{T^{-1}} \cdot \mathbf{R(-\theta)} \cdot \mathbf{R_x} \cdot \mathbf{R(\theta)} \cdot \mathbf{T}$$

如果对坐标点 $(x,y)$ 相对于原点反射,则表示如下:
$$\mathbf{R_{line}}\begin{bmatrix}x\\ y\\ 1 \end{bmatrix}$$
$$=\begin{bmatrix}1&0&0\\ 0&1&c\\ 0&0&1\end{bmatrix}\begin{bmatrix}\cos(\theta)&-\sin(\theta)&0\\ \sin(\theta)&\cos(\theta)&0\\ 0&0&1\end{bmatrix}\begin{bmatrix}1&0&0\\ 0&-1&0\\ 0&0&1 \end{bmatrix}\begin{bmatrix}\cos(\theta)&\sin(\theta)&0\\ -\sin(\theta)&\cos(\theta)&0\\ 0&0&1\end{bmatrix}\begin{bmatrix}1&0&0\\ 0&1&-c\\ 0&0&1\end{bmatrix} \cdot \begin{bmatrix}x\\ y\\ 1\end{bmatrix}$$
$$=\begin{bmatrix}\cos(2\theta)&\sin(2\theta)&-c\sin(2\theta)\\ \sin(2\theta)&-\cos(2\theta)&c(1+\cos2\theta)\\ 0&0&1\end{bmatrix} \cdot \begin{bmatrix}x\\ y\\ 1\end{bmatrix}$$
$$=\frac{1}{1+m^2} \cdot \begin{bmatrix}1-m^2&2m&-2mc\\ 2m&m^2-1&2c\\ 0&0&1+m^2\end{bmatrix} \cdot \begin{bmatrix}x\\ y\\ 1\end{bmatrix}$$

fig,ax = plt.subplots(3, 2, figsize=(12,17))

# 直线 y = mx + c
m,c = 1.5,0.5
x = np.linspace(-5, 5, 10)
y = m*x + c
ax[2][1].plot(x, y, '--')
ax[2][1].text(-0.7, -0.7*m+c, f"  y={m}x + {c}", color="yellow", fontsize=12)

# 定义各个分解步骤的变换矩阵
θ = math.atan(m)
r = np.array([
    [ cos(θ), sin(θ), 0],
    [-sin(θ), cos(θ), 0],
    [      0,      0, 1]])
ri = np.array([
    [ cos(θ),-sin(θ), 0],
    [ sin(θ), cos(θ), 0],
    [      0,      0, 1]])
t = np.array([
    [1, 0, 0],
    [0, 1,-c],
    [0, 0, 1]])
ti = np.array([
    [1, 0, 0],
    [0, 1, c],
    [0, 0, 1]])
rx = np.array([
    [1, 0, 0],
    [0,-1, 0],
    [0, 0, 1]])

# 按步骤顺序进行变换
# 1.平移到原点
S1 = apply(F, t)
# 2.旋转到与X轴对齐
S2 = apply(S1, r)
# 3. 以x轴反射
S3 = apply(S2, rx)
# 4. 逆旋转
S4 = apply(S3, ri)
# 5. 逆平移
S5 = apply(S4, ti)

# 采用连续变换矩阵计算
S = apply(F, np.array([
    [cos(2*θ),  sin(2*θ),    -c*sin(2*θ)],
    [sin(2*θ), -cos(2*θ), c*(1+cos(2*θ))],
    [       0,         0,              1]]))

# 显示
show({
    'ax':ax[0][0],
    'title':'(1) Translation',
    'x':(-1, 2.5, 1), 'y':(-1, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'fill':True},
    ]},{
    'ax':ax[0][1],
    'title':'(2) Rotation',
    'x':(-1, 2.5, 1), 'y':(-1, 2.5, 0.5),
    'shapes':[
        {'shape':S1,'color':'white','line':'--'},
        {'shape':S2,'fill':True},
    ]},{
    'ax':ax[1][0],
    'title':'(3) Reflection-X',
    'x':(-1, 2.5, 1), 'y':(-1, 2.5, 0.5),
    'shapes':[
        {'shape':S2,'color':'white','line':'--'},
        {'shape':S3,'fill':True},
    ]},{
    'ax':ax[1][1],
    'title':'(4) Inverse Rotation',
    'x':(-1, 2.5, 1), 'y':(-1, 2.5, 0.5),
    'shapes':[
        {'shape':S3,'color':'white','line':'--'},
        {'shape':S4,'fill':True},
    ]},{
    'ax':ax[2][0],
    'title':'(5) Inverse Translation',
    'x':(-1, 2.5, 1), 'y':(-1, 2.5, 0.5),
    'shapes':[
        {'shape':S4,'color':'white','line':'--'},
        {'shape':S5,'fill':True},
    ]},{
    'ax':ax[2][1],
    'title':'(6) Result',
    'x':(-1, 2.5, 1), 'y':(-1, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})

# 调整子图间的间距
#plt.subplots_adjust(left=0.1, right=0.7, top=0.7, bottom=0.1, hspace=0.5, wspace=0)
plt.tight_layout()
png

2.5. 剪切 (Shearing)

在二维几何变换中,剪切(Shear)变换是一种将图形沿某个方向拉伸或压缩的操作。它可以分为水平剪切和垂直剪切。

$$\mathbf{H} = \mathbf{H(sh_x,sh_y)} = \begin{bmatrix}1 & sh_x & 0 \\ sh_y & 1 & 0 \\ 0 & 0 & 1\end{bmatrix}$$

其中 $sh_x$ 和 $sh_y$ 是剪切因子。

逆剪切变换矩阵为:
$$\mathbf{H^{-1}} = \mathbf{H(-sh_x,-sh_y)} = \begin{bmatrix}1 & -sh_x & 0 \\ -sh_y & 1 & 0 \\ 0 & 0 & 1\end{bmatrix}$$
$$\mathbf{HH^{-1}}=\begin{bmatrix}1-sh_x\cdot sh_y & 0 & 0 \\ 0 & 1-sh_x\cdot sh_y & 0 \\ 0&0&1 \end{bmatrix}$$

逆剪切变换后,原点在x轴和y轴上同时被缩放了 $1-sh_x\cdot sh_y$ 倍,需要再对其进行逆缩放变换 $S(\frac{1}{1-sh_x\cdot sh_y}, \frac{1}{1-sh_x\cdot sh_y})$。

剪切变换可以分为水平剪切和垂直剪切。以下是剪切变换的应用及其矩阵表示:

2.5.1 水平剪切(Horizontal Shear)

水平剪切变换会将图形沿 x 轴方向拉伸或压缩,而 y 轴方向保持不变。其变换矩阵如下:
$$\mathbf{H(sh_x,0)} = \begin{bmatrix}1 & sh_x & 0\\ 0 & 1 & 0\\ 0 & 0 & 1\end{bmatrix}$$

其中,$ sh_x $ 是水平剪切系数。
如果 $ sh_x > 0 $,图形会向右剪切;
如果 $ sh_x < 0 $,图形会向左剪切。

如果对坐标点 $(x,y)$ ,经过水平剪切变换后的新坐标 $(x + sh_x \cdot y,y)$,则表示如下:

$$\mathbf{H(sh_x,0)}\begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}1&sh_x&0\\ 0&1&0\\ 0&0&1 \end{bmatrix}\begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}x + sh_x \cdot y\\ y\\ 1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))
shx1,shx2 = 0.6,-0.6

# 定义水平剪切矩阵
shearH1 = np.array([
    [1,shx1, 0],
    [0,   1, 0],
    [0,   0, 1]])
shearH2 = np.array([
    [1,shx2, 0],
    [0,   1, 0],
    [0,   0, 1]])

# 应用变换,并显示
S1 = apply(F, shearH1)
S2 = apply(F, shearH2)
show({
    'ax':ax[0],
    'x':(-1.5, 2, 1), 'y':(-1, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'fill':True},
    ]},{
    'ax':ax[1],
    'x':(-1.5, 2, 1), 'y':(-1, 2.5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S2,'fill':True},
    ]})
png

2.5.2 垂直剪切(Vertical Shear)

垂直剪切变换会将图形沿 y 轴方向拉伸或压缩,而 x 轴方向保持不变。其变换矩阵如下:
$$\mathbf{H(0,sh_y)} = \begin{bmatrix}1 & 0 & 0\\ sh_y & 1 & 0\\ 0 & 0 & 1\end{bmatrix}$$

其中,$ sh_y $ 是垂直剪切系数。
如果 $ sh_y > 0 $,图形会向上剪切;
如果 $ sh_y < 0 $,图形会向下剪切。

如果对坐标点 $(x,y)$ ,经过垂直剪切变换后的新坐标 $(x,y + sh_y \cdot x)$,则表示如下:

$$\mathbf{H(0,sh_y)}\begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}1&0&0\\ sh_y&1&0\\ 0&0&1 \end{bmatrix}\begin{bmatrix}x\\ y\\ 1 \end{bmatrix}=\begin{bmatrix}x\\ y + sh_y \cdot x\\ 1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))
shy1,shy2 = 0.6,-0.6

# 定义垂直剪切矩阵
shearV1 = np.array([
    [   1, 0, 0],
    [shy1, 1, 0],
    [   0, 0, 1]])
shearV2 = np.array([
    [   1, 0, 0],
    [shy2, 1, 0],
    [   0, 0, 1]])

# 应用变换,并显示
S1 = apply(F, shearV1)
S2 = apply(F, shearV2)
show({
    'ax':ax[0],
    'x':(-1.5, 2, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'fill':True},
    ]},{
    'ax':ax[1],
    'x':(-1.5, 2, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S2,'fill':True},
    ]})
png

3 逆变换(Inverse Transformation)

在计算机图形学和线性代数中是一个重要概念,主要用于撤销还原变换。

3.1 定义

逆变换是指找到一个变换,使得应用该变换后能够恢复到原始状态。对于一个变换矩阵 $ M $,其逆矩阵 $ M^{-1} $ 满足 $ M \cdot M^{-1} = I $,其中 $ I $ 是单位矩阵。

3.2 性质

  • 逆矩阵的逆矩阵是原矩阵本身,即 $ (M{-1}){-1} = M $。
  • 逆矩阵的转置是转置矩阵的逆,即 $ (M{-1})T = (MT){-1} $。
  • 矩阵乘法的逆矩阵是各矩阵逆矩阵的乘积,且顺序相反,即 $ (AB)^{-1} = B{-1}A{-1} $。

3.3 存在性

  • 只有当矩阵是非奇异矩阵(即行列式不为零)时,逆矩阵才存在。

3.4 计算方法(二维变换)

3.4.1 平移变换的逆变换

$$\mathbf{T^{-1}} = \mathbf{T(-t_x,-t_y)} = \begin{bmatrix}1&0&-t_x\\ 0&1&-t_y\\ 0&0&1 \end{bmatrix}$$
$$\mathbf{T·T^{-1}}=\begin{bmatrix}1&0&t_x-t_x\\ 0&1&t_y-t_y\\ 0&0&1 \end{bmatrix}=\begin{bmatrix}1&0&0\\ 0&1&0\\ 0&0&1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))
t_x,t_y = 1.4,0.7

# 定义一个平移矩阵,及其逆矩阵
translation = np.array([
    [ 1, 0, t_x],
    [ 0, 1, t_y],
    [ 0, 0,   1]])
translation_i = np.array([
    [ 1, 0,-t_x],
    [ 0, 1,-t_y],
    [ 0, 0,   1]])

# 应用变换,并显示
S1 = apply(F, translation)
S2 = apply(S1, translation_i)
show({
    'ax':ax[0],
    'title':'Translation',
    'x':(-1, 3, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'fill':True},
    ]},{
    'ax':ax[1],
    'title':'Translation Inverse',
    'x':(-1, 3, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':S1,'color':'white','line':'--'},
        {'shape':S2,'fill':True},
    ]})
png

3.4.2 缩放变换的逆变换

$$\mathbf{S^{-1}} = \mathbf{S(\frac{1}{s_x},\frac{1}{s_y})} = \begin{bmatrix}\frac{1}{s_x}&0&0\\ 0&\frac{1}{s_y}&0\\ 0&0&1 \end{bmatrix}$$
$$\mathbf{S·S^{-1}} = \begin{bmatrix}\frac{s_x}{s_x}&0&0\\ 0&\frac{s_y}{s_y}&0\\ 0&0&1 \end{bmatrix}=\begin{bmatrix}1&0&0\\ 0&1&0\\ 0&0&1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))
sx,sy = 1.5,-0.3

# 定义一个缩放矩阵,及其逆矩阵
scaling = np.array([
    [  sx,   0, 0],
    [   0,  sy, 0],
    [   0,   0, 1]])
scaling_i = np.array([
    [1/sx,   0, 0],
    [   0,1/sy, 0],
    [   0,   0, 1]])

# 应用变换,并显示
S1 = apply(F, scaling)
S2 = apply(S1, scaling_i)
show({
    'ax':ax[0],
    'title':'Scaling',
    'x':(-1, 3, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'fill':True},
    ]},{
    'ax':ax[1],
    'title':'Scaling Inverse',
    'x':(-1, 3, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':S1,'color':'white','line':'--'},
        {'shape':S2,'fill':True},
    ]})
png

3.4.3 旋转变换的逆变换(逆时针旋转 θ 角度)

$$\mathbf{R^{-1}} = \mathbf{R(-\theta)} = \begin{bmatrix}cos\theta&sin\theta&0\\ -sin\theta&cos\theta&0\\ 0&0&1 \end{bmatrix}$$
$$\mathbf{RR^{-1}}=\begin{bmatrix}cos^2\theta+sin^2\theta&cos\theta sin\theta-sin\theta cos\theta&0\\ sin\theta cos\theta-cos\theta sin\theta&sin^2\theta+cos^2\theta&0\\ 0&0&1 \end{bmatrix}=\begin{bmatrix}1&0&0\\ 0&1&0\\ 0&0&1 \end{bmatrix}$$

fig,ax = plt.subplots(1, 2, figsize=(12,14))

# 定义一个缩放矩阵,及其逆矩阵
θ = math.radians(-60)
roatation = np.array([
    [ cos(θ),-sin(θ), 0],
    [ sin(θ), cos(θ), 0],
    [      0,      0, 1]])
roatation_i = np.array([
    [ cos(θ), sin(θ), 0],
    [-sin(θ), cos(θ), 0],
    [     0,       0, 1]])

# 应用变换
S1 = apply(F, roatation)
S2 = apply(S1, roatation_i)
show({
    'ax':ax[0],
    'title':'Roatation',
    'x':(-1, 3, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'fill':True},
    ]},{
    'ax':ax[1],
    'title':'Roatation Inverse',
    'x':(-1, 3, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':S1,'color':'white','line':'--'},
        {'shape':S2,'fill':True},
    ]})
png

3.4.4 剪切变换的逆变换

$$\mathbf{H^{-1}} = \mathbf{H(-sh_x,-sh_y)} = \begin{bmatrix}1 & -sh_x & 0 \\ -sh_y & 1 & 0 \\ 0 & 0 & 1\end{bmatrix}$$
$$\mathbf{HH^{-1}}=\begin{bmatrix}1-sh_x\cdot sh_y & 0 & 0 \\ 0 & 1-sh_x\cdot sh_y & 0 \\ 0&0&1 \end{bmatrix}$$

逆剪切变换后,原点在x轴和y轴上同时被缩放了 $1-sh_x\cdot sh_y$ 倍,需要再对其进行逆缩放变换 $S(\frac{1}{1-sh_x\cdot sh_y}, \frac{1}{1-sh_x\cdot sh_y})$。

$$\mathbf{S(\frac{1}{1-sh_x\cdot sh_y}, \frac{1}{1-sh_x\cdot sh_y})} \cdot \begin{bmatrix}1-sh_x\cdot sh_y & 0 & 0 \\ 0 & 1-sh_x\cdot sh_y & 0 \\ 0&0&1 \end{bmatrix}$$
$$=\begin{bmatrix}1&0&0\\ 0&1&0\\ 0&0&1 \end{bmatrix}$$

# 剪切因子
shx,shy = 0.9,-1.5

# 只应用水平上的剪切
shearH = np.array([
    [1, shx, 0],
    [0,   1, 0],
    [0,   0, 1]])
shearH_i = np.array([
    [1,-shx, 0],
    [0,   1, 0],
    [0,   0, 1]])
S1 = apply(F, shearH)
S2 = apply(S1, shearH_i)

# 定义一个垂直剪切矩阵
shearV = np.array([
    [   1, 0, 0],
    [ shy, 1, 0],
    [   0, 0, 1]])
shearV_i = np.array([
    [   1, 0, 0],
    [-shy, 1, 0],
    [   0, 0, 1]])
S3 = apply(F, shearV)
S4 = apply(S3, shearV_i)

# 定义一个剪切矩阵,其逆矩阵,及缩放矩阵
shear = np.array([
    [   1, shx, 0],
    [ shy,   1, 0],
    [   0,   0, 1]])
shear_i = np.array([
    [   1,-shx, 0],
    [-shy,   1, 0],
    [   0,   0, 1]])
scale = 1/(1-shx*shy)
scaling = np.array([
    [scale,    0, 0],
    [    0,scale, 0],
    [    0,    0, 1]])
S5 = apply(F, shear)
S6 = apply(S5, shear_i)
S7 = apply(S6, scaling)

# 创建图形和 GridSpec 对象
fig = plt.figure(figsize=(13,15))
gs = GridSpec(3, 6, figure=fig)
ax1 = fig.add_subplot(gs[0, :3])
ax2 = fig.add_subplot(gs[0, -3:])
ax3 = fig.add_subplot(gs[1, :3])
ax4 = fig.add_subplot(gs[1, -3:])
ax5 = fig.add_subplot(gs[2, 0:2])
ax6 = fig.add_subplot(gs[2, 2:4])
ax7 = fig.add_subplot(gs[2, 4:6])

# 显示
show({
    'ax':ax1,
    'title':'Horizontal shear',
    'x':(-1, 4, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'fill':True},
    ]},{
    'ax':ax2,
    'title':'Horizontal Shear Inverse',
    'x':(-1, 4, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':S1,'color':'white','line':'--'},
        {'shape':S2,'fill':True},
    ]},{
    'ax':ax3,
    'title':'Vertical shear',
    'x':(-1, 4, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S3,'fill':True},
    ]},{
    'ax':ax4,
    'title':'Vertical Shear Inverse',
    'x':(-1, 4, 1), 'y':(-1, 3, 0.5),
    'shapes':[
        {'shape':S3,'color':'white','line':'--'},
        {'shape':S4,'fill':True},
    ]},{
    'ax':ax5,
    'title':'Shear',
    'x':(-1, 4, 1), 'y':(-1, 5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S5,'fill':True},
    ]},{
    'ax':ax6,
    'title':'Shear Inverse',
    'x':(-1, 4, 1), 'y':(-1, 5, 0.5),
    'shapes':[
        {'shape':S5,'color':'white','line':'--'},
        {'shape':S6,'fill':True},
    ]},{
    'ax':ax7,
    'title':'Scaling',
    'x':(-1, 4, 1), 'y':(-1, 5, 0.5),
    'shapes':[
        {'shape':S6,'color':'white','line':'--'},
        {'shape':S7,'fill':True},
    ]})
plt.tight_layout()
#plt.subplots_adjust(left=0.1, right=0.5, top=0.7, bottom=0.1, hspace=0.5, wspace=0.2)
png

4 复合变换

4.1 组合变换

多个变换可以通过矩阵乘法组合在一起。例如,先旋转再平移的变换可以表示为 $T \cdot R$。

变换合成时,矩阵相乘的顺序:先作用的放在乘法组合的右端,后作用的放在乘法组合的左端;

连续变换时,先计算变换矩阵,再计算坐标;

  • 优点1:提高了对图形依次做多次变换的运算效率
    如:图形上有n个顶点 $Pi$,如果依次施加的变换为 $T$(先平移),$R$(后旋转),那么顶点$Pi$ 变换后的坐标为:

$\qquad\mathbf{P'_i=R(\theta)·T(t_x,t_y)·P_i}$

$\qquad\mathbf{P'_i=(R(\theta)·T(t_x,t_y))·P_i=T·P_i}$

  • 优点2:能构造复杂的变换矩阵
    对图形作较复杂的变换时,不直接去计算这个变换,而是将其先分解成多个基本变换,再合成总的变换。
    多个变换的组合,可通过单个变换矩阵来计算矩阵乘积;

4.2 连续变换

4.2.1 连续平移

平移向量为 $(t_{1x},t_{1y})$ 和 $(t_{2x},t_{2y})$,点 $P$ 经变换为$P´$,则有:

$$\mathbf{P'=T(t_{2x},t_{2y})\cdot \{T(t_{1x},t_{1y})\cdot P\}=\{T(t_{2x},t_{2y})\cdot T(t_{1x},t_{1y})\}·P}$$
$$\mathbf{T(t_{2x},t_{2y})\cdot T(t_{1x},t_{1y})}$$
$$= \begin{bmatrix}1&0&t_{2x}\\ 0&1&t_{2y}\\ 0&0&1 \end{bmatrix} \cdot \begin{bmatrix}1&0&t_{1x}\\ 0&1&t_{1y}\\ 0&0&1 \end{bmatrix}$$
$$=\begin{bmatrix}1&0&t_{1x}+t_{2x}\\ 0&1&t_{1y}+t_{2y}\\ 0&0&1 \end{bmatrix}$$
$$=\mathbf{T(t_{1x}+t_{2x},t_{1y}+t_{2y})}$$

# 定义连续平移矩阵,即一个复合矩阵
translation1 = np.array([
    [1,0, 1.5],
    [0,1, 0.7],
    [0,0,   1]])
translation2 = np.array([
    [1,0, 0.4],
    [0,1,-2.5],
    [0,0,   1]])
translation3 = np.array([
    [1,0,  -3],
    [0,1,  -1],
    [0,0,   1]])
translation4 = np.array([
    [1,0,  -2],
    [0,1, 1.7],
    [0,0,   1]])
compoundTranslation = translation4 @ translation3 @ translation2 @ translation1

S1 = apply(F, translation1)
S2 = apply(S1, translation2)
S3 = apply(S2, translation3)
S4 = apply(S3, translation4)
S = apply(F, compoundTranslation)

# 显示
fig,ax = plt.subplots(2, 1, figsize=(12,14))
show({
    'ax':ax[0],
    'title':'Translation Step By Step',
    'x':(-5, 5, 1), 'y':(-3, 3, 1),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'color':'#0066FF','line':'--','lable':(1,"1",15)},
        {'shape':S2,'color':'#00AAFF','line':'--','lable':(1,"2",15)},
        {'shape':S3,'color':'#00FFFF','line':'--','lable':(1,"3",15)},
        {'shape':S4,'fill':True,'lable':(1,"4",15)},
    ]},{
    'ax':ax[1],
    'title':'Compound Translation',
    'x':(-5, 5, 1), 'y':(-3, 3, 1),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
png

4.2.2 连续旋转

P 经连续旋转角度分别为 $\theta_1$ 和 $\theta_2$ 后:

$$\mathbf{P'=R(θ_2) \cdot {R(θ_1) \cdot P}=\{R(θ_2) \cdot R(θ_1)\} \cdot P}$$
$$\mathbf{R(θ_2) \cdot R(θ_1)}$$
$$= \begin{bmatrix}cos\theta_2&-sin\theta_2&0\\ sin\theta_2&cos\theta_2&0\\ 0&0&1 \end{bmatrix} \cdot \begin{bmatrix}cos\theta_1&-sin\theta_1&0\\ sin\theta_1&cos\theta_1&0\\ 0&0&1 \end{bmatrix}$$
$$= \begin{bmatrix}cos(\theta_1+\theta_2)&-sin(\theta_1+\theta_2)&0\\ sin(\theta_1+\theta_2)&cos(\theta_1+\theta_2)&0\\ 0&0&1 \end{bmatrix}$$
$$=\mathbf{R(\theta_1+\theta_2)}$$

# 定义连续旋转矩阵,即一个复合矩阵
θ = math.radians(-60)
roatation1 = np.array([
    [cos(θ),-sin(θ), 0],
    [sin(θ), cos(θ), 0],
    [     0,      0, 1]])
θ = math.radians(-45)
roatation2 = np.array([
    [cos(θ),-sin(θ), 0],
    [sin(θ), cos(θ), 0],
    [     0,      0, 1]])
θ = math.radians(180)
roatation3 = np.array([
    [cos(θ),-sin(θ), 0],
    [sin(θ), cos(θ), 0],
    [     0,      0, 1]])
θ = math.radians(-270)
roatation4 = np.array([
    [cos(θ),-sin(θ), 0],
    [sin(θ), cos(θ), 0],
    [     0,      0, 1]])
compoundRoatation = roatation4 @ roatation3 @ roatation2 @ roatation1

S1 = apply(F, roatation1)
S2 = apply(S1, roatation2)
S3 = apply(S2, roatation3)
S4 = apply(S3, roatation4)
S = apply(F, compoundRoatation)

# 显示
fig,ax = plt.subplots(2, 1, figsize=(12,14))
show({
    'ax':ax[0],
    'title':'Rotation Step By Step',
    'x':(-5, 5, 1), 'y':(-3, 3, 1),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'color':'#0066FF','line':'--','lable':(1,"1",15)},
        {'shape':S2,'color':'#00AAFF','line':'--','lable':(1,"2",15)},
        {'shape':S3,'color':'#00FFFF','line':'--','lable':(3,"3",15)},
        {'shape':S4,'fill':True,'lable':(3,"4",15)},
    ]},{
    'ax':ax[1],
    'title':'Compound Rotation',
    'x':(-5, 5, 1), 'y':(-3, 3, 1),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
png

4.2.3 连续缩放

连续放缩因子分别为(s1x, s1y) 和 (s2x, s2y)后:

$$\mathbf{P'=S(s_{2x},s_{2y})\cdot \{S(s_{1x},s_{1y})\cdot P\}=\{S(s_{2x},s_{2y})\cdot S(s_{1x},s_{1y})\}·P}$$
$$\mathbf{S(s_{2x},s_{2y}) \cdot S(s_{1x},s_{1y})}$$
$$= \begin{bmatrix}s_{2x}&0&0\\ 0&s_{2y}&0\\ 0&0&1 \end{bmatrix} \cdot \begin{bmatrix}s_{1x}&0&0\\ 0&s_{1y}&0\\ 0&0&1 \end{bmatrix}$$
$$= \begin{bmatrix}s_{1x} \cdot s_{2x}&0&0\\ 0&s_{1y} \cdot s_{2y}&0\\ 0&0&1 \end{bmatrix}$$
$$= S(s_{1x} \cdot s_{2x},s_{1y} \cdot s_{2y})$$

# 定义连续缩放矩阵,即一个复合矩阵
scaling1 = np.array([
    [ 0.5,   0, 0],
    [   0, 0.5, 0],
    [   0,   0, 1]])
scaling2 = np.array([
    [ 3.5,   0, 0],
    [   0, 3.5, 0],
    [   0,   0, 1]])
scaling3 = np.array([
    [ 1.7,   0, 0],
    [   0, 1.2, 0],
    [   0,   0, 1]])
scaling4 = np.array([
    [ 1.5,   0, 0],
    [   0, 1.1, 0],
    [   0,   0, 1]])
compoundScaling = scaling4 @ scaling3 @ scaling2 @ scaling1

S1 = apply(F, scaling1)
S2 = apply(S1, scaling2)
S3 = apply(S2, scaling3)
S4 = apply(S3, scaling4)
S = apply(F, compoundScaling)

# 显示
fig,ax = plt.subplots(2, 1, figsize=(12,14))
show({
    'ax':ax[0],
    'title':'Scaling Step By Step',
    'x':(-3, 7, 1), 'y':(-1, 5, 1),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S1,'color':'#0066FF','line':'--','lable':(1,"1",15)},
        {'shape':S2,'color':'#00AAFF','line':'--','lable':(1,"2",15)},
        {'shape':S3,'color':'#00FFFF','line':'--','lable':(1,"3",15)},
        {'shape':S4,'fill':True,'lable':(1,"4",15)},
    ]},{
    'ax':ax[1],
    'title':'Compound Scaling',
    'x':(-3, 7, 1), 'y':(-1, 5, 0.5),
    'shapes':[
        {'shape':F,'color':'white','line':'--'},
        {'shape':S,'fill':True},
    ]})
png

4.2.4 基于任意参照点的二维变换

任意点的旋转变换

步骤:

  1. 平移对象使参照(基准)点移到原点
  2. 绕坐标原点旋转
  3. 平移对象使基准点回到原始位置

其组成的复合矩阵为:
$$\mathbf{R(x_r,y_r;\theta)}$$
$$=T(x_r,y_r)·R(\theta)·T(-x_r,-y_r)$$
$$=\begin{bmatrix}1&0&x_r\\ 0&1&y_r\\ 0&0&1 \end{bmatrix}\begin{bmatrix}cos\theta&-sin\theta&0\\ sin\theta&cos\theta&0\\ 0&0&1 \end{bmatrix}\begin{bmatrix}1&0&-x_r\\ 0&1&-y_r\\ 0&0&1 \end{bmatrix}$$
$$=\begin{bmatrix}cos\theta&-sin\theta&x_r(1-cos\theta)+y_rsin\theta\\ sin\theta&cos\theta&y_r(1-cos\theta)-x_rsin\theta\\ 0&0&1 \end{bmatrix}$$

# 创建图形和 GridSpec 对象
fig = plt.figure(figsize=(14,14))
gs = GridSpec(3, 6, figure=fig)
ax1 = fig.add_subplot(gs[0, :3])
ax2 = fig.add_subplot(gs[0, -3:])
ax3 = fig.add_subplot(gs[1, 0:2])
ax4 = fig.add_subplot(gs[1, 2:4])
ax5 = fig.add_subplot(gs[1, 4:6])

# 初始位置
x,y = 2.6,-0.3
θ = math.radians(11)
S = apply(F,np.array([[cos(θ),-sin(θ),    0],
                      [sin(θ), cos(θ),    0],
                      [     0,      0,    1]]),
            np.array([[   1.4,      0,    0],
                      [     0,    1.3,    0],
                      [     0,      0,    1]]),
            np.array([[     1,      0,    x],
                      [     0,      1,    y],
                      [     0,      0,    1]]))

# Move To Origin
S1 = apply(S,np.array([[    1,      0,   -x],
                       [    0,      1,   -y],
                       [    0,      0,    1]]))

# Rotate {degrees} Degrees
degrees = -60
θ = math.radians(degrees)
S2 = apply(S1,np.array([[cos(θ),-sin(θ),    0],
                        [sin(θ), cos(θ),    0],
                        [     0,      0,    1]]))

# Backward Movement
S3 = apply(S2,np.array([[     1,      0,    x],
                        [     0,      1,    y],
                        [     0,      0,    1]]))

# Rotate {degrees} Degrees Directly
SS = apply(S,np.array([[cos(θ),-sin(θ),    0],
                       [sin(θ), cos(θ),    0],
                       [     0,      0,    1]]))

show({
    'ax':ax1,
    'title':'Original Position',
    'x':(-0.5, 5, 1), 'y':(-1, 3, 1),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
    ]},{
    'ax':ax2,
    'title':f'Rotate {degrees} Degrees Directly',
    'x':(-0.5, 5, 1), 'y':(-3.5, 0.5, 1),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
        {'shape':SS,'fill':True},
    ]},{
    'ax':ax3,
    'title':'Move To Origin',
    'x':(-0.5, 4, 1), 'y':(-2, 3, 0.5),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
        {'shape':S1,'fill':True},
    ]},{
    'ax':ax4,
    'title':f'Rotate {degrees} Degrees',
    'x':(-0.5, 4, 1), 'y':(-2, 3, 0.5),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
        {'shape':S2,'fill':True},
    ]},{
    'ax':ax5,
    'title':'Backward Movement',
    'x':(1.5, 6, 1), 'y':(-2, 3, 0.5),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
        {'shape':S3,'fill':True},
    ]})
png
任意点的缩放变换

步骤:

  1. 平移对象使基准点与坐标原点重合
  2. 缩放变换
  3. 反向平移使得基准点回到初始位置

其组成的复合矩阵为:
$$\mathbf{S(x_r,y_r;s_x,s_y)}$$
$$ =T(x_r,y_r) \cdot S(s_x,s_y) \cdot T(-x_r,-y_r)$$
$$=\begin{bmatrix}1&0&x_r\\ 0&1&y_r\\ 0&0&1 \end{bmatrix}\begin{bmatrix}s_x&0&0\\ 0&s_y&0\\ 0&0&1 \end{bmatrix}\begin{bmatrix}1&0&-x_r\\ 0&1&-y_r\\ 0&0&1 \end{bmatrix}$$
$$ =\begin{bmatrix}s_x&0&x_r(1-s_x)\\ 0&s_y&y_r(1-s_y)\\ 0&0&1 \end{bmatrix}$$

# 创建图形和 GridSpec 对象
fig = plt.figure(figsize=(14,14))
gs = GridSpec(3, 6, figure=fig)
ax1 = fig.add_subplot(gs[0, :3])
ax2 = fig.add_subplot(gs[0, -3:])
ax3 = fig.add_subplot(gs[1, 0:2])
ax4 = fig.add_subplot(gs[1, 2:4])
ax5 = fig.add_subplot(gs[1, 4:6])

# 初始位置
θ = math.radians(11)
S = apply(F,np.array([[cos(θ),-sin(θ),    0],
                      [sin(θ), cos(θ),    0],
                      [     0,      0,    1]]),
            np.array([[   1.4,      0,    0],
                      [     0,    1.4,    0],
                      [     0,      0,    1]]),
            np.array([[     1,      0,  2.6],
                      [     0,      1, -0.3],
                      [     0,      0,    1]]))

# Move To Origin
S1 = apply(S,np.array([[    1,      0,   -x],
                       [    0,      1,   -y],
                       [    0,      0,    1]]))

# Scale Up 1.3 Times Both X And Y Axes
sx,sy = 1.3,1.3
S2 = apply(S1,np.array([[    sx,      0,    0],
                        [     0,     sy,    0],
                        [     0,      0,    1]]))

# Backward Movement
S3 = apply(S2,np.array([[     1,      0,    x],
                        [     0,      1,    y],
                        [     0,      0,    1]]))

# Scale Up 1.3 Times Both X And Y Axes Directly
SS = apply(S,np.array([[   -sx,      0,    0],
                       [     0,    -sy,    0],
                       [     0,      0,    1]]))

show({
    'ax':ax1,
    'title':'Original Position',
    'x':(-0.5, 5, 1), 'y':(-1, 3, 1),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
    ]},{
    'ax':ax2,
    'title':f'Scale Up Both X And Y Axes Directly',
    'x':(-5.25, 0.25, 1), 'y':(-3.5, 0.5, 1),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
        {'shape':SS,'fill':True},
    ]},{
    'ax':ax3,
    'title':'Move To Origin',
    'x':(-0.5, 4, 1), 'y':(-1, 4, 0.5),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
        {'shape':S1,'fill':True},
    ]},{
    'ax':ax4,
    'title':f'Scale Up Both X And Y Axes',
    'x':(-1.5, 3, 1), 'y':(-1, 4, 0.5),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
        {'shape':S2,'fill':True},
    ]},{
    'ax':ax5,
    'title':'Backward Movement',
    'x':(1.5, 6, 1), 'y':(-1, 4, 0.5),
    'shapes':[
        {'shape':S,'color':'white','line':'--'},
        {'shape':S3,'fill':True},
    ]})
png

5 在2D游戏坐标系中的简单应用

在二维游戏开发中,理解世界坐标和本地坐标是非常重要的。这两种坐标系统帮助开发者控制和放置游戏中的对象,如角色、道具和场景元素等。下面我将详细解释这两种坐标系统:

# 定义一个精灵类 Sprite ,用表示在二维坐标系中的各种变换。
class Sprite:
    def __init__(self,shape,color='white',line='--',fill=False) -> None:
        self.parent:Sprite = None
        self.childs = []
        self.ref = shape
        self.color = color
        self.line = line
        self.fill = fill
        self.angle = 0
        self.translation = np.array([[1,0,0],[0,1,0],[0,0,1]],dtype=float)
        self.rotation = np.array([[1,0,0],[0,1,0],[0,0,1]],dtype=float)
        self.scaling = np.array([[1,0,0],[0,1,0],[0,0,1]],dtype=float)

    def copy(self,color=None,line=None,fill=None):
        new = copy.deepcopy(self)
        new.color = color if color is not None else self.color
        new.line = line if line is not None else self.line
        new.fill = fill if fill is not None else self.fill
        return new
    
    def add_child(self, child):
        if isinstance(child,Sprite):
            self.childs.append(child)
            child.parent = self

    def set_pos(self, x, y):
        """ 设置本地坐标 """
        self.translation[0][2] = x
        self.translation[1][2] = y
        return self
    
    def set_angle(self, angle):
        """ 设置本地旋转角度 """
        self.angle = angle
        θ = math.radians(self.angle)
        self.rotation[0][0],self.rotation[0][1] = cos(θ),-sin(θ)
        self.rotation[1][0],self.rotation[1][1] = sin(θ), cos(θ)
        return self

    def set_scale(self, sx, sy):
        """ 设置本地缩放 """
        self.scaling[0][0] = sx
        self.scaling[1][1] = sy
        return self
    
    def move(self, x, y):
        """ 移动(基于本地坐标系) """
        self.translation[0][2] += x
        self.translation[1][2] += y
        return self
        
    def rotate(self, angle):
        """ 旋转(基于本地坐标系) """
        self.angle += angle
        θ = math.radians(self.angle)
        self.rotation[0][0],self.rotation[0][1] = cos(θ),-sin(θ)
        self.rotation[1][0],self.rotation[1][1] = sin(θ), cos(θ)
        return self

    def scale(self, sx, sy):
        """ 缩放(基于本地坐标系) """
        self.scaling[0][0] *= sx
        self.scaling[1][1] *= sy
        return self
    
    def to_global(self, local_pos):
        """ 将本地坐标转化为基于世界坐标系的坐标 """
        local_pos = self.translation @ self.rotation @ self.scaling @ local_pos
        if self.parent is not None:
            local_pos = self.parent.to_global(local_pos)
        return local_pos

    def to_local(self, global_pos):
        """ 将世界坐标转化为基于本地坐标系的坐标 """
        if self.parent is not None:
            global_pos = self.parent.to_local(global_pos)
        inv = np.linalg.inv
        global_pos = inv(self.scaling) @ inv(self.rotation) @ inv(self.translation) @ global_pos
        return global_pos

    def set_global_pos(self, x, y):
        """ 设置全局坐标 """
        if self.parent is None:
            self.translation[0][2] = x
            self.translation[1][2] = y
        else:
            local = self.parent.to_local(np.array([x,y,1]))
            self.translation[0][2] = local[0]
            self.translation[1][2] = local[1]
        return self
    
    def move_global(self, x, y):
        """ 基于全局坐标系的移动 """
        if self.parent is None:
            self.translation[0][2] += x
            self.translation[1][2] += y
        else:
            pos = self.to_global(np.array([0,0,1]))
            pos[0] += x
            pos[1] += y
            local = self.parent.to_local(pos)
            self.translation[0][2] = local[0]
            self.translation[1][2] = local[1]
        return self
    
    def draw_coord(self):
        self.coord = [
            np.array([[-0.2,2.3,1],[0,2.5,1],[0.2,2.3,1],[0,2.5,1],[0,0,1]],dtype=float),
            np.array([[2.3,-0.2,1],[2.5,0,1],[2.3,0.2,1],[2.5,0,1],[0,0,1]],dtype=float)]
        return self
    
    def draw(self,ax,color=None,line=None,fill=None):
        # 先应用本地变换
        self.shape = apply(self.ref, self.scaling, self.rotation, self.translation)
        if hasattr(self,'coord'):
            self.coordY = apply(self.coord[0], self.scaling, self.rotation, self.translation)
            self.coordX = apply(self.coord[1], self.scaling, self.rotation, self.translation)
        # 紧接着引用父节点变换
        parent = self.parent
        while parent is not None:
            self.shape = apply(self.shape, parent.scaling, parent.rotation, parent.translation)
            if hasattr(self,'coord'):
                self.coordY = apply(self.coordY, parent.scaling, parent.rotation, parent.translation)
                self.coordX = apply(self.coordX, parent.scaling, parent.rotation, parent.translation)
            parent = parent.parent
        # 子节点变换
        for child in self.childs:
            child.draw(ax,color=color,line=line,fill=fill)
        # 绘制
        if (fill if fill is not None else self.fill) == True:
            ax.fill(self.shape[:,0],self.shape[:,1],color=color if color is not None else self.color)
        else:
            ax.plot(self.shape[:,0],self.shape[:,1], color=color if color is not None else self.color, linestyle=line if line is not None else self.line)
        # 绘制本地坐标系
        if hasattr(self,'coord'):
            ax.plot(self.coordY[:,0], self.coordY[:,1], color='yellow', linestyle='--')
            ax.plot(self.coordX[:,0], self.coordX[:,1], color='green', linestyle='--')
        # 绘制锚点
        ax.plot(self.shape[0][0], self.shape[0][1], 'ro', markersize=5)
    

5.1 世界坐标(World Coordinates)

世界坐标是指游戏世界的全局坐标系统。在这个坐标系统中,每一个位置都是相对于游戏世界的固定参考点(通常是游戏世界的原点)来定义的。

无论游戏中的相机如何移动或旋转,世界坐标系保持不变。

例如,如果你的游戏世界是一个大城市,那么每一个建筑物、街道或其他对象都有一个固定的世界坐标,这些坐标定义了它们在这个城市中的确切位置。

##########################################################################
# 定义图形的顶点坐标
F = F
U = np.array([[0,0,1],[0,0.5,1],[0.165,0.5,1],[0.165,0.25,1],[0.32,0.25,1],[0.32,0.5,1],[0.5,0.5,1],[0.5,0,1],[0,0,1]], dtype=float)
C = np.array([[0,0,1],[0,1,1],[1,1,1],[1,0.64,1],[0.5,0.64,1],[0.5,0.33,1],[1,0.33,1],[1,0,1],[0,0,1]], dtype=float)
# 初始化一个主 Sprite 对象 S
S = Sprite(F,color='gray',line='--',fill=True).draw_coord()
##########################################################################

# 创建显示
fig, ax = plt.subplots(figsize=(24,12))

# 初始化二维世界
show({'ax':ax, 'x':(-9, 16, 1), 'y':(-7, 7, 1), 'shapes':[]})

# 拷贝一个 Sprite 对象 S1
S1 = S.copy(color='gray').draw_coord()
S1.draw(ax,color='gray',fill=False)

# 坐标设置到 (9,2)
S1.set_pos(9, 2)
S1.draw(ax,color='#AAAAAA22',fill=True)

# 旋转 -40 度
S1.set_angle(-40)
S1.draw(ax,color='#AAAAAA44',fill=True)

# 向下移动 5 个单位
S1.move(0, -5)
S1.draw(ax,color='#AAAAAA66',fill=True)

# 左移 10 个单位后,放大 1.5 倍
S1.move(-10, 0).scale(1.5, 1.5)
S1.draw(ax,color='#AAAAAA88',fill=True)

# 向左移 5 个单位后,旋转 40 度,并缩小 1.5 倍
S1.move(-5, 0).rotate(40).scale(1/1.5, 1/1.5)
S1.draw(ax,color='#AAAAAADD',fill=True)
png

5.2 本地坐标(Local Coordinates)

本地坐标,又称为对象坐标或模型坐标,是相对于游戏中某个特定对象的坐标系统。这意味着每个对象都有自己的坐标系统,其原点通常位于对象的中心或一个特定的锚点。

在二维游戏中,如果你有一个角色(比如一个精灵sprite),它在自己的本地坐标系中可能会有不同的部件(如手臂、腿、头等),这些部件的位置是相对于角色本身而不是整个游戏世界。

# 创建显示
fig, ax = plt.subplots(figsize=(24,12))

# 初始化二维世界
show({'ax':ax, 'x':(-9, 16, 1), 'y':(-7, 7, 1), 'shapes':[]})

# 拷贝一个 Sprite 对象 S2
S2 = S.copy(color='gray').draw_coord()

# 为 S2 增加一个子对象 SC1,子对象坐标设置为 (1.5,1)
SC1 = Sprite(C).set_pos(1.5, 1)
S2.add_child(SC1)
S2.draw(ax,color='gray',fill=False)

# 坐标设置到 (9,2)
S2.set_pos(9, 2)
S2.draw(ax,color='#AAAAAA22',fill=True)

# 旋转 -40 度
S2.set_angle(-40)
S2.draw(ax,color='#AAAAAA44',fill=True)

# 向下移动 5 个单位
S2.move(0, -5)
S2.draw(ax,color='#AAAAAA66',fill=True)

# 左移 10 个单位后,放大 1.5 倍
S2.move(-10, 0).scale(1.5, 1.5)
S2.draw(ax,color='#AAAAAA88',fill=True)

# 向左移 5 个单位后,旋转 40 度,并缩小 1.5 倍
S2.move(-5, 0).rotate(40).scale(1/1.5, 1/1.5)
S2.draw(ax,color='#AAAAAADD',fill=True)
png

5.3 世界坐标与本地坐标的转换

在游戏开发中,经常需要在世界坐标和本地坐标之间进行转换。例如,当你想要在世界坐标中移动一个对象时,你需要将这个移动转换为对象的本地坐标系统,以便正确地更新其位置。

转换通常涉及到以下几个步骤:

  1. 平移(Translation):根据对象在世界中的位置移动坐标原点。
  2. 旋转(Rotation):根据对象的朝向旋转坐标轴。
  3. 缩放(Scaling):根据对象的大小比例调整坐标尺度。

理解这些坐标系统及其转换对于控制游戏中的动画、物理和其他交互是非常关键的。它们使得开发者能够更精确地控制游戏元素,提供更丰富、更动态的游戏体验。

# 创建显示
fig, ax = plt.subplots(figsize=(24,12))

# 初始化二维世界
show({'ax':ax, 'x':(-9, 16, 1), 'y':(-7, 7, 1), 'shapes':[]})

# 拷贝一个 Sprite 对象 S3
S3 = S.copy(color='gray').draw_coord()

# 为 S3 增加一个子对象 SC1,子对象坐标设置为 (1.5,1)
SC1 = Sprite(C).set_pos(1.5, 1)
S3.add_child(SC1)
S3.draw(ax,color='gray',fill=False)

# 为 SC1 增加一个子对象 SCC1,子对象坐标设置为 (1.5, 0)
SCC1 = Sprite(U).set_pos(1.5, 0.5)
SC1.add_child(SCC1)
S3.draw(ax,color='gray',fill=False)

# 坐标设置到 (9,2)
# 旋转 -40 度
# 向下移动 5 个单位
# 左移 10 个单位后,放大 1.5 倍
S3.set_pos(9, 2).set_angle(-40).move(0, -5).move(-10, 0).scale(1.5, 1.5)
S3.draw(ax,color='#AAAAAA22',fill=True)

# 子对象 SC1 旋转 -60 度,其子对象 SCC1 向上移动 2 个单位
SC1.rotate(-60)
SCC1.move(0,2)
SC1.draw(ax,color='#AAAAAA44',fill=True)

# 子对象 SC1 设置到世界坐标 (10,1)
SC1.set_global_pos(10,0).draw(ax,color='#AAAAAA66',fill=True)

# 子对象 SC1 旋转 100 度,其子对象 SCC1 向下移动 2 个单位
SCC1.move(0,-2).parent.rotate(100).draw(ax,color='#AAAAAA88',fill=True)

# 子对象 SC1 向左移动 3 个单位,并且其子对象 SCC1 放大 2.5 倍
SCC1.scale(2.5,2.5).parent.move(-3, 0).draw(ax,color='#AAAAAADD',fill=True)
png

相关文章

矩阵的数学定义
关于矩阵的数学定义,包括:矩阵类型、矩阵基本运算、矩阵分解及特殊矩阵运算。