눈팅하는 게임개발자 블로그
OpenGL Advanced 5-5 Framebuffers 본문
원문 사이트
learnopengl.com/Advanced-OpenGL/Framebuffers
LearnOpenGL - Framebuffers
Framebuffers Advanced-OpenGL/Framebuffers So far we've used several types of screen buffers: a color buffer for writing color values, a depth buffer to write and test depth information, and finally a stencil buffer that allows us to discard certain fragmen
learnopengl.com
번역 사이트
heinleinsgame.tistory.com/28?category=757483
[Learn OpenGL 번역] 5-5. 고급 OpenGL - Framebuffers
Framebuffers 고급 OpenGL/Framebuffers 지금까지 우리는 여러가지 유형의 스크린 버퍼들을 사용해왔습니다. 컬러 값들을 작성하는 color buffer, 깊이 정보를 작성하기 위한 depth buffer, 마지막으로 특정한 조
heinleinsgame.tistory.com
Framebuffers
지금까지 여러가지 유형의 스크린 버퍼들을 사용해왔다.
컬러 값들을 작성하는 color buffer, 깊이 정보를 작성하기 위한 depth buffer, 마지막으로 특정 조건에 의해
해당 fragment들을 폐기하는 stencil buffer가 있었다.
이러한 버퍼들을 결합한 것을 framebuffer라고 부르고 이는 메모리의 어딘가에 저장된다.
OpenGL은 framebuffer들을 자유롭게 정의할 수 있도록 해준다.
따라서 직접 컬러를 정의하고 추가적으로 depth, stencil buffer도 정의할 수 있다.
지금까지 수행했던 렌더링 작업들은 모두 기본 framebuffer에 있는 렌더 buffer의 위에서 동작되었다.
기본 framebuffer는 GLFW를 통해 윈도우 창을 생성할 때 자동으로 생성된다.
커스텀 framebuffer를 생성하면 렌더링하는 데에 추가적인 기능들을 사용할 수 있다.
Framebuffer에 대한 개념이 잘 이해되지 않겠지만 이를 잘 활용하면
scene을 여러가지 framebuffer로 렌더링하여 거울을 생성하거나 여러 멋진 전처리 효과들을 생성할 수 있다.
먼저 그들이 실제로 어떻게 동작하는지 다루고 그 후에 이런 멋진 전처리 효과들을 구현해봄으로써
framebuffer를 사용할 것이다.
Creating a 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_FRAMEBUFFER 또는 GL_DRAW_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_INCOMPLETE와 같은 값들을 리턴하게 된다.
GL_FRAMEBUFFER_COMPLETE가 리턴되었다면 문제없이 계속할 수 있다.
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
// 승리의 춤을 실행
이후의 모든 렌더링 작업들은 이제 현재 바인딩된 framebuffer에 첨부된 것들에 렌더링하게 된다.
해당 framebuffer는 기본 framebuffer가 아니기 때문에 렌더링 명령들이 윈도우창의 출력에 아무런 영향을 주지 않는다.
이러한 이유에서 다른 framebuffer에 렌더링하는 것을 off-screen 렌더링이라고 부른다.
모든 렌더링 작업들을 메인 윈도우 창에 나타내기 위해 0을 바인딩하여 다시 기본 framebuffer를 활성화시켜야 한다.
glBindFramebuffer(GL_FRAMEBUFFER, 0);
모든 framebuffer 작업을 완료하면 객체를 제거하는 것 또한 잊지 말아야 한다.
glDeleteFramebuffers(1, &fbo);
완전히 생성되었는지를 확인하기 전에 하나 이상의 것들을 framebuffer에 첨부해야 한다.
첨부물들은 framebuffer에서 buffer처럼 행동하는 메모리 위치이다.
첨부물을 생성할 때 두 개의 옵션을 선택할 수 있는데,
이는 텍스처 또는 renderbuffer 객체이다.
Texture Attachment
텍스처를 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나 mipmapping을 신경쓰지 않아도 된다. 대부분의 경우에 필요가 없다.
해당 전체 화면을 작거나 큰 크기의 텍스처에 렌더링하고 싶다면 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 objcet attachment
Renderbuffer 객체는 텍스처 다음에 나온 framebuffer 첨부물의 유형으로써
텍스처 이미지처럼, renderbuffer 객체는 실질적인 버퍼로서 바이트, 정수형 값, 픽셀 값 등의 것들의 배열이다.
하지만 renderbuffer 객체는 직접적으로 읽어올 수는 없다.
이는 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해야 한다면 텍스처 첨부물을 사용해야 한다.
Rendering to a Texture
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를 사용하는 것이 더 좋은 선택이다.
Renderbuffer 객체를 생성하는 것은 어렵지 않다.
기억해야할 것은 이를 depth 그리고 stencil 첨부물 renderbuffer 객체를 생성한다는 것이다.
internal format을 GL_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을 활성화해야 한다.
모든 것이 성공적으로 수행되었다면 결과는 다음과 같다.
그래서 이 방법이 왜 유용한 것인가?
지금은 하나의 텍스처 이미지로서 완전히 렌더링된 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 효과이지만 펑키한 결과를 만든다.
fragment shader의 단 한줄에 의해서 모든 씬이 반전된 컬러를 갖게 되었다.
Grayscale
또 다른 흥미로운 효과는 scene의 모든 컬러에서 흰색, 회색, 검정색을 제외한 모든 색을 제거하여 전체 이미지를
grayscale하는 것이다. 이를 수행하는 쉬운 방법은 모든 컬러 컴포넌트를 얻어서 평균을 내는 것이다.
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);
}
이 차이를 알아보기는 쉽지 않지만 더 복잡한 scene에서는 이러한 weighted grayscaling 효과가 더욱 현실적인 효과를 낸다.
Kernel effects
하나의 텍스처 이미지에 post-processing 하는 것에 대한 또 다른 이점은 텍스처의 다른 부분으로부터 실제로 컬러 값을
샘플할 수 있다는 점이다.
예를 들어 현재 텍스처 좌표 주변의 작은 영역을 가져올 수 있고 여러 텍스처 값들을 가져올 수도 있다.
그런 다음 이를 창의적인 방법으로 결합하여 흥미로운 효과를 낼 수 있다.
Kernel(혹은 나선형 행렬)은 주변 픽셀 값에 커널 값을 곱한 후 현재 값을 모두 더하여 하나의 값을 형성하는 현재 픽셀을
중심으로 하는 값의 작은 행렬이다. 그래서 기본적으로 현재 픽셀의 주변 방향으로 텍스처 좌표의 작은 offset을
추가하고 kernel을 기반으로 결과를 결합한다.
kernel의 예는 다음과 같다.
[2222-152222]
이 kernel은 8개의 둘러싸인 픽셀 값들을 취하고 이들을 2로 곱한다.
그리고 현재 픽셀에 -15를 곱한다.
이 예시에서 kernel은 기본적으로 주변의 픽셀들을 결정된 weight로 곱하고 현재 픽셀을 큰 음수 weight로 곱함으로써
밸런스를 맞춰준다.
온라인 상에서 찾을 수 있는 대부분의 kernel들은 모든 수들을 서로 합하면 결과가 1이 나온다. 그들의 합산이 1이 나오지 않는다면 이는 결과 텍스처 컬러가 원래의 텍스처 값보다 밝아지던지 어두워지던지 하는 것이다. |
Kernel들은 post-processing에 대해 아주 유용한 도구이다.
사용하거나 실험하기 쉽고 많은 예제들을 온라인에서 찾아볼 수 있기 때문이다.
kernel을 지원하기 위해 fragment shader를 약간 수정해야 한다.
사용할 각 kernel은 3 x 3 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 값들과 곱하여 합산한다.
결과는 다음과 같다.
이것은 플레이어가 약에 취해있는 것처럼 보이는 흥미로운 효과를 낸다.
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 효과를 생성했다.
결과는 다음과 같다.
이러한 blur 효과는 흥미로운 가능성을 만든다.
예를 들어 술에 취한 효과를 내기 위해 시간이 지남에 따라 blur 정도를 바꿀 수 있다.
또는 메인 캐릭터가 안경을 쓰지 않았을 경우 blur 효과를 높일 수 있다.
Blur는 나중에 강좌에서 사용할 컬러 값을 부드럽게 하는 데에 유용한 도구가 될 수도 있다.
이런 작은 kernel을 구현함으로써 손쉽게 멋진 post-processing 효과를 낼 수 있었따.
Edge detection
아래에서 edge-detection kernel을 확인할 수 있다.
sharpen kernel과 매우 유사하다.
[1111-81111]
이 kernel은 모든 모서리를 하이라이트하고 나머지들은 어둡게 만든다.
이미지의 모서리를 신경써야 할 때 유용하게 사용할 수 있다.
이러한 kernel들이 Photoshop과 같은 도구에서 이미지 조작 도구/필터로 사용된다는 것은 놀랄 일이 아니다.
병렬 기능으로 fragment들을 처리하는 그래픽 카드들의 능력 때문에 실시간으로 픽셀마다 이미지를 조작할 수 있다.
그러므로 이미지 편집 도구들은 이미지 처리에 대해서 그래픽 카드를 더 자주 사용하는 경향이 있다.
'공부한거 > OpenGL' 카테고리의 다른 글
OpenGL Advanced 5-8 Advanced GLSL (0) | 2020.12.11 |
---|---|
OpenGL Advanced 5-7 Advanced Data (0) | 2020.12.10 |
OpenGL Advanced 5-4 Face culling (0) | 2020.12.10 |
OpenGL Advanced 5-3 Blending (0) | 2020.12.09 |
OpenGL Advanced 5-2 Stencil testing (0) | 2020.12.08 |