본문 바로가기

Game/Graphics

Learn OpenGL - Advanced Lighting : Deferred Shading

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


Deffferd Shading


 지금까지 조명을 사용한 방식은 Forward Rendering 또는 Forward Shading 이라고 불렀다. 즉, 객체를 렌더링하고 장면의 모든 광원에


따라 빛을 비운 다음 객체를 렌더링하는 등의 직접적인 방법이었다. 상당히 이해하기 쉽고 구현하기는 하지만 각 렌더링된 객체가


렌더링 된 모든 조각에 대해 각 광원에 대해 반복해야하므로 성능면에서 상당히 중요하다! 또한, 앞으로 렌더링은 대부분의 조각 쉐이더


출력을 덮어쓰므로 깊이 복잡도가 높은 장면에서 많은 조각 쉐이더 실행을 낭비하는 경향이 있따. (여러 객체가 동일한 화면 픽셀을 가리는 경우)



 지연된 음영 또는 지연 렌더링은 객체를 렌더링하는 방식을 크게 변경하는 이러한 문제를 극복하려고 시도한다. 이렇게 하면 많은 수의


조명을 사용해 장면을 상당히 최적화 할 수 있는 몇 가지 새로운 옵션이 제공되므로 허용되는 프레임 속도로 수백 또는 수천 개의 조명을


렌더링 할 수 있다. 다음은 지연된 음영으로 렌더링된 1847 포인트 라이트가 있는 이미지이다. 앞으로 렌더링 할 때 불가능할 것이다.


Example of the power of deferred shading in OpenGL as we can easily render 1000s lights with an acceptable framerate


 Deffered shading은 조명과 같은 무거운 렌더링의 대부분을 이후 단계로 연기 또는 연기한다는 개념에 기반한다. Defferd shading은


두 개의 패스로 구성된다. 첫 번째 패스에서는 형상 패스라고 하며 장면을 한 번 렌더링하고 G-버퍼라고 하는 텍스처 모음에 저장한


모든 종류의 기하학적 정보를 검색한다. 위치 벡터, 색 벡터, 법선 벡터, 반사 값을 생각해라. G 버퍼에 저장된 장면의 기하학적 정보는


나중에 (더 복잡한) 조명 계산에 사용된다. 아래는단일 프레임의 G 버퍼의 내용이다.


An example of a G-Buffer filled with geometrical data of a scene in OpenGL


 우리는 screen-filled 쿼드를 렌더링하고 G-버퍼에 저장된 기하학적 정보를 사용해 각 조각에 대한 장면의 조명을 계산하는 라이팅 패스라는


두 번째 패스에서 G-버퍼의 텍스처를 사용한다. 픽셀 단위로 G 버퍼를 반복한다. 정점 쉐이더에서 조각 쉐이더까지 모든 객체를 가져오는


대신 고급 조각 프로세스를 후반 단계로 분리한다. 조명 계산은 이전과 완전히 동일하지만, 이번에는 정점 쉐이더 대신 해당 G-버퍼 텍스처로부터


필요한 모든 입력 변수를 취한다.



 아래 이미지는 Deferred shading의 전체 과정을 보여준다.


Overview of the deferred shading technique in OpenGL


 이 접근법의 가장 큰 장점은 깊이 테스트가 이미 이 조각 정보를 최상단 조각으로 결론짓기 때문에 G-버퍼에서 끝나는 모든 조각이 화면 픽셀로


끝나는 실제 조각 정보라는 것이다. 이렇게하면 조명 패스에서 처리하는 각 픽셀마다 한 번만 수행된다.


사용되지 않는 많은 렌더링 호출로부터 우리는 절약 할 수 있다. 또한, 지연 렌더링은 앞으로의 렌더링에서 사용할 수 있는 것보다 훨씬


많은 양의 광원을 렌더링 할 수 있는 추가 최적화 가능성을 열어준다.



 또한, G 버퍼는 위치 벡터와 같은 장면 데이터가 고정밀도로 필요하기 때문에 메모리를 차지하는 텍스처 색상 버퍼에 비교적 많은 양의


장면 데이터를 저장해야하기 때문에 몇 가지 단점이 있다.또 다른 단점은 블렌딩을 지원하지 않으며 MSAA가 더 이상 작동하지 않는다는 것이다.


(우리가 최상위 단편의 정보만 가지고 있기 때문에)


이 단점에 대한 몇 가지 해결 방법이 이 튜토리얼의 끝에 있다.



 Geometry pass에 G 버퍼를 채우는 것은 위치, 색상, 법선과 같은 객체 정보를 처리량이 적거나 0인 프레임 버퍼에 직접 저장하기 때문에


매우 효율적이다. 다중 렌더링 타겟(MRT)을 사용함으로써 우리는 단일 렌더 패스에서 이 모든 작업을 수행 할 수도 있다.



The G-buffer


 G-버퍼는 최종 조명 패스에 대한 조명 관련 데이터를 저장하는데 사용되는 모든 텍스처의 총칭이다. 이제 앞으로 렌더링을 사용해 조각을


조명하는데 필요한 모든 데이터를 간략히 검토해보겠다.



- lightDir 및 viewDir에 사용된 조각 위치 변수를 계산하는 3D 위치 벡


- albedo라고 하는 RGB 확산 색상 벡터


- 표면의 기울기를 결정하기 위한 3D 법선 벡터


- A specular intensity float (반사 강도는 부동소수점)


- 모든 광원 위치 및 색상 벡터


- 플레이어 또는 뷰어의 위치 벡터



 우리가 처리 할 수 있는 이러한 조각마다 변수를 사용해 우리가 익숙한 (Blinn-)Phong 조명을 계산할 수 있다.


광원 위치와 색상 및 플레이어의 뷰 위치는 균일한 변수를 사용해 구성 할 수 있지만 다른 변수는 모두 객체 조각마다 고유하다.


최종 지연 조명 패스에 똑같은 데이터를 전달하면 2D 쿼드의 조각을 렌더링하더라도 동일한 조명 효과를 계산할 수 있다.



 OpenGL은 텍스처에 저장할 수 있는 것에 제한이 없으므로 G-buffer라는 하나 또는 여러 개의 screen-filled 텍스처에 모든 조각 데이터를


저장하고, 나중에 조명 패스에서 사용하는 것이 좋다. G-buffer 텍스처는 라이팅 패스의 2D 쿼드와 같은 크기를 가지므로 forward rendering


설정에서와 똑같은 조각 데이터를 얻는다. 하지만 이번에는 라이팅 패스에서 나타난다; 하나의 맵핑에는 하나가 있다.



 의사 코드에서는 전체 프로세스가 다음과 같아 보인다:

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();
}

 각 조각에 저장해야하는 데이터는 위치 벡터, 법선 벡터, 색상 벡터, 반사 강도 값이다. 따라서 geometry pass에서 장면의 모든 객체를


렌더링하고 이러한 데이터 구성 요소를 G-buffer에 저장해야한다. 우리는 다시 한 번 렌더 패스에서 여러 색상 버퍼로 렌더링하기 위해


여러 렌더링 타겟을 사용할 수 있다. 이것은 Bloom 튜토리얼에서 간략하게 논의되었다.



 geometry pass의 경우 여러 개의 colorbuffer가 연결된 gBuffer와 하나의 깊이 렌더 버퍼 객체를 직관적으로 호출 할 프레임 버퍼 객체를


초기화해야한다. position / normal texture의 경우 고정밀 텍스처(구성 요소당 16 또는 32 비트 부동 소수점)와 albedo 및 반사 값을


사용하는 것이 좋을 것이다. 기본 텍스처(구성요소당 8비트 정밀도)를 사용하면 문제가 없다.

unsigned int gBuffer;
glGenFramebuffers(1, &gBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
unsigned int gPosition, gNormal, gColorSpec;
  
// - position color buffer
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);
  
// - normal color buffer
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);
  
// - color + specular color buffer
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);
  
// - 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);
  
// then also add render buffer object as depth buffer and check for completeness.
[...]

 여러 렌더 타겟을 사용하기 때문에 우리는 glDrawBuffers로 렌더링하고자 하는 GBuffer와 관련된 컬러 버퍼 중 어떤 것을 OpenGL에

 

명시적으로 알려야한다. 여기서 주목해야 할 점은 위치와 일반 데이터를 각각 RGB 컴포지션으로 저장할 수 있지만 색상 및 반사 강도


데이터를 단일 RGBA 텍스처로 결합해 저장한다는 것이다. 이렇게 하면 추가적인 colorbuffer 텍스처를 선언하지 않아도 된다.


deferred shading 파이프 라인이 복잡해지고 더 많은 데이터가 필요하기 때문에 개별 텍스처에서 데이터를 결합하는 새로운 방법을 빠르게


찾을 수 있다.



 다음으로 G-buffer로 렌더링해야한다. 각각의 객체가 diffuse, normal, specular intensity 텍스처를 가지고 있다고 가정 할 때 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
    gAlbedoSpec.a = texture(texture_specular1, TexCoords).r;
}  

  우리가 여러개의 렌더 타겟을 사용할 때, 레이아웃 지정자는 OpenGL에게 우리가 렌더링 할 현재 활성화된 프레임 버퍼의 컬러 버퍼를


알려준다. 단일 float 값을 다른 색상 버퍼 텍스처 중 하나의 알파 구성 요소에 저장할 수 있으므로 반사 조명을 단일 색상 버퍼 텍스처에 저장하지


않는다.

조명 계산을 사용하면 모든 변수를 동일한 좌표 공간에 유지 하는 것이 매우 중요하다. 이 경우 우리는 모든 변수를 월드 공간에 저장(그리고 계산)한다.

 우리가 이제 gBuffer 프레임 버퍼에 대량의 nanosuit 객체를 렌더링하고 screen-filled 된 쿼드에 컬러 버퍼를 하나씩 투영해 내용을 시각화한다면


우리는 다음과 같은 것을 볼 수 있다:


Image of a G-Buffer in OpenGL with several nanosuits


 세게 공간의 위치와 법선 벡터가 실제로 맞는지 시각화 해보아라. 예를 들어, 오른쪽을 가리키는 법선 벡터는 장면의 원점에서


오른쪽으로 향하는 위치 벡터와 마찬가지로 빨간색으로 더 정렬된다. G-buffer의 내용에 만족하자마자 다음 단계인 lighting pass로 넘어갈 시간이다.



The deferred lighting pass


 우리가 처분할 수 있는 G-buffer의 많은 조각 데이터를 사용해 각 G-buffer 텍스처를 픽셀 단위로 반복하고 조명 알고리즘에 대한


입력으로 내용을 사용해 장면의 최종 조명된 색상을 완전히 계산할 수 있는 옵션이 있다. G-buffer 텍스처 값은 모두 최종 변환된 단편 값을


나타내므로 픽셀당 한 번 비싼 조명 작업만 수행하면 된다. 이로 인해 deferred shading이 매우 효율적이다. 특히 복잡한 렌더링에서는


앞으로 렌더링 설정에서 픽셀당 여러 개의 비싼 조각 쉐이더 호출을 쉽게 호출 할 수 있다.



 조명 패스의 경우 2D screen-filled 쿼드를 렌더링하고 각 픽셀에 비싼 조명 조각 쉐이더를 실행한다:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, gPosition);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, gNormal);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
// also send light relevant uniforms
shaderLightingPass.use();
SendAllLightUniformsToShader(shaderLightingPass);
shaderLightingPass.setVec3("viewPos", camera.Position);
RenderQuad();  

  렌더링 전에 G-buffer의 모든 관련 텍스처를 바인딩하고 조명 관련 균일 변수를 쉐이더에 보낸다.



  조명 패스의 조각 쉐이더는 지금까지 사용해온 조명 튜토리얼 쉐이더와 거의 유사하다. 새로운 점은 G-buffer에서 직접 샘플링하는 조명의


입력 변수를 얻는 방법이다:

#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);
}  

 조명 패스 쉐이더는 G-buffer를 나타내는 3개의 균일한 텍스처를 받아들이고 geometry pass에 저장한 모든 데이터를 보유한다.


이들은 현재 조각의 텍스처 좌표로 샘플링한다면 geometry를 직접 렌더링 하는 것과 동일한 조각 값을 얻을 수 있다.


조각 쉐이더의 시작 부분에서 우리는 간단한 텍스처 룩업을 통해 G-buffer 텍스처로부터 조명 관련 변수를 가져온다.


우리는 Albedo 색상과 specular intensity를 하나의 gAlbedoSpec 텍스처에서 가져온다.



 Blinn-Phong 조명을 계산하기 위해 필요한 per-fragment 변수가 있으므로 조명 코드를 변경할 필요가 없다. deferred shading에서 변경되는


유일한 방법은 조명 입력 변수를 얻는 방법이다.



 총 32개의 작은 표시등으로 간단한 데몰르 실행하면 다음과 같이 보인다:


Example of Deferred Shading in OpenGL


 Deferred shading이 단점 중 하나는 G-buffer의 모든 값이 단일 조각에서 왔고, 블렌딩이 여러 조각의 조합에서 작동하기 때문에


블렌딩을 수행 할 수 없다는 것이다. 또 다른 단점은 deferred shading이 대부분의 장면 조명에 대해 동일한 조명 알고리즘을 사용해야한다는


것이다. 당신은 G-buffer에 더 많은 material-specific 데이터를 포함시킴으로써 이것을 다소 완화할 수 있다.



 이러한 단점을 극복하기 위해 렌더러를 종종 지연된 렌더링 부분과 지연 렌더링 파이프 라인에 적합하지 않은 블렌딩 또는 특수 쉐이더


효과를 위한 앞으로 렌더링 부분으로 나누었다. 이것이 어떻게 작동하는지 설명하기 위해 라이트 큐브는 특수한 쉐이더가 필요하므로


앞으로 렌더러를 사용해 광원을 작은 큐브로 렌더링한다.




Combining deferred rendering with forward rendering


 우리는 각각의 광원을 deferred cube renderer 옆에 있는 빛의 색을 방출하는 광원의 위치에 있는 3D 큐브로 렌더링하려고 한다.


마음에 떠오르는 첫 번째 아이디어는 모든 광원을 deferred shading 파이프 라인의 끝 부분에 있는 deferred light quad 위에 간단히


포워드하는 것이다. 기본적으로 우리가 평소처럼 큐브를 렌더링하지만 지연된 렌더링 작업을 마친 후에야 큐브를 렌더링한다.


코드에서 이것은 다음과 같이 보일 것이다:

// deferred lighting pass
[...]
RenderQuad();
  
// now render all light cubes with forward rendering as we'd normally do
shaderLightBox.use();
shaderLightBox.setMat4("projection", projection);
shaderLightBox.setMat4("view", view);
for (unsigned int i = 0; i < lightPositions.size(); i++)
{
    model = glm::mat4();
    model = glm::translate(model, lightPositions[i]);
    model = glm::scale(model, glm::vec3(0.25f));
    shaderLightBox.setMat4("model", model);
    shaderLightBox.setVec3("lightColor", lightColors[i]);
    RenderCube();
}

 그러나 이러한 렌더링된 큐브는 지연 렌더러의 저장된 geometry 깊이를 고려하지 않으며 결과적으로 항상 이전에 렌더링 된 객체 위에


렌더링된다. 이것은 우리가 찾던 결과가 아니다.


Image of deferred rendering with forward rendering where we didn't copy depth buffer data and lights are rendered on top of all geometry in OpenGL


 우리가 해야할 일은 geometry pass에 저장된 깊이 정보를 기본 프레임 버퍼의 깊이 버퍼에 복사 한 다음 가벼운 큐브를 렌더링하는 것이다.


이렇게 하면 라이트 큐브의 조각은 이전에 렌더링된 geometry 위에 있을때만 렌더링된다.



 우리는 anti-aliasing 튜토리얼에서 다중 샘플 프레임 버퍼를 해결하기 위해 glBlitFramebuffer 함수를 사용해 다른 프레임 버퍼의 내용에


프레임 버퍼의 내용을 복사 할 수 있다. glBlitFramebuffer 함수를 사용하면 프레임 버퍼의 사용자 정의 영역을 다른 프레임 버퍼의 사용자 정의


영역에 복사 할 수 있다.



 Deferred shading pass에 렌더링된 모든 객체의 깊이를 gBufferFBO에 저장했다. 깊이 버퍼의 내용을 기본 프레임 버퍼의 depth 버퍼로


복사하면 조명 큐브는 장면의 모든 geometry가 forward 렌더링으로 렌더링된 것처럼 된다. Anti-aliasing 튜토리얼에서 간략하게 설명했듯이


프레임 버퍼를 읽기 프레임 버퍼로 지정하고 마찬가지로 프레임 버퍼를 쓰기 프레임 버퍼로 지정해야한다:

glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // write to default framebuffer
glBlitFramebuffer(
  0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST
);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// now render light cubes as before
[...]  

 여기서 우리는 전체 읽기 프레임 버퍼의 깊이 버퍼 내용을 기본 프레임 버퍼의 깊이 버퍼에 복사한다. colorbuffers 및 stencil 버퍼에 대해서도


비슷하게 수행 할 수 있다. 이제 라이트 큐브를 렌더링하면 큐브는 실제로 장면의 geometry가 real이며 2D 쿼드의 위에 붙여지지 않는 것처럼


행동한다:


Image of deferred rendering with forward rendering where we copied the depth buffer data and lights are rendered properly with all  geometry in OpenGL


 이 방법을 사용하면 deferred shading과 forward shading을 쉽게 결합 할 수 있다. 이것은 쉐이더 특수 효과가 필요한 객체를 블렌드하고


렌더링 할 수 있기 때문에 훌륭하다. Deferred 렌더링 환경에서는 불가능한 것이다.




A larger number of lights


  deferred rendering은 종종 성능에 대한 막대한 비용을 들이지 않고 엄청난 양의 광원을 렌더링 할 수 있다는 점에서 높이 평가된다.


지연 렌더링은 자체 장면의 광원마다 각 조각의 조명 구성 요소를 계산해야하므로 매우 많은 양의 광원을 허용하지 않는다.


많은 양의 광원을 가능하게 하는 것은 지연된 렌더링 파이프 라인에 적용 할 수 있는 매우 정교한 최적화이다.


즉, 가벼운 볼륨이다.



 일반적으로 대형 라이트된 장면에서 조각을 렌더링 할 때 조각과의 거리에 관계없이 장면에서 각 광원의 기여도를 계산한다.


이러한 광원의 대부분은 조각에 도달하지 않는데 왜 조명 계산을 낭비할까?



 Light volume의 아이디어는 광원의 반경 또는 부피, 즉 그 빛이 조각에 도달 할 수 있는 영역을 계산하는 것이다. 대부분의 광원은


어떤 형태의 감쇠를 사용하기 때문에 광원에서 도달 할 수 있는 최대 거리 또는 반경을 계산하는데 사용할 수 있다.


그런 다음 하나의 조각이 이러한 조명 볼륨 중 하나 이상에 있으면 값 비싼 조명 계산만 수행한다. 이렇게 하면 필요할 때만


조명을 계산하므로 상당히 많은 계산량을 절약 할 수 있다.



 이 방법의 트릭은 대부분 광원의 광량의 크기 또는 반경을 알아내는 것이다.



Calculating a light's volume or radius


 빛의 볼륨 반경을 구하기 위해서는 근본적으로 우리가 밝혀야 할 밝기에 대한 감쇠 방정식의 해를 구해야한다. 이것은 0.0이거나


조금 더 밝아졌지만 여전히 0.03과 같이 어둡다. 라이트의 볼륨 반경을 계산하는 방법을 보여주기 위해 light caster 튜토리얼에서


소개한 보다 어렵지만 광범위한 감쇠 함수 중 하나를 사용한다:



 우리가 원하는 것은 Flight가 0.0 일 때 이 방정식을 푸는 것이다. 빛이 그 거리에서 완전히 어두울 때이다. 그러나 이 방정식은


절대 값 0.0에 도달하지 않으므로 해결책이 없다. 그러나 우리가 할 수 있는 것은 0.0에 대한 방정식을 풀지는 않지만


0.0에 가까우면서도 여전히 어둡게 인식되는 밝기 값으로 해결한다. 이 튜토리얼의 데모 장면에서 허용하는 밝기 값은 5/256 이다.


기본 8비트 프레임 버퍼가 구성 요소당 여러 개의 강도를 표시 할 수 있으므로 256으로 나눈 값이다.

사용되는 감쇠 기능은 가시 범위에서 대부분 어둡기 때문에 5/256보다 더 어두운 밝기로 제한하면 광량이 너무 커져 효과가 떨어진다. 볼륨 경계에서 광원이 갑자기 끊어지는 것을 사용자가 볼 수 없는한 우리는 괜찮을 것이다. 물론 이것은 항상 장면의 유형에 달려있다. 밝기 임계 값이 높을수록 조명 볼륨이 작아지고 효율성이 높아지지만 조명이 볼륨의 경계에서 깨지는 것처럼 보이는 현저한 인공물이 생성될 수 있다.

 우리가 풀어야하는 감쇠 방정식은 다음과 같다:



 여기서 I는 광원의 가장 밝은 색상 요소이다. 빛의 가장 밝은 색상 값을 위한 방정식을 해결하면 이상적인 빛의 볼륨 반경을 가장 잘 반영하기


때문에 광원의 가장 밝은 색상 구성요소를 사용한다.



 여기에서 계속해서 방정식을 풀어보겠다:




 마지막 방정식은 ax^2 + bx + c = 0 형태의 방정식으로 이차 방정식을 사용해 풀 수 있다:



 이것은 일정한 선형 및 2차 파라미터가 주어진 광원에 대한 x의 빛의 반경을 계산할 수 있는 일반 방정식을 제공한다:

float constant  = 1.0; 
float linear    = 0.7;
float quadratic = 1.8;
float lightMax  = std::fmaxf(std::fmaxf(lightColor.r, lightColor.g), lightColor.b);
float radius    = 
  (-linear +  std::sqrtf(linear * linear - 4 * quadratic * (constant - (256.0 / 5.0) * lightMax))) 
  / (2 * quadratic);  

 광원의 최대 강도를 기준으로 반경이 대략 1.0에서 5.0 사이의 반경을 반환한다.



 우리는 장면의 각 광원에 대해 이 반지름을 계산하고 조각이 광원의 볼륨 안에 있는 경우 해당 광원의 조명만 계산하는데 사용한다.


아래는 계산된 조명 볼륨을 고려한 업데이트된 조명 패스 조각 쉐이더이다. 이 접근법은 단지 가르치기 위한 목적으로만 이루어졌으며


우리가 곧 논의할 실제적인 환경에서 실행 가능하지 않다는 점을 유의해라:

struct Light {
    [...]
    float Radius;
}; 
  
void main()
{
    [...]
    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)
        {
            // do expensive lighting
            [...]
        }
    }   
}

 결과는 이전과 완전히 동일하지만 이번에는 각 광원이 볼륨이 있는 광원에 대한 조명만 계산한다.




How we really use light volumes


 위에서 보여준 조각 쉐이더는 실제로 작동하지 않으며 라이팅 계산을 줄이기 위해 라이트의 볼륨을 어떻게 사용할 수 있는지를 보여준다.


현실은 GPU와 GLSL이 루프와 브랜치를 최적화하는데 정말로 나쁘다는 것이다. 그 이유는 GPU에서의 쉐이더 실행은 매우 평행하고 대부분의


아키텍처는 스레드를 대량으로 수집하기 위해서는 효율적으로 동일한 쉐이더 코드를 실행해야한다는 요구 사항이 있다. 쉐이더가 실행되는 것을


보장하기 위해 항상 if 문의 모든 분기를 실행하는 쉐이더가 실행됨으로써 이전의 반경 검사 최적화가 완전히 쓸모없게 된다. 우리는 여전히


모든 광원에 대한 조명을 계산할 것이다!



 Light volume을 사용하는 적절한 방법은 실제 볼을 light volume 반경으로 축소해 렌더링하는 것이다. 이 구의 중심은 광원의 위치에 배치되며


광량 반경에 따라 축척되므로 구가 빛의 가시 볼륨을 정확히 포함한다. 이것은 트릭이 나오는 부분이다: 우리는 구를 렌더링하기 위해 대부분


동일한 deferred fragment shader를 사용한다. 렌더링 된 구체가 광원에 영향을 주는 픽셀과 정확하게 일치하는 조각 쉐이더 호출을


생성하기 때문에 관련 픽셀을 렌더링하고 다른 모든 픽셀은 건너뛴다. 아래 이미지는 이를 보여준다:


Image of a light volume rendered with a deferred fragment shader in OpenGL


 이 작업은 장면의 각 광원에 대해 수행되며 결과로 생성된 조각은 함께 추가적으로 혼합된다. 결과는 이전과 똑같은 장면이지만 이번에는


광원당 관련 조각만 렌더링한다. 이것은 nr_objects * nr_lights 에서부터 nr_objects + nr_lights까지 계산을 효과적으로 줄여주며


많은 수의 조명이 있는 장면에서 매우 효율적이다. 이 방법은 지연 렌더링을 많은 조명을 렌더링하는데 적합하게 만드는 것이다.



 이 방법은 여전히 문제가 있다. 즉, face culling 기능을 사용 설정해야한다. (그렇지 않으면 광원 효과를 두 번 렌더링한다)


사용 설정하면 광원 볼륨을 입력한 후 볼륨이 더 이상 렌더링되지 않는다. 이것은 깔끔한 stencil buffer 트릭으로 해결할 수 있다.



 조명 볼륨을 렌더링하면 성능이 떨어지고 일반적으로 deferred shading보다 빠르지만 최적의 최적화는 아니다. deferred shading을 기반으로


하는 두 가지 다른 인기있고 효율적인 확장이 deferred lighting과 tile-based deferred shading이다. 이들은 많은 양의 빛을 렌더링 할 때


매우 효율적이며 상대적으로 효율적인 MSAA를 허용한다. 그러나 이 튜토리얼의 길이를 위해 최적화는 나중에 할 것이다.




Deferred rendering vs forward rendering


 Light volume을 제외하더라도 deferred shading은 각 픽셀이 단 하나의 쉐이더만 실행하기 때문에 픽셀당 여러 번의


쉐이더를 실행하는 forward rendering에 비해 큰 최적화이다.


Deferred rendering에는 몇 가지 단점이 있다: 큰 메모리 오버 헤드 , forward rendering을 할 때 MSAA와 블렌딩을 수행해야한다.



 작은 장면이 있고 조명이 너무 많지 않은 경우 deferred rendering은 오버 렌더링이 지연 렌더링의 이점보다 중요하기 때문에


반드시 빠르지는 않고 때때로 느릴 수도 있다. 보다 복잡한 장면에서 지연 렌더링은 신속하게 중요한 최적화가 된다.


특히 고급 최적화 확장을 사용하면 더욱 그렇다.



 마지막으로, 앞으로 렌더링 할 때 얻을 수 있는 모든 효과는 기본적으로 지연 렌더링 컨텍스트에서 구현 될 수 있음을 언급하고자 한다.


이것은 종종 작은 translation 단계만 필요하다. 예를 들어, deferred renderer에서 일반 맵핑을 사용하려는 경우 표면 노멀 대신


표준 맵에서 추출한 월드 공간 법선을 출력하도록 geometry pass shader를 변경한다. 조명 패스의 조명 계산을 전혀 변경할 필요가 없다.


parallax mapping을 사용하려면 객체의 확산, 반사, 표준 텍스처를 샘플링하기 전에 geometry pass의 텍스처 좌표를 먼저 이동해야한다.


Deferred rendering의 아이디어를 이해하면 창의력을 발휘하기가 어렵지 않다.