Post

《Math for Programmers》笔记-第一部分 向量和图形

通过代码学数学

本章内容

将数学知识和软件开发结合起来解决商业问题

绕开学习数学时的常见陷阱

从变成角度来思考数学问题

将Python作为一个强大、可扩展的计算器

在本书的第一部分中,你将详细学习各种维度的向量以及如何使用高维数据。第二部分将介绍不同类型的函数,如线性函数和指数函数,并通过分析其变化率来进行比较。最后,第三部分将探讨如何建立数学模型,将数据集的所有维度纳入其中,从而获得更准确的图形。

如何高效的学习数学

大学水平的教科书往往是非常公式化的:先定义一些新的属于,再用这些属于陈述一些事实(即定理),然后证明这些定理为真。

这听起来是一个符合逻辑的顺序:介绍所讲的概念,陈述一些可以得出的结论,然后证明这些结论。那么学习高数课本难在哪里呢?

问题是数学知识实际上并不是这样创造出来的。当提出新的数学思想时,在找到正确的定义之前,可能会经过很长一段时间的实验。

我们从这个过程吸取的主要经验在于,应该从全局思考开始,不要拘泥于形式。一旦对数学的工作原理有了大致的了解,词汇和符号就会成为有用的工具,而不会分散你的注意力。

对于有经验且在工作中热衷于学习变成的程序员来说,可以用几种具体的方法来很好地学习数学。

  1. 使用正式的语言
    • 留意新的数学对象,他们看起来想你所熟知的对象,但行为方式却不一样。
    • 写代码的时候,仅仅写出语法正确的语句是不够的。语句所代表的思想需要时有意义的、合法的。
  2. 构建你自己的计算器
    • Python不仅自带算术运算、math模块,还有众多可以随时引入的第三方数学库,让编程环境更加强大。因为Python是图灵完备的,所以你可以计算任何可以计算的东西,需要的只是一台足够强大的计算机、一个足够巧妙的程序实现,或者两者兼备。
  3. 用函数建立抽象概念
    • 数学函数总是接受输入值,并总是返回没有副作用的输出值
    • 在编程中,我们把行为像数学函数的函数称为纯函数

小结

数学在许多软件工程领域都有着趣味盎然和收益颇丰的应用。

数学可以量化随着时间变化的数据趋势,如预测股票价格的走势。

不同类型的函数表示不同性质的行为。例如,指数折旧函数意味着汽车每行驶一英里就会损失其转售价的一个百分比,而不是一个固定数额。

数字元组(称为向量)代表多维数据。具体来说,三维向量是三元数对,可以表示空间中的点。可以通过组合向量指定三角形来构建复杂的三维图形。

微积分是研究连续变化的数学。许多物理定律是用微积分方方程来写的,这些方程称为微分方程

传统课本中的数学很难学号!应该通过逐步探索来学习数学,而不是直接在定义和定理中挣扎前进。

作为一名程序员,你已经训练了自己精确思考和沟通的能力,这种技能也会帮助你学习数学。

第一部分 向量和图形

第一部分(指本书第2章至第7章)将深入研究称为线性代数的数学分支。线性代数能够在非常高的层次上处理多维度的计算,这里的“维度”是一个集合概念。线性代数让我们把关于维度的集合概念变成可以具体计算的东西。

线性代数中最基本的概念是向量(vector),可以把它看作某个多维空间中的一个数据点。例如,二维空间中的向量对应于平面上的点,可以用形式为(x, y)的有序数来表示,三维空间中的向量可以用形式为(x, y, z)的三元数对来表示。基于这两种情况,我们可以使用向量的集合来定义几何图形,而这些形状又可以被转换成有趣的图形。

线性代数中的另一个关键概念是线性变换(linear transformation)。线性变换是一种函数,将一个向量作为输入并返回一个向量作为输出,同时保持所操作向量(在特殊意义上)的几何形状。例如,如果一个向量的集合位于二维平面的一条直线上,在应用线性变换后,他们仍然会位于一条直线上。我们对于线性变换的最终应用是,在一个Python程序中,随着时间的推移将其应用到图形之上,从而得到三维动画。

在第6章,会对二维空间和三维空间的概念进行逆向研究,得到向量空间(vector space)的一般概念,并更具体地定义维度的概念。值得注意的是,由像素构成的数字图像可以被看作高维向量空间中的向量,可以通过线性变换来进行图像处理。

最后,第7章中会研究线性代数中最普遍的计算工具:解线性方程组(system of linear equations)。一般来说,线性方程组能帮助我们求得线、屏幕或高维空间在向量空间中的相交位置。通过使用Python处理线性方程组,我们将构建视频游戏引擎的第一个版本。

二维向量绘图

本章内容

使用向量集合创建和处理二维图像

用箭头哦、坐标和有序二元组表示二维向量

从使用向量运算在平面上转换图形

使用三角学测量平面内的距离和角度

二维向量绘图

物理学通常把时间看作第四位,事件会发生在特定的时间和地点。数据科学中的数据集通常包含更多维度。举例来说,网站跟踪的用户可以有数百个可测量的属性,这些属性描述了用户的使用习惯。要解决这些图形学、物理学以及数据分析相关的问题,需要一个能够处理高维数据的框架,即向量数学

向量就是多维空间中的对象,有自己特定的算法规则(加法、乘法等)。

二维向量(two-dimensional vector)是平面上相对于远点的一个点。

如何表示二维向量?

可以用尺子测量一维世界中的对象,比如一个物体的长度。要测量二维对象,我们需要两把尺子。这些尺子就是坐标轴,他们彼此垂直,相交于原点。一般将二维坐标写成x坐标和y坐标的有序数对(或者叫做元组)

用Python绘制二维图形

要在屏幕上绘制图形,我们有大量的语言和库可供选择:OpenGL、CSS、SVG等等。具体到Python中,有Pillow和Turtle这样的库,非常适合用向量来进行绘图。

练习:当X坐标再-10到10范围内时,使用draw函数绘制向量(x, x**2)的点

1
2
3
4
5
6
7
# 为了绘制这个图标,给draw函数传递了两个额外的关键参数。grid参数为(1, 10),表示每隔1各单位绘制垂直网格线,以及每隔10个单位绘制水平网格线。nice_aspect_tatio参数设置为False,表示X轴和y轴的比例不必相同。

draw(
    Points(*[(x, x**2) for x in range(-10, 11)]),
    grid=(1,10),
    nice_aspect_ratio=False
)

平面向量运算

和数一样,向量也有自己的运算方式。对向量进行运算可以生成新的向量,且可以将结果可视化。向量运算除了包含代数变化,还有几何变化。

最基本的向量运算是:向量加法。给定两个输入向量,将它们的X坐标相加,得到新的X坐标,然后将它们的y坐标相加,得到新的y坐标。用这些新的坐标创建一个新的向量,就得到了原始向量的向量和

添加一个向量意味着移动或平移一个现有的点或点的集合。

向量的分量和长度

把一个已有的向量分解成更小的多个向量之和是一种非常有用的操作。例如将向量(4, 3)分解为(4, 0)与(0, 3)之和。这两个分解后的向量分别成为x分量与y分量。向量的长度(length)就是代表它的箭头的长度,等价于从远点到它表示的点的距离。

向量与数相乘

将向量乘以数的运算称为标量乘法。处理向量时,普通的数通常被称为标量(scalar)。scalar这个名字非常贴切,因为运算的效果是将目标向量按照给定的系数进行缩放(scale)。标量是否为整数并不重要。

减法、位移和距离

向量的标量乘法和数的乘法一致:一个数的整数倍数与这个数重复相加是一样的。这同样适用于向量,负向量和向量减法也如出一辙。

位移是一个向量,而距离是一个标量(一个数)。

平面上的角度和三角学

像原始坐标对一样,可以用一个新的数对(5,37°)唯一地确定该向量,这种形式的坐标称为极坐标(polar coordinates)。有时候,比如做向量加法时,使用笛卡尔坐标更简单;而其他时候,极坐标更实用,特别是进行向量旋转时。在写代码时,因为没有所谓刻度尺或量角器,所以只能依赖三角函数。

从角度到分量

给定一个角度,该角度上向量的坐标将有一个固定的比值,我们不可能每次都幸运的得到一个整数的比值,但每个角度都有一个固定的比值。比值可以解释为每个水平单位对应了x个垂直单位,x就是比值。这个比值叫作角的正切,正切函数写作tan。tan(37°) ≈ 3/4 tan(116.57°) ≈ -2 tan(45°) ≈ 1 tan(200°) ≈ 0.36

在这里,为表示近似相等,用符号 ≈ 而不是 =。正切函数是一个三角(trigonometric1)函数,因为它可以用来测量三角形。

正切实际上并不给出坐标,只给出比值。在这一点上,另两个三角函数很有帮助:正弦(sin)和余弦(cos)。从角度和距离的关系来看,角的正切等于垂直距离除以水平距离。

相比之下,正弦函数和余弦函数给出了向量的垂直距离、水平距离和整体距离之间的关系,其定义的公示如下:

  1. sin(角度) = 距离 / 垂直距离
  2. cos(角度) = 距离 / 水平距离

Python中的三角学和弧度

在Python中不使用角度,事实上大多数数学家也不使用角度。他们使用弧度(radian)来代替角度,换算系数是 1弧度约等于 57.296°。之所以这样是因为一个特殊的数派,它的值约为3.14159.正是它搭建了角度和弧度的桥梁。 派弧度等于180°,2派弧度等于360°。

1
2
3
>>> from math import tan, pi
>>> tan(pi/4)
0.99999999999999

现在可以实现一个to_cartesian函数,接收一对极值坐标哦并返回相应的笛卡尔坐标。

1
2
3
4
from math import sin, cos
def to_cartesian(polar_vector):
    length, angle = polar_vector[0], polar_vector[1]
    return (length*cos(angle), length*sin(angle))

从分量到角度

给定一对笛卡尔坐标,可以使用勾股定理计算向量的长度。Python的 反正弦(asin)三角函数math.asin()可以实现接收sin(x)的值并返回x。例如:sin(1) = 0.84147098 asin(0.84147098) = 1。

但是math.asin并不完美,因为不同角度可以有相同的正弦。反余弦(acos)在Python中被实现为math.acos,为了找到我们真正想要的x的值,必须确保它的正弦和余弦都与我们期望的值一致。

而反正弦、反余弦或反正切函数都不足以找到平面内某个点与x轴正半轴的夹角。Python可以完成这个工作,match.atan2函数接受平面上一个点的笛卡尔坐标作为参数,返回对应的弧度。例如: atan2(3,-2) = 2.15879893。

总而言之,三角函数是很难反解的。多个不同的输入可以产生相同的输出,所以一个输出并不能对应唯一的输入。

向量集合的变换

当处理向量时,某种坐标系可能比另一种坐标系更好。用笛卡尔坐标移动或平移向量集合很容易,而在极坐标中就不那么自然了。不过,由于极坐标包含角度信息,会使得旋转向量更为方便。

组合向量变换

目前为止,我们已经学习了如何平移、缩放和旋转向量。将这些变换用于向量的集合,会对这些向量在平面上定义的形状产生同样的效果。当依次应用这些向量变换时,效果会非常惊人。

用Matplotlib绘图

Polygon、Points、Arrow和Segment类并无特别之处,只是保存了其构造函数传递的数据。

draw函数首先计算出图形的大小,然后逐一绘制传给它的每个对象。

上升到三维世界

本章内容

建立三维向量的心智模型

进行三维向量运算

使用点积和向量测量长度和方向

在二维平面上渲染三维模型

在三维空间中绘制向量

三维空间中仍保留了x方向和y方向的概念,并增加了z方向来测量高度。可以说所有二维向量也都存在于三维空间中,他们的方向和大小不变,但被固定在一个高度z为0的平面上。

用坐标表示三维向量

要在三维空间指定唯一的点,总共需要三个数,例如(3,4,5)这样的三元数对。

用Python进行三维绘图

和上一章一样,也是用Python的Matplotlib库的包装器来绘制三维空间中的向量。

1
2
3
4
5
from Chapter_03.draw3d import *

draw3d(
    Points3D((2,2,2), (-1,3,1))
)

三维空间中的向量运算

二维平面上的所有算术运算在三维空间中都有对应的运算,而且其几何效果是类似的。

添加三维向量

在三维平面上,要想把任意数量的三维向量相加,可以将他们的所有x坐标、y坐标和z坐标分别相加。在Python中,可以编写一个简洁的函数来对任意数量的输入向量求和并在二维或三位空间中使用,如下所示。

1
2
3
4
5
6
7
def add(*vectors):
    """三维向量相加"""
    by_coordinate = zip(*vectors)

    coordinate_sums = [sum(coords) for coords in by_coordinate]

    return tuple(coordinate_sums)

三维空间中的标量乘法、减法也类似加法,将各个元素计算之后得出对应的三维向量即可。

计算长度和距离

在二维平面中,我们通过勾股定理来计算向量的长度,因为箭头向量和它的分量构成了一个直角三角形。同样,平面内两点之间的距离也只是它们作为向量的最差长度。

计算三维向量的长度和二维长度很类似。无论是对于二维还是三维,向量的长度都是其分量平方和的平方根。

计算角度和方向

像二维向量一样,三维向量可以被看作箭头或者沿一定方向发生的一定长度位移。在二维平面上,这意味着两个数足以指定任何二维向量。在三维空间中,一个角度不足以确定方向,但两个可以。

点积:测量向量对齐

有两种重要的方法可以做到向量相乘,二者都提供了重要的几何学见解。一种叫做点积,使用点运算符书写(例如,u · v);另一种叫作向量积(例如, u * v)。对于数来说,这些符号意思是一样的。对于两个向量来说,运算u · v和 u * v不仅仅有不同的符号,而且代表的意义完全不同。

点积取两个向量并返回一个标量(数),而向量积取两个向量并返回另一个向量。然而,这两种运算都可以推断出三维空间中的长度和方向。

绘制点积

点积(也叫内积)是对两个向量的运算,返回一个标量。点积适用于二维、三维等任意维度的向量。它可以被看作测量输入向量对的“对齐程度”。指向相似方向的两个向量的点积为正,意味着它们是对齐的。并且向量越大,乘积就越大。对于同样对齐的较短向量,点击较小但仍然是正的。相反,如果两个向量指向相反或大致相反的方向,则其点积为负。向量越长,则点积的负值越小。

并非所有的向量对都明确地指向相似或相反的反向,点积可以检测这一点。如果两个向量的方向完全垂直,那么无论他们长度如何,点击都是零。

点积最重要的应用之一:在不做任何三角运算的情况下,计算两个向量是否垂直。这种垂直的情况也可以用来区分其他情况:如果两个向量的夹角小于90°,则向量的点积为正;如果夹角大于90°,则向量的点积为负。

计算点积

给定两个向量坐标,有一个计算点积的简单公式:将相应的坐标相乘,然后将乘积相加。例如点积(1,2,-1)·(3,0,3)中,x坐标的乘积为3,y坐标的乘积为0,z坐标的乘积为-3,因此相加为0,所以点积为零。说明这两个向量是垂直的。

在三维空间中,我们的视角可能有误导性,这使得计算出向量的相对方向比目测更有价值。

在Python中,可以实现一个点积函数来处理任意一对输入向量:

1
2
def dot(u, v):
    return sum([coord1 * coord2 for coord1,coord2 in zip(u,v)])

点积的示例

位于不同轴上的两个向量的点积为0并不奇怪,这说明他们是垂直的。另外,向量越长,其点积的绝对值越大。这说明点积的绝对值与其输入的向量的长度成正比。

用点积测量角度

点积是根据两个向量的夹角而变化的。具体来说,当夹角角度为0到180时,点击u·v的取值范围时u和v长度乘积的1到-1倍。我们已经见过具有这样特征的函数,即余弦函数。其实点积还有另一个公式。如果uv分别表示向量u和v的长度,那么点积的计算公式为。
u·v =u·v·cos(β)

β是向量u和v之间的角度。

向量积:测量定向区域

向量积以两个三维向量u和v作为输入,其输出u * v 是另一个三维向量。它与点积不同之处在于,输入向量的长度和相对方向决定了输出;但不同之处在于,他的输出不仅有大小,还有方向。

在三维空间中确定自己的朝向

数学家可以用手来区分坐标轴的两种可能方向,称为右手方向和左手方向。右手规则如下:如果右手食指指向x轴正方向,中指、无名指和小拇指向y轴正方向弯曲,那么大拇指就会指明z轴的正方向。这就是右手规则,如果他与你的坐标轴一直,那么你就使用了右手方向,方向很重要!

找到向量积的方向

xy平面内的任意两个向量的向量积都位于z轴上。这清楚地说明了为什么向量积在二维中不起作用:它返回的向量位于包含两个输入向量的平面之外。

向量积也遵循右手规则。一旦你找到了垂直于两个输入向量u和v的方向,向量积u * v的方向就将三个向量u、v和u * v置于了右手系中。

求向量积的长度

和点积一样, 向量积的长度也是一个数,它提供了关于输入向量的相对位置的信息。他测量的并不是两个向量的对齐程度,而更像是“它们的垂直程度”。更准确地说,它告诉我们两个输入之间的面积有多大。

以u和v为边的平行四边形的面积等于向量积u * v的长度。

计算三维向量的向量积

向量积的公式为 u * v = (uyvz-uzvy, uzvx - uxvz,uxvy - uyvx)

如果使用培python,则如下所示:

1
2
3
4
def cross(u, v):
    ux,uy,uz = u
    vx,vy,uz = v
    return (uy*uz - uz*vy, uz*vx - ux*vz, ux*vy - uy*vx)

我们的终极项目是:用多边形构建一个三位对象,不在二维画布上绘制它。

在二维平面上渲染三维对象

使用向量定义三位对象

八面体是一个简单的例子,因为它只有6个角(顶点),策略如下:

将一个三角形面建模为三个向量v1、v2和v3,用来定义它的边。具体来说,我们会将v1、v2和v3排序,使(v2 - v1) * (v3 - v1)只想八面体之外。如果一个向外的向量使只想我们的,就意味着我们的视角可以看到这个面,否则这面就是被遮挡住的,不需要绘制。我们可以将这8个三角形面都定义为三个向量v1、v2和v3的三元组,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
octahedron = [
    [(1,0,0),(0,1,0),(0,0,1)],
    [(1,0,0),(0,0,-1),(0,1,0)],
    [(1,0,0),(0,0,-1),(0,-1,0)],
    [(1,0,0),(0,1,0),(0,0,-1)],
    [(-1,0,0),(0,0,1),(0,1,0)],
    [(-1,0,0),(0,1,0),(0,0,-1)],
    [(-1,0,0),(0,-1,0),(0,0,1)],
    [(-1,0,0),(0,0,-1),(0,-1,0)]
]

# 通过以下函数获取顶点

def vertices(faces):
    return list(set([vertex for face in faces for vertex in face]))

二维投影

要把三位点变成二位点,必须选择我们的三维观察方向。一旦我们的视角确定了定义“上”和“右”的两个三维向量,就可以将任意三维向量投射到它们上面,得到两个分量而不是三个分量。

为了给二维绘图着色,我们根据每个三角形对面对给定光源的角度大小,为其选择一个固定的颜色。假设光源基于原点的坐标(1,2,3)向量处,那么三角形面的亮度取决于它与光线的垂直度。另一种测量方法是借助垂直于面的向量与光源的对齐程度。Matplotlib有一个内置的库来计算颜色。

小结

二维中的向量有长度和宽度,而三维空间中的向量还有高度。

三维向量是由X坐标、y坐标和z坐标的三元数对定义的。这些坐标说明了三维空间中的某个点 在每个方向上距离原点有多远。

和二维向量一样,三维向量也可以与标量进行加法、减法和乘法运算。我们可以用勾股定理的三维版本来求它们的长度。

点积是将两个向量相乘并得到一个标量的方法。它衡量了两个向量的对齐程度,其值也可以用来计算两个向量的夹角。

向量积是将两个向量相乘得到第三个向量的方法,这个向量与两个输入向量垂直。

向量积的输出大小就是两个输入向量张成的平行四边形的面积。

任何三维对象的表面都可以表示成三角形的集合,其中每个三角形分别由代表其顶点的三个向量定义

使用向量积,我们可以确定三角形在三维空间中可见的方向。由此可知三角形对观察者是否可见,或者它在给定光源下被照亮的程度。通过绘制和定义对象表面的所有三角形并进行着色,可以让其看起来立体感十足。

变换向量和图形

本章内容

运用数学函数变换和绘制三维对象

变换向量图形来创建计算机动画

识别不改变直线和多边形形状的线性变换

计算线性变换对向量和三维模型的影响

变换三维对象

本章目的在于对三维对象进行改变,以创建在视觉上有所不同的新三维对象。

绘制变换后的对象

不同的标量乘积会以不同的系数(标量倍数)改变茶壶的大小,而不同的平移向量会将茶壶移动到空间中的不同位置。

组合向量变换

依次应用任意数量的变换可以定义新的变换。因为向量变换以向量为输入和输出,所以可以通过函数组合来组合任意多的向量,即通过按照指定顺序应用两个或更多现有函数来定义新的函数。

Python函数可以被赋给变量并作为输入传递给其他参数,或者被即时创建并作为输出值返回。这些都是函数式编程技术,也就是说,函数式编程可以通过现有函数来创建新函数,进而构建复杂的程序。

接下来会反复地取一种向量变换,并把它应用到定义一个三维模型的每个三角形的每个顶点上。下面的polygon_map函数接收一个向量变换和一个多边形列表,并将变换应用于每个多边形的每个顶点,产生一个新的多边形列表。

1
2
3
4
5
def polygon_map(transformation, polygons):
    return [
        [transformation(vertex) for vertex in triangle]
        for  triangle in polygons
    ]

本书会以类似的方式思考旋转问题:对于给定的任意角度,生成一个使模型以该角度旋转的向量变换。

绕轴旋转对象

从某种意义上说,所有的三维向量旋转在平面上都不是孤立的。这意味着z轴不变,只对x坐标和y坐标应用二维旋转函数,可以使三维点围绕z轴旋转。

1
2
3
4
5
6
7
8
9
10
11
# 以下是一段二维旋转函数
def rotate2d(angle, vector):
    l,a = to_polar(vector)
    return to_cartesian((1, a+angle))

# 以下是对三维向量的x坐标和y坐标应用该函数

def totate_z(angle, vector):
    x, y, z = vector
    new_x, new_y = rotate2d(angle, (x, y))
    return new_x, new_y, z

创造属于你的几何变换

可以每次修改任意一个或多个坐标,从而使原本的形状发生变换。

线性变换

线性变换(well-behaved)是一种向量运算再变换前后看起来一样的特殊变化。

向量运算的不变性

对于任意向量v、标量s和旋转R,都保持了相同的特性,旋转或其他任意保持向量与标量乘积的向量变换称为线性变换

线性变换使保持向量与标量乘积的向量变换T。也就是说,对于任意输入向量u和v,有:T(u) + T(v) = T(u + v)

而对于任意一对标量s和向量v,有:T(sv) = sT(v)。

图解线性变换

只有不移动原点的变换才是线性的。任何使用非零向量的平移都会将原点变换到不同的电上,所以不可能是线性的。

其它线性变换的例子包括镜像、投影、剪切等。

为什么要做线性变换

因为线性变换保持了向量和与标量乘积,所以也保持了一类更广泛的向量算术运算。最常规的运算称为线性组合。一个向量集合的线性组合是它们的标量乘积之和。例如,3u - 2v 是向量u和v的线性组合。给定三个向量u、v和w,表达式0.5u - v + 6w是它们的线性组合。因为线性变换保持了向量于标量乘积,所以也保持了线性组合。

线性变换遵循向量的代数性质,保持了向量和、标量乘积和线性组合。它们还遵循向量集合的几何性质,将向量定义的线段和多边形转移到由变换后的向量定义的新线段和多边形。

计算线性变换

任何三维向量都可以被分解为(1,0,0)、(0,1,0)和(0,0,1)这三个向量的线性组合。(1,0,0)、(0,1,0)和(0,0,1)这三个向量被称为三维空间的标准基(standard basis),分别表示为e1、e2和e3。

因为线性变换保持了线性组合,所以在计算线性变换时,只需要知道它如何影响标准基向量即可。

二维线性变换T完全由T(e1)和T(e2)的值来定义,也就是总共2个向量或4个数。同样,三维线性变换T完全由T(e1)、T(e2)和T(e3)的值来定义,也就是总共3个向量或9个数。在任意维中,线性变换的行为由一个向量列表或数组阵列来规定。这类包含数组的阵列称为矩阵

小结

向量变换是将向量作为输入并返回新向量的函数,可以应用于二维或三维向量。

将向量变换应用于三维模型的每个多边形的每个顶点能实现模型的几何变换。

通过函数的组合可以对现有的向量变换进行组合,从而创建与依次应用现有向量变换等价的新变换。

函数式编程是一种编程范式,强调组装和操纵函数。

函数操作柯里化将接收多个参数的函数转换为接受单个参数的函数,并返回一个新函数。柯里化允许将现有的Python函数转化成向量变换。

线性变换是保持向量与标量乘积的向量变换。特别注意,对位于线段上的点应用线性变换后,他们仍然位于线段上。

线性组合是标量乘法和向量加法的最普通组合。每一个三维向量都是三维标准基向量e1、e2和e3的线性组合。同样,每一个二维向量都是二位标准基向量是e1和e2的线性组合。

知道了如何对标准基向量运用线性变换,就可以把向量写成标准基的线性组合,而操作向量就是操作这个线性组合。

在三维空间中,总共3个向量或9个数可确定一个线性变换。

在二维空间中,总共2个向量或4个数可确定一个线性变换。

最后一点很关键:线性变换既是良态的又容易计算,因为用很少的数据就可以指定一个线性变换。

使用矩阵计算变换

本章内容

将线性变换写成矩阵

用矩阵相乘来组合并应用线性变换

用线性变换操作不同维度的向量

使用矩阵平移二维向量或三维向量

上一章提到了一个重要思想:任何三维线性变换都可以只用3个向量或者9个数来指定。正确选择这9个数,可以实现绕轴旋转、平面反射、平面投影、缩放,或者其他任意三维线性变换。

排列在网格中、用于说明如何执行线性变换的数被称为矩阵。矩阵可以利用对标准基向量的操作数据,计算给定的线性变换。学习新的表示法虽然痛苦,但也有回报,最好将向量看作几何对象或数字元组。同样,将线性变换看作数字矩阵,可以扩展我们的心智模型。

用矩阵表示线性变换

把向量和线性变换协程矩阵形式

矩阵是由数组组成的矩形网格,其形状诠释了这个概念。矩阵可以有其他的形状和大小,但现在要关注两种形状:表示向量的单列矩阵和表示线性变换的方阵。

矩阵与向量相乘

把方阵作为作用于列向量的函数来处理是矩阵乘法运算的一种特殊操作。只是在做相同的事情:对向量进行线性变换。与数的相乘不同,当用向量乘矩阵时,顺序很重要。例如,Bv是一个有效的乘积,但vB不是。

矩阵乘法有两个口诀,它们的结果都是一样的。

第一个口诀是:输出向量的每个坐标是输入向量所有坐标的函数。例如,三维输出的第一个坐标是函数f(x,y,z) = ax + by + cz。这是一个线性代数,值是每个变量的倍数之和。线性变换之所以叫线性变换的一个原因是:线性变换是输入坐标的线性函数集合,这些函数给出了各自的输出坐标。

第二个口诀提供了相同公式的不同形式:输出向量的坐标是矩阵的行与目标向量的点积。例如,3 * 3矩阵的第一行是(a,b,c),相乘向量是(x,y,z),所以输出的第一个坐标是(a,b,c)·(x,y,z) = ax + by +cz。

计算两个矩阵相乘,比较简单的公式为:Cij = A的第i行与B的第j列对应元素的乘积之和

用矩阵乘法组合线性变换

在数学术语中,任意数量线性变换的组合也是线性变换。

因为任意线性变换都可以用矩阵来表示,所以任意两个组合的线性变换也可以用矩阵来表示。事实上,矩阵是组合线性变换和构建新的线性变换最好的工具。

乘积矩阵的每一项是第一个矩阵的一行第二个矩阵的一列的点积

实现矩阵乘法

矩阵乘法的结果应该是元组的元组,所以可以把它写成一个推导式。该函数接受两个嵌套的元组a和b,分别表示输入矩阵A和B。输入矩阵a已经是第一个矩阵的行元组,将这些元组与zip(b)匹配,zip(b)是第二个矩阵的列元组。最后,对于每一对组合,再调用时取点积。

1
2
3
4
5
6
7
from vectors import *

def matrix_multiply(a,b):
    return tuple(
        tuple(dot(rot,clo) for col in zip(*b))
        for row in a
    )

用矩阵变换表示三维动画

PyGame内置的始终可以跟踪时间的变化,所以可以根据时间生成矩阵的项。换句话说,与其把矩阵的每一项都看做一个数,不如把它看做一个函数,取当前时间t并返回一个数。将矩阵项视为时间的函数,使整个矩阵随着时间的推移而变化。

不同形状矩阵的含义

向量加法、标量乘法、点乘以及矩阵乘法的函数并不依赖于向量的维度。即使我们不能描绘一个五维向量,也可以在5个数字元组上做同样的代数运算。

列向量组成的矩阵

矩阵和列向量相乘是矩阵乘法的一种特殊情况。为了使矩阵乘法的定义保持一致,只能在列向量的左边乘以一个矩阵。

哪些矩阵可以相乘

第一个矩阵的列数必须与第二个矩阵的行数相匹配。

所有的矩阵都表示向量函数,所有有效的矩阵乘积都可以被解释为这些函数的组合。

将方阵和非方阵视为向量函数

可以把2 * 2矩阵看作对二维向量进行给定线性变换所需的数据。可以把矩阵看作向量作为输入并产生向量作为输出的机器。

而2 * 3的矩阵表示了将三维向量转换为二维向量的函数。一般来说,一个m * n矩阵定义的函数接收n维向量作为输入,并返回m维向量作为输出。任何这样的函数都是线性的,因为它保持了向量和与标量乘积。他不是一个变换,因为它不仅修改输入,还返回一种完全不同的输出:一个具有不同维度的向量。可以称它为线性函数线性映射

从三维到二维的线性映射投影

如果三个向量u、v和w构成了一个向量u + v = w,那么在xy平面上的投影也会构成一个向量和。

组合线性映射

矩阵的优点是,存储了在 给定向量上计算线性函数所需的所有数据。更重要的是,矩阵的维度告诉了我们底层函数的输入向量和输出向量的维度。

用矩阵平移向量

使用矩阵的一个优点是,任意维度的计算看起来都一样,我们不需要绘制二维或三维向量的结构。

线性化平面平移

平移并不是线性变换,当根据给定向量移动平面上的每一个点时,原点会移动,向量和也不会被保持。如果一个二维变换非线性,该如何使用矩阵执行呢?

答案是把二维的点想象成在三维中平移。

寻找做二维平移的三维矩阵

如果想通过某个向量(a,b)来平抑二维向量集合,则一般步骤如下

  1. 将二维向量集合移动到三维空间的平面上,其中z = 1,每个向量的z坐标都为1
  2. 将向量乘以矩阵,并带入给定选项a和b
  3. 删除所有向量的z坐标,这样就只剩下二维向量。

组合平移和其它线性变换

在四维世界里平移三维对象

四维向量是一个箭头,有一定的长度、宽度、高度以及一个其它维度。通过二维空间建立三维空间时,增加了z坐标。这意味着三维向量可以存在于xy平面上(z = 0),也可以存在其他任何平行平面上,其中z取不同的值。

可以用这个模型来思考四维空间:由第四坐标做索引的三维空间集合。第四坐标的一种解释是时间。给定时间的每个快照都是一个三维空间,但所有快照的集合是第四维度,称为时空。时空的原点是时间t等于0的空间原点。

到目前为止,我们只关注了向量,而这些向量是能在计算机屏幕上渲染的空间点。这显然是一个重要的用例,但只涉及向量和矩阵的表面内容。关于向量和线性变换的研究一般称为线性代数

小结

线性变换是由它对标准基向量的作用定义的。当对标准基应用线性变换时,结果向量包含进行变换所需的所有数据。这意味着只需要9个数就可以指定任意的三维线性变换(这3个结果向量的3个坐标)。对于二维线性变换,则需要4个数。

在矩阵表示法中,我们通过将数放在一个矩形网格中来表示线性变换。按照惯例,创建矩阵来计算它所表示的对给定向量的线性变换,叫作将矩阵与向量相乘。当执行这种乘法时,向量通常被写成一列从上到下的坐标,而不是一个元组。

两个方阵也可以相乘。所得矩阵表示两个原始矩阵的线性变换的组合。

要计算两个矩阵的乘积,需要取第一个矩阵的行于第二个矩阵的列的点积。例如,第一个矩阵的第i行和第二个矩阵的第j列的点积就是成绩里第i行第j列的值。

由于方阵表示线性变换,那么非方针就表示从一个维度向量到另一个维度向量的线性函数。也就是说,这些函数将向量和传递给向量和,将标量乘积传递给标量乘积。

矩阵的维度告诉你,其对应的线性函数接收和返回什么样的向量。一个有m行和n列的矩阵称为m * n矩阵,它定义了从n维空间到m维空间的线性函数。

平移不是一个线性函数,但如果在更高维度中执行,可以将其线性化。这一点使我们可以通过矩阵乘法来进行平移(同时进行其他线性变换)。

高维泛化

本章内容

用Python实现对向量进行通用描述的抽象基类

定义向量空间,并且列出其有用的属性

将函数、矩阵、图像和声波描述为向量

探索向量空间的一些有趣的子空间

在分析数据时,线性代数能将我们对二维和三维几何的认知泛化到任意维度上。

再重复遇到类似的模式后,就能更好、更准确地描述这些模式,并完善他们的定义,本章使用类似的逻辑来定义向量空间。向量空间是一组对象的集合,我们可以像处理向量一样处理这些对象。

向量空间中的关键运算是向量加法和标量乘法,可以基于它们进行线性组合(包括取反、减法、加权平均等),还可以推理出那些变换是线性的。

泛化向量的定义

Python支持面向对象编程(OOP),具体来说,Python类支持继承:你可以创建新的类,让其继承父类的属性和行为。

为二维坐标向量创建一个类

可以把向量转换成一个Python类来表示这些信息。我们把代表二维坐标的向量的类称为Vec2。

1
2
3
4
class Vec2():
    def __init__(self,x,y):
        self.x = x
        self.y = y

接下来给这个类添加二维向量运算所需的方法,特别是向量加法和标量乘法。 加法函数add接收第二个向量作为参数,并返回一个新的Vec2对象,其坐标分别是两个向量的X表座和y坐标之和。

1
2
3
4
5
6
def add(self, v2):
    return Vec2(self.x + v2.x, self.y + v2.y)

v = Vec2(3,4)
w = v.add(Vec2(-2,6))
print(w.x) # -2 + 3 = 1

也可以用类似的方式实现标量乘法,将一个标量作为输入,然后返回一个缩放过的向量作为输出。

1
2
3
def scale(self, scalar):
    """标量乘法"""
    return Vec2(self.x * scalar, self.y * scalar)

默认情况下,Python会通过引用的方式来对比类的实例(检测它们是否引用内存中的同一地址),而不是比较他们的值。可以通过重写对应的方法解决这个问题,让Python对Vec2类的对象以不同的方式处理==运算符。

1
2
def __eq__(self,other):
    return self.x == other.x and self.y == other.y

升级Vec2类

改变了==运算符的行为,被称为运算符重载,也可以自定义Python运算符+和*。

1
2
3
4
5
def __add__(self,other):
    return self.add(other)

def __mul__(self,other):
    return self.scale(other)

可以写出一个简练的线性组合:3.0 * Vec2(1,0) + 4.0 * Vec2(0,1)。可以通过重写__repr__方法来改变Vec2对象的字符串表示。

1
2
def __repr__(self):
    return "Vec2({self.x},{self.y})".format(self.x, self.y)

使用同样的方法定义三维向量

虽然Vec3看起来像像Vec2,但所需数据是三个坐标而不是两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Vec3():
    def __init__(self,x,y,z):
        self.x = x
        self.y = y
        self.z = z

    def add(self, v3):
        """向量加法"""
        return Vec2(self.x + v3.x, self.y + v3.y, self.z + v3.z)
    
    def scale(self, scalar):
        """标量乘法"""
        return Vec2(self.x * scalar, self.y * scalar, self.z * scalar)
    
    def __eq__(self,other):
        """向量相等"""
        return self.x == other.x and self.y == other.y and self.z == other.z
    
    def __add__(self,other):
        return self.add(other)

    def __mul__(self,other):
        return self.scale(other)
    
    def __repr__(self):
        return "Vec2({self.x},{self.y},{self.z})".format(self.x, self.y, self.z)

可以写一个通用的average函数,它能用于任意类型的向量

1
2
def average(v1, v2):
    return 0.5 * v1 + 0.5 * v2

传入二维或三维向量都能返回正确且有意义的结果。这就是泛化带来的优雅性和效率提升。对输入的唯一约束是:需要支持向量加法和标量乘法。Vec2对象、Vec3对象、图像或其他类型数据之间的计算方式各不相同,但其中会有重叠部分,即使用什么运算。当我们把什么如何分开思考时,就带了代码复用和数学抽象的大门。

构建向量基类

只有向量加法和标量乘法是向量独有的操作,所以能直接放在Vector基类中,其余操作则还是放在子类中实现。

1
2
3
4
5
6
7
8
9
10
from abc import ABCMeta, abstractmethod

class Vector(metchlass=ABCMeta):
    @abstractmethod
    def scale(self,scalar):
        pass

    @abstractmethod
    def add(self,other):
        pass

abc模块包含工具类、函数和方法装饰器,可以帮助我们实现一个抽象基类,即不会被实例化的类。这种类旨在作为继承它的类的模板。

@abstractmethod装饰器意味着方法不是在基类中实现的,而是需要在子类中实现。它约束子类必须要实现的抽象方法。我们可以为这个抽象类添加所有只依赖于向量加法和标量乘法的方法,比如运算符重载

1
2
3
4
5
6
7
8
    def __mul__(self,other):
        return self.scale(other)

    def __rmul__(self,other):
        return self.scale(other)

    def __add__(self,other):
        return self.add(other)

可以将Vec2和Vec3简化为继承自Vector的子类。Vector基类很好地展示了我们可以用向量做什么。如果能在其中添加任何有用的方法,那么这些方法就可以会对所有类型的向量起作用。

定义向量空间

在数学中,向量的定义基于其具体作用而非针对其本身的描述,与前文定义Vector抽象类差不多。

向量的一个定义(不完备的):向量是一个对象,具备一种与其他向量相加以及与标量相乘的合适方式。

向量空间就是向量的集合,其定义如下:向量空间是一系列向量对象的集合,每个对象都兼容合适的向量加法和标量乘法运算,因此其中任意向量的线性组合都会产生一个也在集合中的向量。

向量空间的一个例子是所有可能的二维向量的无限集合。事实上,你遇到的大多数向量空间都是无限集合,毕竟可以使用无限多的标量生成无限多的线性组合。

“向量空间需要包含其中所有向量的线性组合”这一规则有两个暗示:

  1. 无论你在向量空间里取什么向量v,v * 0都会得到同样的结果。这就是所谓的零向量,记为0。任何向量与零向量相加都不会发生任何变化。
  2. 每个向量v都有一个相反的向量,即-1 * v,写作-v。

对向量空间类进行单元测试

探索不同的向量空间

枚举所有坐标向量空间

Vec1类,即具有单一坐标的向量。这个类不过是单一坐标的包装器,并没有提供其他有价值的运算方法。我们可能永远不会需要一个Vec1类。但重要的是要知道,数本身就是向量。所有实数(包括整数、分数和像派这样的无理数)的集合被表示为R,它本身就是一个向量空间。这种一种特殊情况:标量与向量是同一种对象。

做标向量空间被表示为Rn,其中n代表维度,即坐标数量。例如,二维平面表示为R2。值得一提的向量空间是零维(zero-demensional)空间R0。这是 坐标数为零的向量集,可以被描述为空的元组或继承自Vector的类Vec0。

识别现实中的向量

在二手丰田普锐斯的案例中,可以将汽车数据加载到一个类中。

1
2
3
4
5
6
7
8
9
10
class CarForSale():
    def __init__(self, model_year, mileage, price, posted_datetime, model, source, location, description) :
        self.model_year = model_year
        self.mileage = mileage
        self.price = price
        self.posted_datetime = posted_datetime
        self.model = model
        self.source = source
        self.location = location
        self.description = description

可以将CarForSale对象作为向量处理,例如可以将其表示成一个线性组合来求平均值,需要将其继承自Vector。

忽略文本数据,CarForSale表现得更像一个向量——他的行为就像一个四维向量,其维度包括价格、车型、历程和发布的日期时间。他不完全是一个坐标向量,因为发布日期不是一个数字。即使数据不是数字,这个类也满足向量空间的特征,所以它的实例可以被当作向量来处理。

将函数作为向量处理

数学函数可以被当作向量,特别是接收一个实数并返回一个实数的数学函数。

与二维或三维向量一样,我们可以用可视化或代数的方式处理函数的加法和标量乘法。首先,可以用代数方式写函数,如f(x) = 0.5x + 3 或g(x) = sin(x)。也可以用图表来可视化这些操作。

确定函数是否满足向量空间规则的单元测试要困难得多,因为生成随机函数或测试两个函数是否相等是很困难的。要想知道两个函数是否相等,必须知道它们对每一个可能的输入都返回相同的输出。函数向量空间的唯独究竟是多少,需要多少个实数坐标才能唯一地识别一个函数?

讲矩阵作为向量处理

因为 n * m矩阵就是一个数量为n * m的数字列表,所以它虽然是矩阵的形式,但可以被看成一个n * m维向量。比如说,5 * 3矩阵的向量空间与15维的向量空间的唯一区别是,坐标值是以矩阵的形式呈现的,我们仍然要对坐标逐一相加或者乘以给定的标量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 把5 * 3矩阵当成向量来处理的Python类

class Matrix5_by_3(Vector):
    rows = 5
    columns = 3
    def __init__(self, matrix):
        self.matrix = matrix
    
    def add(self, other):
        return Matrix5_by_3(tuple(
            tuple(a + b for a, b in zip(row1, row2))
            for row1, row2 in zip(self.matrix, other.matrix)
        ))
    
    def scale(self, scalar):
        return Matrix5_by_3(tuple(
            tuple(a * scalar for a in row)
            for row in self.matrix
        ))
    
    @classmethod
    def zero(cls):
        return Matrix5_by_3(
            tuple(
                tuple(0 for j in range(0, cls.columns))
                for i in range(0, cls.rows)
            ))

矩阵的有趣之处并不在于它们是排列在网格中的数,而是可以被看成线性代数的“名片”。我们已经知道,数字列表和函数是向量空间的两种情况。事实上,矩阵在两种意义上都是向量。如果矩阵A有n行m列,它就代表了一个从m维空间到n维空间的线性函数(数学描述为A:Rᵐ -> Rⁿ)

使用向量运算来操作图像

在彩色图像中,需要三个数描述像素的红、绿、蓝(RGB)分量。一般来说,一个300像素 * 300像素的图像由 300 * 300 * 3 = 270000个数值来表示。

ImageVector类继承自Vector,存储了300 * 300的图像像素数据,支持加法和标量乘法运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 用向量描述图像的类
class ImageVector(Vector):
    size = (300, 300)

    def __init__(self, input):
        try:
            img = Image.open(input).resize(ImageVector.size)
            # 用getdata()方法提取其所有像素。每个像素都是由红绿蓝三色构成的元组
            self.pixels = img.getdata()
        except:
            # 构造函数也可以直接接收像素列表
            self.pixels = input

    def image(self):
        # 返回底层的PIL图像,通过类上的size属性指定大小
        img = Image.new('RGB', ImageVector.size)

        img.putdata([(int(r), int(g), int(b))
                     for r, g, b in self.pixels])
        return img
    def add(self, img2):
        # 图片向量的加法是对每个像素的红绿蓝值求和实现的
        return ImageVector(tuple(
            tuple(int(r1) + int(r2) for r1, r2 in zip(p1, p2))
            for p1, p2 in zip(self.pixels, img2.pixels)
        ))
    
    def scale(self, scalar):
        # 将每个像素的红、绿、蓝乘以给定标量
        return ImageVector(tuple(
            tuple(int(r * scalar) for r in p)
            for p in self.pixels
        ))
    
    @classmethod
    def zero(cls):
        total_pixels = cls.size[0] * cls.size[1]
        return ImageVector([(0, 0, 0) for _ in range(0, total_pixels)])
    
    def _repr_png_(self):
        # Jupyter Notebook用来显示图片
        return self.image()._repr_png_()

向量运算是一个通用概念:向量加法和标量乘法的定义概念适用于数、坐标向量、函数、矩阵、图像和许多其他种类的对象。

寻找更小的向量空间

本节讨论如何把一个巨大的向量空间转化成小一些的向量空间(唯独更少),并尽可能保存有用的信息。

定义子空间

向量子空间简称子空间。他是存在于另一个向量空间内的向量空间。三维空间中的的二维xy平面就是一个具体的例子。它的z = 0。这些向量有三个分量,所以是名副其实的三维向量,但他们形成的子集恰好被约束在一个平面上。

并不是三维向量的每一个子集都是子空间。z=0的平面是一个特殊情况,因为所有形式为(x, y, 0)的向量形成了一个完备(self-contained)的向量空间。在数学语言中,说一个子空间是“完备的”代表它在线性组合下是封闭的。

准确的说,二维平面是三维空间的特殊情况,这种二维平面里的一维子空间可以称为直线。事实上,可以把这个子空间看作一条实数线。

可以把X也设置为0.一旦x = 0和y = 0都成立,就只剩下一个点了,即零向量。零向量也是一个向量子空间。无论怎么对零向量进行线性组合计算,结果都是零向量。这是一个针对一维直线、二维平面和三维空间的零维子空间。在几何学意义上,零维子空间就是和一个点,且这个点的值必须是零。因为如果是其他的点,比如v,子空间内就会包含0 * v = 0和其他无穷多不同的标量倍数。

从单个向量开始

一个包含非零向量v的向量子空间必然(至少)包含v的所有标量乘积。从几何角度来看,非零向量v所有标量乘积的集合位于一条通过原点的直线上。

生成更大的空间

对于一个向量或者彝族乡了,生成空间(span)表示所有线性组合的集合。重要的是,生成空间自然是一个向量子空间。

两个非平行向量的生成空间,虽然每个独立向量的生成空间都是一条直线,但是进行线性组合的话,生成空间会覆盖更多的点,比如v + u既不在t的生成空间上,也不再u的生成空间上。

如果想在集合中添加一个向量,生成一个跟高的维度空间,新的项链需要指向一个新的方向,这个方向不能包含在已有的向量的生成空间中。

定义维度概念

任意一个空间的(basis)都有相同的数量的向量,这个数量就是其维度

一般来说,判断一组向量是否是线性无关的需要花一番功夫。即使你知道一个向量是其他一些向量的线性组合,找到这个线性组合也需要进行一番计算。

寻找函数向量空间的子空间。

图像的子空间

生成这种图像子空间的一种方法是,从图像左上角开始,对每10 * 10的块进行判断,如果块内所有像素都相同,则认为这个块是相同的。

求解线性方程组

本章内容

检测二维视频游戏中对象的碰撞

用方程来表示直线并找出直线在平面上的点

绘制和求解三维或更高难度的线性方程组

将向量重写为其他向量的线性组合

相对于代数,求解线性代数要求解的可能是向量或矩阵,而不是数。求解线性方程组,可以归结为是寻找直线、平面或更高维度的对象的交点。

设计一款接机游戏

游戏建模

使用多边形代表小行星,将代表这个形状的向量与其中心点的X坐标和Y坐标分开存储,因为X坐标和Y坐标可能会随着时间变化。再存储一个角度,表示物体在当前时刻的旋转。

PolygonModel类代表一个可以平移或旋转并保持形状不变的游戏实体(小行星或飞船)。

1
2
3
4
5
6
class PolygonModel():
    def __init__(self, points):
        self.points = points
        self.rotaion_angle = 0
        self.x = 0
        self.y = 0

宇宙飞船和小行星是PolygonModel的具体例子,它们会根据各自的形状自动初始化。

1
2
3
4
5
6
7
8
9
10
11
12
class Ship(PolygonModel):
    """宇宙飞船,由3个点给出"""
    def __init__(self):
        super().__init__([(0.5, 0),(-0.25, 0.25),(-0.25, -0.25)])

class Asteroid(PolygonModel):
    """小行星,随即赋予边、长度"""
    def __init__(self):
        sides = randint(5, 9) # 小行星边数是5和9之间的一个随机数
        vs = [vectors.to_cartesian((uniform(0.5, 1.0), 2*pi*i/sides))
                for i in range(0,sides)] # 长度是0.5和1.0之间的随机数,角度是2Π/n的倍数,n是边数
        super.__init__(vs)

渲染游戏

游戏初始状态,需要一艘飞船与多颗小行星。

1
2
3
4
5
6
7
ship = Ship()
asteroid_count = 10
asteroids = [Asteroid() for _ in range(0, asteroid_count)]
# 将小行星的位置设置为范围内的随机点
for ast in asteroids:
    ast.x = randint(-9, 9)
    ast.y = randint(-9, 9)

还需要编写一个to_pixels函数,将坐标从我们的坐标系映射成PyGame的像素坐标。

发射激光

在二维世界中,激光束应该是一条线段,从经过变换的宇宙飞船顶端开始,向飞船指向的方向延申。可以在Ship类上创建一个方法来计算它。

1
2
3
4
5
6
7
8
class Ship(PolygonModel):
    ···
    def laer_segment(self):
        # 勾股定理找到屏幕上最长线段
        dist = 20. * sqrt(2)
        # 获取线段的第一点(飞船顶端)的值
        x, y = self.transformed()[0]
        return ((x,y),(x + dist * cos(self.rotation_angle), y + dist * sin(self.rotaion_angle)))

要检查每一颗小行星是否被激光击中,那么,在游戏每一次循环迭代中,都要检查。可以用使用PolygonModel类上的does_intersect(segment)方法来实现这一点,该方法计算输入线段是否给定PolygonModel的任意线段相交。

1
2
3
4
5
6
7
8
9
10
laser = ship.laser_segment()

keys = pygame.key.get_pressed()

if keys[pygame.K_SPACE]:
    draw_segment(*laser)

for asteroid in asteroids:
    if asteroid.does_intersect(laser):
        asteroids.remove(asteroid)

找到直线的交点

要判断激光束是否击中了小行星,我们将查看小行星的每个线段,并判断其是否与定义激光束的线段相交。这里把它作为一个两个变量的线性方程组来解决。

为直线选择正确的公式

This post is licensed under CC BY 4.0 by the author.