iT邦幫忙

2022 iThome 鐵人賽

DAY 11
3
Modern Web

30個遊戲程設的錦囊妙計系列 第 11

Trick 10: 向量的旋轉原來要歪看正著

  • 分享至 

  • xImage
  •  

不管是2D還是3D,幾乎沒有遊戲能離得開向量,小至幾何繪圖、動畫旋轉等像素操作,大至光跡追蹤、路徑搜尋等邏輯演算,向量都在其中扮演著重量級的魔王,擋在遊戲開發的路上等著程式設計師們前來挑戰。

今天小哈在鐵人賽的系列中,正式帶領同學們對著向量遞出正式的挑戰書。

何謂向量

可能同學們早就懂向量是什麼了,不過小哈還是想先簡單對向量來下點定義。

向量是同時含有方向及長度屬性的幾何物件,在空間中通常會以一端帶有箭頭的直線來表示,直線的長度代表向量的大小,箭頭則代表向量的方向。

在這一系列鐵人挑戰的文章裏,我們只專門針對二維的向量來討論,不過大部分二維向量的概念都能映射到其他維度的向量。

在程式裏,二維向量通常是含有x和y兩個屬性的物件,並擁有基本的長度與方向兩個唯讀屬性,長度可由畢氏定理計算出來,而方向是指和x軸的夾角,可由Math.atan2()計算出來。

class Vector {
    // 在建構子中宣告向量最基本的兩個屬性
    constructor(public x = 0, public y = 0) {
    
    }
    /** 向量長度(以getter函式來定義唯讀屬性)
     * 用畢氏定理求得 √(x²+y²)
     */
    get length(): number {
        /** const和let有點像, 都是用來宣告變數用的
         * 但是以const宣告的變數是唯讀的,不能改變變數裏的值
         */
        const x = this.x;
        const y = this.y;
        return Math.sqrt(x * x + y + y);
    }
    /** 向量與x軸夾角(單位是弧度)*/
    get angle(): number {
        /** Math.atan2()的參數中,
         * 第一個是y的變化量,第二個才是x的變化量
         */
        return Math.atan2(this.y, this.x);
    }
}

CG中,向量類別是定義在Point.ts,名稱是Point,從字面看起來是指一個點的位置。

一個點的位置其實也算是一個向量,只不過這個向量還夾帶著一個起點在原點的附加涵義。反過來說,一個向量其實也可以被當成一個點,只不過這個點是在以向量起點作為原點的座標系統。

向量旋轉後變成什麼

向量旋轉後還是向量啦!不過旋轉後的向量該怎麼計算出來呢?
vector rotate
假設有一個二維向量P(x,y),旋轉一個角度θ後變成P′(x',y'),那我們要怎麼根據x,y,θ這三個數字推導出x'和y'呢?

同學們若上Google求救,或去Youtube找答案,99%會得到下面四個聯立方程式,
https://chart.googleapis.com/chart?cht=tx&chl=x%20%3D%20%5Cmid%20P%20%5Cmid%20%5Ccdot%20cos(%5Ctheta)
https://chart.googleapis.com/chart?cht=tx&chl=y%20%3D%20%5Cmid%20P%20%5Cmid%20%5Ccdot%20sin(%5Ctheta)
https://chart.googleapis.com/chart?cht=tx&chl=x'%20%3D%20%5Cmid%20P%20%5Cmid%20%5Ccdot%20cos(%5Ctheta%20%2B%20%5Calpha)
https://chart.googleapis.com/chart?cht=tx&chl=y'%20%3D%20%5Cmid%20P%20%5Cmid%20%5Ccdot%20sin(%5Ctheta%20%2B%20%5Calpha)
其中的https://chart.googleapis.com/chart?cht=tx&chl=%5Cmid%20P%20%5Cmid 是代表向量P的長度。這四個聯立方程式經過三角函數的公式轉換,以及一連串代入消去,最後解出一組向量旋轉的公式。
https://chart.googleapis.com/chart?cht=tx&chl=x'%20%3D%20x%20%5Ccdot%20cos(%5Ctheta)%20-%20y%20%5Ccdot%20sin(%5Ctheta)
https://chart.googleapis.com/chart?cht=tx&chl=y'%20%3D%20x%20%5Ccdot%20sin(%5Ctheta)%20%2B%20y%20%5Ccdot%20cos(%5Ctheta)

有興趣的同學可以自行從聯立方程式推導出結果。
小哈提示一下其中會用到的兩個重要的三角函數公式:
https://chart.googleapis.com/chart?cht=tx&chl=sin(%5Calpha%20%2B%20%5Cbeta)%20%3D%20sin(%5Calpha)%20%5Ccdot%20cos(%5Cbeta)%20%2B%20cos(%5Calpha)%20%5Ccdot%20sin(%5Cbeta)
https://chart.googleapis.com/chart?cht=tx&chl=cos(%5Calpha%20%2B%20%5Cbeta)%20%3D%20cos(%5Calpha)%20%5Ccdot%20cos(%5Cbeta)%20-%20sin(%5Calpha)%20%5Ccdot%20sin(%5Cbeta)

雖然用這麼多數學計算可以導出最後的結果,不過幾何數學裏,幾乎所有公式都有其隱藏的幾何意義。接下來我們可以從另一個角度來發現旋轉的公式原來是這麼回事。

如何歪看正著

在重新讓向量旋轉之前,小哈想先打破一下同學們對座標平面的認知窠臼。

我們說一個座標在(3,2)的點,他的位置是在x等於3以及y等於2的地方。不過從向量的角度來看,我們可以看成這個點是3個x軸上的單位向量,加上兩個y軸上的單位向量的向量和。

單位向量是指長度為1的向量,因此x軸的單位向量是(1,0),y軸的單位向量是(0,1)。

https://chart.googleapis.com/chart?cht=tx&chl=3%20%5Ctimes%20(1%2C%200)%20%2B%202%20%5Ctimes%20(0%2C%201)%20%3D%20(3%2C%200)%20%2B%20(0%2C%202)%20%3D%20(3%2C%202)

向量和

也就是說,一個向量(x,y)其實是由x個x軸的單位向量,加上y個y軸的單位向量的總和。

有了這一層新的認識之後,現在再來旋轉向量,不過這次我們不只轉動P這個向量,還要連帶把x軸和y軸都跟著一起旋轉θ角度。全部一起轉完之後,我們再歪著頭去看整個座標平面,就可以看到下圖右邊的座標系。
座標旋轉
在這個旋轉過的座標系中,轉過的P'相對於新的x'軸和y'軸,仍然是(3,2),只不過這時候x'軸上的單位向量已經發生了變化,從原本的(1,0)變成了(cos(θ),sin(θ)),而y'軸的單位向量則從(0,1)變成了(-sin(θ),cos(θ))。

旋轉過的單位向量可以由前兩天講過的極座標算出來。同學們看看下面這張圖應該就不難理解這兩個單位向量是怎麼算出來的。
axis rotate

因此,根據前面講過的結論,可以知道新的P'的位置應該就要等於3個新的x'軸單位向量以及2個y'軸單位向量的和。
https://chart.googleapis.com/chart?cht=tx&chl=P'%20%3D%203%20%5Ctimes%20(cos(%5Ctheta)%2Csin(%5Ctheta))%20%2B%202%20%5Ctimes%20(-sin(%5Ctheta)%2Ccos(theta))
展開後就得到,
https://chart.googleapis.com/chart?cht=tx&chl=P'%20%3D%20(%203%20%5Ccdot%20cos(%5Ctheta)%20-%202%20%5Ccdot%20sin(%5Ctheta)%20%2C%203%20%5Ccdot%20sin(%5Ctheta)%20%2B%202%20%5Ccdot%20cos(%5Ctheta)%20)
我們把P'的x和y分開來寫就會發現,
https://chart.googleapis.com/chart?cht=tx&chl=P'x%20%3D%203%20%5Ccdot%20cos(%5Ctheta)%20-%202%20%5Ccdot%20sin(%5Ctheta)
https://chart.googleapis.com/chart?cht=tx&chl=P'y%20%3D%203%20%5Ccdot%20sin(%5Ctheta)%20%2B%202%20%5Ccdot%20cos(%5Ctheta)
仔細一看,這個式子不正和先前我們用聯立方程式導出來的結果一模一樣嗎。

是時候把這個公式寫成函式了。

/** 旋轉向量的函式,參數如下
 * - vector: 要拿來旋轉的向量
 * - theta: 要轉的弧度
 */
function vectorRotate(vector: Vector, theta: number): Vector {
    let cos = Math.cos(theta);
    let sin = Math.sin(theta);
    return new Vector(
        vector.x * cos - vector.y * sin, // 新的x'
        vector.x * sin + vector.y * cos  // 新的y'
    );
}

CG示範專案
在示範專案中,每按一次鈕會讓魔法棒旋轉30度。同學們也可以複製這個專案出來(使用刀叉按鈕),自行改變旋轉角度,親自驗證這個函式的正確性。


上一篇
Trick 9: 活塞運動的嘆息:sin與cos
下一篇
Trick 11: 站在彈道上的女孩-圓與線的碰撞問題
系列文
30個遊戲程設的錦囊妙計32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言