[OpenGL Tutorial] TESSELLATION

OpenGL에서 폴리곤의 제약 중에 폴리곤은 오목한 부분이 있어서는 않된다는 것이다. 즉 아래와 같은 별 모양의 폴리곤은 규칙을 위반한 폴리곤이 되겠다.


사용자 삽입 이미지
위의 폴리곤은 총 8개의 점으로 이루어진 폴리곤이다. 튀어나온 부분(볼록한 부분)이 총 4개인데 폴리곤에서 볼록한 부분은 상관없지만 들어간 부분(오목한 부분) 역시 4개로써 이것은 OpenGL에서는 다음과 같은 모습으로 연출되고만다.


사용자 삽입 이미지


오목한 부분중에 두개의 부분이 무시되어져 버린 상태에서 연출되었다는 것을 알수있다. 그렇다면 이런 현상을 어떻게 막을수있겠는가? 그것은 폴리곤을 오목한 부분이 없도록 쪼개서 나누어 연출해야만 한다. 어떠한 폴리곤이라도 오목한 부분이 없이 나눌 수는 있지만 쪼개기가 어려운 경우가 많다. 그렇다면 좀더 쉽게 위의 오동작 아닌 오동작을 방지하는 방법은 없는가? 다행이도 TESSELLATION이라는 기능이 있다. 이것은 폴리곤의 각 꼭지점만을 명시해 주면 자동으로 폴리곤을 오목한 부분이 없도록 쪼개서 연출해 주는 아주 강력한 기능이다. 바로 이장이 이 TESSELLATION 기능에 대해서 알아보는 장이다.

(왜 OpenGL은 오목한 부분을 가진 폴리곤을 연출하지 못하는가? 그것은 폴리곤을 아주 빠르게 연출하기 위해서이다)

자, 이제 TESSELLATION에 대해서 알아보도록 하자. 소스 코드는 1장의 코드에서 시작한다.

먼저 TESSELLATION(앞으로 Tess라 부르겠다) 기능을 사용하기 위한 객체를 하나 생성해야 겠다. 이를 위해서 먼저 Tess 객체를 위한 포인터 변수를 전역 변수 선언 지역에 하나 정의하자. Tess는 여러가지 속성을 가지고 있으므로 이런 절차가 필요하다.

GLUtriangulatorObj *tess;

이제 tess라는 Tess를 위한 포인터 변수가 만들어졌다. 포인터 변수이므로 이제 실제 인스턴스를 생성해야 한다.

int initGL(GLvoid)
{
    glShadeModel(GL_SMOOTH); // Enable Smooth Shading
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background
    glClearDepth(1.0f); // Depth Buffer Setup
   
    glEnable(GL_DEPTH_TEST); // Enables Depth Testing
    glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); 
    // Really Nice Perspective Calculations
   
    // New Adding
    tess=gluNewTess(); //<*>
    gluTessCallback(tess, GLU_TESS_BEGIN, 
        (void (__stdcall *)(void))CustomTessBegin); //<1>
    gluTessCallback(tess, GLU_TESS_VERTEX, 
        (void (__stdcall *)(void))CustomTessVertex); //<2>
    gluTessCallback(tess, GLU_TESS_END, 
        (void (__stdcall *)(void))CustomTessEnd); //<3>
    gluTessCallback(tess, GLU_TESS_ERROR, 
        (void (__stdcall *)(void))CustomTessError); //<4>
   
    return TRUE; // Initialization Went OK
}

위의 코드는 우리에게 익숙한 초기화 코드가 오는 함수이다. 바로 <*>번의 코드가 Tess객체의 인스턴스를 생성하여 전역변수에서 정의한 tess 포인터 변수에게 넘겨주는 기능을 한다. (주의: Tess는 TESSELLATION을 의미하고 tess는 앞서 정의한 포인터 변수명) Tess 인스턴스를 생성했으므로 우리가 프로그램을 종료할때 해제해 주는 코드가 있어야 한다는 것은 자명한 사실이다. glKillWindow 함수의 맨 아래 부분에 다음 코드를 추가하자.

gluDeleteTess(tess);

이제 실펴보아야 할것은 <1>~<4>번 코드들인데 이것들은 무엇인가? Tess는 Core(핵심) OpenGL API군이 아닌 GLU라는 유틸리티 API군이다. 따라서 코어 OpenGL API가 아닌 Tess가 폴리곤을 그리기 위해서는 코어 OpenGL API의 도움을 받아야 하는데 바로 이 도움을 받기 위한 코어 OpenGL API 함수들을 콜백하기 위한 코드들이다.

gluTessCallback함수는 세개의 인자를 취하는데 첫번째는 Tess 객체의 포인터 변수이고 둘째는 어떤 기능에 대한 콜백함수를 지정할 것인지에 대한 식별자이고 세번째 인자는 실제 콜백될 함수이다. 이제 하나 하나 살펴보자.

<1>번은 Tess가 폴리곤을 그리기 시작할때 호출될 콜백함수를 지정하는 것이다.

<2>번은 Tess가 폴리곤을 그리기 위해서 각 꼭지점을 지정할때 호출할 콜백수를 지정하는 것이다.

<3>번은 Tess가 폴리곤을 다 그렸다고 판단할때 호출할 콜백함수를 지정하는 것이다.

<4>번은 Tess가 폴리곤을 그리는 도중에 에러가 발생할때 호출할 콜백함수를 지정하는 것이다.

그렇다면 각 콜백 함수는 어떤 내용을 담고 있는가. 위에 제시된 4개의 콜백함수의 모습은 다음과 같다.

void __stdcall CustomTessBegin(GLenum prim)
{
    glBegin(prim);
}

void __stdcall CustomTessVertex(void *data)
{
    glVertex3dv((GLdouble *)data);
}

void __stdcall CustomTessEnd()
{
    glEnd();
}

void __stdcall CustomTessError(GLenum errorCode)
{
    char err[256];
   
    sprintf(err, "Tessellation Error: %s\n", gluErrorString(errorCode));
    if(errorCode!=GL_NO_ERROR) 
        MessageBox(hWnd, err, "TESSELLATION Error", MB_OK);
}

CustomTessBegin은 glBegin이라는 코어 OpenGL API를 호출하고 있고 CustomTessVertex는 glVertex3dv라는 코어 OpenGL API를 호출하고 있다. CustomTessEnd는 glEnd라는 코어 OpenGL API를 호출하고 있고 CustomTessError는 에러가 발생할을때 처리할 코드들이 담겨있는데 여기서는 그 에러가 무엇인지를 표현해주기만 한다. 실제로 위의 코드들은 여러분이 직접 타이핑해야 한다. 즉, 정리하자면 Tess는 폴리곤을 그리기 위해서 코어 OpenGL API의 직접적인 도움을 받아 생성한다는 것이다.

자, 이제 Tess를 이용해서 물체를 그려보는 코드를 작성하는 것만이 남았다. 어떤 폴리곤을 그릴까? 우리가 이 장의 맨 첫부분에서 소개했던 별로 하자. 먼저 그 별을 그리기 위해 그 별 모양의 폴리곤의 꼭지점의 데이타를 정의해야한다. 전역 변수에서 다음과 같은 배열을 정의하자.

GLdouble ov[8][3] = {{0.0f, 1.0f, 0.0f},
  {0.4f, 0.4f, 0.0f},
  {1.0f, 0.0f, 0.0f},
  {0.4f, -0.4f, 0.0f},
  {0.0f, -1.0f, 0.0f},
  {-0.4f, -0.4f, 0.0f},
  {-1.0f, 0.0f, 0.0f},
  {-0.4f, 0.4f, 0.0f} };

이미 짐작하고 있던대로 각각의 8개의 꼭지점을 정의하고 있다. 이 배열을 토대로 해서 별 모양의 폴리곤을 그리려고 하는 것이다. 이제 실제로 그려주는 코드를 살펴 보도록하자. DrawGLScene 함수를 살펴보자.

int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
    // Clear Screen And Depth Buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
    glLoadIdentity(); // Reset The Current Modelview Matrix
    glTranslatef(0.0f, 0.0f, -3.0f);
   
    gluTessBeginPolygon(tess, NULL);    // <1>
    gluTessBeginContour(tess);          // <2>
    gluNextContour(tess, GLU_EXTERIOR); // <3>
    gluTessVertex(tess, ov[0], ov[0]);  // <4>
    gluTessVertex(tess, ov[1], ov[1]);  // <5>
    gluTessVertex(tess, ov[2], ov[2]);  // <6>
    gluTessVertex(tess, ov[3], ov[3]);  // <7>
    gluTessVertex(tess, ov[4], ov[4]);  // <8>
    gluTessVertex(tess, ov[5], ov[5]);  // <9>
    gluTessVertex(tess, ov[6], ov[6]);  // <10>
    gluTessVertex(tess, ov[7], ov[7]);  // <11>
    gluTessVertex(tess, ov[0], ov[0]);  // <12>
    gluTessEndContour(tess);            // <13>
    gluTessEndPolygon(tess);            // <14>
   
    return TRUE; // Everything Went OK
}

자, 노란색의 코드가 새롭게 추가된 코드이다. 새롭게 등장한 녀석들에 대해서 살펴보도록 하자.

<1>번 코드는 우리가 생성한 Tess객체의 인스턴스 포인터 변수를 이용해서 폴리곤을 그린다는 시작을 알리는 것이다.

<2>번 코드는 폴리곤의 윤곽의 한계를 지정한다는 의미이고 <13>코드와 쌍으로 사용된다.

<3>번 코드는 앞으로 지정될 폴리곤의 점들은 폴리곤의 가장자리의 점들임을 알리는 것이다.

<4>~<12>번 코드는 폴리곤의 점들을 지정하는 코드인데 이상하게도 2개의 점좌표값을 똑 같이 주었는데 첫번째 좌표값은 Tess가 사용하는 것이고 세번재 좌표는 우리가 앞서 지정한 CustomTessVertex 콜백 함수에 전달되는 좌표값이다.

<13>번은 <2>번과 쌍을 이루는 것으로써 윤곽의 한계 지정을 끝마친다는 것이다.

<14>번은 Tess의 폴리곤 생성을 끝마친다는 의미이다.

이제 실행을 해보자. 아래는 그 실행 결과이다.

사용자 삽입 이미지
어떤가? 독자가 생각했던 대로 실행이 되었는가? 이제 여기서 우리는 이 폴리곤의 가운데에 구멍을 뚤어보자. 즉 다음과 같은 모습을 얻기 위해서 말이다.

사용자 삽입 이미지

일단 가운데 구멍을 뚫기 위해서 사용할 점들의 좌표가 필요하다. 구멍이 사각형이므로 4개의 좌표값만 있으면 충분하다. 그 좌표값들을 정의하기 위해서 다시 전역변수 선언부에 다음과 같은 배열을 정의한다.

GLdouble iv[4][3] = {{0.0f, 0.2f, 0.0f},
   {0.2f, 0.0f, 0.0f},
   {0.0f, -0.2f, 0.0f},
   {-0.2f, 0.0f, 0.0f} };

이제 DrawGLScene에 어떤 코드가 또 새롭게 추가되었는지를 살펴보자.

int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer
    glLoadIdentity(); // Reset The Current ModelviewMatrix
    glTranslatef(0.0f, 0.0f, -3.0f);
   
    gluTessBeginPolygon(tess, NULL);
    gluTessBeginContour(tess);
    gluNextContour(tess, GLU_EXTERIOR);
    gluTessVertex(tess, ov[0], ov[0]);
    gluTessVertex(tess, ov[1], ov[1]);
    gluTessVertex(tess, ov[2], ov[2]);
    gluTessVertex(tess, ov[3], ov[3]);
    gluTessVertex(tess, ov[4], ov[4]);
    gluTessVertex(tess, ov[5], ov[5]);
    gluTessVertex(tess, ov[6], ov[6]);
    gluTessVertex(tess, ov[7], ov[7]);
    gluTessVertex(tess, ov[0], ov[0]);
   
    gluNextContour(tess, GLU_INTERIOR); //<1>
    gluTessVertex(tess, iv[0], iv[0]); //<2>
    gluTessVertex(tess, iv[1], iv[1]); //<3>
    gluTessVertex(tess, iv[2], iv[2]); //<4>
    gluTessVertex(tess, iv[3], iv[3]); //<5>
    gluTessVertex(tess, iv[0], iv[0]); //<6>
    gluTessEndContour(tess);
    gluTessEndPolygon(tess);
   
    return TRUE; // Everything Went OK
}

<1>번을 주의 깊게 실펴보자. 두번째 인자로 GLU_INTERIOR이라고 되어있다. 바로 이것이 키포인트인데 <1>번 코드 이후로 지정되는 점들의 좌표는 모두 구멍을 뚫기 위한 폴리곤의 좌표로 인식되는 것이다.

<2>~<6>은 모두 구멍을 뚫기 위한 폴리곤의 좌표를 지정한다.

이제 실행해 보라. 그러면 우리가 원하는 결과를 얻을수있다.

자, 이제 마지막으로 필자가 소개할 것이 하나 남았다. 바로 그것은 지금까지는 폴리곤을 내부가 채워진 것으로 그려왔는데 이제는 그것이 아닌 내부가 칠해지지 않는, 즉 가장자리 선만을 그리고자 하는 것이다. 아마도 독자중에서는 그거야 쉽지 않은가할 사람도 있을 것이다. 바로 glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)로 해주면 되겠다 할것이다. 그렇다면 이 코드를 지금까지 우리가 만든(구멍 뚫린 폴리곤) 코드의 InitGL 함수에 추가해서 실행시켜 보자. 그 결과는 다음과 같다.


사용자 삽입 이미지


어떤가? 우리는 가장 자리의 선만을 원했지만 그 내부의 선들까지도 모두 그려져 버렸다. 이것은 우리가 원하는 결과가 아니다. 그렇다면 우리가 원하는 결과를 얻기 위해서는 어떻게 해야 하는가? 다행이도 Tess는 한 단줄의 코드로 우리에게 그 결과를 제공한다. gluTessProperty(tess, GLU_TESS_BOUNDARY_ONLY, true), 이 코드를 InitGL함수의 맨 아래부분에 추가한후 실행해 보라. 그 결과는 다음과 같다. (이제 더 이상 glPolygonMode 코드는 필요치 않다)


사용자 삽입 이미지


이제, 우리가 원하는 결과가 얻어졌다.

자 이로써 TESSELLATION에 대한 것에 대해 살펴보는 것을 마치도록 하겠다.

“[OpenGL Tutorial] TESSELLATION”에 대한 6개의 댓글

  1. 정말 좋은 자료 잘 봤습니다

    정리를 정말 깔끔하게 잘 해놓으셨네요

    관련된 자료 찾을려고 돌아다녔는데

    이게 딱 제가 찾던 자료네요 ^^

김형준(Dip2K)에 답글 남기기 응답 취소

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다