WebGL 紋理顏色原理

騰訊雲加社群發表於2019-01-28

本文由雲+社群發表

作者:ivweb qcyhust

導語

WebGL繪製影象時,往著色器中傳入顏色資訊就可以給圖形繪製出相應的顏色,現在已經知道頂點著色器和片段著色器一起決定著向顏色緩衝區寫入顏色資訊並最終呈現出來,那麼這個過程是什麼樣,如果圖形的顏色需要用現有圖片來渲染那麼又該如何操作?

img

顏色緩衝區

在繪製開始前,經常見到呼叫函式清空畫布的程式碼gl.clear(gl.COLOR_BUFFER_BIT),清空畫布的繪圖區實際上就是用之前定義好的背景顏色將顏色緩衝的的顏色清除。顏色緩衝區中存放著需要顯示到畫布上的畫素的顏色資料,它屬於幀快取的一部分,與深度快取、模板快取等一起決定著最終畫布上影象的顯示資訊。

可以將顏色快取區看成影象顏色儲存器,在快取區中以RGB或RGBA的格式儲存著畫布上每一個畫素的顏色資訊,各個畫素點組合起來就構成了顏色快取的矩形陣列。這個定義看起來與圖片儲存器是很相似的,顏色快取為RGB或是RGBA每一個通道分配存放位數,其中RGB就是顏色資料,A表示alpha也就是該畫素的透明度資訊,顏色佔用的位數值就是顏色深度,比如顏色深度為24位,表示每一個畫素24位,一般24位的分配方案就是紅色、藍色、綠色各佔8位,如果需要透明效果的話,可以採用32位顏色深度為alpha通道分配8位。

這裡可以總結得出,畫布上各個畫素點呈現的顏色就是存放在顏色緩衝區的顏色資訊所決定的,而繪製圖形的顏色緩衝區的資訊又是由頂點著色器決定。要知道顏色如何渲染就要深入分析著色器的工作過程。

img

圖形裝配

要繪製一個三角形,我們是這樣定義著色器的:

// 頂點著色器
const VSHADER_SOURCE =
 `attribute vec4 a_Position;
  void main() {
    gl_Position = a_Position;
  }`;

// 片段著色器
const FSHADER_SOURCE =
 `void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }`;
複製程式碼

之後通過gl.program將頂點position座標傳入頂點著色器,這就相當於在畫布上確定了幾個點的座標資訊,這些點需要用線條連線起來才能構成圖形,這個由頂點座標裝配成幾何圖形的過程就叫做圖形裝配。

被裝配的基本圖形被稱作圖元,它包含點、線、面等基本幾何圖形。在呼叫WebGL的drawArrays或drawElements方法時作為引數傳入,從而指定圖元型別。

一個三角形的繪製過程拆分來看就是執行三次頂點著色器,將三個點座標都傳入裝配區,根據繪製函式的圖元引數gl.TRIANGLES將三個點裝配成三角形,然後進入下一個過程——光柵化。

光柵化

簡單來說,光柵化就是將圖形轉化成片元,可以理解成一個個畫素。只有將圖形轉化成畫素後才能交由片段著色器處理。

光柵化結束後,WebGL執行片段著色器。每執行一次片段著色器就處理一個片元,將該片元的顏色寫入顏色緩衝區中,等到圖形中所有的片元處理完畢畫布上就得到了最後的影象。

如上面的例子,每一個片元都會被執行成紅色,由這一個個紅色畫素組成的三角形也就是紅色的。

如果要繪製一個多顏色三角圖形又是一個什麼過程呢?首先需要修改著色器的定義,也許可以這樣:

// 頂點著色器
const VSHADER_SOURCE =
 `attribute vec4 a_Position;
  attribute vec4 a_Color;
  varying vec4 v_Color;
  void main() {
    gl_Position = a_Position;
    v_Color = a_Color;
  }`;

// 片段著色器
const FSHADER_SOURCE =
 `varying vec4 v_Color;
  void main() {
    gl_FragColor = v_Color;
  }`;
複製程式碼

向頂點著色器傳入頂點座標和顏色兩個資料,執行三次後得到三角形三個頂點的座標和顏色,接下來通過圖元裝配得到一個三角形的圖元,到了關鍵的光柵化這一步,該如何定義片元的顏色呢?WebGL採用一個叫做內插的過程來計算顏色的值。

以一條線為例來解釋內插,兩個端點分別為(1.0,0.0,0.0)和(0.0,1.0,0.0),從一端到另一端,R的值從1.0降到0.0,G的值由0.0升到1.0,線上的所有點顏色值都這樣計算出來,實現了平滑的顏色漸變,這就是內插。

經過內插,圖形的每一個片元都指定了自己的顏色,寫入顏色緩衝區後呈現出來。

紋理貼圖

如果要為WebGL建立更加複雜更加自然的現實效果,就需要採用貼圖來將現成的圖片貼到圖形上。

圖片容器中存放的也是一個個RGB或RGBA的畫素,將圖片的資訊讀取後存放在紋理物件或者說紋理影象中,紋理影象有自己的座標系,座標中每一個單元格就存放的紋理影象的畫素資訊,也被稱作紋素。

img

將紋理影象的座標轉換到畫布上圖形的座標的對映過程就是紋理對映,這個過程中,為圖形頂點指定了紋理座標,剩下的顏色由內插計算得出,寫入顏色緩衝區後,圖形的表面就被貼上了影象的顏色。

img

用一個案例來實現紋理貼圖,現在要做的是:

  • 載入好需要的紋理影象
  • 設定紋理座標
  • 對紋理進行配置
  • 片段著色器抽出紋素並賦值給片元

在這個例子中我選擇提前載入圖片。在這裡要注意有的瀏覽器不允許訪問本地檔案,可以考慮自己搭建server或是開啟瀏覽器訪問本地檔案。

function main() {
  const canvas = document.getElementById('webgl');

  const webgl = getWebGLContext(canvas);
  webgl.images = {};

  // 初始化之前先載入圖片
  loadImage([
    `src/images/0.jpeg`,
  ], webgl).then((gl) => {
    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
      console.log('Failed to intialize shaders.');
      return;
    }

    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    const n = initVertexBuffers(gl);
    initTextures(gl, n, 0);
  });
}
複製程式碼

loadImage的實現很簡單,用一個promise來處理非同步載入圖片,傳入陣列為了之後支援多張圖片。在initVertexBuffers中建立資料buffer,將圖形頂點和紋理影象座標一起傳入著色器。

function initVertexBuffers(gl) {
  // 頂點座標和紋理影象座標
  const vertices = new Float32Array([
    -0.3, 0.7, 0.0, 0.0, 1.0,
   -0.3, -0.7, 0.0, 0.0, 0.0,
    0.3, 0.7, 0.0, 1.0, 1.0,
    0.3, -0.7, 0.0, 1.0, 0.0,
  ]);

  const FSIZE = vertices.BYTES_PER_ELEMENT;

  const vertexBuffer = gl.createBuffer();

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

  const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');

  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 5, 0);
  gl.enableVertexAttribArray(a_Position);

  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 5, FSIZE * 3);
  gl.enableVertexAttribArray(a_TexCoord);

  return 4;
}
複製程式碼

然後看看最主要的initTextures,在這裡配置紋理:

function initTextures(gl, n, index) {
  // 建立紋理物件
  const texture = gl.createTexture();
  const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
  const image = gl.images[index];

  // WebGL紋理座標中的縱軸方向和PNG,JPG等圖片容器的Y軸方向是反的,所以先反轉Y軸
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

  // 啟用紋理單元,開啟index號紋理單元
  gl.activeTexture(gl[`TEXTURE${index}`]);

  // 繫結紋理物件
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // 配置紋理物件的引數
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

  // 將紋理影象分配給紋理物件
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

  // 將紋理單元編號傳給著色器
  gl.uniform1i(u_Sampler, index);

  // 繪製
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);
}
複製程式碼

這裡又遇到兩個概念:

紋理物件配置引數 texParameteri方法用來配置紋理物件引數,函式第二個引數傳入配置引數名,第三個引數傳入配置引數值,可以配置的引數有:

  • 伸展(gl.TEXTURE_MAX_FILTER): 繪製圖形比紋理影象大的時候怎麼取紋素,預設值gl.LINEAR
  • 收縮(gl.TEXTURE_MIN_FILTER): 繪製圖形比紋理影象小的時候怎麼取紋素, 預設值gl.NEAREST_MIP_LINEAR
  • 水平填充(gl.TEXTURE_WRAP_S): 定義繪製圖形水平方向如何填充,預設值gl.REPEAT
  • 垂直填充(gl.TEXTURE_WRAP_T): 定義繪製圖形垂直方向如何填充,預設值gl.REPEAT

詳細參考texParameteri

紋理單元 如果需要使用多張圖片就要管理多個紋理圖片,WebGL為了使用多個紋理,用紋理單元來處理紋理影象。WebGL的實現至少支援8個紋理單元,分別用gl.TEXRTRUE0,gl.TEXRTRUE1,...,gl.TEXRTRUE7來表示。

最後是著色器程式碼,在呼叫gl.drawArrays傳入圖元型別TRIANGLE_STRIP後執行:

const VSHADER_SOURCE =
 `attribute vec4 a_Position;
  attribute vec2 a_TexCoord;
  varying vec2 v_TexCoord;
  void main() {
    gl_Position = a_Position;
    v_TexCoord = a_TexCoord;
  }`;

const FSHADER_SOURCE =
 `precision mediump float;
  uniform sampler2D u_Sampler;
  varying vec2 v_TexCoord;
  void main() {
    gl_FragColor = texture2D(u_Sampler, v_TexCoord);
  }`;
複製程式碼

頂點著色器中傳入紋理影象的頂點座標,將它傳遞給片段著色器,在片段著色器中宣告瞭一個專用於紋理物件的資料型別sampler2D,指向一個紋理單元編號(接下來解釋),著色器獲取紋素由函式texture2D完成,傳入引數紋理單元編號和紋理影象座標。

img

多紋理實現

要使用多個紋理就要用到更多的紋理單元,多個紋理可以組合也可以單獨渲染,利用前面的程式碼,可以很容易擴充套件成一起多紋理的案例,加上一些3D效果和動畫,就可以組合成一個輪播圖片。

img

此文已由騰訊雲+社群在各渠道釋出

獲取更多新鮮技術乾貨,可以關注我們騰訊雲技術社群-雲加社群官方號及知乎機構號

相關文章