什么是延迟着色(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) 在复杂光照条件下有着性能优势,但它也有缺点:大内存开销。还有在光源不是很多的场景中,延迟渲染并不一定会更快,甚至有时候由于开销过大还会变得更慢。当然在更复杂的场景中,延迟渲染会变成一个重要的性能优化手段。