블로그 이미지
대갈장군

calendar

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

Notice

2010. 3. 11. 03:47 OpenGL
FBO는 OpenGL에서 사용하는 '외부에 그리기' 기술중 하나로써 나도 종종 사용한다. FBO를 사용함으로써 사용자는 다양한 작업이 가능해 진다. 

대표적인 FBO 사용예로 그려질 scene 내부에 또 다른 scene을 그려야 할 때다. 집안에 있는 TV를 표현할 때 종종 이 방법이 사용된다. FBO의 장점으로는 간단한 셋업과 사용 그리고 context switching이 필요없다는 점 (오버헤드 적음) 각종 버퍼 (depth buffer, stencil buffer, accumulation buffer) 등을 사용할 수 있어 유용하다는 점 등이다. 


1. 셋업

우선 다른 OpenGL 객체와 마찬가지로 FBO 핸들을 받을 변수를 선언해야 하고 그 변수에 핸들을 생성해서 대입해야 한다. glGenFramebuffersEXT() 함수의 첫번째 인자는 glGenTextures() 함수의 첫번째 인자처럼 몇개나 만들건지 지정해주는 인자다.
GLuint fbo;
glGenFramebuffersEXT(1, &fbo);
명령을 수행하려면 바인딩 부터 해야 한다. 바인딩을 함으로써 다음에 이어지는 명령들이 바인딩한 객체에 대해 수행되도록 한다.
glBindFramebufferEXT
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
첫번째 인자가 Target 인자 인데 GL_FRAMEBUFFER_EXT는 고정된 것이라고 보면 된다. 물론 나중에 확장이 되면 다른 형태가 추가 될수 있지만 현재로서는 저 인자가 고정이라고 보면 된다. 두번째 인자로 앞서 정의한 핸들을 넘겨주면 오케이.

2. 깊이 버퍼 (Depth buffer) 추가

FBO 객체 자체로는 할 수 있는게 없다. 왜냐면 걍 객체(식별표)이니까... 이제 이 식별표에 실제적인 저장 공간(버퍼)들을 추가해 줘야 한다. 알다시피 OpenGL이 기본적으로 제공하는 Frame buffer도 깊이 버퍼, 스텐실 버퍼 등등으로 구성되어 있다. 고로 FBO 객체에도 Renderable (렌더링 가능한) 버퍼를 추가 시켜 주어야 한다. 이 버퍼로 사용가능한 것이 텍스쳐 또는 렌더버퍼 중 하나다. 

렌더버퍼는 FBO의 offscreen rendering을 지원하기 위한 객체이다. 적절한 텍스쳐 포멧을 지원하지 않는 스텐실 버퍼나 깊이 버퍼를 표현하기 위해서 사용하는 것이 바로 렌더버퍼 객체다. 이 예제에서는 렌더버퍼를 FBO의 깊이 버퍼로 사용할 것이다. 

그러니까 요약하자면, FBO는 OpenGL이 기본적으로 가지고 있는 깊이 버퍼, 스텐실 버퍼, 색상 버퍼등을 '연결'할 수 있는 객체인데 이때 이 연결 가능한 놈들은 두 종류로써 하나는 일반 텍스쳐이고 다른 하나는 '렌더버퍼(Renderbuffer)'이다. 일반 텍스쳐의 경우 색상 버퍼로 사용할 수 있으나 깊이 버퍼나 스텐실 버퍼와 같이 텍스쳐로 표현할 수 없는 버퍼들은 렌더버퍼를 이용해서 FBO에 연결한다는 말인듯 하다. 

아무튼, 일단 렌더버퍼를 생성하는 방법은 다음과 같다.
GLuint depthbuffer;
glGenRenderbuffersEXT(1, &depthbuffer);
depthbuffer라는 변수 (핸들을 받기위한)를 선언하고 generation 함수를 통해 렌더버퍼를 생성해 낸다. (그래서 함수 이름도 glGenRenderbuffersEXT() 이다. 

이제 이렇게 만든 렌더버퍼를 바인딩 시킴으로써 다음에 이어지는 일련의 명령이 depthbuffer 렌더버퍼에 대해 실행되도록 한다.
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depthbuffer);
앞서 본 것과 마찬가지로 GL_RENDERBUFFER_EXT는 고정이라고 보면 된다. 그리고 두번째 인자로 앞서 만든 렌더버퍼의 핸들을 넘겨준다. 

자, 이제 해야 할 일은 이 깊이 버퍼의 크기를 지정해 주는 일이다. (폭과 높이 지정하기)
glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, 
                         width, height);
이 과정은 일종의 메모리 할당이라고 보면 되겠네. 한가지 알아둘 것은 이 렌더버퍼는 기본 RGB/RGBA 저장용으로 사용가능하며 스텐실 버퍼로써 연결도 가능하단다. RGB/RGBA 저장 공간으로 사용될 수 있다는 점은 조금 신선하긴 하다만 텍스쳐를 사용하는 것이 더 유연할 것이라 생각된다.

다음은 이렇게 실제적으로 메모리를 할당한 깊이 렌더버퍼를 우리가 앞서 만든 fbo 객체에 가져다 붙이는 것이다. 
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, 
                             GL_RENDERBUFFER_EXT, depthbuffer);
위 명령이 좀 복잡해 보여도 실은 단순하다. 현재 바인딩 되어 있는 FBO 객체 (fbo)에 렌더버퍼 (depthbuffer)를 바인딩 하라.. 라는 말이다.

3. 렌더링 텍스쳐 추가하기

앞서 두 단계를 수행했지만 아직까지 FBO에 색상 버퍼를 추가하지 않았다. 이제 색상 버퍼를 추가 해야 하는데 이 색상 버퍼가 사실상 '내 눈에 보여질' 놈이다. 

이 색상 버퍼를 추가하는 방법은 두가지가 있다.
1. 색상 렌더버퍼를 FBO의 색상버퍼로 연결
2. 일반 텍스쳐를 FBO의 색상버퍼로 연결

첫번째 방법도 사용하기는 하지만 우리는 두번째 방법을 사용할 것이다. (앞서 2번에서 설명했듯이 렌더버퍼는 기본 RGB/RGBA를 저장하기 위한 버퍼로도 사용가능하다고 했다. 하지만 분명 일반 텍스쳐 보다는 범용성이 떨어질 듯하다.)

텍스쳐를 FBO에 연결하기 위해서는 당연히 텍스쳐 생성부터 해야 한다.
GLuint img;
glGenTextures(1, &img);
glBindTexture(GL_TEXTURE_2D, img);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8,  width, height, 0, GL_RGBA, 
             GL_UNSIGNED_BYTE, NULL);
이 과정은 일반적인 텍스쳐 생성 과정이다. 이때 주의 해야 할 한 가지 포인트는 폭과 넓이가 앞서 만든 깊이 렌더버퍼의 크기와 같아야 한다는 매우 상식적인 것이다. 

자, 이제 색상 버퍼로 사용될 텍스쳐를 만들었으므로 우리가 앞서 만든 FBO 객체에 연결하자.
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, 
                          GL_TEXTURE_2D, img, 0);
이 명령이 어렵게 보여도 실제로는 그닥 어렵지 않다. 우선 굵은 글자로 표시된 GL_COLOR_ATTACHMENT0_EXT가 좀 어렵게 보일지 모르나 이것은 OpenGL에게 어떤 color attachment point에 연결할 지를 명시해주는 인자일 뿐이다. FBO는 동시에 여러개의 색상 버퍼를 가질 수 있기 때문에 어떤 포인트에 연결할 지를 명시해야 한다. 하나의 색상 버퍼만 사용한다면 저 숫자 0은 당신에게 아무런 중요성이 없는 단순한 표식일 뿐이다. 그리고 GL_TEXTURE_2D는 우리가 2D 텍스쳐를 사용한다는 말이고 img는 사용할 텍스쳐의 핸들, 그리고 마지막 0은 mipmap level 이다. 

이제 FBO 완성을 위해 마지막으로 해야 할 일은 FBO가 잘 만들어 졌는지 체크하는 일이다. 다음 함수로 한방에 체크하자.
GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
모든 셋팅이 제대로 통과하였다면 GL_FRAMEBUFFER_COMPLETE_EXT를 리턴할 것이고 만약 에러가 있다면 다른 값을 리턴한다. 

4. 텍스쳐로 렌더링하기

이제 색상버퍼로 연결시켜 놓은 텍스쳐 (img)로 렌더링을 수행하는 방법을 알아볼 차례. 이건 상당히 쉬운데 걍 glBindFramebufferEXT() 함수만 호출하면 땡이다. 셋업이 조금 어려워서 그렇지 사용 자체는 굉장히 쉽다.

FBO로 렌더링 하기 위해서는 glBindFrameBufferEXT()를 호출하면 되고 렌더링을 멈추기 위해서는 같은 함수를 호출하되 인자값을 0으로 주면 된다.
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo); // FBO 바인딩
glPushAttrib(GL_VIEWPORT_BIT);                      
glViewport(0,0,width, height);                 // 여기서 부터는 텍스쳐에 렌더링한다.
// Render as normal here // output goes to the FBO and it’s attached buffers glPopAttrib(); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); // 언바인딩. FBO에 렌더링하기 중지
자, FBO를 바인딩함으로써 차후 수행되는 모든 그리기 명령은 우리가 설정한 FBO의 색상 버퍼, 깊이 버퍼, 스텐실 버퍼에 적용된다. 그리고 glPushAttrib() 와 glPopAttrib() 함수를 이용해 이전의 3차원 매트릭스 값들을 신속히 복구 및 적용할 수 있게 했다. 

중요한 것은, 다시한번 강조하지만, glBindFramebufferEXT() 함수 하나로 간단하게 FBO로 렌더링을 할 수 있다는 점이다. 앞서 셋업에서 수행했던 몇몇 버퍼 바인딩 및 연결과정은 더이상 필요없다. 

5. 렌더링 된 FBO의 색상 버퍼를 텍스쳐로 사용하기

이제 우리의 궁극적인 목표에 가까워 졌다. 왜 우리는 FBO에 렌더링을 수행했던가? 바로 이 단계를 위해서다. FBO 색상 버퍼에 그려진 그림은 곧바로 텍스쳐로 사용 가능하다. 즉, 집안에 있는 TV 속의 화면을 우리는 방금 FBO를 이용해서 그려낸 것이다. 이제 TV 안의 화면은 그렸으니 집을 그릴때 만들어 놓은 TV 속의 화면을 텍스쳐로 입히자.

알다시피 img 핸들에 FBO의 색상 버퍼가 연결되어 있으므로 이 텍스쳐를 사용하기 위해서는 우선 바인딩 부터 한다.
glBindTexture(GL_TEXTURE_2D, img);
그리고 FBO 밉맵 자동 생성 함수인 glGenerateMipmapEXT() 함수를 호출하자.
glGenerateMipmapEXT(GL_TEXTURE_2D);
한가지 주의할 것은 만약 내가 임의의 밉맵 필터 (GL_LINEAR_MIPMAP_LINEAR와 같은)를 사용할 것이라면 FBO에 연결될 색상 버퍼 생성시 반드시 glGenerateMipmapEXT() 함수를 호출해서 가능한지 여부를 체크해야 한다. 

이 경우, 아래의 명령대로 FBO의 색상버퍼를 생성하면 된다.
glGenTextures(1, &img);
glBindTexture(GL_TEXTURE_2D, img);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8,  width, height, 0, GL_RGBA, 
             GL_UNSIGNED_BYTE, NULL);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glGenerateMipmapEXT(GL_TEXTURE_2D);
img를 바인딩 했다면 이제 할 일은 바인딩한 텍스쳐를 원하는 곳에 그려넣기인데 소스 코드를 보면서 설명해 보면...

void display(void)   
{
// FBO 바인딩 - FBO에 렌더링 하겠다고 알려줌
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
// 각종 화면 관련 속성 저장해 놓기 - FBO에 렌더링 끝나면 이전값으로 복귀하기 위해서
glPushAttrib(GL_VIEWPORT_BIT);
glViewport(0,0,width,height);

// 이제 부터 회전하는 작은 큐브를 FBO에 그린다.
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();

glTranslatef(0.0f,0.0f,-2.0f);
glRotatef(xrot,1.0f,0.0f,0.0f);
glRotatef(yrot,0.0f,1.0f,0.0f);

glBegin(GL_QUADS);
// Front Face
glColor4f(0.0f,1.0f,0.0f,1.0f);
glVertex3f(-0.5f, -0.5,  0.5);
glVertex3f( 0.5, -0.5,  0.5);
glVertex3f( 0.5,  0.5,  0.5);
glVertex3f(-0.5,  0.5,  0.5);
// Back Face
glColor4f(1.0f,0.0f,0.0f,1.0f);
glVertex3f(-0.5, -0.5, -0.5);
glVertex3f(-0.5,  0.5, -0.5);
glVertex3f( 0.5,  0.5, -0.5);
glVertex3f( 0.5, -0.5, -0.5);
// Top Face
glColor4f(0.0f,0.0f,1.0f,1.0f);
glVertex3f(-0.5,  0.5, -0.5);
glVertex3f(-0.5,  0.5,  0.5);
glVertex3f( 0.5,  0.5,  0.5);
glVertex3f( 0.5,  0.5, -0.5);
// Bottom Face
glColor4f(0.0f,1.0f,1.0f,1.0f);
glVertex3f(-0.5, -0.5, -0.5);
glVertex3f( 0.5, -0.5, -0.5);
glVertex3f( 0.5, -0.5,  0.5);
glVertex3f(-0.5, -0.5,  0.5);
// Right face
glColor4f(1.0f,1.0f,0.0f,1.0f);
glVertex3f( 0.5, -0.5, -0.5);
glVertex3f( 0.5,  0.5, -0.5);
glVertex3f( 0.5,  0.5,  0.5);
glVertex3f( 0.5, -0.5,  0.5);
// Left Face
glColor4f(1.0f,1.0f,1.0f,1.0f);
glVertex3f(-0.5, -0.5, -0.5);
glVertex3f(-0.5, -0.5,  0.5);
glVertex3f(-0.5,  0.5,  0.5);
glVertex3f(-0.5,  0.5, -0.5);
glEnd();

// 앞서 저장했던 화면 관련 값을 복구하고 FBO 사용 해제
glPopAttrib();
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

        // 이제부터 그리는 것들은 스크린에 보여질 직접적인 것들
glClearColor(0.0f, 0.0f, 0.2f, 0.5f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
// 여기서 FBO에 그려진 회전하는 규브 (색상 버퍼)를 바인딩한다
glBindTexture(GL_TEXTURE_2D, img);
// 여기서는 밉맵 옵션이 사용되지 않지만 만약 당신이 밉맵을 사용한다면 아래의 주석을 제거하고 텍스쳐 생성시에 
// glGenerateMipmapEXT() 함수를 호출하라.
// glGenerateMipmapEXT(GL_TEXTURE_2D);
glEnable(GL_TEXTURE_2D);

glTranslatef(0.0f,0.0f,-2.0f);
glRotatef(-xrot,1.0f,0.0f,0.0f);
glRotatef(-yrot,0.0f,1.0f,0.0f);

glColor4f(1.0f,1.0f,1.0f,1.0f);

// 이제 FBO의 색상 버퍼를 텍스쳐로 사용하는 또다른 규브 그리기
glBegin(GL_QUADS);
// Front Face
glNormal3f( 0.0f, 0.0f, 1.0);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5f, -0.5,  0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.5, -0.5,  0.5);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.5,  0.5,  0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5,  0.5,  0.5);
// Back Face
glNormal3f( 0.0f, 0.0f,-1.0);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.5, -0.5, -0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5,  0.5, -0.5);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 0.5,  0.5, -0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 0.5, -0.5, -0.5);
// Top Face
glNormal3f( 0.0f, 1.0, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5,  0.5, -0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5,  0.5,  0.5);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.5,  0.5,  0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.5,  0.5, -0.5);
// Bottom Face
glNormal3f( 0.0f,-1.0, 0.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5, -0.5, -0.5);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 0.5, -0.5, -0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 0.5, -0.5,  0.5);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.5, -0.5,  0.5);
// Right face
glNormal3f( 1.0, 0.0f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.5, -0.5, -0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.5,  0.5, -0.5);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 0.5,  0.5,  0.5);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 0.5, -0.5,  0.5);
// Left Face
glNormal3f(-1.0, 0.0f, 0.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5, -0.5, -0.5);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.5, -0.5,  0.5);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-0.5,  0.5,  0.5);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5,  0.5, -0.5);
glEnd();

glDisable(GL_TEXTURE_2D);
xrot+=xspeed;
yrot+=yspeed;

glutSwapBuffers ( );
// 버퍼 스왑 (출력하기)
}

위 프로그램을 돌려보면 일단 두개의 큐브가 나오는데 바깥쪽의 큰 큐브의 각 면은 FBO에 그려진 또 다른 회전하는 작은 큐브를 끊임없이 보여준다. (아래 그림 처럼)


6. 치우기

다 사용했다면 프로그램 종료전에 치우기를 수행해야 한다. (메모리 해제) 뭐 이것도 간단하다.
glDeleteFramebuffersEXT(1, &fbo);
위 명령은 fbo 를 삭제하라는 명령이고 앞서 만든 렌더버퍼를 날려보내기 위해서는 다음 명령을 수행한다.
glDeleteRenderbuffersEXT(1, &depthbuffer);
뭐 가운데 한 단어만 다를뿐 같은 명령이다.

마지막으로 글쓴이는 몇가지 당부를 하고 있는데, FBO를 생성했다 지웠다를 프로그램 실행중 반복하지 말라는 부탁과 렌더링된 FBO의 색상 버퍼를 glTexImage 함수를 이용해서 변경하거나 추가하는 행위를 피하는 것이 좋다고 한다. 이는 당연한 이야기로 이렇게 하면 Performance가 나빠질 것은 불을 보듯 뻔하다.

그리고 ATI를 사용하는 사람들에게는 위 예제가 제대로 동작하지 않을 수도 있단다. ATI 드라이버를 사용할때  FBO의 깊이 버퍼를 렌더버퍼로 연결하는 과정에 버그가 있다고 하는데 아마도 지금이 다 고쳐 졌겠지? 

소스 파일을 글에 첨부하였는데 Visual Studio 솔루션 파일이고 추가로 필요한 프로그램은 FreeGLUT이다. 이 FreeGLUT는 http://freeglut.sourceforge.net/에 가면 다운 받을수 있고 압축을 푼후 VisualStudio2008 폴더에 있는 프로젝트 파일을 열어서 컴파일 하게 되면 freeglut.lib와 freeglut.dll 파일이 생성된다. freeglut.dll은 윈도우즈 폴더에 System32 폴더안에 던져 넣어 버리고 lib 파일은 FBO_Example 프로젝트 폴더 안에 넣거나 FBO_Example/Release 폴더 및 FBO_Example/Debug 폴더에 던져 넣어버리면 문제 없이 컴파일 될것이다. 



posted by 대갈장군