WebGL技術儲備指南

jopen 8年前發布 | 42K 次閱讀 WebGL

WebGL 是 HTML 5 草案的一部分,可以驅動 Canvas 渲染三維場景。WebGL 雖然還未有廣泛應用,但極具潛力和想象空間。本文是我學習 WebGL 時梳理知識脈絡的產物,花點時間整理出來與大家分享。

示例

WebGL 很酷,有以下 demos 為證:

尋找奧茲國
賽車游戲
劃船的男孩(Goo Engine Demo)

本文的目標

本文的預期讀者是:不熟悉圖形學,熟悉前端,希望了解或系統學習 WebGL 的同學。

本文不是 WebGL 的概述性文章,也不是完整詳細的 WebGL 教程。本文只希望成為一篇供 WebGL 初學者使用的提綱。

Canvas

熟悉 Canvas 的同學都知道,Canvas 繪圖先要獲取繪圖上下文:

var context = canvas.getContext('2d'); 

context上調用各種函數繪制圖形,比如:

// 繪制左上角為(0,0),右下角為(50, 50)的矩形
context.fillRect(0, 0, 50, 50); 

WebGL 同樣需要獲取繪圖上下文:

var gl = canvas.getContext('webgl'); // 或 experimental-webgl 

但是接下來,如果想畫一個矩形的話,就沒這么簡單了。實際上,Canvas 是瀏覽器封裝好的一個繪圖環境,在實際進行繪圖操作時,瀏覽器仍然需要調用 OpenGL API。而 WebGL API 幾乎就是 OpenGL API 未經封裝,直接套了一層殼。

Canvas 的更多知識,可以參考:

  • JS 權威指南的 21.4 節或 JS 高級程序設計中的 15 章
  • W3CSchool
  • 阮一峰的 Canvas 教程
  • </ul>

    矩陣變換

    三維模型,從文件中讀出來,到繪制在 Canvas 中,經歷了多次坐標變換。

    假設有一個最簡單的模型:三角形,三個頂點分別為(-1,-1,0),(1,-1,0),(0,1,0)。這三個數據是從文件中讀出來的,是三角形最初始的坐標(局部坐標)。如下圖所示,右手坐標系。

     WebGL技術儲備指南

    模型通常不會位于場景的原點,假設三角形的原點位于(0,0,-1)處,沒有旋轉或縮放,三個頂點分別為(-1,-1,-1),(1,-1,-1),(0,1,-1),即世界坐標。

     WebGL技術儲備指南

    繪制三維場景必須指定一個觀察者,假設觀察者位于(0,0,1)處而且看向三角形,那么三個頂點相對于觀察者的坐標為(-1,-1,-2),(1,-1,-2),(0,1,-2),即視圖坐標。

     WebGL技術儲備指南

    觀察者的眼睛是一個點(這是透視投影的前提),水平視角和垂直視角都是90度,視野范圍(目力所及)為[0,2]在Z軸上,觀察者能夠看到的區域是一個四棱臺體。

     WebGL技術儲備指南

    將四棱臺體映射為標準立方體(CCV,中心為原點,邊長為2,邊與坐標軸平行)。頂點在 CCV 中的坐標,離它最終在 Canvas 中的坐標已經很接近了,如果把 CCV 的前表面看成 Canvas,那么最終三角形就畫在圖中橙色三角形的位置。

     WebGL技術儲備指南

    上述變換是用矩陣來進行的。

    局部坐標 –(模型變換)-> 世界坐標 –(視圖變換)-> 視圖坐標 –(投影變換)–> CCV 坐標。

    以(0,1,0)為例,它的齊次向量為(0,0,1,1),上述變換的表示過程可以是:

     WebGL技術儲備指南

    上面三個矩陣依次是透視投影矩陣,視圖矩陣,模型矩陣。三個矩陣的值分別取決于:觀察者的視角和視野距離,觀察者在世界中的狀態(位置和方向),模 型在世界中的狀態(位置和方向)。計算的結果是(0,1,1,2),化成齊次坐標是(0,0.5,0.5,1),就是這個點在CCV中的坐標,那么 (0,0.5)就是在Canvas中的坐標(認為 Canvas 中心為原點,長寬都為2)。

    上面出現的(0,0,1,1)是(0,0,1)的齊次向量。齊次向量(x,y,z,w)可以代表三維向量(x,y,z)參與矩陣運算,通俗地說,w 分量為 1 時表示位置,w 分量為 0 時表示位移。

    WebGL 沒有提供任何有關上述變換的機制,開發者需要親自計算頂點的 CCV 坐標。

    關于坐標變換的更多內容,可以參考:

    • 計算機圖形學中的5-7章
    • 變換矩陣@維基百科
    • 透視投影詳解
    • </ul>

      比較復雜的是模型變換中的繞任意軸旋轉(通常用四元數生成矩陣)和投影變換(上面的例子都沒收涉及到)。

      關于繞任意軸旋轉和四元數,可以參考:

      • 四元數@維基百科
      • 一個老外對四元數公式的證明
      • </ul>

        關于齊次向量的更多內容,可以參考。

        • 計算機圖形學的5.2節
        • 齊次坐標@維基百科
        • </ul>

          著色器和光柵化

          在 WebGL 中,開發者是通過著色器來完成上述變換的。著色器是運行在顯卡中的程序,以 GLSL 語言編寫,開發者需要將著色器的源碼以字符串的形式傳給 WebGL 上下文的相關函數。

          著色器有兩種,頂點著色器和片元(像素)著色器,它們成對出現。頂點著色器任務是接收頂點的局部坐標,輸出 CCV 坐標。CCV 坐標經過光柵化,轉化為逐像素的數據,傳給片元著色器。片元著色器的任務是確定每個片元的顏色。

          頂點著色器接收的是 attribute 變量,是逐頂點的數據。頂點著色器輸出 varying 變量,也是逐頂點的。逐頂點的 varying 變量數據經過光柵化,成為逐片元的 varying 變量數據,輸入片元著色器,片元著色器輸出的結果就會顯示在 Canvas 上。

           WebGL技術儲備指南

          著色器功能很多,上述只是基本功能。大部分炫酷的效果都是依賴著色器的。如果你對著色器完全沒有概念,可以試著理解下一節 hello world 程序中的著色器再回顧一下本節。

          關于更多著色器的知識,可以參考:

          • GLSL@維基百科
          • WebGL@MSDN
          • </ul>

            程序

            這一節解釋繪制上述場景(三角形)的 WebGL 程序。點這個鏈接,查看源代碼,試圖理解一下。這段代碼出自WebGL Programming Guide,我作了一些修改以適應本文內容。如果一切正常,你看到的應該是下面這樣:

             WebGL技術儲備指南

            解釋幾點(如果之前不了解 WebGL ,多半會對下面的代碼困惑,無礙):

            1. 字符串 VSHADER_SOURCE 和 FSHADER_SOURCE 是頂點著色器和片元著色器的源碼。可以將著色器理解為有固定輸入和輸出格式的程序。開發者需要事先編寫好著色器,再按照一定格式著色器發送繪圖命令。

              </li>

            2. Part2 將著色器源碼編譯為 program 對象:先分別編譯頂點著色器和片元著色器,然后連接兩者。如果編譯源碼錯誤,不會報 JS 錯誤,但可以通過其他 API(如gl.getShaderInfo等)獲取編譯狀態信息(成功與否,如果出錯的錯誤信息)。

              // 頂點著色器
              var vshader = gl.createShader(gl.VERTEX_SHADER);
              gl.shaderSource(vshader, VSHADER_SOURCE);
              gl.compileShader(vshader);
              // 同樣新建 fshader
              var program = gl.createProgram();
              gl.attachShader(program, vshader);
              gl.attachShader(program, fshader);
              gl.linkProgram(program); 
              </li>

            3. program 對象需要指定使用它,才可以向著色器傳數據并繪制。復雜的程序通常有多個 program 對 象,(繪制每一幀時)通過切換 program 對象繪制場景中的不同效果。

              gl.useProgram(program); 
              </li>

            4. Part3 向正在使用的著色器傳入數據,包括逐頂點的 attribute 變量和全局的 uniform 變量。向著色器傳入數據必須使用 ArrayBuffer,而不是常規的 JS 數組。

              var varray = new Float32Array([-1, -1, 0, 1, -1, 0, 0, 1, 0]) 
              </li>

            5. WebGL API 對 ArrayBuffer 的操作(填充緩沖區,傳入著色器,繪制等)都是通過 gl.ARRAY_BUFFER 進行的。在 WebGL 系統中又很多類似的情況。

              // 只有將 vbuffer 綁定到 gl.ARRAY_BUFFER,才可以填充數據
              gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);
              // 這里的意思是,向“綁定到 gl.ARRAY_BUFFER”的緩沖區中填充數據
              gl.bufferData(gl.ARRAY_BUFFER, varray, gl.STATIC_DRAW);
              // 獲取 a_Position 變量在著色器程序中的位置,參考頂點著色器源碼
              var aloc = gl.getAttribLocation(program, 'a_Position');
              // 將 gl.ARRAY_BUFFER 中的數據傳入 aloc 表示的變量,即 a_Position
              gl.vertexAttribPointer(aloc, 3, gl.FLOAT, false, 0, 0);
              gl.enableVertexAttribArray(aloc); 
              </li>

            6. 向著色器傳入矩陣時,是按列存儲的。可以比較一下 mmatrix 和矩陣變換一節中的模型矩陣(第 3 個)。

              </li>

            7. 頂點著色器計算出的 gl_Position 就是 CCV 中的坐標,比如最上面的頂點(藍色)的 gl_Position 化成齊次坐標就是(0,0.5,0.5,1)。

              </li>

            8. 向頂點著色器傳入的只是三個頂點的顏色值,而三角形表面的顏色漸變是由這三個顏色值內插出的。光柵化不僅會對 gl_Position 進行,還會對 varying 變量插值。

              </li>

            9. gl.drawArrays()方法驅動緩沖區進行繪制,gl.TRIANGLES 指定繪制三角形,也可以改變參數繪制點、折線等等。

              </li> </ol>

              關于 ArrayBuffer 的詳細信息,可以參考:

              • ArrayBuffer@MDN
              • 阮一峰的 ArrayBuffer 介紹
              • 張鑫旭的 ArrayBuffer 介紹
              • </ul>

                關于 gl.TRIANGLES 等其他繪制方式,可以參考下面這張圖或這篇博文

                 WebGL技術儲備指南

                深度檢測

                當兩個表面重疊時,前面的模型會擋住后面的模型。比如這個例子,繪制了兩個交叉的三角形( varray 和 carray 的長度變為 18,gl.drawArrays 最后一個參數變為 6)。為了簡單,這個例子去掉了矩陣變換過程,直接向著色器傳入 CCV 坐標。

                 WebGL技術儲備指南

                 WebGL技術儲備指南

                頂點著色器給出了 6 個頂點的 gl_Position ,經過光柵化,片元著色器獲得了 2X 個片元(假設 X 為每個三角形的像素個數),每個片元都離散的 x,y 坐標值,還有 z 值。x,y 坐標就是三角形在 Canvas 上的坐標,但如果有兩個具有相同 x,y 坐標的片元同時出現,那么 WebGL 就會取 z 坐標值較小的那個片元。

                在深度檢測之前,必須在繪制前開啟一個常量。否則,WebGL 就會按照在 varray 中定義的順序繪制了,后面的會覆蓋前面的。

                gl.enable(gl.DEPTH_TEST); 

                實際上,WebGL 的邏輯是這樣的:依次處理片元,如果渲染緩沖區(這里就是 Canvas 了)的那個與當前片元對應的像素還沒有繪制時,就把片元的顏色畫到渲染緩沖區對應像素里,同時把片元的 z 值緩存在另一個深度緩沖區的相同位置;如果當前緩沖區的對應像素已經繪制過了,就去查看深度緩沖區中對應位置的 z 值,如果當前片元 z 值小,就重繪,否則就放棄當前片元。

                WebGL 的這套邏輯,對理解蒙版(后面會說到)有一些幫助。

                頂點索引

                gl.drawArrays()是按照頂點的順序繪制的,而 gl.drawElements()可以令著色器以一個索引數組為順序繪制頂點。比如這個例子

                 WebGL技術儲備指南

                這里畫了兩個三角形,但只用了 5 個頂點,有一個頂點被兩個三角形共用。這時需要建立索引數組,數組的每個元素表示頂點的索引值。將數組填充至gl.ELEMENT_ARRAY,然后調用 gl.drawElements()。

                var iarray = new Uint8Array([0,1,2,2,3,4]);
                var ibuffer = gl.createBuffer(gl.ARRAY_BUFFER, ibuffer);
                gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer);
                gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iarray, gl.STATIC_DRAW); 

                紋理

                attribute 變量不僅可以傳遞頂點的坐標,還可以傳遞其他任何逐頂點的數據。比如 HelloTriangle 程序把單個頂點的顏色傳入了 a_Color,片元著色器收到 v_Color 后直接賦給 gl_FragmentColor,就決定了顏色。

                attribute 變量還可以幫助繪制紋理。繪制紋理的基本原理是,為每個頂點指定一個紋理坐標(在(0,0)與(1,1,)的正方形中),然后傳入紋理對象。片元著色器拿 到的是對應片元的內插后的紋理坐標,就利用這個紋理坐標去紋理對象上取顏色,再畫到片元上。內插后的紋理坐標很可能不恰好對應紋理上的某個像素,而是在幾 個像素之間(因為通常的圖片紋理也是離散),這時可能會通過周圍幾個像素的加權平均算出該像素的值(具體有若干種不同方法,可以參考)。

                比如這個例子

                 WebGL技術儲備指南

                紋理對象和緩沖區對象很類似:使用 gl 的 API 函數創建,需要綁定至常量 gl.ARRAY_BUFFER 和 gl.TEXTURE_2D ,都通過常量對象向其中填入圖像和數據。不同的是,紋理對象在綁定時還需要激活一個紋理單元(此處的gl.TEXTURE0),而 WebGL 系統支持的紋理單元個數是很有限的(一般為 8 個)。

                var texture = gl.createTexture();
                gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
                gl.activeTexture(gl.TEXTURE0);
                gl.bindTexture(gl.TEXTURE_2D, texture);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, textureImage);
                var sloc = gl.getUniformLocation(program, 'u_Sampler');
                gl.uniform1i(sloc, 0); 

                片元著色器內聲明了 sampler2D 類型的 uniform 變量,通過texture2D函數取樣。

                precision mediump float;
                uniform sampler2D u_Sampler;
                varying vec2 v_TexCoord;
                void main() {
                  gl_FragColor = texture2D(u_Sampler, v_TexCoord);
                }; 

                混合與蒙版

                透明效果是用混合機制完成的。混合機制與深度檢測類似,也發生在試圖向某個已填充的像素填充顏色時。深度檢測通過比較z值來確定像素的顏色,而混合機制會將兩種顏色混合。比如這個例子

                 WebGL技術儲備指南

                混合的順序是按照繪制的順序進行的,如果繪制的順序有變化,混合的結果通常也不同。如果模型既有非透明表面又有透明表面,繪制透明表面時開啟蒙版, 其目的是鎖定深度緩沖區,因為半透明物體后面的物體還是可以看到的,如果不這樣做,半透明物體后面的物體將會被深度檢測機制排除。

                開啟混合的代碼如下。gl.blendFunc方法指定了混合的方式,這里的意思是,使用源(待混合)顏色的 α 值乘以源顏色,加上 1-[源顏色的 α]乘以目標顏色。

                gl.enable(gl.BLEND);
                gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 

                所謂 α 值,就是顏色的第 4 個分量。

                var carray = new Float32Array([
                  1,0,0,0.7,1,0,0,0.7,1,0,0,0.7,
                  0,0,1,0.4,0,0,1,0.4,0,0,1,0.4
                  ]); 

                瀏覽器的WebGL系統

                WebGL 系統各個組成部分在既定規則下互相配合。稍作梳理如下。

                 WebGL技術儲備指南

                這張圖比較隨意,箭頭上的文字表示 API,箭頭方向大致表現了數據的流動方向,不必深究。

                光照

                WebGL 沒有為光照提供任何內置的方法,需要開發者在著色器中實現光照算法。

                光是有顏色的,模型也是有顏色的。在光照下,最終物體呈現的顏色是兩者共同作用的結果。

                實現光照的方式是:將光照的數據(點光源的位置,平行光的方向,以及光的顏色和強度)作為 uniform 變量傳入著色器中,將物體表面每個頂點處的法線作為 attribute 變量傳入著色器,遵循光照規則,修訂最終片元呈現的顏色。

                光照又分為逐頂點的和逐片元的,兩者的區別是,將法線光線交角因素放在頂點著色器中考慮還是放在片元著色器中考慮。逐片元光照更加逼真,一個極端的例子是:

                 WebGL技術儲備指南

                此時,點光源在距離一個表面較近處,表面中央 A 處較亮,四周較暗。但是在逐頂點光照下,表面的顏色(的影響因子)是由頂點內插出來的,所以表面中央也會比較暗。而逐片元光照直接使用片元的位置和法線計算與點光源的交角,因此表面中央會比較亮。

                復雜模型

                復雜模型可能有包括子模型,子模型可能與父模型有相對運動。比如開著雨刮器的汽車,雨刮器的世界坐標是受父模型汽車,和自身的狀態共同決定的。若要計算雨刮器某頂點的位置,需要用雨刮器相對汽車的模型矩陣乘上汽車的模型矩陣,再乘以頂點的局部坐標。

                復雜模型可能有很多表面,可能每個表面使用的著色器就不同。通常將模型拆解為組,使用相同著色器的表面為一組,先繪制同一組中的內容,然后切換著色器。每次切換著色器都要重新將緩沖區中的數據分配給著色器中相應變量。

                動畫

                動畫的原理就是快速地擦除和重繪。常用的方法是大名鼎鼎的 requestAnimationFrame 。不熟悉的同學,可以參考正美的介紹

                WebGL庫

                目前最流行的 WebGL 庫是 ThreeJS,很強大,官網代碼

                調試工具

                比較成熟的 WebGL 調試工具是WebGL Inspector

                網絡資源和書籍

                英文的關于 WebGL 的資源有很多,包括:

                • learning webgl
                • WebGL@MDN
                • WebGL Cheat Sheet
                • </ul>

                  國內最早的 WebGL 教程是由郝稼力翻譯的,放在 hiwebgl 上,目前 hiwebgl 已經關閉,但教程還可以在這里找到。郝稼力目前運營著Lao3D

                  國內已經出版的 WebGL 書籍有:

                  • WebGL入門指南:其實是一本講 ThreeJS 的書
                  • WebGL高級編程:還不錯的一本
                  • WebGL編程指南:相當靠譜的全面教程
                  • </ul>

                    最后再夾雜一點私貨吧。讀書期間我曾花了小半年時間翻譯了一本WebGL的書,也就是上面的第 3 本。這本書確實相當靠譜,網上各種教程里很多沒說清楚的東西,這本書說得很清楚,而且還提供了一份很完整的API文檔。翻譯這本書的過程也使我受益匪淺。如果有同學愿意系統學一下 WebGL 的,建議購買一本(文青建議買英文版)

                    </div>
                    來自: http://taobaofed.org/blog/2015/12/21/webgl-handbook/

                     本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
                     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
                     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!