使用 glMatrix


在〈寫個 2D 矩陣庫〉寫了個簡單的程式庫,只不過自行實現矩陣運算好像有點麻煩,有沒有現成的程式庫?

在我撰寫這份文件之前,我曾經玩過〈WebGL〉,在矩陣運算方面,有個 glMatrix 程式庫可以使用,實際上,〈寫個 2D 矩陣庫〉中簡單的程式庫,就是仿造 glMatrix 的簡單版本。

撰寫本文時,glMatrix 版本是 2.0,src 目錄中的原始碼是基於 ES6 實作,使用 ES6 模組功能來管理不同的模組原始碼,可以在 dist 中找到 gl-matrix.js,這是透過建構工具建構後,不使用 ES6 相關語法的版本,在瀏覽器載入的話,會有個全域變數 glMatrix,擁有的特性是 src 看到的各個模組名稱。

gl-matrix-min.js 是 gl-matrix.js 的壓縮版本,實際上線後的頁面可以使用這個版本。

如果要查詢 API 文件,可以查看 Documentation,基本上就是將原始碼中註解文件的部份用 JSDoc 網頁化,我是都直接看原始碼中的註解。

glMatrix 採用 OpenGL/WebGL 的慣例,以線性陣列來實作矩陣時,都是採取行為主(column-major),矩陣相關函式的參數部份可以接受 ArrayFloat32Array 或 Array-like 實例,不過傳回型態多半是 Float32Array 而不是 Array,這是為了配合 WebGL,不過 Float32Array 是 Array-like,必要時也可以使用 Array.from 轉為 Array,用來搭配 p5.js,倒也不會有什麼大問題。

如果 glmatrix-min.js 放在 js 資料夾,可以透過 <script src="js/gl-matrix-min.js"></script> 來引入 glmatrix-min.js,那麼該怎麼使用 mat3 呢?直接透過 glMatrix.mat3 當然也可以,另一個方式是指定給變數,若不想要 glMatrix 全域變數,也可將之刪除:

<script src="js/p5.min.js"></script>
<script src="js/gl-matrix-min.js"></script>
<script>
    const mat3 = glMatrix.mat3;
    delete window.glMatrix;

    ...你的程式碼
</script>

這麼一來 mat3 就成為全域變數了,若不想要它是全域變數呢?在 p5.js 的〈libraries tutorial〉中介紹了,如何將你的程式庫介接至 p5.js,根據該文件,最簡單的方式是:

<script src="js/p5.min.js"></script>
<script src="js/gl-matrix-min.js"></script>
<script>
    p5.prototype.mat3 = glMatrix.mat3;
    delete window.glMatrix;

    ...你的程式碼
</script>

這麼一來就解決了全域變數的問題,而在 p5.js 的 setupdraw 函式中,也可以直接使用 mat3 這個名稱(其實是以 p5 實例為名稱空間,這是 p5.js 實作上的一點小技巧)。

來看幾個 glMatrix 簡單的使用案例,以 mat3 為例,要建立 3 x 3 單位矩陣,可以使用 mat3.create,也就是傳回的矩陣若以陣列表示會是:

[
    1, 0, 0, 
    0, 1, 0, 
    0, 0, 1
]

mat3.translate(m, m, [10, 10]) 的第一個參數會用來儲存位移操作後的結果,第二個參數是要參與位移運算的矩陣,就上例來說,m 的內容會被改變,也就是 m 結果會是:

[
    1, 0, 0,
    0, 1, 0, 
    10, 10, 1
]

如果不想改變 m,就是在第一個參數指定另一個矩陣,例如:

const m = mat3.create();
console.log(m);

const r = mat3.translate(mat3.create(), m, [10, 10]);
console.log(m);  // 可以看到 m 內容沒有改變
console.log(r);  // 矩陣運算後的結果

因此,對於〈寫個 2D 矩陣庫〉中第二個範例,就可以改寫為以下:

<html>
  <body>
    <script src="js/p5.min.js"></script>
    <script src="js/gl-matrix-min.js"></script>
    <script>
        p5.prototype.mat3 = glMatrix.mat3;
        delete window.glMatrix;

        p5.prototype.forApplyMatrix = function(m) {
            return m.filter((elem, idx) => (idx + 1) % 3 !== 0);
        };

        const d1 = 150;
        const d2 = 50;

        function setup() {
            createCanvas(300, 300);
            frameRate(15);
        }

        let angle = 0;
        function draw() {
            angle = (angle + 10) % 360;

            const m = mat3.create();
            mat3.translate(m, m, [width / 2, height / 2]);     
            mat3.rotate(m, m, angle * PI / 180); 

            background(255, 0, 0);

            applyMatrix(...forApplyMatrix(m)); // 套用矩陣
            circle(d1 / 2, 0, d2);
        }
    </script>
  </body>
</html>

執行的結果與〈寫個 2D 矩陣庫〉第二個範例是相同的。

接下來只是個人的實驗,先前文件的範例都使用了 p5.js-widget 提供的編輯器來展示,只不過,該怎麼讓編輯器外掛程式庫啊?單純使用 script 標籤沒有用,p5.js-widget 官方沒有提供方法。

在不修改 p5.js-widget 原始碼的情況下,我的做法是寫個 script 函式來動態載入外部程式庫,載入後將程式庫中要用到的名稱,掛到 window (畢竟 p5.js-widget 只用來示範一些小程式,就別太在意全域變數的問題了),例如:

可以將編輯區的捲軸往下來,就可以看到相關的程式碼了,因為 script 函式是非同步,為了在執行 setupdraw 前能準備好 glMatrix,這邊阻斷了 JavaScript 的執行(阻斷了 0.5 秒),希望這段時間內瀏覽器能完成下載(在瀏覽器的執行緒,不是 JavaScript 的執行緒),這是個不得不為的做法,然而絕大部份情況下能夠成功執行。

之所以不得不用迴圈阻斷的方式,是因為使用 p5.js-widget 其實有些限制,只能以全域模式執行 p5.js,編輯器中的全域範圍又拿不到 p5 這個名稱,因此無法透過 p5.js 的 preload 機制(之後文件中再介紹),來處理非同步下載程式庫後再呼叫 setupdraw