Post Lists

2018년 7월 30일 월요일

Advanced OpenGL - Framebuffers (5)

https://learnopengl.com/Advanced-OpenGL/Framebuffers

Framebuffers
지금까지 우리는 스크린 버퍼의 여러가지 유형들을 사용해 왔다: 컬러 값을 write하는 color buffer, depth information을 write하는 depth buffer, 그리고 마지막으로 우리가 어떤 조건을 기반으로 어떤 fragments를 버리도록 해주는 stencil buffer. 이러한 버퍼들의 조합은 framebuffer라고 불려지고, 메모리 어딘가에 저장된다. OpenGL은 우리에게 우리 자신의 framebuffers를 정의하는 유연성을 주고 따라서 우리 자신의 color와 부가적으로 depth and stencil buffer를 정의하도록 하는 유연성을 준다.

우리가 이제까지 한 렌더링 연산들은 모두 default framebuffer에 부착된 render buffers의 꼭대기에서 처리되었다. 그 기본 프레임버퍼는 너가 너의 window를 만들 때 만들어지고 설정되어진다 (GLFW가 이것을 우리를 위해 한다). 우리 자신의 framebuffer를 만들어서, 우리는 렌더링할 부가적인 수단을 가질 수 있다.

프레임 버퍼의 적용은 즉시 이해가 안될지도 모르지만, 너의 scene을 다른 framebuffer에 렌더링하는 것은 우리가 한 scene에 대한 거울을 만들게 해주고, 예시로 멋진 후처리 효과를 하게 한다. 첫 째로, 우리는 그것들이 실제로 어떻게 작동하는지를 이야기하고, 그러고나서 우리는 이러한 멋진 후처리 효과를 구현하여 그것들을 사용할 것이다.

Creating a framebuffer
OpenGL에서 어떤 다른 오브젝트 처럼, 우리는 glGenFramebuffers라고 불려지는 한 함수를 사용하여 (FBO라고 축약되는) framebuffer object를 만들 수 있다:

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

오브젝트를 만들고 사용하는 것의 이 패턴은 우리가 이제 수십 번 본 것이다. 그래서 그것들의 사용 함수들은 우리가 보아왔던 다른 오브젝트들과 유사하다; 첫 째로, 우리는 frame buffer object를 만들고, 그것을 active framebuffer로서 bind하고, 몇 가지 연산을 하고, 그리고 그 framebuffer를 unbind시킨다. framebuffer를 bind하기 위해서, 우리는 glBindFramebuffer를 사용한다:

  glBindFramebuffer(GL_FRAMEBUFFER, fbo);

GL_FRAMEBUFFER에 bind시켜서, 모든 다음의 read와 write의 framebuffer 연산들이 현재 bind된 framebuffer에 영향을 미칠 것이다. 또한 한 framebuffer를 GL_READ_FRAMEBUFFER 또는 GL_DRAW_FRAMEBUFFER에 각각 바인드 시켜서, 특정하게 하나의 read target 또는 write target에 bind시키는 것도 가능하다. GL_READ_FRAMEBUFFER에 바인드된 framebuffer는 그러고나서 glReadPixels와 같은 모든 read 연산들을 위해 사용된다. 그리고 GL_DRAW_FRAMEBUFFER에 바인드된 framebuffer는 rendering, clearing 그리고 다른 쓰기 연산들을 위한 목적지로서 사용된다. 대부분의 겨웅에, 너는 이 구분을 할 필요가 없고, 너는 일반적으로 GL_FRAMEBUFFER로 둘 다에 바인드 시킨다.

불행하게도, 우리는 아직 우리의 framebuffer를 사용할 수 없다. 그것이 완전하지 않기 때문이다. framebuffer가 완전해지기 위해서, 다음의 요구사항이 만족되어야만 한다:


  • 우리는 적어도 하나의 buffer를 붙여야만 한다. (color, depth or stencil buffer)
  • 적어도 하나의 color attachment가 있어야 한다.
  • 모든 attachments는 또한 완전해야만 한다. (보유된 메모리)
  • 각 버퍼는 같은 수의 샘플들을 가져야 한다.
샘플들이 무엇인지 모르는 것을 걱정하지 말아라. 우리는 나중 튜토리얼에서 그것들을 알아볼 것이다.

요구사항으로부터 우리가 framebuffer를 위해 attachment의 몇 가지 종류를 만들 필요가 있고, 이 attachment를 그 framebuffer에 붙여야만 한다는 것이 명확해질 것이다. 우리가 모든 요구사항을 완료한 후에, 우리는 GL_FRAMEBUFFER와 함께 glCheckFramebufferStatus를 호출하여, 우리가 실제로 성공적으로 framebuffer를 완성지었는지를 확인할 수 있다. 그것은 그러고나서 현재 바인드된 framebuffer를 체크하고, 명세에서 발견되는  이러한 값들 중에 어떤 것을 반환한다. 만약 그것은 GL_FRAMEBUFFER_COMPLETE를 반환하면, 우리는 진행해도 좋다:

  if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
    // execute victory dance

모든 차후의 렌더링 연산들은 이제 현재 바인드된 framebuffer의 attachments에 렌더링 할 것이다. 우리의 framebuffer가 기본 framebuffer가 아니기 때문에, 그 렌더링 명령어들은 너의 윈도우의 시각적 결과에 어떠한 영향도 갖지 않을 것이다. 이 이유 때문에, 다른 framebuffer에 렌더링 하는 동안에 그것이 off-screen rendering이라고 불려진다. 모든 렌더링 연산들이 main window에 시각적 영향을 갖게 하기 위해서, 우리는 0에 bind시켜서 기본 framebuffer를 다시 활성화 시킬 필요가 있다:

  glBindFrameBuffer(GL_FRAMEBUFFER, 0);

우리가 모든 framebuffer 연산들이 끝났을 때, framebuffer object를 delete하는 것을 잊지마라:

  glDeleteFramebuffers(1, &fbo);

이제 완전성 체크(completeness check)가 실행되기 전에, 우리는 그 framebuffer에 한 개 또는 그 이상의 attachments를 붙일 필요가 있다. 한 attachment는 framebuffer를 위해 하나의 버퍼로서의 역할을 할 수 있는 메모리 위치이다. 그것을 한 이미지로 생각해라. 한 attachment를 만들 때, 우리는 취할 두가지 옵션들이 있따: textures or renderbuffer objects.

Texture attachments
한 framebuffer에 한 texture를 attach할 때, 모든 렌더링 명령어들은 그 텍스쳐에 write할 것이다. 마치 그것이 일반적인 color/depth 또는 stencil buffer인 것처럼. textures를 사용하는 것의 이점은 모든 렌더링 연산의 결과가 우리가 우리의 쉐이더에서 쉽게 사용할 수 있는 한 텍스쳐 이미지로서 저장되어질 것이라는 것이다.

한 framebuffer를 위해 한 texture를 만드는 것은 대강 보통의 텍스쳐와 같다:


unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SCREEN_WIDTH, SCREEN_HEGIHT, 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);

여기에서 주된 차이점들은 우리가 그 크기를 스크린 사이즈와 동일하게 설정하고 (비록 이것이 요구되지 않을지라도), 그리고 우리가 texture의 data parameter로서 NULL을 넘겨줬다는 것이다. 이 텍스쳐에 대해, 우리는 오직 메모리만을 할당하고 있고, 실제로는 그것을 채우고 있지 않다. 그 텍스쳐를 채우는 것은 우리가 framebuffer에 렌더링 하자마자 발생할 것이다. 또한 우리가 mipmapping의 wrapping methods중 어떤 것도 신경쓰지 않는다는 것을 유의해라. 우리가 대부분의 경우에 그러한 것들을 필요하지 않기 때문이다.

  Green Box
  만약 너가 너의 전체 스크린을 더 작거나 또는 더 큰 사이즈의 텍스쳐에 렌더링하기 원한다면, 너는 너의 텍스쳐의 새로운 크기로 glViewport를 다시 호출할 필요가 있다. (너의 framebuffer에 렌더링 하기 전에) 만약 그렇게 안한다면, 그 텍스쳐 또는 스크린의 한 작은 부분만이 그 텍스쳐이 그려질 것이다.

한 텍스쳐를 만들었으니, 우리가 해야할 마지막 것은 실제로 그것을 framebuffer에 붙이는 것이다:

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

그 glFrameBufferTexture2D는 다음의 인자들을 갖는다:

  • target : 우리가 목표하는 framebuffer type(draw, read or both)
  • attachment : 우리가 붙이려는 부착물의 유형. 지금 당장은 우리는 color attachment를 붙치고 있다. 끝에 0은 우리가 1개 이상의 color attachment를 붙일 수 있다는 것을 암시한다.
  • textarget : 너가 붙이길 원하는 텍스쳐의 유형
  • texture : 붙일 실제 텍스쳐
  • level : mipmap level. 우리는 이것을 0으로 유지한다.
color attachments를 제외하고, 우리는 또한 framebuffer object에 depth와 stencil texture를 붙일 수 있다. depth attachment를 붙이기 위해서, 우리는 GL_DEPTH_ATTACHMENT를 attachment type으로 명시해야 한다. 그 텍스쳐의 format과 internalforamt(내부 포맷) type은 그러고나서 GL_DEPTH_COMPONENT가 되야한다. 이것은 depth buffer의 storage format을 반영하기 위해서이다. stencil buffer를 붙이기 위해서, 두 번째 인자로서 GL_STENCIL_ATTACHMENT를 사용한다. 그리고 그 텍스쳐의 포맷으로 GL_STENCIL_INDEX를 명시한다.

depth buffer와 stencil buffer 둘 다를 하나의 texture로서 붙이는 것이 또한 가능하다. 그러면 그 텍스쳐의 32 bit value 각각이 depth information의 24bits와 stencil information의 8 bits로 구성된다. depth and stencil buffer를 하나의 텍스쳐로서 붙이기 위해, 우리는 GL_DEPTH_STENCIL_ATTACHMENT type을 사용하고, 결합된 depth와 stencil values를 포함하기 위해 텍스쳐의 포멧을 설정한다. depth and stencil buffer를 하나의 텍스쳐로 framebuffer에 붙이는 한 예시는 아래에서 주어진다:


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 object attachments
Renderbuffer objects는 framebuffer attachments의 가능한 유형으로서 textures 이후에 OpenGL에 도입되었다. 그래서 texturs는 그 좋은 옛날 시절에 사용되는 유일한 부착물이다. texture image처럼, renderbuffer object는 실제의 버퍼이다. 예를들어, 바이트, integers, pixels 또는 무엇의 한 배열이다. renderbuffer object는 부가이익을 가지고 있는데, 그것은 그것의 데이터를 OpenGL의 native rendering format으로 저장한다. 이것은 한 framebuffer에 off-screen rendering하는 것을 최적화 해준다.

Renderbuffer objects는 texture-specific formats으로의 변환없이 모든 render data를 직접적으로 그것들의 버퍼에 저장한다. 따라서 이것은 writeable storage medium(쓰기 가능한 저장 매체)로서 그것들을 더 빠르게 만든다. 그러나, renderbuffer objects는 일반적으로 write-only이고, 따라서 너는 그것들로부터 읽어들일 수 없다. (texture-access로 한 것 처럼). 그 attachment 그 자체로부터 직접적으로가 아닌, 현재 바인드된 framebuffer로부터 픽셀들의 특정한 영역을 반환하는 glReadPixels로  읽어들이는 것은 가능하다.

그것들의 데이터가 이미 그것의 native format이기 때문에, data를 쓰거나 또는 그것들이ㅡ 데이터를 다른 버퍼에 복사시킬 때, 그것들은 꽤 빠르다. 버퍼를 switch하는 그러한 연산들은 따라서 renderbuffer objects를 사용할 때 무척 빠르다. 우리가 각 render iteration 의 끝에서 사용해온 glfwSwapBuffers 함수는 renderbuffer objects로 구현되어지는게 좋다: 우리는 간단히 renderbuffer image에 write하고, 끝에서 다른 것으로 swap한다. Renderbuffer objects는 이러한 종류의 연산에 완벽하다.

renderbuffer object는 만드는 것은 그 framebuffer의 코드와 비슷해 보인다:

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

그리고 유사하게, 우리는 renderbuffer object를 바인드 시키길 원한다. 그래서 모든 이후의 renderbuffer 연산들은 현재의 rbo에 영향을 미친다:

  glBindRenderbuffer(GL_RENDERBUFFER, rbo);

renderbuffer objects는 일반적으로 write-only이기 때문에, 그것들은 종종 depth와 stencil attachments로서 사용된다. 대부분의 경우에 우리는 depth와 stencil buffers로부터 값들을 읽을 필요가 없지만 depth와 stencil testing에 신경쓰기 때문이다. 우리는 testing을 위해서 depth와 stencil values가 필요하지만, 이러한 값들을 sample할 필요가 없다. 그래서 한 renderbuffer object는 이것에 완벽하게 부합한다. 우리가 이러한 버퍼들로부터 sampling하지 않을 때, 한 renderbuffer object는 그것의 더 최적화 되어있기에, 일반적으로 선호된다.

depth and stencil renderbuffer object는 만드는 것은 glRenderbufferStorage 함수를 호출하여 된다:

  glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

renderbuffer object는 만드는 것은 텍스쳐 오브젝트들과 유사하다. 차이점은 texture처럼 일반 목적의 data buffer대신에 이 오브젝트가 특정하게 한 이미지로서 사용되기 위해 설계되었다는 것이다. 여기에서 우리는 GL_DEPTH24_STENCIL8을 내부 포맷으로서 선택한다. 이것은 depth and stencil buffer를 24와 8비트로 각각 보유한다.

해야할 남은 마지막 것은 실레조 그 renderbuffer object에 attach하는 것이다:

  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_sTENCIL_ATTAACHMENT, GL_RENDERBUFFER, rbo);

renderbuffer objects는 너의 framebuffer projects에서 몇 가지 최적화를 제공하지만,  언제 renderbuffer objects를 사용하고 언제 textures를 사용할 지를 아는 것이 중요하다. 일반적인 규칙은 만약 너가 특정한 버퍼로부터 데이터를 샘플할 필요가 없다면, 그 특정한 버퍼에 대해 renderbuffer object를 사용하는 것이 지혜롭다. 만약 언젠가 colors 또는 depth values 같은 특정한 버퍼로부터 데이터를 샘플할 필요가 있다면, 너는 대신에 texture attachment를 사용해야만 한다. 그런데 그것은 성능면에서 큰 영향을 미치지 않는다.

Rendering to a texture
우리는 이제 어떻게 framebuffers가 (어느정도) 어떻게 작동하는지를 알았으니, 그것들을 사용해볼 시간이다. 우리는 scene을 우리가 만들었던 한 framebuffer object에 부착된 한 color texture에 렌더링 할 것이다. 그러고나서 우리는 이 텍스쳐를 전체 스크린을 확장시키는 간단한 quad에 그릴 것이다. 그러면 그 시각적 결과는 그 framebuffer 없는 것과 정확히 같지만 이번에 그것은 하나의 single quad 위에서 출력된다. 이제 왜 이것이 유용한가? 다음 섹션에서 우리는 그 이유를 볼 것이다.

해야할 첫 번째 것은 실제 framebuffer object를 만들고 그것을 바인드하는 것이다. 이것은 상대적으로 매우 간단하다:

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

다음으로 우리는 그 framebuffer에 대한 color attachment로서 붙이는 texture image를 만든다. 우리는 텍스쳐의 크기를 윈도우의 너비와 높이와 동일하게 설정하고 그것의 데이터를 초기화되지 않은채로 유지한다:


// generate texture
unsigned int texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SCREEN_WIDTH, SCREEN_HEGIHT, 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);

// attach it to the currently bound framebuffer object
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);

우리는 또한 OpenGL이 depth testing을 할 수 있기를 원한다 (그리고 부가적으로 너가 관심이 있다면 stencil testing도) 그래서 우리는 또한 depth (그리고 stencil) attachment를 framebuffer에 추가하도록 해야만 한다. 우리는 오직 다른 버퍼가 아닌 color buffer만을 샘플링할 것이기 때문에, 우리는 이 목적을 위해서 renderbuffer object를 만들 수 있다. 너가 그 특정한 버퍼(들)로부터 샘플링 하지 않을 때 그것들이 좋은 선택이라고 기억해라.

renderbuffer object를 만드는 것을 너무 어려운 건 아니다. 우리가 기억해야 할 유일한 것은 우리가 depth and stencil attachment renderbuffer object로서 그것을 만들고 있다는 것이다. 우리는 그것의 내부 포맷을 GL_DETPH24_STENCIL8로 설정한다. 이것은 우리의 목적에 충분한 정확도이다.


unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCREEN_WIDTH, SCREEN_HEGIHT);
glBindRenderbuffer(GL_RENDERBUFFER, 0);

우리가 renderbuffer object를 위해 충분한 메모리를 할당했다면, 우리는 renderbuffer를 unbind할 수 있다.

그러고나서 우리가 framebuffer를 마무리 짓기 전의 마지막 단계로서, 우리는 renderbuffer object를 framebuffer의 depth and stencil attachment로 붙인다:

  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 completed\n";
glBindFramebuffer(GL_FRAMEBUFFER, 0);

그러고나서 우리가 우연히 잘못된 framebuffer에 렌더링하지 않도록하기 위해 framebuffer를 확실히 unbind해라.

frambuffer가 완전해졌으니, 기본 framebuffers 대신에 그 만든 framebuffer의 버퍼에 렌더링하기위해 할 필요가 있는 모든 것은 간단히 framebuffer object에 bind시키는 것이다. 모든 이후의 rendering commands들은 그러고나서 현재 bind된 framebuffer에 영향을 미칠 것이다. 모든 depth and stencil operations는 또한 만약 이용가능한다면 현재 bind된 framebuffer의 depth와 stencil attachments로부터 read할 것이다. 만약 너가 예시로 depth buffer를 빠뜨렸다면, 모든 depth testing operations는 더 이상 작동하지 않을 것이다. 왜냐하면 현재 bind된 framebuffer에 존재하는 depth buffer가 없기 때문이다.

그래서 단일 텍스쳐에 scene을 그리기 위해서, 우리는 다음의 단계를 따라야 할 것이다:

  1. 활성화된 framebuffer로서 bind된 새로운 framebuffer로 항상 하듯이 scene을 렌더링해라.
  2. 기본 framebuffer를 bind해라.
  3. 새로운 framebuffer의 color buffer로 그것의 텍스쳐로서 전체 screen으로 확장하는 quad를 그려라.
우리는 depth testing tutorial에서 사용해온 같은 scene이 있지만, 이번에는 old-school container texture를 사용할 것이다.

그 quad를 그리기 위해서 우리는 간단한 쉐이더들의 fresh set을 만들 것이다. 우리는 어떤 멋진 matrix transformations을 포함하지 않을 것이다. 왜냐하면 우리는 그냥 NDC로서 정점 좌표를 주고있기 때문이다. 그래서 우리는 직접적으로 vertex shader의 output으로서 그것들을 명시할 수 있다. 그 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 sample2D screenTexture;

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

그러고나서 screen quad를 위한 VAO를 만들고 설정하는 것은 너에게 달려있다. framebuffer 절차의 render iteration은 그러고나서 다음의 구조를 갖는다:


glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glEnable(GL_DEPTH_TEST);
glClearColor(0.11f, 0.11f, 0.11f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
DrawScene();

// second color Texture buffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(1.f, 1.f, 1.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);

screenShader.use();
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);

주의해야 할 몇 가지 것들이 있다. 처음에, 우리가 사용하고 있는 각 framebuffer 그것 자신만의 buffers들을 갖기 때문에, 우리는 glClear를 호출하여 적절한 비트 셋으로 그러한 버퍼들 각각을 clear하길 원한다. 둘 째로, quad를 그릴 때, 우리는 depth testing을 비활성화 한다. 왜냐하면 윌는 간다한 quad를 그리기 때문에 depth testing을 신경쓸 필요가 없기 때문이다; 우리는 보통 scene을 그릴 때는 다시 depth testing을 활성화 해야만 할 것이다.

여기에서 잘못될 수 있는 몇 몇 단계들이 꽤 있다. 그래서 만약 너가 output을 갖지 못한다면, 가능한한 debug하고 그 튜토리얼의 관련된 부분을 다시 읽어라. 만약 모든 것이 성공적으로 작동했다면, 너는 이것과 같은 비쥬얼 결과를 갖을 것이다:



왼쪽은 depth testing tutorial에서 봤던 것과 정확히 같은 visual output을 보여준다. (나는 저번에 했던 face culling tutorial과 같다). 그러나 이번에 simple quad에 렌더링된. 만약 우리가 wrieframe에서 scene을 렌더링한다면, 우리가 기본 framebuffer에 single quad를 그리고 있는 것이 명백하다.
(나의 경우에는 scene이 screen보다 더 작은 quad로 그려지고 있었는데, 이유는 quadVertices를 직접 내가 작성했는데 그 때, -1 ~1의 범위가 아닌 -0.5 ~ 0.5의 범위로 해서, 더 작게 렌더링 되고 있었다. 그래서 다시 -1 ~ 1의 범위로 바꾸었다. 위에서 언급한 vertex Shader에서 NDC좌표를 주고있기 때문에 그 screen이 -1 ~ 1로 바뀌어야 한다.)

너는 여기에서 프로그램의 소스코드를 볼 수 있다.

그래서 다시 이것은 무슨 쓸모가 있는가? 음, 우리는 이제 단일의 텍스쳐 이미지로서 자유롭게 완전히 렌더링된 scene의 픽셀들 각각에 접근할 수 있기 때문에, 우리는 fragment shader에서 몇 가지 흥미로운 효과들을 만들 수 있다. 모든 이러한 흥미로운 효과들이ㅡ 결합은 post-processing effects라고 불려진다.

Post-processing
entire scene은 단일의 텍스쳐로 렌더링 되어지기 때문에, 우리는 그 텍스쳐 데이터를 조작하여 간단히 흥미로운 효과들을 만들어낼 수 있다. 이 부분에서, 우리는 너에게 가장 인기있는 후처리 효과들을 보여줄 것이고, 너에게 추가적인 창의성으로 너 자신만의 것을 어떻게 만들지를 보여줄 것이다.

가장 간단한 후처리 효과중 하나로 시작해보자.

Inversion
우리는 render output의 컬러들 각각에 접근할 수 있다. 그래서 그 fragment shader에서 이러한 컬러들의 inverse를 반환하는 것은 그리 어렵지 않다. 우리는 screen texture의 color를 가져와서 1.0에서 그것을 빼서 그것을 역수화 한다:


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

Inversion은 상대적으로 간단한 후처리 효과이지만, 그것은 이미 멋진 결과를 만들어 낸다:


그 전체 scene은 이제 fragment shader에서 한 줄의 코드로 색이 모두 반전되었다. 꽤 멋지지 않은가?

Grayscale
또 다른 흥미로운 효과는 scene에서 white, gray and black 컬러들을 제외하고 모든 컬러들을 제외하는 것이다. 이것은 효과적으로 전체 이미지를 grayscale로 만든다. 이것을 하는 한 쉬운 방법은 간단히 모든 컬러 컴포넌트들을 취해서 그것들의 결과를 평균화시키는 것이다:


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

이것은 벌써 꽤 좋은 결과를 만들지만, 인간의 눈은 초록색에 좀 더 민감하고, 파란색에 덜 민감한 경향이 있다. 그래서 좀 더 물리적으로 정확한 결과를 얻기위해서 우리는 가중치 channels를 사용할 필요가 있을 것이다:


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

단순 나누기로 구현한 grey scale

가중치채널을 사용한 그레이 스케일
너는 아마도 그 차이점을 당장은 눈치채지 못할 것이지만, 좀 더 복잡한 신들에서, 그러한 가중치 grayscaling은 좀 더 현실적인 경향이 있다. (위 아래 사진을 보면은 그렇게 큰 차이는 아닌 것 같지만 아래가 뭔가 음영이라던가 잘표현되어있고 안정적인 느낌이 난다.)

Kernel effects
단일의 텍스쳐 이미지에 대해 후처리를 하는 것의 또 다른 장점은 우리는 실제로 텍스쳐의 다른 부분들로부터 컬러 값들을 샘플링 할 수 있다는 것이다. 우리는 예를들어 현재 텍스쳐 좌표 주변의 작은 부분을 가져와서, 현재 텍스쳐 값 주변의 다양한 텍스쳐 값들을 샘플링할 수 있다. 우리는 그러고나서 창의적인 방식들로 그것들을 합쳐 흥미로운 효과들을 만들 수 있다.

kernel(or convolution matrix)는 그것의 커널 값으로 주변의 픽셀 값들을 곱하고 하나의 값을 만들기위해 함께 더하는 현재 픽셀을 중심으로 하는 값들의 작은 행렬 같은 배열이다.  그래서 기본적으로 우리는 현재 픽셀의 주변 방향에서 텍스쳐 좌표에 대한 작은 offset(변위)를 추가하고, 커널을 기반으로 그 결과를 합친다. 한 커널의 예시는 아래에서 주어진다:



이 커널은 8개의 주변 픽셀값을 가지고, 그것들을 2로 곱하고, 현재 픽셀에 -15를 곱한다. 이 예시 커널은 기본적으로 커널에서 결정된 한 가중치를 주변 픽셀값에 곱하고, 현재 픽셀에 큰 음수 가중치를 곱하여 결과의 균형을 맞춘다.

  Green Box
  너가 인터넷에서 찾을 대부분의 커널들은 모두 합해서 1이다. 만약 너가 모든 가중치를 함께 더한다면. 만약 그것들이 1이 되지 않는다면, 최종 텍스쳐 컬러가 원래의 텍스쳐 값보다 더 밝거나 또는 더 어둡게 나올 것이라는 것을 의미한다.

커널들은 사용하기 매우 쉽기 때문에, 후처리를 위해 매우 유용한 도구이다. 많은 예제들이 있는 실험이 온라인에서 찾아질 수 있다. 우리는 다소 커널을 지원하기 위해 fragment shader에 적용해야 한다. 우리는 우리가 사용할 각 커널이 3x3 ㅏkernel이라는 가정을 한다. (대부분의 커널들이 그렇다):


const float offset = 1.0 / 300.0;

void main()
{
 vec2 offsets[9] = vec2[]
(
 vec2(-offset, offset), // top-left
 vec2(0.0f, offset), // top-center
 vec2(offset, offset), // top-right
 vec2(-offset, 0.0f), // center-left
 vec2(0.0f, 0.0f), // center-center
 vec2(offset, 0.0f), // center-right
 vec2(-offset, -offset), // bottom-left
 vec2(0.0f, -offset), // bottom-center
 vec2(offset, -offset) // bottom -right
);

 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 offsets 배열을 만든다. 그 offset은 간단히 너가 선호하는대로 커스터마이즈할 수 있는 상수 값이다. 그러고나서 우리는 커널을 정의한다. 이것은 주변의 픽셀값을 흥미로운 방식으로 샘프링하여 각 컬러 값을 날카롭게 하는 sharpen kernel이다. 마지막으로, 우리는 샘플링 할 때 각 offset을 현재 텍스쳐 좌표에 더한다 그리고 이러한 텍스쳐값을 우리가 함께 더하는 가중치 커널 값과 곱한다.

이 특정한 sharpen kernel은 이렇게 보인다:

이것은 너의 플레이어가 narcotic(마약) adventure에 있는 곳의 흥미로운 효과를 만들어 낸다.

Blur
blur effect를 만드는 한 커널은 다음과 같이 정의 된다.



모든 값들이 더해서 16이 되기 때문에, 간단히 결합된 샘플링된 컬러들을 반환하는 것은 매우 밝은 컬러를 만들어 낼 것이다. 그래서 우리는 커널의 각 값을 16으로 나눠야만 한다. 그 결과 커널 배열은 다음과 같아질 것이다:


float kernel[9] = float[]
(
 1.f / 16, 2.f / 16, 1.f / 16,
 2.f / 16, 4.f / 16, 2.f / 16,
 1.f / 16, 2.f / 16, 1.0 / 16
);

fragment shader에 있는 kerner float array를 바꿔서, 우리는 완전히 후처리 효과를 바꿀 것이다. 그것은 이제 이것처럼 보인다:


그러한 블러 효과는 흥미로운 가능성을 만들어 낸다. 우리는 누군가 취한 효과를 만들어 내기 위해 예시로 시간에 따라 블러의 양을 다양하게 할 수 있고, 메인 캐리겉가 안경을 쓰고 있지 않을 때 마다, 그 블러를 증가시킬 수 있다. 블러링은 또한 우리에게 나중에 튜토리얼에서 사용할 컬러 값을 smooth시키는데 유용한 도구를 준다.

너는 우리가 그러한 작은 커널 구현을 갖는다면, 멋진 후처리 효과를 만드는 것은 쉽ㄷ바는 것을 볼 수 있다. 이 주제를 마무리하기 위해, 마지막 인기있는 효과를 보여주도록 하자.

Edge detection
아래에서 너는 sharpen kernel과 유사한 edge-detection kernel를 찾을 수 있다:



이 커널은 모든 엣지를 강조하고, 나머지를 어둡게 한다. 이것은 우리가 한 이미지에서 edges만을 신경쓸 때에만 꽤 유용하다.


이것 같은 커널이 Photoshop같은 툴에서 이미지-조작 도구/필터로서 사용된다는 것은 아마도 놀랍게 다가오지 않는다. 매우 좋은 병렬 능력을 가지고 fragments를 처리하는 그래픽 카드의 능력 때문에, 우리는 상대적으로 쉽게 실시간으로 픽셀마다의 basis에 있는 이미지를 조작할 수 있다. image-editing tools은 그러므로 image-processing에 대해 좀 더 자주 그래픽카드를 사용하는 경향이 있다.

댓글 없음:

댓글 쓰기