Deferred Shading
카테고리: OpenGL
태그: Graphics
Main Reference
- Learn OpenGL
- Rinthel Kwon - OpenGL Lecture
Rendering Pass
Forward Rendering
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 |
---|---|
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();
- 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
댓글 남기기