WebGL 3D 入門系列:繪製漸變三角形 --- 深入理解緩衝區

lucefer發表於2019-03-19

WebGL 3D 入門系列 --- 繪製漸變三角形:深入理解緩衝區

本節內容來自於小冊 WebGL 入門與實踐

上節帶領大家學習了基本三角形圖元的繪製過程,以及如何使用緩衝區向著色器傳遞多個資料,但上節只演示了往著色器傳遞座標這一種資料,本節通過繪製漸變三角形,講解一下如何通過緩衝區向著色器傳遞多種資料。

目標

本節通過一個滑鼠每點選三次便會繪製一個漸變三角形的示例,帶大家深入理解緩衝區的用法,最終效果如下圖所示:

WebGL 3D 入門系列:繪製漸變三角形 --- 深入理解緩衝區

通過本節學習,你將會掌握如下內容:

  • 頂點資料在 buffer 中的排布方式。
  • 切換 buffer 時,bindBuffer 的重要性。
  • 使用多個 buffer 讀取多種頂點資料。
  • 使用單個 buffer 讀取多種頂點資料。
  • 如何實現漸變效果。

漸變三角形

上節我們實現的是單色三角形,通過在片元著色器中定義一個 uniform 變數,接收 JavaScript 傳遞過去的顏色值來實現。那漸變三角形的處理與單色三角形有何不同呢?

漸變三角形顏色不單一,在頂點與頂點之間進行顏色的漸變過渡,這就要求我們的頂點資訊除了包含座標,還要包含顏色。這樣在頂點著色器之後,GPU 根據每個頂點的顏色對頂點與頂點之間的顏色進行插值,自動填補頂點之間畫素的顏色,於是形成了漸變三角形。

那既然我們需要為每個頂點傳遞座標資訊和顏色資訊,因此需要在頂點著色器中額外增加一個 attribute 變數a_Color,用來接收頂點的顏色,同時還需要在頂點著色器和片元著色器中定義一個 varying 型別的變數v_Color,用來傳遞頂點顏色資訊。

著色器

  • 依然從頂點著色器開始,頂點著色器新增一個 attribute 變數,用來接收頂點顏色。
    //設定浮點數精度為中等精度。
    precision mediump float;
    //接收頂點座標 (x, y)
    attribute vec2 a_Position;
    //接收瀏覽器視窗尺寸(width, height)
    attribute vec2 a_Screen_Size;
    //接收 JavaScript 傳遞的頂點顏色
    attribute vec4 a_Color;
    //傳往片元著色器的顏色。
    varying vec4 v_Color;
    void main(){
      vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
      position = position * vec2(1.0,-1.0);
      gl_Position = vec4(position, 0, 1);
      v_Color = a_Color;
    }

複製程式碼
  • 片元著色器

片元著色器新增一個 varying 變數 v_Color,用來接收插值後的顏色。

    //設定浮點數精度為中等。
    precision mediump float;
    //接收 JavaScript 傳過來的顏色值(rgba)。
    varying vec4 v_Color;
    void main(){
      vec4 color = v_Color / vec4(255, 255, 255, 1);
      gl_FragColor = color;
   }
複製程式碼

我們的著色器部分還是和之前一樣簡單,只是在頂點著色器中增加了頂點顏色這一變數。

接下來我們用 JavaScript 向著色器傳遞資料。

JavaScript 部分

用緩衝區向著色器傳遞資料有兩種方式:

  • 利用一個緩衝區傳遞多種資料。
  • 另一種是利用多個緩衝區傳遞多個資料。

上節繪製三角形的時候我們給頂點著色器傳遞的只是座標資訊,並且只用了一個 buffer,本節示例,我們除了傳遞頂點的座標資料,還要傳遞頂點顏色。 按照正常思路,我們可以建立兩個 buffer,其中一個 buffer 傳遞座標,另外一個 buffer 傳遞顏色。

WebGL 3D 入門系列:繪製漸變三角形 --- 深入理解緩衝區

建立兩個 buffer,將 a_PositionpositionBuffer 繫結,a_ColorcolorBuffer 繫結,然後設定各自讀取 buffer 的方式。

請謹記:程式中如果有多個 buffer 的時候,在切換 buffer 進行操作時,一定要通過呼叫 gl.bindBuffer 將要操作的 buffer 繫結到 gl.ARRAY_BUFFER 上,這樣才能正確地操作 buffer 。您可以將 bindBuffer 理解為一個狀態機,bindBuffer 之後的對 buffer 的一些操作,都是基於最近一次繫結的 buffer 來進行的。

以下 buffer 的操作需要在繫結 buffer 之後進行:

  • gl.bufferData:傳遞資料。
  • gl.vertexAttribPointer:設定屬性讀取 buffer 的方式。
方式一:多個 buffer 傳遞

我們使用一個 buffer 傳遞座標資訊,另一個 buffer 傳遞顏色資訊。

// 建立 座標資訊 buffer
var positionBuffer = gl.createBuffer();
// 將當前 buffer 設定為 postionBuffer,接下來對 buffer 的操作都是針對 positionBuffer 了。
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 設定 a_Position 變數讀取 positionBuffer 緩衝區的方式。
var size = 2;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.vertexAttribPointer(
      a_Position, size, type, normalize, stride, offset);
      
// 建立 顏色資訊 buffer
var colorBuffer = gl.createBuffer();
// 將當前 buffer 設定為 postionBuffer,接下來對 buffer 的操作都是針對 positionBuffer 了。
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
// 設定 a_Position 變數讀取 positionBuffer 緩衝區的方式。
var size = 4;
var type = gl.FLOAT;
var normalize = false;
var stride = 0;
var offset = 0;
gl.vertexAttribPointer(
      a_Color, size, type, normalize, stride, offset);

複製程式碼

gl.vertexAttribPointer( a_Color, size, type, normalize, stride, offset)。這個方法比較重要,上節已經向大家詳細介紹了,如果還不太明白的,可以再次回顧下上節內容。

我們發現,上面程式碼對 buffer 的操作有些冗餘,我們還是提取出一個方法 createBuffer 放到 webgl-helper.js,減少重複編碼,之後我們對 buffer 的一系列呼叫只需要如下兩句就可以了:

var positionBuffer = createBuffer(gl, a_Position, { size: 2});
var colorBuffer = createBuffer(gl, a_Color, { size: 4});

複製程式碼

假如我們頂點座標陣列中有四個頂點 8 個元素【30, 30, 30, 40, 40, 30, 20, 0】,頂點著色器中的 a_Position 屬性在讀取頂點座標資訊時,以 2 個元素為一組進行讀取:

WebGL 3D 入門系列:繪製漸變三角形 --- 深入理解緩衝區

又假如我們頂點顏色陣列中有兩個頂點 8 個元素 【244, 230, 100, 1, 125, 30, 206, 1】,那麼頂點著色器中的 a_Color 屬性在讀取頂點顏色資訊時,以 4 個元素(r, g, b, a)為一組進行讀取,如下圖所示。

WebGL 3D 入門系列:繪製漸變三角形 --- 深入理解緩衝區

以多少元素作為一個頂點資訊進行讀取的設定,是在呼叫 gl.vertexAttribPointer 時設定的 size 引數值。

言歸正傳,接下來我們為 canvas 新增點選事件:

canvas.addEventListener('click', e => {
    var x = e.pageX;
    var y = e.pageY;
    positions.push(x, y);
    //隨機一種顏色
    var color = randomColor();
    //將隨機顏色的 rgba 值新增到頂點的顏色陣列中。
    colors.push(color.r, color.g, color.b, color.a);
    //頂點的數量是 3 的整數倍時,執行繪製操作。
    if (positions.length % 6 == 0) {
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.DYNAMIC_DRAW);
        render(gl);
    }
})
複製程式碼

萬事俱備,只欠繪製:

    function render(gl) {
      //用設定的清空畫布顏色清空畫布。
      gl.clear(gl.COLOR_BUFFER_BIT);
      if (positions.length <= 0) {
        return;
      }
      //繪製圖元設定為三角形。
      var primitiveType = gl.TRIANGLES;
      //因為我們要繪製三個點,所以執行三次頂點繪製操作。
      gl.drawArrays(primitiveType, 0, positions.length / 2);
    }
複製程式碼

至此,三角形的漸變效果就實現啦。

WebGL 3D 入門系列:繪製漸變三角形 --- 深入理解緩衝區

另一種思路:使用 1 個 buffer 同時傳遞座標和顏色資訊

常規思路使用多個 buffer 傳遞多種資料(座標和顏色),我們再演示另外一種思路:使用 1 個 buffer 同時傳遞多種資料。

著色器部分的程式碼和上面的一樣,無需改動,改動的主要部分是 JavaScript 程式。

首先,我們依然是建立 buffer,只不過這次是建立一個 buffer

var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
複製程式碼

建立完 buffer,接下來設定讀取 buffer 的方式,我們有兩個屬性 a_Positiona_Color,由於我們只有一個 buffer,該 buffer 中既儲存座標資訊,又儲存顏色資訊,所以兩個屬性需要讀取同一個 buffer

WebGL 3D 入門系列:繪製漸變三角形 --- 深入理解緩衝區

我們可以看到,一個頂點資訊佔用 6 個元素,前兩個元素代表座標資訊,後四個元素代表顏色資訊,所以在下面設定屬性讀取 buffer 方式時,a_Colora_Position 的設定會有不同:

  • a_Position:座標資訊佔用 2 個元素,故 size 設定為 2。 座標資訊是從第一個元素開始讀取,偏移值為 0 ,所以 offset 設定為 0.

  • a_Color:由於 color 資訊佔用 4 個元素,所以 size 設定為 4 。 color 資訊是在座標資訊之後,偏移兩個元素所佔的位元組(2 * 4 = 8)。所以,offset 設定為 8。

  • stride:代表一個頂點資訊所佔用的位元組數,我們的示例,一個頂點佔用 6 個元素,每個元素佔用 4 位元組,所以,stride = 4 * 6 = 24 個位元組。

gl.vertexAttribPointer(
      a_Color, 4, gl.FLOAT, false, 24, 8);
      
gl.vertexAttribPointer(
      a_Position, 2, gl.FLOAT, false, 24, 0);

複製程式碼

canvas 的點選事件也有所不同,一個頂點佔用 6 個元素,三個頂點組成一個三角形,所以我們的 positions 的元素數量必須是 18 的整數倍,才能組成一個三角形:

    canvas.addEventListener('click', e => {
      var x = e.pageX;
      var y = e.pageY;
      positions.push(x);
      positions.push(y);
      //隨機出一種顏色
      var color = randomColor();
      //將隨機顏色的 rgba 值新增到頂點的顏色陣列中。
      positions.push(color.r, color.g, color.b, color.a);
      //頂點的數量是 18 的整數倍時,執行繪製操作。
      if (positions.length % 18 == 0) {
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
        render(gl);
      }
    })
複製程式碼

實現效果和上面操作多緩衝區的方式一樣,但是單緩衝區不僅減少了緩衝區的數量,而且減少了傳遞資料的次數以及複雜度。

回顧

至此,我們對緩衝區的講解就結束了,本節所講知識點和上節基本類似,不同點在於用單個緩衝區傳遞多類資料時,gl.vertexAttribPointer 各個引數如何設定,理解這點對我們以後程式設計十分有用,希望大家課下多多練習,深刻理解它的用法。

到目前為止,我們掌握了三角形的繪製方法,接下來學習怎樣用三角形構建其他圖形。

下一節我們將從簡單平面開始:先用三角形構建一個矩形。

小冊

這一系列的內容來自於小冊 WebGL 3D 入門與實踐,如果大家對進階知識感興趣,可以到小冊中去學習:小冊:WebGL 3D 入門與實踐

小冊內容除了包含 WebGL 相關的基礎練習,還包括 3D 圖形概念與相關數學的原理與推導,旨在幫助大家建立圖形學的技術輪廓。這部分圖形學知識獨立於 WebGL,除了可以適用於 WebGL,還適用於 OpenGL 等。

當然,本小冊主要目的還是幫助 Web 前端同學學習 3D 技術,除了介紹適用 WebGL 實現 3D 效果以外,還對 CSS3 中的 3D 技術相關屬性進行了深入剖析,並演示了與數學庫的結合使用,希望能夠讓前端同學不僅僅侷限於一般的二維平面開發,也能夠在 3D 開發上更近一步~

WebGL 3D 入門系列:繪製漸變三角形 --- 深入理解緩衝區

相關文章