WebGL-學習筆記(二)

WebGL學習筆記(二).png

構成三維模型的基本圖形是三角形,所以接下來就從如何繪制一個三角形開始,之后涉及到圖形的變換和動畫。

1. 圖形繪制

先回顧以下繪制單個點的方式:通過gl.getAttribLocati on()獲得了GLSL中的Vertex著色器的屬性值,并利用gl.vertexArrib[1234]f()方法簇給著色器屬性賦值,并將值傳遞給GLSL的內置變量gl_Position,之后調用gl.drawArrays(gl.POINT, 0, 1)的方法繪制點。
如果要繪制多個點怎么辦?當然,我們可以設置多次調用gl.vertexAttrib[1234]fgl.drawArrays(gl.POINT, 0, 1)的方式來實現繪制多個點,例如:

html:
  <canvas id="glCanvas" width="640" height="480"></canvas>
Javascript:
const canvas = document.querySelector('#glCanvas');
const gl = canvas.getContext('webgl');
// 著色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    void main() {
        gl_Position = a_Position;
        gl_PointSize = a_PointSize;
    }
    `;
const FSHADER_SOURCE = `
    precision mediump float;
    uniform vec4 u_FragColor;
    void main() {
        gl_FragColor = u_FragColor;
    }
    `
let program = init(gl, VSHADER_SOURCE, FSHADER_SOURCE);
// 獲取頂點位置的屬性
let a_Position = gl.getAttribLocation(program, 'a_Position');
if (a_Position < 0) {
    console.log('Cant find the position');
    return;
}
// 設置頂點點的尺寸
let a_PointSize = gl.getAttribLocation(program, 'a_PointSize');
    if (a_PointSize < 0) {
        console.log('Cant find the pointsize');
        return;
    }
gl.vertexAttrib1f(a_PointSize, 10.0);
// 設置頂點顏色
let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 設置并繪制多個頂點
drawPoint();
// 繪制多個點的方法
function drawPoint(gl, a_Position) {
    gl.vertexAttrib3f(a_Position, -0.5, -0.5, 0.0);
    gl.drawArrays(gl.POINT, 0, 1);
    gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0);
    gl.drawArrays(gl.POINT, 0, 1);
    gl.vertexAttrib3f(a_Position, 0.5, -0.5, 0.0);
    gl.drawArrays(gl.POINT, 0, 1);
}

這樣反復調用繪制的方案明顯效率低下(反復重繪整個畫布),另外如果我們想繪制其他圖形,建立點之間點連接關系,似乎就沒有辦法了(因為每次繪制的點都是獨立的)。為了解決著兩個問題,就需要用到WebGL提供的緩沖區來一次性存儲多種信息(不僅僅存儲頂點坐標,還可以一次性存入頂點顏色等信息,后面再討論)

1.1 利用緩沖區繪制多個點

WebGL中要使用緩沖區,主要有以下五個步驟:

  1. 利用gl.createBuffer()創建緩沖區對象
  2. 利用gl.bindBuffer()將創建的緩沖區對象和WebGL中的內置對象gl.ARRAY_BUFFER進行綁定
  3. 利用gl.bufferData()gl.ARRAY_BUFFER內置對象傳遞數據(數據不能直接傳遞給緩沖區對象,要通過gl.ARRAY_BUFFER來進行)
  4. 利用gl.vertexAttribPointer()將緩沖區數據分配給GLSL中的變量
  5. 最后利用gl.enableVertexAttribArray()開啟緩沖區對變量的使用,開啟后gl.vertexAttrib[1234]f()方法簇的賦值將失效

完成上述操作后,調用gl.drawArrays()來就可以一次繪制緩沖區數據了,這時只要將第三個參數變為所需要繪制點的數目就可以了。根據前一個例子,這里將drawPoint()進行調整為使用緩沖區

function initPoint(gl, a_Position) {
    let pointData = new Float32Array([
        -0.5, -0.5,
        0, 0.5,
        0.5, -0.5
    ]);
    // 創建緩沖區對象
    let buffer = gl.createBuffer();
    // 綁定緩沖區對象
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    // 將數據傳遞到緩沖區
    gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
    // 將緩沖區數據傳遞給頂點著色器attribute屬性
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
    // 激活頂點和緩沖區到連接
    gl.enableVertexAttribArray(a_Position);
    // 一次性繪制多個點
    gl.drawArrays(gl.POINT, 0, 3);
}

PS:代碼中使用了Float32Array來創建點的數據對象,它是Javascript提供的一種類型化數組,目的是為了說明數組中的所有數據都是同一種數據類型的特殊數組,使處理數組效率更快(可能不用做類型判斷和轉化了),其中類型化數組提供了很多方法和屬性,尤其BYTES_PER_ELEMENT屬性在之后會有很大用處的。

1.2 利用mode控制圖形繪制

在使用緩沖區的基礎上,繪制圖形就很簡單了,只需要改變gl.drawArrays()中的第一個參數就可以了
WebGL的第一個參數mode,提供了7種值:

  • gl.POINT: 繪制點
  • gl.LINES: 繪制線段
  • gl.LINE_STRIP:繪制連續線段,例如傳入[A0, A1, A2, A3]四個坐標信息,那么繪制結果為[A0, A1], [A1, A2], [A2, A3]
  • gl.LINE_LOOP:首位兩個點會連接起來
  • gl.TRIANGLES:繪制三角形
  • gl.TRIANGLE_STRIP:繪制一系列三角形,例如傳入[A0, A1, A2, A3, A4, A5]五個坐標信息,那么繪制結果為[A0, A1, A2], [A2, A1, A3], [A2, A3, A4], [A4, A3, A5](webGL中繪制是按照逆時針方式進行繪制的)
  • gl.TRIANGLE_FAN:以第一個點為所有三角形頂點,繪制三角扇

2. 圖形變換

我們經常會將繪制的圖形進行平移,旋轉,縮放的操作,這些操作統一被稱為仿射變化。

2.1 基本變換

2.1.1 平移

平移要實現的其實是方向坐標上的位移:

x' = x + ux;
y' = y + uy;

因為GLSL中矢量可以直接進行加減運算,所以修改頂點著色器程序:

// 著色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    void main() {
        // 矢量可以直接進行運算
        gl_Position = a_Position + vec4(0.1, 0.1, 0.0, 0.0); 
        gl_PointSize = a_PointSize;
    }
`;

如果想要自定義平移距離,那么可以在頂點著色器程序中新增一個uniform變量(使用uniform是因為變量本身與頂點無關)

// 著色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    unifrom vec4 u_Translate;
    void main() {
        // 矢量可以直接進行運算
        gl_Position = a_Position + u_Translate; 
        gl_PointSize = a_PointSize;
    }
`;
// 設置平移距離
let u_Translate = gl.getUniformLocation(program, 'u_Translate');
gl.uniform4f(u_Translate, 0.1, 0.1, 0.0, 0.0);

PS:要注意因為是使用齊次坐標,所以最后一個變量值要傳遞為0.0(因為默認齊次坐標的第四個變量為1.0,矢量計算后最后一個變量仍然應該為1.0)

2.1.2 縮放

縮放要實現的是方向坐標上的比例變化

x' = ux;
y' = uy;

實現縮放的關鍵是要獲取坐標在某些方向上的分量,可以通過a_Position.xa_Position.y來分別獲取xy方向上的分量,修改著色器程序:

// 著色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    void main() {
        // 分別設置四個方向上的分量信息
        gl_Position.x = a_Position.x * 0.5; 
        gl_Position.y = a_Position.y * 0.5; 
        gl_Position.z = a_Position.z;
        gl_Position.w = 1.0;
        gl_PointSize = a_PointSize;
    }
`;
2.1.3 旋轉

旋轉相比起來就復雜的多了,旋轉必須指明三個要素,旋轉軸,旋轉方向和旋轉角度。
WebGL中旋轉正方向是逆時針方向,遵循右手旋轉法則,也就是大拇指朝向軸的正方向,四指方向就是旋轉正方向。
以僅繞z軸旋轉為例,那么如果坐標系中的點的原角度為a(與x軸正方向角度),轉動角度為b,我們可以利用三角函數得到

x = r*cos(a);
y = r*sin(a);
// 利用三角函數和角公式進行變化
x' = r*cos(a+b) = r*cos(a)*cos(b) - r*sin(a)*sin(b) = x*cos(b) - y* sin(b);
y' = r*sin(a+b) = r*cos(a)*sin(b) + r*sin(a)*cos(b) = x*sin(b) + y*cos(b)

于是同樣通過獲取分量的方式,將頂點著色器程序進行修改

// 著色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    uniform float u_Cosb, u_Sinb;
    void main() {
        // 分別設置四個方向上的分量信息
        gl_Position.x = a_Position.x * u_Cosb - a_Position.y * u_Sinb
        gl_Position.y = a_Position.x * * u_Sinb + a_Position.y * u_Cosb; 
        gl_Position.z = a_Position.z;
        gl_Position.w = 1.0;
        gl_PointSize = a_PointSize;
    }
`;
// 轉動30度
let rad = 90 / 180 * Math.PI;
let u_Sinb = gl.getUniformLocation(program, 'u_Sinb');
gl.uniform1f(u_Sinb, Math.sin(rad));
let u_Cosb = gl.getUniformLocation(program, 'u_Cosb');
gl.uniform1f(u_Cosb, Math.cos(rad));

2.2 矩陣變換

所有的變換其實都是平移,旋轉,縮放的疊加,但是按照之前的方式來進行那么在變換疊加的時候,計算過程就變得麻煩了,例如,我們要先旋轉后平移再縮放。。。由于WebGL支持矩陣運算,所以變換可以使用矩陣變化來處理。
具體矩陣運算可以參見矩陣(其實也就是將方程轉換為了矩陣的方式)
在WebGL中,要使用變換矩陣主要要進行四步驟:

  1. 頂點著色器程序中增加矩陣變量uniform mat4 u_Matrix
  2. 頂點著色器程序中修改gl_Position = u_Matrix * a_Position(注意矩陣計算時的順序)
  3. 創建矩陣變換的數組new Float32Array()
  4. 使用gl.unifromMatrix[1234]fv的方法設置矩陣變換uniform變量的值

以旋轉變化為例,那么:

  // 著色器程序
  const VSHADER_SOURCE = `
      attribute vec4 a_Position;
      attribute float a_PointSize;
      uniform mat4 u_Matrix;
      void main() {
          gl_Position = u_Matrix * a_Position;
          gl_PointSize = a_PointSize;
      }
  `;
let rad = 90 / 180 * Math.PI;
let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
let matrix = new Float32Array([
    Math.cos(rad), Math.sin(rad), 0.0, 0.0,
    -Math.sin(rad), Math.cos(rad), 0.0, 0.0, 
    0.0, 0.0, 1.0, 0.0,
    0.0, 0.0, 0.0, 1.0
])
gl.uniformMatrix4fv(u_Matrix, false, matrix);

PS:特別注意,數組中存儲二維數組有兩種順序,按列主序和按行主序,WebGL和OpenGL中都是按列主序,所以,注意數組中的數據和真是矩陣中數據位置存在轉置(gl.uniformMatrix4fv的第二個參數可以實現矩陣轉置,但是WebGL中并沒有實現轉置操作所以始終默認為false

3. 動畫

3.1 利用requestAnimationFrame進行動畫

動畫基本上就是基于圖形的各種變換的持續執行過程,于是在webGL中要實現動畫,實際上就是在圖形變換的基礎上,調用了動畫函數進行循環調用,不斷重新繪制圖形。可以使用setInterval函數也可以使用requestAnimationFrame來實現動畫的循環調用,不過建議使用后者,因為后者只有在瀏覽器tab頁激活的時候才會執行,而前者會一直執行。還是以旋轉為例,下面是持續旋轉的例子:

    // 著色器程序
    const VSHADER_SOURCE = `
        attribute vec4 a_Position;
        attribute float a_PointSize;
        uniform mat4 u_Matrix;
        void main() {
            gl_Position = u_Matrix * a_Position;
            gl_PointSize = a_PointSize;
        }
    `;
    let start = Date.now();
    let rad = 0;
    animation();
    // 旋轉動畫
    function animation() {
        let now = Date.now();
        let offsetTime = now - start;
        start = now;
        // 假設每秒鐘轉動30度
        rad += offsetTime * 30 / 360 / 1000 * Math.PI;
        let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
        let matrix = new Float32Array([
            Math.cos(rad), Math.sin(rad), 0.0, 0.0,
            -Math.sin(rad), Math.cos(rad), 0.0, 0.0, 
            0.0, 0.0, 1.0, 0.0,
            0.0, 0.0, 0.0, 1.0
        ])
        gl.uniformMatrix4fv(u_Matrix, false, matrix);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.LINE_LOOP, 0, 3);
        requestAnimationFrame(animation);
    }
}
  1. 總結
    一開始我總覺得自己在旋轉的過程中圖形形狀發生了變化,很奇怪,最后花了很多事件才發現原來demo中的canvas不是正方形。。。所以在進行旋轉操作的時候,x軸和y軸并不是相同的單位比例。
  2. 參考
    《WebGL編程指南》
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容