【OpenGL】延迟着色和G缓冲

Source

延迟着色(Deferred Shading)是一种渲染技术,通过将几何信息(如位置、法线和颜色)存储在缓冲区中,分两步完成光照计算,从而提升复杂场景的性能。

延迟着色的核心步骤

  1. 几何阶段 (Geometry Pass)

    • 渲染场景,将每个像素的几何信息(世界空间位置、法线、颜色等)写入帧缓冲区中的多个纹理(称为 G 缓冲区,G-Buffer)。
  2. 光照阶段 (Lighting Pass)

    • 使用 G 缓冲区中的数据,计算每个像素的光照。
    • 只在屏幕空间计算光照,减少计算开销。
  3. 最终合成 (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_WIDTHSCR_HEIGHT:纹理分辨率,通常为屏幕尺寸。
      • GL_RGB:数据格式,表示三个通道。
      • GL_FLOAT:纹理数据类型,浮点数。
  • glTexParameteri(...)

    • 设置纹理采样参数:
      • GL_TEXTURE_MIN_FILTERGL_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 上进行额外处理即可。