您的位置:首頁>正文

WebGL 紋理顏色原理

本文作者:ivweb qcyhust

導語

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

顏色緩衝區

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

可以將顏色緩存區看成圖像顏色記憶體, 在緩存區中以RGB或RGBA的格式存儲著畫布上每一個圖元的顏色資訊, 各個圖元點組合起來就構成了顏色緩存的矩形陣列。

這個定義看起來與圖片記憶體是很相似的, 顏色緩存為RGB或是RGBA每一個通道分配存放位數, 其中RGB就是顏色資料, A表示alpha也就是該圖元的透明度資訊, 顏色佔用的位元數值就是色彩深度, 比如色彩深度為24位元, 表示每一個圖元24位元, 一般24位的分配方案就是紅色、藍色、綠色各占8位元, 如果需要透明效果的話, 可以採用32位色彩深度為alpha通道分配8位。

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

圖形裝配

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

// 頂點著色器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的圖元, 將圖片的資訊讀取後存放在紋理物件或者說紋理圖像中, 紋理圖像有自己的坐標系, 座標中每一個儲存格就存放的紋理圖像的圖元資訊, 也被稱作紋素。

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

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

載入好需要的紋理圖像

設置紋理座標

對紋理進行配置

片段著色器抽出紋素並賦值給片元

在這個例子中我選擇提前載入圖片。在這裡要注意有的流覽器不允許訪問本地檔,可以考慮自己搭建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完成,傳入參數紋理單元編號和紋理圖像座標。

多紋理實現

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

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

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

載入好需要的紋理圖像

設置紋理座標

對紋理進行配置

片段著色器抽出紋素並賦值給片元

在這個例子中我選擇提前載入圖片。在這裡要注意有的流覽器不允許訪問本地檔,可以考慮自己搭建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完成,傳入參數紋理單元編號和紋理圖像座標。

多紋理實現

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

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示