E D R S I H C RSS
ID
Password
Join
과거의 연극은 인생이 송두리째 비쳐진 거울이지만, 오늘의 연극은 인생을 들여다보는 열쇠구멍. -A.H.G.

 * 원문링크 : [http]http://nehe.gamedev.net/tutorials/lesson.asp?l=37
  • 원저자 : Sami "MENTAL" Hamlaoui
  • 소스코드 : (위 링크에서 받으세요~)

얼마전에 Gamedev.net에 적은 아티클의 소스코드를 요청하는 이메일과 반쪽짜리 미완성으로 끝내지 않은 아티클의 두번째 버젼(소스코드와 모든 API가 포함된)을 보고, 나는 이 Nehe 튜토리얼을 파헤쳤다. (이것은 원본의 강화판이라고 할 수 있다.) 이로서 Opengl 전문가들이 이것을 다룰수 있을 거라 생각한다. 모델의 선택에 대해서는 양해를 구한다. 하지만 나는 요근래 널리 알려진 퀘이크 2를 아직까지도 플레이하고 있다. :)

주의 : 이 코드의 원본 아티클은 여기서 얻을 수 있다. [http]http://www.gamedev.net/reference/programming/features/celshading -> 번역은 여기 : Cel-Shading

이 튜토리얼은 원리를 실제로 설명하지는 않고 그저 코드만 제시한다. 왜 이것이 동작하는지에 대해서는 위 링크를 참조해라. 이제 소스코드를 달라는 이메일은 제발 그만했으면 좋겠다.

즐겨라. :)

가장 처음, 우리는 몇가지 추가적인 헤더화일을 첨부해야한다. math.h는 sqrtf()함수(제곱근)를 사용하기 위해서이고, stdio.h는 화일 입출력을 위해서이다.

#include <math.h> // Math Library를 위한 헤더화일
#include <stdio.h> // 표준 입출력 헤더화일

이제 우리는 우리의 데이타를 저장하는 것을 돕기위한 몇가지 구조체를 정의하려고 한다. (수백개의 float 배열들을 가지는 것을 절약해준다.) 첫번째는 tagMATRIX 구조체이다. 근시안적으로 보면, 2차원 4*4 배열이 아닌 16개의 1차원 배열을 담고 있는 것으로 볼 수 있다. 이것은 Opengl이 행렬을 어떻게 저장하고 있는지에 따른 것이다. 우리가 4*4를 사용하면, Opengl에서는 틀린 순서로 그 데이타를 받아들일 것이다.

typedef struct tagMATRIX // OpenGL 행렬을 담고 있는 구조체
{
float Data[16]; // OpenGL 행렬 포멧에 따른 정적 배열 [16]을 사용한다.
}
MATRIX;

두번째는 벡터 클래스이다. 이것은 단순히 x, y, z값들을 담고 있다.

typedef struct tagVECTOR // 단순 벡터를 담고 있는 구조체
{
float X, Y, Z; // 벡터의 요소들
}
VECTOR;

세번째는 정점 구조체이다. 각 정점은 단지 노멀 벡터와 위치만을 필요로한다. (텍스쳐 좌표는 필요없다) 이들은 반드시 이 순서대로 저장되어있어야만 한다. 그렇게 하지 않으면 화일로부터 데이타를 로딩할때 무시무시하게 틀려버리는 것을 경험할 수 있을 것이다.

typedef struct tagVERTEX // 단순 정점을 담고 있는 구조체
{
VECTOR Nor; // 정점의 노멀벡터
VECTOR Pos; // 정점 위치
}
VERTEX;

마지막으로 폴리곤 구조체이다. 나는 이것이 정점들을 저장하는데 있어 어리석은 방법임을 알고 있다. 하지만 단순하다는 측면에서 보자면 이것은 완벽하게 동작한다. 일반적으로 나는 정점배열과 폴리곤 배열(폴리곤 구조체내에 3개의 정점들에 대한 인덱스를 담고 있는 배열)을 사용하곤 한다. 그러나 이 방법은 동작하는 내용을 이해하는데는 더 쉽다.

typedef struct tagPOLYGON // 간단한 폴리곤을 담고 있는 구조체
{
VERTEX Verts[3]; // 3개의 정점 구조체들의 배열
}
POLYGON;

역시 여기서는 매우 간단한 것들을 나열하겠다. 각 변수들의 설명을 위해 주석을 참조해라.

bool outlineDraw = true;                         // 외곽선을 그릴것인지 여부
bool outlineSmooth = false;                      // 선의 안티알리아싱을 할 것인지 여부
float outlineColor[3] = { 0.0f, 0.0f, 0.0f };    // 선의 색상
float outlineWidth = 3.0f;                       // 선의 폭

VECTOR lightAngle;                               // 빛의 방향
bool lightRotate = false;                        // 빛을 회전할 것인지 여부

float modelAngle = 0.0f;                         // 모델의 y축 각도
bool modelRotate = false;                        // 모델을 회전시킬 것인지 여부

POLYGON *polyData = NULL;                        // 폴리곤 데이타
int polyNum = 0;                                 // 폴리곤들의 수

GLuint shaderTexture[1];                         // 하나의 텍스쳐를 위한 저장소

다음 코드는 매우 간단한 모델 화일 포멧을 얻는 법이다. 첫번째 몇개의 바이트들은 장면내에서의 폴리곤의 수를 담고 있고, 화일의 나머지는 tagPOLYGON 구조체의 배열을 담고 있다. 이것 덕분에 데이타는 특별한 순서로 정렬하는 것 없이 그냥 읽어들이는 것이 가능하다.

BOOL ReadMesh () // "model.txt"화일의 내용을 읽어들인다.
{
   FILE *In = fopen ("Data\\model.txt", "rb");               // 화일을 연다.
   if (!In) return FALSE;                                    // 열지 못했다면 FALSE 반환
   fread (&polyNum, sizeof (int), 1, In);                    // 헤더를 읽는다. (다시말하면, 폴리곤의 수를 읽어들인다.)
   polyData = new POLYGON [polyNum];                         // 메모리를 할당한다.
   fread (&polyData[0], sizeof(POLYGON) * polyNum, 1, In);   // 모든 폴리곤 데이타를 읽어들인다.
   fclose (In);                                              // 화일을 닫는다.
   return TRUE;                                              // 이제 다른 일을 한다.
}

몇가지 기본적인 삼각함수가 여기 있다. DotProduct() 함수는 2개의 백터 또는 평면사이의 각을 구하는 데 사용된다. (내적을 산출한다) Magnitude() 함수는 벡터의 길이를 구한다. Normalize() 함수는 벡터의 길이를 1로 감소시킨다.

inline float DotProduct (VECTOR &V1, VECTOR &V2)         // 두개의 벡터사이의 각을 산출한다
{
    return V1.X * V2.X + V1.Y * V2.Y + V1.Z * V2.Z;
}

inline float Magnitude (VECTOR &V)                       // 벡터의 길이를 구한다.
{
    return sqrtf (V.X * V.X + V.Y * V.Y + V.Z * V.Z);
}

void Normalize (VECTOR &V)                               // 길이가 1인 벡터를 생성한다.
{
    float M = Magnitude (V);                             // 벡터의 길이를 산출한다.
    if (M != 0.0f)                                       // 0으로 나누면 안되므로 검사한다. 
    {
        V.X /= M;                                        // 각각의 요소들을 노멀처리한다. 
        V.Y /= M;
        V.Z /= M;
    }
}

이 함수는 제공되는 행렬을 사용하여 벡터를 회전시킨다. 벡터를 단지 회전시키는 것이라는 데 주의하기 바란다 - 이것은 벡터의 위치에는 전혀 아무런 일도 하지 않는다. 이것은 빛을 계산할 때 알맞은 방향을 향하고 있다는 것을 확실하게 하기위한 노멀 벡터의 회전을 실행할때 사용된다.

void RotateVector (MATRIX &M, VECTOR &V, VECTOR &D)                    // 제공된 행렬을 사용하여 벡터를 회전시킨다.
{
    D.X = (M.Data[0] * V.X) + (M.Data[4] * V.Y) + (M.Data[8] * V.Z);   // x 축을 기준으로 회전
    D.Y = (M.Data[1] * V.X) + (M.Data[5] * V.Y) + (M.Data[9] * V.Z);   // y 축을 기준으로 회전
    D.Z = (M.Data[2] * V.X) + (M.Data[6] * V.Y) + (M.Data[10] * V.Z);  // z 축을 기준으로 회전
}

엔진의 맨 처음 주요함수인 Initialize()는 말그대로 초기화를 수행한다. 설명에 불필요한 몇 줄의 소스라인은 생략했음을 밝힌다.

// GL을 초기화하거나 사용자 초기화 코드는 여기에서 실행된다.
BOOL Initialize (GL_Window* window, Keys* keys)
{

이 3개의 변수들은 셰이더 화일을 로딩하기위해서 사용된다. shaderData가 실제 셰이더 값들을 저장하는 동안, Line은 텍스트 화일안의 한개의 선을 위한 공간을 담고 있게 된다. 32대신에 왜 96개의 값을 가지고 있는지 의아해할지도 모르겠다. 자, 우리는 그레이스케일 값을 RGB값으로 변환할 필요가 있다. (그래야 Opengl에서 그것들을 사용할 수 있으니까) 우리는 아직도 그레이스케일 값으로서 값을 담고 있지만, 텍스쳐에 업로딩할때에는 RGB 형태로 있어야하므로 각 RGB별로 같은 값을 사용할 것이다.

    char Line[255];           // 255 문자를 저장하기 위한 배열
    float shaderData[32][3];  // 96개의 셰이더 값을 저장한다.

    FILE *In = NULL;          // 화일 포인터

선을 그릴때 우리는 괜찮고 부드럽게 그려지길 원할 것이다. 이 값들은 초기에는 꺼져있지만, 예제에서는 "2" 키를 누르면 이 요소를 켜고 끌수 있다.

    glShadeModel (GL_SMOOTH);   // 부드러운(Smooth) 컬러 셰이딩을 가능하게 설정한다.
    glDisable (GL_LINE_SMOOTH); // Line Smoothing을 끈다.

    glEnable (GL_CULL_FACE);    // OpenGL 은면제거(Face Culling)을 켠다.

Opengl 광원(lighting)은 끈다. 왜냐하면 우리는 광원 연산을 스스로 할 것이기 때문이다.

    glDisable (GL_LIGHTING);    // Opengl 광원을 끈다.

여기서 우리는 셰이더 화일을 로딩한다. 그것은 ASCII 형태로 담겨져있는 32개의 float형 값들이다. (그러므로 쉽게 수정이 가능하다) 그리고 각 값은 별도의 줄에 적혀있다.

    In = fopen ("Data\\shader.txt", "r");    // 셰이더 화일을 연다.

    if (In) // Check To See If The File Opened
    {
        for (i = 0; i < 32; i++) // Loop Though The 32 Greyscale Values
        {
            if (feof (In)) // Check For The End Of The File
                break;

            fgets (Line, 255, In); // Get The Current Line

Here we convert the greyscale value into RGB, as described above.

            // Copy Over The Value
            shaderData[i][0] = shaderData[i][1] = shaderData[i][2] = atof (Line);
        }

        fclose (In); // Close The File
    }
    else
        return FALSE; // It Went Horribly Horribly Wrong

Now we upload the texture. At it clearly states, do not use any kind of filtering on the texture or else it will look odd, to say the least. GL_TEXTURE_1D is used because it is a 1D array of values.
    glGenTextures (1, &shaderTexture[0]); // Get A Free Texture ID

    glBindTexture (GL_TEXTURE_1D, shaderTexture[0]); // Bind This Texture. From Now On It Will Be 1D

    // For Crying Out Loud Don't Let OpenGL Use Bi/Trilinear Filtering!
    glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 
    glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

    // Upload
    glTexImage1D (GL_TEXTURE_1D, 0, GL_RGB, 32, 0, GL_RGB , GL_FLOAT, shaderData);

Now set the lighting direction. I've got it pointing down positive Z, which means it's going to hit the model face-on.

    lightAngle.X = 0.0f; // Set The X Direction
    lightAngle.Y = 0.0f; // Set The Y Direction
    lightAngle.Z = 1.0f; // Set The Z Direction

    Normalize (lightAngle); // Normalize The Light Direction

Load in the mesh from file (described above).

    return ReadMesh (); // Return The Value Of ReadMesh
}

The opposite of the above function, Deinitalize deletes the texture and polygon data created by Initalize and ReadMesh.

void Deinitialize (void) // Any User DeInitialization Goes Here
{
    glDeleteTextures (1, &shaderTexture[0]); // Delete The Shader Texture

    delete [] polyData; // Delete The Polygon Data
}

The main demo loop. All this does is process the input and update the angle. Controls are as follows:

  • <SPACE> = Toggle rotation
  • 1 = Toggle outline drawing
  • 2 = Toggle outline anti-alaising
  • <UP> = Increase line width
  • <DOWM> = Decrease line width

void Update (DWORD milliseconds) // Perform Motion Updates Here
{
    if (g_keys->keyDown [' '] == TRUE) // Is the Space Bar Being Pressed?
    {
        modelRotate = !modelRotate; // Toggle Model Rotation On/Off
        g_keys->keyDown [' '] = FALSE;
    }

    if (g_keys->keyDown ['1'] == TRUE) // Is The Number 1 Being Pressed?
    {
        outlineDraw = !outlineDraw; // Toggle Outline Drawing On/Off
        g_keys->keyDown ['1'] = FALSE;
    }

    if (g_keys->keyDown ['2'] == TRUE) // Is The Number 2 Being Pressed?
    {
        outlineSmooth = !outlineSmooth; // Toggle Anti-Aliasing On/Off
        g_keys->keyDown ['2'] = FALSE;
    }

    if (g_keys->keyDown [VK_UP] == TRUE) // Is The Up Arrow Being Pressed?
    {
        outlineWidth++; // Increase Line Width
        g_keys->keyDown [VK_UP] = FALSE;
    }

    if (g_keys->keyDown [VK_DOWN] == TRUE) // Is The Down Arrow Being Pressed?
    {
        outlineWidth--; // Decrease Line Width
        g_keys->keyDown [VK_DOWN] = FALSE;
    }

    if (modelRotate) // Check To See If Rotation Is Enabled
        modelAngle += (float) (milliseconds) / 10.0f; // Update Angle Based On The Clock
}

The function you've all been waiting for. The Draw function does everything - calculates the shade values, renders the mesh, renders the outline, and, well that's it really.

void Draw (void)
{

TmpShade is used to store the shader value for the current vertex. All vertex data is calculate at the same time, meaning that we only need to use a single variable that we can just keep reusing.

The TmpMatrix, TmpVector and TmpNormal structures are also used to calculate the vertex data. TmpMatrix is set once at the start of the function and never changed until Draw is called again. TmpVector and TmpNormal on the other hand, change when another vertex is processed.

    float TmpShade; // Temporary Shader Value

    MATRIX TmpMatrix; // Temporary MATRIX Structure
    VECTOR TmpVector, TmpNormal; // Temporary VECTOR Structures

Lets clear the buffers and matrix data.

    glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Buffers
    glLoadIdentity (); // Reset The Matrix

The first check is to see if we want to have smooth outlines. If so, then we turn on anti-alaising. If not, we turn it off. Simple!

    if (outlineSmooth) // Check To See If We Want Anti-Aliased Lines
    {
        glHint (GL_LINE_SMOOTH_HINT, GL_NICEST); // Use The Good Calculations
        glEnable (GL_LINE_SMOOTH); // Enable Anti-Aliasing
    }
    else // We Don't Want Smooth Lines
        glDisable (GL_LINE_SMOOTH); // Disable Anti-Aliasing

We then setup the viewport. We move the camera back 2 units, and then rotate the model by the angle. Note: because we moved the camera first, the model will rotate on the spot. If we did it the other way around, the model would rotate around the camera.

We then grab the newly created matrix from OpenGL and store it in TmpMatrix.

    glTranslatef (0.0f, 0.0f, -2.0f); // Move 2 Units Away From The Screen
    glRotatef (modelAngle, 0.0f, 1.0f, 0.0f); // Rotate The Model On It's Y-Axis

    glGetFloatv (GL_MODELVIEW_MATRIX, TmpMatrix.Data); // Get The Generated Matrix

The magic begins. We first enable 1D texturing, and then enable the shader texture. This is to be used as a look-up table by OpenGL. We then set the color of the model (white). I chose white because it shows up the highlights and shading much better then other colors. I suggest that you don't use black :)

    // Cel-Shading Code
    glEnable (GL_TEXTURE_1D); // Enable 1D Texturing
    glBindTexture (GL_TEXTURE_1D, shaderTexture[0]); // Bind Our Texture

    glColor3f (1.0f, 1.0f, 1.0f); // Set The Color Of The Model

Now we start drawing the triangles. We look though each polygon in the array, and then in turn each of it's vertexes. The first step is to copy the normal information into a temporary structure. This is so we can rotate the normals, but still keep the original values preserved (no precision degradation).

    glBegin (GL_TRIANGLES); // Tell OpenGL That We're Drawing Triangles
    for (i = 0; i < polyNum; i++) // Loop Through Each Polygon
    {
        for (j = 0; j < 3; j++) // Loop Through Each Vertex
        {
            TmpNormal.X = polyData[i].Verts[j].Nor.X; // Fill Up The TmpNormal Structure With The
            TmpNormal.Y = polyData[i].Verts[j].Nor.Y; // Current Vertices' Normal Values
            TmpNormal.Z = polyData[i].Verts[j].Nor.Z;

Second, we rotate the normal by the matrix grabbed from OpenGL earlier. We then normalize this so it doesn't go all screwy.
            // Rotate This By The Matrix
            RotateVector (TmpMatrix, TmpNormal, TmpVector);

            Normalize (TmpVector); // Normalize The New Normal

Third, we get the dot product of the rotated normal and light direction (called lightAngle, because I forgot to change it from my old light class). We then clamp the value to the range 0-1 (from -1 to +1).

            // Calculate The Shade Value
            TmpShade = DotProduct (TmpVector, lightAngle);

            if (TmpShade < 0.0f)
                TmpShade = 0.0f; // Clamp The Value to 0 If Negative

Forth, we pass this value to OpenGL as the texture co-ordinate. The shader texture acts as a lookup table (the shader value being the index), which is (i think) the main reason why 1D textures were invented. We then pass the vertex's position to OpenGL, and repeat. And Repeat. And Repeat. And I think you get the idea.

            glTexCoord1f (TmpShade); // Set The Texture Co-ordinate As The Shade Value
            // Send The Vertex Position
            glVertex3fv (&polyData[i].Verts[j].Pos.X);
        }
    }

    glEnd (); // Tell OpenGL To Finish Drawing

    glDisable (GL_TEXTURE_1D); // Disable 1D Textures

Now we move onto the outlines. An outline can be defined as "an edge where one polygon is front facing, and the other is backfacing". In OpenGL, it's where the depth test is set to less than or equal to (GL_LEQUAL) the current value, and when all front faces are being culled. We also blend the lines in, to make it look nice :)

So, we enable blending and set the blend mode. We tell OpenGL to render backfacing polygons as lines, and set the width of those lines. We cull all front facing polygons, and set the depth test to less than or equal to the current Z value. After this the color of the line is set, and we loop though each polygon, drawing its vertices. We only need to pass the vertex position, and not the normal or shade value because all we want is an outline.

    // Outline Code
    if (outlineDraw) // Check To See If We Want To Draw The Outline
    {
        glEnable (GL_BLEND); // Enable Blending
        // Set The Blend Mode 
        glBlendFunc (GL_SRC_ALPHA ,GL_ONE_MINUS_SRC_ALPHA);

        glPolygonMode (GL_BACK, GL_LINE); // Draw Backfacing Polygons As Wireframes
        glLineWidth (outlineWidth); // Set The Line Width

        glCullFace (GL_FRONT); // Don't Draw Any Front-Facing Polygons

        glDepthFunc (GL_LEQUAL); // Change The Depth Mode

        glColor3fv (&outlineColor[0]); // Set The Outline Color

        glBegin (GL_TRIANGLES); // Tell OpenGL What We Want To Draw

        for (i = 0; i < polyNum; i++) // Loop Through Each Polygon
        {
            for (j = 0; j < 3; j++) // Loop Through Each Vertex
            {
                // Send The Vertex Position
                glVertex3fv (&polyData[i].Verts[j].Pos.X);
            }
        }

        glEnd (); // Tell OpenGL We've Finished

After this, we just set everything back to how it was before, and exit.

        glDepthFunc (GL_LESS); // Reset The Depth-Testing Mode

        glCullFace (GL_BACK); // Reset The Face To Be Culled

        glPolygonMode (GL_BACK, GL_FILL); // Reset Back-Facing Polygon Drawing Mode

        glDisable (GL_BLEND); // Disable Blending
    }
}

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2010-10-28 12:42:52
Processing time 0.5791 sec