渴望學習 Shader的夥伴有福了!高斯模糊詳解!

本文由“壹伴編輯器”提供技術支

前言

咳咳,上篇文章《爲什麼選擇 TypeScript ?》得到了許多朋友的認可,讓我動力滿滿,以後要加油寫出更多好文章分享給大家鴨!

客套話就不再多說了哈哈,今天給大家帶來的是高斯模糊在 Shader 中的實現!

> 這裏預告一下,Shader 入門系列文章《一起學 Shader》已經在積極籌劃中(文件夾已經建好了),感興趣的小夥伴關注一下啦~

本文由“壹伴編輯器”提供技術支

預覽

模糊前

模糊後

深度模糊後

本文由“壹伴編輯器”提供技術支

正文

高斯模糊

在我們開始討論代碼之前,我們要先稍微瞭解以下幾點...

> 下面的講解比較籠統,水平不夠,請見諒!

高斯模糊是什麼?

高斯模糊(Gaussian Blur),也叫高斯平滑,是一種生活中比較常見的圖像處理效果。

經過高斯模糊處理的圖像看起來就像是在一塊毛玻璃後面,也就是俗稱的“毛玻璃效果”。

高斯模糊也常用於處理噪點過高的圖像,使圖像看起來更平滑。

(神祕的微笑)

實現原理是什麼?

從數學的角度來看,高斯模糊的處理過程就是圖像與其正態分佈卷積

- 正態分佈

正態分佈(Normal distribution)是一種概率分佈,主要特徵爲集中性對稱性均勻變動性等。

因正態分佈又稱高斯分佈(Gaussian distribution),所以這種技術就叫做高斯模糊。

我們可以計算當前像素一定範圍內的像素的權重,越靠近當前像素權重越大,形成一個符合正態分佈的權重矩陣。

(圖片來源於網絡,侵刪)

- 卷積

卷積(Convolution)是一種積分變換的數學運算方法。

利用卷積算法,我們可以將當前像素的顏色與周圍像素的顏色按比例進行融合,得到一個相對均勻的顏色。

(圖片來源於網絡,侵刪)

- 卷積核

其中還涉及到一個名爲卷積核(Convolution kernel)的概念,卷積核一般爲矩陣,我們可以將它想象成卷積過程中使用的模板,模板中包含了當前像素周圍每個像素顏色的權重。

> 下圖中間那部分就是卷積核

(圖片來源於網絡,侵刪)

稍微總結

用大白話來解釋高斯模糊,就是採集當前像素一定範圍內的顏色,將採集到的顏色按比例進行合成(越靠近當前像素的顏色比例越高,也就是正態分佈的體現),得到一個比較均勻的顏色。

將圖像中的每個像素都按照上面的流程進行處理,最後就可以得到更爲平滑(模糊)的圖像。

當然採集的範圍越大,得到的圖像就會越模糊。

代碼實現

下面我將在 Cocos Creator 2.3.3 中實現一個高斯模糊的 Shader,除了前面部分屬性定義,核心的邏輯是通用的。

> Shader 文件已添加至 Eazax-CCC 項目,點擊文章底部“閱讀原文”即可獲取

完整代碼

// 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: '節點尺寸' } }
}%


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 循環的次數必須爲常量
  const float RADIUS = 20.0;

  // 獲取模糊顏色
  vec4 getBlurColor (vec2 pos) {
    vec4 color = vec4(0); // 初始顏色
    float sum = 0.0; // 總權重
    // 卷積過程
    for (float r = -RADIUS; r <= RADIUS; r++) { // 水平方向
      for (float c = -RADIUS; c <= RADIUS; c++) { // 垂直方向
        vec2 target = pos + vec2(r / size.x, c / size.y); // 目標像素位置
        float weight = (RADIUS - abs(r)) * (RADIUS - abs(c)); // 計算權重
        color += texture2D(texture, target) * weight; // 累加顏色
        sum += weight; // 累加權重
      }
    }
    color /= sum; // 求出平均值
    return color;
  }
 
  void main () {
    vec4 color = getBlurColor(v_uv0); // 獲取模糊後的顏色
    color.a = v_color.a; // 還原透明度
    gl_FragColor = color;
  }
}%

代碼分析

- CCEffect

首先頭部是平平無奇的 YAML 格式的屬性定義代碼塊。唯一特別的地方就是多了個 size 屬性,用於輸入作用節點的尺寸

properties:
  size: { value: [500.0, 500.0], editor: { tooltip: '節點尺寸' } }

你可能會好奇(也許不會)爲什麼要傳入節點尺寸,這裏稍微說明一下:

1. 在片段着色器階段的頂點座標用視口座標(Viewport Coordinates)表示,視口座標是標準化(Normalize)後的屏幕座標(Screen Coordinates),其可用範圍是(0.0, 0.0)到(1.0, 1.0),原點爲左下角。

例如:屏幕正中間的視口座標應爲(0.5, 0.5)。

2. 我們傳入尺寸的目的就是便於我們計算頂點的實際位置。

例如:在一個 720 x 1280 的屏幕中,像素與像素之間的水平距離爲 1.0 / 720.0,垂直距離爲 1.0 / 1280.0。

- 頂點着色器(Vertex Shader)

緊跟其後的是一個平平無奇的頂點着色器,未對頂點作任何特殊處理,直接將頂點座標以及顏色信息傳遞給下一個着色器。

> 這部分代碼在上面完整代碼裏有,我這裏就不貼了,因爲實在是太平平無奇了...

> 不如貼個貓包(貓貓表情包)緩和一下氣氛吧~

- 片段着色器(Fragment Shader)

> 重頭戲來了!(敲黑板)

1. 首先我們拿到了從頂點着色器傳遞過來的頂點座標顏色信息,另外還接收到了 texture 和 size 屬性。

in vec2 v_uv0;
in vec4 v_color;

uniform sampler2D texture;

// 接收傳入的 size 屬性
uniform Properties {
  vec2 size;
};

2. 接着定義了一個常量 RADIUS 來表示模糊採樣的半徑,半徑越大,採樣的顏色越多,圖像也就越模糊

> 在 GLSL 中循環的次數必須爲常量,因爲循環語句會被展開爲原生 GPU 指令,所以必須確定循環展開次數,Shader 編譯器才能正確地生成 GPU 指令。

const float RADIUS = 20.0;

然後定義了一個函數 getBlurColor 來獲取模糊後的顏色,該函數接收一個頂點座標作爲參數,經卷積加權平均計算後返回最終顏色。(詳細過程請看註釋)

// 獲取模糊顏色
vec4 getBlurColor (vec2 pos) {
  vec4 color = vec4(0); // 初始顏色
  float sum = 0.0; // 總權重
  // 卷積過程
  for (float r = -RADIUS; r <= RADIUS; r++) { // 水平方向
    for (float c = -RADIUS; c <= RADIUS; c++) { // 垂直方向
      vec2 target = pos + vec2(r / size.x, c / size.y); // 目標像素位置
      float weight = (RADIUS - abs(r)) * (RADIUS - abs(c)); // 計算權重
      color += texture2D(texture, target) * weight; // 累加顏色
      sum += weight; // 累加權重
    }
  }
  color /= sum; // 求出一個平均值
  return color;
}

3. 然後是着色器的主函數,在獲取到模糊的顏色之後,將顏色透明度還原爲輸入的透明度,最後將舞臺交還給渲染管線。

void main () {
  vec4 color = getBlurColor(v_uv0); // 獲取模糊後的顏色
  color.a = v_color.a; // 還原透明度
  gl_FragColor = color;
}

本文由“壹伴編輯器”提供技術支

結束語

以上內容皆爲陳皮皮的個人觀點。

文采不佳,如果寫得不好還請各位多多包涵。如果有哪些地方說的不對,還請各位指出,希望與大家共同進步。

接下來我會持續分享自己所學的知識與見解,歡迎各位關注本公衆號。

我們,下次見!

本文由“壹伴編輯器”提供技術支

掃描二維碼

獲取更多精彩

菜鳥小棧

點擊 閱讀原文 獲取 Shader 文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章