選單

Cocos Shader 基礎入門(四):紋理對映

紋理的應用就涉及到一項很重要的技術:

紋理對映

。所謂紋理對映,就是將一張圖片對映到一個幾何圖形的表面上去,比如紋理對映到矩形物體上,這個矩形看上去就像是一張圖片,這張圖片又可以稱為

紋理影象或紋理

紋理對映

紋理對映的作用是根據紋理影象,為光柵化後的每個片元塗上適當的顏色,

組成紋理影象的畫素又稱之為紋素(Texel)

,每一個紋素的顏色都可以使用 RGB 或者 RGBA 格式編碼。

紋理座標

為了能把一張紋理對映到物體上,我們需要指定物體的每個頂點各自對應紋理的哪個部分。紋理使用上更多采用的是 2D 紋理,紋理座標在 x 和 y 軸上,範圍在 0-1 之間。2D 的紋理座標通常又稱之為 uv 座標,u 對應水平方向,也就是 x 軸,v 對應垂直方向,也就是 y 軸。如果是 3D 紋理,第三個則是 w,對應 z 軸。紋理座標始於(0,0)點,也就是紋理左下角,終於(1,1),也就是紋理的右上角。使用紋理座標來獲取紋理顏色的方式稱之為取樣。每個頂點會關聯著一個

紋理座標

,用來表明該從紋理的哪部分取樣。

Cocos Shader 基礎入門(四):紋理對映

紋理座標看起來像是這樣的:

const uvs = [

0, 0, // 左下角

0, 1, // 左上角

1, 0, // 右下角

1, 1 // 右上角

];

對映原理主要是將紋理影象的頂點對映到 WebGL 座標系統的四個頂點。

Cocos Shader 基礎入門(四):紋理對映

紋理環繞方式

紋理座標的範圍通常是從 (0, 0) 到 (1, 1),如果超出這個範圍該怎麼辦呢?OpenGL 預設行為是重複這個紋理影象,但是也提供了一些其它選擇:

Cocos Shader 基礎入門(四):紋理對映

// 可以透過 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);

當紋理座標超出預設範圍時,每個選項都有不同的視覺效果輸出。

Cocos Shader 基礎入門(四):紋理對映

紋理過濾

紋理座標不依賴解析度,可以是任意浮點值,所以 OpenGL 知道如何將紋素對映到紋理座標。但是,如果此時有一個小的紋理需要對映到一個很大的物體上,就可能導致多個畫素都對映到同一個紋素上,相反,單個畫素可能會被對映到多個紋素。紋理過濾就是為了解決不一致時紋理的取樣計算問題,其中最重要的就是如下兩種:

NEAREST 臨近過濾

(下圖左):選擇中心點最接近紋理座標的那個畫素,也是最簡單的紋理過濾方式,效率最高。

LINEAR 線性過濾

(下圖右):選擇中心點周圍最近的 4 個紋素加權計算出來,一個紋理畫素的中心距離紋理座標越近,那麼這個紋理畫素的顏色對最終的樣本顏色的貢獻越大。

Cocos Shader 基礎入門(四):紋理對映

從圖中可以看出,採用臨近過濾的圖片有更明顯的鋸齒感(比如眼眶那個地方),而右邊圖片則更加平滑。我這裡選用的圖片尺寸較大,尺寸小的會更加明顯。線性過濾可以產生更加真實的輸出,但是如果想開發畫素風格的遊戲,就可以用臨近過濾選項。

當對影象進行放大和縮小的時候,我們可以選擇不同的過濾選項。比如:在縮小的時候採用臨近過濾,獲取最高效率;放大時用線性過濾,獲得較好表現。紋理過濾的使用方式跟紋理環繞類似:

// 當進行縮小時

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);

}

最終,我們會在螢幕上看到這樣的成像:

Cocos Shader 基礎入門(四):紋理對映

圖片上下顛倒了,這是因為除了紋理座標之外,圖片自身也是有座標系的,圖片的座標原點始於左上角,終於右下角,取值範圍也是 0-1。把一張圖片載入到紋理中,圖片資料就會從圖片座標系到了紋理座標系,此時圖片就已經出現了上下倒置,所以我們需要一個 flipY 的操作,在渲染的時候將上下再進行一次倒置。

// 翻轉圖片

gl。pixelStorei(gl。UNPACK_FLIP_Y_WEBGL, true);

Cocos Shader 基礎入門(四):紋理對映

可能會有些同學有疑問,為什麼 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;

}`;

Cocos Shader 基礎入門(四):紋理對映

再增加一點細節,就有一種鐳射卡的感覺了。

改變最終輸出的 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;

}`;

Cocos Shader 基礎入門(四):紋理對映

這個原理其實也就是將原來通道的顏色替換成另外一種顏色。

網上有很多紋理的應用例項,大家都可以嘗試著去改造一下。

其他

為什麼在 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 檔案即可測試。

Cocos Shader 基礎入門(四):紋理對映

注意:如果提示沒有 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