Framebuffers
- 지금까지 여러 스크린 버퍼들을 사용해왔다.
color buffer
: 컬러 값 작성depth buffer
: 깊이 정보 작성stencil buffer
: 특정 조건에 의해 해당 fragment를 폐기함.
이러한 버퍼들을 결합한 것을
Framebuffer
라고 한다.지금까지 우리가 수행했던 렌더링 작업들은 모두 기본 framebuffer에 있는 렌더 buffer의 위에 동작되었다.
- 기본 framebuffer는 윈도우 창을 생성할 때 생성된다.(glfw가 자동으로 생성)
- 자신만의 framebuffer를 생성하면, 렌더링하는 데에 추가적인 기능들을 얻을 수 있다.
- scene을 여러가지 framebuffer로 렌더링하면, 거울을 생성할 수 있고, 멋진 전처리 효과들을 생성할 수 있다.
Creating a framebuffer
glGenFramebuffers
: 새로운 framebuffer 객체를 생성한다.(FBO)
1
2
unsigned int fbo;
glGenFramebuffers(1, &fbo);
- 먼저 framebuffer 객체를 생성하고 바인딩하여 framebuffer를 활성화 시킨다.
- 그 후에 조작을 하고, framebuffer를 언바인딩한다.
- framebuffer를 바인딩하기위해,
glBindFramebuffer
를 사용한다.
1
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
GL_FRAMEBUFFER
타겟에 바인딩함으로써 이후에 나오는 모든 framebuffer 읽기, 쓰기 명령이 현재 바인딩된 framebuffer에 영향을 미친다.framebuffer를
GL_READ_FRAMEBUFFER
,GL_DRAW_FRAMEBUFFER
타겟에 바인딩하여 읽기, 쓰기 명령을 구분할 수도 있다.GL_READ_FRAMEBUFFER
에 바인딩된 framebuffer는glReadPixel
과 같은 모든 읽기 명령에 사용된다.GL_DRAW_FRAMEBUFFER
에 바인딩된 framebuffer는 렌더링, 비우기, 다른 쓰기 연산에 대한 목적지로서 사용된다.- 대부분의 경우
GL_FRAMEBUFFER
에 바인딩하여 읽고 쓴다.
framebuffer 요구사항
완전하게 만들기 위한 요구사항
- 최소한 하나의 buffer(color, depth 혹은 stencil buffer)를 첨부(attach)해야 합니다.
- 최소한 하나의 color attachment가 존재해야 합니다.
- 모든 attachment buffer들은 완전해야 합니다(메모리가 할당).
- 각 buffer들은 샘플의 갯수가 같아야 합니다.
샘플은 Anti Aliasing에서 다룬다.
요구사항에 따르면 framebuffer에 첨부할 것들을 생성하고 첨부해야한다.
glCheckFramebufferStatus
함수에 GL_FRAMEBUFFER 를 인자로 넘겨주어 호출하여 준비가 다 되었는지 확인할 수 있다.GL_FRAMEBUFFER_COMPLETE
를 반환하면 성공하고, 그렇지 않으면 실패.
1 2
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) // execute victory dance
이후의 모든 렌더링 작업들은 이제 현재 바인딩된 framebuffer에 첨부된 것들에 렌더링하게 된다.
- 이 버퍼가 기본 framebuffer가 아니기 때문에, 렌더링 명령들이 윈도우 창의 출력에 아무런 영향을 주지않는다.
- 즉,
off-screen rendering
이라고 부른다. - 모든 렌더링 작업들을 메인 윈도우 창에 나타내기 위해
0
을 바인딩하여 다시 기본frambuffer
를 활성화 시켜야한다.
1
glBindFramebuffer(GL_FRAMEBUFFER, 0);
- 모든
framebuffer
작업을 완료하면framebuffer
객체를 제거해야한다.
1
glDeleteFramebuffers(1, &fbo);
완전히 생성되었는지 확인하기전에 하나 이상의 첨부물을 framebuffer에 첨부해야한다.
- 첨부물(attachment)들은 framebuffer에서 buffer처럼 행동하는 메모리 위치(memory location)이다.
- 첨부물을 생성할 때, 두 개의 옵션을 가지고 있다.
- 텍스처 or
render buffer objects
Texture attachments
텍스처를 프레임 버퍼에 첨부할 때, 마치 모든 렌더링 명령이 일반 color/depth/stencil buffer인 것처럼 텍스처에 쓴다.
텍스처를 사용하면, 모든 렌더링 작업의 결과가 셰이더에서 쉽게 사용할 수 있는 텍스처 이미지로 저장된다는 장점이 있다.
framebuffer의 텍스처를 생성하는 것은 일반적인 텍스처와 같다.
1
2
3
4
5
6
7
8
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);
차이점은 두가지,
- 화면의 크기와 동일한 크기(dimensions)로 설정한다.
- 텍스처의 데이터 매개변수로
NULL
을 전달한다.
- 오직 메모리만 할당하고 실제로 채워넣진 않는다.
- 텍스처를 채우는 것은
framebuffer
에 렌더링할 때 수행 될 것이다.- 대부분의 경우, wrapping methods 이나 mipmapping 를 필요로 하지 않으므로 신경쓰지 않는다.
전체화면을 더 작거나 큰 크기의 텍스처에 렌더링하고 싶다면,
glViewport
함수를 다시 호출하여(framebuffer에 렌더링하기 전에) 텍스처의 새로운 크기의 인자로 넘겨주어야한다. (그러지 않으면 화면의 일부분만 텍스처에 그려짐)
- 이제 이 텍스처를 첨부해야한다.
1
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glFramebufferTexture2D
- target: 텍스처를 첨부할 타겟인 framebuffer(draw, read or both)
- attachment: 첨부될 첨부물의 타입, 여기서는 color, 마지막에 붙은 0은 하나 이상의 color 첨부물을 첨부 할 수 있다는 것을 암시한다.
- textarget: 첨부하기 원하는 텍스처 유형
- texture: 첨부할 실제 텍스처
- level: Mipmap 레벨, 0으로 유지할것임.
color 말고 depth, stencil 텍스처를 첨부할 수 있다.
- depth type: GL_DEPTH_ATTACHMENT
- depth format, internalformat: GL_DEPTH_COMPONENT
- stencil type: GL_STENCIL_ATTACHMENT
- stencil format: GL_STENCIL_INDEX
depth, stencil 버퍼를 하나의 텍스처로 만들어 첨부 가능
- 텍스처의 각 32비트 값은 24비트의 depth 정보, 8비트의 stencil 정보로 이루어짐
GL_DEPTH_STENCIL_ATTACHMENT
를 사용하고, 텍스처 형식을 depth 와 stencil 값을 결합한 것으로 설정한다.
1 2 3 4 5 6
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
객체는 텍스처 다음에 새로나온 framebuffer 첨부물의 유형이다.- 그래서 텍스처는 옛날에만 쓰였던 첨부물
- 텍스처 이미지와 마찬가지로 이는 실제 buffer다
- ex) 바이트, 정수형, 픽셀 등의 배열
- 이 객체는 읽기 불가능한 대신, framebuffer에대한
off-screen
렌더링에서 텍스처보다 더 최적화되어 있다.
Renderbuffer
객체는 모든 렌더링 데이터들을 그들의 buffer에 텍스처 형식으로 변환하지 않고 직접적으로 저장한다.- 따라서 좀 더 빠른 저장공간을 형성
- 하지만, renderbuffer 객체는 일반적으로 쓰기만 가능하다.
- 그들로부터 데이터를 읽을 수 없다.(그러므로 메모리 최적화를 수행할 수 있음)
- 느리지만,
glReadPixels
함수를 통해 현재 바인딩된 framebuffer로부터 픽셀의 특정 영역을 읽을 수 있긴하다. (첨부물 자체에서 직접적으로 읽어오는것은 아님)
데이터가 기본(native) 포맷이기 때문에 데이터를 쓰거나 다른 버퍼에 데이터를 복사할 때 매우 빠르다.
- 따라서 버퍼 전환과같은 작업들은 이 객체를 사용할 때 빠르다.
glfwSwapBuffers
함수 또한 renderBuffer 객체로 구현되어있다.- 단순히 렌더 버퍼 이미지에 쓰고, 마지막에 다른 이미지로 교체한다.
생성은 다음과 같이 한다.
1
2
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
- 그리고 비슷하게 바인딩한다. 이후 모든 renderbuffer 작업들은 현재 rbo에 영향을 미친다.
1
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
Renderbuffer 객체가 일반적으로 쓰기만 가능하기 때문에 종종 depth, stencil 첨부몰로 사용된다.(대부분 값을 읽을 필요가 없음)
- 하지만, test시에는 값을 읽어야함.
- 그럼에도 불구하고, 이 값을 샘플링할 필요가 없으므로, 렌더 버퍼 객체를 사용하는것은 적합함.
- 샘플링하지 않을 때는 렌더링 버퍼 객체가 일반적으로 더 최적하므로 더 선호됨.
Depth, Stencil renderbuffer객체 생성은
glRenderbufferStorage
함수를 호출하여 수행된다.GL_DEPTH24_STENCIL8
을 설정하여 24/8 비트로 나눔
1
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
- 첨부는 다음과 같이 한다.
1
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
- Renderbuffer 객체는 framebuffer 프로젝트에 최적화를 제공해준다.
- 주의: 샘플링할 필요가 없다면 그 특정 버퍼에 render buffer를 사용하는것이 현명하다. 하지만, buffer로부터 color 값이나 깊이 값 처럼 데이터를 sample 해야 한다면, 텍스처를 사용해야한다.
Rendering to a texture
생성한 framebuffer 객체에 첨부된 color 텍스처에 scene 을 렌더링할 것이다.
- 그런 다음 이 텍스처를 화면을 가득채운 간단한 사각형에 그릴 것이다.
- 시각적 출력은 앞에서와 동일하다
- 하지만 모든것이 하나의 사각형 위에 그려진다.
먼저 framebuffer 객체를 생성하고 바인딩하자.
1
2
3
unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
- 그 다음, framebuffer에 color 첨부물로 텍스처 이미지를 첨부할것이다.
- 데이터 지정 x, 윈도우 창과 크기 같음
1
2
3
4
5
6
7
8
9
10
11
// generate texture
unsigned int textureColorbuffer;
glGenTextures(1, &textureColorbuffer);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
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);
// attach it to currently bound framebuffer object
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureColorbuffer, 0);
- 또한 OpenGL이 depth testing(또는 stencil testing)을 할 수 있도록 depth와 stencil을 추가할 것이다.
- 이는 renderbuffer로 생성할 것이다.
1
2
3
4
5
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
이 renderbuffer 객체에 충분한 메모리가 할당되면, renderbuffer를 언바운딩하자
그 후, 이 객체를 첨부하자
1
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
- 그런 다음, 완성됬는지 확인하자
1
2
3
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에 영향을 미친다.
- 모든 depth, stencil 작업들은 현재 바인딩된 framebuffer의 depth, stencil 첨부물로부터 값을 읽는다.(이용가능할 경우)
- 만약 depth 버퍼를 첨부하지 않았다면, depth testing 작업들은 동작하지 않는다.
하나의 텍스처에 scene을 그리기 위해 다음과 같은 단계를 거친다.
- 활성화된 framebuffer로서 바인딩된 새로운 framebuffer에 평상시대로 scene을 렌더링한다..
- 기본 framebuffer를 바인딩한다.
- 새 프레임 버퍼의 색상 버퍼를 텍스처로 사용하여 전체 화면에 걸쳐있는 사각형(quad)을 그린다.
depth testing 강좌에서 사용했던 것과 동일한 scene을 그릴 것이다.
- 이번에는 이 컨테이너 텍스처를 사용한다.
사각형을 그리기 위해 간단한 shader 세트를 작성할 것이다.
- 행렬 변환을 포함시키지 않을 것이며, vertex 좌표만을 제공할 것이다.
- vertex shader는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11
#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 함)
1 2 3 4 5 6 7 8 9 10 11
#version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D screenTexture; void main() { FragColor = texture(screenTexture, TexCoords); }
- 그런 다음 화면 쿼드에 대한 VAO를 만들고 구성하는 것은 사용자 몫이다.
- 프레임 버퍼 과정의 단일 렌더링은 다음과 같은 구조이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// first pass
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // we're not using the stencil buffer now
glEnable(GL_DEPTH_TEST);
DrawScene();
// second pass
glBindFramebuffer(GL_FRAMEBUFFER, 0); // back to default
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들은 그들만의 버퍼를 가지고 있으므로, glClear로 각 버퍼를 비워주어야한다.
- 사각형을 그릴때 간단한 사각형을 그리므로 depth testing 을 신경쓰지 않아도 된다.(비활성화, 그리고 일반적인 scene을 그릴때 다시 활성화)
결과는 다음과 같다.
- 왼쪽은 동일한 결과.(하지만 이는 그저 사각형(quad))
- 이 scene을 wireframe으로 렌더링 한다면, 오른쪽 처럼 하나의 사각형만 그려진다.
- 이는 하나의 텍스처 이미지로서 완전히 렌더링 된 scene을 자유롭게 접근할 수 있기 때문에 fragment shader에서 흥미로운 효과들을 생성할 수 있다.
- 이런 효과를
post-processing(전처리)
효과라고 부른다.
- 이런 효과를
Post-processing
- scene texture를 조작해보자.
Inversion(반전)
Fragment shader에서 렌더링 출력의 각 컬러들에 접근하여 컬러를 반전해보자
단순히 컬러에 1을 빼서 이 효과를 얻을 수 있다.
1
2
3
4
void main()
{
FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}
- frag shader 한줄에 의해 모두 반전된 컬러를 가지고 있다.
Grayscale
- 단순히 색을 평균내어 이 효과를 얻을 수 있다.
1
2
3
4
5
6
void main()
{
FragColor = texture(screenTexture, TexCoords);
float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
FragColor = vec4(average, average, average, 1.0);
}
- 정확한 그레이 스케일은 아래와 같이 가중치를 둔다.
- 사람의 눈은 녹색에 예민, 파란색에 덜 예민
1
2
3
4
5
6
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);
}
Kernel effects
텍스처 이미지로 scene을 그리는 것의 또 다른 장점
- 해당 fragment에 국한되지 않은 텍스처의 다른 부분에서 색상 값을 샘플링 할 수 있다는 것이다.
Kernel(convolution matrix)은 주변 픽셀 값에 커널 값을 곱한 후, 현재 값을 모두 더하여, 하나의 값을 형성하는 현재 픽셀을 중심으로하는 작은 행렬이다.
현재 픽셀의 주변 방향의 텍스처 좌표에 작은 오프셋을 추가하고 커널을 기반으로 결과를 결합한다.
커널은 아래와 같은 행렬이다.
이 커널은 8개의 둘러싸인 픽셀 값들을 취하고, 이들을
2
와 곱한다. 그리고 현재 픽셀에-15
를 곱한다.- 이 예제 커널은 주변 픽셀에 커널에서 결정된 여러 가중치를 곱하고 현재 픽셀에 큰 음수 가중치를 곱하여 결과의 균형을 맞춥니다.
대부분 커널은 합이 1로 되게한다. 그렇지 않으면 결과 텍스처가 원래보다 밝거나 어둡게된다.
이 커널은 post-processing에 대해 아주 유용한 도구이다.
- kernel을 지원하기 위해서는 fragment를 약간 수정해야한다.
우리가 사용할 각 kernel은 3x3 kernel이라고가정한다.(대부분 그럼)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
}
- 먼저 각 텍스처 좌표에 대한 9 개의
vec2
offset 배열을 생성한다.- 이 offset은 간단히, 원하는대로 정할 수 있는 상수 값이다.
그런다음 커널을 정의한다.
- 이런 패턴의 커널을
sharpen kernel
이라고 한다.
- 이런 패턴의 커널을
- 효과는 다음과 같다
Blur
- blur 효과를 내는 커널은 아래와 같다.
모든 값의 합이 16이므로, 결과는 매우 밝을을 것이므로, 16으로 나눠야한다.
커널 배열을 다음과 같이 설정하자
1
2
3
4
5
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
);
- 이제 아래와 같이 보인다.
이런 효과는 주인공이 안경을 쓰지 않을 때 혹은 술취했을 때 연출할 때 쓰기 좋다.
또한 블러는 색을 부드럽게하는데 유용한 도구가 될 수 있다.
이러한 작은 kernel을 구현함으로써 손쉽게 post-processing 효과를 낼 수 있다.
Edge detection
커널은 다음과 같다. \(\begin{bmatrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \end{bmatrix}\)
이 커널은 모든 모서리를 강조하고, 나머지를 어둡게한다. 우리가 이미지의 모서리를 신경써야 할 때 유용하게 사용할 수 있다.
- 이와 같은 커널들이 포토샵같은 툴에서 이미지 조작 툴/필터로 사용된다.
- 병렬 기능으로 fragment들을 쉽게 처리할 수 있는 그래픽카드 덕분에, 실시간으로 픽셀마다 이미지를 조작할 수 있다.
- 그러므로 이미지 편집 도구들은 이미지 처리에 대해 그래픽 카드를 더 자주 사용하는 경향이 있다.