WebGL和Three.js工作原理圖解
一、我們講什么?
我們講兩個(gè)東西:
- 1、WebGL背后的工作原理是什么?
- 2、以Three.js為例,講述框架在背后扮演什么樣的角色?
二、我們?yōu)槭裁匆私庠恚?/h2>
我們假定你對(duì)WebGL已經(jīng)有一定了解,或者用Three.js做過(guò)了一些東西,這個(gè)時(shí)候,你可能碰到了這樣一些問(wèn)題:
- 1、很多東西還是做不出來(lái),甚至沒(méi)有任何思路;
- 2、碰到bug無(wú)法解決,甚至沒(méi)有方向;
- 3、性能出現(xiàn)問(wèn)題,完全不知道如何去優(yōu)化。
這個(gè)時(shí)候,我們需要了解更多。
三、先了解一個(gè)基礎(chǔ)概念
1、什么是矩陣?
簡(jiǎn)單說(shuō)來(lái),矩陣用于坐標(biāo)變換,如下圖:
2、那它具體是怎么變換的呢,如下圖:
3、舉個(gè)實(shí)例,將坐標(biāo)平移2,如下圖:
如果這時(shí)候,你還是沒(méi)有理解,沒(méi)有關(guān)系,你只需要知道,矩陣用于坐標(biāo)變換。
四、WebGL的工作原理
4.1、WebGL API
在了解一門(mén)新技術(shù)前,我們都會(huì)先看看它的開(kāi)發(fā)文檔或者API。
查看Canvas的繪圖API,我們會(huì)發(fā)現(xiàn)它能畫(huà)直線、矩形、圓、弧線、貝塞爾曲線。
于是,我們看了看WebGL繪圖API,發(fā)現(xiàn):
它只能會(huì)點(diǎn)、線、三角形?一定是我看錯(cuò)了。
沒(méi)有,你沒(méi)看錯(cuò)。
就算是這樣一個(gè)復(fù)雜的模型,也是一個(gè)個(gè)三角形畫(huà)出來(lái)的。
4.2、WebGL繪制流程
簡(jiǎn)單說(shuō)來(lái),WebGL繪制過(guò)程包括以下三步:
1、獲取頂點(diǎn)坐標(biāo)
2、圖元裝配(即畫(huà)出一個(gè)個(gè)三角形)
3、光柵化(生成片元,即一個(gè)個(gè)像素點(diǎn))
接下來(lái),我們分步講解每個(gè)步驟。
4.2.1、獲取頂點(diǎn)坐標(biāo)
頂點(diǎn)坐標(biāo)從何而來(lái)呢?一個(gè)立方體還好說(shuō),如果是一個(gè)機(jī)器人呢?
沒(méi)錯(cuò),我們不會(huì)一個(gè)一個(gè)寫(xiě)這些坐標(biāo)。
往往它來(lái)自三維軟件導(dǎo)出,或者是框架生成,如下圖:
寫(xiě)入緩存區(qū)是啥?
沒(méi)錯(cuò),為了簡(jiǎn)化流程,之前我沒(méi)有介紹。
由于頂點(diǎn)數(shù)據(jù)往往成千上萬(wàn),在獲取到頂點(diǎn)坐標(biāo)后,我們通常會(huì)將它存儲(chǔ)在顯存,即緩存區(qū)內(nèi),方便GPU更快讀取。
4.2.2、圖元裝配
我們已經(jīng)知道,圖元裝配就是由頂點(diǎn)生成一個(gè)個(gè)圖元(即三角形)。那這個(gè)過(guò)程是自動(dòng)完成的嗎?答案是并非完全如此。
為了使我們有更高的可控性,即自由控制頂點(diǎn)位置,WebGL把這個(gè)權(quán)力交給了我們,這就是可編程渲染管線(不用理解)。
WebGL需要我們先處理頂點(diǎn),那怎么處理呢?我們先看下圖:
我們引入了一個(gè)新的名詞,叫“頂點(diǎn)著色器”,它由opengl es編寫(xiě),由javascript以字符串的形式定義并傳遞給GPU生成。
比如如下就是一段頂點(diǎn)著色器代碼:
attribute vec4 position;
void main() {
gl_Position = position;
}
attribute修飾符用于聲明由瀏覽器(javascript)傳輸給頂點(diǎn)著色器的變量值;
position即我們定義的頂點(diǎn)坐標(biāo);
gl_Position是一個(gè)內(nèi)建的傳出變量。
這段代碼什么也沒(méi)做,如果是繪制2d圖形,沒(méi)問(wèn)題,但如果是繪制3d圖形,即傳入的頂點(diǎn)坐標(biāo)是一個(gè)三維坐標(biāo),我們則需要轉(zhuǎn)換成屏幕坐標(biāo)。
比如:v(-0.5, 0.0, 1.0)轉(zhuǎn)換為p(0.2, -0.4),這個(gè)過(guò)程類似我們用相機(jī)拍照。
4.2.2.1、頂點(diǎn)著色器處理流程
回到剛才的話題,頂點(diǎn)著色器是如何處理頂點(diǎn)坐標(biāo)的呢?
如上圖,頂點(diǎn)著色器會(huì)先將坐標(biāo)轉(zhuǎn)換完畢,然后由GPU進(jìn)行圖元裝配,有多少頂點(diǎn),這段頂點(diǎn)著色器程序就運(yùn)行了多少次。
你可能留意到,這時(shí)候頂點(diǎn)著色器變?yōu)椋?/p>
attribute vec4 position;
uniform mat4 matrix;
void main() {
gl_Position = position * matrix;
}
這就是應(yīng)用了矩陣matrix,將三維世界坐標(biāo)轉(zhuǎn)換成屏幕坐標(biāo),這個(gè)矩陣叫投影矩陣,由javascript傳入,至于這個(gè)matrix怎么生成,我們暫且不討論。
4.2.3、光柵化
和圖元裝配類似,光柵化也是可控的。
在圖元生成完畢之后,我們需要給模型“上色”,而完成這部分工作的,則是運(yùn)行在GPU的“片元著色器”來(lái)完成。
它同樣是一段opengl es程序,模型看起來(lái)是什么質(zhì)地(顏色、漫反射貼圖等)、燈光等由片元著色器來(lái)計(jì)算。
如下是一段簡(jiǎn)單的片元著色器代碼:
precision mediump float;
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
gl_FragColor即輸出的顏色值。
4.2.3.1、片元著色器處理流程
片元著色器具體是如何控制顏色生成的呢?
如上圖,頂點(diǎn)著色器是有多少頂點(diǎn),運(yùn)行了多少次,而片元著色器則是,生成多少片元(像素),運(yùn)行多少次。
4.3、WebGL的完整工作流程
至此,實(shí)質(zhì)上,WebGL經(jīng)歷了如下處理流程:
1、準(zhǔn)備數(shù)據(jù)階段
在這個(gè)階段,我們需要提供頂點(diǎn)坐標(biāo)、索引(三角形繪制順序)、uv(決定貼圖坐標(biāo))、法線(決定光照效果),以及各種矩陣(比如投影矩陣)。
其中頂點(diǎn)數(shù)據(jù)存儲(chǔ)在緩存區(qū)(因?yàn)閿?shù)量巨大),以修飾符attribute傳遞給頂點(diǎn)著色器;
矩陣則以修飾符uniform傳遞給頂點(diǎn)著色器。
2、生成頂點(diǎn)著色器
根據(jù)我們需要,由Javascript定義一段頂點(diǎn)著色器(opengl es)程序的字符串,生成并且編譯成一段著色器程序傳遞給GPU。
3、圖元裝配
GPU根據(jù)頂點(diǎn)數(shù)量,挨個(gè)執(zhí)行頂點(diǎn)著色器程序,生成頂點(diǎn)最終的坐標(biāo),完成坐標(biāo)轉(zhuǎn)換。
4、生成片元著色器
模型是什么顏色,看起來(lái)是什么質(zhì)地,光照效果,陰影(流程較復(fù)雜,需要先渲染到紋理,可以先不關(guān)注),都在這個(gè)階段處理。
5、光柵化
能過(guò)片元著色器,我們確定好了每個(gè)片元的顏色,以及根據(jù)深度緩存區(qū)判斷哪些片元被擋住了,不需要渲染,最終將片元信息存儲(chǔ)到顏色緩存區(qū),最終完成整個(gè)渲染。
五、Three.js究竟做了什么?
我們知道,three.js幫我們完成了很多事情,但是它具體做了什么呢,他在整個(gè)流程中,扮演了什么角色呢?
我們先簡(jiǎn)單看一下,three.js參與的流程:
黃色和綠色部分,都是three.js參與的部分,其中黃色是javascript部分,綠色是opengl es部分。
我們發(fā)現(xiàn),能做的,three.js基本上都幫我們做了。
- 輔助我們導(dǎo)出了模型數(shù)據(jù);
- 自動(dòng)生成了各種矩陣;
- 生成了頂點(diǎn)著色器;
- 輔助我們生成材質(zhì),配置燈光;
- 根據(jù)我們?cè)O(shè)置的材質(zhì)生成了片元著色器。
而且將webGL基于光柵化的2D API,封裝成了我們?nèi)祟惸芸炊?3D API。
5.1、Three.js頂點(diǎn)處理流程
從WebGL工作原理的章節(jié)中,我們已經(jīng)知道了頂點(diǎn)著色器會(huì)將三維世界坐標(biāo)轉(zhuǎn)換成屏幕坐標(biāo),但實(shí)際上,坐標(biāo)轉(zhuǎn)換不限于投影矩陣。
如下圖:
之前WebGL在圖元裝配之后的結(jié)果,由于我們認(rèn)為模型是固定在坐標(biāo)原點(diǎn),并且相機(jī)在x軸和y軸坐標(biāo)都是0,其實(shí)正常的結(jié)果是這樣的:
5.1.1、模型矩陣
現(xiàn)在,我們將模型順時(shí)針旋轉(zhuǎn)Math.PI/6,所有頂點(diǎn)位置肯定都變化了。
box.rotation.y = Math.PI/6;
但是,如果我們直接將頂點(diǎn)位置用javascript計(jì)算出來(lái),那性能會(huì)很低(頂點(diǎn)通常成千上萬(wàn)),而且,這些數(shù)據(jù)也非常不利于維護(hù)。
所以,我們用矩陣modelMatrix將這個(gè)旋轉(zhuǎn)信息記錄下來(lái)。
5.1.2、視圖矩陣
然后,我們將相機(jī)往上偏移30。
camera.position.y = 30;
同理,我們用矩陣viewMatrix將移動(dòng)信息記錄下來(lái)。
5.1.3、投影矩陣
這是我們之前介紹過(guò)的了,我們用projectMatrix記錄。
5.1.4、應(yīng)用矩陣
然后,我們編寫(xiě)頂點(diǎn)著色器:
gl_Position = position * modelMatrix * viewMatrix * projectionMatrix;
這樣,我們就在GPU中,將最終頂點(diǎn)位置計(jì)算出來(lái)了。
實(shí)際上,上面所有步驟,three.js都幫我們完成了。
5.2、片元著色器處理流程
我們已經(jīng)知道片元著色器負(fù)責(zé)處理材質(zhì)、燈光等信息,但具體是怎么處理呢?
如下圖:
5.3、three.js完整運(yùn)行流程:
當(dāng)我們選擇材質(zhì)后,three.js會(huì)根據(jù)我們所選的材質(zhì),選擇對(duì)應(yīng)的頂點(diǎn)著色器和片元著色器。
three.js中已經(jīng)內(nèi)置了我們常用著色器。