WebGL學習(1) - 三角形

Jeff.Zhong發表於2017-11-09

  原文地址:WebGL學習(1) - 三角形

  還記得第一次看到canvas的粒子特效的時候,真的把我給驚豔到了,原來在瀏覽器也能做出這麼棒的效果。結合《HTML5 Canvas核心技術》和網上的教程,經過半年斷斷續續的學習,對canvas的學習終於完結,對常用的canvas特效基本能做到信手拈來的。canvas特效請看:樣例列表

  眾所周知,canvas是2D繪圖技術,雖然可以通過座標變換,位置計算也能做到3D的效果。但3D場景資料量畢竟比2D要高一個數量級的,純粹用canvas的話,不管是效能和開發的複雜度會成為一個瓶頸。
  這也是webGL出現的原因,解決web端3D渲染的場景。webGL會呼叫到GPU,處理大量重複的3D場景資料時,效能非常有優勢。同時webGL是基於openGL ES 2.0, 因此它處理3D場景是非常成熟的。但為什麼不直接學習three.js呢?因為本人對圖形學感興趣,只是希望做一些自己喜歡的效果的同時深入瞭解計算機圖形學,沒指望通過它做商業專案。

  為了讓學習更有動力和目的性,我們以例項為導向學習webGL,再從中展開到需要學習哪些知識點。這次我們來實現如下的動畫,該教程參考了《WebGL程式設計指南》

實際效果請看:旋轉的三角形

WebGL學習(1) - 三角形

webGL渲染流程

webGL的渲染流程如下,其中第2,3,4步是重點,裡面細節比較多。接著我們就按這個流程一步一步解決問題

  1. 獲取webGL繪圖上下文
  2. 初始化著色器
  3. 建立、繫結緩衝區物件
  4. 向頂點著色器和片元著色器寫入資料
  5. 設定canvas背景色,清空canvas
  6. 繪製

webGL繪圖上下文

webGL是canvas基礎之上的3D繪圖技術,只是上下文不同,get3DContext函式作用就是依次降級獲取上下文。

    var canvas=document.getElementById('canvas'),
        gl=get3DContext(canvas,true);
    function get3DContext(canvas, opt) {
      var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
      var context = null;
      for (var i = 0, len=names.length; i < len; i++) {
        try {
          context = canvas.getContext(names[i], opt);
        } catch(e) {}
        if (context) {
          break;
        }
      }
      return context;
    }

著色器

  著色器就是嵌入到js中的webGL程式碼,是由GLSL語言編寫的,可以把著色器看成是js程式碼連線webGL的中介軟體。頂點著色器和片元著色器分別用於操作頂點和顏色光照,《WebGL程式設計指南》中是把著色器寫成字串,但從可維護性考慮,還是寫在script標籤中比較好。GLSL語言與C語言非常像,只要熟悉了GLSL特有的部分,其實還是比較簡單的。

限定符
限定符只能用於全域性變數,有3種型別:attribute,uniform,varying,目前只用到前兩種
attribute用於表示頂點資訊
uniform用於表示除頂點外的其他資訊,可以是除結構體和陣列之外的任意型別
varying用於頂點著色器向片元著色器傳輸資料

GLSL特有的資料型別
向量:
vec2, vec3, vec4 //表示有2,3,4個浮點數的向量
ivec2, ivec3, ivec4 //表示有2,3,4個整形的向量
bvec2, bvec3, bvec4 //表示有2,3,4個布林值的向量
矩陣:
mat2, mat3, mat4 //表示有2x2,3x3,4x4的浮點數的矩陣

頂點著色器

    <script type="x-shader/x-vertex" id="vs">
      attribute vec4 a_Position; //頂點,4個浮點的向量,attribute變數傳輸與頂點有關的資料,表示逐頂點的資訊
      uniform mat4 u_xformMatrix; //變換矩陣,4*4浮點矩陣, uniform變數傳輸的是所有頂點都相同的資料
      void main() { 
        gl_Position=u_xformMatrix*a_Position;
      } 
    </script>

片元著色器

    <script type="x-shader/x-fragment" id="fs">
      precision mediump float; // 精度限定
      uniform vec4 u_FragColor;  // 顏色
      void main() {
        gl_FragColor = u_FragColor;
      }
    </script>

  接著就是建立著色器了,首先從頁面script標籤取出著色器程式碼,初始化著色器;接著建立程式物件,最後連線程式物件。中間的步驟其實非常的囉嗦,已經把這幾個步驟封裝,我們只需要呼叫createShaders就可以了。

    /**
     * 根據script id建立著色器
     * @param  {Object} gl  context
     * @param  {String} vid script id
     * @param  {String} fid script id
     * @return {Boolen} 
     */
    function createShaders(gl,vid,fid){
        var vshader,fshader,element,program;

        [vid,fid].forEach(function(id){
            element= document.getElementById(id);
            if(element){
                switch(element.type){  
                    // 頂點著色器的時候  
                    case 'x-shader/x-vertex': vshader = element.text; break;
                    // 片段著色器的時候  
                    case 'x-shader/x-fragment': fshader = element.text; break;
                    default : break;
                }
            }
        });
        if(!vshader){
            console.log('VERTEX_SHADER String not exist');
            return false;
        }
        if(!fshader){
            console.log('FRAGMENT_SHADER String not exist');
            return false;
        }
        program = createProgram(gl, vshader, fshader);
        if (!program) {
            console.log('Failed to create program');
            return false;
        }

        gl.useProgram(program);
        gl.program = program;
        return true;
    }

    /**
     * 建立連線程式物件
     * @param  {Object} gl       上下文
     * @param  {String} vshader  頂點著色器程式碼
     * @param  {String} fshader  片元著色器程式碼
     * @return {Object}         
     */
    function createProgram(gl, vshader, fshader) {
      // 建立著色器物件
      var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
      var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
      if (!vertexShader || !fragmentShader) {
        return null;
      }

      // 建立程式物件
      var program = gl.createProgram();
      if (!program) {
        return null;
      }

      // 連線著色器物件
      gl.attachShader(program, vertexShader);
      gl.attachShader(program, fragmentShader);

      // 連線程式物件
      gl.linkProgram(program);

      // 檢查連線結果
      var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
      if (!linked) {
        var error = gl.getProgramInfoLog(program);
        console.log('Failed to link program: ' + error);
        gl.deleteProgram(program);
        gl.deleteShader(fragmentShader);
        gl.deleteShader(vertexShader);
        return null;
      }
      return program;
    }

    /**
     * 載入著色器
     * @param  {Object} gl     上下文
     * @param  {Object} type   型別
     * @param  {String} source 程式碼字串
     * @return {Object}       
     */
    function loadShader(gl, type, source) {
      // 建立著色器物件
      var shader = gl.createShader(type);
      if (shader == null) {
        console.log('unable to create shader');
        return null;
      }

      // 設定著色器程式
      gl.shaderSource(shader, source);

      // 編譯著色器
      gl.compileShader(shader);

      // 檢查編譯結果
      var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
      if (!compiled) {
        var error = gl.getShaderInfoLog(shader);
        console.log('Failed to compile shader: ' + error);
        gl.deleteShader(shader);
        return null;
      }

      return shader;
    }

緩衝區

  建立好緩衝區物件後,需要把它分配給變數,然後使它生效。注意頂點陣列使用的是型別化陣列Float32Array,這樣更加高效。vertexAttribPointer方法這裡指定了每個頂點分量的個數為2,因為我們目前只定義x,y座標,z座標使用系統預設。

    /**
     * 建立緩衝區
     * @param  {Array} data
     * @param  {Object} bufferType
     * @return {Object}     
     */
    function createBuffer(data,bufferType){  
      // 生成快取物件  
      var buffer = gl.createBuffer();  
      if (!buffer) {
        console.log('Failed to create the buffer object');
        return null;
      }
      // 繫結快取(gl.ARRAY_BUFFER<頂點>||gl.ELEMENT_ARRAY_BUFFER<頂點索引>) 
      gl.bindBuffer(bufferType||gl.ARRAY_BUFFER, buffer);  
        
      // 向快取中寫入資料  
      gl.bufferData(bufferType||gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);  
        
      // 將繫結的快取設為無效  
      // gl.bindBuffer(gl.ARRAY_BUFFER, null);  
        
      // 返回生成的buffer  
      return buffer;
    } 

    // 建立緩衝區並傳人頂點
    var vertices=new Float32Array([-0.5, 0.5,   -0.5, -0.5,   0.5, 0.5, 0.5, -0.5 ])
    if(!createBuffer(vertices)){
        return;
    }

    // 分配緩衝區物件給a_Position變數
    // (地址,每個頂點分量的個數<1-4>,資料型別<整形,符點等>,是否歸一化,指定相鄰兩個頂點間位元組數<預設0>,指定緩衝區物件偏移量<預設0>)
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

    // 啟動
    gl.enableVertexAttribArray(a_Position);

寫入資料

  首先要獲取變數的地址,然後再給變數賦值,感覺挺麻煩的。attribute標記的變數使用getAttribLocation獲取,同理uniform標記的變數使用getUniformLocation獲取。
  我們的動畫要使圖形繞座標原點旋轉,那麼這就需要用到矩陣的變換,矩陣相關的知識就不詳細說明了。要注意webGL使用的是列主序的矩陣,計算好變換矩陣後,把值賦予變數就ok。

    // 獲取 u_FragColor變數的儲存地址並賦值
    var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
    if (!u_FragColor) {
        return;
    }
    //顏色模式為rgba,值範圍0~1
    gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);

    // 繞z軸旋轉
    var deg=Math.PI/180*(angle++),
        cos=Math.cos(deg),
        sin=Math.sin(deg);

    //  webgl中是按列主序 旋轉加位移
    var xformMatrix=new Float32Array([
        cos,sin,0.0,0.0,
        -sin,cos,0.0,0.0,
        0.0,0.0,1.0,0.0,
        0.3,0.0,0.0,1.0
    ]);

    // v表示可以向著色器傳輸多個數值(地址變數,webgl中必須false,矩陣)
    gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);

背景操作

每次執行動畫前進行清屏,和canvas中的設定fillStyle,執行clearRect,效果一樣。

    // 設定清屏顏色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 清屏
    gl.clear(gl.COLOR_BUFFER_BIT);

繪製

最後渲染圖形,注意第一個引數,指定不同的值,它就渲染為不同的圖形,大家可以用不同的值試試效果。
POINTS //點
LINES //線段
LINE_STRIP //線條
LINE_LOOP //迴路
TRIANGLES //三角形
TRIANGLE_STRIP //三角帶
TRIANGLE_FAN //三角扇

    // (基本圖形,第幾個頂點,執行幾次),修改基本圖形項可以生成點,線,三角形,矩形,扇形等
    gl.drawArrays(gl.TRIANGLES, 0, 3);

最後主體程式碼如下:

    var canvas=document.getElementById('canvas'),
        gl=get3DContext(canvas,true);

    function main() {
        if (!gl) {
          console.log('Failed to get the rendering context for WebGL');
          return;
        }

        if (!createShaders(gl, 'fs', 'vs')) {
          console.log('Failed to intialize shaders.');
          return;
        }

        // 建立緩衝區並傳人頂點
        var vertices=new Float32Array([-0.5, 0.5,   -0.5, -0.5,   0.5, 0.5, 0.5, -0.5 ])
        if(!createBuffer(vertices)){
          console.log('Failed to create the buffer object');
          return;
        }

        // 獲取頂點位置
        var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
        if (a_Position < 0) {
          console.log('Failed to get the storage location of a_Position');
          return;
        }

        // 分配緩衝區物件給a_Position變數
        gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(a_Position);


        // 獲取 u_FragColor變數的儲存地址並賦值
        var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
        if (!u_FragColor) {
          console.log('Failed to get the storage location of u_FragColor');
          return;
        }
        gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);


        // 獲取矩陣變數
        var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
        if (!u_xformMatrix) {
          console.log('Failed to get the storage location of u_xformMatrix');
          return;
        }

        var xformMatrix,angle=0;
        // 設定清屏顏色
        gl.clearColor(0.0, 0.0, 0.0, 1.0);

        // 執行動畫
        (function animate(){
          var deg=Math.PI/180*(angle++),
              cos=Math.cos(deg),
              sin=Math.sin(deg);

            // 旋轉加位移
            xformMatrix=new Float32Array([
              cos,sin,0.0,0.0,
              -sin,cos,0.0,0.0,
              0.0,0.0,1.0,0.0,
              0.3,0.0,0.0,1.0
            ]);

            // v表示可以向著色器傳輸多個數值(地址變數,webgl中必須false,矩陣)
            gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);

            gl.clear(gl.COLOR_BUFFER_BIT);

            // (基本圖形,第幾個頂點,執行幾次),修改基本圖形項可以生成點,線,三角形,矩形,扇形等
            gl.drawArrays(gl.TRIANGLES, 0, 3);

            requestAnimationFrame(animate);
        }());
    }

    main();

總結

  相比canvas,webGL的api要原始得多,涉及到很多底層的openGL細節,但經過封裝後,我們可以把那部分細節看成一個黑箱。大部分的操作都是基於矩陣變換,儘管有很多方便的第三方矩陣庫,但有牢固的線性代數基礎還是大有裨益的。GLSL程式語言也是一樣需要熟練掌握。

相關文章