Post Lists

2018년 8월 17일 금요일

Advanced Lighting - Bloom (8)

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

Bloom
밝은 광원들과 밝기 빛나는 지역은 종종 viewer에게 전달하기에 어렵다. 한 모니터의 강도의 범위가 제한되기 때문이다. 한 모니터에서 밝은 광원을 구분하는 한 가지 방법은 그것들이 빛나게 만들어서 그것들의 빛이 광원 주변에 흘러나오는 것이다. 이것은 효과적으로 viewer에게 이러한 광원원 또는 밝은 지역들이 강렬하게 빛난다는 환영을 준다.

이 light bleeding or glow effect는 bloom이라고 불려지는 후처리 효과로 얻어진다. Bloom은 scene의 모든 밝게 비춰지는 지역에게 glow-like effect를 준다. glow가 있고 없는 한 scene의 예제는 아래에서 보여질 수 있다. (언리얼의 이미지 제공):

Bloom은 오브젝트들의 밝기에 대해 눈에 띌만한 시각적 신호를 준다. bloom이 오브젝트들이 정말 밝다는 환영을 주는 경향이 있기 때문이다. 미묘한 방식으로 처리될 떄, bloom은 상당히 너의 scene의 조명을 증진시키고, 극적인 효과의 넓은 범위를 하게 해준다.

Bloom은 HDR rendering과 가장 잘 조합되어 작동한다. 흔한 잘못된 개념은 HDR이 많은 사람들이 그 용어를 바꿔가면서 bloom과 같다고 하는 것이다. 그러나, 그것들은 다른 목적을 위해 사용되는 완전히 다른 기법들이다. 기본 8-bit precision framebuffers로 bloom을 구현하는 것이 가능하다.  bloom effect없이 HDR를 사용하는 것이 가능하듯이. 간단히 HDR은 bloom이 구현하기에 좀 더 효과적으로 만든다는 것이다. (우리가 나중에 볼 것이다.)

이 과정을 단계별 방식으로 봐보자. 우리는 4개의 빛나는 광원들이 색칠된 큐브로서 시각화된 scene을 렌더링 한다. 그 색이 있는 light cubes는 1.5 ~ 15.0의 밝기 값을 가진다. 만약 우리가 이것을 HDR colorbuffer에 렌더링하려고 한다면 그 scene은 다음과 같이 보인다:

우리는 이 HDR colorbuffer texture를 취해서, 어떤 밝기를 초과하는 fragments들을 추출한다. 이것은 우리에게 그것들의 fragment 강도가 어떤 threshold를 초과했기에 밝게 칠해진 영역만을 보여주는 한 이미지를 준다:

우리는 그러고나서 이 thresholded brightness texture를 취하고 그 결과를 blur한다. 그 bloom effect의 강도는 크게 범위와 사용되는 blur filter의 세기에 의해 결정된다.

그 최종 blurred texture는 glow or light-bleeding effect를 얻기위해 우리가 사용하는 것이다. 이 블러된 텍스쳐는 원래 HDR scene texture 위에 더해진다. 그 밝은 지역들은 blur filter 때문에 너비와 높이 둘 다에서 확장되었기 때문에, 그 scene의 밝은 영역들은 glow or bleed light하는 것처럼 보인다.

Bloom은 그 자체로 복잡한 기법은 아니지면 정확히 옳게 하기에 어렵다. 그것의 시각적 퀄리티의 대부분은 추출된 밝기의 지역을 blurring하기위해 사용되는 blur filter의 퀄리티와 종류에 의해 결정된다. 그 blur filter를 간단히 조정해보는 것은 그 bloom effect의 quality를 급격히 변화시킬 수 있다.

이러한 스텝들을 따르는 것은 우리에게 bloom post-processing effect를 준다. 아래의 이미지는 간단히 bloom을 구현하는데 요구되는 단계들을 요약한다.

그 첫 번째 단계는 우리가 어떤 threshold를 기반으로 한 scene의 모든 밝은 컬러들을 추출하는 것을 요구한다. 그것을 처음에 알아보자.

Extracting bright color
첫 번째 단계는 렌더링 된 씬으로부터 두 이미지들을 추출하는 것을 요구한다. 우리는 그 scene을 두 번 렌더링할 수 있다. 다른 쉐이더들을 가진 다른 frambuffer에 둘다. 그러나 우리는 또한 한 개 이상의 fragment shader output을 명시하는 것을 허용하는 Multiple Render Targets (MRT)라고 불리는 깔끔한 트릭을 사용할 수 있다; 이것은 우리에게 한 번의 render pass에서 처음 두 개의 이미지들을 추출하는 옵션을 준다. fragment shader의 output전에 layout location specifier를 명시하여, 우리는 한 fragment shader가 어떤 color buffer에 쓸지를 통제할 수 있다:


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

그러나 이것은 우리가 실제로 써야 할 다양한 장소를 가져야만 작동한다. 많은 fragment shader outputs을 사용하기 위한 요구사항으로서, 우리는 현재 bind된 framebuffer object에 attached된 수 많은 colorbuffers가 필요하다. 너는 framebuffers 튜토리얼에서 우리가 texture를 framebuffer의 colorbuffer로 링크시킬 떄 color attachment를 명시할 수 있다는 것을 기억할지도 모른다. 이제까지 우리는 항상 GL_COLOR_ATTACHMENT0을 사용했지만, 또한 GL_COLOR_ATTACHMENT1을 사용하여, 우리는 framebuffer object에 부착된 두 개의 컬러 버퍼들을 가질 수 있다:


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, SCREEN_WIDTH, SCREEN_HEGIHT, 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에게 explicitly하게 우리가 glDrawBuffers를 통해서 많은 colorBuffers에 렌더링할 것이라고 말해야만 한다. 그렇지 않으면 OpenGL이 오직 다른 것들을 무시하고 한 프레임버퍼의 첫 번째 color attachment에만 렌더링할 것이기 때문이다. 우리는 나중의 오퍼레이션에서 렌더링하고 싶은 color attachment enums의 배열을 넘겨서 이것을 한다:


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

이 프레임버퍼에 렌더링할 때, 한 fragment shader가 layout location specifier를 사용할 때 마다, 그 개별 colorbuffer는 그 fragments를 렌더링 하기위해 사용되어야 한다. 이것은 우리에게 밝은 영역을 추출하기 위한 추가 render pass를 아껴주기 때문에 훌륭하다. 우리는 이제 렌더링될 fragment로부터 그것들을 직접 추출할 수 있다:


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

in VS_OUT
[
 vec3 FragPos;
 vec3 Normal;
 vec2 TexCoords;
} fs_in;

struct Light
{
 vec3 Position;
 vec3 Ambient;
 vec3 Diffuse;
 vec3 Specular;
}

uniform sampler2D diffuseTexture;
uniform vec3 viewPos;
uniform Light lights[4];

void main()
{
 // common factor
 vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
 vec3 normal = normalize(fs_in.Normal);
 vec3 viewDir = normalize(viewPos - fs_in.FragPos);

 vec3 result = vec3(0.0);
 
 for(int i = 0; i < 4; ++i)
 {
  vec3 ambient = lights[i].Ambient * color;
  
  vec3 lightDir = normalize(lights[i].Position - fs_in.FragPos);
  float diff = max(dot(lightDir, normal), 0.0);
  vec3 diffuse = diff * lights[i].Diffuse * color;
  
  vec3 halfwayDir = normalize(lightDir + viewDir);
  vec3 spec = pow(max(dot(normal, halfwayDir), 0.0), 64);
  vec3 specular = spec * lights[i].Specular;

  result += (ambient + diffuse + specular);
 }

 FragColor = vec4(result, 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);

}

여기에서 우리는 처음에 보통 lighting을 계산하고, 그것을 첫 번째 fragment shader의 output 변수 FragColor로 넘긴다. 그러고나서 우리는 그것의 밝기가 어떤 threshold를 넘는지를 결정하기 위해 FragColor에 현재 저장된 것을 사용한다. 우리는 그것을 적절히 grayscale로 변환하여 한 fragment의 밝기를 계산한다. (두 벡터의 내적을 취해서, 우리는 ㅎ과적으로 두 벡터의 각 개별 컴포넌트를 곱하고, 그 결과를 합한다) 그리고 만약 그것이 어떤 threshold를 넘는다면, 우리는 그 컬러를 모든 밝은 영역을 보유한 두 번째 color buffer로 output한다; light cubes에 렌더링하는것과 유사하다.

이것은 또한 왜 bloom이 믿을 수 없이 HDR rendering과 잘 작동하는지를 보여준다. 우리가 high dynamic range에서 렌더링하기 때문에, 컬러값들은 1.0을 초과할 수 있고, 그것은 우리가 기본 범위 밖의 밝기 threshold를 명시하는 것을 허용한다. 이것은 우리에게 이미지에서 밝은것으로 고려될 것에 대해 좀 더 많은 통제력을 준다. HDR없이, 우리는 그 threshold를 여전히 가능하지만 1.0보다 낮게 설정해야만 한다. 그러나 그 영역들을 더 빠르게 밝다고 고려된다. 그리고 이것은 가끔씩 너무 우세하게 된 glow effect를 이끈다. (예를들어, 하얗게 빛나는 눈을 생각해보아라.)

두 개의 컬러버퍼 내에서, 우리는 그러고나서 그 scene의 보통 이미지와 추출된 밝은 영역의 이미지를 갖는다; 한 번의 render pass로 얻어지는 것이다.

추출된 밝은 영역의 이미지로, 우리는 이제 그 이미지를 블러할 필요가 있다. 우리는 framebuffer tutorial의 후처리 섹션에서 했던 것처럼 간단한 box filter로 이것을 할 수 있지만, 우리는 오히려 Gaussian blur라고 불리는 좀 더 고급의 그리고 더 좋게 보이는 blur filter를 사용할 것이다.

Gaussian blur
후처리 blur에서, 우리는 간단히 한 이미지의 주변 픽셀들의 평균을 취한다. 그것이 우리에게 쉬운 blur를 주지만, 그것은 최상의 결과를 주지 않는다. Gaussian blur는 Gaussian curve에 기반으로 하는데, 그것은 흔히 종 모양의 curve이고, 그것의 중심으로 가까울수록 높은 값을 주고. 거리에 따라서 점차적으로 떨어진다. 가우시안 곡선은 수학적으로 다른형태로 나타내어질 수 있지만, 일반적으로 다음의 모양을 가진다:

가우시안 곡선은 그것의 중심에 가까울 수록 더 큰 면적을 가지기 때문에, 한 이미지를 블러하기 위해 그것의 값을 가중치로 사용하는 것은 최상의 결과를 준다. 가까이에 있는 샘플들이 더 높은 우선을 갖기 때문이다. 만약 예를들어 우리가 한 fragment 주변에 32x32 box가 있다면, 우리는 그 fragment에의 거리가 더 클수록 점점 더 작은 가중치를 사용한다; 이것은 일반적으로 더 좋고 좀 더 현실적인 blur를 준다. 그리고 이것이 Gaussian blur라고 알려져있다.

Gaussian blur filter를 구현하기 위해서, 우리는 2차원 가우시안 곡선 방젖ㅇ식으로부터 얻을 수 있는 2차원 가중치 박스가 필요하다. 그러나 이 접근법의 문제는 그것이 빠르게 성능에 매우 무겁게 된다는 것이다. 예를들어 32x32의 blur kernel를 가지고, 이것은 우리가 각 fragment에 대해 총 1024번 한 텍스쳐를 샘플링하는 것을 요구한다.

우리에게 운 좋게도 그 가우시안 방정식은 우리가 이차원 방정식을 더 작은 방정식으로 분리시키는 깔끔한 특징을 갖는다: horizontal weights를 묘사하는 것과 vertical weights를 묘사하는 다른 것. 우리는 처음에 horizontal weights로 전체 텍스쳐에 대해 horizontal blur를 한다. 그러고나서 결과 텍스쳐에 대해 vertical blur를 한다. 이 특성때문에, 그 결과는 정확히 같지만, 우리에게 엄청난 양의 성능을 아끼게 해준다. 1024개와 비교해서 32 + 32개의 샘플들만 하면 되기 때문이다. 이것은 two-pass Gaussian blur라고 알려져 있다.

이것은 우리가 적어도 한 이미지에 대해 두 번 블러할 필요가 있다는 것을 의미한다. 그리고 이것은 framebuffer objects의 사용으로 또 다시 가장 잘 작동한다. 가우시안 블러를 구현하는데 있어서 특정하게, 우리는 ping-pong framebuffers를 구현할 것이다. 그것은 한 쌍의 framebuffers인데, 거기에서 우리는 주어진 횟수만큼 몇 가지 쉐이더 효과를 번갈아가면서 다른 framebuffer의 colorbuffer를 현재 framebuffer의 colorbuffer에 렌더링 한다. 우리는 기본적으로 지속적으로 그릴 프레임버퍼를  switch하고 또한 그릴 텍스쳐도 스위치한다. 이것은 우리가 첫 번째 프레임 버퍼에 있는 그 scene의 텍스쳐를 블러하게 하고, 그러고나서 그 첫 번째 framebuffer의 colorbuffer를 두 번째 framebuffer로 blur시킨다. 그러고나서 그 두 번째 framebuffer의 colorbuffer가 첫 번쨰로 들어간다. 등등.

우리가 framebuffer를 자세히 알아보기전에, Gaussian blur 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 ize 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(tex_offset.y * i, 0.0)).rgb * weight[i];
   result += texture(image, TexCoords - vec2(tex_offset.y * i, 0.0)).rgb * weight[i];
  }
 }
 FragColor = vec4(result, 1.0);
}

여기에서 우리는 현재 fragment 주변의 수평 또는 수직 샘플들에 대해 특정한 weight를 할당하기 위해 사용할 가우시안 가중치의 상대적으로 작은 sample를 취한다. 너는 우리가 기본적으로 우리가 horizontal uniform을 무슨 값으로 정했는지에 따라 그 blur filter를 수평과 수직 부분으로 나눈 것을 볼 수 있다. 우리는 texture의 크기 (textureize로부터 vec2로 얻어지는)를 1.0으로 나눠서 얻어지는 texel의 정확한 크기에 offset distance의 기반을 둔다.

한 이미지를 blurring하기 위해, 우리는 두 가지 기본 framebuffers를 만든다, 그리고 그 각각은 오직 하나의 colorbuffer texture를 갖는다:


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, SCREEN_WIDTH, SCREEN_HEGIHT, 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 texture와 추출된 brightness texture를 얻은 후에, 우리는 처음에 ping-pong framebuffers들 중에 하나를 brightness texture로 채우고, 그러고나서 그 이미지를 10번 blur한다. (5번은 수평으로 5번은 수직으로):


bool horizontal = true, first_iteration = true;
int amount = 10;
GaussianShader.use();
for (unsigned int i = 0; i < amount; ++i)
{
 glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]);
 GaussianShader.setBool("horizontal", horizontal);
 glBindTexture(GL_TEXTURE_2D, first_iteration ? colorBuffers[1] : pingpongBuffer[!horizontal]);
 renderQuad();

 horizontal = !horizontal;
 if (first_iteration)
  first_iteration = false;
}

매 반복마다, 우리는 우리가 수평으로 또는 수직으로 블러 할 것인지에 따라  두 개의 프레임 버퍼중 하나를 바인드 시키고, 블러할 텍스쳐로서 다른 프레임버퍼의 컬러버퍼를 바인드 시킨다. 첫 번째 반복에서, 우리는 특정하게 우리가 블러하고 싶은 텍스쳐를(brightnessTexture) 로 바인드시킨다. 두 컬러버퍼들이 비어있기 때문이다. 이 과정을 10번 반복하여, 그 brightness image는 5번 반복된 완전한 가우시안 블러를 하게 된다. 이 구성은 우리가 우리가 원하는 어떤 이미지든 종종 블러할 수 있게 한다; 가우시안 블러를 더 많이 할수록, 그 블러는 더욱 강해진다.

그 추출된 brightness texture 5번 blurring하여, 우리는 scene의 모든 밝은 영역의 적절히 블러된 이미지를 얻는다:


그 bloom effect를 완전히 할 마무리 단계는 이 블러된 밝기 텍스쳐를 원래 scene의 HDR texture와 합치는 것이다.

Blending both textures
그 scene의 HDR texture와 scene의 블러된 brightness texture로, 우리는 그 악명높은 bloom or glow effect를 얻기위해 두 개를 합칠 필요가 있다. 마지막 fragment shader에서 (우리가 HDR tutorial에서 사용했던 것과 크게 유사한) 우리는 두 텍스쳐를 더해서 섞는다:


#version 330 core

out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D screenTexture;
uniform sampler2D bloomBlur;

void main()
{
 const float gamma = 2.2;
 const float exposure = 1.0;
 vec3 hdrColor = vec3(texture(screenTexture, TexCoords));
 vec3 bloomColor = vec3(texture(bloomBlur, TexCoords));
 hdrColor += bloomColor;
 
 // Reinhard tone mapping
 // vec3 mapped = hdrColor / (vec3(1.0) + hdrColor);

 // Exposure tone mapping
 vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
 
 // gamma correction
 mapped = pow(mapped, vec3(1.0 / gamma));

 FragColor = vec4(mapped, 1.0);
}

여기에서 주목해야 할 흥미로운 것은 우리가 tone mapping을 하기전에 bloom effect를 더한 것이다. 이 방식으로, bloom의 더 해진 밝기는 또한 결과적으로 더 좋은 상대적인 lighting과 함께 부드럽게 LDR range로 변환된다.

두 텍스쳐를 함께 더해서, 우리의 scne의 모든 밝은 지역들은 이제 적절한 glow effect를 얻는다:


그 색이 칠해진 큐브는 이제 좀 더 밝아 보이고, 빛을 뿜어내는 오브젝트로서 더 좋은 환영을 준다. 이것은 상대적으로 간단한 scene이다. 그래서 그 bloom effect는 여기에선 너무 이상적이지 않지만, 잘 비춰지는 scenes에서 적절히 설정되었을 때 그것은 엄청난 차이를 만들 수 있다. 너는 이 간단한 데모의 소스코드를 여기에서 볼 수 있다.

이 튜토리얼에 대해, 우리는 상대적으로 간단한 가우시안 블러 필터를 사용했다. 거기에서 우리는 오직 각 방향에 대해 5개의 샘플들을 취한다. 더 큰 반경을 따라 더 많은 샘플들을 취하거나 그 blur filter를 추가 횟수를 더하여 반복한다면, 우리는 blur effect를 개선시킬 수 있다. 그 blur의 퀄리티는 직접적으로 bloom effect의 퀄리티와 상호관계가 있기 때문에, 그 blur step을 개선시키는 것은 상당한 개선을 만들 수 있다. 그러한 개선들 중 몇몇은 blur filters들을 다양한 사이즈의 blur kernels로 합치거나 선택적으로 가중치를 합치기 위해 수 많은 가우시안 커브들을 사용하는 것이다. 아래에 있는 Kalogirou와 EpicGames의 부가 자료는 Gaussian blur를 개선하여 어떻게 그 bloom effect를 개선시킬지를 이야기한다.

Additional resources





댓글 없음:

댓글 쓰기