標籤:pre 部分 ndpi 功能 返回 座標 get 協助 pac
原文:http://gad.qq.com/program/translateview/7172922
翻譯:王成林(麥克斯韋的麥斯威爾) 審校:黃秀美(厚德載物)
這是本系列大家盼望已久的第五篇。如果你對向量瞭解不多,請先查看本系列的前四篇文章:介紹,向量基礎,向量的幾何表示,向量的運算。
這篇速查表會列舉一些遊戲中常見的幾何問題,以及使用數學向量解決它們的方法。
基本向量運算的完整表單
首先,先複習一下。
首先我假設你有一個可用的向量類。它的功能大部分集中在2D上,但是3D的原理相同。差別只在於向量乘積,在2D中我假設向量相乘只會返回代表“z”軸的標量。我會特別指出任何只適用於2D或3D的情況。
嚴格來說,一個點不是一個向量——但是一個向量可以代表從原點(0,0)到點的距離,那麼把向量當做點代表位置就很合理了。
我預期在該類中你擁有每個分量,以及下列各運算(使用C++風格的標記法,包括運算元的重載——但根據你的需求應該很容易將它翻譯到任何其它的語言)的使用許可權。如果一個運算不可用,你仍可以通過拓展該類或者建立一個“VectorUtils”的類來實現它。以下的例子一般適用於2D向量——但對於3D通常只需簡單地按照x、y的形式加入z座標即可。
- Vector2foperator+(Vector2f vec):返回兩向量的和。(在沒有重載的語言中,該函數可能叫做add()。下面幾個例子類似。)
a+b=Vector2f(a.x+b.x,a.y+b.y);
- Vector2foperator-(Vector2f vec):返回兩向量的差
a-b=Vector2f(a.x-b.x,a.y-b.y);
- Vector2foperator*(Vector2f vec):返回兩向量的逐分量的乘積
a*b=Vector2f(a.x*b.x,a.y*b.y);
- Vector2foperator/(Vector2f vec):返回兩向量的逐分量的商
a/b=Vector2f(a.x/b.x,a.y/b.y);
- Vector2foperator*(float scalar):返迴向量所有分量分別乘以一標量參數的結果。
a*s=Vector2f(a.x*s,a.y*s);
s*a=Vector2f(a.x*s,a.y*s);
- Vector2foperator/(float scalar):返迴向量所有分量分別除以一標量參數的結果。
a/s=Vector2f(a.x/s,a.y/s);
- floatdot(Vector2f vec):返回兩向量的點乘
a.dot(b)=a.x*b.x+a.y*b.y;
- floatcross(Vector2f vec):(2D情況)返回兩向量叉乘(3D向量)的z分量
a.cross(b)=a.x*b.y-a.y*b.x;
- Vector3fcross(Vector3f vec):(3D情況)返回兩個向量的叉乘。
a.cross(b)=Vector3f(a.y*b.z-a.z*b.y, a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x);
a.length()=sqrt(a.x*a.x+a.y*a.y);
- floatsquaredLength():返迴向量長度的平方。適用於比較兩向量的長度,避免了計算平方根。
a.squaredLength()=a.x*a.x+a.y*a.y;
- floatunit():返回一個指向同一方向長度為1的向量。
a.unit()=a/a.length();
- Vector2fturnLeft():返迴向量向左旋轉90度的結果。適用於計演算法向量。(假設y軸指向上,否則就是向右轉)
a.turnLeft=Vector2f(-a.y,a.x);
- Vector2fturnRight():返迴向量向右旋轉90度的結果。適用於計演算法向量。(假設y軸指向上,否則就是向左轉)
- a.turnRight=Vector2f(a.y,-a.x);
- Vector2frotate(float angle):按照特定角度旋轉向量。儘管很少能夠在向量類中找到,但這是非常有用的運算。等效於乘以一個2x2旋轉矩陣
a.rotate(angle)=Vector2f(a.x*cos(angle)-a.y*sin(angle),a.x*sin(angle)+a.y*cos(angle));
a.angle()=atan2(a.y,a.x);
簡單的樣本——熱身
例1——兩點間距離
也許你知道可以用畢達哥拉斯定理得出,但是向量方法更簡單。給定向量a和向量b:
1 |
float distance = (a-b).length(); |
例2——矯直(alignment)
一些情況下你也許想要按照一張照片的中心矯直它。有時要按照它的左上方或者中上點。更廣泛來說,為了最大限度地控制矯直線,你可以使用一個兩分量從0到1(甚至超出也可以,如果你希望的話)的向量進行任意方向的矯直。
1 2 |
// imgPos, imgSize and align are all Vector2f Vector2f drawPosition = imgPos + imgSize * align |
例3——參數化直線方程
兩點定義一條直線,但是該定義有很多玄機。一個處理直線不錯的方法是使用參數化方程:一個點(“P0”)和一個方向(“dir”)。
1 2 |
Vector2f p0 = point1; Vector2f dir = (point2 - point1).unit(); |
有了這個,你可以,例如,你可以得到一個距離p010單位遠的點:
1 |
Vector2f p1 = p0 + dir * 10; |
例4:——中點和兩點間的內延(interpolation)
給定向量p0和p1。它們的中點是(p0+p1)/2。更廣泛來講,p0和p1定義的線段可以通過線性內延在0到1間改變t來得到
1 |
Vector2f p = (1-t) * p0 + t * p1; |
|
|
在t=0時,你得到p0;在t=1時,你得到p1;在t=0.5時,你得到中點,等等。
例5:找到一線段的法向
你已經知道如何找到線段的方向了(例3)。將它旋轉90度得到法向量,所以對它使用turnLeft()或turnRight()即可得到結果。
使用點乘的投影
點乘對於計算向量沿一個方向投影的長度具有很大協助。我們需要向量(“a”)和一個代表投影方向(“dir”)的單位向量(所以確保你首先使用unit())。那麼投影長度就是a.dot(dir)。例如,如果a=(3,4),dir=(1,0),那麼a.dot(dir)=3。你可以判斷這是正確的,因為(1,0)是x軸的方向。實際上a.x恒等於a.dot(Vector2f(1,0)),a.y恒等於a.dot(Vector2f(0,1))。
因為a和b的點乘還可以被定義為|a||b|cos(alpha)(alpha是兩向量夾角),所以如果兩者垂直結果為0,如果兩者夾角小於90°結果為正,大於90°結果為負。我們可以使用這一點判斷兩向量是否指向同一大方向。
如果將點乘的結果乘以方向向量,你會得到沿著那方向的向量的投影——我們稱之為“at”(t代表切向)。如果我們做a-at,我們會得到垂直於方向向量的向量——我們稱之為“an”(n代表法向)。at+an=a。
例6——決定最靠近dir的方向
假設你有許多指向不同方向的單位向量,你想要找出哪一個方向最靠近dir。只需找到列表中向量和dir點乘的結果最大的那個。同樣,最小的點乘意味著距離最遠的那個。
例7——判斷兩向量的夾角是否小於alpha
使用上述方程,我們知道如果兩向量a和b的單位向量的點乘小於cos(alpha),那麼它們之間的夾角小於alpha。
123 |
bool isLessThanAlpha(Vector2f a, Vector2f b, float alpha) { return a.unit().dot(b.unit()) < cos (alpha); } |
例8——判斷一點所在的半平面
假設平面中有任意一點p0,和一個方向(單位)向量,dir。假設一條穿過p0,垂直於dir的無線長的線將平面一分為二:dir指向的半平面和dir沒有指向的半平面。那麼如何判斷一個點p是否在dir指向的那一邊呢?記住當向量間夾角小於90°時它們的點乘為正,那麼只需做投影然後檢查:
123 |
bool isInsideHalfPlane(Vector2f p, Vector2f p0, Vector dir) { return (p - p0).dot(dir) >= 0; } |
例9——迫使一點在半平面內
和上面範例相似,但是不僅僅是檢查,如果投影小於0的話,我們使用它將目標-投影移動到dir方向,這樣目標點就在半平面的邊緣了。
12345 |
Vector2f makeInsideHalfPlane(Vector2f p, Vector2f p0, Vector dir) { float proj = (p - p0).dot(dir); if (proj >= 0) return p; else return p - proj * dir; } |
例10——檢查/迫使一點在一凸多面體內。
一個凸多面體可以被定義為多個半平面相交所得結果,每個交線作為多面體的邊。它們的p0是邊上的頂點,它們的dir是邊的內面法線向量(例如,如果你按順時針方向走,那就是turnRight()法向)。一個點在多面體內部若且唯若它在所有的半平面內。同樣地,你可以迫使它在多面體內(通過移動到最靠近的邊)通過對所有半平面運用makeInsideHalfPlane 演算法。[哎呀!其實只有當所有角度>=90°時才管用]
例11——按照給定法線反射一個向量
彈球類遊戲,球撞向一個斜牆面。已知球的速度向量和牆的法向量(見例5)。現實情況下它會如何反彈?簡單!只需反射球的法向速度,保持它的切向速度即可。
12345 |
Vector2f vel = getVel(); Vector2f dir = getWallNormal(); // Make sure this is a unit vector Vector2f velN = dir * vel.dot(dir); // Normal component Vector2f velT = vel - velN; // Tangential component Vector2f reflectedVel = velT - velN; |
想要更貼近現實,你可以將velT和velN分別乘以一個代表摩擦力和恢複係數的常數。
例12——抵消沿軸的運動
有時我們需要將運動限制在一個給定座標軸內。思路和上面相同:將速度分解到法向和切向上,然後僅保留切線速度。這有助於計算沿軌道運動的人的速度。
旋轉
例13——點繞定點做旋轉
假設我們有一點,rotate()會將點按原點進行旋轉。這很有趣,但是也有限制。按任意一定點旋轉簡單而且實用——只需用點減去定點,也就是將定點平移到原點,然後旋轉,然後再把定點加回去。
123 |
Vector2f rotateAroundPivot(Vector2f p, Vector2f pivot) { return (pos - pivot).rotate(angle) + pivot; } |
例14——判斷向哪一方向旋轉
假設我們有一個角色想要轉身面向敵人。已知他的方向和面向敵人的方向。那麼他應該向左轉還是向右轉?叉乘給出了簡單的答案:curDir.cross(targetDir)返回正數如果你應該左轉,負數如果你應該右轉(返回0如果已經面對他了或者180°背對他)。
其他幾何樣本
有一些其它實用的例子,其中沒有過多使用向量,但是很有用:
例15——等距空間到螢幕座標
等距遊戲中,你知道世界的(0,0)在螢幕的什麼位置(讓我們稱之為原點,使用一個向量代表它),但是你如何知道(x,y)在螢幕的什麼位置呢?首先,你需要兩個代表座標基的向量,新x軸和新y軸。對於一個典型的等距遊戲來說,它們可以是bx=Vector2f(2,1)和by=Vector2f(-2,1)——它們不一定是單位向量。現在,一切都簡單了。
12 |
Vector2f p = getWorldPoint(); Vector2f screenPos = bx * p.x + by * p.y + origin; |
沒錯,的確很簡單。
例16——等距螢幕到全局座標
相同情況,但是這次你想要知道滑鼠滑過哪塊兒拼接圖(tile)。這要更複雜一些。我們知道(x’,y’)=(x*bx.x+y*by.x,x*bx.y+y*by.y)+原點,所以可以先減去原點,然後對線性方程求解。使用克拉默法則,我們可以聰明地使用2D叉乘(查看文章開始的定義)進行簡化:
12345 |
Vector2f pos = getMousePos() - origin; float demDet = bx.cross(by); float xDet = pos.cross(by); float yDet = bx.cross(pos); Vector2f worldPos = Vector2f(xDet / demDet, yDet / demDet); |
我看多許多人都在用“找到矩形然後查看位元影像”的方法,現在你不需要了。
【轉】遊戲程式員的數學食糧05——向量速查表