1. 程式人生 > >向量幾何在遊戲程式設計中的使用

向量幾何在遊戲程式設計中的使用

<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,由