Stories

Detail Return Return

可視化學習:如何使用後期處理通道增強圖像效果 - Stories Detail

前言

大家好,本文分享的是如何使用後期處理通道增強圖像效果,通過前面幾篇文章,我們瞭解了一些動態生成紋理的方法,比如符號距離場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對象的內容作為紋理,再進行下一次渲染;這一步的渲染過程可以根據需要重複若干次。

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

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

總結

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

煙霧效果參考文章

高斯模糊例子

完整代碼

user avatar nqbefgvs Avatar hyfhao Avatar yangxiansheng_5a1b9b93a3a44 Avatar huaweiclouddeveloper Avatar risejl Avatar zz_641473ad470bc Avatar ysxq Avatar jackn Avatar ohaha Avatar jibvxiz Avatar lingleidejiandao Avatar user_4jsjtpci Avatar
Favorites 17 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.