紋理的應用就涉及到一項很重要的技術:
紋理對映
。所謂紋理對映,就是將一張圖片對映到一個幾何圖形的表面上去,比如紋理對映到矩形物體上,這個矩形看上去就像是一張圖片,這張圖片又可以稱為
紋理影象或紋理
。
紋理對映
紋理對映的作用是根據紋理影象,為光柵化後的每個片元塗上適當的顏色,
組成紋理影象的畫素又稱之為紋素(Texel)
,每一個紋素的顏色都可以使用 RGB 或者 RGBA 格式編碼。
紋理座標
為了能把一張紋理對映到物體上,我們需要指定物體的每個頂點各自對應紋理的哪個部分。紋理使用上更多采用的是 2D 紋理,紋理座標在 x 和 y 軸上,範圍在 0-1 之間。2D 的紋理座標通常又稱之為 uv 座標,u 對應水平方向,也就是 x 軸,v 對應垂直方向,也就是 y 軸。如果是 3D 紋理,第三個則是 w,對應 z 軸。紋理座標始於(0,0)點,也就是紋理左下角,終於(1,1),也就是紋理的右上角。使用紋理座標來獲取紋理顏色的方式稱之為取樣。每個頂點會關聯著一個
紋理座標
,用來表明該從紋理的哪部分取樣。
紋理座標看起來像是這樣的:
const uvs = [
0, 0, // 左下角
0, 1, // 左上角
1, 0, // 右下角
1, 1 // 右上角
];
對映原理主要是將紋理影象的頂點對映到 WebGL 座標系統的四個頂點。
紋理環繞方式
紋理座標的範圍通常是從 (0, 0) 到 (1, 1),如果超出這個範圍該怎麼辦呢?OpenGL 預設行為是重複這個紋理影象,但是也提供了一些其它選擇:
// 可以透過 gl。texParameter[fi] 對座標不同軸向設定(2D 紋理 st 對應 uv,3D 紋理 str 對應 uvw )
// void gl。texParameterf(target, pname, param);
// 引數請參考:https://developer。mozilla。org/zh-CN/docs/Web/API/WebGLRenderingContext/texParameter
// 由於應用條件較多,可以直接上鍊接瞭解一下,然後對應理解教程裡涉及的部分即可
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);
當紋理座標超出預設範圍時,每個選項都有不同的視覺效果輸出。
紋理過濾
紋理座標不依賴解析度,可以是任意浮點值,所以 OpenGL 知道如何將紋素對映到紋理座標。但是,如果此時有一個小的紋理需要對映到一個很大的物體上,就可能導致多個畫素都對映到同一個紋素上,相反,單個畫素可能會被對映到多個紋素。紋理過濾就是為了解決不一致時紋理的取樣計算問題,其中最重要的就是如下兩種:
NEAREST 臨近過濾
(下圖左):選擇中心點最接近紋理座標的那個畫素,也是最簡單的紋理過濾方式,效率最高。
LINEAR 線性過濾
(下圖右):選擇中心點周圍最近的 4 個紋素加權計算出來,一個紋理畫素的中心距離紋理座標越近,那麼這個紋理畫素的顏色對最終的樣本顏色的貢獻越大。
從圖中可以看出,採用臨近過濾的圖片有更明顯的鋸齒感(比如眼眶那個地方),而右邊圖片則更加平滑。我這裡選用的圖片尺寸較大,尺寸小的會更加明顯。線性過濾可以產生更加真實的輸出,但是如果想開發畫素風格的遊戲,就可以用臨近過濾選項。
當對影象進行放大和縮小的時候,我們可以選擇不同的過濾選項。比如:在縮小的時候採用臨近過濾,獲取最高效率;放大時用線性過濾,獲得較好表現。紋理過濾的使用方式跟紋理環繞類似:
// 當進行縮小時
gl。texParameteri(gl。TEXTURE_2D, gl。TEXTURE_MIN_FILTER, gl。NEAREST);
// 當進行放大時
gl。texParameteri(gl。TEXTURE_2D, gl。TEXTURE_MAG_FILTER, gl。LINEAR);
將紋理應用到矩形上
接著,試著把紋理座標關聯給上一章的矩形。先將所有的頂點顏色還原成白色,下方列出了所有程式碼:
function createShader(gl, type, source) {
// 。。。
}
function createProgram(gl, vertexShader, fragmentShader) {
// 。。。
}
function main() {
const image = new Image();
// 如果是用 WebGL 中文文件上內建的執行環境編輯內容的,可以直接用網站內建的紋理圖片。https://webglfundamentals。org/webgl/resources/leaves。jpg。
// 由於我這裡是自定義了本地的檔案,因此建立了一個本地伺服器來載入圖片。使用本地檔案的方式在文章末尾處。
image。src = “http://192。168。55。63:8080/logo。png”;
image。onload = function() {
render(image);
};
}
function render() {
const canvas = document。createElement(‘canvas’);
document。getElementsByTagName(‘body’)[0]。appendChild(canvas);
canvas。width = 400;
canvas。height = 300;
const gl = canvas。getContext(“webgl”);
if (!gl) {
return;
}
const vertexShaderSource = `
attribute vec2 a_position;
// 紋理貼圖 uv 座標
attribute vec2 a_uv;
attribute vec4 a_color;
varying vec4 v_color;
varying vec2 v_uv;
// 著色器入口函式
void main() {
v_color = a_color;
v_uv = a_uv;
gl_Position = vec4(a_position, 0。0, 1。0);
}`;
const vertexShader = createShader(gl, gl。VERTEX_SHADER, vertexShaderSource);
// 讓頂點的比例和影象比例一致
const ratio = (image。width / image。height) / (canvas。width / canvas。height);
const positions = [
-ratio, -1,
-ratio, 1,
ratio, -1,
ratio, 1
];
const uvs = [
0, 0, // 左下角
0, 1, // 左上角
1, 0, // 右下角
1, 1 // 右上角
];
// 在片元著色器文字處暫時遮蔽顏色帶來的影響,但此處顏色值我們還是上傳給頂點著色器
const colors = [
255, 0, 0, 255,
0, 255, 0, 255,
0, 0, 255, 255,
255, 127, 0, 255
];
const indices = [
0, 1, 2,
2, 1, 3
];
const vertexBuffer = gl。createBuffer();
gl。bindBuffer(gl。ARRAY_BUFFER, vertexBuffer);
const attribOffset = (positions。length + uvs。length) * Float32Array。BYTES_PER_ELEMENT + colors。length;
const arrayBuffer = new ArrayBuffer(attribOffset);
const float32Buffer = new Float32Array(arrayBuffer);
const colorBuffer = new Uint8Array(arrayBuffer);
// 當前頂點屬性結構方式是 pos + uv + color
// 按 float 32 分佈 pos(2)+ uv(2) + color(1)
// 按子節分佈 pos(2x4) + uv(2x4) + color(4)
let offset = 0;
let i = 0;
for (i = 0; i
float32Buffer[offset] = positions[i];
float32Buffer[offset + 1] = positions[i + 1];
offset += 5;
}
offset = 2;
for (i = 0; i
float32Buffer[offset] = uvs[i];
float32Buffer[offset + 1] = uvs[i + 1];
offset += 5;
}
offset = 16;
for (let j = 0; j
// 2 個 position 的 float,加 4 個 unit8,2x4 + 4 = 12
// stride + offset
colorBuffer[offset] = colors[j];
colorBuffer[offset + 1] = colors[j + 1];
colorBuffer[offset + 2] = colors[j + 2];
colorBuffer[offset + 3] = colors[j + 3];
offset += 20;
}
gl。bufferData(gl。ARRAY_BUFFER, arrayBuffer, gl。STATIC_DRAW);
const indexBuffer = gl。createBuffer();
gl。bindBuffer(gl。ELEMENT_ARRAY_BUFFER, indexBuffer);
gl。bufferData(gl。ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl。STATIC_DRAW);
const fragmentShaderSource = `
precision mediump float;
varying vec2 v_uv;
varying vec4 v_color;
// GLSL 有一個供紋理物件使用的內建資料型別,叫做取樣器(Sampler),它以紋理型別作為字尾
// 比如此處使用的是 2D 紋理,型別就定義為 sampler2D
uniform sampler2D u_image;
// 著色器入口函式
void main() {
// 使用 GLSL 內建函式 texture2D 取樣紋理,它第一個引數是紋理取樣器,第二個引數是對應的紋理座標
// 函式會使用之前設定的紋理引數對相應的顏色值進行取樣,這個片段著色器的輸出就是紋理的(插值)紋理座標上的(過濾後的)顏色。
gl_FragColor = texture2D(u_image, v_uv);
}`;
const fragmentShader = createShader(gl, gl。FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vertexShader, fragmentShader);
gl。viewport(0, 0, gl。canvas。width, gl。canvas。height);
gl。clearColor(0, 0, 0, 255);
gl。clear(gl。COLOR_BUFFER_BIT);
gl。useProgram(program);
const positionAttributeLocation = gl。getAttribLocation(program, “a_position”);
gl。enableVertexAttribArray(positionAttributeLocation);
const uvAttributeLocation = gl。getAttribLocation(program, “a_uv”);
gl。enableVertexAttribArray(uvAttributeLocation);
const colorAttributeLocation = gl。getAttribLocation(program, “a_color”);
gl。enableVertexAttribArray(colorAttributeLocation);
gl。bindBuffer(gl。ARRAY_BUFFER, vertexBuffer);
gl。vertexAttribPointer(positionAttributeLocation, 2, gl。FLOAT, false, 20, 0);
// 新增頂點屬性紋理座標,這裡大家應該都很清楚了,就不再多說了
gl。vertexAttribPointer(uvAttributeLocation, 2, gl。FLOAT, false, 20, 8);
gl。vertexAttribPointer(colorAttributeLocation, 4, gl。UNSIGNED_BYTE, true, 20, 16);
const texture = gl。createTexture();
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。NEAREST);
gl。texParameteri(gl。TEXTURE_2D, gl。TEXTURE_MAG_FILTER, gl。LINEAR);
// gl。texImage2D(target, level, internalformat, format, type, HTMLImageElement? pixels);
// 此介面主要為了指定二維紋理影象,影象的來源有多種,可以直接採用 HTMLCanvasElement、HTMLImageElement 或者 base64。此處選用最基礎的 HTMLImageElement 講解。
// 關於引數的詳細內容請參考:https://developer。mozilla。org/zh-CN/docs/Web/API/WebGLRenderingContext/texImage2D
gl。texImage2D(gl。TEXTURE_2D, 0, gl。RGBA, gl。RGBA, gl。UNSIGNED_BYTE, image);
gl。bindBuffer(gl。ELEMENT_ARRAY_BUFFER, indexBuffer);
gl。drawElements(gl。TRIANGLES, indices。length, gl。UNSIGNED_SHORT, 0);
}
最終,我們會在螢幕上看到這樣的成像:
圖片上下顛倒了,這是因為除了紋理座標之外,圖片自身也是有座標系的,圖片的座標原點始於左上角,終於右下角,取值範圍也是 0-1。把一張圖片載入到紋理中,圖片資料就會從圖片座標系到了紋理座標系,此時圖片就已經出現了上下倒置,所以我們需要一個 flipY 的操作,在渲染的時候將上下再進行一次倒置。
// 翻轉圖片
gl。pixelStorei(gl。UNPACK_FLIP_Y_WEBGL, true);
可能會有些同學有疑問,為什麼 sampler2D 是個 uniform,但是卻不用 gl。uniform 相關來賦值呢?因為在 OpenGL 中,會給紋理分配一個預設的紋理位置,稱之為
紋理單元
。預設啟用的紋理單元是 0,因此,我之前沒有執行任何位置值分配,紋理貼圖會自動繫結到預設紋理單元上。當然,我們也可以透過 gl。uniform 來給片段著色器設定多個紋理,只需要啟用對應的紋理單元。通用裝置支援 8 個紋理單元,現代中高階裝置支援會更多,這個只能具體機型具體分析,一般限制在 8 個即可,它們的編號分別是 gl。TEXTURE0 - 8。透過這種編號方式,我們在迴圈紋理單元的時候會很方便,不過這個都是後話了。
接下來,我們嘗試多加一個紋理。在原有程式碼上進行如下改造:
function main() {
// 新增加一張紋理貼圖
const images = [“http://192。168。55。63:8080/logo。png”, “http://192。168。55。63:8080/close-icon。png”];
const dataList = [];
let index = 0;
for (let i = 0; i
const image = new Image();
image。src = images[i];
dataList。push(image);
image。onload = function () {
index++;
if (index >= images。length) {
render(dataList);
}
};
}
}
function render(dataList) {
// 。。。
// 重新定義頂點位置
const ratio = 0。5;
const positions = [
-ratio, -1,
-ratio, 1,
ratio, -1,
ratio, 1
];
// 。。。
// 修改片元著色器文字
const fragmentShaderSource = `
precision mediump float;
varying vec2 v_uv;
varying vec4 v_color;
// 新增一個紋理
uniform sampler2D u_image0;
uniform sampler2D u_image1;
// 著色器入口函式
void main() {
vec4 tex1 = texture2D(u_image0, v_uv);
vec4 tex2 = texture2D(u_image1, v_uv);
// 將紋理色值相乘
// rgb 和黑色相乘都為黑色(黑色 rgb 每分量都是 0),和白色相乘,都為原色(白色 rbg 每分量都是 1)
gl_FragColor = tex1 * tex2;
}`;
// 。。。
// 判斷有紋理才設定翻轉
if(dataList。length > 0){
gl。pixelStorei(gl。UNPACK_FLIP_Y_WEBGL, true);
}
for (let j = 0; j
const data = dataList[j];
const samplerName = `u_image$`;
const u_image = gl。getUniformLocation(program, samplerName);
// 設定每個紋理的位置值
gl。uniform1i(u_image, j);
const texture = gl。createTexture();
gl。activeTexture(gl。TEXTURE0 + j);
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。NEAREST);
gl。texParameteri(gl。TEXTURE_2D, gl。TEXTURE_MAG_FILTER, gl。LINEAR);
gl。texImage2D(gl。TEXTURE_2D, 0, gl。RGBA, gl。RGBA, gl。UNSIGNED_BYTE, data);
}
}
原圖和渲染後的圖片對比:
到這裡為止,相信大家應該瞭解了紋理對映是怎麼回事,接下來,我們再來看幾個使用案例。
更多案例
這裡展示的幾個案例,還是按照一個紋理呈現。
不同頂點色應用到紋理
如果你跟著學到了這裡,應該可以輕鬆實現了吧!
// 此處展示出部分應用程式碼
const colors = [
255, 0, 0, 255,
0, 255, 0, 255,
0, 0, 255, 255,
255, 127, 0, 255
];
const fragmentShaderSource = `
precision mediump float;
varying vec2 v_uv;
varying vec4 v_color;
uniform sampler2D u_image;
void main() {
vec4 tex1 = texture2D(u_image, v_uv);
gl_FragColor = tex1 * v_color;
}`;
再增加一點細節,就有一種鐳射卡的感覺了。
改變最終輸出的 RGB 順序
const fragmentShaderSource = `
precision mediump float;
varying vec2 v_uv;
varying vec4 v_color;
uniform sampler2D u_image;
void main() {
vec4 tex1 = texture2D(u_image, v_uv)。bgra;
gl_FragColor = tex1;
}`;
這個原理其實也就是將原來通道的顏色替換成另外一種顏色。
網上有很多紋理的應用例項,大家都可以嘗試著去改造一下。
其他
為什麼在 GLSL 中變數的字首都是 a_, u_ 或 v_ ?
這是一個命名約定,不是強制的,只是為了更清晰的知道值應該從哪裡來,比如:a_ 就是指向頂點輸入屬性 attribute,代表資料是從頂點緩衝中來;u_ 就是全域性變數 uniform,可以直接對著色器設定;v_ 代表可變數 varying,是從頂點著色器的頂點中插值而來。
本地伺服器搭建
由於本次教程的 WebGL 測試內容我都放在自定義資料夾裡。因此,需要一個伺服器去執行 HTML 檔案。資料夾內容如下:
接著,在資料夾下安裝 npm 庫 http-server:
npm install http-server -g
// 安裝完成之後執行
http-server
// 此時控制檯會出現類似如下:
Available on:
http://127。0。0。1:8080
http://192。168。55。63:8080
選擇其中一個地址使用即可,但是不可混用,不然可能出現跨域問題。比如:我用 “http://192。168。55。45:8080” 開啟專案,那麼圖片載入也用這個地址。
最後,在瀏覽器上輸入該地址,點選 html 檔案即可測試。
注意:如果提示沒有 npm 命名,可能是因為你沒有安裝過 Node。js 任何版本,請安裝 Node。js。如果切換了網路,因此 IP 地址已經改變,記得也要重新執行 http-server,重新生成本地伺服器。
內容參考:
1。 WebGL 基礎:
https://webglfundamentals。org/webgl/lessons/zh_cn/webgl-fundamentals。html
2。 WebGL API 對照表:
https://www。khronos。org/files/webgl/webgl-reference-card-1_0。pdf
3。 OpenGL 中文文件:
https://learnopengl-cn。github。io/01%20Getting%20started/04%20Hello%20Triangle/
4。 OpenGL 紋理旋轉及翻轉問題詳解:
https://juejin。cn/post/6854573205378727949