前言
接觸過Canvas的小夥伴應該都知道,在Canvas2D中我們要載入一個圖片很簡單,透過呼叫drawImage
API就能將影像繪製到畫布上,當然在WebGL中我們也可以繪製影像,在繪製時我們需要用到WebGL中的紋理物件,在之前WebGL實現網格背景的文章中,我使用了一個叫做紋理座標的配置,現在要完成紋理的載入我們也需要用到紋理座標,並且我們可以透過對紋理座標處理實現簡單的”馬賽克“效果。透過對紋理的使用學習,我感覺自己對紋理座標的認知,和之前學習網格背景時,有點不一樣了,這大概就是學習的過程吧,不斷更新自己的認知。
接下來我會介紹紋理的基礎使用,並在此基礎上實現簡單的區域性“馬賽克”效果。
建立紋理並繫結到上下文
在Shader中使用紋理之前,我們需要先在JavaScript中建立紋理物件。
// 建立紋理物件
const texture = gl.createTexture();
// 座標翻轉
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
// 將紋理繫結到當前上下文
gl.bindTexture(gl.TEXTURE_2D, texture);
// 在圖片載入完畢後,指定紋理影像
gl.texImage2D(
gl.TEXTURE_2D,
level,
internalFormat,
srcFormat,
srcType,
image,
);
// 設定紋理的一些引數
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);
上述程式碼中pixelStorei
方法設定了畫素儲存格式,它的作用是將圖片座標進行翻轉,因為GIF、JPEG和PNG圖片使用的座標系統以左上角為原點,X軸水平向右,Y軸垂直向下,而紋理座標是左下角為原點,Y軸垂直向上,兩者Y軸的方向相反,所以為了使圖片在WebGL中正常顯示,需要這一步操作。
還有一點需要注意的是,在WebGL中使用的圖片需要和當前頁面同源。
使用紋理
完成紋理的建立後,我們就可以透過紋理單元編號啟用指定紋理,來在WebGL中使用。
// 按照單元編號啟用紋理
gl.activeTexture(gl.TEXTURE0);
// 將紋理繫結到上下文
gl.bindTexture(gl.TEXTURE_2D, texture);
// 獲取Shader中紋理變數
const loc = gl.getUniformLocation(program, "tMap");
// 將對應的紋理單元寫入Shader變數
gl.uniform1i(loc, 0);
預設情況下WebGL會使用第一個紋理,所以如果我們只有一個紋理的話,不呼叫activeTexture
方法也能正常使用紋理。
啟用紋理後,我們就可以將對應的紋理單元寫入Shader變數,這樣就可以在Shader中使用紋理了,可以看到這裡向Shader傳遞的並不是紋理物件,而是紋理單元編號。
載入紋理
等到JavaScript傳遞紋理資訊後,Shader就可以使用這個紋理了。
precision mediump float; // 新增如下精度描述
uniform sampler2D tMap; // 紋理相關
varying vec2 vUv;
void main() {
gl_FragColor = texture2D(tMap, vUv);
}
在片元著色器中,我們使用sampler2D型別的變數接收紋理資訊。
可以看到在GLSL程式碼中,我們使用了一個叫texture2D的函式,這個函式的作用是根據紋理座標,從紋理中提取顏色。
透過對紋理的簡單使用,我目前的理解是,紋理座標是與頂點座標存在一個對應的關係。
// ...
let vertices = new Float32Array([ // 頂點座標
[-1, -1],
[-1, 1],
[1, 1],
[1, -1]
].flat()),
// ...
let vertices = new Float32Array([ // 紋理座標
[0, 0],
[0, 1],
[1, 1],
[1, 0]
].flat()),
兩組座標是對應的,所以在頂點著色器中雖然我們只是指定了頂點,但根據對應關係,此時頂點對應的紋理座標也是已知的。
因為前面對紋理的引數設定(gl.CLAMP_TO_EDGE
),紋理是拉伸鋪在整個紋理座標上。(我還沒深入學習紋理的引數,這裡我只是根據單詞意思做的猜測。)
所以就可以透過texture2D
函式根據紋理座標提取到影像對應位置的畫素資訊了,也就是顏色色值,並將它賦值給gl_FragColor常量、給片元上色。
至此我們就實現了簡單的紋理載入,將影像繪製到WebGL的畫布上了。
接下去我們就來實現圖片的區域性“馬賽克”效果。
因為對於紋理的具體使用步驟我們已經知道了,所以在接下去的例子中,我就使用課程提供的gl-renderer
庫來簡化紋理的載入使用操作,專注於效果的實現。
實現區域性“馬賽克”
在處理照片時,我們常常需要將一些敏感的或者是不想展示的資訊使用馬賽克的效果處理掉,那麼在WebGL中我們要怎麼去實現呢?
-
首先我們設定馬賽克效果的中心點,對應的是紋理座標的值。
renderer.uniforms.center = [-2.0, -2.0];
初始中心點隨意設定一個在0-1之外的位置。
-
接著設定馬賽克的範圍。
const radiusPX = 100; renderer.uniforms.radiusX = radiusPX / canvasRef.value.width; renderer.uniforms.radiusY = radiusPX / canvasRef.value.height;
我們將馬賽克的半徑範圍設定為100px,並將它轉換為WebGL內對應的數值,使用uniform傳遞給Shader。
-
然後我們新增滑鼠點選事件的監聽。
const clickHandler = e => { e.preventDefault(); const {width, height} = canvasRef.value.getBoundingClientRect(); const {offsetX: x, offsetY: y} = e; // 轉換為紋理座標上的值 const center = []; center[0] = x / width; center[1] = (height - y) / height; renderer.uniforms.center = center; };
offsetX和offsetY分別表示滑鼠位置距離元素左邊和頂部的距離,所以
height-y
表示滑鼠位置距離元素底部多遠,透過分別除以寬和高獲得滑鼠在WebGL紋理座標上的值。這樣透過監聽滑鼠點選事件,我們就可以動態更新馬賽克的位置。
-
完成uniform的傳遞後,我們就可以在片元著色器中使用了。
首先我們對片元對應的紋理座標進行縮放,X座標放大50倍,Y座標放大27.7倍,與畫布的寬高比例一致,得到50乘以27.7,也就是1350個20x20大小的方格;同時獲取到X和Y座標的整數部分,整數部分相當於片元所在方格在橫縱座標方向的索引。
vec2 st = vUv * vec2(50, 27.7); vec2 uv = floor(st);
接著根據原始紋理座標的位置與中心點的距離,我們使用橢圓的公式來判斷片元是否在馬賽克範圍內。(因為畫布寬高不一樣,所以這裡我們用橢圓公式判斷。)
// 中心點座標 float x0 = center.x; float y0 = center.y; if (pow(abs(vUv.x - x0), 2.0) / pow(radiusX, 2.0) + pow(abs(vUv.y - y0), 2.0) / pow(radiusY, 2.0) <= 1.0) { color = texture2D(tMap, vec2(uv.x / 50.0, uv.y / 27.7)); } else { color = texture2D(tMap, vUv); }
如果是在範圍內的片元,我們就將方格的索引進行縮放,對應到原始紋理座標上的值,因為一個方格內所有的片元對應的索引值一致,所以按照這個值提取到的顏色是一樣的,也就是一個方格內是一種顏色。
如果不在馬賽克範圍內,就按照普通的提取紋理色值的方式。
-
最後就是將顏色值賦值給常量
gl_FragColor
,完成了給片元上色。
到這裡我們就實現了一個簡單的馬賽克效果,可以透過點選滑鼠給圖片指定位置新增馬賽克效果,是一個圓形的樣子,我們可以透過使用不同的公式判斷,呈現不同的馬賽克形狀,比如正方形。
總結
在WebGL中紋理也是比較重要的內容,可以讓我們使用圖片,最早我是在JavaScript高階程式設計這本書中接觸到紋理的,但是因為書裡給出的程式碼並不完整,並且我當時也沒去深入瞭解,所以當時的程式碼並沒有跑起來,現在我透過學習一個視覺化教程才知道說紋理要怎麼去用,瞭解到透過不同引數的設定可以實現不同的紋理表現,呈現不同的視覺效果,本期內容只是簡單的紋理使用,對紋理感興趣的小夥伴可以再自己深入研究。