Deferred Shading

Date:     Updated:

카테고리:

태그:

Main Reference
- Learn OpenGL
- Rinthel Kwon - OpenGL Lecture


Rendering Pass

Forward Rendering

image

for (auto mesh : meshes) :
    for (auto light : lights) :
        draw(mesh, light);

지금까지 해온 Lighting 연산을 Forward Rendering 방식이라 한다. Forward Rendering 방식은 각 오브젝트마다, 각 Light에 대해서 Lighting 연산을 수행한다. 따라서 오브젝트마다 별도의 라이팅 방식을 선택할 수 있고(ex. BRDFs), alpha값에 따라 투명도를 계산할 수 있다. 하지만 depth-test를 켰다 하더라도, 오브젝트를 그리는 순서에 따라 같은 픽셀을 다시 그리는 오버드로우 현상이 발생한다. 또한 light의 갯수가 많아지면 퍼포먼스가 급격하게 떨어진다는 치명적인 단점이 존재한다.


Deferred Rendering

Deferred Rendering G-Buffer
image11 image-1
for (auto mesh : meshes) :
    drawToGBuffer(mesh);

for (auto light : lights) :
    drawToFrameBuffer(light);

Deferred Rendering 방식은 말 그대로 라이팅 연산을 지연시켜 마지막에 한 번에 처리한다. 먼저 오브젝트들을 G-Buffer라는 frame buffer에 그리게 되는데, G-Buffer에는 lighting 계산에 필요한 정보를 담은 텍스처들의 뭉치가 들어간다. 예를 들어 world position, albedo color, normal, depth, … 등이 있다. 이후 G-Buffer를 기반으로 lighting 연산을 수행하기 때문에, light의 수가 많더라도 오버드로우 문제 없이 빠르게 수행할 수 있다. 하지만 반대로 투명 물체를 다룰 수 없으며, 모든 오브젝트에 대해 같은 렌더링 모델을 사용할 수 밖에 없다. 가장 큰 문제는 G-Buffer라는 엄청나게 큰 buffer가 필요하다는 것이다. G-Buffer는 화면 사이즈의 같은 Texture가 여러 개 포함된 buffer이기 때문에 상당히 큰 버퍼이며, 이 버퍼를 옮기기 위해서는 하드웨어가 높은 bandwidth를 감당할 수 있어야 한다.


G-Buffer

Frame Buffer

bool Framebuffer::InitWithColorAttachments(const std::vector<TexturePtr>& colorAttachments) {
    m_colorAttachments = colorAttachments;
    glGenFramebuffers(1, &m_framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer); // 화면이 아닌 frame buffer에 그리겠다

    for (size_t i = 0; i < m_colorAttachments.size(); i++) {
        glFramebufferTexture2D(GL_FRAMEBUFFER,
            GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D,
            m_colorAttachments[i]->Get(), 0);
    }

    if (m_colorAttachments.size() > 1) {
        std::vector<GLenum> attachments;
        attachments.resize(m_colorAttachments.size());
        for (size_t i = 0; i < m_colorAttachments.size(); i++)
            attachments[i] = GL_COLOR_ATTACHMENT0 + i;
        glDrawBuffers(m_colorAttachments.size(), attachments.data());
    }

    int width = m_colorAttachments[0]->GetWidth();
    int height = m_colorAttachments[0]->GetHeight();                  

    glGenRenderbuffers(1, &m_depthStencilBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, m_depthStencilBuffer);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
    glBindRenderbuffer(GL_RENDERBUFFER, 0); 

    glFramebufferRenderbuffer(
        GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
        GL_RENDERBUFFER, m_depthStencilBuffer);

    auto result = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (result != GL_FRAMEBUFFER_COMPLETE) {
        SPDLOG_ERROR("failed to create framebuffer: {}", result);
        return false;
    }

    BindToDefault();

    return true;
}
  • 이전 포스팅의 Bloom Algorithm과 마찬가지로 frame buffer에 여러 텍스처를 연동해야 함
  • FrameBuffer 클래스가 color attachment를 여러 개 갖도록 리팩토링
  • glDrawBuffers로 OpenGL에게 여러개의 color attachment임을 알려주는 것 주의


// fragment shader
#version 330 core

layout (location = 0) out vec4 gPosition;
layout (location = 1) out vec4 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

in vec3 position;
in vec3 normal;
in vec2 texCoord;

struct Material {
    sampler2D diffuse;
    sampler2D specular;
};
uniform Material material;

void main() {
    // store the fragment position vector in the first gbuffer texture
    gPosition = vec4(position, 1.0);
    // also store the per-fragment normals into the gbuffer
    gNormal = vec4(normalize(normal), 1.0);
    // and the diffuse per-fragment color
    gAlbedoSpec.rgb = texture(material.diffuse, texCoord).rgb;
    // store specular intensity in gAlbedoSpec’s alpha component
    gAlbedoSpec.a = texture(material.specular, texCoord).r;
}
  • vertex shader는 일반적인 vs와 동일
  • fragment shader에서는 G-Buffer에 필요한 텍스처들 각각 저장
  • 텍스처의 각 픽셀은 32bit data이므로, albedo와 specular intensity의 데이터 같이 저장하면 효율적
    • 어차피 alpha값은 의미 없기 때문
  • G-Buffer에서 사용되는 Material은 라이팅 연산과 관련 없는 파라미터만 있으면 됨


// Render()...
    if (ImGui::Begin("G-Buffers")) {
        const char* bufferNames[] = { "position", "normal", "albedo/specular" };
        static int bufferSelect = 0;
        ImGui::Combo("buffer", &bufferSelect, bufferNames, 3);
        float width = ImGui::GetContentRegionAvailWidth();
        float height = width * ((float)m_height / (float)m_width);
        auto selectedAttachment = m_deferGeoFramebuffer->GetColorAttachment(bufferSelect);
        ImGui::Image((ImTextureID)selectedAttachment->Get(),
            ImVec2(width, height), ImVec2(0, 1), ImVec2(1, 0));
    }
    ImGui::End();

012704

  • G-Buffer는 off-screen buffer이기 때문에 체크하려면 ImGUI 이용


Lighting

m_deferLights.resize(32);
for (size_t i = 0; i < m_deferLights.size(); i++) {
    m_deferLights[i].position = glm::vec3(
        RandomRange(-10.0f, 10.0f),
        RandomRange(1.0f, 4.0f),
        RandomRange(-10.0f, 10.0f));
    m_deferLights[i].color = glm::vec3(
        RandomRange(0.05f, 0.3f),
        RandomRange(0.05f, 0.3f),
        RandomRange(0.05f, 0.3f));
}
  • Deferred Rendering의 장점은 뭐니뭐니해도 수 많은 light를 다룰 수 있다는 점
  • Forward 방식에서는 생각하기 힘든 32개의 light를 세팅해보자.


// fragment shader
#version 330 core

out vec4 fragColor;
in vec2 texCoord;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedoSpec;

struct Light {
  vec3 position;
  vec3 color;
};

const int NR_LIGHTS = 32;
uniform Light lights[NR_LIGHTS];
uniform vec3 viewPos;

void main() {
    // retrieve data from G-buffer
    vec3 fragPos = texture(gPosition, texCoord).rgb;
    vec3 normal = texture(gNormal, texCoord).rgb;
    vec3 albedo = texture(gAlbedoSpec, texCoord).rgb;
    float specular = texture(gAlbedoSpec, texCoord).a;
    // then calculate lighting as usual
    vec3 lighting = albedo * 0.1; // hard-coded ambient component
    vec3 viewDir = normalize(viewPos - fragPos);
    for(int i = 0; i < NR_LIGHTS; ++i) {
        // diffuse
        vec3 lightDir = normalize(lights[i].position - fragPos);
        vec3 diffuse = max(dot(normal, lightDir), 0.0) * albedo * lights[i].color;
        lighting += diffuse;
    }
    fragColor = vec4(lighting, 1.0);
}
  • vertex shader에서는 tex_coord만 받아오면 됨
  • uniform Light lights[NR_LIGHTS] : 기본적으로 light를 배열로 받아옴
  • 오브젝트마다 따로 받아오던 pos, normal, albedo, 등을 depth-test가 적용된 G-Buffer에서 가져오고
  • 각 light에 대해서 lighting 연산 후 결과를 누적하는 방식


// Render()...
    m_deferLightProgram->Use();
    glActiveTexture(GL_TEXTURE0);
    m_deferGeoFramebuffer->GetColorAttachment(0)->Bind();
    glActiveTexture(GL_TEXTURE1);
    m_deferGeoFramebuffer->GetColorAttachment(1)->Bind();
    glActiveTexture(GL_TEXTURE2);
    m_deferGeoFramebuffer->GetColorAttachment(2)->Bind();
    glActiveTexture(GL_TEXTURE0);
    m_deferLightProgram->SetUniform("gPosition", 0);
    m_deferLightProgram->SetUniform("gNormal", 1);
    m_deferLightProgram->SetUniform("gAlbedoSpec", 2);
    for (size_t i = 0; i < m_deferLights.size(); i++) {
        auto posName = fmt::format("lights[{}].position", i);
        auto colorName = fmt::format("lights[{}].color", i);
        m_deferLightProgram->SetUniform(posName, m_deferLights[i].position);
        m_deferLightProgram->SetUniform(colorName, m_deferLights[i].color);
    }
    m_deferLightProgram->SetUniform("transform", glm::scale(glm::mat4(1.0f), glm::vec3(2.0f)));
    m_plane->Draw(m_deferLightProgram.get());
  • G-Buffer 패스에서 연산한 텍스처들 바인딩
  • fmt 라이브러리를 이용하면 light 정보를 간편하게 shader로 전달 가능
  • G-Buffer를 기반으로 계산한 lighting 결과는 화면 전체를 덮는 plane 오브젝트를 이용해 렌더링하는게 가장 간편한 방식


Result

191430



맨 위로 이동하기

OpenGL 카테고리 내 다른 글 보러가기

댓글 남기기