視覺化學習:如何使用後期處理通道增強影像效果

beckyye發表於2024-06-14

前言

大家好,本文分享的是如何使用後期處理通道增強影像效果,透過前面幾篇文章,我們瞭解了一些動態生成紋理的方法,比如符號距離場SDF、基於引數方程生成圖案、基於噪聲生成紋理,等等。這些生成紋理的技術有相似的地方,就是根據片元的紋理座標,對片元著色,直接生成紋理。

因為GPU是並行渲染的,每個畫素的著色器程式是並行執行的,這樣的渲染很高效。但是在實際需求中,有時我們計算片元色值時,需要依賴周圍畫素點或者某個其他位置畫素點的顏色資訊,這樣的話想要一次性完成繪製就無法做到了。我們需要先透過第一次的繪製,來得到動態生成的紋理,接著我們才能根據紋理座標獲取到這個紋理上任一位置的顏色資訊,再做後續處理。也就是我們至少要執行兩次處理,才能實現我們最終想要的效果。

那麼具體要怎麼做呢,下面我就用一個高斯模糊的例子來進行演示。

高斯模糊的例子

假設我們透過以下Shader程式碼繪製了隨機的三角形圖案。

const fragment = `
  #ifdef GL_ES
  precision highp float;
  #endif

  varying vec2 vUv;

  ${distance.base}

  ${noise.random2d}

  ${color.hsb}

  void main() {
    vec2 st = vUv;
    st *= 10.0;
    vec2 i_st = floor(st);
    vec2 f_st = 2.0 * fract(st) - vec2(1);
    float r = random(i_st);
    float sign = 2.0 * step(0.5, r) - 1.0;

    float d = triangle_distance(
      f_st,
      vec2(-1),
      vec2(1),
      sign * vec2(1, -1)
    );
    gl_FragColor.rgb = (smoothstep(-0.85, -0.6, d) - smoothstep(0.0, 0.05, d)) * hsb2rgb(vec3(r + 1.2, 0.5, r));
    gl_FragColor.a = 1.0;
  }
`;

image

以上就是動態生成的紋理,在生成的過程中我們無法直接給紋理新增高斯模糊的濾鏡。

為了使用這個第一次渲染的結果,我們需要準備一個新的片元著色器。

## blurFragment
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;
uniform sampler2D tMap;

void main() {
  vec4 color = texture2D(tMap, vUv);

  gl_FragColor.rgb = color.rgb;
  gl_FragColor.a = color.a;
}

這裡的變數tMap就是第一次渲染生成的紋理。那麼我們要怎麼獲取這個紋理呢?這就要用到WebGL中的幀緩衝物件,Frame Buffer Object。

當我們沒有繫結幀緩衝物件時,Shader生成的圖形會使用預設的緩衝區,直接輸出繪製到畫布上,當然這樣我們是拿不到渲染結果的,這裡為了對渲染結果二次加工,我們需要在執行渲染前繫結幀緩衝物件,這樣在渲染時就會實現類似OffscreenCanvas的離屏繪製,將渲染結果輸出到幀緩衝物件中。

const fbo = renderer.createFBO(); // 建立幀緩衝物件
renderer.bindFBO(fbo); // 繫結,指定輸出到的幀緩衝物件
renderer.render(); // 輸出到幀緩衝物件
renderer.bindFBO(null); // 解除繫結

const blurProgram = renderer.compileSync(blurFragment, vertex);
renderer.useProgram(blurProgram);
renderer.setMeshData(program.meshData);
renderer.uniforms.tMap = fbo.texture; // 將前一個著色器程式生成的紋理作為新著色器的 tMap 變數
renderer.render();

在完成輸出後,就解除繫結,並使用新的片元著色器建立一個新的著色器程式,並開啟使用。

此時我們可以透過fbo.texture獲取到前一個著色器程式生成的紋理,並傳遞給新的著色器使用。

可以看到,現在畫布上的圖案和之前的並沒有什麼區別,這是因為我們的第二次渲染,是透過紋理座標對映直接取樣、獲取到顏色資訊並著色。

現在我們就來新增高斯模糊的處理程式碼。

對高斯模糊不瞭解的小夥伴可以參考這篇部落格,它的原理簡單來說就是:

按照高斯分佈的權重,對當前畫素點及其周圍畫素點的顏色按照高斯分佈的權重 加權平均。這樣能讓圖片各畫素色值與周圍色值的差異減小,從而達到平滑,或者說是模糊的效果。

varying vec2 vUv; // 當前片元對映的紋理座標
uniform sampler2D tMap;
uniform int axis; // 標記對哪個座標軸進行高斯模糊的處理

void main() {
  vec4 color = texture2D(tMap, vUv);

  // 高斯矩陣的權重值
  float weight[5];
  weight[0] = 0.227027;
  weight[1] = 0.1945946;
  weight[2] = 0.1216216;
  weight[3] = 0.054054;
  weight[4] = 0.016216;

  // 每一個相鄰畫素的座標間隔,這裡的512可以用實際的Canvas畫素寬代替
  float tex_offset = 1.0 / 512.0;
  vec3 result = color.rgb;
  result *= weight[0];
  for (int i = 1; i < 5; ++ i) {
    float f = float(i);
    if (axis == 0) { // X軸的高斯模糊
      result += texture2D(tMap, vUv + vec2(tex_offset * f, 0.0)).rgb * weight[i];
      result += texture2D(tMap, vUv - vec2(tex_offset * f, 0.0)).rgb * weight[i];
    } else { // Y軸的高斯模糊
      result += texture2D(tMap, vUv + vec2(0.0, tex_offset * f)).rgb * weight[i];
      result += texture2D(tMap, vUv - vec2(0.0, tex_offset * f)).rgb * weight[i];
    }
  }

  gl_FragColor.rgb = result.rgb;
  gl_FragColor.a = color.a;
}

因為我們設定畫布寬高是512,所以這個tex_offset表示1個畫素在WebGL畫布上的單位長度,透過加減tex_offset * f,就能根據座標得到附近畫素點的顏色資訊,完成高斯模糊的處理。

因為高斯模糊有兩個方向,所以至少要執行兩次渲染,當然如果想要達到更好的效果,可以執行多次渲染。接下來我們就來修改JavaScript部分的程式碼。

// 建立兩個FBO物件交替使用
const fbo1 = renderer.createFBO();
const fbo2 = renderer.createFBO();
// 第一次,渲染原始圖形
renderer.bindFBO(fbo1);
renderer.render();
const blurProgram1 = renderer.compileSync(blurFragment1, vertex);
// 第二次,對X軸高斯模糊
renderer.useProgram(blurProgram1);
renderer.setMeshData(program.meshData);
renderer.bindFBO(fbo2);  // 繫結幀緩衝物件
renderer.uniforms.tMap = fbo1.texture;
renderer.uniforms.axis = 0;
renderer.render(); // 將第二次的繪製結果輸出到幀緩衝物件
// 第三次,對Y軸高斯模糊
renderer.useProgram(blurProgram1);
renderer.bindFBO(fbo1);
renderer.uniforms.tMap = fbo2.texture;
renderer.uniforms.axis = 1;
renderer.render(); // 將第三次的繪製結果輸出到幀緩衝物件
// 第四次,對X軸高斯模糊
renderer.useProgram(blurProgram1);
renderer.bindFBO(fbo2);
renderer.uniforms.tMap = fbo1.texture;
renderer.uniforms.axis = 0;
renderer.render(); // 將第四次的繪製結果輸出到幀緩衝物件
// 第五次,對Y軸高斯模糊
renderer.useProgram(blurProgram1);
renderer.bindFBO(null); // 解除FBO繫結
renderer.uniforms.tMap = fbo2.texture;
renderer.uniforms.axis = 1;
renderer.render(); // 將第五次的繪製結果輸出到畫布上

在以上程式碼中,我們執行了五次渲染,第一次渲染是生成初始紋理並輸出到FBO物件,後面四次是對紋理進行高斯模糊處理,在執行之後一次渲染之前,我們對FBO物件解除繫結,這樣最終的渲染結果就會繪製到螢幕上。

這樣我們就透過後期處理通道實現了動態紋理的平滑模糊的濾鏡效果。

image

當然除了實現高斯模糊之外,我們還可以透過後期處理通道實現其他型別的二次加工。

核心原理

透過上面這個簡單的例子,相信大家都知道如何去使用後期處理通道來增強影像的視覺效果了,簡單來說就三個步驟:

第一步,是把第一次渲染後的圖案輸出到幀緩衝物件FBO中;

第二步,就是把FBO物件的內容作為紋理,再進行下一次渲染;這一步的渲染過程可以根據需要重複若干次。

第三步,就是把最終結果輸出到螢幕上。

這樣我們就對動態生成的紋理實現了二次加工。

總結

我相信大家看下來應該都知道怎麼做了,可以自己動手嘗試一下,有興趣的小夥伴還可以去嘗試實現更多的視覺效果,比如輝光效果、煙霧效果等等。

煙霧效果參考文章

高斯模糊例子

完整程式碼

相關文章