1. 程式人生 > >WebGL簡易教程(四):顏色

WebGL簡易教程(四):顏色

目錄

  • 1. 概述
  • 2. 示例:繪製三角形
    • 1) 資料的組織
    • 2) varying變數
  • 3. 結果
  • 4. 理解
    • 1) 圖形裝配和光柵化
    • 2) 內插過程
  • 5. 參考

1. 概述

在上一篇教程《WebGL簡易教程(三):繪製一個三角形(緩衝區物件)》中,通過使用緩衝區物件(buffer object)來向頂點著色器傳送資料。那麼,如果這些資料(與頂點相關的資料,如法向量、顏色等)需要繼續傳送到片元著色器該怎麼辦呢?

例如這裡給三角形的每個頂點賦予不同的顏色,繪製一個彩色的三角形。這個時候就需要用到之前(《WebGL簡易教程(二):向著色器傳輸資料》)介紹過的varying變量了。

2. 示例:繪製三角形

改進上一篇中繪製三角形(HelloTriangle.js)的程式碼:

// 頂點著色器程式
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' + // attribute variable
  'attribute vec4 a_Color;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' + // Set the vertex coordinates of the point
  '  v_Color = a_Color;\n' +
  '}\n';

// 片元著色器程式
var FSHADER_SOURCE = 
  'precision mediump float;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

function main() {
  // 獲取 <canvas> 元素
  var canvas = document.getElementById('webgl');

  // 獲取WebGL渲染上下文
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }

  // 初始化著色器
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }

  // 設定頂點位置
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the positions of the vertices');
    return;
  }

  // 指定清空<canvas>的顏色
  gl.clearColor(0.0, 0.0, 0.0, 1.0);

  // 清空<canvas>
  gl.clear(gl.COLOR_BUFFER_BIT);

  // 繪製三角形
  gl.drawArrays(gl.TRIANGLES, 0, n);
}

function initVertexBuffers(gl) {
  // 頂點座標和顏色
  var verticesColors = new Float32Array([    
     0.0,  0.5,  1.0,  0.0,  0.0, 
    -0.5, -0.5,  0.0,  1.0,  0.0, 
     0.5, -0.5,  0.0,  0.0,  1.0, 
  ]);

  //
  var n = 3; // 點的個數
  var FSIZE = verticesColors.BYTES_PER_ELEMENT;   //陣列中每個元素的位元組數

  // 建立緩衝區物件
  var vertexBuffer = gl.createBuffer();
  if (!vertexBuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

  // 將緩衝區物件繫結到目標
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  // 向緩衝區物件寫入資料
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  //獲取著色器中attribute變數a_Position的地址 
  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 -1;
  }
  // 將緩衝區物件分配給a_Position變數
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 5*FSIZE, 0);

  // 連線a_Position變數與分配給它的緩衝區物件
  gl.enableVertexAttribArray(a_Position);

  //獲取著色器中attribute變數a_Color的地址 
  var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
  if(a_Color < 0) {
    console.log('Failed to get the storage location of a_Color');
    return -1;
  }
  // 將緩衝區物件分配給a_Color變數
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
  // 連線a_Color變數與分配給它的緩衝區物件
  gl.enableVertexAttribArray(a_Color);  

  // 解除繫結
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  return n;
}

1) 資料的組織

與之前的例子相似,資料仍然通過緩衝區傳遞到頂點著色器。在頂點著色器中,定義了兩個attribute變數,分別代表位置和顏色資訊:

// 頂點著色器程式
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' + // attribute variable
  'attribute vec4 a_Color;\n' +
…
  '}\n';

這意味著需要向頂點著色器傳遞兩次資料。這裡採取的做法仍然是一次性向緩衝區寫入位置和顏色等所有的資料,然後分批次傳入頂點著色器:

  // 建立緩衝區物件
  var vertexBuffer = gl.createBuffer();
  if (!vertexBuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

// 將緩衝區物件繫結到目標
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  // 向緩衝區物件寫入資料
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  //獲取著色器中attribute變數a_Position的地址 
  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 -1;
  }
  // 將緩衝區物件分配給a_Position變數
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 5*FSIZE, 0);

  // 連線a_Position變數與分配給它的緩衝區物件
  gl.enableVertexAttribArray(a_Position);

  //獲取著色器中attribute變數a_Color的地址 
  var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
  if(a_Color < 0) {
    console.log('Failed to get the storage location of a_Color');
    return -1;
  }
  // 將緩衝區物件分配給a_Color變數
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
  // 連線a_Color變數與分配給它的緩衝區物件
  gl.enableVertexAttribArray(a_Color);  

可以看到建立緩衝區物件、將緩衝區物件繫結到目標、向緩衝區物件寫入資料這三個步驟都是一致的。但分配attribute變數和連線attribute變數這兩個步驟分別進行了兩次。其中的關鍵點就在於gl.vertexAttribPointer()這個函式。之前使用這個函式都是使用的預設值,這裡通過設定步進和偏移值,分別訪問了緩衝區中不同的資料。

通過gl.vertexAttribPointer()函式定義可以知道,傳送到緩衝區的資料是2(size)的位置資料和3(size)的顏色資料,所以步進引數stride都是5(size)。第一次傳送位置資料的時候是從初始位置開始的,所以offset是0;而第二次傳送顏色資料的時候需要偏移第一個位置資料,所以offfset是2(size)。

2) varying變數

在之前的教程(《WebGL簡易教程(二):向著色器傳輸資料》)中提到,可以傳送資料給片元著色器,來給繪製場景賦予顏色。但是這裡卻通過緩衝區把資料傳遞給了頂點著色器。因此,在這裡給頂點著色器和片元著色器,分別定義了一個相同的varying變數:

// 頂點著色器程式
var VSHADER_SOURCE =
  …
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  …
  '  v_Color = a_Color;\n' +
  '}\n';

// 片元著色器程式
var FSHADER_SOURCE = 
  …
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

varying變量表達的正是一種可變的變數,它的作用就是從頂點著色器向片元著色器傳輸資料。在頂點著色器的main函式中,將從緩衝區物件中獲取的attribute變數a_Color賦值給預先定義的varying變數v_Color;同時在片元著色器中又定義了一個同類型同名的varying變數v_Color,那麼頂點著色器中該變數的值就會自動傳入到片元著色器中。最後在片元著色器的main函式中將該值傳入到gl_FragColor中,就得到最終的結果了。其示意圖如下:

3. 結果

最後的執行結果如下,最後會發現得到了一個顏色平滑過渡的,三個角各是紅、綠、藍顏色的三角形:

4. 理解

1) 圖形裝配和光柵化

更進一步思考下,這裡雖然給每個頂點賦予的顏色值,但是為什麼三角形的表面都賦予了顏色,並且是平滑過渡的效果呢?其實這裡省略了頂點著色器與片元著色器之間資料傳輸細節——圖形裝配和光柵化。

點組成線,線組成面,將孤立的點組成基本圖形(圖元)的過程就是圖形裝配。圖形裝配的輸入資料就是頂點著色器中gl_Position得到的值,由gl.drawArrays()中第一個引數值來確定裝配成什麼樣的圖元。在這個例子中,頂點著色器告訴WebGL系統,準備了三個點,WebGL通過影象裝配,將其裝配成三角形。

知道裝配的圖形還是不夠的,理論上的三角形是連續不斷的圖形,而一般的圖形顯示裝置都是離散的片元(畫素)。影象轉換成片元,就是光柵化的過程。

圖形裝配和光柵化的示意圖如下:

2) 內插過程

在第二節詳解示例中的程式碼時,提到了頂點著色器和片元著色都定義了相同的varying變數v_Color,資料就會從頂點著色器傳入到片元著色器。但其實兩者雖然同名,但並不是一回事。在頂點著色器中,這個varying變數是與頂點相關的值,而經過圖形裝配和光柵化後,片元著色器的varying變數就是與片元相關的值了。並且,這個值是經過內插過程得到的。

在這個例子中,給三個頂點賦予了三個不同的顏色值。WebGL就根據三個頂點的顏色值內插了三角形中每個片元(畫素)的顏色值,並傳遞給片元著色器。所謂內插過程,可以想象成一條漸變色帶,知道確定了起止顏色,就能獲取中間任意位置的顏色。

5. 參考

本來部分程式碼和插圖來自《WebGL程式設計指南》,原始碼連結:https://share.weiyun.com/5VjlUKo ,密碼:sw0x2x。會在此共享目錄中持續更新後續的內