[App 개발] NeHe Lesson 10
본문
보잘것 없습니다만 제 10 강에 잘 오셨습니다. 지금까지 육면체를 회전시키거나 별들을 그리면서 3D 프로그래밍의 기본적인 내용을 살펴보셨습니다. 그러나 잠시! 당장 나가서 Quake IV 를 짜기 시작하지는 마세요. 데쓰매치 상대로 회전하는 육면체를 올릴 생각은 아니시겠죠? :-) 요즘에는 크고 복잡하면서 동적인 자유도 6을 가지는 3D 영역에 mirror, portal, warping같은 화려한 효과들, 그리고 당연히 빠른 화면 갱신 속도가 필요합니다. 이 강의에서는 3D 세계를 구성하는 기본 구조와 그 속에서 어떻게 돌아다닐 수 있는지를 설명할 것입니다.
데이터 구조
숫자들의 모임으로 3D 환경을 코딩해주는 것도 가능하긴 하지만, 환경이 점점 복잡해지게 되면 프로그램 짜기가 훨씬 어려워지게 됩니다. 따라서 우리는 데이터를 좀 더 능률적인 형태로 구분해야 합니다. 리스트의 첫 번째 요소는 섹터입니다. 기본적으로 3D 환경은 섹터들의 모임으로 구성됩니다. 섹터는 방, 육면체, 혹은 여러 가지를 포함하는 집합이 될 수도 있습니다.
typedef struct tagSECTOR // 섹터를 정의한다
{
int numtriangles; // 섹터에 쓰일 삼각형의 갯수
TRIANGLE* triangle; // 삼각형 배열의 포인터
} SECTOR; // 구조체의 이름을 SECTOR 라고 하자
섹터는 다각형의 집합이며, 따라서 다음 구성 요소는 삼각형이 되겠습니다. (일단 프로그램을 만들기 쉬운 삼각형만 고려하겠습니다)
typedef struct tagTRIANGLE // 삼각형을 정의한다
{
VERTEX vertex[3]; // 세 꼭지점의 배열
} TRIANGLE; // 구조체 이름은 TRIANGLE
기본적으로 삼각형은 꼭지점들로 이루어진 다각형이고, 따라서 마지막 구성 요소는 꼭지점입니다. 꼭지점은 OpenGL 이 다루는 진짜 데이터입니다. 삼각형의 각 점은 3D 공간의 위치 (x, y, z)와 해당하는 텍스쳐의 좌표 (u, v)로 구성됩니다.
typedef struct tagVERTEX // 꼭지점을 정의한다
{
float x, y, z; // 3D 좌표
float u, v; // 텍스쳐의 좌표
} VERTEX; // 구조체 이름은 VERTEX
파일 읽기
우리가 만들 공간 정보를 프로그램 안에 직접 (하드 코드로) 넣는 것은 프로그램을 정적이고 따분하게 만듭니다. 공간 정보를 디스크에서 읽을 수 있게 만들면 우리는 프로그램을 매번 컴파일하지 않고도 다른 공간을 실험해볼 수 있는 자유를 얻게 됩니다. 그리고 프로그램의 입출력을 모두 이해하지 않아도 공간을 변경하거나 다른 사람들과 교환할 수 있다는 잇점이 있습니다. 우리가 만들 데이터 파일의 형식을 텍스트로 만들어서 고치기 쉽고 프로그램 짜기도 쉽게 하겠습니다. 이진 파일을 사용하는 방법은 다음 기회에 하겠습니다.
문제는 어떻게 우리 파일을 읽을 것인가 입니다. 먼저 SetupWorld() 함수를 선언합니다. filein이라는 파일 핸들을 선언하여 읽기 전용으로 파일을 엽니다. 모든 작업이 끝나면 파일을 닫아주어야 합니다. 지금까지 설명한 코드는 다음과 같습니다.
// 이것을 먼저 앞에서 선언해 두세요: char* worldfile = "data\\\\world.txt";
void SetupWorld() // 우리 공간을 초기화한다
{
FILE *filein; // 작업할 파일 핸들
filein = fopen(worldfile, "rt"); // 우리 파일을 연다
...
(우리 데이터를 읽고)
...
fclose(filein); // 파일을 닫는다
return;
}
이제 해야 할 일은 텍스트를 한 줄씩 프로그램 변수에 읽어 들이는 일인데, 여러 가지 방법이 가능합니다. 한 가지 문제는 텍스트 파일의 모든 줄이 다 의미 있는 정보는 아니라는 점입니다. 빈 줄이나 주석문은 읽을 필요가 없습니다. 파일에서 의미 있는 문장을 문자열에 읽어들이는 readstr()라는 함수를 만들어 봅시다.
void readstr(FILE *f,char *string) // 파일을 읽어 문자열에 저장
{
do // 반복문 시작
{
fgets(string, 255, f); // 한 줄 읽기
} while ((string[0] == '/') || (string[0] == '\\n')); // 읽을만한 가치가 있는감?
Return;
}
그리고, 데이터를 읽어 섹터에 저장해야 합니다. 우리 강좌에서는 섹터 하나만 다룰 것입니다만, 여러 개의 섹터를 다루는 엔진을 구현하는 것은 쉽습니다. 다시 SetupWorld() 함수로 돌아가서 먼저 우리는 몇 개의 삼각형을 쓸 것인지를 알아야 합니다. 우리 데이터 파일에서는 삼각형의 갯수를 다음과 같이 선언하겠습니다.
NUMPOLLIES n
다음은 삼각형의 갯수를 읽는 코드입니다.
int numtriangles; // 섹터 안에 있는 삼각형 개수
char oneline[255]; // 텍스트 파일 데이터를 읽을 문자열
...
readstr(filein,oneline); // 데이터를 한 줄 읽고
sscanf(oneline, "NUMPOLLIES %d\\n", &numtriangles); // 삼각형의 갯수를 읽는다
공간을 읽어들이는 나머지 루틴은 동일한 과정입니다. 다음은 섹터를 초기화하고 데이터를 읽어들이는 코드입니다.
// 먼저 변수를 하나 선언해 둡니다: SECTOR sector1;
char oneline[255]; // 데이터를 읽을 문자열
int numtriangles; // 섹터 안의 삼각형의 갯수
float x, y, z, u, v; // 3D 와 텍스쳐 좌표
...
sector1.triangle = new TRIANGLE[numtriangles]; // 삼각형을 갯수만큼 할당하여 포인터를 저장
sector1.numtriangles = numtriangles; // 섹터1의 삼각형 갯수를 저장
// 섹터 안의 삼각형을 모두 참조
for (int triloop = 0; triloop < numtriangles; triloop++) // 모든 삼각형을 참조
{
// 삼각형 안의 세 꼭지점을 참조
for (int vertloop = 0; vertloop < 3; vertloop++) // 모든 꼭지점을 참조
{
readstr(filein,oneline); // 작업할 문자열을 읽는다
// 각각의 꼭지점의 값을 읽는다
sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);
// 각 꼭지점의 값들을 저장한다
sector1.triangle[triloop].vertex[vertloop].x = x;
sector1.triangle[triloop].vertex[vertloop].y = y;
sector1.triangle[triloop].vertex[vertloop].z = z;
sector1.triangle[triloop].vertex[vertloop].u = u;
sector1.triangle[triloop].vertex[vertloop].v = v;
}
}
데이터 파일 안에 삼각형은 다음과 같이 정의됩니다.
X1 Y1 Z1 U1 V1
X2 Y2 Z2 U2 V2
X3 Y3 Z3 U3 V3
공간을 그린다
섹터를 메모리에 읽었으면 이제 화면에 출력합니다. 지금까지 우리는 간단한 회전과 이동을 했습니다만 우리 카메라는 언제나 원점 중심에 있지는 않습니다. 잘 만든 3D 엔진들은 모두 사용자들이 공간을 움직이고 살펴볼 수 있게끔 하지요. 우리도 그렇게 하겠습니다. 한 가지 방법은 카메라를 움직여서 카메라의 위치에 해당하는 3D 환경을 그리는 것인데, 속도가 느리고 만들기가 어렵습니다.
우리가 쓸 방법은 이렇습니다.
1. 먼저 사용자의 명령에 따라서 카메라의 위치를 회전하거나 이동합니다.
2. 그리고 원점을 중심으로 카메라의 회전 방향과 반대로 공간을 회전시켜서 카메라가 회전한 것 같이 착각을 일으킵니다.
3. 그 다음 카메라가 이동한 것과 반대 방향으로 공간을 이동시킵니다. 역시 카메라가 움직인 것 같은 착각을 일으킵니다.
구현하기 아주 쉽습니다. 먼저 첫 번째 순서, 카메라를 회전하고 이동하는 것부터 해 보겠습니다.
if (keys[VK_RIGHT]) // 오른쪽 화살표키가 눌렸나
{
yrot -= 1.5f; // 씬을 왼쪽으로 회전
}
if (keys[VK_LEFT]) // 왼쪽 화살표키
{
yrot += 1.5f; // 씬을 오른쪽으로 회전
}
if (keys[VK_UP]) // 위쪽 화살표키
{
xpos -= (float)sin(heading*piover180) * 0.05f; // 관찰자의 방향에 따라서 x평면에서 움직인다
zpos -= (float)cos(heading*piover180) * 0.05f; // 관찰자의 방향에 따라서 z평면에서 움직인다
if (walkbiasangle >= 359.0f)
{
walkbiasangle = 0.0f; // walkbiasangle 변수가 359가 되면 0 으로 되돌린다
}
else
{
walkbiasangle+= 10; // walkbiasangle 변수를 10 씩 증가
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f; // 플레이어 걷는 것처럼 들썩거리게 만드는 효과
}
if (keys[VK_DOWN]) // 아래쪽 화살표 키
{
xpos += (float)sin(heading*piover180) * 0.05f; // 사용자의 방향에 따라서 x평면으로 움직임
zpos += (float)cos(heading*piover180) * 0.05f; // 사용자의 방향에 따라서 z평면으로 움직임
if (walkbiasangle <= 1.0f)
{
walkbiasangle = 359.0f; // walkbiasangle 이 0이 되면 359 로 만든다
}
else
{
walkbiasangle-= 10; // walkbiasangle 를 10 씩 감소
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f; // 플레이어가 들썩거리게 한다
}
간단하죠. 왼쪽이나 오른쪽 키가 눌리면 회전 변수 yrot 가 증가, 감소하게 됩니다. 위 아래 키가 눌리게 되면 sin, cos함수를 이용하여 카메라의 새 위치를 계산합니다. (삼각함수 아시죠?) Piover180은 degree와 radian을 변환하는 계수입니다.
walkbias는 무엇이냐? 제가 만든 단어입니다. :-) 기본적으로 이것은 사람이 걸어다닐 때 머리가 위 아래로 물에 떠 있는 부표처럼 들썩거리는 것을 표현하는 offset값을 간단히 카메라의 y축 위치에 sin 곡선을 대입하여 표현한 것입니다. 그냥 앞뒤로 왔다 갔다 하는 것은 별로 멋이 없어서 첨가한 것입니다.
이제 변수들이 준비되었으니 두 번째와 세 번째 과정으로 넘어가겠습니다. 이것을 독립적인 함수로 구현할 만큼 복잡하지 않기 때문에 그냥 화면 출력 함수에 구현하겠습니다.
int DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
GLfloat x_m, y_m, z_m, u_m, v_m;
GLfloat xtrans = -xpos; // 플레이어의 X 축 상의 이동에 쓰이는 변수
GLfloat ztrans = -zpos; // 플레이어의 y 축 상의 이동에 쓰이는 변수
GLfloat ytrans = -walkbias-0.25f; // 위아래로 움직임을 표현
GLfloat sceneroty = 360.0f - yrot; // 플레이어의 보는 방향
int numtriangles; // 삼각형의 개수
glRotatef(lookupdown,1.0f,0,0); // 위아래를 보는 것에 따라 회전
glRotatef(sceneroty,0,1.0f,0); // 플레이어가 향한 방향으로 회전
glTranslatef(xtrans, ytrans, ztrans); // 플레이어의 방향으로 카메라 이동
glBindTexture(GL_TEXTURE_2D, texture[filter]); // 텍스쳐 선택
numtriangles = sector1.numtriangles; // 섹터 1의 삼각형 갯수
// 각각의 삼각형을 구현한다
for (int loop_m = 0; loop_m < numtriangles; loop_m++) // 모든 삼각형을 참조
{
glBegin(GL_TRIANGLES); // 삼각형 그리기 시작
glNormal3f( 0.0f, 0.0f, 1.0f); // 법선
x_m = sector1.triangle[loop_m].vertex[0].x;
y_m = sector1.triangle[loop_m].vertex[0].y;
z_m = sector1.triangle[loop_m].vertex[0].z;
u_m = sector1.triangle[loop_m].vertex[0].u;
v_m = sector1.triangle[loop_m].vertex[0].v;
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);
x_m = sector1.triangle[loop_m].vertex[1].x;
y_m = sector1.triangle[loop_m].vertex[1].y;
z_m = sector1.triangle[loop_m].vertex[1].z;
u_m = sector1.triangle[loop_m].vertex[1].u;
v_m = sector1.triangle[loop_m].vertex[1].v;
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);
x_m = sector1.triangle[loop_m].vertex[2].x;
y_m = sector1.triangle[loop_m].vertex[2].y;
z_m = sector1.triangle[loop_m].vertex[2].z;
u_m = sector1.triangle[loop_m].vertex[2].u;
v_m = sector1.triangle[loop_m].vertex[2].v;
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);
glEnd(); // 삼각형 그리기 끝
}
return TRUE;
}
최신글이 없습니다.
최신글이 없습니다.
댓글목록 0