用JavaScript玩轉計算機圖形學(一)光線追蹤入門
系列簡介
記得小時候讀過一本關於計算機圖形學(computer graphics, CG)的入門書,從此就愛上了CG。本系列希望,採用很多人認識的JavaScript語言去分享CG,令更多人有機會接觸,並愛上CG。
本系列的特點之一,是讀者能在瀏覽器裡直接執行程式碼,也可重覆修改程式碼測試。透過這種互動,也許能更深刻體會內容。讀者只要懂得JavaScript(因為JavaScript很簡單,學過Java/C/C++/C#之類的語言也應沒問題)和一點點線性代數(linear algebra)就可以了。
筆者在大學期間並沒有修讀CG課程,雖然看過相關書籍,始終未親手做過全域光照的渲染器,本文也作為個人的學習分享。此外,筆者也差不多十年沒接觸JavaScript,希望各位不吝賜教。
本文簡介
多數程式設計師聽到3D CG,就會聯想到Direct3D、OpenGL等API。事實上,這些流行的API主要為實時渲染(real-time rendering)而設,一般採用光柵化(rasterization)方式,渲染大量的三角形(或其他幾何圖元種類(primitive types))。這種基於光柵化的渲染系統,只支援區域性光照(local illumination)。換句話說,渲染幾何圖形的一個畫素時,光照計算只能取得該畫素的資訊,而不能訪問其他幾何圖形資訊。理論上,陰影(shadow)、反射(reflection)、折射(refraction)等為全域性光照(global illumination)效果,實際上,柵格化渲染系統可以使用預處理(如陰影貼圖(shadow mapping)、環境貼圖(environment mapping))去模擬這些效果。
全域性光照計算量大,一般也沒有特殊硬體加速(通常只使用CPU而非GPU),所以只適合離線渲染(offline rendering),例如3D Studio Max、Maya等工具。其中一個支援全域性光照的方法,稱為光線追蹤(ray tracing)。光線追蹤能簡單直接地支援陰影、反射、折射,實現起來亦非常容易。本文的例子裡,只用了數十行JavaScript程式碼(除canvas外不需要其他特殊外掛和庫),就能實現一個支援反射的光線追蹤渲染器。光線追蹤可以用來學習很多計算機圖形學的課題,也許比學習Direct3D/OpenGL更容易。現在,先介紹點理論吧。
光線追蹤
光柵化渲染,簡單地說,就是把大量三角形畫到螢幕上。當中會採用深度緩衝(depth buffer, z-buffer),來解決多個三角形重疊時的前後問題。三角形數目影響效能,但三角形在螢幕上的總面積才是主要瓶頸。
光線追蹤,簡單地說,就是從攝影機的位置,通過影像平面上的畫素位置(比較正確的說法是取樣(sampling)位置),發射一束光線到場景,求光線和幾何圖形間最近的交點,再求該交點的著色。如果該交點的材質是反射性的,可以在該交點向反射方向繼續追蹤。光線追蹤除了容易支援一些全域性光照效果外,亦不侷限於三角形作為幾何圖形的單位。任何幾何圖形,能與一束光線計算交點(intersection point),就能支援。
上圖(來源)顯示了光線追蹤的基本方式。要計算一點是否在陰影之內,也只須發射一束光線到光源,檢測中間有沒有障礙物而已。不過光源和陰影留待下回分解。
初試畫板
光線追蹤的輸出只是一個影像(image),所謂影像,就是二維顏色陣列。
要在瀏覽器內,用JavaScript生成一個影像,目前可以使用HTML 5的<canvas>。但現時Internet Explorer(直至版本8)還不支援<canvas>,其他瀏覽器如Chrome、Firefox、Opera等就可以。
以下是一個簡單的實驗,把每個象素填入顏色,左至右越來越紅,上至下越來越綠。
左邊的canvas定義如下:
修改程式碼試試看
|
這實驗說明,從canvas取得的影像資料canvas.getImageData(...).data是個一維陣列,該陣列每四個元素代表一個象素(按紅, 綠, 藍, alpha排列),這些象素在影像中從上至下、左至右排列。
解決實驗平臺的技術問題後,可開始從基礎類別開始實現。
基礎類
筆者使用基於物件(object-based)的方式編寫JavaScript。
三維向量
三維向量(3D vector)可謂CG裡最常用型別了。這裡三維向量用Vector3類實現,用(x, y, z)表示。 Vector3亦用來表示空間中的點(point),而不另建類。先看程式碼:
1234567891011121314151617 | Vector3 = function (x, y, z) { this .x = x; this .y = y; this .z = z; }; Vector3.prototype = { copy : function () { return new Vector3( this .x, this .y, this .z); }, length : function () { return Math.sqrt( this .x * this .x + this .y * this .y +
|