向量几何在游戏编程中的使用

<1>简单的2-D追踪

-Twinsen编写

-本人水平有限,疏忽错误在所难免,还请各位数学高手、编程高手不吝赐教
-我的Email-address: [email protected]


Andre Lamothe说:“向量几何是游戏程序员最好的朋友”。一点不假,向量几何在游戏编程中的地位不容忽视,因为在游戏程序员的眼中,显示屏幕就是一个坐标 系,运动物体的轨迹就是物体在这个坐标系曲线运动结果,而描述这些曲线运动的,就是向量。使用向量可以很好的模拟物理现象以及基本的AI。

现在,先来点轻松的,复习一下中学知识。

向量v(用粗体字母表示向量)也叫矢量,是一个有大小有方向的量。长度为1的向量称为单位向量,也叫幺矢,这里记为E。长度为0的向量叫做零向量,记为0,零向量没有确定方向,换句话说,它的方向是任意的。

一、向量的基本运算


 
1、向量加法:a+b等于使b的始点与a的终点重合时,以a的始点为始点,以b的终点为终点的向量。

2、向量减法:a-b等于使b的始点与a的始点重合时,以b的终点为始点,以a的终点为终点的向量。

3、 数量乘向量:k*a,k>0时,等于a的长度扩大k倍;k=0时,等于0向量;k<0时,等于a的长度扩大|k|倍然后反向。

4、向量的内积(数量积、点积): a.b=|a|*|b|*cosA 等于向量a的长度乘上b的长度再乘上a与b之间夹角的余弦。
   它的几何意义就是a的长度与b在a上的投影长度的乘积,或者是b的长度与a在b上投影长的乘积,它是一个标量,而
且可正可负。因此互相垂直的向量的内积为0。

5、向量的矢积(叉积): a x b = |a|*|b|*sinA*v = c, |a|是a的长度,|b|是b的长度,A是a和b之间的不大于180的夹角,v是与a,b所决定的平面垂直的幺矢,即axb与a、b都垂直。在右手坐标系下,a,b,c构成右手系,即右手拇指伸直,其余四指按由a到b的不大于180度的角卷曲,此时拇指所指方向就是c的方向。因此axb!=bxa。如果是左手系,那么上图中a x b = -c ,即a,b和-c构成左手系。a x b的行列式计算公式如上图右边所示。两个向量的矢积是一个向量。

6、正交向量的内积:互相垂直的两个向量是正交的,正交向量的内积为零。a.b = |a|.|b|*cos(PI/2) = |a|.|b|*0 = 0。

二、向量的性质

没有下面的这些性质做基础,我们后面向量技巧的推导将无法进行。

1) a + b = b + a
2) (a + b) + c = a + (b + c)
3) a + 0 = 0 + a = a
4) a + (-a) = 0
5) k*(l*a) = (k*l)*a = a*(k*l)
6) k*(a + b) = k*a + k*b
7) (k + l)*a = k*a + l*a
8) 1*a = a

9) a.b = b.a
10)a.(b + c) = a.b + a.c
11)k*(a.b) = (k*a).b = a.(k*b)
12)0.a = 0
13)a.a = |a|^2


三、自由向量的代数(分量)表示

1、向量在直角坐标中的代数表示方法:

a=(x,y)



其中x,y分别是向量在x轴和y轴上的分量。任何一个在直角坐标轴上的分量为(x,y)的向量都相等。比如上图中的每个向量都表示为(-2,1)。

或者写成a=x*i+y*j,即i和j的线性组合,这里i是x轴方向的单位向量(1,0),j是y轴方向的单位向量(0,1),因此i正交于j。任意一个2-D向量都可以表成i与j的线性组合。

|i| = |j| = 1

2、向量的代数(分量)表示的运算:

向量加法分量表示:a+b=(xa,ya)+(xb,yb)=(xa+xb,ya+yb)

向量减法分量表示:a-b=(xa,ya)-(xb,yb)=(xa-xb,ya-yb)

向量的内积(数量积、点积)分量表示:

a.b
=(xa * i + ya * j).(xb * i + yb * j)
= xa * i * xb * i + xa * i * yb * j + ya * j * xb * i + ya * j * yb * j
=(xa * xb) * (i * i) + (xa * yb) * (i * j) + (xb * ya) * (i * j) + (ya * yb) * (j * j)
= xa * xb + ya * yb


3、向量长度(模)的计算以及单位化(归一化):

设a=(x,y),则
|a| = |(x,y)| = |x*i + y*j| = sqrt(x^2*i^2 + y^2*j^2) = sqrt(x^2 + y^2),这里sqrt是开平方符号。

a的单位向量为a/|a|,即(x,y)/sqrt(x^2 + y^2)。

四、简单的2-D追踪

现在,有了向量的基本知识,我们就可以分析一个常见的问题-屏幕上一点到另一点的追踪,其实这一问题也可理解为画线 问题,画线的算法有很多: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 ;

// 然后判断是否攻击成功

现在,你编写的敌人可以用跟踪导弹攻击玩家了。你也可以稍加修改,变为直线攻击武器。这样比较普遍。
基本的跟踪效果用向量可以很好的模拟。

此时,我们只用到了所述向量知识的很少的一部分。其他的知识会慢慢用到游戏中。这次先介绍到这里。
下次我将说说利用向量模拟2-D物体任意角度返弹的技巧:)但是!别忘了复习一下向量的基础知识,我们要用到它们。

<2>2-D物体任意角度的反弹
-Twinsen编写

-本人水平有限,疏忽错误在所难免,还请各位数学高手、编程高手不吝赐教
-我的Email-address: [email protected]

第一次我说了一下向量知识的基础内容和一点使用技巧,浅显的展示了它在游戏编程中的作用。这次深入一些,充分利用向量的性质模仿一个物理现象。

首先,我要介绍一下将要使用的两个基本但非常重要的技巧。

一、求与某个向量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 ;
// 现在你就可以看到质点被无情的反弹回去了
// 而且是按照物理法则在理想环境下模拟


就是这么简单,一个物理现象就可以模拟出来,但是还不完善,只是针对直线障碍,且没有碰撞检测,下次分析一下后者,还是用向量的知识。这次先到这,See u next time!

<3>2-D边界碰撞检测
-Twinsen编写

-本人水平有限,疏忽错误在所难免,还请各位数学高手、编程高手不吝赐教
-我的Email-address: [email protected]

一、使用向量进行障碍检测的原理

上次说了使用向量模拟任意角度的反弹,这次谈谈它的前提---障碍碰撞。

在游戏中进行障碍碰撞检 测,基本思路是这样的:给定一个障碍范围,判断物体在这次移动后会不会进入这个范围,如果会,就发生碰撞,否则不发生碰撞。在实际操作中,是用物体的边界 来判断还是其他部位判断完全取决于编程者。这时候,就可以从这个部位沿着速度的方向引出一条速度向量线,判断一下这条线段(从检测部位到速度向量终点)和 障碍边界线有没有交点,如果有,这个交点就是碰撞点。

 

上面物体A,在通过速度向量移动之后将到达B位置。但是,这次移动将不会顺利进行,因为我们发现,碰撞发生了。碰撞点就在那个红色区域中,也就是速度向量和边界线的交点。 我们接下来的工作就是要计算这个交点,这是一个解线性方程组的过程,那么我们将要用到一样工具...


二、一个解线性方程组的有力工具---克兰姆(Cramer)法则

首先要说明一下的是,这个法则是有局限性的,它必须在一个线性方程组的系数行列式非零的时候才能够使用。别紧张,我会好好谈谈它们的。首先让我来叙述一下这个法则(我会试着让你感觉到这不是一堂数学课):

如果线性方程组:

A11*X1 + A12*X2 + ... + A1n*Xn = b1
A21*X1 + A22*X2 + ... + A2n*Xn = b2
...................................
An1*X1 + An2*X2 + ... + Ann*Xn = bn

的系数矩阵 A =
__               __
| A11 A12 ... A1n |
| A21 A22 ... A2n |
| ............... |
| An1 An2 ... Ann |
--               -- 

的行列式 |A| != 0 
线性方程组有解,且解是唯一的,并且解可以表示为:

X1 = d1/d , X2 = d2/d , ... , Xn = dn/d (这就是/A/=d为什么不能为零的原因)

这里d就是行列式/A/的值,dn(n=1,2,3...)是用线性方程组的常数项b1,b2,...,bn替换系数矩阵中的第n列的值得到的矩阵的行列式的值,即:

     | b1 A12 ... A1n |
d1 = | b2 A22 ... A2n |
     | .............. |
     | bn An2 ... Ann |

     | A11 b1 ... A1n |
d2 = | A21 b2 ... A2n |
     | .............. |
     | An1 bn ... Ann | 

...

     | A11 A12 ... b1 |
dn = | A21 A22 ... b2 |
     | .............. |
     | An1 An2 ... bn |

别去点击关闭窗口按钮!我现在就举个例子,由于我们现在暂时只讨论2-D游戏(3-D以后会循序渐进的谈到),就来个2-D线性方程组:

(1) 4.0*X1 + 2.0*X2 = 5.0
(2) 3.0*X1 + 3.0*X2 = 6.0

这里有两个方程,两个未知量,则根据上面的Cramer法则:

    | 4.0 2.0 |
d = | 3.0 3.0 | = 4.0*3.0 - 2.0*3.0 = 6.0 (2阶行列式的解法,'/'对角线相乘减去'/'对角线相乘)

     | 5.0 2.0 |
d1 = | 6.0 3.0 | = 5.0*3.0 - 2.0*6.0 = 3.0

     | 4.0 5.0 |
d2 = | 3.0 6.0 | = 4.0*6.0 - 5.0*3.0 = 9.0

则 

X1 = d1/d = 3.0/6.0 = 0.5
X2 = d2/d = 9.0/6.0 = 1.5   

好了,现在就得到了方程组的唯一一组解。 
是不是已经掌握了用Cramer法则解2-D线性方程组了?如果是的话,我们继续。


三、深入研究

这里的2-D障碍碰撞检测的实质就是判断两条线段是否有交点,注意不是直线,是线段,两直线有交点不一定直线上的线段也有交点。现在我们从向量的角度,写出两条线段的方程。

现在有v1和v2两条线段,则根据向量加法:

v1e = v1b + s*v1
v2e = v2b + t*v2

v1b和v2b分别是两线段的一端。s,t是两个参数,它们的范围是[0.0,1.0],当s,t=0.0时,v1e=v1b,v2e=v2b;当s,t=1.0时,v1e和v2e分别是两线段的另一端。s,t取遍[0.0,1.0]则v1e和v2e取遍两线段的每一点。

那么我们要判断v1和v2有没有交点,就让v1e=v2e,看解出的s,t是不是在范围内就可以了:

v1e = v2e
=> v1b + s*v1 = v2b + t*v2
=> s*v1 - t*v2 = v2b - v1b
写成分量形式:

s*x_v1 - t*x_v2 = x_v2b - x_v1b
s*y_v1 - t*y_v2 = y_v2b - y_v1b

现在是两个方程式,两个未知数,则根据Cramer法则:

    | x_v1 -x_v2 |   | 4.0 -2.0 |
d = | y_v1 -y_v2 | = | 1.0 -3.0 | = -10.0

     | x_v2b-x_v1b -x_v2 |   | 5.0 -2.0 |
d1 = | y_v2b-y_v1b -y_v2 | = | 2.0 -3.0 | = -11.0           

s = d1/d = -11.0/-10.0 = 1.1 > 1.0

现在s已经计算出来,没有在[0.0,1.0]内,所以两线段没有交点,从图上看很直观。t没有必要再计算了。所以是物体与障碍没有发生碰撞。如果计算出的s,t都在[0.0,1.0]内,则把它们带入原方程组,计算出v1e或者v2e,它的分量就是碰撞点的分量。

四、理论上的东西已经够多的了,开始写程序

我现在要写一个用于处理障碍碰撞检测的函数,为了测试它,我还准备安排一些障碍:

这是一个凸多边形,我让一个质点在初始位置(10,8),然后给它一个随机速度,这个随机速度的两个分速度在区间[1.0,4.0]内,同时检测是否与边界发生碰撞。当碰撞发生时,就让它回到初始位置,重新给一个随机速度。

// 首先我要记下凸多边形的边界坐标
float poly[2][8] = { 
{ 6.0f , 2.0f , 4.0f , 8.0f , 14.0f , 18.0f , 14.0f , 6.0f } , // 所有点的x分量,最后一个点和第一个点重合
{ 2.0f , 6.0f , 10.0f , 14.0f , 12.0f , 8.0f , 4.0f , 2.0f } // 所有点的y分量
} ;
// 定义一些变量
float x,y ; // 这是质点的位置变量
float vx , vy ; // 质点的速度向量分量

// 好,开始编写碰撞检测函数
bool CollisionTest() { // 当发生碰撞时返回true,否则返回false

float s , t ; // 线段方程的两个参数
// 各个参量
float x_v1 , x_v2 , y_v1 , y_v2 ; 
float x_v2b , x_v1b , y_v2b , y_v1b ; 

for( int i = 0 ; i < 8-1 ; ++i ) { // 循环到倒数第二个点

// 障碍线段
x_v1 = poly[0][i+1]-poly[0][i] ;
y_v1 = poly[1][i+1]-poly[1][i] ; 
// 物体速度向量
x_v2 = vx ;
y_v2 = vy ;
// 障碍向量初始点
x_v1b = poly[0][i] ;
y_v1b = poly[1][i] ;
// 物体位置
x_v2b = x ;
y_v2b = y ;
// 计算d,d1和d2
//    | x_v1 -x_v2 |   
//d = | y_v1 -y_v2 | 
//     | x_v2b-x_v1b -x_v2 |
//d1 = | y_v2b-y_v1b -y_v2 |
//     | x_v1 x_v2b-x_v1b |
//d2 = | y_v1 y_v2b-y_v1b |

d = (x_v1*(-y_v2))-((-x_v2)*y_v1) ;
d1 = ((x_v2b-x_v1b)*(-y_v2))-((-x_v2)*(y_v2b-y_v1b)) ;
d2 = (x_v1*(y_v2b-y_v1b))-((x_v2b-x_v1b)*y_v1) ;

// 判断d是否为零
if( abs(d) < 0.001f ) // 如果等于零做近似处理,abs()用于求绝对值
d = 0.001f ; 

// 计算参量s,t
s = d1/d ;
t = d2/d ;
// 判断是否发生碰撞
// 如果发生了就返回true
if( 0.0f <= s && 1.0f >= s && 0.0f <= t && 1.0f >= t ) 
return true ;

} // for( int i = 0 ; i < 8-1 ; ++i )

// 没有发生碰撞,返回false
return false ;

} // end of function

// 现在对函数做测试
// 初始化质点
x = 10.0f , y = 8.0f ;
vx = vy = (float)(rand()%4+1) ; 

// 进入主循环中
// 假设现在已经在主循环中 
if( CollisionTest() ) { // 如果物体与质点发生碰撞
x = 10.0f , y = 8.0f ;
vx = vy = (float)(rand()%4+1) ;
}
// 质点移动
x+=vx ;
y+=vy ;


现在你就可以结合上次的讨论模拟一个完整的理想物理情景:一个物体在不规则障碍中移动、反弹,永不停息...除非...

至此为止我们讨论了2-D游戏的障碍碰撞检测以及它的编程实现,在此过程中涉及到了线性代数学的知识,以后随着深入还会不断的加入更多的数学、物理知识。下次我们继续讨论,BYE! 

<4>2-D物体间的碰撞响应
-Twinsen编写 

-本人水平有限,疏忽错误在所难免,还请各位数学高手、编程高手不吝赐教
-我的Email-address: [email protected]

这次我要分析两个球体之间的碰撞响应,这样我们就可以结合以前的知识来编写一款最基本的2-D台球游戏了,虽然粗糙了点,但却是个很好的开始,对吗?

一、初步分析

中学时候上物理课能够认真听讲的人(我?哦,不包括我)应该很熟悉的记得:当两个球体在一个理想环境下相撞之后,它们的总动量保持不变,它们的总机械能也守恒。但这个理想环境是什么样的呢?理想环境会不会影响游戏的真实性?对于前者我们做出在碰撞过程中理想环境的假设:

1)首先我们要排除两个碰撞球相互作用之外的力,也就是假设没有外力作用于碰撞系统。
2)假设碰撞系统与外界没有能量交换。
3)两个球体相互作用的时间极短,且相互作用的内力很大。

有了这样的假设,我们就可以使用动量守恒和动能守恒定律来处理它们之间的速度关系了,因为1)确保没有外力参与,碰 撞系统内部动量守恒,我们就可以使用动量守恒定律。2)保证了我们的碰撞系统的总能量不会改变,我们就可以使用动能守恒定律。3)两球发生完全弹性碰撞, 不会粘在一起,没有动量、能量损失。

而对于刚才的第二个问题,我的回答是不会,经验告诉我们,理想环境的模拟看起来也是很真实的。除非你是在进行科学研究,否则完全可以这样理想的去模拟。

现在,我们可以通过方程来观察碰撞前后两球的速度关系。当两球球心移动方向共线(1-D处理)时的速度,或不共线(2-D处理)时共线方向的速度分量满足:

(1)m1 * v1 + m2 * v2 = m1 * v1' + m2 * v2' (动量守恒定律)
(2)1/2 * m1 * v1^2 + 1/2 * m2 * v2^2 = 1/2 * m1 * v1'^2 + 1/2 * m2 * v2'^2 (动能守恒定律)

这里m1和m2是两球的质量,是给定的,v1和v2是两球的初速度也是我们已知的,v1'和v2'是两球的末速度,是我们要求的。好,现在我们要推导出v1'和v2'的表达式:

由(1),得到v1' = (m1 * v1 + m2 * v2 - m2 * v2') / m1,代入(2),得
1/2 * m1 * v1^2 + 1/2 * m2 * v2^2 = 1/2 * m1 * (m1 * v1 + m2 * v2 - m2 * v2')^2 + 1/2 * m2 * v2'^2

=> v2' = (2 * m2 * v1 + v2 * (m1 - m2)) / (m1 + m2),则
=> v1' = (2 * m1 * v2 + v1 * (m1 - m2)) / (m1 + m2)

我们现在得到的公式可以用于处理当两球球心移动方向共线(1-D处理)时的速度关系,或者不共线(2-D处理)时共线方向的速度分量的关系。不管是前者还是后者,我们都需要把它们的速度分解到同一个轴上才能应用上述公式进行处理。

二、深入分析

首先我要说明一件事情:当两球碰撞时,它们的速度可以分解为球心连线方向的分速度和碰撞点切线方向的分速度。而由于 它们之间相互作用的力只是在切点上,也就是球心连线方向上,因此我们只用处理这个方向上的力。而在切线方向上,它们不存在相互作用的力,而且在理想环境下 也没有外力,因此这个方向上的力在碰撞前后都不变,因此不处理。好,知道了这件事情之后,我们就知道该如何把两球的速度分解到同一个轴上进行处理。

现在看上面的分析图,s和t是我们根据两个相碰球m1和m2的位置建立的辅助轴,我们一会就将把速度投影到它们上面。v1和v2分别是m1和m2的初速度,v1'和v2'是它们碰撞后的末速度,也就是我们要求的。s'是两球球心的位置向量,t'是它的逆时针正交向量。s1是s'的单位向量,t1是t'的单位向量。

我们的思路是这样的:首先我们假设两球已经相碰(在程序中可以通过计算两球球心之间的距离来判断)。接下来我们计算一下s'和t',注意s'和t'的方向正反无所谓(一会将解释),现在设m1球心为(m1x, m1y),m2球心为(m2x, m2y),则s'为(m1x-m2x, m1y-m2y),t'为(m2y-m1y, m1x-m2x)(第一篇的知识)。

则设
sM = sqrt((m1x-m2x)^2+(m1y-m2y)^2),
tM = sqrt((m2y-m1y)^2+(m1x-m2x)^2),有

s1 = ((m1x-m2x)/sM, (m1y-m2y)/sM) = (s1x, s1y)
t1 = ((m2y-m1y)/tM, (m1x-m2x)/tM) = (t1x, t1y)

现在s和t轴的单位向量已经求出了,我们根据向量点乘的几何意义,计算v1和v2在s1和t1方向上的投影值,然后将s轴上投影值代
入公式来计算s方向碰撞后的速度。注意,根据刚才的说明,t方向的速度不计算,因为没有相互作用的力,因此,t方向的分速度不变。所以我们要做的就是:把v1投影到s和t方向上,再把v2投影到s和t方向上,用公式分别计算v1和v2在s方向上的投影的末速度,然后把得到的末速度在和原来v1和v2在t方向上的投影速度再合成,从而算出v1'和v2'。好,我们接着这个思路做下去:

先算v1(v1x, v1y)在s和t轴的投影值,分别设为v1s和v1t:

v1s = v1.s1
=> v1s = v1x * s1x + v1y * s1y 

v1t = v1.t1
=> v1t = v1x * t1x + v1y * t1y

再算v2(v2x, v2y)在s和t轴的投影值,分别设为v2s和v2t:

v2s = v2.s1
=> v2s = v2x * s1x + v2y * s1y

v2t = v2.t1
=> v2t = v2x * t1x + v2y * t1y

接下来用公式

v1' = (2 * m1 * v2 + v1 * (m1 - m2)) / (m1 + m2) 
v2' = (2 * m2 * v1 + v2 * (m1 - m2)) / (m1 + m2) 

计算v1s和v2s的末值v1s'和v2s',重申v1t和v2t不改变:

假设m1 = m2 = 1

v1s' = (2 * 1 * v2s + v1s * (1 - 1)) / (1 + 1)
v2s' = (2 * 1 * v1s + v2s * (1 - 1)) / (1 + 1)

=> v1s' = v2s
=> v2s' = v1s

好,下一步,将v1s'和v1t再合成得到v1',将v2s'和v2t再合成得到v2',我们用向量和来做:

首先求出v1t和v2t在t轴的向量v1t'和v2t'(将数值变为向量)

v1t' = v1t * t1 = (v1t * t1x, v1t * t1y)
v2t' = v2t * t1 = (v2t * t1x, v2t * t1y)

再求出v1s'和v2s'在s轴的向量v1s'和v2s'(将数值变为向量)

v1s'= v1s' * s1 = (v1s' * s1x, v1s' * s1y) 
v2s'= v2s' * s1 = (v2s' * s2x, v2s' * s2y)

最后,合成,得

v1' = v1t' + v1s' = (v1t * t1x + v1s' * s1x, v1t * t1y + v1s' * s1y)
v2' = v2t' + v2s' = (v2t * t1x + v2s' * s2x, v2t * t1y + v2s' * s2y)

从而就求出了v1'和v2'。下面解释为什么说s'和t'的方向正反无所谓:不论我们在计算s'时使用m1的球心坐标减去m2的球心坐标还是相反的相减顺序,由于两球的初速度的向量必有一个和s1是夹角大于90度小于270度的,而另外一个与s1的夹角在0度和90度之间或者说在270度到360度之间,则根据向量点积的定义|a|*|b|*cosA,计算的到的两个投影值一个为负另一个为正,也就是说,速度方向相反,这样就可以用上面的公式区求得末速度了。同时,求出的末速度也是方向相反的,从而在转换为v1s'和v2s'时也是正确的方向。同样的,求t'既可以是用s'逆时针90度得到也可以是顺时针90度得到。

三、编写代码

按照惯例,该编写代码了,其实编写的代码和上面的推导过程极为相似。但为了完整,我还是打算写出来。

// 用于球体碰撞响应的函数,其中v1a和v2a为两球的初速度向量,
// v1f和v2f是两球的末速度向量。
// m1和m2是两球的位置向量
// s'的分量为(sx, sy),t'的分量为(tx, ty)
// s1是s的单位向量,分量为(s1x, s1y)
// t1是t的单位向量,分量为(t1x, t1y)

void Ball_Collision(v1a, v2a, &v1f, &v2f, m1, m2){

// 求出s'
double sx = m1.x - m2.x ; 
double sy = m1.y - m2.y ;

// 求出s1
double s1x = sx / sqrt(sx*sx + sy*sy) ;
double s1y = sy / sqrt(sx*sx + sy*sy) ;

// 求出t'
double tx = -sy ;
double ty = sx ;

// 求出t1
double t1x = tx / sqrt(tx*tx + ty*ty) ;
double t1y = ty / sqrt(tx*tx + ty*ty) ;

// 求v1a在s1上的投影v1s
double v1s = v1a.x * s1x + v1a.y * s1y ;

// 求v1a在t1上的投影v1t
double v1t = v1a.x * t1x + v1a.y * t1y ;

// 求v2a在s1上的投影v2s
double v2s = v2a.x * s1x + v2a.y * s1y ;

// 求v2a在t1上的投影v2t
double v2t = v2a.x * t1x + v2a.y * t1y ;

// 用公式求出v1sf和v2sf
double v1sf = v2s ;
double v2sf = v1s ;

// 最后一步,注意这里我们简化一下,直接将v1sf,v1t和v2sf,v2t投影到x,y轴上,也就是v1'和v2'在x,y轴上的分量
// 先将v1sf和v1t转化为向量 

double nsx = v1sf * s1x ;
double nsy = v1sf * s1y ;
double ntx = v1t * t1x ;
double nty = v1t * t1y ;

// 投影到x轴和y轴
// x轴单位向量为(1,0),y轴为(0,1)
// v1f.x = 1.0 * (nsx * 1.0 + nsy * 0.0) ;
// v1f.y = 1.0 * (nsx * 0.0 + nsy * 1.0) ;
// v1f.x+= 1.0 * (ntx * 1.0 + nty * 0.0) ;
// v1f.y+= 1.0 * (ntx * 0.0 + nty * 1.0) ;

v1f.x = nsx + ntx ;
v1f.y = nsy + nty ;

// 然后将v2sf和v2t转化为向量 

nsx = v2sf * s1x ;
nsy = v2sf * s1y ;
ntx = v2t * t1x ;
nty = v2t * t1y ;

// 投影到x轴和y轴
// x轴单位向量为(1,0),y轴为(0,1)
// v2f.x = 1.0 * (nsx * 1.0 + nsy * 0.0) ;
// v2f.y = 1.0 * (nsx * 0.0 + nsy * 1.0) ;
// v2f.x+= 1.0 * (ntx * 1.0 + nty * 0.0) ;
// v2f.y+= 1.0 * (ntx * 0.0 + nty * 1.0) ;

v2f.x = nsx + ntx ;
v2f.y = nsy + nty ;

}// end of function

呼~~是不是感觉有点乱阿?不管怎么样,我有这种感觉。但我们确实完成了它。希望你能够理解这个计算的过程,你完全可以依照这个过程自己编写更高效的代码,让它看上去更清楚:)至此位置,我们已经掌握了编写一个台球游戏的基本知识了,Let's make it!

事实上,一切才刚刚起步,我们还有很多没有解决的问题,比如旋转问题,击球的角度问题等等,你还会深入的研究一下,对吗?一旦你有了目标,坚持下去,保持激情,总会有成功的一天:)这次就到这里,下次我们接着研究,Bye for now~~

<5>物体的旋转
-Twinsen编写

-本人水平有限,疏忽错误在所难免,还请各位数学高手、编程高手不吝赐教
-我的Email-address: [email protected]



欢 迎回来这里!此次我们要讨论向量的旋转问题,包括平面绕点旋转和空间绕轴旋转两部分。对于游戏程序员来说,有了向量的旋转,就代表有了操纵游戏中物体旋转 的钥匙,而不论它是一个平面精灵还是一组空间的网格体亦或是我们放在3-D世界某一点的相机。我们仍需借助向量来完成我们此次的旅程,但这还不够,我们还 需要一个朋友,就是矩阵,一个我们用来对向量进行线性变换的GooL GuY。就像我们刚刚提及向量时所做的一样,我们来复习一下即将用到的数学知识。(这部分知识我只会一带而过,因为我将把重点放在后面对旋转问题的分析 上)

一、矩阵的基本运算及其性质

对于3x3矩阵(也叫3x3方阵,行列数相等的矩阵也叫方阵)m和M,有

1、矩阵加减法

m +(-) M =

[a b c]      [A B C]   [a+(-)A b+(-)B c+(-)C]
[d e f] +(-) [D E F] = [d+(-)D e+(-)E f+(-)F] 
[g h i]      [G H I]   [g+(-)G h+(-)H i+(-)I]

性质:
1)结合律 m + (M + N) = (m + M)  + N
2) 交换律 m + M = M + m

2、数量乘矩阵

k x M =

    [A B C]   [kxA kxB kxC]
k x [D E F] = [kxD kxE kxF] 
    [G H I]   [kxG kxH kxI]

性质:

k和l为常数
1) (k + l) x M = k x M + l x M
2) k x (m + M) = k x m + k x M
3) k x (l x M) = (k x l) x M
4) 1 x M = M
5) k x (m x M) = (k x m) x M = m x (k x M)

3、矩阵乘法

m x M =

[a b c]   [A B C}   [axA+bxD+cxG axB+bxE+cxH axC+bxF+cxI]
[d e f] x [D E F] = [dxA+exD+fxG dxB+exE+fxH dxC+exF+fxI]
[g h i]   [G H I]   [gxA+hxD+ixG gxB+hxE+ixH gxC+hxF+ixI]

可以看出,矩阵相乘可以进行的条件是第一个矩阵的列数等于第二个矩阵的行数。
由矩阵乘法的定义看出,矩阵乘法不满足交换率,即在一般情况下,m x M != M x m。

性质:
1) 结合律 (m x M) x N = m x (M x N)
2) 乘法加法分配律 m x (M + N) = m x M + m x N ; (m + M) x N = m x N + M x N 

4、矩阵的转置
      
m' = 
 
[a b c]'   [a d g]
[d e f]  = [b e h]  
[g h i]    [c f i]

性质: 
1)(m x M)' = M' x m'
2)(m')' = m
3)(m + M)' = m' + M'
4)(k x M)' = k x M'    

5、单位矩阵
   
    [1 0 0]
E = [0 1 0] 称为3级单位阵
    [0 0 1]

性质:对于任意3级矩阵M,有E x M = M ; M x E = M

6、矩阵的逆

如果3x3级方阵m,有m x M = M x m = E,这里E是3级单位阵,则可以说m是可逆的,它的逆矩阵为M,也记为m^-1。相反的,也可以说M是可逆的,逆矩阵为m,也记为M^-1。

性质:
1) (m^-1)^-1 = m
2) (k x m)^-1 = 1/k x m^-1
3)(m')^-1 = (m^-1)'
4) (m x M)^-1 = M^-1 x n^-1

矩阵求逆有几种算法,这里不深入研究,当我们用到的时候在讨论。
在我们建立了矩阵的概念之后,就可以用它来做坐标的线性变换。好,现在我们开始来使用它。

二、基础的2-D绕原点旋转

首先是简单的2-D向量的旋转,以它为基础,我们会深入到复杂的3-D旋转,最后使我们可以在3-D中无所不能的任意旋转。

在2-D的迪卡尔坐标系中,一个位置向量的旋转公式可以由三角函数的几何意义推出。比如上图所示是位置向量R逆时针旋转角度B前后的情况。在左图中,我们有关系:

x0 = |R| * cosA
y0 = |R| * sinA
=>
cosA = x0 / |R|
sinA = y0 / |R|

在右图中,我们有关系:

x1 = |R| * cos(A+B)
y1 = |R| * sin(A+B)

其中(x1, y1)就是(x0, y0)旋转角B后得到的点,也就是位置向量R最后指向的点。我们展开cos(A+B)和sin(A+B),得到

x1 = |R| * (cosAcosB - sinAsinB)
y1 = |R| * (sinAcosB + cosAsinB)

现在把
cosA = x0 / |R|
sinA = y0 / |R|

代入上面的式子,得到

x1 = |R| * (x0 * cosB / |R| - y0 * sinB / |R|)
y1 = |R| * (y0 * cosB / |R| + x0 * sinB / |R|)
=>
x1 = x0 * cosB - y0 * sinB
y1 = x0 * sinB + y0 * cosB

这样我们就得到了2-D迪卡尔坐标下向量围绕圆点的逆时针旋转公式。顺时针旋转就把角度变为负:

x1 = x0 * cos(-B) - y0 * sin(-B)
y1 = x0 * sin(-B) + y0 * cos(-B)
=>
x1 = x0 * cosB + y0 * sinB
y1 = -x0 * sinB + y0 * cosB

现在我要把这个旋转公式写成矩阵的形式,有一个概念我简单提一下,平面或空间里的每个线性变换(这里就是旋转变换)都对应一个矩阵,叫做变换矩阵。对一个点实施线性变换就是通过乘上该线性变换的矩阵完成的。好了,打住,不然就跑题了。

所以2-D旋转变换矩阵就是:

[cosA  sinA]      [cosA -sinA]
[-sinA cosA] 或者 [sinA cosA]

我们对点进行旋转变换可以通过矩阵完成,比如我要点(x, y)绕原点逆时针旋转:

          [cosA  sinA]   
[x, y] x  [-sinA cosA] = [x*cosA-y*sinA  x*sinA+y*cosA]

为了编程方便,我们把它写成两个方阵

[x, y]   [cosA  sinA]   [x*cosA-y*sinA  x*sinA+y*cosA]
[0, 0] x [-sinA cosA] = [0              0            ]

也可以写成

[cosA -sinA]   [x 0]   [x*cosA-y*sinA  0]
[sinA  cosA] x [y 0] = [x*sinA+y*cosA  0]


三、2-D的绕任一点旋转

下面我们深入一些,思考另一种情况:求一个点围绕任一个非原点的中心点旋转。

我 们刚刚导出的公式是围绕原点旋转的公式,所以我们要想继续使用它,就要把想要围绕的那个非原点的中心点移动到原点上来。按照这个思路,我们先将该中心点通 过一个位移向量移动到原点,而围绕点要保持与中心点相对位置不变,也相应的按照这个位移向量位移,此时由于中心点已经移动到了圆点,就可以让同样位移后的 围绕点使用上面的公式来计算旋转后的位置了,计算完后,再让计算出的点按刚才的位移向量 逆 位移,就得到围绕点绕中心点旋转一定角度后的新位置了。看下面的图


现在求左下方的蓝色点围绕红色点旋转一定角度后的新位置。由于红色点 不在原点,所以可以通过红色向量把它移动到原点,此时蓝色的点也按照这个向量移动,可见,红色和蓝色点的相对位置没有变。现在红色点在原点,蓝色点可以用 上面旋转变换矩阵进行旋转,旋转后的点在通过红色向量的的逆向量回到它实际围绕下方红色点旋转后的位置。

在这个过程中,我们对围绕点进行了三次线性变换:位移变换-旋转变换-位移变换,我们把它写成矩阵形式:

设红色向量为(rtx, rty)

[x y 1]   [1   0   0]   [cosA  sinA 0]   [1    0    0]   [x' y' -]
[0 1 0] x [0   1   0] x [-sinA cosA 0] x [0    1    0] = [-  -  -] 
[0 0 1]   [rtx rty 1]   [0     0    1]   [-rtx -rty 1]   [-  -  -]

最后得到的矩阵的x'和y'就是我们旋转后的点坐标。

注意到矩阵乘法满足结合律:(m x M) x N = m x (M x N),我们可以先将所有的变换矩阵乘在一起,即

    [1   0   0]   [cosA  sinA 0]   [1    0    0]  
M = [0   1   0] x [-sinA cosA 0] x [0    1    0]   
    [rtx rty 1]   [0     0    1]   [-rtx -rty 1]  

然后再让

[x y 1]
[0 1 0] x M 
[0 0 1]

像这样归并变换矩阵是矩阵运算一个常用的方法,因为当把诸多变换矩阵归并为一个矩阵之后,对某点或向量的重复变换只需要乘一个矩阵就可以完成,减少了计算的开销。

本 小节讨论的这种“其他变换-绕点旋转变换-其他变换”的思想很重要,因为有时候复杂一些的旋转变换不可能一步完成,必须使用这种旁敲侧击、化繁为简的方 法,尤其是在3-D空间中,可能需要在真正做规定度数的旋转前还要做一些其他必要旋转变换,也就是要做很多次的旋转,但总体的思想还是为了把复杂的问题分 成若干简单的问题去解决,而每一个简单问题都需要一个变换矩阵来完成,所以希望读者深入思考一下这种方法。

好,2-D的旋转探讨完毕。接下来,我们进入3-D空间,讨论更为复杂一些的旋转。Here We Go!

 
四、基础的3-D绕坐标轴方向旋转

就像2-D绕原点旋转一样,3-D的绕坐标轴旋转是3-D旋转的基础,因为其他复杂的3-D旋转最后都会化简为绕坐 标轴旋转。其实,刚才我们推导出的在xoy坐标面绕o旋转的公式可以很容易的推广到3-D空间中,因为在3-D直角坐标系中,三个坐标轴两两正交,所以z 轴垂直于xoy面,这样,在xoy面绕o点旋转实际上在3-D空间中就是围绕z轴旋转,如下图左所示:

这张图描述了左手系中某点在xoy、yoz、xoz面上围绕原点旋转的情况,同时也是分别围绕z、x、y坐标轴旋转。可见在3-D空间中绕坐标轴旋转相当于在相应的2-D平面中围绕原点旋转。我们用矩阵来说明:

设p(x, y, z)是3-D空间中的一点,也可以说是一个位置向量,当以上图中的坐标为准,p点所围绕的中心轴指向你的屏幕之外时,有

p绕z轴逆时针和顺时针旋转角度A分别写成:

[x y z 1]   [cosA -sinA 0 0]    [x y z 1]   [cosA sinA  0 0]
[0 1 0 0] x [sinA cosA  0 0] 和 [0 1 0 0] x [-sinA cosA 0 0] 
[0 0 1 0]   [0    0     1 0]    [0 0 1 0]   [0     0    1 0]
[0 0 0 1]   [0    0     0 1]    [0 0 0 1]   [0     0    0 1]

p绕x轴逆时针和顺时针旋转角度A分别写成:

[x y z 1]   [1 0     0    0]    [x y z 1]   [1 0     0    0]
[0 1 0 0] x [0 cos  -sinA 0] 和 [0 1 0 0] x [0 cosA  sinA 0] 
[0 0 1 0]   [0 sin  cosA  0]    [0 0 1 0]   [0 -sinA cosA 0]
[0 0 0 1]   [0 0    0     1]    [0 0 0 1]   [0 0     0    1]

p绕y轴逆时针和顺时针旋转角度A分别写成:

[x y z 1]   [cosA  0 sinA 0]    [x y z 1]   [cosA 0  -sinA 0]
[0 1 0 0] x [0     1 0    0] 和 [0 1 0 0] x [0     1  0    0] 
[0 0 1 0]   [-sinA 0 cosA 0]    [0 0 1 0]   [sinA  0  cosA 0]
[0 0 0 1]   [0     0 0    1]    [0 0 0 1]   [0     0  0    1]

以后我们会把它们写成这样的标准4x4方阵形式,Why?为了便于做平移变换,还记得上小节做平移时我们把2x2方阵写为3x3方阵吗?

让我们继续研究。我们再把结论推广一点,让它适用于所有和坐标轴平行的轴,具体一点,让它适用于所有和y轴平行的轴。
这个我们很快可以想到,可以按照2-D的方法“平移变换-旋转变换-平移变换”来做到,看下图

要实现point绕axis旋转,我们把axis按照一个位移向量移动到和y轴重合的位置,也就是变换为 axis',为了保持point和axis的相对位置不变,point也通过相同的位移向量做相应的位移。好,现在移动后的point就可以用上面的旋转 矩阵围绕axis'也就是y轴旋转了,旋转后用相反的位移向量位移到实际围绕axis相应度数的位置。我们还是用矩阵来说明:

假设axis为x = s, z = t,要point(x, y, z)围绕它逆时针旋转度数A,按照“平移变换-旋转变换-位移变换”,我们有

[x y z 1]   [1  0 0  0]   [cosA  0 sinA 0]   [1 0 0 0]   [x' y z' -]
[0 1 0 0]   [0  1 0  0]   [0     1 0    0]   [0 1 0 0]   [-  - -  -]
[0 0 1 0] x [0  0 1  0] x [-sinA 0 cosA 0] x [0 0 1 0] = [-  - -  -]
[0 0 0 1]   [-s 0 -t 1]   [0     0 0    1]   [s 0 t 1]   [-  - -  -]

则得到的(x', y, z')就是point围绕axis旋转角A后的位置。

同理,平行于x轴且围绕轴y=s,z=t逆时针旋转角A的变换为

[x y z 1]   [1  0 0  0]   [1 0    0     0]   [1 0 0 0]   [x  y' z' -]
[0 1 0 0]   [0  1 0  0]   [0 cosA -sinA 0]   [0 1 0 0]   [-  -  -  -]
[0 0 1 0] x [0  0 1  0] x [0 sinA cosA  0] x [0 0 1 0] = [-  -  -  -]
[0 0 0 1]   [0 -s -t 1]   [0 0    0     1]   [0 s t 1]   [-  -  -  -]

平行于z轴且围绕轴x=s,y=t逆时针旋转角A的变换为

[x y z 1]   [1  0  0  0]   [cosA -sinA 0 0]   [1 0 0 0]   [x' y' z  -]
[0 1 0 0]   [0  1  0  0]   [sinA cosA  0 0]   [0 1 0 0]   [-  -  -  -]
[0 0 1 0] x [0  0  1  0] x [0    0     1 0] x [0 0 1 0] = [-  -  -  -]
[0 0 0 1]   [-s -t 0  1]   [0    0     0 1]   [s t 0 1]   [-  -  -  -]

逆时针旋转就把上面推出的相应逆时针旋转变换矩阵带入即可。至此我们已经讨论了3-D空间基本旋转的全部,接下来的一小节是我们3-D旋转部分的重头戏,也是3-D中功能最强大的旋转变换。


五、3-D绕任意轴的旋转


Wow!终于来到了最后一部分,这一节我们将综合运用上面涉及到的所有旋转知识,完成空间一点或着说位置向 量围绕空间任意方向旋转轴的旋转变换(我在下面介绍的一种方法是一个稍微繁琐一点的方法,大体上看是利用几个基本旋转的综合。我将在下一篇中介绍一个高档 一些的方法)。

何谓任意方向的旋转轴呢?其实就是空间一条直线。在空间解析几何中,决定空间直线位置的两个值是直线上一点以及直线的方向向量。在旋转中,我们把这个直线称为一个旋转轴,因此,直线的这个方向向量我们叫它轴向量,它类似于3-D动画中四元数的轴向量。我们在实际旋转之前的变换矩阵需要通过把这个轴向量移动到原点来获得。


我 们先讨论旋转轴通过原点的情况。目前为止对于3-D空间中的旋转,我们可以做的只是绕坐标轴方向的旋转。因此,当我们考虑非坐标轴方向旋转的时候,很自然 的想到,可以将这个旋转轴通过变换与某一个坐标轴重合,同时,为了保持旋转点和这个旋转轴相对位置不变,旋转点也做相应的变换,然后,让旋转点围绕相应旋 转轴重合的坐标轴旋转,最后将旋转后的点以及旋转轴逆变换回原来的位置,此时就完成了一点围绕这个非坐标轴方向旋转轴的旋转。我们再来看图分析。

图中有一个红色的分量为(x0, y0, z0)的轴向量,此外有一个蓝色位置向量围绕它旋转,由于这个轴向量没有与任何一个坐标轴平行,我们没有办法使用上面推导出的旋转变换矩阵,因此必须将该 轴变换到一个坐标轴上,这里我们选择了z轴。在变换红色轴的同时,为了保持蓝色位置向量同该轴的相对位置不变,也做相应的变换,然后就出现中图描述的情 况。接着我们就用可以用变换矩阵来围绕z轴旋转蓝色向量相应的度数。旋转完毕后,再用刚才变换的逆变换把两个向量相对位置不变地还原到初始位置,此时就完 成了一个点围绕任意过原点的轴的旋转,对于不过原点的轴我们仍然用“位移变换-旋转变换-位移变换”的方法,一会讨论。

在理解了基本思路之后,我们来研究一下变换吧!我们就按上图将红色轴变到z轴上,开始吧!

首先我们假设红轴向量是一个单位向量,因为这样在一会求sin和cos时可以简化计算,在实际编程时可以先将轴向量标准化。然后我准备分两步把红色轴变换到z轴上去:

1)将红色轴变换到yoz平面上
2) 将yoz平面上的红色轴变到z轴上

至于这两个变换的方法...我实在没有别的办法了,只能够旋转了,你觉得呢?先把它旋转到yoz平面上。

我们设轴向量旋转到yoz面的变换为(绕z轴旋转):

[cosA  sinA 0 0]
[-sinA cosA 0 0]
[0     0    1 0]
[0     0    0 1] 

接着我们要求出cosA和sinA,由上图,沿着z轴方向看去,我们看到旋转轴向量到yoz面在xoy面就是将轴的投影向量旋转角度A到y轴上,现在我不知道角度A,但是我们可以利用它直接求出cosA和sinA,因为我们知道关系:

cosA = y0 / 轴向量在xoy面的投影长
sinA = x0 / 轴向量在xoy面的投影长

我们设轴向量的投影长为lr = sqrt(x0^2 + y0^2),呵呵,现在,我们第一步的变换矩阵就出来了:

[y0/lr  x0/lr 0 0]
[-x0/lr y0/lr 0 0]
[0      0     1 0]
[0      0     0 1]

同时我们得到逆变换矩阵:

[y0/lr -x0/lr 0 0]
[x0/lr y0/lr  0 0]
[0      0     1 0]
[0      0     0 1]

然后我们进行第二步:将yoz平面上的红色轴变到z轴上。我们的变换矩阵是(绕x轴旋转):

[1 0     0    0]
[0 cosB  sinB 0]
[0 -sinB cosB 0]
[0 0     0    1]

由图,这是经第一次旋转后的轴向量在yoz面中的情形,此次我们要求出上面变换中的cosB和sinB,我们仍不知 道角度B,但我们还是可以利用它求cosB和sinB。由于第一次旋转是围绕z轴,所以轴向量的z分量没有变,还是z0。此外,轴向量现在的y分量和原来 不同了,我们再看一下第一次变换那张图,可以发现轴向量在旋转到yoz面后,y分量变成了刚才轴向量在xoy面上的投影长lr了。Yes!我想是时候写出 cosB和sinB了:

cosB = z0 / 轴向量的长
sinB = lr / 轴向量的长

还记得我们刚才假设轴向量是一个单位向量吗?所以

cosB = z0
sinB = lr

至此我们的第二个变换就出来了:

[1 0   0   0]
[0 z0  lr  0]
[0 -lr z0  0]
[0 0   0   1]

相应逆变换矩阵:

[1 0   0   0]
[0 z0  -lr 0]
[0 lr  z0  0]
[0 0   0   1]

现在总结一下,我们对于空间任意点围绕某个任意方向且过原点的轴旋转的变换矩阵就是:



    [y0/lr  x0/lr 0 0]   [1 0   0  0]   [cosA  sinA 0 0]   [1 0  0   0]   [y0/lr  -x0/lr 0 0]
    [-x0/lr y0/lr 0 0]   [0 z0  lr 0]   [-sinA cosA 0 0]   [0 z0 -lr 0]   [x0/lr  y0/lr  0 0]
M = [0      0     1 0] x [0 -lr z0 0] x [0     0    1 0] x [0 lr z0  0] x [0      0      1 0]
    [0      0     0 1]   [0 0   0  1]   [0     0    0 1]   [0 0  0   1]   [0      0      0 1]

上面的变换是“旋转变换-旋转变换-旋转变换-旋转变换-旋转变换”的变换组。当我们需要让空间中的某个位置向量围绕一个轴旋转角度A的时候,就可以用这个向量相应的矩阵乘上这个M,比如

[x y 0 0]       [x' y' z' -]
[0 1 0 0]       [-  -  -  -]
[0 0 1 0] x M = [-  -  -  -] 
[0 0 0 1]       [-  -  -  -]

当然,M中矩阵相应的元素是根据轴向量得到的。

以 上的变换矩阵是通过把轴向量变到z轴上得到的,而且是先旋转到yoz面上,然后再旋转到z轴上。我们也可以不这样做,而是先把轴向量旋转到xoz面上,然 后再旋转到z轴上。此外,我们还可以把轴向量变到x或y轴上,这一点我们可以自己决定。虽然变换不同,但推导的道理是相同的,都是这种“其他变换-实际旋 转变换-其他变换”的渗透形式。

刚才分析的是旋转轴过原点的情况,对于一般的旋转轴,虽然我们也都是把它的轴向量放到原点来考虑,但我们不能只是让 旋转点围绕过原点的轴向量旋转完就算完事,我们仍需要采用“平移变换-旋转变换-平移变换”方法。即先将旋转轴平移到过原点方向,旋转点也做相应平移,接 着按上面推出的变换阵旋转,最后将旋转轴和点逆平移回去。这里,我们只需在M的左右两边各加上一个平移变换即可。这个平移变换的元素是根据轴向量与原点之 间的距离向量得到的,比如旋转轴与原点的距离向量是(lx, ly, lz),则我们的变换就变成


    [1   0    0  0]       [1  0  0  0]
    [0   1    0  0]       [0  1  0  0]
m = [0   0    1  0] x M x [0  0  1  0]
    [-lx -ly -lz 1]       [lx ly lz 1]

变换矩阵m就是全部7个变换矩阵的归并,适用于各种旋转情况。

我们现在已经讨论完了一般的2-D、3-D旋转了。可以看出其基本的思想还是能够化繁为简的变换、归并。而实际的旋转也仍是用我们最最基本的2-D绕原点旋转公式。其实还有很多的旋转效果可以用我们上面的变换、公式稍加修改获得。比如螺旋形旋转、旋转加前进、随机旋转等等。

6>3-D空间中的基变换与坐标变换
-Twinsen编写

-本人水平有限,疏忽错误在所难免,还请各位数学高手、编程高手不吝赐教
-我的Email-address: [email protected]

一、空间坐标系的基和基矩阵

在3 -D空间中,我们用空间坐标系来规范物体的位置,空间坐标系由3个相互垂直的坐标轴组成,我们就把它们作为我们观察3-D空间的基础,空间中物体的位置可 以通过它们来衡量。当我们把这3个坐标轴上单位长度的向量记为3个相互正交的单位向量i,j,k,空间中每一个点的位置都可以被这3个向量线性表出,如 P<1,-2,3>这个点可以表为i-2j+3k。

我们把这3个正交的单位向量称为空间坐标系的基,它们单位长度为1且正交,所以可以成为标准正交基。三个向量叫做基向量。现在我们用矩阵形式写出基向量和基。

       
i =  | 1 0 0 |
         
j =  | 0 1 0 | 

k =  | 0 0 1 |

     
    | i |    | 1 0 0 |
    
B = | j | =  | 0 1 0 |

    | k |    | 0 0 1 |

这样的矩阵我们叫它基矩阵。有了基矩阵,我们就可以把空间坐标系中的一个向量写成坐标乘上基矩阵的形式,比如上面的向量P可以写成:

P = C x B

=>

                          | 1 0 0 |                          

| 1 -2 3 | = | 1 -2 3 | x | 0 1 0 |
        
                          | 0 0 1 |

这样的话,空间坐标系下的同一个向量在不同的基下的坐标是不同的。


二、局部坐标系和局部坐标

和空间坐标系(也可以叫做全局坐标系或者世界坐标系)并存的称为局部坐标系(也叫坐标架——coordinate frame),它有自己的基,这些基向量把空间坐标系作为参考系。比如
   
     | x'|   | -1  0   0  |
     
B' = | y'| = | 0   1   0  |

     | z'|   | 0   0   -1 |


      | x''|   | 2^½ /2    0   2^½ /2    |

B'' = | y''| = | 0        -1   0          |

      | z''|   | -(2^½) /2   0   2^½ /2  |

就是两个局部坐标系的基,如图:

 

现在我们可以把上面那个空间坐标中的向量P|1 -2 3|(以后都用矩阵表示)表示在不同的基下,我把它写成一个大长串的式子:



                      | x' |                        | x''|
 
P = | Px' Py' Pz' | x | y' | = | Px'' Py'' Pz'' | x | y''|

                      | z' |                        | z''|

这里| Px' Py' Pz'|是P在B'下的坐标,| Px'' Py'' Pz''|是P在B''下的坐标,我把它写的具体点吧:


                            | -1 0  0 |                            | 2^½ /2      0   2^½ /2|
 
| 1 -2 3 | = | -1 -2 -3 | x | 0  1  0 | = | 2*2^½   -2   2^½ | x  | 0          -1   0      |

                            | 0  0 -1 |                            | -(2^½) /2   0   2^½ /2|

这就是说,在空间坐标系下面的向量| 1 -2 3 |在基B'下的坐标为|-1 -2 -3|,在B''下的坐标为| 2*2^½   -2   2^½ |。当然空间坐标系也有自己的基B|i j k|^T(因为是列向量,所以写成行向量的转置),但我们现在是拿它当作一个参考系。

在研究了局部坐标系之后,我现在要分析两个应用它们的例子,先来看



三、空间坐标系中一个点围绕任一轴的旋转

上一篇讨论3-D空间旋转的时候说到有一个高档的方法做3-D空间任意轴旋转,现在我们的知识储备已经足够理解这个方法了(Quake引擎使用的就是这个方法)。


如上所示,空间坐标系中的一个局部坐标系xyz中有一个向量a(2,5,3)和一个点p(8,4,2)现在我要让p点围绕a向量旋转60度,得到p’点,该如何做呢?从目前掌握的旋转知识来看,我们有两个理论基础:

1)在一个坐标系中的一个点,如果要它围绕该坐标系中一个坐标轴旋转,就给它的坐标值乘相应的旋转矩阵,如

[cosA -sinA 0 ]
[sinA cosA  0 ]
[0    0     1 ]

等等。

2)我们已经学习了局部坐标系的理论了,知道空间中一个点在不同的坐标系中的坐标不同。利用这一点,我们可以很方便的让一个点或者向量在不同的坐标系之间转换。

我们联系这两个理论根据,得出我们的思路:

1构造另一个局部坐标系abc,使得a成为该坐标系的一个坐标轴。

2 把p的坐标变换到abc中,得到p’,用旋转公式让p’围绕已经成为坐标轴的a旋转,得到p’’。

3把p’’再变换回坐标系xyz,得到p’’’,则p’’’就是p围绕a旋转后的点。

下面我们逐步说明。

首先我们构造abc,我们有无数种方法构 造,因为只要保证b、c之间以及他们和a之间都正交就可以了,但我们只要一个。根据上图,我们首先产生一个和a正交的b。这可以通过向量的叉乘来完成:我 们取另一个向量v(显然,这个向量是不能和a共线的任何非零向量),让它和a决定一个平面x,然后让v叉乘a得到一个垂直于x的向量b,因为b垂直于x, 而a在平面x上,因此b一定垂直于a,然后用a叉乘b得到c,最后单位化a、b、c,这样就得到了局部坐标系abc。

然后我们把p点变换到abc坐标系中,得到p’,即p’就是p在abc中的坐标:

|a  b  c| * p’=  |x  y  z| * p

p’ = |a  b  c|^-1 * |x  y  z| * p

      |ax bx cx|        |1 0 0|   |px|

p’ = |ay by cy| ^-1 *  |0 1 0| * |py|

      |az bz cz|        |0 0 1|   |pz|

注意这里|a b c|^-1即矩阵|a b c|的逆矩阵,因为a、b、c是三个正交向量,并且是单位向量,因此|a b c|是一个正交矩阵,正交矩阵的转置和逆相等,这是它的一个特性,因此上面的公式就可以写成:

      |ax ay az|     |1 0 0|   |px|

p’ = |bx by bz|  *  |0 1 0| * |py|

      |cx cy cz|     |0 0 1|   |pz|

这个时候p’就是p在abc坐标系下的坐标了。此时a已经是一个坐标轴了,我们可以用旋转矩阵来做。

p’’ = RotMatrix * p’

[1 0          0]   |p’x|
p’’ = [0 cos60 -sin60] * |p’y|
        [0 sin60  cos60]   |p’z|

 

最后,我们把p’’再次变换回xyz坐标系,得到最终的p’’’

|a  b  c| * p’’ = |x  y  z| * p’’’

p’’’ = |x  y  z|^-1 * |a  b  c| * p’’

p’’’ = |a  b  c| * p’’

最后

p’’’ = |a  b  c| * RotMatrix * |a   b   c|^T * p = M * p

这样就得到了xyz坐标系中点p围绕a旋转60度后的点。

最后,我用Quake3引擎的相应函数(来自idSoftware ——quake3-1[1].32b-source——mathlib.c)来完成对这个算法的说明:

/*

===============

RotatePointAroundVector

dst是一个float[3],也就是p’’’

dir相当于a,point就是p,degrees是旋转度数

===============

*/

void RotatePointAroundVector( vec3_t dst, const vec3_t dir, const vec3_t point,

                          float degrees ) {

    float  m[3][3];

    float  im[3][3];

    float  zrot[3][3];

    float  tmpmat[3][3];

    float  rot[3][3];

    int i;

    vec3_t vr, vup, vf;

    float  rad;

    vf[0] = dir[0];

    vf[1] = dir[1];

    vf[2] = dir[2];

   // 首先通过dir得到一个和它垂直的vr

   // PerpendicularVector()函数用于构造和dir垂直的向量

   // 也就是我们上面的第1步

    PerpendicularVector( vr, dir );

   // 通过cross multiply得到vup

   // 现在已经构造出坐标轴向量vr, vup, vf

    CrossProduct( vr, vf, vup );

   // 把这三个单位向量放入矩阵中

    m[0][0] = vr[0];

    m[1][0] = vr[1];

    m[2][0] = vr[2];

    m[0][1] = vup[0];

    m[1][1] = vup[1];

    m[2][1] = vup[2];

    m[0][2] = vf[0];

    m[1][2] = vf[1];

    m[2][2] = vf[2];

   // 产生转置矩阵im

    memcpy( im, m, sizeof( im ) );

  

    im[0][1] = m[1][0];

    im[0][2] = m[2][0];

    im[1][0] = m[0][1];

    im[1][2] = m[2][1];

    im[2][0] = m[0][2];

    im[2][1] = m[1][2];

   // 构造旋转矩阵zrot

    memset( zrot, 0, sizeof( zrot ) );

    zrot[0][0] = zrot[1][1] = zrot[2][2] = 1.0F;

  

    rad = DEG2RAD( degrees );

    zrot[0][0] = cos( rad );

    zrot[0][1] = sin( rad );

    zrot[1][0] = -sin( rad );

    zrot[1][1] = cos( rad );

   // 开始构造变换矩阵M

   // tmpmat = m * zrot

    MatrixMultiply( m, zrot, tmpmat );

   // rot = m * zrot * im

    MatrixMultiply( tmpmat, im, rot );

   // 则 rot = m * zrot * im 和我们上面推出的

   // M = |a  b  c| * RotMatrix * |a   b   c|^T  一致

   // 变换point这个点

   // p’’’ = M * p

    for ( i = 0; i < 3; i++ ) {

       dst[i] = rot[i][0] * point[0] + rot[i][1] * point[1] + rot[i][2] * point[2];

    }

}

四、世界空间到相机空间的变换

 

空间坐标系XYZ,相机坐标系UVN。这 时候相机空间的基(以下简称相机)在空间坐标系中围绕各个坐标轴旋转了一定角度<a,b,c>,然后移动了<x,y,z>。对于 模型我们可以看作相对于相机的逆运动,即模型旋转了一定角度<-a,-b,-c>,然后移动了<-x,-y,-z>,可以把相机 和物体的运动看成两个互逆的变换。这样,可以通过对相机的变换矩阵求逆来得到模型的变换矩阵。下面来具体看一下,如何得到相机变换矩阵,并且求得它的逆矩 阵。

首先声明一下,对于一个模型的变换,我们可以给模型矩阵左乘变换矩阵:

M x P = P'

| A B C D |     | x |    | Ax + By + Cz + D |

| E F G H |     | y |    | Ex + Fy + Gz + H |
             x         =  
| I J K L |     | z |    | Ix + Jy + Kz + L |

| M N O P |     | 1 |    | Mx + Ny + Oz + P |

也可以右乘变换矩阵:

P^T x M^T = P'^T

                | A E I M |

                | B F J N |
| x y z 1|   x               =  |Ax+By+Cz+D Ex+Fy+Gz+H Ix+Jy+Kz+L Mx+Ny+Oz+P| 
                | C G K O |

                | D H L P |

可以看出两种变换方式是一个转置关系,结果只是形式上的不同,但这里我们使用后者,即右乘变换矩阵,因为比较普遍。

很显然,相机的变换可以分成两个阶段:旋转和平移。我们先来看旋转。

在空间坐标系中,相机旋转之前世界坐标系xyz和相机坐标系u0v0n0的各个轴向量的方向相同,有关系:

                    | u0 |                  | x |

P = |Pu0 Pv0 Pn0| x | v0 |  =  |Px Py Pz| x | y |

                    | n0 |                  | z |

这里P是空间坐标系中的一个向量。|u0 v0 n0|^T是相机基矩阵,|Pu0 Pv0 Pn0|是P在相机基矩阵下的坐标。|x y z|^T是
世界基矩阵,|Px Py Pz|是P在它下面的坐标。有Pu0 = Px, Pv0 =Py, Pn0 = Pz。

相机和向量P都旋转之后,有关系:

                     | u |                   | x |

P' = |Pu0 Pv0 Pn0| x | v | = |Px' Py' Pz'| x | y |

                     | n |                   | z |

P'是P同相机一起旋转后的向量。|u v n|^T是相机旋转后的基矩阵,|Pu0 Pv0 Pn0|是P'在它下面的坐标,因为P是和相机一起旋转的,所以坐标不变。|x y z|^T仍为世界基矩阵,|Px' Py' Pz'|是P'在它下面的坐标。

现在看


                | u |                   | x |

|Pu0 Pv0 Pn0| x | v | = |Px' Py' Pz'| x | y |

                | n |                   | z |

因为|x y z|^T为一个单位阵,且Pu0 = Px, Pv0 =Py, Pn0 = Pz。 所以得到


             | u |                    

|Px Py Pz| x | v | = |Px' Py' Pz'| 

             | n |                   

即|Px Py Pz|和相机一起旋转后变成|Px' Py' Pz'|,即P x R = P',而旋转变换矩阵R就是:

| u |

| v |

| n |

写成标准4x4矩阵:

| ux uy uz 0|

| vx vy vz 0|

| nx ny nz 0|

| 0  0  0  1|

平移矩阵T很简单:

| 1 0 0 0 |

| 0 1 0 0 |

| 0 0 1 0 |

| x y z 1 |

则相机矩阵就是:

             | ux uy uz 0 |     | 1 0 0 0 |
                                
             | vx vy vz 0 |     | 0 1 0 0 |
C = R x T =                  x              
             | nx ny nz 0 |     | 0 0 1 0 |
   
             | 0  0  0  1 |     | x y z 1 |

它的逆矩阵,即相机的逆变换矩阵为

                      | 1  0  0  0 |     | ux vx nx 0 |   | ux   vx   nx  0 |
                                         
                      | 0  1  0  0 |     | uy vy ny 0 |   | uy   vy   ny  0 |
C^-1 = T^-1 x R^-1 =                  x                 =    
                      | 0  0  1  0 |     | uz nz nz 0 |   | uz   vz   nz  0 |

                      | -x -y -z 1 |     | 0  0  0  1 |   |-T.u -T.v -T.n 1 |

深入探索透视投影变换

-Twinsen编写
-本人水平有限,疏忽错误在所难免,还请各位数学高手、编程高手不吝赐教
-email: [email protected]

透视投影是3D固定流水线的重要组成部分,是将相机空间中的点从视锥体(frustum)变换到规则观察体(Canonical View Volume)中,待裁剪完毕后进行透视除法的行为。在算法中它是通过透视矩阵乘法和透视除法两步完成的。

透视投影变换是令很多刚刚进入3D图形领域的开发人员感到迷惑乃至神秘的一个图形技术。其中的理解困难在于步骤繁琐,对一些基础知识过分依赖,一旦对它们中的任何地方感到陌生,立刻导致理解停止不前。

没错,主流的3D APIs如OpenGL、D3D的确把具体的透视投影细节封装起来,比如

gluPerspective(…)就 可以根据输入生成一个透视投影矩阵。而且在大多数情况下不需要了解具体的内幕算法也可以完成任务。但是你不觉得,如果想要成为一个职业的图形程序员或游戏 开发者,就应该真正降伏透视投影这个家伙么?我们先从必需的基础知识着手,一步一步深入下去(这些知识在很多地方可以单独找到,但我从来没有在同一个地方 全部找到,但是你现在找到了J)。

我们首先介绍两个必须掌握的知识。有了它们,我们才不至于在理解透视投影变换的过程中迷失方向(这里会使用到向量几何、矩阵的部分知识,如果你对此不是很熟悉,可以参考《向量几何在游戏编程中的使用》系列文章)。

齐次坐标表示

透视投影变换是在齐次坐标下进行的,而齐次坐标本身就是一个令人迷惑的概念,这里我们先把它理解清楚。

根据《向量几何在游戏编程中的使用6》中关于基的概念。对于一个向量v以及基oabc,

 

可以找到一组坐标(v1,v2,v3),使得

v = v1 a + v2 b + v3 c (1)

而对于一个点p,则可以找到一组坐标(p1,p2,p3),使得

p – o = p1 a + p2 b + p3 c (2)

从上面对向量和点的表达,我们可以看出为了在坐标系中表示一个点(如p),我们把点的位置看作是对这个基的原点o所进行的一个位移,即一个向量——p – o(有的书中把这样的向量叫做位置向量——起始于坐标原点的特殊向量),我们在表达这个向量的同时用等价的方式表达出了点p:

p = o + p1 a + p2 b + p3 c (3)

(1)(3)是坐标系下表达一个向量和点的不同表达方式。这里可以看出,虽然都是用代数分量的形式表达向量和点,但表达一个点比一个向量需要额外的信息。如果我写出一个代数分量表达(1, 4, 7),谁知道它是个向量还是个点!

我们现在把(1)(3)写成矩阵的形式:

这里(a,b,c,o)是坐标基矩阵,右边的列向量分别是向量v和点p在基下的坐标。这样,向量和点在同一个基下就有了不同的表达:3D向量的第4个代数分量是0,而3D点的第4个代数分量是1。像这种这种用4个代数分量表示3D几何概念的方式是一种齐次坐标表示。

“齐次坐标表示是计算机图形学的重要手段之一,它既能够用来明确区分向量和点,同时也更易用于进行仿射(线性)几何变换。”—— F.S. Hill, JR

这样,上面的(1, 4, 7)如果写成(1,4,7,0),它就是个向量;如果是(1,4,7,1),它就是个点。

下面是如何在普通坐标(Ordinary Coordinate)和齐次坐标(Homogeneous Coordinate)之间进行转换:

从普通坐标转换成齐次坐标时,

如果(x,y,z)是个点,则变为(x,y,z,1);

如果(x,y,z)是个向量,则变为(x,y,z,0)

从齐次坐标转换成普通坐标时,

如果是(x,y,z,1),则知道它是个点,变成(x,y,z);

如果是(x,y,z,0),则知道它是个向量,仍然变成(x,y,z)

以上是通过齐次坐标来区分向量和点的方式。从中可以思考得知,对于平移T、旋转R、缩放S这3个最常见的仿射变换,平移变换只对于点才有意义,因为普通向量没有位置概念,只有大小和方向,这可以通过下面的式子清楚地看出:

而旋转和缩放对于向量和点都有意义,你可以用类似上面齐次表示来检测。从中可以看出,齐次坐标用于仿射变换非常方便。

此外,对于一个普通坐标的点P=(Px, Py, Pz),有对应的一族齐次坐标(wPx, wPy, wPz, w),其中w不等于零。比如,P(1, 4, 7)的齐次坐标有(1, 4, 7, 1)、(2, 8, 14, 2)、(-0.1, -0.4, -0.7, -0.1)等等。因此,如果把一个点从普通坐标变成齐次坐标,给x,y,z乘上同一个非零数w,然后增加第4个分量w;如果把一个齐次坐标转换成普通坐标,把前三个坐标同时除以第4个坐标,然后去掉第4个分量。

由于齐次坐标使用了4个分量来表达3D概念,使得平移变换可以使用矩阵进行,从而如F.S. Hill, JR所说,仿射(线性)变换的进行更加方便。由于图形硬件已经普遍地支持齐次坐标与矩阵乘法,因此更加促进了齐次坐标使用,使得它似乎成为图形学中的一个标准。

简单的线性插值

这是在图形学中普遍使用的基本技巧,我们在很多地方都会用到,比如2D位图的放大、缩小,Tweening变换,以及我们即将看到的透视投影变换等等。基本思想是:给一个x属于[a, b],找到y属于[c, d],使得x与a的距离比上ab长度所得到的比例,等于y与c的距离比上cd长度所得到的比例,用数学表达式描述很容易理解:

这样,从a到b的每一个点都与c到d上的唯一一个点对应。有一个x,就可以求得一个y。

此外,如果x不在[a, b]内,比如x < a或者x > b,则得到的y也是符合y < c或者y > d,比例仍然不变,插值同样适用。

透视投影变换

好,有了上面两个理论知识,我们开始分析这次的主角——透视投影变换。这里我们选择OpenGL的透视投影变换进行分析,其他的APIs会存在一些差异,但主体思想是相似的,可以类似地推导。经过相机矩阵的变换,顶点被变换到了相机空间。这个时候的多边形也许会被视锥体裁剪,但在这个不规则的体中进行裁剪并非那么容易的事情,所以经过图形学前辈们的精心分析,裁剪被安排到规则观察体(Canonical View Volume, CVV)中进行,CVV是一个正方体,x, y, z的范围都是[-1,1],多边形裁剪就是用这个规则体完成的。所以,事实上是透视投影变换由两步组成:

1)  用透视变换矩阵把顶点从视锥体中变换到裁剪空间的CVV中。

2)  CVV裁剪完成后进行透视除法(一会进行解释)。

我们一步一步来,我们先从一个方向考察投影关系。

上图是右手坐标系中顶点在相机空间中的情形。设P(x,z)是经过相机变换之后的点,视锥体由eye——眼睛位置,np——近裁剪平面,fp——远裁剪平面组成。N是眼睛到近裁剪平面的距离,F是眼睛到远裁剪平面的距离。投影面可以选择任何平行于近裁剪平面的平面,这里我们选择近裁剪平面作为投影平面。设P’(x’,z’)是投影之后的点,则有z’ = -N。通过相似三角形性质,我们有关系:

同理,有

这样,我们便得到了P投影后的点P’

从上面可以看出,投影的结果z’始终等于-N,在投影面上。实际上,z’对于投影后的P’已经没有意义了,这个信息点已经没用了。但对于3D图形管线来说,为了便于进行后面的片元操作,例如z缓冲消隐算法,有必要把投影之前的z保存下来,方便后面使用。因此,我们利用这个没用的信息点存储z,处理成:

这个形式最大化地使用了3个信息点,达到了最原始的投影变换的目的,但是它太直白了,有一点蛮干的意味,我感觉我们最终的结果不应该是它,你说呢?我们开始结合CVV进行思考,把它写得在数学上更优雅一致,更易于程序处理。假入能够把上面写成这个形式:

 

那么我们就可以非常方便的用矩阵以及齐次坐标理论来表达投影变换:

其中

哈,看到了齐次坐标的使用,这对于你来说已经不陌生了吧?这个新的形式不仅达到了上面原始投影变换的目的,而且使用了齐次坐标理论,使得处理更加规范化。注意在把变成的一步我们是使用齐次坐标变普通坐标的规则完成的。这一步在透视投影过程中称为透视除法(Perspective Division),这是透视投影变换的第2步,经过这一步,就丢弃了原始的z值(得到了CVV中对应的z值,后面解释),顶点才算完成了投影。而在这两步之间的就是CVV裁剪过程,所以裁剪空间使用的是齐次坐标,主要原因在于透视除法会损失一些必要的信息(如原始z,第4个-z保留的)从而使裁剪变得更加难以处理,这里我们不讨论CVV裁剪的细节,只关注透视投影变换的两步。

矩阵

就是我们投影矩阵的第一个版本。你一定会问为什么要把z写成

有两个原因:

1)  P’的3个代数分量统一地除以分母-z,易于使用齐次坐标变为普通坐标来完成,使得处理更加一致、高效。

2)  后面的CVV是一个x,y,z的范围都为[-1,1]的规则体,便于进行多边形裁剪。而我们可以适当的选择系数a和b,使得这个式子在z = -N的时候值为-1,而在z = -F的时候值为1,从而在z方向上构建CVV。

接下来我们就求出a和b:

 

这样我们就得到了透视投影矩阵的第一个版本:

使用这个版本的透视投影矩阵可以从z方向上构建CVV,但是x和y方向仍然没有限制在[-1,1]中,我们的透视投影矩阵的下一个版本就要解决这个问题。

为了能在x和y方向把顶点从Frustum情形变成CVV情形,我们开始对x和y进行处理。先来观察我们目前得到的最终变换结果:

我们知道-Nx / z的有效范围是投影平面的左边界值(记为left)和右边界值(记为right),即[left, right],-Ny / z则为[bottom, top]。而现在我们想把-Nx / z属于[left, right]映射到x属于[-1, 1]中,-Ny / z属于[bottom, top]映射到y属于[-1, 1]中。你想到了什么?哈,就是我们简单的线性插值,你都已经掌握了!我们解决掉它:

则我们得到了最终的投影点:

下面要做的就是从这个新形式出发反推出下一个版本的透视投影矩阵。注意到经过透视除法的形式,而P’只变化了x和y分量的形式,az+b和-z是不变的,则我们做透视除法的逆处理——给P’每个分量乘上-z,得到

而这个结果又是这么来的:

则我们最终得到:

M就是最终的透视变换矩阵。相机空间中的顶点,如果在视锥体中,则变换后就在CVV中。如果在视锥体外,变换后就在CVV外。而CVV本身的规则性对于多边形的裁剪很有利。OpenGL在构建透视投影矩阵的时候就使用了M的形式。注意到M的最后一行不是(0 0 0 1)而是(0 0 -1 0),因此可以看出透视变换不是一种仿射变换,它是非线性的。另外一点你可能已经想到,对于投影面来说,它的宽和高大多数情况下不同,即宽高比不为1,比如640/480。而CVV的宽高是相同的,即宽高比永远是1。这就造成了多边形的失真现象,比如一个投影面上的正方形在CVV的面上可能变成了一个长方形。解决这个问题的方法就是在对多变形进行透视变换、裁剪、透视除法之后,在归一化的设备坐标(Normalized Device Coordinates)上进行的视口(viewport)变换中进行校正,它会把归一化的顶点之间按照和投影面上相同的比例变换到视口中,从而解除透视投影变换带来的失真现象。进行校正前提就是要使投影平面的宽高比和视口的宽高比相同。

便利的投影矩阵生成函数

3D APIs都提供了诸如gluPerspective(fov, aspect, near, far)或者D3DXMatrixPerspectiveFovLH(pOut, fovY, Aspect, zn, zf)这样的函数为用户提供快捷的透视矩阵生成方法。我们还是用OpenGL的相应方法来分析它是如何运作的。

gluPerspective(fov, aspect, near, far)

fov即视野,是视锥体在xz平面或者yz平面的开角角度,具体哪个平面都可以。OpenGL和D3D都使用yz平面。

aspect即投影平面的宽高比。

near是近裁剪平面的距离

far是远裁剪平面的距离。

上图中左边是在xz平面计算视锥体,右边是在yz平面计算视锥体。可以看到左边的第3步top = right / aspect使用了除法(图形程序员讨厌的东西),而右边第3步right = top x aspect使用了乘法,这也许就是为什么图形APIs采用yz平面的原因吧!

到目前为止已经完成了对透视投影变换的阐述,我想如果你一直跟着我的思路下来,应该能够对透视投影变换有一个细节层次上的认识。当然,很有可能你已经是一个透视投影变换专家,如果是这样的话,一定给我写信,指出我认识上的不足,我会非常感激J。Bye!


深入探讨透视投影坐标变换
 

写3d图形程序,就一定会做坐标变换。而谈到坐标变换,就不得不提起投影变换,因为它是所有变换中最不容易弄懂的。但有趣的是,各种关于透视变换的文档却 依然是简之又简,甚至还有前后矛盾的地方。看来如此这般光景,想要弄清楚它,非得自己动手不可了。所以在下面的文章里,作者尝试推导一遍这个难缠的透视变 换,然后把它套用到 DX和 PS2lib 的实例中去。

  1. 一般概念

所谓透视投影变换,就是view 空间到project 空间的带透视性质的坐标变换步骤(这两

个空间的定义可以参考其他文档和书籍)。我们首先来考虑它应该具有那些变换性质。很显然,它至少要保证我们在view空间中所有处于可视范围内的点通过变换之后,统统落在project空间的可视区域内。好极了,我们就从这里着手——先来看看两个空间的可视区域。

由于是透视变换,view空间中的可见范围既是常说的视平截体(view frustum)。如图,

(图1)

它就是由前后两个截面截成的这个棱台。

从view空间的x正半轴看过去是下图这个样子。

(图2)

接下来是project空间的可视范围。这个空间应当是处于你所见到的屏幕上。实际上将屏幕表面视作project空间的xoy平面,再加一条垂直屏幕向 里(或向外)的z轴(这取决于你的坐标系是左手系还是右手系),这样就构成了我们想要的坐标系。好了,现在我们可以用视口(view port)的大小来描述这个可视范围了。比如说全屏幕640*480的分辨率,原点在屏幕中心,那我们得到的可视区域为一个长方体,它如下图(a)所示。

(图3)

但是,这样会带来一些设备相关性而分散我们的注意力,所以不妨先向DirectX文档学学,将project空间的可视范围定义为x∈[-1,1], y∈[-1,1], z∈[0,1]的一个立方体(上图b)。这实际上可看作一个中间坐标系,从这个坐标系到上面我们由视口得出的坐标系,只需要对三个轴向做一些放缩和平移操作即可。另外,这个project坐标系对clip操作来说,也是比较方便的。

  1. 推导过程

先从project空间的x正半轴看看我们的变换目标。

(图4)

这个区域的上下边界为y’=±1, 而图2中的上下边界为y = ± z * tan(fov/2),要实现图

2到图4的变换,我们有y’ = y * cot(fov/2) / z。这下完了,这是一个非线性变换,怎么用矩阵计算来完成呢?还好我们有w这个分量。注意到我们在做投影变换之前所进行的两次坐标变换——world变换 和view变换,他们只是一系列旋转平移和缩放变换的叠加。仔细观察这些变换矩阵,你会发现它们其实不会影响向量的w分量。换句话说,只要不是故意,一个 w分量等于1的向量,再来到投影变换之前他的w分量仍旧等于1。好的,接下来我们让w’= w*z, 新的w就记录下了view空间中的z值。同时在y分量上我们退而求其次,只要做到y’ = y * cot(fov/2)。那么,在做完线性变换之后,我们再用向量的y除以w,就得到了我们想要的最终的y值。

x分量的变换可以如法炮制,只是fov要换一换。事实上,很多用以生成投影变换矩阵的函数都使用了aspect这个参数。这个参数给出了视平截体截面的纵横比(这个比值应与view port的纵横比相等,否则变换结果会失真)。如果我们按照惯例,定义aspect = size of X / size of Y。那么我们就可以继续使用同一个fov而给出x分量的变换规则:x’ = x * cot(fov/2) / aspect。

现在只剩下z分量了。我们所渴望的变换应将z = Znear 变换到z = 0,将z = Zfar变换到z = 1。这个很简单,但是等等,x, y最后还要除以w,你z怎能例外。既然也要除,那么z = Zfar 就不能映射到z = 1了。唔,先映射到z = Zfar试试。于是,有z’ = Zfar*(z-Znear)/(Zfar – Znear)。接下来,看看z’/z的性质。令f(z) = z’/z = Zfar*(z-Znear)/(z*(Zfar – Znear))。

则f’(z) = Zfar * Znear / ( z^2 * (Zfar –Znear )), 显而易见f’(z) > 0。所以除了z = 0是一个奇点,函数f(z)是一个单调增的函数。因此,当Znear≤z≤Zfar时,f(Znear)≤f(z)≤f(Zfar),

即0≤f(z)≤1。

至此,我们可以给出投影变换的表达式了。

x’ = x*cot(fov/2)/aspect

y’ = y*cot(fov/2)

z’ = z*Zfar / ( Zfar – Znear ) – Zfar*Znear / ( Zfar – Znear )

w’ = z

以矩阵表示,则得到变换矩阵如下,

cot(fov/2)/aspect 0 0 0

0 cot(fov/2) 0 0

0 0 Zfar/(Zfar-Znear) 1

0 0 -Zfar*Znear/(Zfar-Znear) 0。

做完线性变换之后,再进行所谓的“归一化”,即用w分量去除结果向量。

  现在我们考虑一下这个变换对全view空间的点的作用。首先是x和y分量,明了地,当z>0时,一切都如我们所愿;当z<0时,x和y的 符号在变换前后发生了变化,从图象上来说,view空间中处于camera后面的图形经过变换之后上下颠倒,左右交换;当z= 0 时,我们得到的结果是无穷大。这个结果在实际中是没有意义的,以后我们得想办法弄掉它。再来看z,

仍旧拿我们上面定义的f(z)函数来看,我们已经知道当z≥Zfar时,f(z)≥1;同时当z→+∞,f(z)→Zfar/(Zfar-Znear);当z→+0时,f(z)→-∞; z→-0时,f(z)→+∞; z→∞时,f(z)→Zfar/(Zfar-Znear).由此我们画出f(z)的图像。

(图5)

由此图可以看出当z≤0时,如果我们仍旧使用f(z)进行绘制会产生错误。所以我们会想需要clip操作——只要这个三角形有任意一个顶点经过变换后z值落在[Zfar/(Zfar-Znear), +∞]区间中,我们就毫不怜悯地抛弃她——因为无论如何,这个结果是错的。那么万一有三角形在view空间内横跨了Znear到0的范围,按我们想应该是 画不出来了。但是回想一下我们所看见过的DirectX程序,似乎从未看到过这种情况。有点奇怪,但是不得不先放放,稍后再说。

3.到DirectX中求证

在DirectX中拿一个用fov生成投影矩阵的函数来看。

D3DXMATRIX* D3DXMatrixPerspectiveFovLH( D3DXMATRIX* pOut, FLOAT fovy, FLOAT Aspect,

FLOAT zn, FLOAT zf )

这个函数恰好使用了我们刚才推导所使用的几个参数,经过一些数据的代入计算之后,我们就会发现它所产生的矩阵就是我们计算出来的。看来,DirectX的 思路和我们是一致的。好的,一个问题解决了,但一个新的问题接着产生——DirectX是怎么做clip的?我不知道,而且看样子现在也知道不了,只能期 待牛人相助或者是碰到一本好书了。

4.研究ps2lib的投影变换

  其实投影变换都是一回事,但是PS2lib的函数怎么有点不一样呢?仔细看看,原来我们的思路是先做“归一化”,然后再做view port的放缩和平移,而PS2不是这样——它把“归一化”放在最后。接下来,我们就按这个顺序试试。

先看缩放操作,把它和除z交换顺序很方便,直接换便是了。于是我们记view port 的宽度为Vw,高度为Vh, Z缓存的最大值为Zmax, 最小值为Zmin则有

x’ = x * cot(fov/2)/aspect*(Vw/2)

y’ = y * cot(fov/2)*(Vh/2)

z’ = Zfar(z-Znear)/(Zfar-Znear) * (Zmax-Zmin);

w’ = z

再看平移部分,既然是要平移后再除,则必须平移原来的z倍,于是我们又记view port中心坐标为(Cx, Cy),就有

x’’ = x’ + z * Cx

y’’ = y’ + z * Cy

z’’ = z’ + z * Zmin

w’’ = w

好的,我们看看cot(fov/2)等于什么,从图2看,实际上它就是D/(Vh/2),那么cot(fov/2)/aspect实际上就是D/(Vw/2)。但是,ps2在这上面耍了个小花招,它在view空间中的view port和project空间的view port可以不相等。最明显的一点是,它在view空间中的view port的高度为480,但实际上它的输出的y向分辨率只有224。也就是说,ps2想要输出纵横比等于电视机的图像,就必须在y向上再加一个缩放。这个缩放在我们的变换中体现在哪呢?就在y’ = D/(Vh/2) * (Vhscr/2)中,注意到两个Vh不相等(project空间中的Vh记成Vhscr),两个值一运算就得到x’ = D*(224/480) = 0.466667D。这个0.4666667就是ps2lib函数参数ay的由来。同理,我们亦可得知ax一般应取值为1。那么,实际上ps2lib函数的scrz,ax, ay三个参数的作用等同于DirectX的象形函数的fov和aspect,在确定的规则下,他们可以相互转换,得到性质完全相同的透视变换。至于这个规则,这里就不给出了。

转回正题,有了上面的讨论,我们就可以展开我们的变换表达式如下,

x’’ = x * scrz * ax + z * Cx

y’’ = x * scrz * ay + z * Cy

z’’ = z * (Zfar*Zmax–Znear*Zmin)/(Zfar – Znear)

–Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear)

w’’ = z

z分量好像还有点不一样,注意到一般ps2程序在z buffer的操作为greater&equal,而DirectX的操作为less&equal,就是说,z方向得做些变动——得把z=Znear映射到z’’ = Zmax,z=Zfar映射到z’’=Zmin。说变就变,我们马上有

z’ = Zfar(z-Znear)/(Zfar-Znear)*(Zmin-Zmax)

z’’ = z’+Zmax

再次展开,得到z’’ = z * (Zfar*Zmin–Znear*Zmax)/(Zfar – Znear )

+ Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear)

好了,用矩阵把这个变换写出来,

scrz*ax 0 0 0

0 scrz*ay 0 0

Cx Cy (Zfar*Zmin–Znear*Zmax)/(Zfar – Znear ) 1

0 0 Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear) 0,

这下就完全一样了。下面的任务就是看看这个变换的性质。因为最后同样要除以z,所以x,y分量上的情形的和原来我们推导的DirectX的投影变换是一样的,区别在z分量上。来看新的f(z)函数,它的图像为

(图6)

5.结论

至此,我们已经完成了预定的目标。但是,将坐标变换完全掌握之后,为了做一个像样的图形程序,我们还有更多事情要做——至少在PS2上是这样。

猜你喜欢

转载自blog.csdn.net/ActionReaction/article/details/36626237