WebGL之延遲著色

Jeff.Zhong發表於2020-09-18

什麼是延遲著色(Deferred Shading)?它是相對於正常使用的正向著色(Forward Shading)而言的,正向著色的工作模式:遍歷光源,獲取光照條件,接著遍歷物體,獲取物體的幾何資料,最後根據光照和物體幾何資料進行計算。

但是正向著色(Forward Shading)在光源非常多的情況下,對效能的消耗非常大。因為程式要對每一個光源,每一個需要渲染的物體,每一個需要渲染的片段進行迭代!還有片段著色器的輸出會被之後的輸出覆蓋,正向渲染會在場景中因多個物體重合在一個畫素上浪費大量的片段著色器執行時間。

延遲著色(Deferred Shading),就是為了解決上述問題而生,尤其是需要渲染幾百上千個光源的場景。

本節實現的效果請看:延遲著色 deferred sharding
延遲著色

正向著色虛擬碼:

foreach light {
  foreach visible mesh {
    if (light volume intersects mesh) {
      render using this material/light shader;
      accumulate in framebuffer using additive blending;
    }
  }
}

延遲著色

延遲著色(Deferred Shading)工作模式就是將計算量大的渲染光照部分 延遲(Defer) 到後期進行處理,因此它包含兩個處理階段(Pass):

  1. 第一個 幾何處理階段(Geometry Pass) 是將物件的各種幾何資訊進行渲染並儲存在叫做G緩衝(G-buffer)的紋理中。主要包括位置向量(Position Vector)顏色向量(Color Vector)法向量(Normal Vector)。這些儲存在G緩衝中的幾何資訊將會用來做之後的光照計算。
  2. 第二個 光照處理階段(Lighting Pass) 是對G緩衝中的幾何資料的每一個片段進行光照計算。光照處理階段不是直接將每個物件從頂點著色器帶到片段著色器,而是對G緩衝中的每個畫素進行迭代,從對應的G緩衝紋理獲取輸入變數。

延遲著色虛擬碼:

// g-buffer pass
foreach visible mesh {
  write material properties to g-buffer;
}

// light accumulation pass
foreach light {
  compute light by reading g-buffer;
  accumulate in framebuffer;
}

幀緩衝

延遲著色(Deferred Shading)G緩衝(G-buffer) 是基於 幀緩衝(frameBuffer) 實現的,涉及到高階應用,幀緩衝 真的是無處不在啊!該demo的幾何處理階段分別對位置(position)法向量(normal)顏色(color) 進行快取,那麼對應就要建立3個顏色附件,別忘了同時建立用於深度測試用的 深度緩衝(Z-Buffer)

const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
const fbo = {
  framebuffer: fb,
  textures: []
};

// 建立顏色紋理
for(let i = 0; i < 3; i++){
  const tex = initTexture(gl, { informat: gl.RGBA16F, type: gl.FLOAT }, width, height);
  framebufferInfo.textures.push(tex);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, tex, 0);
}

// 建立深度渲染緩衝區
const depthBuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);

多渲染目標 draw buffers

WebGL 實現多渲染目標需要開啟 WEBGL_draw_buffers 這個擴充套件,但是 WebGL 2.0 直接就能使用的。我這裡為了方便就基於 WebGL 2.0 來實現,多渲染目標呼叫方式如下:

gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);

著色器

因為延遲著色器分兩個階段,那麼對應就需要兩對著色器,首先來看幾何處理階段的著色器。

幾何處理階段 頂點著色器(vertex)

#version 300 es
in vec4 aPosition;
in vec4 aNormal;
uniform mat4 modelMatrix;
uniform mat4 vpMatrix;
out vec3 vPosition;
out vec3 vNormal;

void main() {
  gl_Position = vpMatrix * modelMatrix * aPosition;
  vNormal = vec3(transpose(inverse(modelMatrix)) * aNormal);
  vPosition = vec3(modelMatrix * aPosition);
}

幾何處理階段 片段著色器(fragment),這裡的三個輸出變數對應就是幀緩衝中的三個顏色紋理。

#version 300 es
precision highp float;
layout (location = 0) out vec3 gPosition;// 位置
layout (location = 1) out vec3 gNormal;  // 法向量
layout (location = 2) out vec4 gColor;   // 顏色
uniform vec4 color;
in vec3 vPosition;
in vec3 vNormal;

void main() {
  gPosition = vPosition;
  gNormal = normalize(vNormal);
  gColor = color;
}

接著就是光照處理階段的著色器組了。

光照處理階段 頂點著色器(vertex),這個非常簡單,對映到幀緩衝,也就是個平面貼圖而已。

#version 300 es
in vec3 aPosition;
in vec2 aTexcoord;
out vec2 texcoord;

void main() {
  texcoord = aTexcoord;
  gl_Position = vec4(aPosition, 1.0);
}

光照處理階段 片段著色器(fragment),需要從對應的紋理貼圖取出對應的幾何資料。也就是使用 texture 函式結合貼圖和 貼圖座標(texcoord) 就可以計算出對應的幾何資料,再結合光照資料渲染出最終結果。

#version 300 es
precision highp float;
uniform vec3 viewPosition;
uniform vec3 lightDirection;
uniform vec3 lightColor;
uniform vec3 ambientColor;
uniform float shininess;
// 各種自定義變數 ...
uniform sampler2D gPosition;// 位置
uniform sampler2D gNormal;  // 法向量
uniform sampler2D gColor;   // 顏色
in vec2 texcoord;           // 座標
out vec4 FragColor;

void main() {
  vec3 fragPos = texture(gPosition, texcoord).rgb;
  vec3 normal = texture(gNormal, texcoord).rgb;
  vec3 color = texture(gColor, texcoord).rgb;
 
  // todo: 各種計算過程...
  // 環境光
  vec3 ambient = ambientColor * color;
	
  // 漫反射
  // ...
  vec3 diffuse = lightColor * color * cosTheta;
	
  // 高光
  // ...
  vec3 specular = lightColor * specularIntensity;

  FragColor = vec4(ambient + diffuse + specular, 1.0);
}

WebGL 流程

最後就是使用 JavaScript 將整個流程串起來,WebGL 的其他技術細節不再詳細介紹了,具體可以看我之前的 WebGL 教程。這裡介紹一下大體的流程:

  1. 幾何處理階段:繫結幀緩衝,切換到對應的program,設定各種變數,輸出到幀緩衝;
  2. 光照處理階段:切換回正常緩衝,切換到對應的program,設定各種變數,同時將前面幾何處理階段得到紋理作為輸入變數,輸出渲染結果;
/**
 * 場景繪製到幀緩衝區 
 */ 
gl.bindFramebuffer(target, fbo.framebuffer); // 繫結幀緩衝
gl.viewport(0, 0, width, height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 清屏
gl.useProgram(program);
//取樣到3個顏色附件(對應的幾何紋理)
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);
setUniforms(program, uniforms);// 設定uniform變數
setBuffersAndAttributes(gl, vao);// 設定快取和attribute變數
drawBufferInfo(gl, vao); // 寫入緩衝區

/**
 * 從幀快取渲染到正常緩衝區
 */
gl.bindFramebuffer(target, null); // 切換回正常緩衝
gl.viewport(0, 0, width, height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(fProgram);
const uniforms = {
  // 其他變數 ...
  gPosition: fbo.textures[0],// 位置紋理
  gNormal: fbo.textures[1],// 法向量紋理
  gColor: fbo.textures[2], // 顏色紋理
};
setUniforms(fProgram, uniforms);
setBuffersAndAttributes(gl, fVao);
drawBufferInfo(gl, fVao); // 輸出畫面

本節實現的效果請看:延遲著色 deferred sharding
demo 使用了1個平行光源,10個點光源,3個聚光燈實現了類似舞廳五彩斑斕的渲染效果。

最後

延遲著色(Deferred Shading) 在複雜光照條件下有著效能優勢,但它也有缺點:大記憶體開銷。還有在光源不是很多的場景中,延遲渲染並不一定會更快,甚至有時候由於開銷過大還會變得更慢。當然在更復雜的場景中,延遲渲染會變成一個重要的效能優化手段。

相關文章