向量运用前言
本篇是线性代数的简短实用介绍,因为它适用于游戏开发,线性代数是向量及其用途的研究。向量在2D和3D开发中都有许多应用,并且cocos2dX、Unity3D、Unreal Engine广泛使用它们,对矢量数学有深入的了解对于成为一名强大的游戏开发者至关重要。
对于一个初学者来说,三维空间的几何似乎有点让人望而生畏,在纸上可以画出来的二维空间几何就已经足够难以理解了,但是现在我们竟然要使用和掌握三维空间的几何?
好消息是在图形学中直接使用三角形是非常罕见的并且有很多方法可以用来避免这么做,我们有其他更好理解和使用的工具来代替,你可能在上图中已经认出我们的老朋友-向量(vector)。
游戏中的向量概念
在数学中,一个向量是指一个既有方向(direction)又有大小(magnitue)的结构。在游戏开发中它经常用来描述位置的变化,并且可以与其他向量相加或者相减来得到新的位置变化(一个向量代表一个位置的变化,两个这样的向量相加得到的是这两段位置变化的总效果)。通常情况下,你会发现向量是数学库或者物理库的一部分。
它们通常包含一个或多个组件,比如x、y和z,向量可以是一维向量(只包含x分量)、二维向量(包含x、y分量)、三维向量(包含x、y、z分量)甚至是四维向量(一般是x、y、z、w分量),四维向量可以用来描述其他一些东西,比如一个带额外alpha值的颜色。
对于初学者来说最困难的事情之一就是他们在刚接触向量的时候如何去理解看上去就是空间的一个点的东西为什么可以用来描述一个方向。
让我们用二维向量(3,3)来举例说明这个事情,要理解为什么向量能够代表一个方向你只需要看下面这张图。我们都知道需要两个点才能形成一条线,所以第二个点在哪里呢?缺失的那个点就是位于(0,0)的原点(origin),我们画一条从原点(0,0)到(3,3)的线段,我们就得到下图这么一个效果:
正如你在上图中看到的那样,原点作为第二个点引入以后就与第一个点一起赋予了我们的向量一个方向,但是你也会看到,第一个点(3,3)可以被移动(或者说位移)来接近或者远离原点。
第一个点到原点的距离就被称为大小,可以用二次方程a^2 + b^2 = c^2计算得到,在我们举得例子中,就是3^2 + 3^2 =c^2, c = sqrt(18) ~= 4.24。如果我们把向量的每个分量除以4.24那么我们就把向量放缩成了大小正好为1(也就是到原点的距离为1)的向量,在接下来的例子中我们将看到为什么这个被称为向量归一化的过程非常有用,向量的归一化保留了向量的方向,但是提供了通过对数字(也就是标量)值进行乘法来放缩大小的能力。
在接下来的例子中,我将假设你的数学库用Vector2 代指二维向量,用Vector3代指三维向量,它们在不同的库和编程语言中有各种不同的名字,举个例子来说,vector、vector3、 Vector3f、 vec3、 point、 point3f等等都是向量的名字,你的数学库中关于向量部分肯定有很多文档和例子。
注意:向量类型在编程语言的世界里面通常有两种含义,既可以用来指传统的数学/物理场景中的向量,也可以用来表示自行控制的n维单位。这里仅仅是做一个小提醒。
游戏中向量可以是位置、方向或者速度
像其他变量一样,你代码中的向量到底代表着什么含义完全取决于你的控制:它可以是一个位置、方向或者速度。下面是游戏中常见的一些向量用法。
位置
向量代表着真实位置,与你的世界坐标原点(0, 0, 0)的一个偏移量,它代表着一个位置值。
方向
向量看起来非常像是一个箭头指着某个方向,它确实是可以这么用,举个例子来说,如果你有一个指向南的向量,那么你可以把这个向量赋予你的所有单位作为它们的新方向,那么它们都将面向南。
方向向量的一个特例是长度为1的向量,它也被称为归一化的向量或者简称为标准向量。
一个速度(velocity )
向量可以描述一个运动,在这种情况下,它描述的是特定时间内的位置的变化。
向量加法和减法运用
向量加法是用来累加两个向量所描述的不同,并写入最后的向量中。
比如说,一个物体移动了A向量这么大的位移,然后又移动了B向量这么大的位移,那么结果就仿佛是它一共移动了C向量这么大的位移(其中C = A + B)。
对于向量减法来说,就相当于把第二个向量反转,然后把反转的向量加到第一个向量身上。
在直角坐标系里面,定义原点为向量的起点,两个向量和与差的坐标分别等于这两个向量相应坐标的和与差若向量的表示为(x,y)形式。
A(X1,Y1) B(X2,Y2) 则A+B=(X1+X2,Y1+Y2),A-B=(X1-X2,Y1-Y2)
简单地讲:向量的加减就是向量对应分量的加减,类似于物理的正交分解。
物体之间的距离
如果在这个例子中,向量代表的分别是物体A和B的位置,那么 B – A将是代表着A和B物体位置差的向量,B – A所得到的结果将表示A位置移动到B位置所需的方向和距离。
举个例子来说,要得到人到树的距离向量你必须用树的位置减去人的位置,如下图所示:
我用了伪代码(pseudo-code )来保持代码的简洁方便阅读,在括号内的三个数字(x,y,z)代表着一个向量。
注意:伪代码是一种算法描述语言,使用伪码的目的是使被描述的算法可以容易地以任何一种编程语言(Pascal,C,Java等)实现。因此,伪代码必须结构清晰、代码简单、可读性好,并且类似自然语言,介于自然语言与编程语言之间,以编程语言的书写形式指明算法职能。使用伪代码, 不用拘泥于具体实现。相比程序语言(例如Java,C++,C, Dephi 等等)它更类似自然语言
tree_position = (10, 10, 0) my_position = (3, 3, 0) # distance anddirection you would need to move # to getexactly where the tree is vector_to_tree = tree_position - my_position
大炮炮弹的运动速度
除了位置向量以外,对象可能还有一个向量用来表示速度。举个例子来说,大炮炮弹的速度向量描述的是它下一秒将要移动的距离。当第一次被发射的时候,大炮炮弹可能具有如下这些属性:
# position: 10units in Y and Z direction position = (0, 10, 10) # initialmovement is 500 units in X direction over the next second velocity = (500, 0, 0)
每秒钟要基于速度向量来更新一次炮弹的位置:
# add velocityto position and update position position += velocity
概念: 仿真
等等!我们不希望每一秒才更新一次物体,事实上,我们希望尽可能的频繁更新物体的信息。
但是我们不能指望两次更新之间的时间总是固定的,所以我们使用了delta时间,这是上一次更新到这一次更新的时间差。
因为delta时间代表的是逝去时间的一个时间差,所以我们可以用它来得到这次更新到上一次更新之间的这段时间内物体的移动速度所导致的位置差。
position += velocity * delta
这是一个非常基本的仿真,为了实现一个仿真,我们在自己的世界里面建模了我们的对象该具有怎样的行为(比如说大炮炮弹永远具有不变的速度),然后我们加载最初的游戏状态(大炮炮弹开始的时候具有初始位置和速度)。
最后一块拼图是要把所有的东西融合在一起,这就是update循环,它会定期执行,我们用delta时间(也就是时间间隔)来记录上一次更新到这次更新的时间间隔。在每次update调用的时候,它会根据我们预先定义好的规则(比如说用炮弹的速度来更新炮弹的位置)来对每个仿真物体进行更新。
炮弹重力、空气阻力和风
我们的炮弹移动是很无聊的:它永远是向一个方向移动并且移动的速度永远是不变的,我们需要它对周围的世界做出反应。举个例子来说,我们希望重力能让炮弹下落,希望空气阻力会让炮弹的速度变慢,至于风呢,仅仅是加进来为了好玩。
在一个游戏中重力实际上意味着什么呢?嗯,它会产生一个副作用,在物体向下的方向增加物体的速度,因为在我们的例子中Y轴是向上的,所以我们的重力向量将是下面这样的:
# increasevelocity of every object -2 down per second gravity_vector = (0, -2, 0)
所以,在每次进行update调用之前,我们可以修改速度变量,如下面代码所示:
velocity += gravity_vector * delta # applygravity effect position += velocity * delta # updateposition
让我们假设空气很厚,所以空气会每半秒就降低一次炮弹的速度。
velocity += gravity_vector * delta # applygravity effect velocity *= 0.5 * delta # apply 0.5slowdown per second position += velocity * delta # updateposition
速度会受到空气阻力的影响因为炮弹总是在空气中行进,空气会阻挡它的前进进而减少它的动能,所以我们需要调整下炮弹在空气中前进的速度。
但是,还有一个恒定的力会改变炮弹的运动,就像风一样
# modifykinetic energy / velocity velocity += gravity_vector * delta # applygravity effect velocity *= 0.5 * delta # apply 0.5slowdown per second # add all forces final_change_per_second = velocity + wind_force_per_second # updateposition position += final_change_per_second * delta
这个例子的着眼点在于说明用简单的向量数学构建如此复杂的一个行为是多么的容易。
概念: 方向
通常情况下,你不会需要从A到B的距离,而是需要从A指向B的方向,向量A到向量B的距离当然可以用来表示方向,但是如果你需要从A向B移动“很小一点点”,但是要精确的按照你希望的速度该怎样做呢?
在这种情况下向量长度应该无关紧要的,如果我们把方向向量的长度缩减为1,就可以用于这个目的以及其他一些情况,我们把这个缩减称为归一化(normalization ),得到的向量称为标准向量(normalvector)。
所以,一个标准向量它的长度应该总是1,否则它就不是一个标准向量。
一个标准向量代表的是一个角度,而没有实际位置移动相关的其他任何信息,如果我们用一个标量数字乘以一个标准向量,我们就得到了一个方向向量,同时它的长度就是标量数字的大小。
在你的数学库里面应该有一个normalize函数,来从任意的向量中得到一个标准向量。
所以如果要朝B精确的移动3个单位长度,代码如下:
final_change = (B - A).normalize() * 3
向量描述地形平面
一个标准向量也可以用来描述一个平面所朝向的方向。你可以把平面想象成从一个特定点P出发的无限大的片,对这个片的旋转可以通过法向量N来精确描述出来。
要旋转这个片/平面,你应该改变它的法向量。
注意:法向量是空间解析几何的一个概念,垂直于平面的直线所表示的向量为该平面的法向量。由于空间内有无数个直线垂直于已知平面,因此一个平面都存在无数个法向量(包括两个单位法向量)。如果一个非零向量n与平面a垂直,则称向量n为平面a的法向量。垂直于平面的直线所表示的向量为该平面的法向量,每一个平面存在无数个法向量。
球体碰撞演示
1、点到一个平面的距离
如果要得到某个点到一个平面的最短距离,首先计算出这个点到平面上任意一点的距离向量,不要对这个向量进行归一化,然后将其与平面的法向量相乘,得到的就是这个点到这个平面的最短距离。
distance_to_a_plane = (point - plane_point) dotplane_normal;
2、这个点是否在这个平面上?
利用上一个例子的内容,计算这个点到这个平面的最短距离,如果等于0,那么这个点就在这个平面上。
3、一个向量是否与一个平面平行?
如果这个向量与平面的法向量垂直的话,那么这个向量就是与这个平面平行的。
我们已经知道,如果两个向量的点积等于0的时候这两个向量是垂直的。
所以当向量与平面法向量的点积等于0的时候,那么这个向量就是与这个平面平行的。
4、线段是否与一个平面相交
让我们假设下,线段从P1点开始到P2点结束。在平面上的一个特定点是SP而平面的法向量是SN。
如果我们假想一个平面穿过线段的第一个点P1,那么要解决这个问题就可以转换为计算哪个点(P2还是SP)既更接近P1又与SN更加平行。这个值可以通过点积计算得到,如下所示:
dot1 = SN dot (SP - P1) dot2 = SN dot (P2 - P1)
你可以计算它与平面相交的”程度“,也就是将这两个值相比较(相除):
u = (SN dot (SP - P1)) / (SN dot (P2 - P1))
1、如果 u == 0,那么线段是与平面平行的。
2、如果 u <= 1 并且 u > 0, 那么线段与平面相交。
3、如果 u > 1,那么线段与平面不相交。
可以将线段的向量与u相乘得到精确的相交点:
intersectionpoint = (P2 - P1) * u
5、向量积(Cross Product)
向量积也是对两个向量的一个操作,结果是一个新的向量,它与前两个向量垂直,并且它长度是前两个向量长度的均值。
注意:向量积,数学中又称外积、叉积,物理中称矢积、叉乘,是一种在向量空间中向量的二元运算。与点积不同,它的运算结果是一个向量而不是一个标量,并且两个向量的叉积与这两个向量的和垂直。
两个向量a和b的叉积写作a×b(有时也被写成a∧b,避免和字母x混淆)。
向量积可以被定义为:|向量a×向量b|=|a||b|sinθ在这里θ表示两向量之间的夹角(共起点的前提下)(0° ≤ θ ≤ 180°),它位于这两个矢量所定义的平面上。
需要注意的是对于向量积操作来说,参数的顺序是有影响的,如果调换了参数的顺序,生成的结果向量长度不变,但是方向将会完全相反。
假设物体以某个角度往墙那里移动,但是墙是无摩擦的,所以物体应该沿着墙的表面移动而不是停下来,在这种情况下,如何计算物体的新位置?
首先,我们用一个向量来表示如果没有墙的情况下物体应该移动的距离,我们将称它为“变化向量“,然后,我们将假设物体触碰到了墙,并且我们还需要墙表面的法向量。
我们将使用向量积来得到一个新的向量,它与”变化向量“和平面法向量相垂直:
temp_vector = change crossplane_normal
然后,最后的方向是与新的向量以及之前的平面法向量相垂直的:
new_direction = temp_vectorcross plane_normal
所以,就如下面代码这样得到最后的结果:
new_direction = (change crossplane_normal) cross plane_normal
简单的2D追踪
有了向量的基本知识,我们就可以分析一个常见的问题:屏幕上一点到另一点的追踪,其实这一问题也可理解为画线问题,画线的算法有很多:DDA画线法、中点画线法以及高效的Bresenham算法。但这些算法一般只是画一些两端固定的线段时所使用的方法,再做一些动态的点与点之间的跟踪时显得不很灵活,使用向量的方法可以很好的解决此类问题。
现在假设你正在编写一个飞行射击游戏,你的敌人需要一种很厉害的武器,如:跟踪导弹,这种武器在行进的同时不断的修正自己与目标之间的位置关系,使得指向的方向总是玩家,而不论玩家的位置在哪里,这对一个水平不高的玩家(我?)来说可能将是灭顶之灾,玩家可能很诧异敌人会拥有这么先进的秘密武器,但对于你来说只需要再程序循环中加入几行代码,它们的原理是向量的单位化和基本向量运算。
首先,我们要知道玩家的位置(x_player, y_player)。
然后,我们的导弹就可以通过计算得到一个有初始方向的速度,速度的方向根据玩家的位置不断修正,它的实质是一个向量减法的计算过程。
速度的大小我们自己来设置,它可快可慢,视游戏难易度而定,它的实质就是向量单位化和数乘向量的过程。具体算法是:导弹的更新速度(vx_missile, vy_missile) = 玩家的位置(x_player, y_player) - 导弹的位置(x_missile, y_missile),然后再对(vx_missile, vy_missile)做缩小处理,导弹移动,判断是否追到玩家,重新更新速度,缩小...
看一下这个简单算法的代码:
// 假设x_player,y_player是玩家位置分量
// x_missile,y_missile是导弹位置分量
// xv_missile,yv_missile是导弹的速度分量
// 让我们开始吧!
float n_missile ; // 这是玩家位置与导弹位置之间向量的长度 float v_rate ; // 这是导弹的速率缩放比率
// 计算一下玩家与导弹之间的位置向量
xv_missile = x_player-x_missile ; // 向量减法,方向由导弹指向玩家,x分量 yv_missile = y_player-y_missile ; // y分量
// 计算一下它的长度
n_missile = sqrt( xv_missile*xv_missile + yv_missile*yv_missile ) ;
// 归一化导弹的速度向量:
xv_missile /= n_missile ; yv_missile /= n_missile ;
// 此时导弹的速率为1,注意这里用速率。
// 导弹的速度分量满足xv_missile^2+yv_missile^2=1
// 好!现在导弹的速度方向已经被修正,它指向玩家。
// 由于现在的导弹速度太快,为了缓解一下紧张的气氛,我要给导弹减速
v_rate = 0.2f ; // 减速比率 xv_missile *= v_rate ; // 这里的速率缩放比率,你可以任意调整大小 yv_missile *= v_rate ; // 可以加速:v_rate大于1;减速v_rate大于0小于1,这里就这么做!
// 导弹行进!导弹勇敢的冲向玩家!
x_missile += xv_missile ; y_missile += yv_missile ;
// 然后判断是否攻击成功
现在,你编写的敌人可以用跟踪导弹攻击玩家了,你也可以稍加修改,变为直线攻击武器,这样比较普遍,基本的跟踪效果用向量可以很好的模拟。