과거의 연극은 인생이 송두리째 비쳐진 거울이지만, 오늘의 연극은 인생을 들여다보는 열쇠구멍. -A.H.G.
* 원문링크 :
http://nehe.gamedev.net/tutorials/lesson.asp?l=37
http://nehe.gamedev.net/tutorials/lesson.asp?l=37
- 원저자 : Sami "MENTAL" Hamlaoui
- 소스코드 : (위 링크에서 받으세요~)
주의 : 이 코드의 원본 아티클은 여기서 얻을 수 있다.
http://www.gamedev.net/reference/programming/features/celshading -> 번역은 여기 : Cel-Shading
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
}
}








