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

OpenGL Advanced Lighting 6-7 Bloom 본문

공부한거/OpenGL

OpenGL Advanced Lighting 6-7 Bloom

Palamore 2020. 12. 14. 15:54

원문 사이트

learnopengl.com/Advanced-Lighting/Bloom

 

LearnOpenGL - Bloom

Bloom Advanced-Lighting/Bloom Bright light sources and brightly lit regions are often difficult to convey to the viewer as the intensity range of a monitor is limited. One way to distinguish bright light sources on a monitor is by making them glow; the lig

learnopengl.com

번역 사이트

gyutts.tistory.com/181?category=755809

 

Learn OpenGL - Advanced Lighting : Bloom

link : https://learnopengl.com/Advanced-Lighting/Bloom Bloom 밝은 광원 및 밝게 조명된 영역은 모니터의 강도 범위가 제한되어 있기 때문에 종종 시청자에게 전달하기 어렵다. 모니터에서 밝은 광원을 구별하

gyutts.tistory.com

Bloom

밝은 광원 및 밝게 조명된 영역은 모니터의 강도 범위가 제한되어 있기 때문에 종종 viewer에게 전달하기 어렵다.

모니터에서 밝은 광원을 구별하는 한 가지 방법은 빛을 비추는 것이다.

광원을 중심으로 빛의 번짐을 발생시킨다.

이는 효과적으로 viewer로 하여금 이런 광원 또는 밝은 영역이 강렬하게 빛나는 착각을 준다.

 

이 가벼운 블리딩 또는 글로우 효과는 bloom이라는 post-processing effect로 얻을 수 있다.

블룸(Bloom)은 모든 밝은 조명 영역에 반짝이는 효과를 준다.

반짝이는 장면과 그렇지 않은 장면의 예가 아래에 있다.(Unreal 엔진의 이미지)

블룸은 오브젝트의 밝기에 대한 눈에 띄는 시각적인 단서를 제공하여 해당 오브젝트가 실제로 밝아지는 것처럼 보이게 한다.

미묘한 방식에서 블룸을 사용하면 scene 조명이 크게 향상되고 다양한 극적인 효과를 얻을 수 있다.

 

Bloom은 HDR 렌더링과 함께 사용하면 가장 효과적이다. 일반적인 편견은 HDR이 많은 사람들이 상호교환적으로

용어를 사용하는 만큼 Bloom과 같은 기술이라고 생각하는데,

그러나 그들은 다른 목적으로 사용된 완전히 다른 기술이다.

블룸 효과 없이 HDR을 사용할 수 있는 것처럼

기본 8비트 정밀도 프레임 버퍼로 블룸을 구현할 수 있다.

HDR을 사용하면 블룸을 구현하는데 더 효과적이다.

 

Bloom을 구현하기 위해 조명된 장면을 평소와 같이 렌더링하고 장면의 HDR 색상 버퍼와 밝은 영역만 보이는 장면의 이미지를 모두 추출한다.

그 다음 추출된 밝기 이미지가 희미해지고 결과가 원본 HDR 장면 이미지 위에 추가된다.

 

이 프로세스를 단계별로 설명해보겠다.

밝은 큐브로 시각화된 4개의 밝은 광원으로 가득 찬 scene을 렌더링한다.

컬러 라이트 큐브의 밝기 값은 1.5와 15.0 사이이다. 이를 HDR 컬러 버퍼로 렌더링하면 장면은 다음과 같다.

이 HDR 컬러 버퍼 텍스처를 취해 특정 밝기를 초과하는 모든 fragment를 추출한다.

이렇게 하면 fragment 강도가 특정 임계 값을 초과할 때 밝은 색상의 영역만 보여주는 이미지가 제공된다.

그 다음 이 임계 값 밝기 텍스처를 취해 결과를 흐리게 만든다.

bloom 효과의 강도는 주로 사용되는 블러 필터의 범위와 강도에 의해 결정된다.

결과로 흐린 텍스처가 빛이나 번쩍거리는 효과를 얻는데 사용된다.

이 흐리게 처리된 텍스처는 원본 HDR 장면 텍스처 위에 추가된다.

흐림 필터로 인해 밝은 영역이 너비와 높이로 확장되므로 장면의 밝은 영역이 빛나거나 번쩍이는 것처럼 보인다.

블룸 자체는 복잡한 기술은 아니지만 정확히 맞히기는 어렵다.

대부분의 시각적인 품질은 추출된 밝기 영역을 흐리게 하는데 사용되는 흐림 필터의 품질 및 유형에 의해 결정된다.

블러 필터를 간단히 조정하면 블룸 효과의 품질을 크게 바꿀 수 있다.

첫 번째 단계는 어떤 임계값을 기반으로 장면의 모든 밝은 색상을 추출해야한다는 것이다.

먼저 이에 대해 알아보자.

 

Extracting bright color

첫 번째 단계에서는 렌더링된 장면에서 두 개의 이미지를 추출해야 한다.

장면을 두 번 렌더링할 수 있다.

둘 다 서로 다른 쉐이더를 사용해 다른 프레임 버퍼로 렌더링하지만 MRT(Multiple Render Targets)라는 깔끔한

작은 트릭을 사용해 하나 이상의 fragment shader 출력을 지정할 수 있다.

이렇게 하면 단일 렌더링 패스에서 처음 두 이미지를 추출할 수 있는 옵션이 제공된다.

fragment shader가 출력되기 전에 레이아웃 위치 지정자를 지정함으로써 fragment shader가 어떤 컬러

버퍼에 쓸지를 제어할 수 있다.

layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor; 

그러나 이는 실제로 쓸 곳이 여러 개인 경우에만 작동한다.

다중 fragment shader 출력을 사용하기 위해서는 현재 바인딩된 프레임 버퍼 객체에 첨부된 여러 색상 버퍼가 필요하다.

프레임 버퍼 강좌에서 텍스처를 프레임 버퍼의 색상 버퍼로 연결할 때 색상 첨부를 지정할 수 있음을 기억할 것이다.

지금까지는 항상 GL_COLOR_ATTACHMENT0를 사용했지만,

GL_COLOR_ATTACHMENT1을 사용해 프레임 버퍼 객체에 첨부된 두 개의 색상 버퍼를 가질 수 있다.

// set up floating point framebuffer to render scene to
unsigned int hdrFBO;
glGenFramebuffers(1, &hdrFBO);
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
unsigned int colorBuffers[2];
glGenTextures(2, colorBuffers);
for (unsigned int i = 0; i < 2; i++)
{
    glBindTexture(GL_TEXTURE_2D, colorBuffers[i]);
    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_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    // attach texture to framebuffer
    glFramebufferTexture2D(
        GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0
    );
}  

OpenGL에게 glDrawBuffers를 통해 여러 색상 버퍼 렌더링을 명시적으로 알려줘야 한다.

그렇지 않으면 OpenGL은 다른 모든 색상을 무시하고 프레임 버퍼의 첫 번째 색상 첨부로 렌더링한다.

post-precssing에서 렌더링할 색상 첨부 열거형 배열을 전달해 이 작업을 수행할 수 있다.

unsigned int attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
glDrawBuffers(2, attachments);  

이 프레임 버퍼에 렌더링할 때, fragment shader가 레이아웃 위치 지정자를 사용할 때마다

각 색상 버퍼가 fragment를 렌더링하는데 사용된다.

이는 밝은 영역을 추출하기 위한 여분의 렌더링 패스를 렌더링 할 fragment로부터 직접 추출할 수 있기 때문에 훌륭하다.

#version 330 core
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;

[...]

void main()
{            
    [...] // first do normal lighting calculations and output results
    FragColor = vec4(lighting, 1.0);
    // check whether fragment output is higher than threshold, if so output as brightness color
    float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
    if(brightness > 1.0)
        BrightColor = vec4(FragColor.rgb, 1.0);
    else
        BrightColor = vec4(0.0, 0.0, 0.0, 1.0);
}

여기서는 먼저 조명을 정상으로 계산해 첫 번째 fragment shader의 출력 변수 FragColor에 전달한다.

그 다음 FragColor에 현재 저장된 내용을 사용해 밝기가 특정 임계 값을 초과하는지 확인한다.

조각의 밝기를 먼저 그레이 스케일로 적절하게 변환해 계산한다.

(두 벡터의 내적을 취해 두 벡터의 각 개별 구성요소를 효과적으로 곱해 결과를 더함.)

특정 임계 값을 초과하면 색상을 출력한다.

모든 밝은 영역을 유지하는 두 번째 색상 버퍼로 색상을 출력한다.

라이트 큐브를 렌더링할 때도 마찬가지이다.

 

이는 또한 블룸이 HDR 렌더링과 함께 아주 잘 작동하는 이유를 보여준다.

높은 동적 범위에서 렌더링하기 때문에 색상 값이 1.0을 초과할 수 있으므로 기본 범위를 벗어나는 밝기 임계

값을 지정할 수 있으므로 이미지의 어떤 부분을 밝게 보는지 훨씬 더 많이 제어할 수 있다.

HDR이 없으면 임계 값을 1.0보다 낮게 설정해야 하지만 region은 훨씬 빠르게 밝아서 글로우 효과가 너무 지배적이게 된다.

두 개의 컬러 버퍼 내에서 정상적인 장면의 이미지와 추출된 밝은 영역의 이미지를 갖는다.

모두 단일 렌더링 패스에서 얻는다.

추출된 밝은 영역의 이미지로 이제 이미지를 흐리게 처리해야 한다.

frame buffer 강좌의 post-processing 섹션에서 설명한 것처럼 간단한 상자 필터를 사용해

이 작업을 수행할 수 있지만 Gaussian blur라는 보다 고급이며 보기가 쉬운 흐림 필터를 사용한다.

 

Gaussian blur

post-processing blur에서는 이미지의 모든 주변 픽셀의 평균을 취하고 쉽게 흐림 효과를 주는 반면 최상의 결과를 얻지는 못한다.

가우시안 블러는 가우스 곡선을 기반으로 한다.

이 곡선은 일반적으로 종 모양 커브로 기술되어 중심점에서 멀어질수록 높은 값을 점차적으로 떨어트린다.

가우스 곡선은 수학적으로 다른 형태로 표현될 수 있지만 일반적으로 다음과 같은 모양을 갖는다.

가우시안 커브는 중심 근처에서 더 큰 영역을 가지므로 가중치로 값을 사용하면 이미지를 흐리게 처리하여

가까운 샘플이 우선 순위가 높을 때 큰 효과를 얻을 수 있다.

예를 들어, 주위에 32 x 32 상자를 샘플링하면 점진적으로 작은 가중치를 사용해 fragment까지의 거리가 커진다.

이를 일반적으로 Gaussian blur라고 하며 이는 더 좋고 사실적인 블러를 제공한다.

 

Gaussian blur 필터를 구현하려면 2차원 가우스 곡선 방정식에서 얻을 수 있는 2차원 가중치 상자가 필요하다.

그러나 이 접근법의 문제점은 퍼포먼스가 굉장히 무거워진다는 점이다.

예를 들어, 32 x 32의 blur kernel을 취하면 각 fragment에 대해 총 1024번 텍스처를 샘플링해야 한다.

 

다행스럽게도 Gaussian 방정식은 2차원 방정식을 두 개의 작은 방정식으로 분리할 수 있는 매우 정교한 특성을 가지고 있다.

하나는 수평 가중치를 설명하고 다른 하나는 수직 가중치를 설명한다.

그 다음 먼저 전체 텍스처에 수평 가중치를 적용한 수평 흐림 효과를 적용한 다음 결과 텍스처에수직 흐림 효과를 적용한다.

이 속성으로 인해 결과는 동일하지만 1024에 비해 32 + 32샘플만 수행하면 되므로 엄청난 양의 성능을 절약할 수 있다.

이를 two-pass Gaussian blur라고 한다.

이는 이미지를 최소한 두 번 흐리게 처리해야 한다는 것을 의미하며 이는 프레임 버퍼 객체의 사용과 함께 가장 잘 작동한다.

특히 gaussian blur를 구현하기 위해 ping-pong 프레임 버퍼를 구현할 것이다.

이는 다른 프레임 버퍼의 색상 버퍼를 현재 프레임 버퍼의 색상 버퍼에 주어진 횟수만큼 렌더링하는 한 쌍의 프레임 버퍼이다.

기본적으로 드로잉할 프레임 버퍼와 드로잉할 텍스처를 바꿔 놓는다.

이를 통해 첫 번째 프레임 버퍼에서 장면의 질감을 먼저 흐리게 처리한 다음 첫 번째 프레임 버퍼의 색상 버퍼를

두 번째 프레임 버퍼로 흐리게 처리한 다음 두 번째 프레임 버퍼의 색상 버퍼를 첫 번째 프레임 버퍼로 흐리게 처리할 수 있다.

 

프레임 버퍼에 대해 알아보기 전에 먼저 가우시안 블러의 fragment shader에 대해 알아보자.

#version 330 core
out vec4 FragColor;
  
in vec2 TexCoords;

uniform sampler2D image;
  
uniform bool horizontal;
uniform float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);

void main()
{             
    vec2 tex_offset = 1.0 / textureSize(image, 0); // gets size of single texel
    vec3 result = texture(image, TexCoords).rgb * weight[0]; // current fragment's contribution
    if(horizontal)
    {
        for(int i = 1; i < 5; ++i)
        {
            result += texture(image, TexCoords + vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
            result += texture(image, TexCoords - vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
        }
    }
    else
    {
        for(int i = 1; i < 5; ++i)
        {
            result += texture(image, TexCoords + vec2(0.0, tex_offset.y * i)).rgb * weight[i];
            result += texture(image, TexCoords - vec2(0.0, tex_offset.y * i)).rgb * weight[i];
        }
    }
    FragColor = vec4(result, 1.0);
}

여기에서는 가우시안 가중치의 비교적 작은 샘플을 사용해 현재 조각 주위의 수평 또는 수직 샘플에

특정 가중치를 할당한다.

기본적으로 흐림 필터는 수평 유니폼을 설정한 값에 따라 가로 및 세로 섹션으로 분할된다.

여기서는 텍스처 크기에 1.0을 나누어 얻은 텍셀의 실제 크기에 오프셋 거리를 기반으로 한다.

 

이미지를 흐릿하게 하기 위해 colorbuffer 텍스처만을 가진 두 개의 기본 프레임 버퍼를 만든다.

unsigned int pingpongFBO[2];
unsigned int pingpongBuffer[2];
glGenFramebuffers(2, pingpongFBO);
glGenTextures(2, pingpongBuffer);
for (unsigned int i = 0; i < 2; i++)
{
    glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[i]);
    glBindTexture(GL_TEXTURE_2D, pingpongBuffer[i]);
    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_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glFramebufferTexture2D(
        GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pingpongBuffer[i], 0
    );
}

그 다음 HDR 텍스처와 추출된 밝기 텍스처를 얻은 후 우선 ping-pong 프레임 버퍼 중 하나에 밝기 텍스처를 채운 다음

이미지를 10번 흐리게 한다.

 

(가로 5번, 세로 5번)

bool horizontal = true, first_iteration = true;
int amount = 10;
shaderBlur.use();
for (unsigned int i = 0; i < amount; i++)
{
    glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]); 
    shaderBlur.setInt("horizontal", horizontal);
    glBindTexture(
        GL_TEXTURE_2D, first_iteration ? colorBuffers[1] : pingpongBuffers[!horizontal]
    ); 
    RenderQuad();
    horizontal = !horizontal;
    if (first_iteration)
        first_iteration = false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0); 

각각의 반복 작업은 수평 또는 수직으로 흐리게 할 지 여부와 다른 프레임 버퍼의 색상 버퍼를 흐리게 처리할지 여부에 따라

두 프레임 버퍼중 하나를 바인딩한다.

두 색상 버퍼 모두 흐리게 처리하고자 하는 텍스처를 구체적으로 바인딩하는 첫 번째 반복은 비어있게 된다.

이 과정을 10번 반복하면 밝기 이미지가 5번 반복된 완전한 가우시안 블러로 끝난다.

이 구조는 원하는만큼 자주 이미지를 흐리게 한다.

가우시안 블러 반복이 많을 수록 블러가 강해진다.

 

추출된 brightness 텍스처를 5번 흐리게 하면 scene의 모든 밝은 영역을 적절히 흐리게 표현할 수 있다.

블룸 효과를 완성하기 위한 마지막 단계는 흐린 밝기 텍스처와 원본 장면의 HDR 텍스처를 결합하는 것이다.

 

Blending both textures

scene의 HDR 텍스처와 흐린 밝기 텍스처를 사용하면 불투명한 블룸이나 글로우 효과를 내기 위해 두 가지를 결합하면 된다.

최종 fragment shader에서는 두 텍스처를 추가로 혼합한다.

#version 330 core
out vec4 FragColor;
  
in vec2 TexCoords;

uniform sampler2D scene;
uniform sampler2D bloomBlur;
uniform float exposure;

void main()
{             
    const float gamma = 2.2;
    vec3 hdrColor = texture(scene, TexCoords).rgb;      
    vec3 bloomColor = texture(bloomBlur, TexCoords).rgb;
    hdrColor += bloomColor; // additive blending
    // tone mapping
    vec3 result = vec3(1.0) - exp(-hdrColor * exposure);
    // also gamma correct while we're at it       
    result = pow(result, vec3(1.0 / gamma));
    FragColor = vec4(result, 1.0);
}  

여기서 주목할 점은 톤 매핑을 적용하기 전에 블룸 효과를 추가한다는 것이다.

이 방법으로 블룸의 추가된 밝기도 결과적으로 상대 조명에 비해 LDR 범위로 부드럽게 변환된다.

 

두 텍스처를 합쳐서 장면의 모든 밝은 영역에 적절한 글로우 효과가 나타난다.

유색 큐브는 이제 훨씬 더 밝게 빛나고 발광 개체로 더 좋은 효과가 발생한다.

이는 상대적으로 단순한 장면이므로 블룸 효과가 너무 인상적이지는 않지만 잘 조명된 장면에서는 적절히 구성하면

큰 효과를 낼 수 있다.

 

이 강좌에서는 상대적으로 간단한 Gaussian 블러 필터를 사용했다.

여기서는 각 방향으로 5개의 샘플만 가져온다.

더 큰 반경을 따라 더 많은 샘플을 채우거나 추가 횟수만큼 블러 필터를 반복함으로써 블러 효과를 향상시킬 수 있다.

블러의 품질은 블룸 효과의 품질과 직접적으로 관련이 있기 때문에 블러 단계를 향상시키는 것이 크게 개선될 수 있다.

이러한 개선 사항 중 일부는 다양한 크기의 블러 커널과 블러 필터를 결합하거나 다중 가우스 곡선을 사용해

가중치를 선택적으로 결합한다.