寫在前面
[上一篇筆記](/articles/varying 變量與內插-webgl 筆記 7/) 中,我們利用 varying 變量把色值傳遞到片元著色器,實現了漸變效果,而更有用的顏色設置方法就是貼圖(紋理映射)
一。原理
貼圖就是把圖片中的顏色映射到幾何圖形上去,步驟如下:
-
在頂點著色器中為每個頂點指定紋理坐標
-
然後在片元著色器中根據每個片元的紋理坐標從紋理圖像中抽取紋素顏色
這裡仍然是利用 varying 變量傳遞紋理坐標的,而且因為有內插過程,圖片才能完美地罩在幾何圖形上
紋理坐標是一種新坐標系,與 canvas 和 WebGL 坐標系都不一樣,原點在 canvas 左下角,s 軸向右為正,t 軸向上為正,canvas 右上角坐標為 (1.0, 1.0)
二。紋理映射
特別注意:圖像格式沒有限制,可以使用瀏覽器支持的任意格式圖像,但作為紋理的圖片的尺寸必須是 2^mx2^n 的,否則會報錯:
WebGL: drawArrays: texture bound to texture unit 0 is not renderable. It maybe non-power-of-2 and have incompatible texture filtering or is not 'texture complete'. Or the texture is Float or Half Float type with linear filtering while OES_float_linear or OES_half_float_linear extension is not enabled.
這個叫NPOT 問題(Non Power Of Two),是貼圖的常識,非 2^mx2^n 尺寸的圖也能貼,但通常都更複雜更耗性能
1. 設置紋理坐標
建立紋理坐標與 WebGL 坐標的聯繫,設置頂點對應的紋理坐標,然後片元著色器執行時通過 gl_FragColor = texture2D(u_Sampler, v_TexCoord); 抽取紋素設置片元顏色,圖就貼上去了
著色器源程序如下:
// 頂點著色器源程序
var vsSrc = 'attribute vec4 a_Position;' +
'attribute vec2 a_TexCoord;' + // 接受紋理坐標
'varying vec2 v_TexCoord;' + // 傳遞紋理坐標
'void main() {' +
'gl_Position = a_Position;' + // 設置坐標
'v_TexCoord = a_TexCoord;' + // 設置紋理坐標
'}';
// 片元著色器源程序
//!!! 需要聲明浮點數精度,否則報錯 No precision specified for (float)
var fsSrc = 'precision mediump float;' +
'uniform sampler2D u_Sampler;' + // 取樣器
'varying vec2 v_TexCoord;' + // 接受紋理坐標
'void main() {' +
'gl_FragColor = texture2D(u_Sampler, v_TexCoord);' + // 設置顏色
'}';
其中 sampler2D 是取樣器類型,圖片紋理最終存儲在該類型對象中
片元著色器中通過 texture2D(sampler2D sampler, vec2 coord) 來抽取紋素顏色,第一個參數是紋理單元編號,第二個參數是紋理坐標,返回值由給 gl.texImage2D 傳入的 internalformat 參數決定,如果紋理圖像不可用,就返回 vec4(0.0, 0.0, 0.0, 1.0)
2. 配置和加載紋理
加載圖片的方式仍然是 new Image,但 WebGL不允許使用跨域紋理圖像,具體步驟如下:
-
創建紋理對象
// 創建 texture var texture = gl.createTexture(); // 創建紋理對象 -
創建 image
var image = new Image(); -
給 image 添加 load 事件處理器,在事件處理器中配置紋理
image.onload = function() { //--- 加��紋理 // 1. 對紋理圖像進行 y 軸反轉 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 2. 開開啟 0 號紋理單元 gl.activeTexture(gl.TEXTURE0); // 3. 向 target 綁定紋理對象 gl.bindTexture(gl.TEXTURE_2D, texture); // 4. 配置紋理參數 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // 用圖片邊緣顏色填充空白區域 // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); // 鏡像填充(軸對稱) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); // 5. 配置紋理圖像 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); // 6. 將 0 號紋理傳遞給著色器 gl.uniform1i(u_Sampler, 0); // 繪製矩形 gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLE_STRIP, 0, arrVtx.length / 4); };
配置紋理的過程比較複雜,待會兒再詳細展開
-
給 image.src 賦值加載圖像
image.src = 'miao256x128.png';
注意圖片尺寸,錯誤信息中的 non-power-of-2 告訴我們必須使用 2^mx2^n 尺寸的圖片作為紋理
3. 內部狀態
如圖:
[caption id="attachment_917" align="alignnone" width="605"]
webgl-texture[/caption]
三。配置紋理
1. 對紋理圖像進行 y 軸反轉
// 1. 對紋理圖像進行 y 軸反轉
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
如果不反轉就會發現貼好的圖片是倒的,就像貼「福」字一樣,因為 WebGL 紋理坐標系統中的 t 軸和圖像坐標系統的 y 軸是方向相反。圖像坐標系是我們遇到的第 4 個坐標系,坐標軸/原點與 canvas 坐標系一樣
當然,也可以手動反轉 y 坐標,在片元著色器裡讓 y = 1.0 - y 即可,或者,也可以倒著貼。。但為什麼不用這麼方便的函數呢?
gl.pixelStorei(pname, param)
---
pname
gl.UNPACK_FLIP_Y_WEBGL 對圖像進行 y 軸反轉
gl.UNPACK_PERMULTIPLY_ALPHA_WEBGL 給圖像 rgb 色值的每一個分量乘以 A
param
0 或者非 0 整數
注意:參數值只能是整數,而不是 true/false
2. 開啟 x 號紋理單元
// 2. 開開啟 0 號紋理單元
gl.activeTexture(gl.TEXTURE0);
WebGL 通過紋理單元的機制來同時使用多個紋理,每個紋理單元管理一張紋理圖像,gl.TEXTURE0~7 一共 8 個,也就是說最多只能同時管理 8 張紋理圖像
使用紋理單元之前,要通過 gl.activeTexture(gl.TEXTURE0); 來激活它
3. 綁定紋理對象
// 3. 向 target 綁定紋理對象
gl.bindTexture(gl.TEXTURE_2D, texture);
告訴 WebGL 系統紋理對象使用的是哪種類型的紋理,支持 2 種類型紋理:gl.TEXTURE_2D、gl.TEXTURE_CUBE_MAP,類似於緩衝區操作,同樣是指定 target,參數個數及含義也類似
同樣,在 WebGL 中也無法直接操作紋理對象,必須通過將紋理對象綁定到紋理單元上,然後通過操作紋理單元來操作紋理對象
4. 配置紋理參數
// 4. 配置紋理參數
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 用圖片邊緣顏色填充空白區域
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
// 鏡像填充(軸對稱)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
設置紋理圖像映射到圖像上的具體方式,如何根據紋理坐標獲取紋素顏色?按哪種方式重複填充紋理?也就是如何內插生成片元
texParameteri(target, pname, param)
---
target
紋理類型,值為 gl.TEXTURE_2D 或者 gl.TEXTURE_CUBE_MAP
pname
紋理參數,值為 gl.TEXTURE_MAG_FILTER 表示紋理尺寸小於圖形尺寸時如何放大紋理,默認值是 gl.LINEAR
或者 gl.TEXTURE_MIN_FILTER 表示紋理尺寸大於圖形尺寸時如何縮小紋理,默認值是 gl.NEAREST_MIPMAP_LINEAR
或者 gl.TEXTURE_WRAP_S 表示如何填充紋理圖像左側或者右側的區域,默認值是 gl.REPEAT
或者 gl.TEXTURE_WRAP_T 表示如何填充紋理圖像上方和下方的區域,默認值是 gl.REPEAT
param
參數值,gl.TEXTURE_MAG_FILTER 和 gl.TEXTURE_MIN_FILTER 可選值為 gl.NEAREST 和 gl.LINEAR,前者表示使用紋理上距映射後像素中心最近的那個像素的顏色值,後者表示使用距新像素中心最近的 4 個像素的顏色值的加權平均,效果更好,但開銷比較大。這 2 種是非金字塔紋理,還有金字塔紋理(不常用,不做介紹)
gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 可選值為 gl.REPEAT 平鋪、gl.MIRRORED_REPEAT 鏡像平鋪、gl.CLAMP_TO_EDGE 使用紋理圖像邊緣值
因為 gl.TEXTURE_MIN_FILTER 的默認值是特殊的金字塔紋理,所以要做修改 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);,不改的話會報錯,且不顯示紋理
5. 配置紋理圖像
// 5. 配置紋理圖像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
把紋理圖像分配給紋理對象,同時告訴 WebGL 系統關於紋理圖像的一些特性
gl.texImage2D(target, level, internalformat, format, type, image)
---
target
紋理類型,值為 gl.TEXTURE_2D 或者 gl.TEXTURE_CUBE_MAP
level
傳入 0,該參數是為金字塔紋理準備的,一般不用這個參數
internalformat
圖像的內部格式,必須與 format 取值相同
format
圖像的外部格式,根據圖像格式來定 jpg/bmp 用 gl.RGB、png 用 gl.RGBA、灰度圖用 gl.LUMINANCE 或 gl.LUMINANCE_ALPHA,此外還有 gl.ALPHA
type
紋理數據的類型,通常使用 gl.UNSIGNED_BYTE,可選值還有 gl.UNSIGNED_SHORT_5_6_5、gl.UNSIGNED_SHORT_4_4_4_4、gl.UNSIGNED_SHORT_5_5_5_1
image
包含紋理圖像的 image 對象
6. 把 x 號紋理單元傳遞給片元著色器中的取樣器變量
// 6. 將 0 號紋理傳遞給著色器
gl.uniform1i(u_Sampler, 0);
本例中取樣器聲明為 uniform sampler2D 類型,uniform 是因為紋理圖像不隨著片元變化,sampler2D 對應 gl.TEXTURE_2D,而 samplerCube 用於 gl.TEXTURE_CUBE_MAP 類型紋理
通過 gl.uniform1i(u_Sampler, 0); 給 sampler2D 變量賦值,1 表示傳入分量個數,i 表示分量是 int 類型,這是 WebGL 慣用的命名方式,比如 gl.uniform4fv(name, value) 表示 value 的值應該是 4 維向量(有 4 個元素的類型化數組)
四。DEMO
包含上述代碼的完整的例子,請查看:
多圖片紋理是指把多張圖片貼在同一塊區域,比如:
[caption id="attachment_918" align="alignnone" width="218"]
webgl-texture-multi-image-example[/caption]
疊加效果是 2 個紋素色值分量相乘的結果,vec4(r1, g1, b1, a1) * vec4(r2, g2, b2, a2),結果矢量是 vec4(r1 * r2, g1 * g2, b1 * b2, a1 * a2),所以色值左乘右乘效果一樣
多圖片紋理的內部狀態如圖:
[caption id="attachment_919" align="alignnone" width="604"]
webgl-texture-multi-image[/caption]
五。總結
紋理映射的原理很簡單:從紋理圖片中讀取顏色,再賦值給片元,varying 變量內插保證了著色均勻
但操作比較複雜,所以我們再次封裝了 util,接口說明如下:
/**
* load texture image
* by default, configure gl.TEXTURE_MIN_FILTER as gl.LINEAR
*!!! can be overrode at callback
* @param {String} imgPath texture image path
* @param {Function} callback(image, unit) args: image object and texture unit number
* @return
* @throws {Error} If all texture unit was active now
*/
function loadTexture(imgPath, callback) {
//...
}
利用 callback 保證了靈活性,具體實現細節請查看 gl-util.js
至此,WebGL 最最基礎的東西就結束了,下一篇筆記是關於 GLSL ES 的詳細說明,再然後就是無盡的矩陣漩渦。。
參考資料
- WebGL 編程指南》
暫無評論,快來發表你的看法吧