10. 로딩 및 3D 세계에서의 이동

소개


The tutorial you have all been waiting for! This tutorial was made by a fellow programmer named Lionel Brits. In this lesson you will learn how to load a 3D world from a data file, and move through the 3D world. The code is made using lesson 1 code, however, the tutorial web page only explains the NEW code used to load the 3D scene, and move around inside the 3D world. Download the VC++ code, and follow through it as you read the tutorial. Keys to try out are [B]lend, [F]iltering, [L]ighting (light does not move with the scene however), and Page Up/Down. I hope you enjoy Lionel's contribution to the site. When I have time I'll make the Tutorial easier to follow. 









본문


This tutorial was created by Lionel Brits (ßetelgeuse). This lesson only explains the sections of code that have been added. By adding just the lines below, the program will not run. If you're interested to know where each of the lines of code below go, download the source code, and follow through it, as you read the tutorial. 

이 튜토리얼은 Lionel Brits가 만들었습니다. 이 레슨은 추가된 코드 섹션에 대해서만 설명합니다. 아래의 라인을 그냥 추가하면, 프로그램은 동작하지 않을 것입니다. 각 라인이 어떤 코드 아래에 들어가는지 알기를 원한다면, 소스코드를 다운로드하여 튜토리얼을 읽으면서 따라가시길 바랍니다.

Welcome to the infamous Tutorial 10. By now you have a spinning cube or a couple of stars, and you have the basic feel for 3D programming. But wait! Don't run off and start to code Quake IV just yet. Spinning cubes just aren't going to make cool deathmatch opponents :-) These days you need a large, complicated and dynamic 3D world with 6 degrees of freedom and fancy effects like mirrors, portals, warping and of course, high framerates. This tutorial explains the basic "structure" of a 3D world, and also how to move around in it. 

인기없는 튜토리얼 10에 오신걸 환영합니다. 지금부터 회전하는 큐브, 수많은 별들을 만들게 될것이고, 기본적인 3D 프로그래밍에 대한 감각을 갖게 될겁니다. 그러나 기다리세요! 도망가지 마세요. 퀘이크4의 코드는 아직 시작도 안했습니다. 회전하는 큐브는 상대편과 (쿨한 데스매치)를 하지 않을것입니다. :-) 요즘 여러분들은 6방향의 자유와 당연히 높은 프레임레이트의 거울, 포탈, 물결의 멋진 이펙트의 크고 복잡하고 다이나믹한 3D 월드를 원하고 있습니다. 이 튜토리얼은 3D월드 "구조"의 기본에 대한 설명과 어떻게 월드안에서 그것을 움직일 수 있는지를 알려줍니다.

데이터 구조


While it is perfectly alright to code a 3D environment as a long series of numbers, it becomes increasingly hard as the complexity of the environment goes up. For this reason, we must catagorize our data into a more workable fashion. At the top of our list is the sector. Each 3D world is basically a collection of sectors. A sector can be a room, a cube, or any enclosed volume.

긴 숫자의 나열로서 3D 환경을 (직접)코딩하는 것은 완벽하지만, 그렇게 하면 복잡한 환경을 만드는 것은 점점더 여려워 집니다. 이 이유로 인해, 더 잘 동작하는 구조로 분류 만들어야 합니다. 리스트 최상단은 섹터입니다. 각 3D 월드는 각 섹터들의 모음입니다. 각 섹터는 방, 큐브, 또는 다른 닫힌 부피입니다.
typedef struct tagSECTOR // 우리의 섹터 구조를 만듭니다.
{
int numtriangles; // 섹터안의 삼각형 갯수
TRIANGLE* triangle; // 삼각형 배열의 포인터
} SECTOR; // 이걸 SECTOR라 부릅시다
A sector holds a series of polygons, so the next catagory will be the triangle (we will stick to triangles for now, as they are alot easier to code.)

섹터는 폴리곤들을 가지고 있고, 그래서 다음 카테고리는 삼각형이 될 것입니다.(앞으로는 삼각형으로만 간주할 것입니다. 그것이 코드를 더 쉽게 만들어주기 때문입니다.)  
typedef struct tagTRIANGLE // 우리의 삼각형 구조를 만듭니다.
{
VERTEX vertex[3]; // 3개의 버텍스의 배열
} TRIANGLE; // 이걸 TRIANGLE이라 부릅시다.
The triangle is basically a polygon made up of vertices (plural of vertex), which brings us to our last catagory. The vertex holds the real data that OpenGL is interested in. We define each point on the triangle with it's position in 3D space (x, y, z) as well as it's texture coordinates (u, v).

삼각형은 기본적으로 버텍스들로 만들어지는 하나의 폴리곤인데(vertices는 vertex의 복수형이다.), 버텍스는 마지막 카테고리입니다. 버텍스는 OpenGL이 관심있어하는 실제데이터들을 가지고 있습니다. 우리는 3D 공간(x,y,z)의 위치와 텍스처 좌표 (u,v)로 삼각형의 각 점을 정의 합니다.

typedef struct tagVERTEX // 우리의 버텍스 구조를 만듭니다.
{
float x, y, z; // 3D 좌표들
float u, v; // 텍스처 좌표들
} VERTEX; // 이걸 VERTEX라 부릅시다.

파일 로딩

Storing our world data inside our program makes our program quite static and boring. Loading worlds from disk, however, gives us much more flexibility as we can test different worlds without having to recompile our program. Another advantage is that the user can interchange worlds and modify them without having to know the in's and out's of our program. The type of data file we are going to be using will be text. This makes for easy editing, and less code. We will leave binary files for a later date. 

우리 월드 데이타를 우리의 프로그램에 저장하는 것은 우리 프로그램이 정적이고 지루하게 만듭니다. 그러나 디스크에서 월드를 로딩하는 것은 프로그램을 재 컴파일 없이 다른 월드를 테스트 할수 있는 더 많은 유용성을 줍니다. 다른 장점은 사용자가 프로그램의 입력과 출력을 몰라도 월드를 변경할 수 있게 합니다.

The question is, how do we get our data from our file. First, we create a new function called SetupWorld(). We define our file as filein, and we open it for read-only access. We must also close our file when we are done. Let us take a look at the code so far:

질문은, 어떻게 파일로부터 데이타를 얻을수 있는가입니다. 먼저, SetupWorld()라는 함수를 만듭시다. 파일을 filein으로 정의하고, 그것을 읽기전용 접근("rt")로 엽시다. 다 끝나면, 파일을 닫아야 합니다. 코드를 같이 봅시다:
 
// 앞에서 char* worldfile = "data\\world.txt"라고 선언함
void SetupWorld() // 우리의 월드를 셋팅합니다.
{
FILE *filein; // 작업할 파일
filein = fopen(worldfile, "rt"); // 파일을 엽니다.

...
(read our data)
...

fclose(filein); // 파일을 닫습니다.
return; // 돌아갑니다.
}
Our next challenge is to read each individual line of text into a variable. This can be done in a number of ways. One problem is that not all lines in the file will contain meaningful information. Blank lines and comments shouldn't be read. Let us create a function called readstr(). This function will read one meaningful line of text into an initialised string. Here's the code:

다음 도전은 각 라인의 텍스트를 읽어서 변수에 넣는 것입니다. 이것은 여러가지 방법으로 할 수 있습니다. 문제는 파일에 있는 모든 라인이 의미있는 정보를 가지지 않을 거라는 것입니다. 빈라인과 주석은 읽지 말아야 합니다. readstr()이라는 함수를 하나 만듭시다. 이 함수는 의미있는 라인의 텍스트만 읽어서 초기화된 문자열에 넣을 것입니다. 여기 코드가 있습니다:

void readstr(FILE *f,char *string) // 하나의 문자열을 읽습니다.
{
do // 루프를 시작합니다.
{
fgets(string, 255, f); //하나의 라인을 읽습니다.
} while ((string[0] == '/') || (string[0] == '\n')); // 이것이 쓸만한 문자열인지 확인합니다.
return; // 돌아갑니다.
}
Next, we must read in the sector data. This lesson will deal with one sector only, but it is easy to implement a multi-sector engine. Let us turn back to SetupWorld().Our program must know how many triangles are in our sector. In our data file, we will define the number of triangles as follows: 

다음은, 섹터데이터를 읽어야 합니다. 이 레슨은 하나의 섹터만을 다룰것이지만, 멀티섹터 엔진으로 구현하기는 쉽습니다. 데이터 파일에서, 다음과 같이 다수의 삼각형들을 정의할 수 있습니다:

NUMPOLLIES n 

삼각형 갯수 n


Here's the code to read the number of triangles:

여기에 다수의 삼각형을 읽는 코드가 있습니다:
int numtriangles; // 섹터안의 삼각형 갯수
char oneline[255]; // 문자열을 저장할 공간
...
readstr(filein,oneline); // 한줄의 데이터를 읽음
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles); // 삼각형 갯수를 읽음
The rest of our world-loading process will use the same process. Next, we initialize our sector and read some data into it:

남은 월드 로딩 프로세스도 같은 프로세스를 사용할 것입니다. 다음은, 섹터를 초기화하고, 데이터를 읽어서 그안에 넣을 것입니다:

// Previous Declaration: SECTOR sector1;
char oneline[255]; // 저장할 문자열 데이터
int numtriangles; // 섹터안의 삼각형 갯수
float x, y, z, u, v; // 3D와 텍스처 좌표
...
sector1.triangle = new TRIANGLE[numtriangles]; // numtriangles만큼의 메모리 할당 및 포인터 설정
sector1.numtriangles = numtriangles; // 섹터 1에 있는 삼각형의 갯수 numtriangles 
// Step Through Each Triangle In Sector
for (int triloop = 0; triloop < numtriangles; triloop++) // 모든 삼각형을 루프
{
// Step Through Each Vertex In Triangle
for (int vertloop = 0; vertloop < 3; vertloop++) // 모든 버텍스를 루프
{
readstr(filein,oneline); // 작업할 문자열을 읽음
// Read Data Into Respective Vertex Values
sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);
// Store Values Into Respective Vertices
sector1.triangle[triloop].vertex[vertloop].x = x; // 섹터 1, 삼각형 triloop, 버텍스 vertloop, x 값 x sector1.triangle[triloop].vertex[vertloop].y = y; //섹터 1, 삼각형 triloop, 버텍스 vertloop, y 값 y sector1.triangle[triloop].vertex[vertloop].z = z; //섹터 1, 삼각형 triloop, 버텍스 vertloop, z 값 z sector1.triangle[triloop].vertex[vertloop].u = u; // Sector 1, Triangle triloop, Vertice vertloop, u Value=u //섹터 1, 삼각형 triloop, 버텍스 vertloop, u 값 u sector1.triangle[triloop].vertex[vertloop].v = v; // Sector 1, Triangle triloop, Vertice vertloop, v Value=v //섹터 1, 삼각형 triloop, 버텍스 vertloop, v 값 v
}
}
Each triangle in our data file is declared as follows:

각 데이터 파일에 들어있는 삼각형은 다음과 같이 정의됩니다:

X1 Y1 Z1 U1 V1
X2 Y2 Z2 U2 V2
X3 Y3 Z3 U3 V3

월드를 디스플레이하기

Now that we can load our sector into memory, we need to display it on screen. So far we have done some minor rotations and translations, but our camera was always centered at the origin (0,0,0). Any good 3D engine would have the user be able to walk around and explore the world, and so will ours. One way of doing this is to move the camera around and draw the 3D environment relative to the camera position. This is slow and hard to code. What we will do is this:
Rotate and translate the camera position according to user commands
Rotate the world around the origin in the opposite direction of the camera rotation (giving the illusion that the camera has been rotated)
Translate the world in the opposite manner that the camera has been translated (again, giving the illusion that the camera has moved)
This is pretty simple to implement. Let's start with the first stage (Rotation and translation of the camera).

이제 섹터를 메모리에 로드할수 있게 되어서, 화면에 그것을 보이게 해야합니다. 이미 전에 마이너 회전과 이동을 했지만, 카메라는 항상 원점(0,0,0)에 있었습니다. 어떤 좋은 3D 엔진은 사용자에게 주위를 걸어보고 월드를 탐험할 수 있게 합니다. 이걸 하는 하나의 방법은 카메라를 주위로 움직이고 3D 환경을 카메라 위치에 맞추는 것입니다. 이것은 느리고 코딩하기 어렵습니다. 우리가 할 것은 다음과 같습니다: 카메라 위치를 사용자의 명령에 따라 회전하고 이동합니다. 카메라 로테이션 방향과 반대방향으로 원점을 중심으로 월드를 회전합니다.(카메라가 회전했다는 환상을 주기 위함) 카메라가 이동된 방향의 반대방향으로 월드를 이동합니다.(역시, 카메라가 이동 했다는 환상을 주기 위함) 이건 매우 구현하기에 단순합니다. 첫번째 단계부터 시작합니다.(카메라의 회전과 이동)

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이 359 이상인가?
{
walkbiasangle = 0.0f; // walkbiasangle를 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; // 플레이어의 방향을 X평면을 기준으로 이동시킵니다
if (walkbiasangle <= 1.0f) // Iwalkbiasangle이 1 이하인가?
{
walkbiasangle = 359.0f; // walkbiasangle를 359으로 설정
}
else // 그렇지 않으면
{
walkbiasangle-= 10; // walkbiasangle를 10만큼 감소시킵니다
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f; // 플레이어가 튕기게 만듭니다
}
That was fairly simple. When either the left or right cursor key is pressed, the rotation variable yrot is incremented or decremented appropriatly. When the forward or backwards cursor key is pressed, a new location for the camera is calculated using the sine and cosine calculations (some trigonometry required :-). Piover180 is simply a conversion factor for converting between degrees and radians. 

그건 매우 심플했습니다. 왼쪽 또는 오른쪽 방향키가 눌려지면, 회전 변수인 yrot은 적절히 증가하거나 감소합니다. 앞으로 또는 뒤로 방향키가 눌려지면, 카메라의 새로운 위치가 사인 코사인 계산을 통해(약간의 삼각함수가 필요함) 계산됩니다. Piover180은 도와 라디안을 단순히 변환하는 팩터입니다.

Next you ask me: What is this walkbias? It's a word I invented :-) It's basically an offset that occurs when a person walks around (head bobbing up and down like a buoy. It simply adjusts the camera's Y position with a sine wave. I had to put this in, as simply moving forwards and backwards didn't look to great. 

다음에는 저에게 이런 질문을 할것입니다: 이 walkbias는 뭐죠? 그건 제가 발명한 단어인데요. 기본적으로 사람이 걸어갈때 발생하는 기본적인 오프셋 값입니다.(아래위로 출렁이는 부표같은) 그것은 쉽게 카메라의 Y 위치를 사인파와 같이 적용되게 합니다. 저는 단순히 앞뒤로 움직여서 좋게 보이지 않아서 이것을 넣어야 했습니다.

Now that we have these variables down, we can proceed with steps two and three. This will be done in the display loop, as our program isn't complicated enough to merit a seperate function.

이제 이 변수들을 아래로 읽어 보면, 2~3단계로 진행할 수 있습니다. 이것은 우리 프로그램이 복잡하지 않아서 함수를 나누는데 충분한 메리트를 얻지 못하므로 디스플레이 루프안에서 수행됩니다. 
int DrawGLScene(GLvoid) // DOpenGL 장면을 그립니다 
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 스크린과 깊이버퍼를 지웁니다
glLoadIdentity(); // 현재 매트릭스를 초기화 합니다
GLfloat x_m, y_m, z_m, u_m, v_m; // Temp X, Y, Z, U 그리고 V 버텍스에 대한 부동소수점(float) 변수
GLfloat xtrans = -xpos; // X축으로 플레이어 이동을 위해 사용됨
GLfloat ztrans = -zpos; // Z축으로 플레이어 이동을 위해 사용됨
GLfloat ytrans = -walkbias-0.25f; // 플레이어를 아래/위로 튕기게 하기 위해 사용됨
GLfloat sceneroty = 360.0f - yrot; // 플레이어 각도를 360도로 나타내줌

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의 삼각형 갯수를 얻음
// Process Each Triangle
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; // 첫번째 점에 대한 X 버텍스 좌표
y_m = sector1.triangle[loop_m].vertex[0].y; // 첫번째 점에 대한 Y 버텍스 좌표
z_m = sector1.triangle[loop_m].vertex[0].z; // 첫번째 점에 대한 Z 버텍스 좌표
u_m = sector1.triangle[loop_m].vertex[0].u; // 첫번째 점에 대한 U 텍스처 좌표
v_m = sector1.triangle[loop_m].vertex[0].v; // 첫번째 점에 대한 V 텍스처 좌표
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 텍스처 좌표와 버텍스 좌표의 설정
x_m = sector1.triangle[loop_m].vertex[1].x; // 두번째 점에 대한 X 버텍스 좌표
y_m = sector1.triangle[loop_m].vertex[1].y; // 두번째 점에 대한 Y 버텍스 좌표
z_m = sector1.triangle[loop_m].vertex[1].z; // 두번째 점에 대한 Z 버텍스 좌표
u_m = sector1.triangle[loop_m].vertex[1].u; // 두번째 점에 대한 U 텍스처 좌표
v_m = sector1.triangle[loop_m].vertex[1].v; // 두번째 점에 대한 V 텍스처 좌표
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 텍스처 좌표와 버텍스 좌표의 설정
x_m = sector1.triangle[loop_m].vertex[2].x; // 세번째 점에 대한 X 버텍스 좌표
y_m = sector1.triangle[loop_m].vertex[2].y; // 세번째 점에 대한 Y 버텍스 좌표
z_m = sector1.triangle[loop_m].vertex[2].z; // 세번째 점에 대한 Z 버텍스 좌표
u_m = sector1.triangle[loop_m].vertex[2].u; // 세번째 점에 대한 U 텍스처 좌표
v_m = sector1.triangle[loop_m].vertex[2].v; // 세번째 점에 대한 V 텍스처 좌표
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m); // 텍스처 좌표와 버텍스 좌표의 설정
glEnd(); // 삼각형 그리기 완료
}
return TRUE; // 돌아갑니다.
}
And voila! We have drawn our first frame. This isn't exactly Quake but hey, we aren't exactly Carmack's or Abrash's. While running the program, you may want to press F, B, PgUp and PgDown to see added effects. PgUp/Down simply tilts the camera up and down (the same process as panning from side to side.) The texture included is simply a mud texture with a bumpmap of my school ID picture; that is, if NeHe decided to keep it :-). 

와! 이제 우리는 첫번째 프레임을 그렸습니다. 이건 절대로 퀘이크 같지는 않지만, 우리는 절대 카멕이나 Abrash가 아니죠. 프로그램이 수행되는 동안, 여러분은 F, B, PgUp, PgDown을 누름으로서 추가된 이펙트를 볼수 있습니다. PgUp/Down은 단순히 카메라를 아래위로 틸트 시킵니다.(같은 프로세스로 좌우로 패닝시킵니다.) 텍스쳐는 제 학교 ID 사진의 범프맵이 입혀친 진흙 텍스쳐를 단순히 포함합니다; NeHe가 받아들이기로 한다면

So now you're probably thinking where to go next. Don't even consider using this code to make a full-blown 3D engine, since that's not what it's designed for. You'll probably want more than one sector in your game, especially if you're going to implement portals. You'll also want to have polygons with more than 3 vertices, again, essential for portal engines. My current implementation of this code allows for multiple sector loading and does backface culling (not drawing polygons that face away from the camera). I'll write a tutorial on that soon, but as it uses alot of math, I'm going to write a tutorial on matrices first. 

그래서 이제부터 여러분들은 다음으로 어디로 갈지 생각하고 있을겁니다. 그러나 이것은 본격적인 3D 엔진을 디자인 하지 않는 코드이므로, 이 코드를 본격적인 3D 엔진으로 사용하는 것을 고려하지 마세요. 특히 포탈을 구현할 예정이라면 아마도 1 섹터 이상을 여러분들의 게임에서 사용하길 원할겁니다. 그리고 포탈 엔진을 위해 3개 이상의 버텍스로 이루어진 폴리곤들을 원할겁니다. 저의 이코드에서 현재 구현은 멀티 섹터로딩과 백페이스 컬링(카메라에서 안보이는 폴리곤들을 그리지 않는)을 지원합니다. 저는 그 튜토리얼을 조만간 올릴겁니다. 그렇지만 많은 수학을 사용할 것이므로, 매트릭스에 대한 튜토리얼을 먼저 쓸겁니다.

NeHe (05/01/00): 

I've added FULL comments to each of the lines listed in this tutorial. Hopefully things make more sense now. Only a few of the lines had comments after them, now they all do :) 

저는 이 튜토리얼에서 리스트된 모든 라인에 주석을 달았습니다. 

Please, if you have any problems with the code/tutorial (this is my first tutorial, so my explanations are a little vague), don't hesitate to email me (iam@cadvision.com) Until next time... 

여러분들이 이 코드와 튜토리얼에 문제가 있다면(이건 저의 첫번재 튜토리얼 입니다, 그래서 저의 설명이 약간 부족할 수가 있어요), 주저하지말고 저에게 이메일을 보내주세요.(iam@cadvision.com) 다음시간 까지...

Lionel Brits (ßetelgeuse) 

Jeff Molofee (NeHe) 


소스코드 다운로드

이 강좌의 소스코드를 다운받으실 수 있습니다. 자신의 환경에 맞는 파일을 받아 사용하세요.


원문 정보

  • 저자: Jeff Molofee (NeHe), Lionel Brits (ßetelgeuse) 
  • 원문보기: Lesson 10

번역문 정보

  • 초벌번역: 남정수

현재 상태

  • 초벌번역시작

Comments