본문 바로가기

Game/Graphics

Learn OpenGL - Advanced Lighting : Bloom

link : https://learnopengl.com/Advanced-Lighting/Bloom


Bloom

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


모니터에서 밝은 광원을 구별하는 한 가지 방법은 빛을 비추는 것이다. 광원을 중심으로 빛의 번짐이 발생한다.


이것은 효과적으로 관측자에게 이러한 광원 또는 밝은 영역이 강렬하게 빛나는 환상을 준다.



 이 가벼운 블리딩 또는 글로우 효과는 블룸이라는 사후 처리 효과로 얻을 수 있다. 블룸(Bloom)은 모든 밝은 조명 영역에 반짝이는


효과를 준다. 반짝이는 장면과 없는 장면의 예가 아래에 있다. (Unreal의 이미지):



 블룸은 오브젝트의 밝기에 대한 눈에 띄는 시각적 단서를 제공하여 블룸은 환상 오브젝트가 실제로 밝아지는 경향이 있다.


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



 Bloom은 HDR 렌더링과 함께 사용하면 가장 효과적이다. 일반적인 오해는 HDR이 많은 사람들이 상효교환적으로 용어를 사용하는만큼


Bloom과 동일하다는 것이다. 그러나 그들은 다른 목적으로 사용된 완전히 다른 기술이다. 블룸 효과없이 HDR을 사용할 수 있는 것처럼


기본 8비트 정밀도 프레임 버퍼로 블룸을 구현할 수 있다. HDR을 사용하면 블룸을 구현하는데 더 효과적이다.



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


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



 이 프로세스를 단계별로 설명해보겠다. 우리는 밝은 큐브로 시각화된 4개의 밝은 광원으로 가득 찬 장면을 렌더링한다.


컬러 라이트 큐브의 밝기 값은 1.5와 15.0 사이이다. 이것을 HDR 컬러 버퍼로 렌더링한다면 장면은 다음과 같이 보일 것이다:


Image of a HDR scene where we need to add the bloom or glow effect in OpenGL


 우리는 이 HDR 컬러 버퍼 텍스처를 취해 특정 밝기를 초과하는 모든 조각을 추출한다. 이렇게하면 조각 강도가 특정 임계 값을


초과 할 때 밝은 색상의 영역만 보여주는 이미지가 제공된다:


Bright regions extracted of a scene for the bloom or glow post-processing effect in OpenGL


 그런 다음 이 임계 값 밝기 텍스처를 취해 결과를 흐리게 만든다. 블룸 효과의 강도는 주로 사용되는 블러 필터의 범위와


강도에 의해 결정된다:


Bright regions extracted for glow or bloom effect are blurred in OpenGL


 결과로 흐린 텍스처가 빛이나 번쩍거리는 효과를 얻는데 사용된다. 이 흐리게 처리된 텍스처는 원본 HDR 장면 텍스처 위에 추가된다.


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


Example of the Bloom or Glow post-processing effect in OpenGL with HDR


 블룸 자체는 복잡한 기술은 아니지만 정확히 맞히기는 어렵다. 대부분의 시각적 품질은 추출된 밝기 영역을 흐리게 하는데 사용되는


흐림 필터의 품질 및 유형에 의해 결정된다. 블러 필터를 간단히 조정하면 블룸 효과의 품질을 크게 바꿀 수 있다.


Steps required for implementing the bloom or glow post-processing effect in OpenGL


 첫 번째 단계는 우리가 어떤 임계 값을 기반으로 장면의 모든 밝은 색상을 추출해야한다는 것이다. 먼저 그것에 대해 알아 보겠다.




Extracting bright color


 첫 번째 단계에서는 렌더링 된 장면에서 두 개의 이미지를 추출해야한다. 장면을 두 번 렌더링 할 수 있다. 둘 다 서로 다른 쉐이더를


사용해 다른 프레임 버퍼로 렌더링하지만 MRT (Multiple Render Targets)라는 깔끔한 작은 트릭을 사용해 하나 이상의 조각 쉐이더


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


출력되기 전에 레이아웃 위치 지정자를 지정함으로써 조각 쉐이더가 어떤 컬러 버퍼에 쓸지를 제어 할 수 있다.

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

 그러나 우리가 실제로 쓸 곳이 여러 개인 경우에만 작동한다. 다중 조각 쉐이더 출력을 사용하기 위해서는 현재 바인딩된 프레임


버퍼 객체에 첨부된 여러 색상 버퍼가 필요하다. 프레임 버퍼 튜토리얼에서 텍스처를 프레임 버퍼의 색상 버퍼로 연결할 때


색상 첨부를 지정할 수 있음을 기억할 것이다. 지금까지는 항상 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_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, 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은 다른 모든 색상을


무시하고 프레임 버퍼의 첫 번째 색상 첨부로 렌더링한다. 후속 작업에서 렌더링 할 색상 첨부 열거형 배열을 전달해 이 작업을 수행 할 수 있다:

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

  이 프레임 버퍼에 렌더링 할 때, 조각 쉐이더가 레이아웃 위치 지정자를 사용할 때마다 각 색상 버퍼가 조각을 렌더링하는데 사용된다.


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

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

 여기서는 먼저 조명을 정상으로 계산해 첫 번째 조각 쉐이더의 출력 변수 FragColor에 전달한다. 그런 다음 FragColor에 현재 저장된


내용을 사용해 밝기가 특정 임계 값을 초과하는지 확인한다. 조각의 밝기를 먼저 그레이 스케일로 적절하게 변환해 계산한다.


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


특정 임계 값을 초과하면 색상을 출력한다. 모든 밝은 영역을 유지하는 두 번째 색상 버퍼로 색상을 출력한다. 라이트 큐브를 렌더링


할 때도 마찬가지이다.



 이것은 또한 블룸이 HDR 렌더링과 함께 아주 잘 작동하는 이유를 보여준다. 높은 동적 범위에서 렌더링하기 때문에 색상 값이 1.0을


초과 할 수 있으므로 기본 범위를 벗어나는 밝기 임계 값을 지정할 수 있으므로 이미지의 어떤 부분을 밝게 보는지 훨씬 더 많이


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



 두 개의 컬러 버퍼 내에서 우리는 정상적인 장면의 이미지와 추출된 밝은 영역의 이미지를 갖는다. 모두 단일 렌더링 패스에서 얻는다.


Image of two colorbuffers obtained from a single render pass with multiple color attachments for the bloom or glow effect in OpenGL


 추출된 밝은 영역의 이미지로 이제 이미지를 흐리게 처리해야한다. 프레임 작성 튜토리얼의 사후 처리 섹션에서 설명한 것처럼


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




Gaussian blur


 사후 처리 블러에서는 이미지의 모든 주변 픽셀의 평균을 취하고 쉽게 흐림 효과를 주는 반면 최상의 결과를 얻지는 못한다.


가우시안 블러는 가우스 곡선을 기반으로 한다. 이 곡선은 일반적으로 종 모양 커브로 기술되어 중심점에 가까운 높은 값을


점차적으로 떨어뜨린다. 가우스 곡선은 수학적으로 다른 형태로 표현 될 수 있지만 일반적으로 다음고 같은 모양을 갖는다:


Image of a Gaussian Curve used for blurring a bloom or glow image in OpenGL


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


높을 때 큰 효과를 얻을 수 있다. 예를 들어, 조각 주위에 32x32 상자를 샘플링하면 점진적으로 작은 가중치를 사용해 조각까지의


거리가 커진다. 이것을 일반적으로 Gaussian Blur라고 하는 더 좋고 사실적인 블러를 제공한다.



 가우시안 흐림 필터를 구현하려면 2차원 가우스 곡선 방정식에서 얻을 수 있는 2차원 가중치 상자가 필요하다. 그러나 이 접근법의


문제점은 성능이 급격히 높아진다는 것이다. 예를 들어, 32x32의 블러 커널을 취하면 각 조각에 대해 총 1024번 텍스처를


샘플링해야한다!



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


하나는 수평 가중치를 설명하고 다른 하나는 수직 가중치를 설명한다. 그런 다음 먼저 전체 텍스처에 수평 가중치를 적용한


수평 흐림 효과를 적용한 다음 결과 텍스처에 수직 흐림 효과를 적용한다. 이 속성으로 인해 결과는 동일하지만 1024에 비해


32+32 샘플만 수행하면 되므로 엄청난 양의 성능을 절약할 수 있다. 이를 two-pass Gaussian blur라고 한다.


Image of two-pass Gaussian blur with the same results as normal gaussian blur, but now saving a lot of performance in OpenGL


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


특히 가우시안 흐림을 구현하기 위해 우리는 ping-pong 프레임 버퍼를 구현할 것이다. 이것은 다른 프레임 버퍼의 색상 버퍼를


현재 프레임 버퍼의 색상 버퍼에 주어진 횟수만큼 렌더링하는 한 쌍의 프레임 버퍼이다. 우리는 기본적으로 드로잉 할 프레임 버퍼와


드로잉 할 텍스처를 바꿔 놓는다. 이를 통해 첫 번째 프레임 버퍼에서 장면의 질감을 먼저 흐리게 처리 한 다음 첫 번째 프레임


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


흐리게 처리 할 수 있다.



 프레임 버퍼에 대해 알아보기 전에 먼저 가우시안 블러의 조각 쉐이더에 대해 알아보겠다:

#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_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, 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번 흐리게하면 장면의 모든 밝은 영역을 적절히 흐리게 표현할 수 있다.


Blurred image using Gaussian Blur of extracted brightness regions for the glow or bloom effect in OpenGL


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



Blending both textures


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


최종 조각 쉐이더에서는 두 텍스처를 추가로 혼합한다:

#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 범위로 부드럽게 변환된다.



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


Example of the Bloom or Glow post-processing effect in OpenGL with HDR


 유색 큐브는 이제 훨씬 더 밝게 나타나고 발광 개체로 더 좋은 환상을 준다. 이것은 상대적으로 단순한 장면이므로 블룸 효과가 너무 인상적이지는


않지만 잘 조명된 장면에서는 적절히 구성하면 큰 효과를 낼 수 있다.



 이 튜토리얼에서는 상대적으로 간단한 Gaussian 블러 필터를 사용했다. 여기서는 각 방향으로 5개의 샘플만 가져온다. 더 큰 반경을 따라 더 많은


샘플을 채우거나 추가 횟수만큼 블러 필터를 반복함으로써 블러 효과를 향상시킬 수 있다. 블러의 품질은 블룸 효과의 품질과 직접적으로 관련이


있기 때문에 블러 단계를 향상시키는 것이 크게 개선 될 수 있다. 이러한 개선 사항 중 일부는 다양한 크기의 블러 커널과 블러 필터를


결합하거나 다중 가우스 곡선을 사용해 가중치를 선택적으로 결합한다.