WebGL 處理圖片,來玩
“咕咕咕咕咕...”
天空中傳來了鴿群的聲音,隨之而來的是悅耳的鴿哨兒??

近日在 凹凸實驗室 公眾號看到一篇文章《GLSL 著色器,來玩》,你們都來玩,那我走?
玩笑話,其內(nèi)容還是有趣的,對于我這種 WebGL 青瓜蛋子還是十分友好;環(huán)境搭建直接使用了 ThreeJS,這樣就可以讓學(xué)習(xí)繁雜的 WebGL 基礎(chǔ) API 變成黑盒,讓新手更易學(xué)習(xí)。當(dāng)然如果想用原生 WebGL 標準 API 來繪制,可以看我公眾號的 WebGL 專欄。
既然那么好玩,索性跟風(fēng)也寫個著色器的小文章來玩吧??
小序
何為著色器,簡而言之即為自定義 GPU 處理圖形能力的程序,GLSL 基礎(chǔ)語法不在本文涉及范圍內(nèi),讀者可自行閱讀《GLSL ES 語法基礎(chǔ)》。
既然要玩,也要玩點東西出來,即要有點小成果;凹凸實驗室 文章中以動態(tài)渲染不同顏色為例,那本文就以渲染圖片為例,做一個可以切換圖片渲染效果的 Demo 吧(可以理解為切換濾鏡效果)。
Demo 所涉及 GLSL 基礎(chǔ)知識并未超出《GLSL ES 語法基礎(chǔ)》一文之所及,僅應(yīng)用了些許數(shù)學(xué)方面的小知識,從而實現(xiàn)圖片不同的渲染效果。
環(huán)境搭建
Demo 中并未使用任何第三方 WebGL 庫,只使用了自己用 TypeScript 重寫的 webgl-utils,整個項目框架使用了 webpack-react-template(兩個項目 GitHub 鏈接會附在文末)。要處理 GLSL 文件,故需對 webpack.config.js 進行些許修改:
// webpack.config.js
const config = {
// ...
module: {
rules: [
// ...
{
test: /\.(svg|glsl)$/,
issuer: /\.(js|ts)x?$/,
use: [
{
loader: "raw-loader",
},
],
},
],
},
};
僅需讓 raw-loader 處理一下 GLSL 文件,順便在 types.d.ts 中加入對 GLSL 文件的聲明即可:
declare module '*.glsl' {
export default '' as string;
}
程序主體
整個程序主體由下面幾行代碼組成:
const Main = ({ type }: Props) => {
const canvas = useRef<HTMLCanvasElement>({} as HTMLCanvasElement);
const [gl, setGL] = useState<WebGL2RenderingContext | null>(null);
const [program, setProgram] = useState<WebGLProgram | null>(null);
useEffect(() => {
canvas.current.width = Math.floor(window.innerWidth * 0.6);
canvas.current.height = Math.floor(window.innerHeight * 0.7);
const ctx = canvas.current.getContext('webgl2');
if (!ctx) {
throw new Error('Failed to get WebGL2 context');
}
setGL(ctx);
const p = createProgramFromSources(ctx, ShadersMap[type], [], []);
setProgram(p);
}, [type]);
const animation = useCallback(() => {
if (gl && program) {
void render(gl, program, Image);
}
}, [gl, program]);
requestAnimationFrame(() => {
animation();
});
return (
<canvas ref={canvas} id="canvas" />
);
};
而最重要的 render 職責(zé)就是加載并渲染圖片,WebGL 如何加載圖片,大家可以閱讀 《WebGL 紋理映射》。紋理映射中選用了 超級賽亞人之神 圖片,這次就選 我妻善逸 ,渲染后效果如下圖:

本次主要做了:交換紅藍通道、灰白、高斯模糊以及馬賽克四種效果,其中除了簡單的數(shù)學(xué)知識外,還涉及到了一個重要的知識點 切換著色器。以下例子中頂點著色器都無需修改,只需對片元著色器進行修改即可。
原圖
渲染原圖的頂點著色器和片元著色器很簡單:
// vertex-shader.glsl
#version 300 es
in vec2 a_Position;
in vec2 a_TexCoord;
uniform vec2 u_Resolution;
out vec2 v_TexCoord;
void main() {
vec2 zeroToOne = a_Position / u_Resolution;
vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
v_TexCoord = a_TexCoord;
}
上面有個小點需要注意,頂點著色器程序中將絕對的像素位置轉(zhuǎn)換到了 區(qū)間內(nèi),至于為何乘的為 而非 則取決于 gl.texParameteri 的設(shè)置。
// fragment-shader.glsl
#version 300 es
precision highp float;
uniform sampler2D u_Image;
in vec2 v_TexCoord;
out vec4 outColor;
void main() {
outColor = texture(u_Image, v_TexCoord);
}
切換紅藍通道
片元著色器中的顏色是 rgba 形式的 vec4 變量,交換紅藍通道只需按如下變量賦值即可:
// ...
void main() {
outColor = texture(u_Image, v_TexCoord).bgra;
}
渲染效果如下:

著實有些瘆人 :(
灰白圖
將彩色圖片轉(zhuǎn)成灰白圖片,只需將 rgb 三個通道的顏色值進行平均:
// ...
void main() {
vec4 color = texture(u_Image, v_TexCoord);
float average = (color.r + color.g + color.b) / 3.0;
outColor = vec4(average, average, average, color.a);
}

高斯模糊
高斯模糊(Gaussian Blur)是一種很常見的效果,通常用來降低噪聲或降低細節(jié)層次,我們可以在 PhotoShop 的專業(yè)繪圖軟件中見到其身影。
它使用正態(tài)分布計算圖像中每個像素的變換,分布不為 0 的像素組成的卷積矩陣與原圖像做變換,每個像素值都是周圍元素的加權(quán)平均。因為圖片是二維信息,所以要使用二維正態(tài)分布,如下圖:

(圖片來源:https://images0.cnblogs.com/blog/502930/201309/11201048-3f10d0cc1d9d4d65a7326e467fc5bc11.jpg)
原像素有最大的二維正態(tài)分布值,即有最大權(quán)重,故模糊后的像素最接近原像素,模糊后的整個圖像還能看出原圖像的影子。簡單來說,高斯模糊的過程就是原圖像與二維正態(tài)分布做卷積,不再展開講(因為我也不會)??
#version 300 es
precision highp float;
uniform sampler2D u_Image;
uniform vec2 u_Resolution;
in vec2 v_TexCoord;
out vec4 outColor;
// 每個像素的權(quán)重
// 最中間的為原像素,權(quán)重最高
float weight[9] = float[] (
0.0947416, 0.118318, 0.0947416,
0.118318, 0.147761, 0.118318,
0.0947416, 0.118318, 0.0947416
);
void main() {
vec4 color;
for(int i = 0; i < 9; i++) {
vec2 coord;
coord.x = v_TexCoord.x + float(i % 3 - 1) / u_Resolution.x;
coord.y = v_TexCoord.y + float(int(i / 3) - 1) / u_Resolution.y;
color = color + texture(u_Image, coord) * weight[i];
}
outColor = color;
}
對原像素方圓 1 像素的值做加權(quán)平均,效果如下圖:

仔細觀察還是可以發(fā)現(xiàn)差別的:

馬賽克
最后來講一個令人“厭惡”的效果 —— 馬賽克,好像任何東西打了碼之后就會變得很邪惡??

實現(xiàn)馬賽克這個效果需引入另一個概念 —— 噪聲,很容易理解就是多余不必要的干擾信息。實現(xiàn)也很簡單,我們只需生成一個“第三者”來“插足”原圖即可:
#version 300 es
precision highp float;
uniform sampler2D u_Image;
uniform vec2 u_Resolution;
in vec2 v_TexCoord;
out vec4 outColor;
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
vec2 st = v_TexCoord.xy / u_Resolution.xy * 20000.0;
vec2 ipos = floor(st);
vec3 color = vec3(random(ipos));
vec4 tex = texture(u_Image, v_TexCoord);
outColor = vec4(tex.rgb * 0.2 + color * 0.8, 1.0);
}
此處的“第三者”就是根據(jù)紋理像素坐標生成的 color,然后根據(jù)相應(yīng)權(quán)重與原圖疊加在一起:

其實真正的“打碼”并不是這種方式,打碼一般是將指定區(qū)域內(nèi)的內(nèi)容做加權(quán)平均,然后讓該區(qū)域內(nèi)的像素展示相同的顏色。比如我們以微信截圖的馬賽克為例:

我們使用截圖工具對 cdd(臭弟弟) 打碼后效果:

會發(fā)現(xiàn)截圖工具每次將“步兵”轉(zhuǎn)成“騎兵”后,每個馬賽克像素大小的大小以及位置都是相同的,并且針對于同一個圖片打碼后的效果也都是相同的。這就是對圖片的特定大小區(qū)域內(nèi)的內(nèi)容做加權(quán)平均并設(shè)為相同顏色后的效果。這種效果各位可以自己嘗試實現(xiàn)。
切換著色器??
上面我們編寫了五種不同的片元著色器,但如何在一個程序中使用這五種不同的著色器呢?

回想一下在哪兒我們用到了 Shader?是不是在 gl.attachShader 的時候?所以當(dāng)我們想使用不同的 Shader 時,我們直接使用新的 program 并 attachShader 即可(記得要 useProgram),效果如下:

篇末不點題
若 WebGL 基礎(chǔ) API 知識量為 1,則其所涉及的其他領(lǐng)域知識(諸如圖形學(xué)、數(shù)學(xué)、物理等)可能為 100 甚至更多,唯有興趣趨勢才有激情和動力前進。
最近項目較忙,故停更一陣,畢竟生活、工作才是重心;后續(xù)會繼如以往,分享感興趣、有趣、有用的內(nèi)容,但至于面試等相關(guān)文章,俯拾即是,不寫也罷。
課外輔導(dǎo)鏈接??
知識點學(xué)習(xí)??
高斯模糊:https://zh.wikipedia.org/wiki/%E9%AB%98%E6%96%AF%E6%A8%A1%E7%B3%8A 正態(tài)分布:https://zh.wikipedia.org/wiki/%E6%AD%A3%E6%80%81%E5%88%86%E5%B8%83 卷積:https://zh.wikipedia.org/wiki/%E5%8D%B7%E7%A7%AF
GitHub Repo
webgl-utils-ts:https://github.com/LiJiahaoCoder/webgl-utils-ts webpack-react-template:https://github.com/LiJiahaoCoder/webpack-react-template 本文示例源碼:https://github.com/LiJiahaoCoder/webgl-process-image
