눈팅하는 게임개발자 블로그

OpenGL Advanced Lighting 6-8 Deferred Shading 본문

공부한거/OpenGL

OpenGL Advanced Lighting 6-8 Deferred Shading

Palamore 2020. 12. 14. 18:03

원문 사이트

learnopengl.com/Advanced-Lighting/Deferred-Shading

 

LearnOpenGL - Deferred Shading

Deferred Shading Advanced-Lighting/Deferred-Shading The way we did lighting so far was called forward rendering or forward shading. A straightforward approach where we render an object and light it according to all light sources in a scene. We do this for

learnopengl.com

번역 사이트

gyutts.tistory.com/182?category=755809

 

Learn OpenGL - Advanced Lighting : Deferred Shading

link : https://learnopengl.com/Advanced-Lighting/Deferred-Shading Deffferd Shading  지금까지 조명을 사용한 방식은 Forward Rendering 또는 Forward Shading 이라고 불렀다. 즉, 객체를 렌더링하고 장면의..

gyutts.tistory.com

Deferred Shading

지금까지 조명을 사용한 방식은 Forward Rendering 또는 Forward Shading이라고 불렸다.

즉, 객체를 렌더링하고 장면의 모든 광원에 따라 빛을 비춘 다음 객체를 렌더링하는 등의 직접적인 방법이었다.

상당히 이해하기 쉽기는 하지만 각 렌더링된 객체가 렌더링된 모든 fragment에 대해 각 광원에 대해 반복해야 하므로

성능 면에서 상당히 무겁다.

또한, 앞으로 렌더링은 대부분의 fragment shader 출력을 덮어쓰므로 깊이 복잡도가 높은 장면에서 많은

fragment shader의 실행을 낭비하는 경향이 있다.(여러 객체가 동일한 화면 픽셀을 가리는 경우)

 

deferred shading 또는 deferred rendering은 객체를 렌더링하는 방식을 크게 변경하여 위와 같은 문제들을

극복하려고 시도한다. 이렇게 하면 많은 수의 조명을 사용해 장면을 상당히 최적화할 수 있는 몇 가지 새로운

옵션이 제공되므로 허용되는 프레임 속도로 수백 또는 수천 개의 조명을 렌더링할 수 있다.

다음은 deferred shading으로 렌더링된 1847개의 point light가 있는 이미지이다.

forward rendering으로는 실현 불가능하다.

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

deferred shading은 두 개의 패스로 구성된다. 첫 번째 패스에서는 형상 패스라고 하며 장면을 한 번 렌더링하고

G-버퍼라고 하는 텍스처 모음에 저장한 모든 종류의 기하학적 정보를 검색한다.

위치 벡터, 색 벡터, 법선 벡터, 반사 값을 생각하라.

G 버퍼에 저장된 장면의 기하학적 정보는 나중에 (더 복잡한) 조명 계산에 사용된다.

아래는 단일 프레임 G 버퍼의 내용이다.

screen-filled 쿼드를 렌더링하고 G-버퍼에 저장된 기하학적 정보를 사용해 각 fragment에 대한 scene의

조명을 계산하는 라이팅 패스라는 두 번째 패스에서 G-버퍼의 텍스처를 사용한다.

픽셀 단위로 G 버퍼를 반복한다.

정점 쉐이더에서 fragment 쉐이더까지 모든 객체를 가져오는 대신 고급 fragment 프로세스를 후반 단계로 분리한다.

조명 계산은 이전과 완전히 동일하지만, 이번에는 정점 쉐이더 대신 해당 G-버퍼 텍스처로부터 필요한 모든 입력 변수를 취한다.

 

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

이 접근법의 가장 큰 장점은 depth test가 이미 이 fragment 정보를 최상단 fragment로 결정했기 때문에

G-버퍼에서 끝나는 모든 fragment가 화면 픽셀에 반영되는 실제 fragment 정보라는 것이다.

이렇게 하면 조명 패스에서 각 픽셀마다 한 번만 처리하면 된다.

 

사용되지 않는 많은 렌더링 호출을 절약할 수 있는 것이다.

또한, deferred rendering은 forward rendering에서 사용할 수 있는 것보다 훨씬 많은 양의

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

 

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

버퍼에 비교적 많은 양의 scene 데이터를 저장해야 한다. 이에 따라서 몇 가지 단점이 있다.

또 다른 단점은 블렌딩을 지원하지 않으며 MSAA가 더 이상 작동하지 않는다는 것이다.(최상위 단편의 정보만 가지고 있기 때문이다.)

이 단점에 대한 몇 가지 해결 방법이 해당 강좌의 끝에 있다.

 

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

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

 

The G-buffer

G-버퍼는 최종 조명 패스에 대한 조명 관련 데이터를 저장하는데 사용도히는 모든 텍스처의 총칭이다.

일단 forward 렌더링을 사용해 fragment를 조명하는데 필요한 모든 데이터를 간략히 검토해보겠다.

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

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

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

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

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

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

 

이런 fragment마다 변수를 사용해 익숙한 Blinn-Phong 조명을 계산할 수 있다.

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

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

 

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

모든 fragment 데이터를 저장하고, 나중에 조명 패스에서 사용하는 것이 더 좋다.

G-buffer 텍스처는 라이팅 패스의 2D 쿼드와 같은 크기를 가지므로 forward rendering 설정에서와 똑같은 fragment 데이터를 얻는다.

 

해당 프로세스의 전체를 코드로 나타내면 다음과 같다.

while(...) // render loop
{
    // 1. geometry pass: render all geometric/color data to g-buffer 
    glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    glClearColor(0.0, 0.0, 0.0, 1.0); // keep it black so it doesn't leak into g-buffer
    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);
    lightingPassShader.use();
    BindAllGBufferTextures();
    SetLightingUniforms();
    RenderQuad();
}

각 fragment에 저장해야 하는 데이터는 위치 벡터, 법선 벡터, 색상 벡터, 반사 강도 값이다.

따라서 geometry pass에서 scene의 모든 객체를 렌더링하고 이러한 데이터 구성요소를 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_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, 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_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, 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 텍스처로 결합해 저장한다는 것이다.

이렇게 하면 추가적인 colobuffer 텍스처를 선언하지 않아도 된다.

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 프레임 버퍼에 대량의 backpack 객체를 렌더링하고 screen-filled된 쿼드에 컬러 버퍼를 하나씩 투영해

내용을 시각화한다면 다음과 같은 결과가 나온다.

월드 공간의 위치와 법선 벡터가 실제로 맞는지 시각화 해보아라.

예를 들어, 오른쪽을 가리키는 법선 벡터는 장면의 원점에서 오른쪽으로 향하는 위치 벡터와 마찬가지로

빨간색으로 더 정렬된다.

G-buffer의 내용이 적절했다면 다음 단계인 lighting pass로 넘어갈 시간이다.

 

the deferred lighting pass

폐기할 수 있는 g-buffer의 많은 fragment 데이터를 사용해 각 g-buffer 텍스처를 픽셀 단위로 반복하고

조명 알고리즘에 대한 입력으로 내용을 사용해 scene의 최종 조명된 색상을 완전히 계산할 수 있는 옵션이 있따.

g-buffer 텍스처 값은 모두 최종 변환된 단편 값을 나타내므로 픽셀당 한 번 비싼 조명 작업만 수행하면 된다.

이로 인해 deferred shading이 매우 효율적이다.

특히 복잡한 렌더링에서는 앞으로 렌더링 설정에서 픽셀당 여러 개의 비싼 fragment shader 호출을 쉽게 할 수 있다.

 

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

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의 모든 관련 텍스처를 바인딩하고 조명 관련 균일 변수를 쉐이더에 보낸다.

 

조명 패스의 fragment shader는 지금까지 사용해온 조명 강좌 쉐이더와 거의 유사하다.

새로운 점은 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에 저장한 모든 데이터를 보유한다.

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

fragment shader의 시작 부분에서 간단한 텍스처 룩업을 통해 g-buffer 텍스처로부터 조명 관련 변수를 가져온다.

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

 

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

deferred shading에서 변경되는 유일한 방법은 조명 입력 변수를 얻는 방법이다.

 

총 32개의 작은 광원으로 간단한 데모를 실행하면 다음과 같이 보인다.

deferred shading의 단점 중 하나는 g-buffer의 모든 값이 단일 fragment에서 왔고, 블렌딩이 여러 fragment의 조합에서

작동하기 때문에 블렌딩을 수행할 수 없다는 것이다.

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

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

 

이러한 단점을 극복하기 위해 렌더러를 종종 deferred rendering 부분과 deferred rendering에 적합하지 않은

특수 쉐이더 효과들 또는 블렌딩을 수행하기 위한 forward rendering 파이프라인 부분으로 나눈다.

이것이 어떻게 작동하는지 설명하기 위해 라이트 큐브는 특수한 쉐이더가 필요하므로

forward renderer를 사용해 광원을 작은 큐브로 렌더링한다.

 

Combining deferred rendering with forward rendering

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

시도해 볼 만한 첫 번째 아이디어는 모든 광원을 deferred shading 파이프라인의 끝 부분에 있는

deferred light quad 위에 간단히 forward 렌더링하는 것이다.

기본적으로 평소처럼 큐브를 렌더링하지만 deferred 렌더링 작업을 마친 후에 큐브를 렌더링하는 것이다.

코드는 다음과 같다.

// 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(1.0f);
    model = glm::translate(model, lightPositions[i]);
    model = glm::scale(model, glm::vec3(0.25f));
    shaderLightBox.setMat4("model", model);
    shaderLightBox.setVec3("lightColor", lightColors[i]);
    RenderCube();
}

그러나 이를 통해 렌더링된 큐브는 deferred 렌더러의 저장된 geometry depth를 고려하지 않으며

결과적으로 항상 이전에 렌더링된 객체 위에 렌더링된다. 이는 원하던 결과가 아니다.

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

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

 

anti-aliasing 강좌에서 다중 샘플 프레임 버퍼를 해결하기 위해 glBiltFramebuffer 함수를 통해 다른 프레임 버퍼의

내용에 프레임 버퍼의 내용을 복사할 수 있다는 것을 알 수 있다.

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

 

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 쿼드의 위에 붙여지지 않는 것처럼

행동한다.

이 방법을 사용하면 deferred shading과 forward shading을 쉽게 결합할 수 있다.

이는 쉐이더 특수효과가 필요한 객체를 블렌드하고 렌더링할 수 있기 때문에 유용하다.

deferred 렌더링 환경에서는 불가능한 것이다.

 

A larger number of lights

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

deferred rendering은 그 자체만으로 scene의 광원마다 각 fragment의 조명 구성 요소를 계산하므로

매우 많은 양의 광원을 허용하지는 않는다.

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

즉, 가벼운 볼륨이다.

 

일반적으로 큰 규모의 조명이 존재하는 scene에서 fragment를 렌더링할 때 fragment와의 거리에 관계없이

장면에서 각 광원의 기여도를 계산한다.

이러한 광원의 대부분은 fragment에 도달하지 않으므로 조명 계산을 낭비할 필요가 없다.

 

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

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

그 다음 하나의 fragment가 이러한 조명 볼륨 중 하나 이상에 존재한다면 값 비싼 조명 계산만을 수행한다.

이렇게 하면 필요할 때만 조명을 계산하므로 상당히 많은 계산량을 절약할 수 있다.

 

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

 

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 보다 더 어두운 밝기로 제한하면 광량이 너무 커져 효과가 떨어진다.
볼륨 경계에서 광원이 갑자기 끊어지는 것을 사용자가 볼 수 없는 한 괜찮을 것이다.
물론 이것은 항상 scene의 유형에 달려있다. 밝기 임계 값이 높을 수록 조명 볼륨이 작아지고
효율성이 높아지지만 조명이 볼륨의 경계에서 깨지는 것처럼 보이는 이상한 인공물이 생성될 수도 있다.

결국 감쇠 방정식은 다음과 같다.

여기서 I는 광원의 가장 밝은 색상 요소이다.

빛의 가장 밝은 색상 값을 위한 방정식을 해결하면 이상적인 빛의 볼륨 반경을 가장 잘 반영하기 때문에

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

 

여기서 계속해서 방정식을 풀어본다.

마지막 방정식은 이차 방정식을 이용해 풀 수 있다.

이는 일정한 선형 및 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 사이의 반경을 반환한다.

 

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

아래는 계산된 조명 볼륨을 고려한 업데이트된 조명 패스 fragment shader이다.

이 접근법은 단지 교육용으로만 이루어졌으며 곧 논의할 실제적인 환경에서는 실행이 불가능하다는 점을 유념해야 한다.

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

위에서 보여준 fragment shader는 실제로 작동하지 않으며 라이팅 계산을 줄이기 위해 라이트의 볼륨을 어떻게 사용하는지를 보여줄 뿐이다.

실제로 GPU와 GLSL이 루프와 브랜치를 최적화하는데 정말 좋지 않다.

그 이유는 GPU에서의 쉐이더 실행은 매우 평행하고 대부분의 아키텍처는 스레드를 대량으로 수집하기 위해

효율적으로 동일한 쉐이더 코드를 실행해야 한다는 요구 사항이 있다.

쉐이더가 실행되는 것을 보장하기 위해 항상 if문의 모든 분기를 실행하는 쉐이더가 실행됨으로써

이전의 반경 검사 최적화가 완전히 쓸모없게 된다.

이는 여전히 모든 광원에 대한 조명을 계산할 것이다.

 

Light volume을 사용하는 적절한 방법은 실제 볼을 light volume 반경으로 축소해 렌더링하는 것이다.

이 구의 중심은 광원의 위치에 배치되며

광량 반경에 따라 축척되므로 구가 빛의 가시 볼륨을 정확히 포함한다.

이것이 트릭이 나오는 부분이다, 구를 렌더링하기 위해 대부분 동일한 deferred fragment shader를 사용한다.

렌더링된 구체가 광원에 영향을 주는 픽셀과 정확하게 일치하는 fragment shader 호출을 생성하기 때문에

관련 픽셀을 렌더링하고 다른 모든 픽셀은 건너뛴다.

아래 이미지는 이를 보여준다.

이 작업은 scene의 각 광원에 대해 수행되며 결과로 생성된 fragment는 함께 추가적으로 혼합된다.

결과는 이전과 똑같은 장면이지만 이번에는 광원당 관련 fragment만 렌더링한다.

이는 nr_objects * nr_lights에서부터 nr_objects + nr_lights까지 계산을 효과적으로 줄여주며

많은 수의 조명이 있는 장면에서 매우 효율적이다.

이 방법은 deferred rendering을 많은 조명을 렌더링하는데 적합하게 만드는 것이다.

 

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

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을 할 때 사용할 수 있었던

여러 효과들을 사용할 수 없다.

 

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

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

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

 

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

이는 종종 작은 translation 단계만 필요하다.

예를 들어, deferred renderer에서 normal mapping을 사용하려는 경우 표면 normal 대신

표준 맵에서 추출한 월드 공간 법선을 출력하도록 geometry pass shader를 변경한다.

조명 패스의 조명 계산을 전혀 변경할 필요가 없다.

parallax mapping을 사용하려면 객체의 확산, 반사, 표준 텍스처를 샘플링하기 전에

geometry pass의 텍스처 좌표를 먼저 이동해야 한다.

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