HDR & Bloom

Date:     Updated:

카테고리:

태그:

Main Reference
- Learn OpenGL
- Rinthel Kwon - OpenGL Lecture
- HDR : High Dynamic Range


High Dynamic Range (HDR)

Dynamic Range Color Space
1232421 346252

실제 인간이 인지할 수 있는 빛의 밝기는 모니터가 표현할 수 있는 밝기보다 훨씬 높은 수치이다. 마찬가지로 모니터가 표현할 수 있는 색상 영역도 제한적이다. 방송 표준이 제정되는 시기 일반 디스플레이는 0.1 ~ 100 nits의 밝기를 표시할 수 있었고, 1000의 명암비를 표현할 수 있었다. 이를 Standard Dynamic Range (SDR)이라 한다. 반면 HDR은 100000이상의 명암비를 표현할 수 있어야 한다.


이미지 영역에서의 HDR과, 디스플레이 영역에서의 HDR 정의가 살짝 달라서 조금 의아했다. 이미지 영역에서는 RGB 각 채널의 값이 32bit로 표기되어야 HDR이라 부르고, 일반적인 8bit 이미지는 LDR이라 부른다. 반면 디스플레이 영역에서는 표기할 수 있는 bit-depth가 10bit만 되어도 HDR이라 한다고 한다.


Tone Mapping

HDR을 지원하지 않는 모니터에 HDR 이미지를 띄우게 되면 이미지의 일부가 너무 밝거나 너무 어두워져 디테일을 잃게 된다. 따라서 일반적인 사용자를 배려해야 하는 상황에서는 HDR 이미지를 LDR로 매핑하는 과정을 거쳐야 한다. 이때 Tone Mapping 기법을 이용하면 HDR 이미지의 밝기 범위를 적절하게 조정하고, 디테일을 유지하면서 LDR 이미지로 매핑할 수 있게 된다. 먼저 간단하게만 원리를 설명하자면 노출값을 달리한 여러 장의 이미지를 합쳐 새로운 이미지를 만들어 내는 것이다.


glBindTexture(GL_TEXTURE_2D, colorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F,
    SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);

// fragment shader
uniform float exposure;
void main() {
  const float gamma = 2.2;
  vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
  // 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);
}

먼저 Frame Buffer 생성 시 사용하는 텍스처가 RGBA_int8이 아닌 float16 또는 float32로 저장하게 설정해야 한다. 이후 노출도에 따라 프레임 버퍼를 렌더링 한 후, 렌더링된 결과들을 토대로 최종 화면을 렌더링하면 된다.


1235312


Bloom

Effect Algorithm
14_bloom_comparison 14_bloom_overview

Bloom Effect는 1 이상의 밝은 빛을 시각적으로 표현해주는 기법이다. 원리를 간단하게 설명하면 다음과 같다.

  • 원본 이미지에서 밝기가 1 이상인 영역 추출
  • 해당 영역 블러링
  • 원본 이미지와 합성 후 tone mapping


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

unsigned int attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
glDrawBuffers(2, attachments);
  • Bloom 이펙트를 구현하기 위해 먼저 빛나는 영역을 추출해야 함
  • 기본 장면 & 빛나는 영역 함께 렌더링 (Multiple Render Target)
    • Frame Buffer에 두 개의 color attachment 텍스처 바인딩
  • GL_COLOR_ATTACHMENT0부터 차례대로 바인딩 후
  • glDrawBuffers로 opengl에게 알려줘야 함


// fragment shader
#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);
    // if fragment output is higher than threshold, output 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);
}
  • brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
    • 밝기를 구할 때 RGB 각 채널을 더해 3으로 나누는 것이 아닌 특이한 벡터를 내적하는 방식
    • 실제 밝기에 가장 큰 영향을 미치는 컬러는 G 채널이라고 함..
  • brightness가 1보다 크면 해당 컬러 저장
  • brightness가 1보다 작으면 0 저장


14_bloom_separate_gaussian

참고로 Blurring의 경우 일반적으로 Gaussian 함수를 이용하게 된다. Gaussian의 경우 원점을 기준으로 모든 점이 대칭이기 때문에 굳이 K^2 연산을 할 필요가 없다. 먼저 한 축에 대해서 Blurring 연산을 하고, 이를 다른 축에 대해 blurring 연산을 하게 되면 2*K 연산으로 해결이 가능하다.


Result

Before After
14_bloom_test_result 14_bloom_test_result (1)



맨 위로 이동하기

OpenGL 카테고리 내 다른 글 보러가기

댓글 남기기