本文所有知識點均來源于《圖形渲染實戰 2D架構設計與實現》這本書,作者關于向量數學知識點的講解是我目前看到的最全面、最容易理解的。這裡隻是抛磚引玉,感興趣請閱讀書籍
1. 概念向量的定義:向量是具有方向(Direction)和大小(Length / Magnitude)的空間變量。
向量的大小:已知一個向量a = [ x , y ],那麼該向量的大小可以使用|| a ||來表示,其值為一個标量(Scalar),可以由Math.sqrt(x^2 y^2)這個公式計算得出。
2.向量加減法2.1加法
在圖6.2中,向量a(實線且大小為200)與向量b(實線且大小為282.84)相加的幾何解釋就是:平移向量,使向量b的尾部(向量b的圓點部分)連接向量a的頭部(向量a箭頭部分),接着從向量a的尾部向向量b的頭部畫一個向量(大小為446.21),該向量就是向量a b形成的新的向量,這就是向量加法的三角形法則。而在圖6.2所示的向量加法幾何特性圖示中,使用的是向量加法的平行四邊形法則繪制的。
向量加法很有用。舉個例子,在力學中,如果有兩個力(向量)同時施加在某個物體上,那麼該物體受到的合力就是這兩個力(向量)相加。
2.2減法
向量減法的幾何含義,如圖6.3所示,可以固定任意一個向量,平移另外一個向量,讓兩個向量的尾部重合,此時如果是:會發現,向量的減法是具有方向性的,并不滿足交換律。
向量與标量不能相加,但是它們能相乘。當一個标量和一個向量相乘時,将得到一個新的向量,該向量與原向量平行,但長度不同或方向相反。如圖6.5所示,畫布中心左側的向量的方向都是相同的(平行),向量的大小則以每20個單位遞增。而畫布中心右側的向量,方向相反(仍舊平行),其大小也是以每20個單位遞增。
向量與标量相乘的本質是縮放向量,因此實現的靜态方法名為scale。現在大家應該知道标量的英文為什麼叫Scalar了,因為标量(Scalar)用來縮放(Scale)向量(Vector)
2.4向量的點乘向量能與标量相乘,向量也能和向量相乘。兩個向量相乘被稱為點乘(也常稱為點積或内積)
兩個向量a和b,夾角為θ,根據餘弦定律:
|| a - b ||²= || a ||² || b ||²-2 || a || || b || cosθ。
将上述表達式的左側|| a - b ||²展開,寫成|| a ||² || b ||²-2 ( a·b ),則可以得到:
|| a ||² || b ||²-2 ( a·b ) = || a ||2 || b ||2-2 || a || || b || cosθ。
從而就可以導出如下公式:
a·b = || a || || b || cosθ。
根據上面的公式,可以寫成如下形式:
cosθ = a·b / ( || a || || b || ) 其中 ( || a || || b || ) 的值總是正數。
由此得到如下重要的信息:
根據這個公式:cosθ = a·b / ( || a || || b || ),那麼能夠很容易計算出向量a與向量b之間的夾角,僞代碼如下:
// 根據公式:cosθ = a·b / ( || a || || b || ),計算向量a和向量b的夾角
// 根據isRadian參數的取值來判斷是返回弧度還是返回角度
public static getAngle(a: Vec2, b: Vec2, isRadian: boolean = false): number {
let dot: number = Vec2.dotProduct(a, b);
let radian: number = Math.acos(dot / (a.length * b.length));
if (isRadian === false) {
radian = Math2D.toDegree(radian);
}
return radian;
}
為了與夾角區分,使用朝向來表示物體的方向。具體代碼如下:
// 計算兩個向量的連起來與x軸的夾角
public static getOrientation(
from: Vec2,
to: Vec2,
isRadian: boolean = false
): number {
let diff: Vec2 = Vec2.difference(to, from);
let radian = Math.atan2(diff.y, diff.x);
if (isRadian === false) {
radian = Math2D.toDegree(radian);
}
return radian;
}
背景描述:當鼠标指針的位置在向量的區域範圍之外(如圖6.7所示),則正常顯示該向量。但當鼠标指針的位置移動到向量區域範圍内,則會加粗顯示該向量,并且會:
(1)标記出鼠标指針位置(圓圈表示)、坐标信息,以及線段起點到鼠标指針處的向量。
(2)标記出鼠标指針位置在向量上的投影點(圓圈),坐标信息,以及從投影點到鼠标指針位置之間的向量。
(3)标記出鼠标指針位置與線段起點之間以角度表示的夾角。
(4)所有的坐标信息是相對全局坐标系原點(左上角)的偏移表示。
圖6.7和圖6.8所示的效果是經典的向量投影算法。簡單地說,就是将一個點(鼠标位置表示)投影到由起點和終點所形成的向量上。該算法比較常用,僞代碼如下:
/**
* 判斷點是否在線上
* @param pt
* @param start
* @param end
* @param closePoint
* @returns
*/
public static projectPointOnLineSegment(
pt: vec2,
start: vec2,
end: vec2,
closePoint: vec2
): boolean {
let v0: vec2 = vec2.create();
let v1: vec2 = vec2.create();
let d: number = 0;
// 向量起點到鼠标位置的方向向量
vec2.difference(pt, start, v0);
// 向量起點到向量終點的方向向量
vec2.difference(end, start, v1);
// 原向量變成單位向量,并返回原向量的長度
d = v1.normalize();
// 将v0投影到v1上,獲取投影長度
// v0*v1 = ||v0|| * ||v1|| * cosθ
// v1是單位向量,所有 ||v1|| = 1
// 于是 v0*v1 = ||v0|| * cosθ,也就是v0在v1上的投影長度
let t: number = vec2.dotProduct(v0, v1);
// 如果t < 0,說明鼠标在起始點之外,返回起始點
if (t < 0) {
closePoint.x = start.x;
closePoint.y = start.y;
return false;
} else if (t > d) { // 投影長度 > 線段長度,說明鼠标位置超過線段終點範圍
closePoint.x = end.x;
closePoint.y = end.y;
return false;
} else {
// 鼠标點位于線段中間
// 使用scaleAdd計算出相對于全局坐标的坐标偏遠信息
// start 起點向量
// v1 * t 投影向量
// start v1(單位向量)*t(标量) 相對于全局坐标的向量
vec2.scaleAdd(start, v1, t, closePoint);
return true;
}
}
為了更好地了解getOrientation(向量朝向)和getAngle(向量夾角)方法之間的區别,參考如圖6.10所示的效果,可以知道:
如果一個點在圓的半徑範圍之内,則說明發生了碰撞
/**
* 判斷坐标是否在圓内部
* @param pt 鼠标坐标
* @param center 圓中心坐标
* @param radius 半徑
* @returns
*/
public static isPointInCircle(
pt: vec2,
center: vec2,
radius: number
): boolean {
let diff: vec2 = vec2.difference(pt, center);
let len2: number = diff.squaredLength;
// 避免使用Math.sqrt方法
if (len2 <= radius * radius) {
return true;
}
return false;
}
點與線段的碰撞檢測是一個比較基礎和重要的算法,該算法可以由兩部分組成:
/**
* 判斷坐标是否在線附近
* @param pt 鼠标位置
* @param start 線段起始點
* @param end 線段終止點
* @param radius 碰撞半徑
* @returns
*/
public static isPointOnLineSegment(
pt: vec2,
start: vec2,
end: vec2,
radius: number = 2
): boolean {
let closePt: vec2 = vec2.create();
if (Math2D.projectPointOnLineSegment(pt, start, end, closePt) === false) {
return false;
}
return Math2D.isPointInCircle(pt, closePt, radius);
}
關于點與矩形的碰撞檢測算法非常簡單
/**
* 判斷坐标是否在矩形内部
* @param ptX 鼠标位置
* @param ptY 鼠标位置
* @param x 矩形左上角
* @param y 矩形左上角
* @param w 矩形寬度
* @param h 矩形高度
* @returns
*/
public static isPointInRect(
ptX: number,
ptY: number,
x: number,
y: number,
w: number,
h: number
): boolean {
if (ptX >= x && ptX <= x w && ptY >= y && ptY <= y h) {
return true;
}
return false;
}
假設橢圓的中心點定義的坐标值為[ centerX,centerY ],并且半徑分别為[ radiusX , radiusY ],在這種情況下,一個點P ( pX ,pY )如果在橢圓的内部,那麼要滿足如下公式:
有了上述公式,就可以實現isPointInEllipse方法。
/**
* 判斷坐标是否在橢圓内部
* @param ptX
* @param ptY
* @param centerX
* @param centerY
* @param radiusX
* @param radiusY
* @returns
*/
public static isPointInEllipse(
ptX: number,
ptY: number,
centerX: number,
centerY: number,
radiusX: number,
radiusY: number
): boolean {
let diffX = ptX - centerX;
let diffY = ptY - centerY;
let n: number =
(diffX * diffX) / (radiusX * radiusX)
(diffY * diffY) / (radiusY * radiusY);
return n <= 1.0;
}
如圖6.11所示為點與三角形的關系,從圖中可以知道,點P與[ v0 , v1 , v2 ]形成的三角形之間的關系有兩種,要麼點P在三角形内部,要麼點P在三角形的外部。
來觀察一下它們之間的區别,你會發現:
向量叉乘比較特别,隻能使用3D向量,現在假設有兩個3D向量 a=[x0,y0, z0]和b=[x1, y1, z1],那麼
為了将2D向量vec2以3D向量的形式來表示,可以将3D向量的z分量設置為0,例如a=[x0, y0,0], b=[x1, y1,0],套用上述叉積公式,會得到:
可以看到,對于2D向量的叉積來說,其x和y分量總是為0,但是對于點與三角形碰撞檢測算法來說,叉積後的z分量x0y1-y0x1才是最關鍵的,先把z分量的計算作為vec2的一個靜态方法。
/**
* 計算三角形兩條邊向量的叉乘
* @param v0
* @param v1
* @param v2
* @returns
*/
public static sign(v0: vec2, v1: vec2, v2: vec2): number {
// v2->v0邊向量
let e1: vec2 = vec2.difference(v0, v2);
// v2->v1邊向量
let e2: vec2 = vec2.difference(v1, v2);
return vec2.crossProduct(e1, e2);
}
/**
* 判斷鼠标是否在三角形内部
* @param pt
* @param v0
* @param v1
* @param v2
* @returns
*/
public static isPointInTriangle(pt: vec2, v0: vec2, v1: vec2, v2: vec2) {
// 計算三角形的三個定點與鼠标形成的三個子三角形的邊向量的叉乘
let b1: boolean = Math2D.sign(v0, v1, pt) < 0.0;
let b2: boolean = Math2D.sign(v1, v2, pt) < 0.0;
let b3: boolean = Math2D.sign(v2, v0, pt) < 0.0;
// 三個三角形的方向一緻,說明點在三角形内部
// 否則點在三角形外部
return b1 === b2 && b2 === b3;
}
通過上面的代碼,結合上圖6.11會發現,實際上并不關心子三角形兩條邊向量叉積的數值大小,更關心的是叉積的正負性,隻要三個子三角形的兩條邊向量的叉積的正負性都一緻的話,表示三個子三角形的頂點排列順序一緻,那麼該點P肯定在[v0 , v1 , v2 ]形成的三角形的内部,否則肯定在外部。
3.3.5點與任意凸多邊形的碰撞檢測如圖6.12所示,可以将任意的凸多邊形很方便地分解成三角形,然後依次調用上一節實現的點與三角形碰撞檢測算法,這樣就能獲得點與任意凸多邊形碰撞檢測的算法。
/**
* 判斷坐标是否在凸多邊形内部
* @param pt
* @param points
* @returns
*/
public static isPointInPolygon(pt: vec2, points: vec2[]): boolean {
if (points.length < 3) {
return false;
}
// 以point[0]為共享點,遍曆多邊形點集,構成三角形,判斷點是否在三角形内部,
// 一旦點與某個三角形發生碰撞,就返回true
for (let i: number = 2; i < points.length; i ) {
if (Math2D.isPointInTriangle(pt, points[0], points[i - 1], points[i])) {
return true;
}
}
return false;
}
參考圖6.13會發現,左側的凸多邊形(六邊形)頂點形成的6個子三角形分别是[ v0, v1 , v2 ]、[ v1 , v2 , v3 ]、[ v2 , v3 , v4 ]、[ v3 , v4 , v5 ]、[ v4 , v5 , v0 ]和[v5 , v0 , v1 ],這6個子三角形頂點的順序都是順時針排列的。
而觀察上圖右側的凹多邊形,由5個頂點組成,形成的5個子三角形分别是[ v0 , v1, v2 ]、[ v1 , v2 , v3 ]、[ v2 , v3 , v4 ]、[ v3 , v4 , v0 ]和[ v4 , v0 , v1 ],會發現最後一個三角形[ v4 , v0 , v1]的頂點順序是逆時針排列,而其他的三角形都是順時針排列。
如何判斷一個三角形的頂點順序,已經在上一節中了解過了,那麼直接來看一下判斷凸多邊形的算法。
/**
* 判斷是否凸多邊形
* @param points
* @returns
*/
public static isConvex(points: vec2[]): boolean {
// 算出第一個三角形的定點順序
let sign: boolean = Math2D.sign(points[0], points[1], points[2]) < 0;
let j: number, k: number;
for (let i: number = 1; i < points.length; i ) {
j = (i 1) % points.length;
k = (i 2) % points.length;
// 如果當前多邊形的頂點順序和第一個多邊形的頂點順序不一緻,則說明是凹邊形
if (sign !== Math2D.sign(points[i], points[j], points[k]) < 0) {
return false;
}
}
// 凸多邊形
return true;
}
向量是具有大小和方向的空間變量。而以前常用的數值都是标量,其僅有大小,而沒有方向。在向量一節中,知道了如何計算向量的大小、向量的方向、向量的加減法、負向量、向量的縮放,以及向量的點乘。同時更加詳細地解釋了向量的上述這些操作相對應的幾何含義,這是本文的關鍵點。隻有深刻地理解向量的幾何含義,才能靈活地應用向量來解決問題。
核心關注點與基本幾何形體之間的碰撞檢測算法,涉及點與線段、點與圓、點與矩形、點與橢圓、點與三角形,以及點與凸多邊形之間的碰撞檢測,并且講解了多邊形的三角形化,以及如何判斷凸多邊形的算法。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!