大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 13 篇文章。本系列文章從 WebGL 基本運作機制以及使用的原理開始介紹,最後建構出繪製 3D、光影效果之網頁。介紹完 WebGL 運作方式與 2D transform 後,本章節講述的是建構並 transform 渲染成 3D 物件,如果在閱讀本文時覺得有什麼未知的東西被當成已知的,可能可以在前面的文章中找到相關的內容
有了 matrix4.perspective()
使用眼睛/相機的方式進行成像,反而讓畫面變成一片慘白,假設把 3D 物件本身的 transform 取消,也就是 worldMatrix
設定成 matrix4.identity()
,那麼 3D 物件與 frustum 區域的相對關係看起來像是這樣:
3D 物件不在 frustum 中,因此什麼都看不到,我們當然可以直接把 3D 物件做 translate 移動到 frustum 中,但是在現實生活中,如果架設好了一個場景,放了很多物件,而相機的位置不對,這時候我們會移動的是相機,而不是整個場景,這就是接下來要做的事情
viewMatrix
與視角 transform先前在製作作用在 a_position
上的 u_matrix
之前,會先產生兩個矩陣相乘:viewMatrix
與 worldMatrix
,繪製多個物件時 viewMatrix
為同一顆相機/畫面下的所有物件共用,worldMatrix
則為物件本身的 transform,會因為不同物件而異;接下來要加入的視角 transform 想當然爾為同一顆相機/畫面下的所有物件共用,因此 viewMatrix
除了 clip space 的 transform 之外,也要開始包含視角相關的 transform,成為名符其實的 viewMatrix
雖然筆者才剛說我們不應該移動整個場景來符合相機位置(就放在 viewMatrix
這部份的抽象來說確實像是移動相機本身),但是視角 transform 本身能做的事情就是移動場景,所有的 a_position
/ 物件都會經過視角 transform:
clip space <= perspective <= 視角 transform <= worldMatrix transform <= a_position
^...viewMatrix transform...^
假設我們想要把相機放在這個位置:
把移動相機的 transform 叫做 cameraMatrix
,因為視角 transform 只能移動整個場景,所以視角 transform 可以當成『反向做 cameraMatrix
』,對整個場景做反向的 cameraMatrix
,在定義 cameraMatrix
這件事情上,就真的抽象成移動相機了;反向這件事可以靠反矩陣(inverse matrix) 來做到,為什麼的部份筆者只好再推薦一次 3Blue1Brown 的 Youtube 影片 -- 反矩陣、行空間與零空間,看了精美的動畫之後,希望大家就能理解為什麼反矩陣在幾何上等於把某個 transform 反向的做
以實際作用在 a_position
向量上的視角 transform(等於 inverse(cameraMatrix)
)來說,看起來像是這樣:
最後當然得在 lib/matrix.js
中加入 matrix4.inverse(m)
的實做,不過,有做過三階反矩陣運算就會知道計算量不小,更何況我們需要的是 4x4 四階,寫成程式碼的公式行數實在不少,筆者就不直接放在文章中了,有需要可以在下方完整程式碼中找到
viewMatrix
內要實做上圖紅色箭頭的 cameraMatrix
,看起來用 matrix4.translate()
就足夠,因此在主程式 render()
內定義 viewMatrix
之前加上:
const cameraMatrix = matrix4.translate(250, 0, 400);
並且像是上面說的,使用 matrix4.inverse(cameraMatrix)
加入 viewMatrix
:
const viewMatrix = matrix4.multiply(
matrix4.perspective(state.fieldOfView, gl.canvas.width / gl.canvas.height, 0.1, 2000),
matrix4.inverse(cameraMatrix),
);
存檔看看結果:
這個 P 上下顛倒了,而且現在現在看到的不是正面,在建構模型時,除了其背面往 +z 長之外,我們使用 2D 時慣用的 y 軸正向為螢幕下方方向,這些與 3D 中使用的慣例都是相反的,同時也可以看本篇第一張圖中標示的『螢幕上方方向』想像看到的畫面;如果要重新定位 P 形狀的 3D 模型實在是太累,因此筆者選擇修改 rotationX
的預設值轉 210
度過去:
// async function setup() {
// ...
return {
gl,
program, attributes, uniforms,
buffers, modelBufferArrays,
state: {
fieldOfView: 45 * Math.PI / 180,
translate: [150, 100, 0],
- rotate: [degToRad(30), degToRad(30), degToRad(0)],
+ rotate: [degToRad(210), degToRad(30), degToRad(0)],
scale: [1, 1, 1],
},
}
}
HTML 那邊的預設值也得改一下,看起來就好多囉:
完整程式碼可以在這邊找到: