延迟着色(Deferred Shading)是一种渲染技术,通过将几何信息(如位置、法线和颜色)存储在缓冲区中,分两步完成光照计算,从而提升复杂场景的性能。
延迟着色的核心步骤
-
几何阶段 (Geometry Pass):
- 渲染场景,将每个像素的几何信息(世界空间位置、法线、颜色等)写入帧缓冲区中的多个纹理(称为 G 缓冲区,G-Buffer)。
-
光照阶段 (Lighting Pass):
- 使用 G 缓冲区中的数据,计算每个像素的光照。
- 只在屏幕空间计算光照,减少计算开销。
-
最终合成 (Final Pass):
- 将光照阶段的输出与其他效果(如后期处理)结合,最终输出到屏幕。
延迟着色代码框架
1. 初始化 G 缓冲区
unsigned int gBuffer;
glGenFramebuffers(1, &gBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
unsigned int gPosition, gNormal, gAlbedoSpec;
// 位置缓冲
glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);
// 法线缓冲
glGenTextures(1, &gNormal);
glBindTexture(GL_TEXTURE_2D, gNormal);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);
// 颜色 + 镜面反射强度缓冲
glGenTextures(1, &gAlbedoSpec);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec, 0);
// 设置帧缓冲区的颜色附件
unsigned int attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
glDrawBuffers(3, attachments);
// 深度缓冲
unsigned int rboDepth;
glGenRenderbuffers(1, &rboDepth);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "G-Buffer Framebuffer not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
详解:
创建帧缓冲对象 (Framebuffer Object, FBO)
unsigned int gBuffer;
glGenFramebuffers(1, &gBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
-
glGenFramebuffers(1, &gBuffer);
- 创建一个帧缓冲对象 (Framebuffer),用于离屏渲染。
gBuffer
是帧缓冲对象的 ID。
-
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
- 将
gBuffer
绑定为当前的帧缓冲对象,接下来的所有渲染操作都会输出到这个缓冲区,而不是默认的屏幕缓冲
- 将
2. 创建 G 缓冲区中的纹理附件 (Textures as Attachments)
2.1. 创建位置缓冲区
glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);
-
glGenTextures(1, &gPosition);
- 创建一个纹理对象,用于存储顶点的 世界空间位置。
gPosition
是纹理的 ID。
-
glBindTexture(GL_TEXTURE_2D, gPosition);
- 将
gPosition
绑定为当前激活的 2D 纹理目标,后续配置均应用到这个纹理。
- 将
-
glTexImage2D(...)
- 创建一个 2D 纹理,并分配内存空间:
GL_RGB16F
:每个像素存储 3 个分量(RGB),16 位浮点数,用于精确表示顶点位置。SCR_WIDTH
和SCR_HEIGHT
:纹理分辨率,通常为屏幕尺寸。GL_RGB
:数据格式,表示三个通道。GL_FLOAT
:纹理数据类型,浮点数。
- 创建一个 2D 纹理,并分配内存空间:
-
glTexParameteri(...)
- 设置纹理采样参数:
GL_TEXTURE_MIN_FILTER
和GL_TEXTURE_MAG_FILTER
:指定纹理缩小和放大时的采样方式,这里使用GL_NEAREST
(最近邻采样)保证不模糊。
- 设置纹理采样参数:
-
glFramebufferTexture2D(...)
- 将
gPosition
纹理附加到帧缓冲的GL_COLOR_ATTACHMENT0
槽位,用于存储位置数据。
- 将
2.2. 创建法线缓冲区
glGenTextures(1, &gNormal);
glBindTexture(GL_TEXTURE_2D, gNormal);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);
这部分代码与 gPosition
的流程类似,但 gNormal
用于存储 顶点法线,格式和精度相同 (GL_RGB16F
)。
2.3. 创建颜色 + 镜面强度缓冲区
glGenTextures(1, &gAlbedoSpec);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec, 0);
-
GL_RGBA
- 该纹理存储颜色数据(RGB)和镜面强度(Alpha 通道)。
- 数据类型为
GL_UNSIGNED_BYTE
,精度为 8 位无符号整数。
-
作用:
RGB
保存像素颜色信息(漫反射颜色)。A
保存镜面反射强度,用于光照计算。
-
GL_COLOR_ATTACHMENT2
- 将纹理附加到帧缓冲的第三个颜色附件。
3. 设置多个颜色附件
unsigned int attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
glDrawBuffers(3, attachments);
glDrawBuffers(...)
- 告诉 OpenGL 渲染时将输出数据写入哪些颜色附件。
GL_COLOR_ATTACHMENT0
对应位置,GL_COLOR_ATTACHMENT1
对应法线,GL_COLOR_ATTACHMENT2
对应颜色+镜面反射强度。
4. 深度缓冲区
unsigned int rboDepth;
glGenRenderbuffers(1, &rboDepth);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
-
glGenRenderbuffers(1, &rboDepth);
- 创建一个渲染缓冲对象 (Renderbuffer Object),用于存储深度信息。
-
glRenderbufferStorage(...)
- 分配渲染缓冲区的存储空间:
GL_DEPTH_COMPONENT
:只存储深度信息。- 分辨率为屏幕大小。
- 分配渲染缓冲区的存储空间:
-
glFramebufferRenderbuffer(...)
- 将渲染缓冲附加到帧缓冲的深度附件 (
GL_DEPTH_ATTACHMENT
),用于深度测试。
- 将渲染缓冲附加到帧缓冲的深度附件 (
5. 检查帧缓冲完整性
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "G-Buffer Framebuffer not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
-
glCheckFramebufferStatus(...)
- 检查帧缓冲是否完整。若返回值不是
GL_FRAMEBUFFER_COMPLETE
,则配置存在问题。
- 检查帧缓冲是否完整。若返回值不是
-
glBindFramebuffer(GL_FRAMEBUFFER, 0);
- 解除帧缓冲绑定,恢复默认的屏幕缓冲。
2. 几何阶段 (Geometry Pass) 在几何阶段,渲染场景,将几何信息存储到 G 缓冲区。
顶点着色器(geometry_vertex_shader.glsl):
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoords;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
TexCoords = aTexCoords;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
片段着色器(geometry_fragment_shader.glsl):
#version 330 core
layout(location = 0) out vec3 gPosition;
layout(location = 1) out vec3 gNormal;
layout(location = 2) out vec4 gAlbedoSpec;
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
uniform sampler2D texture_diffuse;
uniform sampler2D texture_specular;
void main()
{
gPosition = FragPos;
gNormal = normalize(Normal);
vec3 albedo = texture(texture_diffuse, TexCoords).rgb;
float spec = texture(texture_specular, TexCoords).r;
gAlbedoSpec = vec4(albedo, spec);
}
3. 光照阶段 (Lighting Pass) 使用屏幕空间四边形渲染光照。
片段着色器(lighting_fragment_shader.glsl):
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedoSpec;
uniform vec3 viewPos;
uniform vec3 lightPos;
uniform vec3 lightColor;
void main()
{
// 从 G 缓冲区中读取数据
vec3 FragPos = texture(gPosition, TexCoords).rgb;
vec3 Normal = normalize(texture(gNormal, TexCoords).rgb);
vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb;
float Specular = texture(gAlbedoSpec, TexCoords).a;
// 光照计算
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(Normal, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, Normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 16.0);
vec3 specular = Specular * spec * lightColor;
vec3 ambient = 0.1 * Albedo;
vec3 result = (ambient + diffuse + specular) * Albedo;
FragColor = vec4(result, 1.0);
}
在主循环中渲染光照:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
lightingShader.use();
lightingShader.setVec3("viewPos", camera.Position);
lightingShader.setVec3("lightPos", lightPos);
lightingShader.setVec3("lightColor", lightColor);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, gPosition);
lightingShader.setInt("gPosition", 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, gNormal);
lightingShader.setInt("gNormal", 1);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
lightingShader.setInt("gAlbedoSpec", 2);
// 绘制屏幕四边形
renderQuad();
4. 最终合成
根据需要添加后期处理,直接在 FragColor 上进行额外处理即可。