OpenGL

[Learn OpenGL 번역] 5-5. 고급 OpenGL - Framebuffers

짱승_ 2018. 8. 11. 17:57

Framebuffers

고급 OpenGL/Framebuffers

  지금까지 우리는 여러가지 유형의 스크린 버퍼들을 사용해왔습니다. 컬러 값들을 작성하는 color buffer, 깊이 정보를 작성하기 위한 depth buffer, 마지막으로 특정한 조건에 의해 해당 fragment들을 폐기하는 stencil buffer가 있었습니다. 이러한 버퍼들을 결합한 것을 framebuffer라고 부르고 이는 메모리의 어딘가에 저장됩니다. OpenGL은 우리만의 framebuffer들을 자유롭게 정의할 수 있도록 해주고 따라서 우리는 우리가 직접 컬러를 정의할 수 있고 추가적으로 depth, stencil buffer도 정의할 수 있습니다.


  지금까지 우리가 수행했던 렌더링 작업들은 모두 기본 framebuffer에 있는 렌더 buffer의 위에서 동작되었습니다. 기본 framebuffer는 여러분이 윈도우 창을 생성할 때 생성됩니다(GLFW가 자동으로 해줍니다). 우리만의 framebuffer를 생성하면 렌더링하는 데에 추가적인 기능들을 얻을 수 있습니다.


  Framebuffer에 대한 개념이 잘 이해가 가지 않으실 것입니다. 하지만 여러분의 scene을 여러가지 framebuffer로 렌더링하면 거울을 생성할 수 있고 멋진 전처리 효과들을 생성할 수 있습니다. 먼저 그들이 실제로 어떻게 동작하는지 다루고 그 후에 이러한 멋진 전처리 효과들을 구현해봄으로써 framebuffer를 사용할 것입니다.

Framebuffer 생성

  OpenGL의 다른 객체들과 마찬가지로 glGenFramebuffers 라고 불리는 함수를 사용하여 framebuffer 객체(FBO)를 생성할 수 있습니다.


unsigned int fbo;
glGenFramebuffers(1, &fbo);

  이러한 객체 생성과 사용법은 여러번 봤었던 패턴이므로 그들의 함수들은 우리가 봐왔던 다른 객체들과 비슷합니다. 먼저 framebuffer 객체를 생성하고 바인딩하여 framebuffer를 활성화시킵니다. 그 후에 조작을 하고 framebuffer를 언바인딩합니다. framebuffer를 바인딩하기 위해 glBindFramebuffer 함수를 사용합니다.


glBindFramebuffer(GL_FRAMEBUFFER, fbo);  

  GL_FRAMEBUFFER 타겟에 바인딩함으로써 이후에 나오는 모든 framebuffer 읽기, 작성 명령이 현재 바인딩된 framebuffer에 영향을 미칩니다. 또한 framebuffer를 GL_READ_FRAMEBUFFERGL_READ_FRAMEBUFFER 타겟에 바인딩하여 읽기, 작성 명령을 구분할 수도 있습니다. GL_READ_FRAMEBUFFER에 바인딩된 framebuffer는 glReadPixels 과 같은 모든 읽기 명령에 사용됩니다. GL_DRAW_FRAMEBUFFER에 바인딩된 framebuffer는 렌더링, 비우기, 다른 작성 연산에 대한 목적지로서 사용됩니다. 대부분의 경우에 여러분의 이렇게 분리할 필요가 없고 일반적으로 GL_FRAMEBUFFER에 바인딩합니다.


  불행히도, 여기까지만 해서는 아직 우리만의 framebuffer를 사용할 수 없습니다. framebuffer를 완전하게 만들기 위해 다음과 같은 요구사항을 만족해야합니다.


  • 최소한 하나의 buffer(color, depth 혹은 stencil buffer)를 첨부해야 합니다.
  • 최소한 하나의 color 첨부가 존재해야 합니다.
  • 모든 첨부 buffer들은 완적해야 합니다(메모리가 할당).
  • 각 buffer들은 샘플의 갯수가 같아야 합니다.

  샘플이 무엇인지 몰라도 걱정하지 마세요. 나중의 강좌에서 다룰것입니다.


  요구사항에 따르면 framebuffer에 대한 첨부할 것들을 생성하고 첨부해야 합니다. 모든 첨부들을 완료한 후에 glCheckFramebufferStatus 함수에 GL_FRAMEBUFFER를 인자로 넘겨주어 호출하여 실제로 완성되었는지 확인할 수 있습니다. 그러면 현재 바인딩된 framebuffer를 확인하고 이러한 값들을 리턴하게 됩니다. GL_FRAMEBUFFER_COMPLETE가 리턴되었다면 계속해도 좋습니다.


if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
  // 승리의 춤을 실행

  이후의 모든 렌더링 작업들은 이제 현재 바인딩된 framebuffer에 첨부된 것들에 렌더링하게 됩니다. 우리의 framebuffer는 기본 framebuffer가 아니기 때문에 렌더링 명령들이 여러분의 윈도우창의 출력에 아무런 영향을 주지 않습니다. 이러한 이유에서 다른 framebuffer에 렌더링하는 것을 off-screen 렌더링이라고 부릅니다. 모든 렌더링 작업들을 메인 윈도우창에 나타내기 위해 0을 바인딩하여 다시 기본 framebuffer를 활성화 시켜야 합니다.


glBindFramebuffer(GL_FRAMEBUFFER, 0);   

  모든 framebuffer 작업을 완료하면 framebuffer 객체를 제거하는 것을 잊지 마세요.


glDeleteFramebuffers(1, &fbo);  

  완전히 생성되었는지를 확인하기전에 우리는 하나 이상의 것들을 framebuffer에 첨부해야 합니다. 첨부물들은 framebuffer에서 buffer처럼 행동하는 메모리 위치입니다. 첨부물을 생성할 때 우리는 두개의 옵션을 가지고 있습니다. 텍스처 또는 renderbuffer 객체입니다.

Texture 첨부물

  텍스처를 framebuffer에 첨부할 때 모든 렌더링 명령들은 마치 일반적인 color/depth 혹은 stencil buffer처럼 텍스처에 작성합니다. 텍스처를 사용하여 얻는 이점은 렌더링 작업의 결과가 텍스처 이미지로 저장되기 때문에 우리의 shader에서 쉽게 사용할 수 있다는 점입니다.


  Framebuffer를 위한 텍스처를 생성하는 것은 일반적인 텍스처와 거의 비슷합니다.


unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
  
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  

  여기에서 큰 차이점은 텍스처의 크기를 스크린 크기로 설정한다는 것과 텍스처의 data 파라미터에 NULL을 집어넣는다는 것입니다. 이 텍스처에 대해 우리는 오직 메모리만 할당하고 실제로 채워넣진 않고 있습니다. 텍스처를 채우는 것은 우리가 framebuffer에 렌더링을 하면 수행도리 것입니다. 또한 우리는 어떠한 wrapping method나 mimapping을 신경쓰지 않고 있다는 것을 알아두세요. 대부분의 경우에 필요가 없기 때문입니다.

  여러분의 전체 화면을 작거나 큰 크기의 텍스처에 렌더링하고 싶다면 glViewport 함수를 다시 호출하여(framebuffer에 렌더링하기 전에) 텍스처의 새로운 크기를 인자로 넘겨주세요. 그러지 않으면 텍스처 혹은 화면의 일부분만 텍스처에 그려질 것입니다.

  텍스처를 생성했으므로 이제 마지막 해야할 일은 이 텍스처를 실제로 framebuffer에 첨부하는 것입니다.


glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);  

  glFrameBufferTexture2D 함수는 다음과 같은 파라미터들을 가지고 있습니다.


  • target: 텍스처를 첨부할 타겟 framebuffer(draw, read 혹은 둘다)
  • attachment: 첨부할 첨부물의 유형. 지금 우리는 color 첨부물을 첨부하고 있습니다. 마지막에 붙은 0은 우리가 하나 이상의 color 첨부물을 첨부할 수 있다는 것을 암시합니다. 이는 나중에 다루도록 하겠습니다.
  • textarget: 첨부하기 원하는 텍스처의 유형
  • texture: 첨부할 실제 텍스처
  • level: Mipmap 레벨. 우리는 0으로 유지할것입니다.

  Color 첨부물 외에 우리는 또한 depth, stencil 텍스처를 framebuffer 객체에 첨부할 수 있습니다. depth 첨부물을 첨부하기 위해 우리는 첨부물 유형을 GL_DEPTH_ATTACHMENT로 지정합니다. 텍스처의 format(형식), internalformat(내부형식)GL_DEPTH_COMPONENT로 지정해야 depth buffer의 저장 형식을 반영합니다. stencil buffer를 첨부하기 위해서는 두 번째 파라미터에 GL_STENCIL_ATTACHMENT를 지정하고 텍스처의 형식을 GL_STENCIL_INDEX로 지정합니다.


  Depth, stencil buffer를 하나의 텍스처로 만들어 첨부할 수도 있습니다. 그러면 텍스처의 각 32비트 값은 24비트의 depth 정보, 8비트의 stencil 정보로 이루어집니다. depth, stencil buffer를 하나의 텍스처로 첨부하기 위해 우리는 GL_DEPTH_STENCIL_ATTACHMENT를 사용하고 텍스처의 형식을 depth와 stencil 값을 결합한 것으로 설정합니다. 이 예제는 다음과 같습니다.


glTexImage2D(
  GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, 
  GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
);

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);  

Renderbuffer 객체 첨부물

  Renderbuffer 객체는 텍스처 다음에 나온 framebuffer 첨부물의 유형입니다. 그래서 텍스처는 옛날에만 쓰였던 첨부물입니다. 텍스처 이미지와 마찬가지로 renderbuffer 객체는 실제 buffer입니다. 예를 들어 바이트, 정수형, 픽셀 등의 배열. renderbuffer 객체는 데이터를 OpenGL의 자연적인 렌더링 형식에 저장하여 framebuffer에 off-screen 렌더링을 할 때에 최적화되어 있다는 이점을 가지고 있습니다.


  Renderbuffer 객체는 모든 렌더링 데이터들을 그들의 buffer에 아무런 텍스처 형식에 따른 변환 없이 직접적으로 저장할 수 있습니다. 따라서 좀 더 빠른 저장공간을 만들 수 있습니다. 하지만 renderbuffer 객체는 일반적으로 작성만 가능합니다. 따라서 그들로부터 데이터를 읽을 수는 없습니다. glReadPixels 함수를 통해 현재 바인딩된 framebuffer로부터 픽셀의 특정 영역을 읽을 수 있지만 첨부물 자체에서 직접적으로 읽어올 수는 없습니다.


  데이터가 이미 자연적인 형식안에 들어있기 때문에 데이터를 작성하거나 단순히 데이터를 다른 buffer로 복사할 때 꽤 빠릅니다. 따라서 buffer 교환같은 작업들은 renderbuffer 객체를 사용할 때 빠릅니다. glfwSwapBuffers 함수 또한 renderbuffer 객체로 구현되어있습니다. 우리는 단순히 renderbuffer 이미지에 작성을하고 마지막에 다른 것과 교체합니다. Renderbuffer 객체는 이러한 작업에 아주 좋습니다.


  Renderbuffer 객체를 생성하는 것은 framebuffer의 코드와 비슷합니다.


unsigned int rbo;
glGenRenderbuffers(1, &rbo);

  그리고 비슷하게 renderbuffer를 바인딩하므로 이후의 모든 renderbuffer 작업들은 현재의 rbo에 영향을 미칩니다.


glBindRenderbuffer(GL_RENDERBUFFER, rbo);  

  Renderbuffer 객체가 일반적으로 작성만 가능하기 때문에 depth, stencil 첨부물로 사용됩니다. 대부분의 경우에 depth, stencil buffer로부터 값을 읽어올 필요가 없지만 depth, stencil testing을 할때엔 필요합니다. testing을 위해서는 depth, stencil 값들이 필요합니다. 하지만 이 값들을 sample할 필요는 없으므로 renderbuffer 객체는 이 경우에 아주 딱 맞습니다. 이 buffer들로부터 sampling하고 있지 않다면 renderbuffer 객체가 일반적으로 최적화를 위해 사용됩니다.


  Depth, Stencil renderbuffer 객체를 생성하는 것은 glRenderbufferStorage 함수를 호출하여 수행됩니다.


glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

  Renderbuffer 객체를 생성하는 것은 텍스처 객체와 비슷합니다. 여기에서 우리는 internal format을 GL_DEPTH24_STENCIL8로 설정했습니다. 이는 depth, stencil buffer를 24비트와 8비트로 나눈다는 의미입니다.


  마지막 남은 일은 실제로 renderbuffer 객체를 첨부하는 것입니다.


glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);  

  Renderbuffer 객체는 여러분의 framebuffer 프로젝트에 최적화를 제공해줍니다. 하지만 renderbuffer 객체를 사용할 때와 텍스처를 사용할 때에 깨닫는게 중요합니다. 일반적인 규칙은 여러분이 특정 버퍼에서 데이터를 절대 sample할 필요가 없다면 그 특정 buffer에 renderbuffer를 사용하는 것이 현명합니다. 언젠간 특정 buffer로부터 color 값이나 깊이 값처럼 데이터를 sample해야 한다면 텍스처 첨부물을 사용해야 합니다.

텍스처에 렌더링

  Framebuffer가 어떻게 동작하는지 알게되었으니 이제 사용해볼 시간입니다. 우리는 생성한 framebuffer 객체에 첨부된 color 텍스처에 scene을 렌더링할 것입니다. 그런 다음 이 텍스처를 화면을 가득채운 간단한 사각형에 그릴 것입니다. 시각적인 출력은 정확히 framebuffer 없이 수행했을 때와 동일합니다. 하지만 모든 것이 하나의 사각형 위에 그려집니다. 이제 이게 왜 유용한 것일까요? 다음 섹션에서 이 이유를 알아봅시다.


  먼저 해야할 일은 실제 framebuffer 객체를 생성하고 바인딩 하는 것입니다. 이는 비교적 쉽습니다.


unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);    

  그 다음 framebuffer에 color 첨부물로서 첨부할 텍스처 이미지를 생성합니다. 텍스처의 크기를 윈도우 창의 크기와 동일하게 설정하고 데이터를 지정하지 않습니다.


// 텍스처 생성
unsigned int texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);

// 현재 바인딩된 framebuffer 객체에 첨부
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);  

  또한 우리는 OpenGL이 depth testing(그리고 원한다면 stencil testing까지)을 할 수 있도록 해야하기 때문에 framebuffer에 depth(그리고 stencil) 첨부물도 추가해야 합니다. 우리는 오직 color buffer만 sampling할 것이기 때문에 이 목적에 renderbuffer를 생성할 수 있습니다. 특정 버퍼를 sample하지 않을 것이라면 이 것이 좋은 선택이라는 것을 기억하나요?


  Renderbuffer 객체를 생성하는 것은 어렵지 않습니다. 기억해야할 것은 이 것을 depth 그리고 stencil 첨부물 renderbuffer 객체를 생성한다는 것입니다. internal formatGL_DEPTH24_STENCIL8로 설정하여 우리의 목적에 대해 충분히 만족하는 옵션을 선택합니다.


unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo); 
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);  
glBindRenderbuffer(GL_RENDERBUFFER, 0);

  이 renderbuffer 객체에 충분한 메모리가 할당되면 이 renderbuffer를 언바운딩할 수 있습니다.


  그런 다음 framebuffer를 완성하기 전의 마지막 단계로서 renderbuffer 객체를 framebuffer에 첨부합니다.


glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

  그런 다음 마지막 평가로 이 framebuffer가 실제로 완성이 되었는지 확인하고 그게 아니라면 에러 메시지를 출력합니다.


if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
	std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

  그리고 이제 framebuffer를 언바운딩하여 우연히 잘못된 framebuffer에 렌더링되는 일이 발생하는 것을 방지합니다.


  이제 framebuffer가 완성되었으므로 기본 framebuffer 대신에 이 framebuffer에 렌더링하는 것은 간단히 framebuffer 객체를 바운딩하기만 하면 됩니다. 이 후의 모든 렌더링 명령들은 현재 바운딩된 framebuffer에 영향을 미칩니다. 모든 depth, stencil 작업들은 가능하다면 현재 바인딩된 framebuffer의 depth, stencil 첨부물로부터 값을 읽습니다. 예를 들어 depth buffer를 빼먹었다면 모든 depth testing 작업들은 동작하지 않게 됩니다. 현재 바운딩된 framebuffer에 depth buffer가 없기 때문이죠.


  그래서 하나의 텍스처에 scene을 그리기 위해 다음과 같은 단계를 거쳐야 합니다.


  1. 활성화된 framebuffer로서 바인딩된 새로운 framebuffer에 평상시대로 scene을 렌더링합니다.
  2. 기본 framebuffer를 바인딩합니다.
  3. 전체 화면에 맞게 늘린 사각형을 그리고 텍스처로 새로운 framebuffer의 color buffer를 사용합니다.

  우리는 depth testing 강좌에서 사용했던 것과 동일한 scene을 그릴 것입니다. 하지만 이번에는 올드스쿨 컨테이너 텍스처를 사용합니다.


  사각형을 그리기 위해 간단한 shader 세트를 생성할 것입니다. 우리는 어떠한 행렬 변환도 포함시키지 않을 것입니다. vertex 좌표만을 제공할 것이기 때문입니다. 이 vertex shader는 다음과 같습니다.


#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
    TexCoords = aTexCoords;
}  

  복잡한 것은 전혀 없습니다. 심지어 이 fragment shader는 더 기본적입니다. 오직 텍스처를 sample하기만 하기때문이죠.


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

uniform sampler2D screenTexture;

void main()
{ 
    FragColor = texture(screenTexture, TexCoords);
}

  사각형에 대한 VAO를 생성하고 설정하는 것은 여러분에 달려있습니다. framebuffer 과정의 반복은 다음과 같은 구조를 가지고 있습니다.


// 첫 번째 단계
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 지금은 stencil buffer를 사용하지 않습니다
glEnable(GL_DEPTH_TEST);
DrawScene();	
  
// 두 번째 단계
glBindFramebuffer(GL_FRAMEBUFFER, 0); // 다시 기본값으로
glClearColor(1.0f, 1.0f, 1.0f, 1.0f); 
glClear(GL_COLOR_BUFFER_BIT);
  
screenShader.use();  
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);  

  알아두어야 할 것이 몇가지 있습니다. 첫째, 우리가 사용하고 있는 각 framebuffer들은 그들의만 buffer 세트를 가지고 있기때문에 glClear 함수를 사용하여 각 버퍼들을 비워주어야 합니다. 둘째, 사각형을 그릴때 간단한 사각형만 그리므로 depth testing을 신경쓸 필요가 없기때문에 depth testing을 비활성화하고 있습니다. 일반적인 scene을 그릴 때 다시 depth testing을 활성화해야 합니다.


&nbps; 여러분이 아무런 결과를 얻지 못하였다면 디버그하면서 이 강좌의 관련된 섹션을 찾아보세요. 모든것이 성공적으로 수행되었다면 다음과 같은 결과를 볼 수 있습니다.


An image of a 3D scene in OpenGL rendered to a texture via framebuffers

  왼쪽은 depth testing 강좌에서 보았던 것과 정확히 동일한 결과입니다. 하지만 이번에는 사각형에 렌더링을 한것이죠. 이 scene을 wireframe으로 렌더링 한다면 하나의 사각형만 그려질 것입니다.


  전체 소스 코드는 여기에서 확인할 수 있습니다.


  그래서 이것이 왜 유용한 것일까요? 지금 우리는 하나의 텍스처 이미지로서 완전히 렌더링된 scene을 자유롭게 접근할 수 있기 때문에 fragment shader에서 흥미로운 효과들을 생성할 수 있습니다. 이러한 모든 효과들을 통틀어서 post-processing(전처리) 효과라고 부릅니다.

Post-processing

  이제 하나의 텍스처에 전체 scene에 렌더링 되었으므로 텍스처 데이터를 조작하여 흥미로운 효과들을 생성할 수 있습니다. 이번 섹션에서 우리는 가장 많이 쓰이는 post-processing 효과들을 보여줄 것입니다.


  가장 간단한 post-processing 효과들부터 시작해봅시다.

Inversion(반전)

  Fragment shader에서 렌더링 출력의 각 컬러들에 대해 접근하여 이 컬러들을 반전시키는 것은 어렵지 않습니다. 우리는 screen 텍스처의 컬러를 얻어와 1.0에서 이 값을 빼서 반전시킵니다.


void main()
{
    FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}  

  Inversion은 비교적 간단한 post-processing 효과이지만 펑키한 결과를 만듭니다.


Post-processing image of a 3D scene in OpenGL with inversed colors

  이제 전체 scene이 모두 반전된 컬러를 가지고 있습니다. fragment shader의 한줄에 의해서 말이죠. 아주 멋지죠?

Grayscale

  또다른 흥미로운 효과는 scene의 모든 컬러에서 흰색, 회색, 검정색을 제외한 모든 색을 제거하여 전체 이미지를 graysacle 하는 것입니다. 이를 수행하는 쉬운 방법은 모든 컬러 컴포넌트를 얻어서 평균을 내는 것입니다.


void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
    FragColor = vec4(average, average, average, 1.0);
}   

  이 것은 꽤 좋은 결과를 생성합니다. 하지만 인간의 눈은 녹색에 예민하고 파란색에 덜 예민하므로 물리적으로 가장 정확한 결과는 weighted 채널을 사용해야 합니다.


void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = 0.2126 * FragColor.r + 0.7152 * FragColor.g + 0.0722 * FragColor.b;
    FragColor = vec4(average, average, average, 1.0);
}   
Post-processing image of a 3D scene in OpenGL with grayscale colors

  여러분은 아마도 차이를 알아차리지 못했을 것입니다. 하지만 더 복잡한 scene에서는 이러한 weighted grayscaling 효과가 더욱 현실적인 효과를 냅니다.

Kernel 효과들

  하나의 텍스처 이미지에 post-processing 하는 것에 대한 또다른 이점은 텍스처의 다른 부분으로부터 실제로 컬러 값을 샘플할 수 있다는 것입니다. 예를 들어 현재 텍스처 좌표 주변의 작은 영역을 가져올 수 있고 여러 텍스처 값들을 가져올 수도 있습니다. 그런 다음 이를 창의적인 방법으로 결합하여 흥미로운 효과를 낼 수 있습니다.


  Kernel(혹은 나선형 행렬)은 주변 픽셀 값에 커널 값을 곱한 후 현재 값을 모두 더하여 하나의 값을 형성하는 현재 픽셀을 중심으로하는 값의 작은 행렬입니다(?). 그래서 기본적으로 우리는 현재 픽셀의 주변 방향으로 텍스처 좌표의 작의 offset을 추가하고 kernel을 기반으로 결과를 결합합니다. kernel의 예는 다음과 같습니다.

[2222152222]

  이 kernel은 8개의 둘러싸인 픽셀 값들을 취하고 이들을 2로 곱합니다. 그리고 현재 픽셀에 -15를 곱합니다. 이 예로들은 kernel은 기본적으로 주변의 픽셀들을 결정된 weight로 곱하고 현재 픽셀을 큰 음수 weight로 곱함으로써 밸런스를 맞춰줍니다.

  인터넷에서 찾을 수 있는 대부분의 kernel들은 모든 수들을 서로 합하면 결과가 1이 나옵니다. 그들의 합산이 1이 나오지 않는다면 이는 결과 텍스처 컬러가 원래의 텍스처 값보다 밝아지던지 어두워지던지 하는 것입니다.

  Kernel들은 post-processing에 대해 아주 유용한 도구입니다. 사용하거나 실험하기 쉽고 많은 예제들을 온라인에서 찾아볼 수 있기 때문입니다. kernel을 지원하기 위해 우리는 frament shader를 약간 수정해야 합니다. 우리가 사용할 각 kernel은 3x3 kernel이라고 가정합니다(대부분이 kernel이 그렇습니다).


const float offset = 1.0 / 300.0;  

void main()
{
    vec2 offsets[9] = vec2[](
        vec2(-offset,  offset), // 좌측 상단
        vec2( 0.0f,    offset), // 중앙 상단
        vec2( offset,  offset), // 우측 상단
        vec2(-offset,  0.0f),   // 좌측 중앙
        vec2( 0.0f,    0.0f),   // 정중앙
        vec2( offset,  0.0f),   // 우측 중앙
        vec2(-offset, -offset), // 좌측 하단
        vec2( 0.0f,   -offset), // 중앙 하단
        vec2( offset, -offset)  // 우측 하단   
    );

    float kernel[9] = float[](
        -1, -1, -1,
        -1,  9, -1,
        -1, -1, -1
    );
    
    vec3 sampleTex[9];
    for(int i = 0; i < 9; i++)
    {
        sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
    }
    vec3 col = vec3(0.0);
    for(int i = 0; i < 9; i++)
        col += sampleTex[i] * kernel[i];
    
    FragColor = vec4(col, 1.0);
}  

  Fragment shader에서 우리는 먼저 주변의 각 텍스처 좌표에 대한 9개의 vec2 offset의 배열을 생성합니다. 이 offset은 간단히 여러분이 원하는대로 정할 수 있는 상수 값입니다. 드런 다음 kernel을 정의 합니다. 이 경우에 이 kernel은 sharpen(날카롭게) kernel입니다. 이는 흥미로운 방법으로 주변의 픽셀들을 샘플링함으로써 각 컬러를 날카롭게 만듭니다. 마지막으로 각 offset을 현재 텍스처 좌표에 더한 후 이 텍스처 값들을 weighted kernel 값들과 곱하여 합산합니다.


  이 sharpen kernel은 다음과 같은 효과를 냅니다.


Post-processing image of a 3D scene in OpenGL with blurred colors

  이 것은 약에 취해있는 것처럼 흥미로운 효과를 냅니다.

Blur

  Blur 효과를 내는 kernel은 다음과 같이 정의됩니다.

[121242121]/16

  모든 값의 합산이 16이기 때문에 간단히 샘플링된 컬러들을 결합하면 매우 밝아지므로 kernel의 각 값들을 16으로 나눕니다. 최종 kernel 배열은 다음과 같습니다.


float kernel[9] = float[](
    1.0 / 16, 2.0 / 16, 1.0 / 16,
    2.0 / 16, 4.0 / 16, 2.0 / 16,
    1.0 / 16, 2.0 / 16, 1.0 / 16  
);

  Fragment shader에서 kernel float 배열만 수정했는데 완전히 다른 post-processing 효과를 생성했습니다. 이제 다음과 같이 보일 것입니다.


Post-processing image of a 3D scene in OpenGL with sharpened colors

  이러한 blur 효과는 흥미로운 가능성을 만듭니다. 예를 들어 술에 취한 효과를 내기 위해 시간이 지남에 따라 blur 정도를 바꿀 수 있습니다. 또는 메인 캐릭터가 안경을 쓰지 않았을 경우 blur 효과를 높일 수 있습니다. Blur는 우리가 나중에 강좌에서 사용할 컬러 값을 부드럽게 하는데에 유용한 도구가 될 수 있습니다.


  우리는 이러한 작은 kernel을 구현함으로써 손쉽게 멋진 post-processing 효과를 낼 수 있었습니다. 마지막으로 효과 하나를 더 보여주고 마치도록 하겠습니다.

Edge detection

  아래에서 edge-detection kernel을 확인할 수 있습니다. sharpen kernel과 매우 유사합니다.

[111181111]

  이 kernel은 모든 모서리를 하이라이트하고 나머지들은 어둡게 만듭니다. 우리가 이미지의 모서리를 신경써야 할때 유용하게 사용할 수 있습니다.


Post-processing image of a 3D scene in OpenGL with edge detection filter

  이러한 kerenl들이 Photoshop과 같은 도구에서 이미지 조작 도구/필터로 사용된다는 것은 놀랄 일이 아닙니다. 병렬 기능으로 fragment들을 처리하는 그래픽 카드들의 능력 때문에 우리는 실시간으로 픽셀 마다 이미지를 조작할 수 있습니다. 그러므로 이미지 편집 도구들은 이미지 처리에 대해서 그래픽 카드를 더 자주 사용하는 경향이 있습니다.



출처 : https://learnopengl.com, Joey de Vries

반응형