Post Lists

2018년 8월 17일 금요일

Advanced Lighting - Deferred Shading (9)

https://learnopengl.com/Advanced-Lighting/Deferred-Shading

Deferred Shading
우리가 이제까지 했던 라이팅 방식은 forward rendering or forward shading이라고 불려진다. 그것은 간단한 접근법인데, 우리는 한 오브젝트를 렌더링하고, 한 scene에서 모든 광원들에 따라 그것을 비추고 그리고 그러고나서 다음 오브젝트를 렌더링 한다. 등등, 그 scene에 있는 각 오브젝트에 대해. 꽤 이해하고 구현하기에 쉽지만, 성능에 있어서 또한 꽤 무겁다. 각 렌더링 된 오브젝트가 많은 모든 렌더링 된 fragment에 대해 각 light source에 대해 반복해야만 하기 때문이다. Forward rendering은 또한 높은 depth complexity가 있는 scenes들에서 많은 fragment shader 작동을 낭비하는 경향이 있다. (수 많은 오브젝트들이 같은 스크린 픽셀들을 가린다) 대부분의 fragment shader outputs이 덮여 씌워진다.

우리가 오브젝트들을 렌더링하는 방식을 급격히 바꾸는 Deferred shading or deferred rendering은 이러한 문제를 극복하려고 한다. 이것은 우리에게 많은 수의 lights을 가진 scenes을 상당히 최적화하는 몇 가지 새로운 옵션들을 준다. 그리고 이것은 우리가 수용할만한 프레임율로 수백 또는 수전개의 lights를 렌더링할 수 있게 해준다. deferred shading으로 렌더링된 1847개의 point lights가 있는 한 scene의 이미지가 아래에 있다. (Hannes Nevalainen 이미지 제공); forward rendering으로는 가능하지 않은 것이다.

Deferred shading은 우리가 (lighting같은) 대부분의 무거운 렌더링을 나중의 단계로 defer or postpone (미루다)는 아이디어에 기반을 둔다. Deferred shading은 두 개의 passes들로 구성된다: geometry pass라고 불려지는 첫 번째 pass에서 우리는 그 scene을 한 번 렌더링하고, G-buffer라고 불리는 텍스쳐들의 모음에 우리가 저장하는 오브젝트로부터 모든 종류의 geometrical information을 얻어온다; position vector, color vectors, normal vectors and/or specular values를 생각해보아라. G-buffer에 저장된 한 scene의 geometric information은 그러고나서 나중에 (좀 더 복잡한) lighting calculations를 위해 사용된다. 한 single frame의 G-buffer의 내용이 아래에 있다.

우리는 lighting pass라고 불려지는 second pass에서 G-buffer의 텍스쳐를 사용한다. 거기에서 우리는 스크린을 채우는 quad를 렌더링하고, G-buffer에 저장된 geometrical information을 사용하여 각 fragment에 대해 scene의 lighting을 계산한다. vertex shader에서 fragment shader로 가는 각 오브젝트를 모두 취하는 대신에, 우리는 그것의 advanced fragment processes를 나중 단계로 분리시킨다. lighting calculations은 우리가 익숙한 것과 정확히 같게 남아 있는다. 그러나 이번에,우리는 vertex shader 대신에 (몇 가지 uniform variables를 더해서) 대응되는 G-buffer textures로부터 모든 요구되는 input variables를 취한다.

아래의 이미지는 deferred shading의 전체 프로세스를 좋게 보여준다.

이 접근법의 중요한 장점은 무슨 fragment가 G-buffer에 끝나든 간에 그것이 screen pixel로서 있게 되는 실제 fragment information이라는 것이다. depth test가 이미 이 fragment information이 가장 위에 있는 fragment로 결론지었기 때문이다. 이것은 각 pixel에 대해 우리가 하는 lighting pass를 오직 한 번 처리하게끔 보장한다; 이것은 우리에게 많은 사용되지 않는 렌더링 호출을 아껴준다. 게다가, deferred rendering은 우리가 forward rendering으로 사용할 수 있는 것보다 더욱 많은 양의 광원들을 렌더링하게 해주는 이후의 최적화에 대한 가능성을 열어준다.

그것은 또한 몇 가지 단점이 있는데, G-buffer는 우리가 메모리를 잡아먹는 texture colorbuffers안에 상대적으로 많은 양의 scene data를 저장하는 것을 요구하기 때문이다. 특히, position vectors같은 scene data가 높은 정밀도를 요구하기 때문이다. 또 다른 불이익은 그것은 blending을 지원하지 않는다 (우리가 오직 가장 위에 있는 fragment의 정보를 가지고 있기 때문에)  그리고 MSAA가 더 이상 작동하지 않는다. 튜토리얼의 끝에서 우리가 도달하는이러한 불이익에 대해 몇 가지 작업할것들이 있다.

The G-buffer
G-buffer는 최종 lighting pass를 위해 lighting과 관련된 데이터를 저장하기 위해 사용되는 모든 텍스쳐의 종합적 용어이다. forward rendering으로 우리가 한 fragment를 비추는데 필요한 모든 데이터를 간단하게 다시 살펴봐보자:

  • lightDir과 ViewDir을 위해 사용되는 (보간된) fragment position variable을 계산하는 3D position vector
  • albedo라고 알려진 RGB diffuse color vector
  • 표면의 경사를 결정하기 위한 3D normal vector
  • specular intensity float
  • 모든 광원의 위치와 컬러 벡터
  • 플레이어 또는 viewer의 포지션 벡터
이러한 우리가 마음대로 할 수 있는 (fragment 마다의) 변수들로, 우리가 익숙한 (Blinn-)Phong lighting을 계산할 수 있다. 광원의 포지션과 컬러, 그리고 플레이어의 view position은 uniform 변수들로 설정되어질 수 있지만, 다른 변수들은 모두 한 오브젝트의 fragments에 대해 각각 특정한 것이다. 만약 우리가 어느정도 최종 deferred lighting pass에 정확히 같은 데이터를 보낼 수 있다면, 우리는 같은 lighting effects를 계산할 수 있다. 비록 우리가 2D quad의 fragments들을 렌더링하고 있을지라도.

OpenGL에서 우리가 한 텍스쳐에 저장할 수 있는 것의 한계는 없다. 그래서 G-buffer라고 불리는 하나 또는 많은 screen을 채우는 texture에 모든 fragment마다의 데이터를 저장하는 것이 가능하고, 이러한 것을 lighting pass에서 나중에 사용하는것이 가능하다. G-buffer textures가 lighting pass의 2D quad와 같은 사이즈를 갖을 것이기 때문에, 우리는 forward rendering 환경에서 우리가 가졌던 것과 정확히 같은 fragment data를 얻게된다, 그러나 이번에는 lighting pass에서; 일대일 매핑이 있다.

슈도코드에서 전체 프로세스는 이렇게 보일 것이다:


while(...) // render loop
{
    // 1. geometry pass: render all geometric/color data to g-buffer
    glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    gBufferShader.use();
    for(Object obj : Objects)
    {
        ConfigureShaderTransformsAndUniforms();
        obj.Draw();
    }

    // 2. lighting pass: use g-buffer to calculate the scene's lighting
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glClear(GL_COLOR_BUFFER_BIT);
    lightingPassShader.use();
    BindAllGBufferTextures();
    SetLightingUniforms();
    RenderQuad();
}

각 fragment의 우리가 저장할 필요가 있는데이터는 position vector, normal vector, color vector, 그리고 specular intensity value이다. geometry pass에서 우리는 따라서 scene의 모든 오브젝트들을 렌더링 할 필요가 있고 이러한 데이터 컴포넌트들을 G-buffer에 저장해야 한다. 우리는 또 다시 한 번의 render pass에서 여러 개의 colorbuffers에 렌더링할 multiple render targets를 사용할 수 있다; 이것은 간단히 bloom tutorial에서 이야기 되었다.

geometry pass를 위해, 우리는 attached된 수많은 colorbuffers와 하나의depth renderbuffer object를 가지고 우리가 직관적으로 gBuffer라고 부르는 하나의 framebuffer object를 초기화할 필요가 있을 것이다. position과 normal texture에 대해, 우리는 가급적 높은 정밀도 텍스쳐 (component당 16 or 32-bit float)를 그리고, albedo와 specular values에 대해 우리는 기본 텍스쳐로 괜찮을 것이다. (component당 8-bit precision).


unsigned int gBuffer;
glGenFramebuffers(1, &gBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);

unsigned int gPosition, gNormal, gAlbedoSpec;

// Position color buffer
glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, GL_RGB16F, SCREEN_WIDTH, SCREEN_HEGIHT, 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);

// Normal color buffer
glGenTextures(1, &gNormal);
glBindTexture(GL_TEXTURE_2D, gNormal);
glTexImage2D(GL_TEXTURE_2D, GL_RGB16F, SCREEN_WIDTH, SCREEN_HEGIHT, 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);

// Albedo + specular color buffer
glGenTextures(1, &gAlbedoSpec);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
glTexImage2D(GL_TEXTURE_2D, GL_RGBA, SCREEN_WIDTH, SCREEN_HEGIHT, 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);

// tell OpenGL which color attachments we'll use (of this framebuffer) for rendering
unsigned int attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
glDrawBuffers(3, attachments);

unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCREEN_WIDTH, SCREEN_HEGIHT);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not completed\n";
glBindFramebuffer(GL_FRAMEBUFFER, 0);

우리가 여러가지 렌더 타겟을 사용하기 때문에, 우리는 explicitly하게 OpenGL에게 어떤 우리가 렌더링하고 싶은 colorbuffers가 gBuffer와 연관되어있는지 glDrawBuffers로 말해야만 한다. 또한 여기에서 주목해야할 흥미로운 것은 우리가 position, normal data를 RGB texture에 저장한다는 것이다. 우리는 각 3개의 components를 가지고 있기 때문이다. 그러나, 우리는 color andspeculardata를 하나의 RGBA texture에 저장한다; 이것은 우리가 부가적인 컬러버퍼 텍스쳐를 선언하는것을 아껴준다. 너의 deferred shading pipeline이 좀 더 복잡해지고 더 많은 데이터를 필요함에 따라, 너는 데이터를 개별 텍스쳐에 결합할 새로운방법을 빠르게 찾을 것이다.

다음으로 우리는 G-buffer에 렌더링할 필요가 있다. 각 오브젝트가 diffuse, normal 그리고 specularintensity texture를 가지고 있다고 가정하여, 우리는 G-buffer에 렌더링할 다음의fragment shader같은 것을 사용할 것이다:


#version 330 core

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

in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;

uniform sampler2D texture_diffuse1;
uniform sampler2D texture_specular1;

void main()
{
    // store the fragment position vector in the first gbuffer texture
    gPosition = FragPos;
    // also store the per-fragment normals into the gbuffer
    gNormal = normalize(Normal);
    // and the diffuse per-fragment color
    gAlbedoSpec.rgb = texture(texture_diffuse1, TexCoords).rgb;
    // store specular intensity in gAlbedoSpec's alpha component
    glAlbedoSpec.a = texture(texture_specular1, TexCoords).r;
}

우리가 다양한 render targets을 사용함에 따라, 그 layout specifier는 OpenGL에게 현재 활성화된 framebuffer의어떤 colorbuffer에 우리가 렌더링할지를 말해준다. 우리가 specular intensity를 단일 colorbuffer texture에 저장하지 않는 것을 주목해라. 우리는 그것의 단일 float value를 다른 컬러버퍼 텍스쳐들 중의 하나의 alpha component에 저장할 수 있기 때문이다.

  Red Box
  lighting calculations으로, 모든 변수들을 같은 좌표 공간에서 유지하는 것이 매우 중요하다; 이 경우에 우리는 모든 변수들을 world-space에서 저장한다. (그리고 계산한다).

만약 우리가 이제 nanosuit objects에 있는 많은 콜렉션을 gBuffer framebuffer에 렌더링하고, 컬러버퍼들을 screen을 채우는 quad에 하나씩 사영하여 그것의 내용을 시각화하려한다면, 우리는 이것과 같은 것을 볼 것이다:

(left-top : position, left_bottom : albedo, right-top : normal, 오른쪽 하단에 specular를 하려고했으나 귀찮아서 안함.)


while (!glfwWindowShouldClose(window))
{
 // time adjustment
 float currentFrame = glfwGetTime();
 deltaTime = currentFrame - lastFrame;
 lastFrame = currentFrame;

 if (currentFrame - setTime >= 1.0)
 {
  setTime = currentFrame;
  std::cout << "frame: " << frame_count << '\n';
  frame_count = 0;
 }

 // Input
 processInput(window);
 glClearColor(0.11, 0.11, 0.11, 1.0);
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

 glm::mat4 view = camera.GetViewMatrix();
 glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCREEN_WIDTH / (float)SCREEN_HEGIHT, 0.1f, 100.f);

 glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
 glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEGIHT);
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 Deferred.use();
 Deferred.setMat4("view", view);
 Deferred.setMat4("projection", projection);
 glm::mat4 t_model(1.f);
 Deferred.setMat4("model", t_model);
 glActiveTexture(GL_TEXTURE0);
 glBindTexture(GL_TEXTURE_2D, container_diffuse);
 glActiveTexture(GL_TEXTURE1);
 glBindTexture(GL_TEXTURE_2D, container_specular);
 renderCube();


 // second color Texture buffer (post processing)
 glBindFramebuffer(GL_FRAMEBUFFER, 0);
 glClearColor(1.f, 1.f, 1.f, 1.f);
 glClear(GL_COLOR_BUFFER_BIT);

 // left-top
 glViewport(0, SCREEN_HEGIHT / 2, SCREEN_WIDTH / 2, SCREEN_HEGIHT / 2);
 def_Frame.use();
 glActiveTexture(GL_TEXTURE0);
 glBindTexture(GL_TEXTURE_2D, gPosition);
 renderQuad();

 // right-top
 glViewport(SCREEN_WIDTH / 2, SCREEN_HEGIHT / 2, SCREEN_WIDTH / 2, SCREEN_HEGIHT / 2);
 def_Frame.use();
 glActiveTexture(GL_TEXTURE0);
 glBindTexture(GL_TEXTURE_2D, gNormal);
 renderQuad();
 // second color Texture buffer (post processing)

 // left-bottom
 glViewport(0, 0, SCREEN_WIDTH / 2, SCREEN_HEGIHT / 2);
 def_Frame.use();
 glActiveTexture(GL_TEXTURE0);
 glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
 renderQuad();

 glfwSwapBuffers(window);
 glfwPollEvents();

 ++frame_count;
}
(main loop 코드는 이런 식으로 된다.)

world-space position과 normal vectors가 정말로 옳은지 시각화하려고 해보아라. 예를들어, 오른쪽을 가리키는 normal vectors는 (양의 x축) 빨간 색과 좀 더 일치할 것이다. scene의 원점으로부터 오른쪽을 가리키는 position vectors와 유사하게. 너가 G-buffer의 내용에 만족하자마자, 다음 단계로 넘어갈 시간이다: lighting pass.

The deferred lighting pass
G-buffer에 우리 마음대로 할 수 있는 fragment data의 큰 모음과 함께, 우리는 픽셀마다 G-buffer textures의 각각에 대해 반복하여 그 scene의 최종 lighted colors를 완전히 계산할 옵션을 가지고 있고 그것들의 내용을 lighting algorithm의 입력으로서 사용한다. G-buffer 텍스쳐 값들은 모두 최종 변환된 fragment values를 나타내기 때문에, 우리는 오직 픽셀마다 한 번씩 비싼 lighting operations을 해야만 한다. 이 것은 deferred shading을 꽤 효율적으로 만든다. 특히 forward rendering setting에서 픽셀마다 수 많은 비싼 fragmetn shader calls을 불러일으켜야만 하는 복잡한 scenes에서.

lighting pass에 대해,우리는 2D 스크린을 채우는 quad를 렌더링할 것이고 (후처리 효과처럼), 그리고 각 픽셀에대해 비싼 lighting fragment shader를 실행할 것이다:

우리는 렌더링 하기전에 G-buffer의 모든 관련된 텍스쳐들을 바인드 시키고 또한 그 쉐이더에 lighting 관련 uniform 변수들을 보낸다.

lighting pass의 fragment shader는 크게 우리가 이제까지 사용한 lighting toturial shaders들과 비슷하다. 새로운 것은 우리가 이제 G-buffer로부터 직접 샘플링하는 lighitng의 input 변수들을 얻는 방식이다:


#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

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, TexCoords).rgb;
    vec3 Normal = texture(gNormal, TexCoords).rgb;
    vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb;
    float Specular = texture(gAlbedoSpec,TexCoords).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);

    // post=processing for HDR with tone mapping and gamma corection
    const float gamma = 2.2;
    const float exposure = 1.0;

    vec3 hdrColor = lighting;
    vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
    mapped = pow(mapped, vec3(1.0 / gamma));
    FragColor = vec4(mapped, 1.0);
}

lighting pass shader는 G-buffer를 나타내고 우리가 geometry pass에서 저장한 모든 데이터를 보유한 3개의 uniform textures를 받아들인다. 만약 우리가 이러한 것들을 현재의 fragment의 texture coordinates로 샘플링 하려고 한다면, 우리는 정확히 같은 fragment variables를 얻는다. 마치 우리가 그 geometry를 직접적으로 렌더링하는 것처럼. fragment shader의 시작에서, 우리는 lighting과 관련된 변수들을 G-buffer texture로부터 간단한 texture lookup으로 얻어온다. 우리가 단일의 gAlbedoSpec texture로부터 Albedo color와 Specular intensity 둘 다 얻어오는 것에 주목해라.

우리가 이제 Blinn-Phong lighting을 계산하기 위해 필수적인 per-fragment 변수들을 (그리고 관련된 uniform 변수들을) 가졌으니, 우리는 lighting code를 바꿀 필요가 없다. 우리가 deferred shading에서 바꿔야 할 유일한 것은 lighting input variables를 얻는 방식이다.

총 32개의 작은 lights를 가진 simple demo를 작동시키는 것은 이것처럼 보인다:


deferred shading의 단점들 중 하나는 blending을 하는 것이 불가능 하다는 것이다. G-buffer에 있는 모든값들이 단일 fragments로부터 오기 때문이고, blending은 여러가지 fragments들의 조합으로 작동하기 때문이다. 또 다른 단점은 deferred shading이 너가 너의 대부분의 scene의 lighting에 대해 같은 lighting algorithm을 사용하도록 강요하는 것이다: 너는 어느정도 이것을 G-buffer에 material-특정 data를 포함시켜서 완화할 수 있다.

이러한 단점들을 (특히 blending) 극복하기 위해서, 우리는 종종 그 renderer를 두 부분으로 쪼갠다: 하나는 deferred rendering part, 그리고 다른 하나는 특히 blending 또는 deferred rendering pipeline에 적합하지 않은 특별한 쉐이더 효과 또는 blending을 위한 forward rendering part. 이것이 어떻게 작동하는지를 보여주기 위해서, 우리는 forward renderer를 사용하여 광원들을 작은 cubes들로 렌더링할 것이다. 그 light cubes들은 특별한 쉐이더를 요구하기 때문이다. (간단히 하나의 light color를 output해내는).

Combining deferred rendering with forward rendering
deferred render와 함께, light의 컬러를 뿜어내는 light source의 위치에 있는 3D cube로 광원 각각을 렌더링하고 싶다고 하자. 머리에 떠오르는 첫 번째 아이디어는 모든 광원들을 간단히 deferred shading pipeline 끝에 deferred lighting quad의 위에 forward render하는 것이다. 그래서 기본적으로 우리가 보통 하듯이, cubes들을 렌더링한다. 오직 우리가 deferred rendering operations을 끝낸후에. 코드에서 이것은 이것처럼 보일 것이다:

그러나, 이러한 렌더링된 큐브들은 그 deferred renderer의 저장된 geometry depth를 전혀 고려하지 않고, 결과적으로 항상 이전에 렌더링된 오브젝트들의 위에 렌더링된다; 이것은 우리가 찾는 결과가 아니다.

우리가 할 필요가 있는것은 처음에 geometry pass에 저장된 depth information을 default framebuffer의 depth buffer에 복사하고, 그러고나서 그 light cubes를 렌더링하는 것이다. 이 방식으로 그 light cubes들의 fragments가 이전에 렌더링된 geometry의 위에 있을 때 렌더링된다.

우리는 framebuffer의 내용을 glBlitFramebuffer의 도움으로 다른 framebuffer의 내용으로 복사할 수 있다. 이 함수는 우리가 multisampled framebuffers를 resolve하기위해 anti-aliasing tutorial에서 사용했던 것이다. glBlitFramebuffer 함수는 우리가 한 framebuffer의 사용자 정의 영역을 다른 프레임버퍼의 사용자 정의 영역으로 복사시키게 한다.

우리는 gBuffer FBO에서 deferred shading pass에 렌더링된 모든 오브젝트들의 깊이를 저장했다. 만약 우리가 간단히 그것의 depth buffer의 내용을 default framebuffer의 깊이 버퍼에 복사하려 한다면, 그 light cubes는 그러고나서 그 scene의 geometry 모든 것이 forward rendering으로 렌더링된 것처럼 렌더링 할 것이다. anti-aliasing 튜토리얼에서 간단히 설명했듯이, 우리는 한 framebuffer를 read framebuffer로 명시하고, 유사하게 한 framebuffer를 write framebuffer로 명시한다:


glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // write to default framebuffer
glBlitFramebuffer(0, 0, SCREEN_WIDTH, SCREEN_HEGIHT,
 0, 0, SCREEN_WIDTH, SCREEN_HEGIHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
lightBoxShader.use();
lightBoxShader.setMat4("view", view);
lightBoxShader.setMat4("projection", projection);
for (unsigned int i = 0; i < lightPositions.size(); ++i)
{
 glm::mat4 t_model(1.0);
 t_model = glm::translate(t_model, lightPositions[i]);
 t_model = glm::scale(t_model, glm::vec3(0.25f));
 lightBoxShader.setMat4("model", t_model);
 lightBoxShader.setVec3("lightColor", lightColors[i]);
 renderCube();
}

여기에서 우리는 read framebuffer의 전체 depth buffer content를 default framebuffer의 depth buffer에 복사한다; 이것은 유사하게 colorbuffers와 stencil buffers에도 될 수 있다. 이제 우리가 그 light cubes를 렌더링한다면, 그 cubes들은 정말로 그 scene의 geometriy가 진짜이고 2D quad위에 간단히 복사되지 않은 것처럼 행동한다:


너는 여기에서 그 데모의 풀 소스코드를 볼 수 있다.

이 접근 방법으로 우리는 쉽게 deferred shading과 forward shading을 합칠 수 있다.우리가 이제 blending을 적용하고 특별한 쉐이더 이펙트들을 요구하는 오브젝트들을 렌더링 할 수 있으니 이것은 훌륭하다. deferred rendering context에서는 불가능한 것이다.

A large number of lights
deferred rendering이 칭찬받는 것은 성능에 많은 비용이 들어가는 것 없이 엄청난 양의 광원을 렌더링하는 능력이다. Deferred rendering은 그 자체로 많은 양의 광원들을 허용하지 않는다. 우리가 여전히 그 scene의 광원들 각각에 대해 각 fragment의 lighting component를 계산해야만 하기 때문이다. 많은 양의 광원을 가능하게 하는 것은 deferred rendering pipeline에 적용할 수 있는 매우 깔끔한 최적화이다 : light volumes 기법.

보통, 우리가 크게 lighted되는 scene에서 한 fragment를 렌더링할 때, 우리는 한 scene에서 각 light source의 contribution을 계산하곤 한다. fragment에 대해 그것들의 거리와 상관없이. 이러한 광원들의 큰 부분은 결코 fragment에 도달하지 않을 것이다. 그래서 왜 모든 lighting 연산들을 낭비하는가?

light volumes 뒤에 있는 아이디어는 광원의 radius(반경) 또는 volume을 계산하는 것이다. 즉, 그것의 빛이 fragments에 도달할 수 있는 지역이다. 대부분의 광원들이 attenuation의 형태를 사용하기에, 우리는 그것들의 빛이 도달할 수 있는 최대 거리 또는 반경을 계산하기 위해 그것을 사용할 수 있다. 우리는 그러고나서 한 fragment가 이러한 light volumes의 하나 이상 안에 있다면 비싼 lighting calculations을 한다. 이것은 우리에게 상당한 양의 연산을 아껴준다. 우리는 오직 필요한 곳에서만 lighting을 계산하기 때문이다.

이 접근법의 트릭은 대개 광원의 light volume의 크기 또는 radius를 알아내는 것이다.

Calculating a light's volume or radius
light의 volume raidus를 얻기위해, 우리는 기본적으로 우리가 어둡다고 여기는 한 밝기에 대한 attenuation equation을 풀어야 한다.  이것은 0.0 또는 좀 더 밝은 것이 될 수 있지만, 여전히 0.03과 같이 어둡다고 고려된다. 우리가 어떻게 light의 volume radius를 계산할 수 있는지를 보여주기 위해서, 우리는 우리가 light casters tutorials에서 도입했던 가장 어렵지만 광범위한 attenuation functions 중의 하나를 사용할 것이다:



우리가 하기 원하는 것은 F_light가 0.0일 때의 방정식을 푸는 것이다. 즉, 그 빛이 그 거리에서 완전히 어두울 때이다. 그러나, 이 방정식은 결코 0.0의 값에 도달하지 않을 것이다.그래서 해는 없을 것이다. 그러나 우리가 할 수 있는 것은 0.0에 대해 방정식을 해결하는 것이 아니라, 0.0에 근접하지만 여전히 어둡다고 인지되는 한 밝기 값에 대해 그것을 해결하는 것이다. 우리가 이 튜토리얼의 데모 씬에 적절하여 고른 밝기 값은 5/256이다; 기본 8-bit framebuffer가 component마다 많은강도를보여줄 수 있기에 256로 나누어 졌다.

  Green Box
  사용된 attenuation function은 대개 그것의 보이는 범위에서 어둡다. 그래서 만약 우리가 그것을 5/256보다 더 어두운 밝기로 제한한다면, light volume은 너무 커서 따라서 덜 효과적이다. 한 사용자가 volume borders에서 광원의 갑작스러운 cut-off를 볼 수 없는 한, 괜찮을 것이다. 물론 이것은 항상 한 scene의 type에 의존한다; 더 높은 밝기 threshold는 더 작은 light volumes을 만들어내고, 따라서 더 효율성이 좋지만, lighting이 volume's borders에서 멈춘 것 같은 눈에 띌만한 artifacts를 만들 수 있다.

우리가 풀어야 할 attentuation equation은



여기에서 I_max는 광원의 가장 밝은 color component이다. 우리는 빛의 가장 밝은 강도에 대해 방정식을 푸는 것이 이상적인 light volume radius를 가장 잘 반영하기에, 한 광원은 가장 밝은 컬러 컴포넌트를 사용한다.

여기부터 우리는 그 방정식을 푼다:


그 마지막 방정식은 ax^2 + bx + c = 0의 형태의 방정식이다. 우리는 이것을 2차 방젖ㅇ식을 사용하여 풀 수 있다:



이것은 우리에게 x를 계산하도록 하는 일반 방정식을 준다. 즉, constant, linear and quadratic parameter가 주어질 때 광원에 대해 light volume의 radius이다:


float linear = 0.7;
float quadratic = 1.8;
for (unsigned int i = 0; i < NR_LIGHTS; ++i)
{
 float lightMax = std::fmaxf(std::fmaxf(lightColors[i].r, lightColors[i].g), lightColors[i].b);
 float radius =
  (-linear + std::sqrtf(linear * linear - 4 * quadratic * (constant - (256.0 / 5.0) * lightMax)))
  / (2 * quadratic);
 lightRadius.push_back(radius);
}

이것은 대강 light의 최대 밝기를 기반으로 1.0과 5.0 사이의 radius를 반환한다.

우리는 scene의 각 light source에 대해 이 radius를 계산하고, 한 fragment가 그 광원의 volume에 있다면 그 광원에 대해 lighting을 계산하기 위해 그것을 사용한다. 그 계산된 light volumes를 고려하는 업데이트된 lighting pass fragment shader가 아래에 있다. 이 접근법은 가르치는 목적으로만 단순히 되어있고 우리가 곧 이야기할 실제적인 환경에서는 실행가능하지 않다는 것을주목해라.


#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

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

struct Light
{
    vec3 Position;
    vec3 Color;

    float Linear;
    float Quadratic;
    float Radius;
};

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

void main()
{
    // retrieve data from G-buffer
    vec3 FragPos = texture(gPosition, TexCoords).rgb;
    vec3 Normal = texture(gNormal, TexCoords).rgb;
    vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb;
    float Specular = texture(gAlbedoSpec,TexCoords).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)
    {
        // calculate distance between light source and current fragment
        float distance = length(lights[i].Position - FragPos);

        if(distance < lights[i].Radius)
        {
            // diffuse
            vec3 lightDir = normalize(lights[i].Position - FragPos);
            vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Albedo * lights[i].Color;

            vec3 halfwayDir = normalize(lightDir + viewDir);
            float spec = pow(max(dot(Normal, halfwayDir), 0.0), 64.0);
            vec3 specular = spec * Specular * lights[i].Color;

            float attenuation = (1.0) / (1.0 + lights[i].Linear * distance + lights[i].Quadratic * distance * distance);

            diffuse *= attenuation;
            specular *= attenuation;

            lighting += diffuse + specular;
        }
    }
    
    // post=processing for HDR with tone mapping and gamma corection
    const float gamma = 2.2;
    const float exposure = 1.0;

    vec3 hdrColor = lighting;
    vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
    mapped = pow(mapped, vec3(1.0 / gamma));
    FragColor = vec4(mapped, 1.0);
}


그 결과는 이전과 정확히 같지만, 이번에는 각 light는 그것이 volume안에 있는 광원에 대해서만 lighting을 계산한다.

너는 여기에서 그 데모의 최종 소스코드를 볼 수 있다.

How we really use light volumes
위에서 보여준 fragment shader는 실제로 작동하지 않고 우리가 어떻게 어느정도 lighting calculations을줄이기 위해 light의 volume을 사용할 수 있는지만을 보여준다. 현실은 너의 GPU와 GLSL가 loops와 branch를 최적화하는데에 나쁘다는 것이다. 이것에 대한 이유는 GPU에서 쉐이더 실행은 매우 병렬적이고 대부분의 아키텍쳐들이 효율적이기 위해서 정확히 같은 쉐이더 코드를 작동시킬 필요가 있는 큰 threads들의 모음에 대해 요구사항을 가지기 때문이다. 이것은 그 쉐이더작동이 항상 같도록 하기 위해서, if문의 모든 branches들을 항상 실행하도록 하는 한 쉐이더가 작동된다는 것이고. 이것은 우리의 이전의radius check optimization이 완전히 쓸모없게 만든다; 우리는 여전히 모든 light sources에 대해 계산하고 있는 것이다!

light volumes를 사용하는 것에 대한 적절한 접근법은 실제로 light volume radius로 스케일링 된 구를 렌더링하는 것이다. 이러한 구들의 중심은 그 light source의 위치에 있고, 그것이 light volume radius로 스케일링 되기 때문에, 그 구는 정확히 그 light의 보이는 volume을 수반한다. 이것이 트릭이 오는 곳이다: 우리는 구를 렌더링하기 위해 같은 deferred fragment shader를 크게 사용한다. 그 렌더링된 sphere가 광원이 영향을 미치는 픽셀과 정확히 일치하는 fragment shader invocations(호출)을 만들어 내기 때문에, 우리는 관련된 픽셀만을 렌더링하고 모든 다른 픽셀들은 건너뛴다. 아래의 이미지가 이것을 보여준다:

이것은 그 scene에서 각 광원을 위해 되고, 최종 fragments는 추가적으로 함께 blended도니다. 그 결과는 그러고나서 이전과 정화히 같은 scene이지만, 이번에 광원마다오직관련된 fragments만을 렌더링 한다. 이것은 효과적으로 nr_object * nr_lights의 연산을 nr_objects + nr_lights로 줄여준다. 그리고 이것은 수 많은 lights가 있는 scenes들에서 엄청나게 효율적으로 만들어준다.

이 접근법으로 한 가지 문제가 여전히 있다: face culling이 활성화 되어야 한다 (그렇지 않으면 우리는 한 빛의 effect를 두 번 렌더링한다) 그리고 그것이 활성화 되었을 때, 그 사용자는 한 광원의 volume에 들어갈지도 모른다. 거기에서 그 volume은 더 이상 렌더링 되지 않는다 (back-face culling 때문에), 이것은 광원의 영향을 제거해버린다; 이것은 깔끔한 stencil buffer trick으로 해결될 수 있다.

light volumes을 렌더링 하는 것은 성능에 있어서 엄청난 비용이 필요하다. 그리고 그것이 일반적으로 deferred shading보다 더 빠를지라도, 그것이 최상의 최적화는 아니다. deferred shading에 더해서 두 가지 다른 인기있는 (그리고 더 효율적인) 확장인 deferred lighting과 tile-based deferred shading이 존재한다. 이러한 것들은 수 많은 light를 렌더링하는데 엄청나게 효율적이고 또한 상대적으로 효율적인 MSAA를 허용한다. 그러나, 이 튜토리얼의 길이를 위해서, 나는 이 최적화를 나중의 튜토리얼로 남겨둘 것이다.

Deferred rendering vs forward rendering
그 자체로 (light volumes 없이) deferred shading은 이미 큰 최적화이다. 각 픽셀이 오직 하나의 fragment shader만을 작동시키기 때문이다. 우리가 종종 pxiel당 여러번 fragment shader를 작동시키는 forward rendering과 비교한다면. Deferred rendering은 몇 가지 불이익이 있다: 큰 메모리 오버헤드, MSAA 안됌, 그리고 blending은 여전히 forward rendering으로 처리되어야만한다.

너가 작은 scene을 가지고 너무 많은 lights를 가지고 있지 않을 때, deferred rendering은 필연적으로 더 빠르지 않고 가끔은 더 느리다. 왜냐하면 그 오버헤드가 deferred rendering의 장점을 능가해버리기 때문이다. 좀 더 복잡한 scenes에서 deferred rendering은 빠르게 중요한 최적화가 된다; 특히 좀 더 고급 최적화 확장과 함께

마지막 note로서, 나는 기본적으로 forward rendering으로 이루어질 수 있는 모든 이펙드들이 또한 deferred rendering context에서 구현되어 질 수 있다고 언급하고 싶다; 이것은 종종 작은 translation step만을 요구한다. 예를들어, 만약 우리가 deferred renderer에서 normal mapping을 원한다면, 우리는 normal map으로부터 (TBN 행렬을 사용해서) surface normal 대신에 추출된 world-space normal을 만들어내기 위해서 geometry pass shaders를 바꿔야한다; lighting pass에서 조명 계산은 전혀 바뀔 필요가 없다. 그래서 만약 너가 parallax mapping 작동시키길 원한다면, 너는 한 오브젝트의 diffuse, specular or normal textures들을 샘플링 하기전에 geometry pass에서 텍스쳐 좌표를 처음에 displace 시키길 원할 것이다. 일단 너가 deferred rendering 뒤에 있는 아이디어를 이해하기만 한다면, 창의적이게 되는 것은 어렵지 않다.

Additional resources

===========================================
중요한 deferred shading을 끝냈다. 이 부분은 상당히 중요한데, 성능을 상당히 개선시킬 수 있기 때문이다. 그래서, 마지막에 언급한 좀 더 advanced된 deferred shading을 하려면 많은 시간이 요구될 것 같다.

이것은 개발할 때 어떤 rendering을 선택할지 잘 골라야 할 것 같다.





댓글 없음:

댓글 쓰기