向量几何在游戏编程中的使用【2】2-D物体任意角度的反弹
http://blog.csdn.net/popy007/article/details/376937
<2>2-D物体任意角度的反弹
-Twinsen编写
-本人水平有限,疏忽错误在所难免,还请各位数学高手、编程高手不吝赐教
-我的Email-address: popyy@netease.com
第一次我说了一下向量知识的基础内容和一点使用技巧,浅显的展示了它在游戏编程中的作用。这次深入一些,充分利用向量的性质模仿一个物理现象。
首先,我要介绍一下将要使用的两个基本但非常重要的技巧。
一、求与某个向量a正交的向量b
根据向量内积的性质以及正交向量之间的关系,有:
设a=(xa,ya),b=(xb,yb)
a.b = 0
=> xa*xb + ya*yb = 0
=> xa*xb = -ya*yb
=> xa/-ya = yb/xb
=> xb = -ya , yb = xa 或 xb = ya , yb = -xa
则向量(xa,ya)的正交向量为(xb,yb)=(-ya,xa)
比如上图中,向量(2,3)的逆时针旋转90度的正交向量是(-3,2),顺时针旋转90度的正交向量为(3,-2)。
这样,任给一个非零向量(x,y),则它相对坐标轴逆时针转90度的正交向量为(-y,x),顺时针转90度的正交向量为(y,-x)。
二、计算一个向量b与另一向量a共线的两个相反的投影向量
我们看一下上面的图,很明显,cosA(A=X)关于y轴对称,是偶函数,因此cosA = cos(-A),
又因为cosA是周期函数,且周期是2*PI,则有cos(A+2*PI) = cosA = cos(-A) = cos(-A+2*PI),
则根据cosA = cos(2*PI-A)以及a.b = |a|*|b|*cosA,有
a.b = |a|*|b|*cosA = |a|*|b|*cos(2*PI-A)
现在,根据上图,就有a.b = |a|*|b|*cosA = |a|*|b|*cos(2*PI-A) = ax*bx + ay*by
按照这个规则,当上面的b与c的模相等时,有|a|*|b| = |a|*|c|,进一步的,当它们与a的夹角A
= B时,就有
a.b = |a|*|b|*cosA = |a|*|c|*cosB = a.c ,相应的有
a.b = |a|*|b|*cosA = |a|*|b|*cos(2*PI-A) = |a|*|c|*cosB = |a|*|c|*cos(2*PI-B)
= a.c 也就是
ax*bx + ay*by = ax*cx + ay*cy
我们还注意到在一个周期内,比如在[0,2*PI]中,cosA有正负两种情况,分别是:在(0,PI/2)&(3*PI/2, 2*PI)为正,在(PI/2,3/2*PI)为负。好,知道了这件事情之后,再看a.b = |a|*|b|*cosA,|a|和|b|都为正,所以a.b的正负性就由cosA决定,换句话说,a.b与它们夹角A的余弦cos有相同的符号。所以,还看上面的图,我们就有:
1)当A在(0, PI/2)&(3*PI/2, 2*PI)中,此时2*PI-A在(-PI/2,0)&(0, PI/2)中,a.b为正
2)当A在(PI/2, 3*PI/2)中,此时2*PI-A也在(PI/2, 3*PI/2)中,a.b为负
现在我们再来看一下同模相反(夹角为PI)向量b和b'与同一个向量a的两个内积之间有什么关系。
首先B + B'= 2*PI - PI = PI,所以有b = -b', b' = -b,即
(bx, by) = (-b'x, -b'y) = -(b'x, b'y)
(b'x, b'y) = (-bx, -by) = -(bx, by)
所以
a.b =(ax, ay) . (bx, by) = (ax, ay) . -(b'x, b'y) = a.-b'= -(a.b')
a.b'= (ax, ay) . (b'x, b'y) = (ax, ay) . -(bx, by) = a.-b = -(a.b)
我们看到,一个向量b的同模相反向量b'与向量a的内积a.b',等于b与a的内积的相反数-(a.b)。
好,有了上面的基础,我们就可以求一个向量b与另一向量a共线的两个相反的投影向量c和c'了。
要求b在a上的投影向量c,我们可以用一个数乘上一个单位向量,这个单位向量要和a方向一至,我们记为a1。而这个数就是b在a上的投影长。
先来求单位向量a1,我们知道它就是向量a乘上它自身长度的倒数(数乘向量),它的长度我们
可以求出,就是m = sqrt(ax^2 + ay^2),所以a1就是(ax/m, ay/m),记为(a1x, a1y)。
再求投影长/c/(注意//与||的区别,前者是投影长,可正可负也可为零,后者是实际的长度,衡为非负)。 根据内积的几何意义:一个向量b点乘另一个向量a1,等于b在a1上投影长与a1的长的乘积。那我们要求b在a上的投影长,就用它点乘a的单位向量a1就可以了,因为单位向量的长度为1,b的投影长/c/乘上1还等于投影长自身,即:
/c/ = b.a1 = (bx, by) . (a1x, a1y) = bx * a1x + by * a1y
好,我们得到了c的投影长,现在就可以求出c:
c = /c/*a1 = ( (bx * a1x + by * a1y)*a1x, (bx * a1x + by * a1y)*a1y )
总结一下,就是c = (b.a1)*a1。
我们看到,b与a1的夹角在(0, PI/2)之间,因此它们的点积/c/是个正值。因此当它乘a1之后,得到向量的方向就是a1的方向。
现在来看b',它是b的同模相反向量,它和a1的夹角在(PI/2, 3*PI/2)之间,因此b'点乘a1之后得到/c'/是个负值,它再乘a1,得到向量的方向和a1相反。我们知道,一个向量b的同模相反向量b'与向量a的内积a.b',等于b与a的内积的相反数-(a.b)。因此,/c'/
= -/c/,也就是说,它们的绝对值相等,符号相反。因此它们同乘一个a1,得到的的两个模相等向量c与c'共线。
让我们把它完成:
(b'.a1) = -(b.a1)
=> -(b'.a1) = (b.a1), 好,代入c = (b.a1)*a1,得到
c = -(b'.a1)*a1
=> (b'.a1)*a1 = -c = c'
c = ( b . a1 ) * a1 = (-b'. a1) * a1
c'= ( b'. a1 ) * a1 = (-b . a1) * a1
至此为止,我们得出结论:当一个向量b与另一个向量a的夹角在(0, PI/2)&(3*PI/2, 2*PI)之间,它在a方向上的投影向量c就是c = ( b . a1 ) * a1,其中a1是a的单位向量;它在a相反方向的投影向量c'是c'=
( b'. a1 ) *a1,其中向量b'是b的同模相反向量。
相反的,也可以这样说:当一个向量b'与另一个向量a的夹角在(PI/2, 3*PI/2)之间,它在a相反方向上的投影向量c'是
c'= ( b'. a1 ) * a1,其中 a1是a的单位向量;它在a方向上的投影向量c是c = ( b . a1 ) * a1。其中向量b是b'的同模相反向量。
特别的,点乘两个单位向量,得到它们夹角的余弦值:
E.E = |E|*|E|*cosA = 1*1*cosA = cosA
好了,可完了。 现在就可以看一下
三、使用向量模拟任意角度反弹的原理
根据初等物理,相互接触的物体在受到外力具有接触面相对方向相对运动趋势的时候,接触面会发生形变从而产生相互作用的弹力。
弹力使物体形变或形变同时运动形式发生改变。在知道了这件事情之后,我们开始具体讨论下面这种情况:
矩形框和小球碰撞,碰撞时间极短,墙面无限光滑从而碰撞过程没有摩擦,碰撞时间极短,没有能量损失...总之是一个理想的物理环境。我们在这种理想环境下讨论,小球与墙面发生了完全弹性碰撞,且入射角和反射角相等:A=A',B=B',C=C',...。虚线是法线,它和墙面垂直。小球将在矩形框中永无休止的碰撞下去,且每次碰撞过程中入射角和反射角都相等。
我们再具体点,现在假设上面那个矩形墙壁的上下面平行于x轴,左右面平行于y轴。这样太好了,我们在编写程序的时候只要判断当球碰到上下表面的时候将y方向速度值取返,碰到左右表面时将x方向速度值取返就行了,这种方法常常用在简单物理模型和规则边界框的游戏编程上,这样可以简化很多编程步骤,编写简单游戏时可以这样处理。可事实不总是像想向中的那么好。如果情况像下面这样:
虽然在碰撞过程中入射角仍然等于反射角,但是边界的角度可没那么“纯”了,它们的角度是任意的,这样就不能简单的将x方向或者y方向的速度取返了,我们要另找解决办法。
我们现在的任务是:已知物体的速度向量S和边界向量b,求它的反射向量F。我们先来看一下在碰撞过程中都有哪些向量关系:
设b是障碍向量,S是入射速度向量,F是反射速度向量,也就是我们要计算的向量。A是入射角度,A'是反射角度,A=A'。N是b的法向量,即N垂直于b。n是与N共线的向量,n'是N方向的单位向量。T是垂直于N的向量。根据向量加法,现在有关系:
(1) S + n = T
(2) n + T = F
合并,得
F = 2*T - S
我们已经找到了计算F的公式了。这里S是已知的,我们要计算一下T,看(1)式:
T = S + n
要计算T,S是已知的,就要计算一下n。我们知道,n是S在N方向上投影得到的,S已知所以要得到n就要再计算一下N,而N又是和b垂直的。还记得刚才我们导出的使用向量的两个技巧吧,这里我们都要用到:
1、任给一个非零向量(x,y),则它相对坐标轴逆时针转90度的垂直向量为(-y,x),顺时针转90度垂直向量为(y,-x)。
2、当一个向量b与另一个向量a的夹角在(0, PI/2)&(3*PI/2, 2*PI)之间,它在a方向上的投影向量c就是c = ( b . a1 ) * a1,其中a1是a的单位向量;它在a相反方向的投影向量c'是c'=
( b'. a1 ) * a1,其中向量b'是b的同模相反向量。
我们知道了b,用技巧1可以计算出N。然后归一化N计算出n',再用技巧2,这里S和n'之间的夹角在(PI/2, 3*PI/2)中,因此要想用c =
( b. a1 ) * a1,必须要使b = -S,a1=n'。这样就计算出了n。然后根据上面的(1)式计算出T,好了,有了T和F =
2*T - S ,你就拥有了一切!
计算出的F就是物体碰撞后的速度向量,在2-D中它有两个分量x和y,3-D中有x,y,z三个分量。这里也证明了使用向量的一个好处就是在一些类似这样关系推导过程中不用去考虑坐标问题,而直接的用简单的向量就可以进行。
这里注意我们的障碍向量b在实际的编程中是用障碍的两个端点坐标相减计算出的,计算的时候不需要考虑相减的顺序问题。因为虽然用不同的相减顺序得到b的方向相反,且计算得到的单位法向量n'方向也相反(看上图的虚线部分),但是当用-S去点乘单位法向量n'之后得到的值也是相反的,它有一个自动的调节功能:现在假设以b为界,S一侧为正方向。则如果单位法向量n'是正方向,与-S点积值也是正,正的n'再乘正点积得正的n;如果单位法向量为负方向,与-S点积值也为负值,负的n'再乘负的点积得到的n为正方向。总之n的方向是不变的,算出的F当然也是不变的。
四、编码实现它
现在我想编码实现它,但之前有一点我想说一下,可能读者已经想到了,在反弹之前我们要先判断什么时候开始反弹,也就是什么时候碰撞,这是一个碰撞检测问题,本来这是我们应该先要解决的问题,但我想把它放到下一次在具体说,所以这里的编码省略碰撞检测的一步,直接计算反弹速度向量!目的是把上述理论迅速用到算法中去。
// 在游戏循环中
// 移动的物体简化为质点,位置是x=0.0f,y=0.0f
// 质点速度向量的分量是Svx=4.0f,Svy=2.0f
// 障碍向量是bx=14.0f-6.0f=8.0f,by=4.0f-12.0f=-8.0f
// 则障碍向量的垂直向量是Nx=-8.0f,Ny=-8.0f
// 这里可以加入碰撞检测
// 现在假设已经碰撞完毕,开始反弹计算!
// 计算N的长度
float lengthN = sqrt( Nx*Nx + Ny*Ny ) ;
// 归一化N为n'
float n0x = Nx / lengthN ; // n0x就是n'的x分量
float n0y = Ny / lengthN ; // n0y就是n'的y分量
// 计算n,就是S在N方向上的投影向量
// 根据b'= (-b.a1').a1',有n = (-S.n').n'
float nx = -(Svx*n0x+Svy*n0y)*n0x ; // n的x分量
float ny = -(Svx*n0x+Svy*n0y)*n0y ; // n的y分量
// 计算T
// T = S + n
float Tx = Svx + nx ; // T的x分量
float Ty = Svy + ny ; // T的y分量
// 有了T,有了F = 2*T - S,好了,你现在拥有一切了
// 计算F
float Fx = 2*Tx - Svx ; // F的x分量
float Fy = 2*Ty - Svy ; // F的y分量
// 现在已经计算出了反弹后的速度向量了
// 更新速度向量
Svx = Fx ;
Svy = Fy ;
// 质点移动
x+=Svx ;
y+=Svy ;
// 现在你就可以看到质点被无情的反弹回去了
// 而且是按照物理法则在理想环境下模拟