前端也要懂圖形化: 淺談 WebGL 技術
WebGL 概述
WebGL 是一項結合了 HTML5 和 JavaScript,用來在網頁上繪製和渲染複雜三維圖形的技術。WebGL 通過 JavaScript 操作 OpenGL 接口的標準,把三維空間圖像顯示在二維的屏幕上。
WebGL 與 OpenGL
OpenGL 本身是一套規範,不是 API,通過 OpenGL 來統一各個顯卡廠家實現操作圖形、圖像的實現標準。WebGL 的技術規範繼承自 OpenGL ES,從 2.0 版本開始,OpenGL 支持可編程着色器方法,這個支持可以讓我們通過着色器語言編寫着色器程序,代表我們可以精確控制每個像素的位置和顏色。在 OpenGL2.0 規範中,GPU 可以執行着色器程序,根據着色器程序生成像素數據,最終顯示在屏幕上。
WebGL 程序的結構
相對於傳統網頁,支持 WebGL 的瀏覽器底層接入了 OpenGL/OpenGL ES 標準,WebGL 通過實現標準支持着色器語言編程語言 GLSL ES,在我們實際開發過程中,GLSL ES 通常是以字符串的形式存在 JavaScript 中,我們可以通過 JavaScript 修改 GLSL ES 字符串來改變着色器程序。
着色器
着色器是 WebGL 依賴的實現圖像渲染的一種繪圖機制。WebGL 在 GPU 中運行,因此需要使用能夠在 GPU 上運行的代碼,這樣的代碼需要提供成對的方法,他們分別是頂點着色器和片元着色器。
頂點着色器
頂點着色器的作用是計算頂點的位置,根據計算出的一系列頂點位置,WebGL 可以對點, 線和三角形在內的一些圖元進行光柵化處理。WebGL 中顯示的物體由一系列頂點組成,每個頂點都有位置和顏色等信息,在默認的情況下,所有像素的顏色都由線性插值計算得來,自動形成平滑漸變。
下面是頂點着色器的示例代碼,頂點着色器通過a_Position
、a_PointSize
分別接收並設置頂點的位置和大小,通過a_Color
從程序獲取顏色並通過v_Color
傳遞給片元着色器。其中,gl_Position 和 gl_PointSize 是着色器的內置變量,分別代表頂點的位置和大小,因此這段代碼的作用是設置頂點的位置和大小,同時接收程序傳入的顏色a_Color
並把它傳遞給片元着色器。
在着色器內,一般命名以gl_
開頭的變量是着色器的內置變量,除此之外webgl_
和_webgl
還是着色器保留字,自定義變量不能以webgl_
或_webgl
開頭。變量聲明一般包含 < 存儲限定符 >< 數據_類型_ >< _變量名稱_ >,以attribute vec4 a_Position
爲例,attribute
表示存儲限定符,vec
是數據類型,a_Position
爲變量名稱。
const vs_source = `
attribute vec4 a_Position;
attribute float a_PointSize;
attribute vec4 a_Color;
varying vec4 v_Color;
void main() {
gl_Position = a_Position;
gl_PointSize = a_PointSize;
v_Color = a_Color;
}
`;
片元着色器
片元着色器的作用是計算出當前繪製圖元中每個像素的顏色值,逐片元控制片元的顏色和紋理等渲染。關於片元,片元包含顏色、深度和紋理等信息,片元相對像素多出許多信息,從直觀表現上看兩者都是像素點。關於圖元,圖元是指 WebGL 中可以直接繪製 7 種基本圖形,它們分別是:
-
孤立點:
gl.POINTS
-
孤立線段:
gl.LINES
-
連續線段:
gl.LINE_STRIP
-
連續線圈:
gl.LINE_LOOP
-
孤立三角形:
gl.TRIANGLES
-
三角帶:
gl.TRIANGLE_STRIP
-
三角扇:
gl.TRIANGLE_FAN
下面是片元着色器的示例代碼,首先設置了float
爲中等精度,然後通過v_Color
接收來自頂點着色器的顏色並將其設置給內置變量 gl_FragColor,其中通過內置變量 gl_FragColor 來確定頂點像素顏色。
關於精度可以詳見 WebGL 着色器精度設置 [1]。
const fs_source = `
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
`;
存儲限定符
在上面例子中,我們在聲明變量時,除了指定變量類型外,還使用了存儲限定符。在 GLSL 中,應該根據變量用途使用存儲限定符修飾。一般常用到三種存儲限定符:
attribute 屬性
attribute 只能用於頂點着色器,用來存儲頂點着色器中每個頂點的輸入,包括頂點位置座標、紋理座標和顏色等信息。
通常情況下我們會使用緩衝,緩衝是程序發送給 GPU 的數據,attribute 用來從緩衝中獲取所需數據,並將它提供給頂點着色器。程序可以指定每次頂點着色器運行時讀取緩衝的規則。
使用緩衝設置頂點信息
生成緩衝代碼
// 創建緩衝對象
const vertexBuffer = gl.createBuffer();
將數據寫入緩衝
// 緩衝數據
const vertices = new Float32Array([
-0.5, 0.5, 10.0, 1.0, 0.0, 0.0, 1.0,
-0.5, -0.5, 20.0, 0.0, 1.0, 0.0, 1.0,
0.5, 0.5, 30.0, 0.0, 0.0, 1.0, 1.0
]);
// 綁定緩衝對象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 將緩衝數據填充緩衝對象
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
設置緩衝讀取規則和啓用緩衝對象
// 調用頂點緩衝,將緩衝數據中一組7個數據中的前2個數據傳給a_Position
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, SIZE * 7, 0);
// 調用頂點緩衝,將緩衝數據中一組7個數據中的第3(偏移2個數據取1一個)個數據傳給a_PointSize
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, SIZE * 7, SIZE * 2);
// 調用頂點緩衝,將緩衝數據中一組7個數據中的第4-7(偏移3個數據取4個)個數據傳給a_Color
gl.vertexAttribPointer(a_Color, 4, gl.FLOAT, false, SIZE * 7, SIZE * 3);
// 激活a_Position使用緩衝數組,下同
gl.enableVertexAttribArray(a_Position);
gl.enableVertexAttribArray(a_PointSize);
gl.enableVertexAttribArray(a_Color);
uniform 全局變量
uniform 可以存在頂點着色器和片元着色器,用來存儲圖元處理過程中保持不變的值,例如顏色。值得一提的是,頂點着色器和片元着色器共享了 uniform 變量的命名空間,如果在頂點着色器和片段着色器中都聲明瞭同名 uniform 變量,二者聲明的類型和着色器的精度必須一致。
varying 可變量
varying 一般同時存在頂點着色器和片元着色器中,它的作用是從頂點着色器向片元着色器傳輸數據,在圖元裝配後,WegGL 會對圖元光柵化,在光柵化過程中,varying 聲明的變量的值會進行內插,使 varying 變量的值線性(默認)變化。
一個簡單的例子:彩色三角形
在這段代碼中,指定三角形的三個點,分別位於畫布的左上角、左下角和右上角,他們的顏色分別爲紅色、綠色和藍色,大家可以運行示例代碼查看效果。
// 頂點着色器
const vs_source = `
attribute vec4 a_Position;
attribute float a_PointSize;
attribute vec4 a_Color;
varying vec4 v_Color;
void main() {
gl_Position = a_Position;
gl_PointSize = a_PointSize;
v_Color = a_Color;
}
`;
// 片元着色器
const fs_source = `
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
`;
const canvas = document.getElementById('app');
const gl = canvas.getContext('webgl');
function initShader() {
// 創建shader
const vs_shader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs_shader, vs_source);
gl.compileShader(vs_shader);
if (!gl.getShaderParameter(vs_shader, gl.COMPILE_STATUS)) {
const error = gl.getShaderInfoLog(vs_shader);
console.log('Failed to compile vs_shader:' + error);
gl.deleteShader(vs_shader);
return;
}
const fs_shader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs_shader, fs_source);
gl.compileShader(fs_shader);
if (!gl.getShaderParameter(fs_shader, gl.COMPILE_STATUS)) {
const error = gl.getShaderInfoLog(fs_shader);
console.log('Failed to compile fs_shader:' + error);
gl.deleteShader(fs_shader);
return;
}
// 創建program
const program = gl.createProgram();
gl.attachShader(program, vs_shader);
gl.attachShader(program, fs_shader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const error = gl.getProgramInfoLog(program);
console.log('無法鏈接程序對象:' + error);
gl.deleteProgram(program);
gl.deleteShader(fs_shader);
gl.deleteShader(vs_shader);
return;
}
gl.useProgram(program);
gl.program = program;
// 獲取着色器變量位置和賦值
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return;
}
const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if (a_Color < 0) {
console.log('Failed to get the storage location of a_Color');
return;
}
// 使用緩衝區表示多個值
const vertices = new Float32Array([
-0.5, 0.5, 1.0, 0.0, 0.0, 1.0,
-0.5, -0.5, 0.0, 1.0, 0.0, 1.0,
0.5, 0.5, 0.0, 0.0, 1.0, 1.0
])
const SIZE = vertices.BYTES_PER_ELEMENT;
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, SIZE * 6, 0);
gl.vertexAttribPointer(a_Color, 4, gl.FLOAT, false, SIZE * 6, SIZE * 2);
gl.enableVertexAttribArray(a_Position);
gl.enableVertexAttribArray(a_Color);
}
initShader();
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
我們分別只設置了三個點的顏色,但是渲染出來的圖形卻是彩色的三角形,造成這樣的結果的原因是因爲v_Color
在傳遞的過程中經過了內插。以左上角點和右上角點對比,代表rgb
顏色三個值線性向目標值變化,內插過程將顏色分配給光柵化後每個片元(像素)顏色,造成了顏色漸變。
WebGL 渲染過程
以上面例子爲例,三角形的有 3 個點,需要執行 3 次頂點着色器,把 3 個點位置存儲在圖形裝配區域。頂點着色器執行完畢後,三個點的座標都已經處在圖形裝配區,開始裝配圖形,由於我們設置的是gl.TRIANGLES
,圖形裝配出一個三角形。隨後會進行光柵化,將裝配好的圖像轉化成片元組合,也就是像素點,varying 變量的插值也在這一過程中進行。光柵化後,程序調用片元着色器,假定光柵化後有 10 個片元,那麼片元着色器將執行 10 次,每次調用處理一個片元,片元着色器計算該片元的顏色並寫入顏色緩衝區,當最後一個片元被處理完成,瀏覽器就會顯示出最終的結果。
圖片渲染
通過頂點信息繪製像素點功能很強大,但對於複雜圖形不夠好用,通常我們需要渲染圖片,這樣就需要紋理映射了,這就是爲什麼在 WebGL 中,我們更傾向把圖片描述爲紋理的原因。
紋理映射
紋理映射原理很簡單,就是將一張圖片映射到一個幾何圖形的表面。由於在 WebGL 中,WebGL 能直接繪製的圖形只有點、線和三角形,因此在紋理映射中,圖片被映射到由兩個三角形組成的矩形。
紋理映射具體步驟:
- 準備映射的紋理圖圖像,紋理圖像應該滿足 2 的冪次方
圖片分辨率爲非 2 的冪次方(104 * 104),圖片不能被渲染,並提示圖片分辨率可能不是 2 的冪次方
圖片分辨率爲 2 的冪次方(256 * 256),正常顯示
- 爲幾何圖形配置映射方式,頂點座標和紋理座標對應,需要注意,構建順序與新增頂點的奇偶性相關。(假設 v0 代表第一個頂點
-
如果新增頂點時奇數,頂點排列順序爲:T = [n-1 n-2 n];
-
如果新增頂點爲偶數,頂點排列順序爲:T = [n-2 n-1 n];
下面頂點組成的三角形分別爲:T1 = [v0, v1, v2],T2 = [v2, v1, v3]
// 頂點座標、紋理座標,繪製順序:左上->左下->右上->右下
const vertices = new Float32Array([
-0.5, 0.5, 0.0, 1.0, // v0
-0.5, -0.5, 0.0, 0.0, // v1
0.5, 0.5, 1.0, 1.0, // v2
0.5, -0.5, 1.0, 0.0, // v3
]);
當把第一個座標和第二個座標互換時,繪製
// 頂點座標、紋理座標,繪製順序:左下->左上->右上->右下
const vertices = new Float32Array([
-0.5, -0.5, 0.0, 0.0, // v0
-0.5, 0.5, 0.0, 1.0, // v1
0.5, 0.5, 1.0, 1.0, // v2
0.5, -0.5, 1.0, 0.0, // v3
]);
-
加載紋理圖像,對其進行一些配置,以在 WebGL 中使用,分別有如下操作
-
創建紋理對象
-
圖片 Y 軸反轉,因爲圖片座標系 y 軸垂直向下,而紋理座標系(st 座標系)t 軸垂直向上,在我們的例子中,頂點座標和紋理座標是相反的,所以在紋理存儲像素中需要對 y 軸翻轉
假如未未進行 y 軸反轉
-
激活指定紋理單元
-
綁定紋理對象到紋理單元
-
設置紋理過濾,紋理過濾是指,當繪製範圍(頂點座標組成的矩形區域)與紋理本身大小不匹配時,如何獲取紋理顏色。需要注意的是,設置錯誤的過濾可能會導致渲染失敗
-
gl.TEXTURE_MAP_FILTER
放大方法,紋理撐滿至繪製範圍 -
gl.TEXTURE_MIN_FILTER
縮小方法,紋理縮小至繪製範圍 -
gl.TEXTURE_WRAP_S
水平填充,紋理大小不變,左右填充 -
gl.TEXTURE_WRAP_T
垂直填充,紋理大小不變,上下填充
過濾設置錯誤,假如畫布大小 400 * 400,頂點座標範圍 - 0.5~0.5 對應大小 200 * 200,圖片大小 256 * 256,紋理比繪製範圍大,但是過濾僅設置了gl.TEXTURE_MAG_FILTER
,獲取紋理顏色失敗,控制檯又輸出了熟悉的錯誤:圖片不能被渲染,是否未設置合適紋理過濾
-
將圖像綁定到紋理
-
將紋理單元傳遞給着色器中的採樣器
-
在片元着色器中將相應的像素從紋理中抽取出來,並將像素的顏色賦給片元
以上操作相關的代碼片段如下
// 頂點着色器
const vs_source = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {
gl_Position = a_Position;
v_TexCoord = a_TexCoord;
}
`;
// 片元着色器
const fs_source = `
precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`;
...
function initShader() {
...
// 頂點座標和紋理座標
const vertices = new Float32Array([
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.0,
]);
...
}
const texture = gl.createTexture();
const u_Sample = gl.getUniformLocation(gl.program, 'u_Sample');
const img = new Image();
img.onload = function () {
// 加載紋理圖像,對其進行一些配置,以在WebGL中使用
// 紋理像素存儲y軸反轉
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 激活紋理單元0
gl.activeTexture(gl.TEXTURE0);
// 綁定紋理
gl.bindTexture(gl.TEXTURE_2D, texture);
// 設置過濾的過濾方式,這裏僅設置了gl.TEXTURE_MIN_FILTER,也可以設置多種過濾
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 生成紋理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
// 將紋理單元0傳遞給採樣器
gl.uniform1i(u_Sample, 0);
gl.clearColor(0.0, 1.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 繪圖順序跟點有關
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
img.src = 'power-of-2-image';
參考資料
[1]
WebGL 着色器精度設置: https://blog.csdn.net/u014291990/article/details/103173077
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/k8MbHKyzSRlBWS53Ymw3uQ