前言
咳咳,上篇文章《為什么選擇 TypeScript 娄琉?》得到了許多朋友的認(rèn)可次乓,讓我動(dòng)力滿滿,以后要加油寫出更多好文章分享給大家鴨孽水!
客套話就不再多說了哈哈票腰,今天給大家?guī)淼氖?strong>高斯模糊在 Shader 中的實(shí)現(xiàn)!
這里預(yù)告一下女气,Shader 入門系列文章已經(jīng)在積極籌劃中(文件夾已經(jīng)建好了)杏慰,感興趣的小伙伴關(guān)注一下啦~
預(yù)覽
模糊前
模糊后
深度模糊后
正文
高斯模糊
在我們開始討論代碼之前,我們要先稍微了解以下幾點(diǎn)...
下面的講解比較籠統(tǒng)主卫,水平不夠逃默,請(qǐng)見諒!
高斯模糊是什么簇搅?
高斯模糊(Gaussian Blur)完域,也叫高斯平滑,是一種生活中比較常見的圖像處理效果瘩将。
經(jīng)過高斯模糊處理的圖像看起來就像是在一塊毛玻璃后面吟税,也就是俗稱的“毛玻璃效果”。
高斯模糊也常用于處理噪點(diǎn)過高的圖像姿现,使圖像看起來更平滑肠仪。
實(shí)現(xiàn)原理是什么?
從數(shù)學(xué)的角度來看备典,高斯模糊的處理過程就是圖像與其正態(tài)分布做卷積异旧。
- 正態(tài)分布
正態(tài)分布(Normal distribution)是一種概率分布振劳,主要特征為集中性 炭玫、對(duì)稱性和均勻變動(dòng)性等裙椭。
因正態(tài)分布又稱高斯分布(Gaussian distribution)捌朴,所以這種技術(shù)就叫做高斯模糊溪烤。
我們可以計(jì)算當(dāng)前像素一定范圍內(nèi)的像素的權(quán)重稀火,越靠近當(dāng)前像素權(quán)重越大坏晦,形成一個(gè)符合正態(tài)分布的權(quán)重矩陣雌澄。
- 卷積
卷積(Convolution)是一種積分變換的數(shù)學(xué)運(yùn)算方法倚喂。
利用卷積算法每篷,我們可以將當(dāng)前像素的顏色與周圍像素的顏色按比例進(jìn)行融合,得到一個(gè)相對(duì)均勻的顏色端圈。
- 卷積核
其中還涉及到一個(gè)名為 卷積核(Convolution kernel)的概念焦读,卷積核一般為矩陣,我們可以將它想象成卷積過程中使用的模板舱权,模板中包含了當(dāng)前像素周圍每個(gè)像素顏色的權(quán)重矗晃。
下圖中間的那部分就是卷積核
稍微總結(jié)
用大白話來解釋高斯模糊,就是采集當(dāng)前像素一定范圍內(nèi)的顏色刑巧,將采集到的顏色按比例進(jìn)行合成(越靠近當(dāng)前像素的顏色比例越高喧兄,也就是正態(tài)分布的體現(xiàn))无畔,得到一個(gè)比較均勻的顏色。
將圖像中的每個(gè)像素都按照上面的流程進(jìn)行處理吠冤,最后就可以得到更為平滑(模糊)的圖像浑彰。
當(dāng)然采集的范圍越大,得到的圖像就會(huì)越模糊拯辙。
代碼實(shí)現(xiàn)
下面我將在 Cocos Creator 2.3.3 中實(shí)現(xiàn)一個(gè)高斯模糊的 Shader郭变,除了前面部分屬性定義,核心的邏輯是通用的涯保。
Shader 文件已添加至 Eazax-CCC 項(xiàng)目诉濒,這里是 傳送門
完整代碼
// Eazax-CCC 高斯模糊 1.0.0.20200523
CCEffect %{
techniques:
- passes:
- vert: vs
frag: fs
blendState:
targets:
- blend: true
rasterizerState:
cullMode: none
properties:
size: { value: [500.0, 500.0], editor: { tooltip: '節(jié)點(diǎn)尺寸' } }
}%
CCProgram vs %{
precision highp float;
#include <cc-global>
in vec3 a_position;
in vec2 a_uv0;
in vec4 a_color;
out vec2 v_uv0;
out vec4 v_color;
void main () {
gl_Position = cc_matViewProj * vec4(a_position, 1);
v_uv0 = a_uv0;
v_color = a_color;
}
}%
CCProgram fs %{
precision highp float;
in vec2 v_uv0;
in vec4 v_color;
uniform sampler2D texture;
uniform Properties {
vec2 size;
};
// 模糊半徑
// for 循環(huán)的次數(shù)必須為常量
const float RADIUS = 20.0;
// 獲取模糊顏色
vec4 getBlurColor (vec2 pos) {
vec4 color = vec4(0); // 初始顏色
float sum = 0.0; // 總權(quán)重
// 卷積過程
for (float r = -RADIUS; r <= RADIUS; r++) { // 水平方向
for (float c = -RADIUS; c <= RADIUS; c++) { // 垂直方向
vec2 target = pos + vec2(r / size.x, c / size.y); // 目標(biāo)像素位置
float weight = (RADIUS - abs(r)) * (RADIUS - abs(c)); // 計(jì)算權(quán)重
color += texture2D(texture, target) * weight; // 累加顏色
sum += weight; // 累加權(quán)重
}
}
color /= sum; // 求出平均值
return color;
}
void main () {
vec4 color = getBlurColor(v_uv0); // 獲取模糊后的顏色
color.a = v_color.a; // 還原透明度
gl_FragColor = color;
}
}%
代碼分析
- CCEffect
首先頭部是平平無奇的 YAML 格式的屬性定義代碼塊。唯一特別的地方就是多了個(gè) size 屬性夕春,用于輸入作用節(jié)點(diǎn)的尺寸未荒。
properties:
size: { value: [500.0, 500.0], editor: { tooltip: '節(jié)點(diǎn)尺寸' } }
你可能會(huì)好奇(也許不會(huì))為什么要傳入節(jié)點(diǎn)尺寸,這里稍微說明一下:
在片段著色器階段的
uv
坐標(biāo)為紋理坐標(biāo)(Texture Coordinate)及志,其可用范圍是(0.0, 0.0)到(1.0, 1.0)片排,原點(diǎn)為左下角。例如:屏幕正中間的像素坐標(biāo)為(0.5, 0.5)速侈。
我們傳入尺寸的目的就是便于我們計(jì)算頂點(diǎn)的實(shí)際位置率寡。
例如:在一個(gè) 720 x 1280 的屏幕中,像素與像素之間的水平距離為 1.0 / 720.0倚搬,垂直距離為 1.0 / 1280.0冶共。
- 頂點(diǎn)著色器(Vertex Shader)
緊跟其后的是一個(gè)平平無奇的頂點(diǎn)著色器,未對(duì)頂點(diǎn)作任何特殊處理每界,直接將頂點(diǎn)坐標(biāo)以及顏色信息傳遞給下一個(gè)著色器捅僵。
這部分代碼在上面完整代碼里有,我這里就不貼了盆犁,因?yàn)閷?shí)在是太平平無奇了...
不如貼個(gè)貓包(貓貓表情包)緩和一下氣氛吧~
- 片段著色器(Fragment Shader)
重頭戲來了C馈(敲黑板)
- 首先我們拿到了從頂點(diǎn)著色器傳遞過來的頂點(diǎn)坐標(biāo) 和顏色信息 篡九,另外還接收到了 texture 和 size 屬性谐岁。
in vec2 v_uv0;
in vec4 v_color;
uniform sampler2D texture;
// 接收傳入的 size 屬性
uniform Properties {
vec2 size;
};
- 接著定義了一個(gè)常量 RADIUS 來表示模糊采樣的半徑,半徑越大榛臼,采樣的顏色越多伊佃,圖像也就越模糊。
在 GLSL 中循環(huán)的次數(shù)必須為常量沛善,因?yàn)檠h(huán)語句會(huì)被展開為原生 GPU 指令航揉,所以必須確定循環(huán)展開次數(shù),Shader 編譯器才能正確地生成 GPU 指令金刁。
const float RADIUS = 20.0;
然后定義了一個(gè)函數(shù) getBlurColor 來獲取模糊后的顏色帅涂,該函數(shù)接收一個(gè)頂點(diǎn)坐標(biāo)作為參數(shù)议薪,經(jīng)卷積加權(quán)平均計(jì)算后返回最終顏色。(詳細(xì)過程請(qǐng)看注釋)
// 獲取模糊顏色
vec4 getBlurColor (vec2 pos) {
vec4 color = vec4(0); // 初始顏色
float sum = 0.0; // 總權(quán)重
// 卷積過程
for (float r = -RADIUS; r <= RADIUS; r++) { // 水平方向
for (float c = -RADIUS; c <= RADIUS; c++) { // 垂直方向
vec2 target = pos + vec2(r / size.x, c / size.y); // 目標(biāo)像素位置
float weight = (RADIUS - abs(r)) * (RADIUS - abs(c)); // 計(jì)算權(quán)重
color += texture2D(texture, target) * weight; // 累加顏色
sum += weight; // 累加權(quán)重
}
}
color /= sum; // 求出一個(gè)平均值
return color;
}
- 然后是著色器的主函數(shù)媳友,在獲取到模糊的顏色之后斯议,將顏色透明度還原為輸入的透明度,最后將舞臺(tái)交還給渲染管線醇锚。
void main () {
vec4 color = getBlurColor(v_uv0); // 獲取模糊后的顏色
color.a = v_color.a; // 還原透明度
gl_FragColor = color;
}
傳送門
更多分享
公眾號(hào)
菜鳥小棧
我是陳皮皮,這是我的個(gè)人公眾號(hào)焊唬,專注但不僅限于游戲開發(fā)恋昼、前端和后端技術(shù)記錄與分享。
每一篇原創(chuàng)都非常用心赶促,你的關(guān)注就是我原創(chuàng)的動(dòng)力液肌!
Input and output.