[OpenGL Tutorial] Output String on the Screen(Outline)

사용자 삽입 이미지지난장에서는 비트맵으로 문자를 출력했었다. 이번에는 문자를 객체로 처리하는 방법인 Outline Font에 대해서 알아보도록 한다. 이 장의 시작은 7장의 소스 코드에서 시작한다.

전역 변수를 2개 추가하자. Outline Font로써 문자를 처리하면 일반 객체처럼 회전이 가능하다. 이를 확인해 보기 위해 문자를 회전시키는데 사용하는 변수 하나와 Outline Font 문자들에 대한 정보를 저장하는 변수 변수인 gmf을 선언한다. 배열이 96개인 이유는 ASCII의 코드중에서 숫자, 영문자와 특수문자에 대해 사용할 수 있도록 하기 위함이다.

GLuint base;
GLfloat rot = 0.0f; // 
GLYPHMETRICSFLOAT gmf[96]; // 

이제 해야 할 일은 모든 문자들을 Outline Font로써 리스트에 등록 저장하는 것이다. 즉, OpenGL에서 사용할수 있도록 폰트의 리스트를 만들어야 한다. 다음의 코드가 폰트의 리스트를 생성하는 코드이다.

GLvoid BuildFont(GLvoid)
{
    HFONT font;
    base = glGenLists(96);
    font = CreateFont(-12,
                0,
                0,
                0,
                FW_BOLD,
                FALSE,
                FALSE,
                FALSE,
                HANGUL_CHARSET,  // <1-1>
                OUT_TT_PRECIS,
                CLIP_DEFAULT_PRECIS,
                ANTIALIASED_QUALITY,
                FF_DONTCARE|DEFAULT_PITCH,
                "Comic Sans MS"); // <1-2>
   
    SelectObject(hDC, font);
   
    wglUseFontOutlines(hDC,  // <2-1>
                32, // <2-2>
                96, // <2-3>
                base, // <2-4>
                0.1f, // <2-5>
                0.5f, // <2-6>
                WGL_FONT_POLYGONS, // <2-7>
                gmf); // <2-8>
}

지난장에서 설명했던 것은 제외하고 새로운 것만을 설명하도록 하겠다.

<1-1>의 코드를 살펴보자. 기억력이 좋은 독자는 지난장에서의 <1-1>의 코드가 무엇이었는지 기억하는가? ANSI_CHARSET이였다. 하지만 한글 윈도우즈에서는 Outline Font를 사용할때 ANSI_CHARSET로 해주면 Outline Font를 얻을수없다. 원하는 것을 얻기위해서는 HANGUL_CHARSET으로 지정해 주어야 한다.

<1-2>에서 폰트를 바꾸었다. 지난장과 같이 폰트를 바꾸지 않아도 상관없다.

이제 가장 핵심이 되는 실제 Outline Font의 리스트를 얻어오는 함수에 대해서 알아보도록 하겠는데 바로 <2-1>에서 보이는 wglUseFontOutlines가 바로 그것이다. 총 8개의 인자가 필요한데 하나 하나 알아보자.

<2-1> 코드는 OpenGL과 연결되어 있는 DC에서 폰트 정보를 얻기 위한 DC의 핸들이다.

<2-2> 코드는 ASCII에서 시작할 코드의 번호이고 <2-3> 코드는 <2-2>코드에서 지정된 코드에서 몇개의 문자를 얻을 것인지를 지정하는 갯수값이다.

<2-4> 코드는 실제 리스트를 만들때 기준이 되는 값을 지정하는 것이다.

<2-5> 코드는 폰트로부터 얼마나 정밀하게 객체를 생성할 것인지를 나타낸다. 0.0값이 가장 정말하게 만들수있다.

<2-6> 코드는 Z축으로 문자 객체의 두깨를 설정한다.

<2-7> 코드가 가질수있는 값은 2가지인데 첫째는 WGL_FONT_POLYGONS와 WGL_FONT_LINES이다. 첫째는 면으로 구성된 폰트 객체를 생성하고 두번째는 라인만으로 구성된 폰트 객체를 생성한다.

<2-8> 코드는 실제 생성된 각각의 문자에 대한 Outline 정보가 저장되는 배열 변수이다.

리스트를 생성했다면 생성된 것들을 반환하는 함수도 필요하다. 다음의 코드가 그와 같은 것이다. 이미 지난장에서 만들었고 그대로 사용하면 된다.

GLvoid KillFont(GLvoid)
{
    glDeleteLists(base, 96);
}

glDeleteLists 함수로써 첫번재 인자는 메모리 상에서 제거할 리스트의 기준값이 오고 두번째는 그 기준에서부터 몇개의 리스트를 제거할 것인지를 나타낸다.

이제 사용할 폰트 리스트가 완벽하게 준비가 되었다. 이제 이 준비된 리스트를 화면상에 찍을 수 있는 함수를 만들어보자. 출력한 물자열을 인자로 받아 출력해 주는 함수이다. 이미 지난장에서 만들었고 그대로 사용하면 된다.

GLvoid glPrint(const char *text)
{
    glPushAttrib(GL_LIST_BIT); //<1>
    glListBase(base - 32); //<2>
    glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); //<3>
    glPopAttrib(); //<4>
}

자! 이제 화면상에 글자를 그려주는 함수까지 준비가 되었다. 이제 위에서 만든 것들을 사용만 하면 되겠다. 먼저 initGL(GLvoid) 함수에 위에서 만든 BuildFont 함수를 추가함으로써 폰트 리스트를 생성하도록 한다.

int InitGL(GLvoid)
{
    glShadeModel(GL_SMOOTH);
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
    glClearDepth(1.0f);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
   
    ///////////////////////// NEW //////////////////////////
    BuildFont();
    glEnable(GL_LIGHTING); // <1>
    glEnable(GL_LIGHT0); // <2>
    glEnable(GL_COLOR_MATERIAL); // <3>
    ///////////////////////// NEW //////////////////////////
   
    return TRUE;
}

위의 코드를 보면 새롭게 추가된 코드가 3개이다. <1>과 <2>는 빛을 켜는 코드이고 <3>은 색상을 추적하여 물체의 재질을 설정하는 코드이다. 이 코드가 새로운 독자는 빛과 재질에 대한 장을 보기 바란다.

그리고 WinProc 함수의 WM_CLOSE를 처리하는 부분(이 부분은 프로그램이 종료될때 실행된다)에서 KillFont 함수를 추가하여야 하는데 이미 지난장에서 추가하였다.

case WM_CLOSE:
{
    ///////////////////////// NEW //////////////////////////
    KillFont();
    //////////////////////// NEW //////////////////////////
    PostQuitMessage(0);

    return 0;
}

자 이제 문자열을 화면상에 그려주는 것만 남았다. 무언가를 그려주는 코드는 항상 DrawGLScene(GLvoid) 부에 위치했고 이번에도 역시 마찬가지이다. 다음의 코드를 살펴보자.

int DrawGLScene(GLvoid)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glTranslatef(0.0f, 0.0f, -6.0f);
   
    glColor3f(0.90f, 0.9f, 1.0f);
    glScalef(1.0f, 2.0f, 1.0f);
    glRotatef(rot, 0.0f, 1.0f, 0.0f);
    glTranslatef(-1.0f, -0.3f, 0.0f);
    glPrint("Dip2K");
    rot += 1.0f;
   
    return TRUE;
}

DrawGLScene 함수의 구현은 모두 새롭게 작성되어졌다.

기본 흐름은 옅은 파랑색으로 재질을 설정하며 Y축으로 물체(문자)를 늘렸다. 그리고 매 순간마다 Y축으로 1도씩 회전시키는 에니메이션으로써 분명하게 문자의 3차원임일 확인한다. 비트맵의 경우 특별하게 문자의 위치를 지정하는 함수가 따로 있었던데 반해서 Outline Font는 단지 객체이므로 따로 그럴 필요가 없다.

다음은 최종적인 이 프로그램의 실행 결과이다.


사용자 삽입 이미지
어떤가? 드디어 우리가 원하는 결과를 얻었다.

[OpenGL Tutorial] Output String on the Screen(Bitmap)

사용자 삽입 이미지OpenGL에서 문자를 출력해야 할 경우가 있다. 그렇다면 OpenGL에서는 문자를 출력하기 위한 루틴이 준비되어져 있는가? 대답은 “전혀, 아니다” 이다. 그 이유는 각 플랫폼 OS에 따라 문자를 출력하는 방식이 너무나도 다르기 때문이라고 하는 이도 있다. 그래서인지 각 플랫폼마다 각각 그에 맞는 OpenGL API를 확장하여 제공하는데 문자 출력 부분의 함수도 같이 제공된다. 윈도우즈에서는 위글(WGL)이라 불리우며 Applet은 아글(AGL), 유닉스 계열의 OS은 XGL의 이름으로 재공된다. 본장은 여러가지 플랫폼들중에 우리가 가장 많이 사용하는 윈도우즈에서 문자를 출력해 보는 것으로 구성하려고 한다. 즉, 이 장은 다른 OS에서는 이용될수 없다.

우리의 컴퓨터에는 많은 글꼴이 하드 디스크에 저장되어져 있고 또 사용되어진다. 이 많은 글꼴들을 OpenGL에서 사용하는 방법에는 2가지가 있는데 첫째는 문자 하나 하나를 Bitmap 방식으로 사용하는 경우와 문자 하나 하나를 객체로 처리하여 사용하는 경우가 있다. Bitmap 방식으로 사용할 경우 이것은 객체가 아니기 때문에 회전이라든지 확대/축소라든지 텍스쳐 맵핑등 OpenGL에서 객체에 적용할 수 있는 것들이 크게 제약된다. 하지만 객체 단위로 처리할 경우 말 그대로 문자 하나 하나가 객체이므로 모든 특수효과를 적용할수 있다. 그러나 Bitmap에 비하여 객체단위로 문자를 처리하는 것은 많은 어버헤드와 연산 시간이 소요된다. 이장에서는 Bitmap으로 문자를 처리하는 것에 대해 다루고 다음에 장에서 객체를 이용한 문자 출력을 설명하기로 한다. 참고로 객체 단위로 문자를 처리하는 경우 그 문자를 Outline 형태의 문자라 한다.

이곳에서 사용할 소스 코드는 1장에서 만들어진 것에서 부터 시작된다.

전역 변수를 하나 설정하자. 이 변수는 폰트 리스트의 기준으로 사용된다.

GLuint base;

이제 해야 할 일은 모든 문자들을 비트맵 형식으로 저장하는 것이다. 즉, OpenGL에서 사용할수 있도록 폰트의 리스트를 만들어야 한다. 다음의 코드가 폰트의 리스트를 생성하는 코드이다.

GLvoid BuildFont(GLvoid)
{
    HFONT font; // <1>
    base = glGenLists(96); // <2>
    font = CreateFont(-24, // <3-1>
                0,
                0,
                0,
                FW_BOLD, // <3-2>
                FALSE, // <3-3>
                FALSE, // <3-4>
                FALSE, // <3-5>
                ANSI_CHARSET, // <3-6>
                OUT_TT_PRECIS,
                CLIP_DEFAULT_PRECIS,
                ANTIALIASED_QUALITY,
                FF_DONTCARE|DEFAULT_PITCH,
                "Courier New"); // <3-6>
   
    SelectObject(hDC, font); // <4>
    wglUseFontBitmaps(hDC, 32, 96, base); // <5>
}

<1>번 코드는 윈도우즈에 설치되어 있는 폰트를 사용하기 위해 폰트를 생성하기 위해 사용하는 변수이다.

<2>번 코드는 총 96개의 리스트를 만든다. 숫자와 영문자(대, 소문자) 그리고 특수 기호를 모두 합한 갯수이다. 잠깐 리스트에 대해서 소개하자면 리스트란 우리가 어떤 물체를 만든다고 할때 그 물체를 하나의 리스트에 지정해 두면 이후에 또 그 물체가 필요할 경우 지정된 리스트만을 호출해서 필요한 물체를 사용할 수 있도록 하는 기능이다. 장점이라면 리스트로 미리 물체를 생성해 놓으면 그렇지 않을때보다 속도가 많이 개선된다. 단점이라면 메모리를 차지 하고 있다는 것이다. 리스트를 생성하는 함수가 glGenLusts인데 몇개의 리스트를 생성하는지를 인자로 갖는다. 반환값은 생성된 리스트의 첫번째를 참조하는 기준(Base)값을 리턴한다.

<3-1>번 코드에서 보이는 CreateFont는 Font를 생성하는하여 반환하는 것으로 아주 많은 인자를 갖는다. 필자도 이 모든 인자에 대해서 자세히 알지 못하지만 중요하다고 생각되는 인자에 대해서만 언급하도록 하겠다. 첫번재 인자는 폰트의 크기이다. 이 경우 -24인 음수를 주었는데 이는 만약 크기 24인 폰트를 생성하는데 24인 폰트를 만들수없다면 그와 가장 가까운 크기로 만들어라는 의미에서 음수 형태로 지정한 것이다. 그리고 <3-2>의 인자는 폰트를 어느 정도로 두껍게 할지에 대해 지정하는 것이다. 주로 갖는 값은 FW_SHIN, FW_BOLD, FW_NORMAL 등이 있다. <3-3>은 Italic 모양인지를 지정하는 것이고 <3-4>는 언더라인 형태인지를, <3-5>는 Strike Out 형태인지를 Boolean 형태로 지정한다. <3-6>은 생성할 폰트의 Char Set를 지정하는 것으로 대부분의 영문자에 대해서는 ANSI_CHARSET이면 충분하고 한국 윈도우즈의 경우 HANGUL_CHARSET으로 지정할 수도 있는데 다음장에서 설명할 Outline 형태의 문자 출력에서는 HANGUL_CHARSET으로 지정해야만 된다. 그리고 벳딩과 같은 그림문자 기호는 SYMBOL_CHARSET으로 지정해야 원하는 바를 얻을수 있다. 그리고 <3-6>은 사용하기를 원하는 폰트의 이름을 지정한다. 이 함수에 대한 나머지 자세한 것들은 관련 자료를 찾아보길 바란다.

<4>번 코드는 생성한 폰트를 해당 DC에서 사용하도록 지정하는 코드이다.

<5>번 코드는 OpenGL이 사용하는 DC에서 사용하는 폰트를 통해서 폰트 리스트를 생성하는 위글 함수이다. 위글 함수는 윈도우즈에서만 제공되는 함수이다. 첫번째 인자는 DC의 헨들, 두번째는 코드값과 밀접한데 우리가 ASCII 코드는 0에서 255까지의 값을 갖는다. 즉 두번째는 ASCII 코드값중에서 몇번부터 사용할 것인지를 지정한다. 세번째는 두번째에서 지정한 코드에서부터 몇개의 코드를 취할 것인지를 지정한다. 위의 코드는 32번 코드에서부터 총 96개의 문자를 취해 리스트를 생성하고자 한다는 의미이다. 네번째 인자는 생성될 리스트의 기준이 될 변수이다. 이 코드가 가장 핵심적인 코드가 되겠다.

리스트를 생성했다면 생성된 것들을 반환하는 함수도 필요하다. 다음의 코드가 그와 같은 것이다.

GLvoid KillFont(GLvoid)
{
    glDeleteLists(base, 96);
}

glDeleteLists 함수로써 첫번재 인자는 메모리 상에서 제거할 리스트의 기준값이 오고 두번째는 그 기준에서부터 몇개의 리스트를 제거할 것인지를 나타낸다.

이제 사용할 폰트 리스트가 완벽하게 준비가 되었다. 이제 이 준비된 리스트를 화면상에 찍을 수 있는 함수를 만들어보자. 출력한 물자열을 인자로 받아 출력해 주는 함수이다.

GLvoid glPrint(const char *text)
{
    glPushAttrib(GL_LIST_BIT); //<1>
    glListBase(base - 32); //<2>
    glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); //<3>
    glPopAttrib(); //<4>
}

<1>번 코드는 리스트에 대한 상태를 저장하는 것인데 이는 우리가 리스트를 하나만 사용하는 경우에는 별로 중요치 않으나 또 다른 다른 리스트 사용할 수도 있다. 또 다른 리스트를 위해서 리스트 상태를 저장해 두는 것이다.

<2>번 코드는 사용하고자 하는 리스트의 기준을 OpenGL에게 알려주는 것이다. 기준에서 32를 뺐는데(base-32) 우리가 위에서 폰트 리스트를 생성할때 ASCII 코드중에서 32번부터 시작을 했기 때문이다.

<3>번 코드는 실제적으로 리스트에서 필요한 것들을 가져와 그려주는 핵심이 된다. text 변수는 문자열 배열이다. 우리가 ASCII에서 ‘A’는 65번이다. ‘B’는 66번이다. text 변수에 “ABC”라는 문자가 있다고 해보자. text는 문자열 배열이라고 볼수있고 각 셀의 값은 부호없는 Byte형이다. 모두 3개의 문자로 이루어져 있다. 이것은 곧 첫번재 셀에는 65가 두번째 셀에는 66이 세번째 셀에는 67의 Unsigned Byte Type의 값이 들어있는 것과 동일하다. 이부분을 이해했다면 glCallLists를 살펴보자. 첫번재 인자는 해석해야될 셀의 겟수를 나타내고 두번째는 셀의 데이타 타입을 세번째는 그 셀의 배열을 나타내게 된다. 결국 하나 하나의 셀을 순서대로 처리해서 화면상에 그려주는 것이다. 일단 하나의 셀이 처리되면 자동으로 다음에 처리될 셀을 위해 좌표가 오른쪽으로 이동된다.

자! 이제 화면상에 글자를 그려주는 함수까지 준비가 되었다. 이제 위에서 만든 것들을 사용만 하면 되겠다. 먼저 initGL(GLvoid) 함수에 위에서 만든 BuildFont 함수를 추가함으로써 폰트 리스트를 생성하도록 한다.

int InitGL(GLvoid)
{
    glShadeModel(GL_SMOOTH);
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
    glClearDepth(1.0f);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
   
    ///////////////////////// NEW //////////////////////////
    BuildFont();
    ///////////////////////// NEW //////////////////////////
   
    return TRUE;
}

그리고 WinProc 함수의 WM_CLOSE를 처리하는 부분(이 부분은 프로그램이 종료될때 실행된다)에서 KillFont 함수를 추가한다.

case WM_CLOSE:
{
    ///////////////////////// NEW //////////////////////////
    KillFont();
    ///////////////////////// NEW //////////////////////////
    PostQuitMessage(0);
    return 0;
}

자 이제 문자열을 화면상에 그려주는 것만 남았다. 무언가를 그려주는 코드는 항상 DrawGLScene(GLvoid) 부에 위치했고 이번에도 역시 마찬가지이다. 다음의 코드를 살펴보자.

int DrawGLScene(GLvoid)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glTranslatef(0.0f, 0.0f, -4.0f);
    glBegin(GL_QUADS);
    glColor3f(0.0f, 0.0f, 0.0f); glVertex2f(-1.0f, -1.0f);
    glColor3f(1.0f, 0.0f, 0.0f); glVertex2f(1.0f, -1.0f);
    glColor3f(0.0f, 1.0f, 0.0f); glVertex2f(1.0f, 1.0f);
    glColor3f(0.0f, 0.0f, 1.0f); glVertex2f(-1.0f, 1.0f);
    glEnd();
   
    glColor3f(1.0f, 1.0f, 1.0f);      //<1>
    glRasterPos2f(-1.4f, 0.0f);       //<2>
    glPrint("Dip2K's OpenGL Forum");  //<3>
   
    return TRUE;
}

노란색의 코드가 새롭게 추가된 것들인데 중요한 것은 <1>번과 <2>번 코드이다. 나머지 것들은 모두 지난장에서 살펴보았으므로 설명은 생략하기로 하고 중요한 것들만 살펴보자.

<1>번 코드는 그려줄 비트맵의 색을 지정하는 코드이다. 비트맵 역시 glColor로써 색을 지정한다.

<2>번 코드는 우리가 어디에 문자를 그려줄 것인가를 지정하는 것이다. Bitmap의 문자는 특별이 위치를 지정해 주는 함수가 따로 준비되어 있는데 바로 glRasterPos2f가 그 함수이다. 첫번째는 X좌표이고 두번째는 Y좌표를 나타낸다. Bitmap은 Perspective가 적용되지 않으며 항상 Ortho 모드로만 적용된다.

<3>번 코드는 우리가 이미 만들어 놓은 함수이다. 출력하고자 하는 문자열을 넣어주기만 하면 그만이다.

다음은 최종적인 이 프로그램의 실행 결과이다.

사용자 삽입 이미지

어떤가? 드디어 우리가 원하는 결과를 얻었다.

[OpenGL Tutorial] Texture Mapping

사용자 삽입 이미지

이번 장에서는 컴퓨터 그래픽의 꽃이라고 할수있는 텍스쳐 맵핑에 대해서 알아보도록하자. 컴퓨터 그래픽에 있어서 가장 발전한 기술이 바로 이 텍스쳐 맵핑이라고 한다. 텍스쳐 맵핑이 무엇인지 잠깐 설명하도록 하겠다. 둠이라는 게임을 해 보았는가? 벽으로 막힌 건물 안에서 사악한 주인공이 조용이 살고 싶어하는 착한 괴물을 죽이고 다니는 아주 불건전한 겜이다. 여기서 둠에 나오는 벽을 생각해 보자. 그 벽은 원래 그냥 폴리곤(면)에 불과 했다. 그런데 그 면위에 그림을 입혀 마치 진짜 벽처럼 보이게 한 것이다. 바로 그림을 폴리곤에 입히는 것을 텍스쳐 맵핑이라고 한다. 그 둠이라는 게임에서 사용한 텍스쳐 맵핑 기술은 아주 원시적인 기술이다. 둠의 텍스쳐 맵핑은 단지 평평한 면에만 그림을 입힐수있다. 하지만 OpenGL에서는 곡면에도 그림을 입힐수있다.

대강 텍스쳐 맵핑에 대한 소개는 이쯤에서 접고 OpenGL에서 텍스쳐 맵핑 가운데 2차원 텍스쳐 맵핑에 대해서 알아보도록 하자. 2차원 텍스쳐 맵핑을 설명하기 위해 필자가 준비한 실습 예제는 우리가 지난장에서 만든 정육면체의 각면들에 세가지 그림을 입히는 것, 즉 그림(텍스쳐)를 맵핑하는 것으로 하겠다. 시작에 앞서 그 결과를 먼저 보여주겠다.

사용자 삽입 이미지

그 결과를 살며 보았으면 이제 어떻게 이런 결과를 얻었는지의 그 과정을 설명하기 위해 5장에서 사용한 소스를 기본으로 해서 제작해도록 하겠다. 자! 5장의 소스를 비주얼씨++로 열어 보기 바란다.

텍스쳐 맵핑을 하기 위해 준비해야될 것들은 무엇인가? 바로 텍스쳐, 즉 그림이다. 이 장에서는 3개의 그림이 필요하겠다. OpenGL에서는 간단하게 그림 파일(BMP)을 바로 텍스쳐 맵핑 소스로 사용할 수 없다. 이 그림 파일을 텍스쳐 맵핑 소스로 변환해 주는 루틴이 필요한다. 다음이 바로 그 루틴이다.

AUX_RGBImageRec *LoadBMPFile(char *filename)
{
    FILE *hFile = NULL;
    if(!filename) return NULL;
   
    hFile = fopen(filename, "r");
    if(hFile) {
        fclose(hFile);
        return auxDIBImageLoad(filename);
    }
   
    return NULL;
}

AUX_RGBImageRec 구조체는 AUX 라이브러리의 함수로 BMP 파일의 크기와 그림 데이타 포인터를 가지고 있다. 위의 함수는 파일 이름 인자를 가지는데 그 인자에 해당하는 것이 BMP 파일이다. 중요한 함수는 auxDIBImageLoad 함수이고 나머지 모든 루틴은 파일이 존재하는지을 참조하기 위함이다. 모든 오류에 있어서 NULL 값을 반환한다.

이제 그림 파일에서 맵핑 소스를 읽어 들이는 루틴이 완성되었다. 그렇다면 텍스쳐 맵핑을 OpenGL에서 사용할수있도록 Setting을 해줘야 하는데 그에 해당되는 코드들을 살펴보자.

AUX_RGBImageRec *texRec[3]; //<1>
memset(texRec, 0, sizeof(void *)*3); //<2>

if((texRec[0]=LoadBMPFile("img.bmp")) && //<3>
    (texRec[1]=LoadBMPFile("img2.bmp")) &&
    (texRec[2]=LoadBMPFile("img3.bmp"))) {
    glGenTextures(3, &tex[0]); //<4>
    for(int i=0; i<3; i++) { //<5>
        glBindTexture(GL_TEXTURE_2D, tex[i]); //<6>
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //<7>
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //<8>
        glTexImage2D(GL_TEXTURE_2D, 0, 3, texRec[i]->sizeX, texRec[i]->sizeY, 0, 
                 GL_RGB, GL_UNSIGNED_BYTE,texRec[i]->data); //<9>
    }
} else return FALSE;

for(int i=0; i<3; i++) { //<10>
    if(texRec[i]) {
        if(texRec[i]->data) free(texRec[i]->data);
        free(texRec[i]);
    } else return FALSE;
}

glEnable(GL_TEXTURE_2D); //<11>
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); //<12>

자, 하나 하나 살펴 보도록하자.

<1> 코드는 우리가 3개의 텍스쳐 맵핑 소스를 만들어야 하기 때문에 3개의 Cell을 갖는 배열을 선언한 것이다. AUX_RGBImageRec 구조체 타입은 이미 앞에서 살펴보았는데 바로 그림 파일에서 맵핑 소스를 얻기 위해 사용할 함수의 반환값을 저장할 목적으로 쓰인다.

<2> 코드는 메모리 상의 변수를 0으로 만드는 코드인데 굳이 필요치 않는 코드이나 깔끔을 떠는 프로그래머들이 즐겨 사용하는 스타일로 어떤 변수를 사용하기에 앞서 0으로 초기화를 시켜본 경험이 있는 독자도 있을 것이다. 바로 그것이다.

<3> 코드는 IF 문인데 앞에서 살펴본 그림파일에서 텍스쳐 맵핑 소스를 생성하는 함수를 사용해서 각각의 3가지 그림 파일을 실제 읽어 들이는 루틴이다. 오류(파일이 없다)가 발생하지 않는 다면 실행점이 IF문의 TRUE 블럭으로 진입한다.

<4> 코드에서 새로운 함수가 등장했다. 앞으로 OpenGL의 새로운 함수가 많이 등장할 것이다. glGenTextures 함수는 2개의 인자를 갖는데 첫째는 우리가 몇개의 텍스쳐 맵핑 소스를 생성할 것인지를 지정하는 것이고 두번째는 그중 가장 첫번째 텍스쳐 맵핑 소스의 식별자의 주소값이다. 여기서 텍스쳐 맵핑 소스의 식별자라 함은 일단 텍스쳐 맵핑 소스가 생성되었으면 나중에 사용하기 위해서 그 맵핑 소스에 이름을 붙이게 되는데 그 이름으로써 정수값을 사용하며 그 정수값을 식별자라 부른다. 그런데 뜸금없이 tex라는 변수가 튀어 나왔는데 이 변수는 전역 변수 선언부에 다음과 같이 얌전하게 선언되어 있다.

GLuint tex[3];

3개의 텍스쳐를 사용할 것이므로 역시 3개의 Cell을 갖는 배열을 선언하였다.

<5>번 코드는 FOR 문인데 3번 반복하게 되어 있는데 한번의 반복당 하나의 텍스쳐 맵핑 소스에 대한 특성을 지정하게 된다.

<6>코드에 나오는 함수인 glBindTexture는 앞으로 설정할 텍스쳐의 식별자를 지정하는 것으로 첫번째 인자는 텍스쳐가 1차원(GL_TEXTURE_1D)인지, 2차원(GL_TEXTURE_2D)인지를 지정한다. 두번째 인자는 다음에 설정할 텍스쳐 맵핑의 식별자가 온다. 이 함수 이후로 이루어지는 텍스쳐 맵핑의 특성에 대한 설정은 모두 앞서 지정된 식별자에 해당하는 텍스쳐 맵핑에 대한 것이다.

<7>, <8>번 코드를 설명하기 전에 먼저 알아야 할 것이 있다. 실제 텍스쳐 맵핑의 크기와 맵핑되어질 물체의 크기가 똑같다면 간단하게 맵핑을 시키면 되겠으니 작거나 크다면 어떤 식으로 텍스쳐 맵핑 소스를 물체의 크기에 맞게 줄이거나 키울 것인가? 바로 그 어떻게 줄이거나 키울 것에 대한 방법으로 대표적으로 2가지가 있는데 하나는 보간법(GL_LINEAR)과 최근점법(GL_NEAREST)가 있다. 이해가 되었다면 glTexParameteri에 대해서 살펴보자. 세개의 인자를 취하는데 첫번째는 1차원인지, 2차원인지를 지정하는 것이고 두번째 인자는 만약 텍스쳐 맵핑 소스가 맵핑되어질 물체보다 클 경우에 어떤 식으로 축소시켜야 하는지를 지정하는 GL_TEXTURE_MIN_FILTER와 텍스쳐 맵핑 소스가 맵핑되어질 물체보다 작을 경우 어떤 식으로 확대시켜야 하는지를 지정하는 GL_TEXTURE_MAG_FILTER가 온다. 세번째 인자는 실제 그 축소, 확대시키는 방법을 기술하는데 대표적으로 GL_LINEAR와 GL_NEAREST가 온다. GL_LENEAR이 GL_NEAREST보다 나은 장면을 얻을 수 있으나 속도가 느리다. 위의 코드에서는 나은 장면을 얻기위해 모든 텍스쳐 맵핑 소스에 대해서 보간법을 사용하였다.


<9>번 코드는 꽤 많은 인자를 요구하는 glTexImage2D라고 하는 놈이다. 모두 9개의 인자를 갖는다. 첫번째는 텍스쳐 이미지가 1차원인지 2차원인지를 지정하는 것이고 두번째 인자는 항상 0으로 지정하게 된다. 사실 이 인자는 Detail Level을 나타내는 놈으로 보다 나은 Midmap 텍스쳐 맵핑을 구현할때 사용하는 것이다. 이장에서는 사용법에 대해 언급하지 않겠지만 간략하게 소개하자면 앞에서 우리는 텍스쳐 맵핑 소스가 물체보다 크거나 작을때 맵핑 소스를 줄이거나 키운다고 했다. 하지만 Midmap 텍스쳐 맵핑은 처음부터 미리 큰 맵핑 소스와 그리고 중간 맵핑 소스, 작은 맵핑 소스등 많은 맵핑 소스를 준비해서 보다 나은 장면을 얻을 수 있는데 이런 Midmap 맵핑 소스 이미지들은 프로그래머가 처음부터 그림 파일로 만들어 놓고 지정하는 경우와 컴퓨터가 자동으로 생성하는 경우가 있다. 이제 계속 인자에 대해 알아보자. 세번째 인자는 생상 구성 요소의 갯수인데 BMP 파일은 RGB의 세가지 색상요소가 있으므로 3이 온다. 네번재와 다섯번째는 이미지의 가로 길이와 세로 길이를 나타내게 되고 여섯번째는 이미지의 경계에 대한 픽셀두깨를 지정하게 된다. 이 예제에서 사용하는 그림들은 모두 불필요한 경계가 없으므로 0이 된다. 일곱번째는 텍스쳐 맵핑 소스의 구성 요소를 분명하게 지정하는 것으로 GL_COLOR_INDEX, GL_RED, GL_GREEN, GL_BLUE, GL_RGB 등 여러 값이 올수있으나 여기서는 GL_RGB로 이미 우리는 그림파일에서 텍스쳐 맵핑을 준비하는 것이고 그 그림파일은 RGB값을 갖는 구조이기 때문이다. 여덜번째는 색상 구성 요소들이 갖는 값의 타입을 나타내는데 모든 BMP파일에 있어서 RGB의 세가지 구성요소들은 부호 없는 Byte형이므로 GL_UNSIGNED_BYTE가 온다. 마지막으로 아홉번째는 텍스쳐 맵핑 소스의 실제 데이타값(그림 데이타)에 대한 포인터가 온다.

여기까지가 텍스쳐 맵핑 소스를 생성하고 그 맵핑 소스에 대한 특성을 설정하는 것들이다. 이후로 이제 불필요한 할당된 메모리를 해제하고 텍스쳐 맵핑을 사용한다고 지정하는 것들이 남았다.

<10>번 코드가 바로 이제는 더이상 필요치 않는 할당된 메모리를 해제하는 코드이다.

<11>은 2차원 텍스쳐 맵핑을 사용하겠노라고 OpenGL에게 알린다.

<12>에 나오는 함수인 glTexEnvi는 세개의 인자를 갖는데 중요한 것은 세번째 인자로써 올수있는 값은 GL_MODULATE와 GL_BLEND와 GL_DECAL이 올수있다. GL_MODULATE는 화면상의 픽셀 색과 텍스춰의 픽셀 색을 서로 곱하게 된다. <12>번 코드를 생략하면 자동으로 GL_MODULATE로 지정된다. GL_DECAL은 화면상의 픽셀 색을 텍스춰의 픽셀 색으로 대체한다. 이 경우 빛에 대해서 전혀 텍스쳐가 영향을 받지 않는다. GL_BLEND는 화면상의 픽셀 색을 텍스쳐의 픽색 색과 섞고 특정한 색과 결합시키게 된다.

자!! 이렇게 해서 텍스쳐 맵핑 소스를 만드는 것에 대한 것들을 알아 보았다. 그렇다면 이 코드들은 어디에 위치해야 하는가? 간단하게 OpenGL을 초기화는 곳에 위치하도록 하자. 즉 void InitGL(GLvoid) 함수에 위치 시키자.

이제 우리는 이 텍스쳐 맵핑 소스를 물체에 적용하는 방법에 대해 알아볼 차례이다. 적용은 실제 물체를 그리는 코드에서 이루어진다. 다음의 DrawGLScene 함수를들어다 보자.

int DrawGLScene(GLvoid)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
   
    glTranslatef(0.0f, 0.0f, -7.0f);
   
    glColor3f(0.8f, 0.8f, 0.8f);
   
    glRotatef(rot, 1.0f, 1.0f, 0.0f);
    glBindTexture(GL_TEXTURE_2D, tex[0]); //<1>
    glBegin(GL_QUADS);
    glNormal3f(0.0f, 0.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f); glVertex3f(1.0f, -1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f); glVertex3f(1.0f, 1.0f, 1.0f); 
    glEnd();
   
    glBindTexture(GL_TEXTURE_2D, tex[1]); //<1>
    glBegin(GL_QUADS);
    glNormal3f(1.0f, 0.0f, 0.0f);
    glTexCoord2f(0.0f, 0.0f); glVertex3f(1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(1.0f, -1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f); glVertex3f(1.0f, -1.0f, -1.0f);
    glTexCoord2f(1.0f, 0.0f); glVertex3f(1.0f, 1.0f, -1.0f);
    glEnd();
   
            .
            .
            .
   
    glBindTexture(GL_TEXTURE_2D, tex[0]); //<1>
    glBegin(GL_QUADS);
    glNormal3f(0.0f, 1.0f, 0.0f);
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f); glVertex3f(1.0f, 1.0f, 1.0f);
    glTexCoord2f(1.0f, 0.0f); glVertex3f(1.0f, 1.0f, -1.0f);
    glEnd();
   
    rot+=1.0f;
    if(rot>359.0f) rot=0.0f;
   
    return TRUE;
}

5장에서의 정육면체를 그릴때의 방법을 약간 변형했는데 5장에서는 한번의 glBegin과 glEnd를 사용하여 모든 면을 그려주었다. 하지만 이 장에서는 각각의 면에 대해서 glBegin와 glEnd를 사용하여 그려주었다. 그 이유는 각 면에 대해서 서로 다른 텍스쳐 맵핑을 시켜야 하기 때문이다. 살펴 볼것은 면에 대해서 어떤 텍스쳐 맵핑을 사용할 것인지는 앞서 언급한 텍스쳐 맵핑 식별자를 사용하는데 <1>번 코드가 바로 그것이다. 그리고 실제 텍스쳐 맵핑을 지정하는 것에 대한 것인데 위의 코드를 주의 깊게 살펴보면 면에 대한 각 점들을 지정해 줄때 glTexCoord2f라는 함수를 통해서 텍스쳐 맵핑 좌표를 같이 지정해 주었다는 점이다. 이것은 무엇을 의미하는가? 텍스쳐 맵핑의 소스는 크기에 상관없는 S-T 자표계를 사용하는데 S-T좌표계라는 시작점은 0으로 하고 끝점은 1로 한다는 점이다. 아래의 그림과 같이 말이다.

사용자 삽입 이미지

즉 glTexCoord2f를 이용하여 glVertex3f로 지정한 점에 대해 텍스쳐 맵핑 좌표를 설정해 줄때 glVertex3f로 지정한 위치에 대해 glTexCoord2f 좌표가 서로 일치시켜 그림을 맵핑하게 되는 것이다.

각각의 면들에 대해서 동일한 방법으로 텍스쳐 맵핑 좌표를 설정하였다.

이로써 텍스쳐 맵핑에 대한 것들을 마칠까 한다.

[OpenGL Tutorial] Lighting

사용자 삽입 이미지빛, 이 세상에 빛이 없다면 과연 아름다움이란 존재할까. 아마도 존재하지 않을 것이다. 이처럼 보이는 것에는 빛이란 것으로 하여금 아름다움이나 더 무었다운 것으로 보여지게 한다. 이처럼 중요한 빛의 사용을 OpenGL에서 알아보자. 빛에 대해 설명할 것들은 참으로 많다. 하나 하나 짚어보도록 하자.

색에는 빨강, 초록, 파랑이 기본 요소이듯 빛에도 3가지의 기본 요소가 있다. 그것은 주변광(Ambient), 확산광(Diffuse), 반사광(Specular)이다. 이 세가지에 대한 설명은 OpenGL Super Bible(page 268)에 나오는 내용을 그대로 옮기니 잘 읽어두길 바란다.

Ambient Light
특정한 방향이에서 오는 빛이 아니다. 빛의 근원은 있지만 방 혹은 장면 주위에서 반사되어 들어오기 때문에 방향이 없는 광선이다. Ambient light에 의해서 조명되는 물체는 전체적으로 모든 방향으로부터 빛이 비추어진다. 물체를 회전시켜도 Ambient Light에 의해서 색이 변하지 않는다. 즉, 방향을 갖고 있지 않은 Ambient Light가 그 물체를 비추고 있었기 때문에, 어느 각도에서도 같은 색으로 보여지는 것이다.

Diffuse Light
특정한 방향으로 비춰지지만, 그 빛의 반사는 여러 방향으로 이루어진다. 빛이 골고루 반사된다 하더라도, 물체를 비스듬히 비출 때보다 바로 위에서 비출 때가 물체 표면을 더 밝게 보인다. 예로써 형광등의 빛이나 오후에 창을 통해 들어오는 햇빛을 생각하면 된다.

Specular Light
역시 방향을 가지는 빛이다. 하지만 사방으로 밫을 반사시키는 Diffuse Light와는 달리 특정 방향으로만 뚜렷하게 빛을 반사시킨다. 강한 Specular Light이 물체에 닿으면 그 표면에 Spot가 생기는데 이것을 Specular Highlight라고 한다.

자신이 원하는 빛의 값을 얻는데는 정확한 공식이 없다. 많은 예를 접해봄으로써 느낌으로 익혀야 한다. 과학적 기술에 예술적 감각이 필요한 것이다. 빛을 사용하려면 몇가지 준비해야할 것들이 많다. 첫째로 빛을 비추는 조명이 필요하다. 둘째로 물체가 필요한다. 셋째로 빛이 물체에 비칠때 반사각을 결정한 법선벡터가 필요하다. 넷째로 물체의 재질이 필요한데 같은 빛이라도 어떤 재질의 물체가 빛을 받느냐에 따라 다르기 때문이다. 이 네가지를 하나 하나 익혀보도록하자.

1. 빛을 비추는 조명(Lighting Model Setting)

OpenGL에는 최대 9개의 독립적이 광원을 설정하여 사용할 수 있으며 기본적으로 디폴트 광원이 하나 준비되어있다. 하지만 일반적으로 디폴트 광원은 건드리지 않고 독립적인 광원을 설정하여 사용하는데 그 이유는 독립적인 광원은 보다 더 많은 설정 사항이 있어 프로그래머의 입맛에 맛는 광원을 얻을 수 있기 때문이다. 독립적인 광원의 이름은 GL_LIGHT0, GL_LIGHT1, GL_LIGHT2, … 이다. 그럼 이 독립적인 광원들이 가지는 속성 값들에는 어떤 것들이 있을까? 먼저 위에서 말한 세가지 Ambient, Diffuse, Specular Light값이 있다. 그리고 빛의 위치. 빛이 스포트 라이트이라면 그에 해당하는 스포트 라이트 값 등이다. 여그서는 빛에 대한 모든 것을 다루지 못하고(현재 필자의 지식의 한계) Ambient, Diffuse, Specular Light와 빛의 위치들 만으로 빛을 생성하여 보겠다. 특별한 조명 장치가 아닌 실세계의 모든 빛은 이 장에서 언급하는 것들만으로 충분이 가능하다고 생각한다. 필자의 능력이 확장되는대로 소개할 것을 약속한다. 사실 그 외의 빛의 속성값에 대한 것을 실제로 사용해 보았으나 눈으로 분명하게 느끼지 못해 소개하기가 껄끄럽다. 자! 그럼 본격적인 빛의 속성값의 설정과 빛의 사용을 지시하는 예를 들어보자.

하나의 가정을 하지 저기 높은 어느 특정한 곳에 전체적으로 노란빛을 띠는 빛이 있다고 해보자. 그렇다면 하얀색의 물체는 그 노란색의 빛을 받아 노란색으로 보일 것이다. 그렇다면 노란빛을 띠는 빛의 속성값은 어떻게 정의하는가? 정의하는 코드는 다음과 같다.

GLfloat ambientLight[] = { 0.25f, 0.25f, 0.0f, 1.0f };     // <1>
GLfloat diffuseLight[] = { 0.9f, 0.9f, 0.0f, 1.0f };       // <2>
GLfloat lightPos[] = { -100.0f, 130.0f, 150.0f, 1.0f };    // <3>
   
glEnable(GL_LIGHTING);                                     // <4>
glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);            // <5>
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);            // <6>
glLightfv(GL_LIGHT0, GL_POSITION, lightPos);               // <7>
glEnable(GL_LIGHT0);                                       // <8>

1번 코드는 ambient light값을 설정하기 위한 변수이고 2번 코드는 diffuse light값을 설정하기 위한 변수이다. 노란색을 띠는 빛이므로 Ambient은 약한 노란색이고 Diffuse는 조금 강한 노란색의 속성값을 저장하기 위한 코드이다. 3번 코드는 빛의 위치값을 저장하기 위한 변수이다. 4번 코드는 우리가 OpenGL에서 빛을 사용하겠다는 것이다. 5번과 6번, 7번은 빛의 각각의 속성값을 실제로 설정하기 위한 것인데 모두 GL_LIGHT0라는 빛의 속성을 지정한 것이다. 결국 8번 코드에서 앞에서 GL_LIGHT0의 빛을 사용하겠다고 지정함으로써 기본적인 빛의 값은 설정되고 빛이 켜지는 것이다. 그런데 한가지 우리가 알아보지 못한 빛의 속성이 있다. 그것은 바로 Specular Light 속성인데 빛을 정면에서 받을 경우 정면에서 받은 물체의 면이 띠게될 색을 저정해 주는 것이다. 일반적으로 빛을 정면에서 받으면 순백색을 띠게 되는데 대부분 하얀색을 이 속성값으로 지정하게 된다. 즉 다음과 같다.

GLfloat specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
glLightfv(GL_LIGHT0, GL_SPECULAR, specular);

이것으로 빛의 속성값을 설정하는 것에 대해서 마치도록 하고 이제 물체를 만들어 보자.

2. 빛을 받는 물체 그리고 면

빛과 물체에서 중요한 것은 바로 면에 있다. 즉 빛이 물체의 면을 비출때 어떻게 되는가 하느냐에 따라 다른 장면을 만들어 낸다. 우리가 알고 있듯 면에 빛이 비칠때 수직으로 비치는 경우가 아닌라 조금 경사지게 비친다고 해보자. 면에 빛이 들어올때의 각을 입사각이라고 하고 나갈때의 빛의 각도를 반사각이라고 하면 면을 기준으로 서로 입사각과 반사각은 같다. 그렇다면 OpenGL은 그 입사각과 반사각을 어떤식으로 알수있을까? 그것은 법선을 이용하는 것이다. 즉 면에 법선을 정의해줌으로써 빛이 어느 각도로 입사하고 또 반사할지를 결정하는 것이다. 사실 법선은 면에 할당되는것이 아니라 하나의 점에 하나 이상 정의될수있다. 그러나 면을 이루는 각 점이 같은 법선백터를 가진다면 그 면은 하나의 법선백터를 가진다고 이해할 수가 있다. 즉 우리가 물체를 정의해줄때 각점에 대해서 법선 백터를 정의줘야 우리가 원하는 빛의 효과를 기대할 수 있다. 글로써 이해가 되지 않는 독자를 위해 다음 그림을 제시한다.

사용자 삽입 이미지

다음으로 면에는 앞과 뒤가 있다. 빛을 고려한다면 면의 앞면과 뒷면을 분명하게 지정해 줘야 한다. 앞면과 뒷면은 어떤식으로 결정하는가? 면은 폴리곤이다. 폴리곤은 각 점들로 이루어진 폐각형인데 이 점들을 지정하는 방향으로 앞면과 뒷면을 결정하게 된다. OpenGL은 기본적으로 반시계방향으로 면을 지정할 경우 앞면이고 그 면의 다른 방향의 면이 뒷면이 된다. 그러면 왜 OpenGL은 앞면과 뒷면을 구분하는가? 그냥 면이면 충분하지 않았을까? 그것은 꼭 필요한 연산만을 위한 것이다. 예를 들어 정육면체에서 안쪽의 면들은 연산에서 고려할 필요가 없다. 왜냐하면 볼 필요가 없기 때문이다. 즉 불필요한 계산을 피하고자 앞면과 뒷면을 고려한 것이다. 또 하나의 이유는 앞면과 뒷면이 서로 다른 재질로 이루어졌을 경우의 구현을 위해서이다.

정리를 해보자. 빛을 받는 물체에 있어서 우리가 고려해야할 것은 바로 법선백터와 앞면과 뒷면을 정확히 구분하여야 한다는 점이다. 다음의 코드는 그 두가지를 분명하게 고려한 것이다.

 glBegin(GL_QUADS);
 glNormal3f(0.0f, 0.0f, 1.0f);   // <1>
 glVertex3f(1.0f, 1.0f, 1.0f);   // <2>
 glVertex3f(-1.0f, 1.0f, 1.0f);  // <2>
 glVertex3f(-1.0f, -1.0f, 1.0f); // <2>
 glVertex3f(1.0f, -1.0f, 1.0f);  // <2>
   
 glNormal3f(1.0f, 0.0f, 0.0f);
 glVertex3f(1.0f, 1.0f, 1.0f);
 glVertex3f(1.0f, -1.0f, 1.0f);
 glVertex3f(1.0f, -1.0f, -1.0f);
 glVertex3f(1.0f, 1.0f, -1.0f);
   
 glNormal3f(0.0f, 0.0f, -1.0f);
 glVertex3f(1.0f, 1.0f, -1.0f);
 glVertex3f(1.0f, -1.0f, -1.0f);
 glVertex3f(-1.0f, -1.0f, -1.0f);
 glVertex3f(-1.0f, 1.0f, -1.0f);
   
 glNormal3f(-1.0f, 0.0f, 0.0f);
 glVertex3f(-1.0f, 1.0f, -1.0f);
 glVertex3f(-1.0f, -1.0f, -1.0f);
 glVertex3f(-1.0f, -1.0f, 1.0f);
 glVertex3f(-1.0f, 1.0f, 1.0f);
   
 glNormal3f(0.0f, -1.0f, 0.0f);
 glVertex3f(1.0f, -1.0f, -1.0f);
 glVertex3f(1.0f, -1.0f, 1.0f);
 glVertex3f(-1.0f, -1.0f, 1.0f);
 glVertex3f(-1.0f, -1.0f, -1.0f);
   
 glNormal3f(0.0f, 1.0f, 0.0f);
 glVertex3f(-1.0f, 1.0f, -1.0f);
 glVertex3f(-1.0f, 1.0f, 1.0f);
 glVertex3f(1.0f, 1.0f, 1.0f);
 glVertex3f(1.0f, 1.0f, -1.0f);
 glEnd();

<1>코드가 바로 법선 벡터를 지정한 것인데 시작점은 항상 원점이다. 즉 <1>은 (0,0,0)에서 (0,0,1)까지의 백터이다. OpenGL의 성능향상을 위해 법선백터는 항상 크기가 1인 단위벡터를 사용한다. 한번 법선 백터를 지정하면 다음으로 지정되는 좌표들은 그 법선벡터를 갖게 된다. <2>코드들은 면을 구성하는 것인데 앞면이라는 것을 나타내기 위해 그 지정순서가 반시계방향이다. 나머지 면들에 대해서도 모두 마찬가지이다.

3. 물체의 재질의 설정(Material Setting)

물체가 준비되었으나 아직 빛을 받아들이기에는 부족하다. 왜냐하면 같은 모양의 물체라도 나무냐 금속이냐에 따라서 빛이 비치는 모습이 다르기 때문이다. 즉 물체의 재질이 따라 다르다는 말이다. 물체의 재질을 설정하는 방법에 대해 알아도록하자.
물체가 재질로는 어떤 속성들이 있을까? Ambient, Diffuse, Specualr, Emission, Shiness가 있다. 쉽고 익숙한 말로 풀어보면 Ambient는 물체가 은은하게 나타내는 색이고 Diffuse는 물체의 주된 색상이다. Specular은 물체가 빛에 정면으로 비칠때 나타내는 색이고 Emisson은 조금 다른 것으로 발광색이다. Shiness는 빛나는 정도를 의미한다. 물체의 대부분의 경우 Ambient와 Diffuse는 같은 값을 갖으며 빛을 정면으로 받을때의 색은 순백색이다. Shiness는 Spot Highlight의 크기를 나타내는데 그 값은 0~128사이로 커질수록 그 Spot Highlight의 크기가 작아지면서 더욱 진하게 된다. 그럼 예를 들어보자. Shiness가 10정도이고 조금 밝은 회색의 물체인데 빛을 정면으로 받으면 하얀색인 물체의 재질을 설정하는 것을 코드로 예를 들어보자.

GLfloat gray[] = {0.75f, 0.75f, 0.75f };
GLfloat specular[] = {1.0f, 1.0f, 1.0f, 1.0f}

glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, gray);
glMaterialfv(GL_FRONT, GL_SPECULAR, specref);
glMateriali(GL_FRONT, GL_SHININESS, 10);

이렇게 재질이 지정되면 다음에 만들어지는 모든 물체는 앞에서 설정된 재질로 지정이 된게 된다. 또다른 간단한 재질 설정법이 있는데 그것은 우리가 많이 사용하는 glColor3f 함수를 이용하는 것이다. 이것으로 간단하게 Ambient와 Diffuse를 설정할 수 있다. 위의 코드와 동일한 일을 하는 코드를 다시 예로 들어보면 다음과 같다.

GLfloat specular[] = {1.0f, 1.0f, 1.0f, 1.0f}

glEnable(GL_COLOR_MATERIAL);
glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
glColor4f(0.75f, 0.75f, 0.75f, 1.0f);
glMaterialfv(GL_FRONT, GL_SPECULAR, specref);
glMateriali(GL_FRONT, GL_SHININESS, 10);

한가지 주목할만한 것으로 GL_FRONT라는 인자인데 상반되는 인자로 GL_BACK이다. 이것을 이용하면 면의 앞면과 뒷면의 재질을 서로 다르게 설정할 수 있다. 두면을 동시에 같게 설정할 수 있는 인자로 GL_FRONT_AND_BACK이다.

이제 최종적으로 전체적인 소스 코드로 이 장을 정리하도록 하겠다. 사용하는 코드는 1장의 소스 코드이다.

먼저 물체를 회전시키는데 필요한 변수이다 전역 변수를 선언하는 곳에 선언하자.

GLfloat rot = 0.0f;

빛을 설정하고 재질을 설정하는 코드가 들어갈 함수는 initGL 정도가 알맞겠다.

int InitGL(GLvoid) // All Setup For OpenGL Goes Here
{
    GLfloat ambientLight[] = { 0.25f, 0.25f, 0.0f, 1.0f };
    GLfloat diffuseLight[] = { 0.9f, 0.9f, 0.0f, 1.0f };
    GLfloat lightPos[] = { -100.0f, 130.0f, 150.0f, 1.0f };
    GLfloat specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
    GLfloat specref[] = { 1.0f, 1.0f, 1.0f, 1.0f };
   
    glShadeModel(GL_SMOOTH); // Enable Smooth Shading
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Black Background
    glClearDepth(1.0f); // Depth Buffer Setup
    glEnable(GL_DEPTH_TEST); // Enables Depth Testing
    glEnable(GL_CULL_FACE); // 뒷면에 대해서는 계산하지 말라
    glFrontFace(GL_CCW);   // 시계방향이 앞면이다.
    glEnable(GL_LIGHTING); // 빛을 사용한다.
   
    glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
    glLightfv(GL_LIGHT0, GL_POSITION, lightPos);
    glLightfv(GL_LIGHT0, GL_SPECULAR, specular);
    glEnable(GL_LIGHT0); // 0번빛을 사용한다.
   
    glEnable(GL_COLOR_MATERIAL); 
    glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
    glMaterialfv(GL_FRONT, GL_SPECULAR, specref);
    glMateriali(GL_FRONT, GL_SHININESS, 10);
   
    glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do
    // Really Nice Perspective    Calculations
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); 
   
    return TRUE; // Initialization Went OK
} 

실제로 물체를 그려주는 코드는 DrawScean 정도 알맞겠다.

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, -7.0f);
   
    glColor3f(0.95f, 0.95f, 0.95f);
   
    glRotatef(rot, 1.0f, 1.0f, 1.0f);
    glBegin(GL_QUADS);
    glNormal3f(0.0f, 0.0f, 1.0f);
    glVertex3f(1.0f, 1.0f, 1.0f);
    glVertex3f(-1.0f, 1.0f, 1.0f);
    glVertex3f(-1.0f, -1.0f, 1.0f);
    glVertex3f(1.0f, -1.0f, 1.0f); 
   
    glNormal3f(1.0f, 0.0f, 0.0f);
    glVertex3f(1.0f, 1.0f, 1.0f);
    glVertex3f(1.0f, -1.0f, 1.0f);
    glVertex3f(1.0f, -1.0f, -1.0f);
    glVertex3f(1.0f, 1.0f, -1.0f);
   
    glNormal3f(0.0f, 0.0f, -1.0f);
    glVertex3f(1.0f, 1.0f, -1.0f);
    glVertex3f(1.0f, -1.0f, -1.0f);
    glVertex3f(-1.0f, -1.0f, -1.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);
  
    glNormal3f(-1.0f, 0.0f, 0.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);
    glVertex3f(-1.0f, -1.0f, -1.0f);
    glVertex3f(-1.0f, -1.0f, 1.0f);
    glVertex3f(-1.0f, 1.0f, 1.0f);
   
    glNormal3f(0.0f, -1.0f, 0.0f);
    glVertex3f(1.0f, -1.0f, -1.0f);
    glVertex3f(1.0f, -1.0f, 1.0f);
    glVertex3f(-1.0f, -1.0f, 1.0f);
    glVertex3f(-1.0f, -1.0f, -1.0f);
  
    glNormal3f(0.0f, 1.0f, 0.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);
    glVertex3f(-1.0f, 1.0f, 1.0f);
    glVertex3f(1.0f, 1.0f, 1.0f);
    glVertex3f(1.0f, 1.0f, -1.0f);
    glEnd();
   
    rot+=1.0f;
    if(rot>359.0f) rot=1.0f;
   
    return TRUE;
}

실제 실행되는 화면은 다음과 같다.

사용자 삽입 이미지

어떤가? 밝은 회색의 물체가 노란 불빛때문에 전체적으로 노란색을 띠게 되는데 물체가 회전할때마다 빛의 영향으로 현실감있게 색이 변한다.