本文由雲+社區發表
作者: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效果和動畫,就可以組合成一個輪播圖片。
此文已由騰訊雲+社區在各渠道發布
獲取更多新鮮技術干貨,可以關注我們騰訊雲技術社區-雲加社區官方號及知乎機構號