티스토리 뷰

언제부터 지식을 돈 주고 사고 팔게 되었는지 모르겠지만, 무언가 검색하다 걸리는 유료 리포트 사이트들을 볼 때마다 불쾌했다. 사본적이 없어서 모르겠지만 내용이 좋을 것 같지도 않다. 난 돈 있는 자와 돈 없는 자 간에 정보의 격차가 있어서는 안 된다고 생각하기 때문에 이런 사이트를 싫어한다. 하지만 지금 당장 그 사람들에게 리포트 파는 짓을 그만두라고 할 수는 없는 노릇이다. 일단 내가 내 리포트라도 공개 하는 게 좋을 것 같다.

 

그래서 이번 학기 컴퓨터 공학부 전공 수업을 들으며 작성한 리포트 중 올릴만한 가치가 있는 것들을 몇 개 공개하려고 한다. 아 물론 리포트 파일 자체를 올리진 않는다. 돈 주고 파는 것에 목적이 있는 게 아니니, 이와 관련된 자료를 필요로 하는 모든 사람들이 볼 수 있게 재 작성해서 올릴 것이다.

 

첫 번째 주제는 컴퓨터 그래픽스 과목에서 텀 프로젝트로 나온 마지막 과제이다. 원래는 최대 4명의 팀으로 진행하는 프로젝트인데 요구사항과 기간을 보고 이 정도는 혼자 할 수 있겠다 해서 혼자 했고, 별 무리 없이 모든 요구사항을 만족했다.

 

주제: 1인칭 3D 비행 시뮬레이션


요구사항은 아래와 같다

a. OpenGL 및 GLUT 사용

b. C, C++ 언어를 사용하여 구현 (MFC, WINAPI etc.)

c. 지형 생성 및 텍스쳐 적용

d. 지형의 Level Of Detail

e. 주변 건물 및 움직이는 오브젝트 추가

f. 시점 이동 가능

g. 특정 오브젝트로의 시점 변경

h. 오브젝트간 충돌 감지

 

추가구현 (가산점)

a. 툰 텍스처링 (2D 텍스처)

 

 

이 중에서 난이도 있는 부분은 LOD와 툰 텍스쳐링이기 때문에 이 글에선 두 요구사항을 위주로 설명한다. 프로그램의 전체 소스 파일은 아래 링크에서 받을 수 있다.

 

1. 지형 LOD 렌더링

 

(랜덤으로 생성된 지형의 모습)

 

요구사항에 맞춰, 지형 렌더링시 LOD(level of detail)를 사용한다. LOD는 카메라와 오브젝트간의 거리에 따라 모델 디테일을 다르게 렌더링 하는것인데, 멀리 있는 오브젝트를 디테일하게 표현하는 것은 낭비이기 때문에 멀리 있는 오브젝트는 간단하게 표현하고 가까이 있는 오브젝트를 디테일하게 표현한다는 개념이다. 많은 기법이 있지만 여기서는 간단하게 지형을 대상으로만 했다.

하지만 LOD를 할때 문제가 있는데, 바로 크랙이다.

 

(레벨 차이가 나는 두 타일 사이에는 크랙이 발생한다)

 

여기서 사용된 LOD 렌더링 및 크랙 해결에 대해 대략적으로 설명하자면, 우선 쿼드 트리를 기반으로 렌더링 할 지형을 재귀적으로 찾는다. 그리고 각 노드를 level을 key로 한 multimap에 넣는다. (즉시 렌더링하지 않는 이유는 크랙을 해결하기 위해서이다.)

그리고 multimap에 등록된 각 노드들을 순차적으로 렌더링하는데, 노드들은 level 순으로 정렬되어있으므로 가장 큰 노드부터 렌더링하게 된다.

한 노드를 그릴 때마다 LOD에 의해 그려지는 지형 높이를 별도로 저장한다. 그리고 하위 노드를 그릴 때 이 저장한 값을 참고해서 이미 지형이 그려진 부분이면 원래 높이가 아닌 LOD용 높이 값을 참조해서 그린다. 이로써 크랙을 해결할 수 있다.

 

(참고로 LOD 크랙에 대한 자료를 찾아보면 방대한 자료가 나오는데, 더 자연스럽게 해결하지만 훨씬 어렵다.)

 

(LOD로 인한 디테일 차이. 위 방법으로 레벨이 다른 타일들 사이의 크랙이 해결되었다.)

 

설명이 복잡한 감이 있지만, 코드를 보면 쉽다.

크게 LOD 지형 계산 –> 계산된 지형 그리기 순서이다.

 

   1: void Terrain::Render(const vector3 &mypos)
   2: {
   3:     int myxpos = min(max(mypos.x / TILE_SIZE, 0), TERRAIN_MAX_X - 1);
   4:     int myzpos = min(max(mypos.z / TILE_SIZE, 0), TERRAIN_MAX_Z - 1);
   5:  
   6:     glEnable(GL_TEXTURE_2D);
   7:     glEnable(GL_LIGHTING);
   8:     
   9:     // lod terrain
  10:     for(int x = 0; x < TERRAIN_MAX_X; x++)
  11:     {
  12:         for(int z = 0; z < TERRAIN_MAX_Z; z++)
  13:         {
  14:             m_lodHeights[x][z] = -1;
  15:         }
  16:     }
  17:     m_drawSeqMap.clear();
  18:  
  19:     m_terrainTexture.bind();
  20:  
  21:     glColor3f(1, 1, 1);
  22:     glBegin(GL_TRIANGLES);
  23:     CalcLODTerrain(0, TERRAIN_MAX_X - 1, 0, TERRAIN_MAX_Z - 1, myxpos, myzpos, 0);
  24:  
  25:     for(DrawSeqMap::iterator i = m_drawSeqMap.begin(); i != m_drawSeqMap.end(); i++)
  26:     {
  27:         DRAW_TILE &dt = i->second;
  28:  
  29:         if(dt.xmax - dt.xmin >= 2)
  30:         {
  31:             DrawTile(dt.xmin, dt.xmax, dt.zmin, dt.zmax, 3);
  32:         }
  33:         else
  34:         {
  35:             DrawTile(dt.xmin, dt.xmax, dt.zmin, dt.zmax, 1);
  36:         }
  37:     }
  38:  
  39:     glEnd();
  40:     
  41:     
  42: }

 

CalcLODTerrain에서 지형을 재귀적으로 분할해서 draw sequence에 추가한다.

 

   1: bool Terrain::CalcLODTerrain(int xmin, int xmax, int zmin, int zmax, int myposx, int myposz, int lev)
   2: {
   3:     if(xmax - xmin <= 1 || zmax - zmin <= 1)
   4:         return false;
   5:  
   6:     int xmid = (xmax + xmin) / 2;
   7:     int zmid = (zmax + zmin) / 2;
   8:  
   9:     int d = 4;
  10:  
  11:     if((xmin - d <= myposx && myposx <= xmax + d) && (zmin - d <= myposz && myposz <= zmax + d))
  12:     {
  13:         bool childdrew = false;
  14:  
  15:         childdrew |= CalcLODTerrain(xmin, xmid, zmin, zmid, myposx, myposz, lev + 1);
  16:         childdrew |= CalcLODTerrain(xmin, xmid, zmid, zmax, myposx, myposz, lev + 1);
  17:         childdrew |= CalcLODTerrain(xmid, xmax, zmin, zmid, myposx, myposz, lev + 1);
  18:         childdrew |= CalcLODTerrain(xmid, xmax, zmid, zmax, myposx, myposz, lev + 1);
  19:  
  20:         if(!childdrew)
  21:         {
  22:             m_drawSeqMap.insert(pair<int, DRAW_TILE>(lev, DRAW_TILE(xmin, xmid, zmin, zmid)));
  23:             m_drawSeqMap.insert(pair<int, DRAW_TILE>(lev, DRAW_TILE(xmin, xmid, zmid, zmax)));
  24:             m_drawSeqMap.insert(pair<int, DRAW_TILE>(lev, DRAW_TILE(xmid, xmax, zmin, zmid)));
  25:             m_drawSeqMap.insert(pair<int, DRAW_TILE>(lev, DRAW_TILE(xmid, xmax, zmid, zmax)));
  26:         }
  27:  
  28:         return true;
  29:     }
  30:     else
  31:     {
  32:         m_drawSeqMap.insert(pair<int, DRAW_TILE>(lev, DRAW_TILE(xmin, xmid, zmin, zmid)));
  33:         m_drawSeqMap.insert(pair<int, DRAW_TILE>(lev, DRAW_TILE(xmin, xmid, zmid, zmax)));
  34:         m_drawSeqMap.insert(pair<int, DRAW_TILE>(lev, DRAW_TILE(xmid, xmax, zmin, zmid)));
  35:         m_drawSeqMap.insert(pair<int, DRAW_TILE>(lev, DRAW_TILE(xmid, xmax, zmid, zmax)));
  36:  
  37:         return false;
  38:     }
  39: }

 

최종적으로 DrawTile에서 이 draw sequence 순서대로 지형 타일을 그리는데, 이때 큰 타일(detail이 낮은)이 이미 그려진 상태라면 작은 타일(detail이 높은)을 그릴 때 큰 타일의 높이를 가지고 그리는 것이 LOD크랙 해결의 핵심이다.

 

   1: void Terrain::DrawTile(int xmin, int xmax, int zmin, int zmax, int lev)
   2: {
   3:     //DEBUG_LOG("lod %d %d, %d %d", xmin, xmax, zmin, zmax);
   4:  
   5:     if(xmax - xmin == 0 || zmax - zmin == 0)
   6:         return;
   7:  
   8:     if(lev == 1)
   9:     {
  10:         // 사각형의 네 선분에 대해서 height 결정
  11:         int xdelta = xmax - xmin;
  12:         for(int i = 0; i <= xdelta; i++)
  13:         {    
  14:             m_lodHeights[xmin + i][zmin] = (GetTileHeight(xmax, zmin) - GetTileHeight(xmin, zmin)) * i / xdelta + GetTileHeight(xmin, zmin);
  15:             m_lodHeights[xmin + i][zmax] = (GetTileHeight(xmax, zmax) - GetTileHeight(xmin, zmax)) * i / xdelta + GetTileHeight(xmin, zmax);
  16:         }
  17:  
  18:         int zdelta = zmax - zmin;
  19:         for(int i = 0; i <= zdelta; i++)
  20:         {
  21:             m_lodHeights[xmin][zmin + i] = (GetTileHeight(xmin, zmax) - GetTileHeight(xmin, zmin)) * i / zdelta + GetTileHeight(xmin, zmin);
  22:             m_lodHeights[xmax][zmin + i] = (GetTileHeight(xmax, zmax) - GetTileHeight(xmax, zmin)) * i / zdelta + GetTileHeight(xmax, zmin);
  23:         }
  24:  
  25:         float y = 0;
  26:         int repeat = xmax - xmin;
  27:  
  28:         //1
  29:         glTexCoord2f(0, 0);
  30:         DrawVert(xmin, zmin);
  31:  
  32:         glTexCoord2f(0, repeat);
  33:         DrawVert(xmin, zmax);
  34:  
  35:         glTexCoord2f(repeat, repeat);
  36:         DrawVert(xmax, zmax);
  37:  
  38:         //2
  39:         glTexCoord2f(0, 0);
  40:         DrawVert(xmin, zmin);
  41:  
  42:         glTexCoord2f(repeat, repeat);
  43:         DrawVert(xmax, zmax);
  44:  
  45:         glTexCoord2f(repeat, 0);
  46:         DrawVert(xmax, zmin);
  47:     }
  48:     else
  49:     {
  50:         int xmid = (xmax + xmin) / 2;
  51:         int zmid = (zmax + zmin) / 2;
  52:  
  53:         DrawTile(xmin, xmid, zmin, zmid, lev - 1);
  54:         DrawTile(xmin, xmid, zmid, zmax, lev - 1);
  55:         DrawTile(xmid, xmax, zmin, zmid, lev - 1);
  56:         DrawTile(xmid, xmax, zmid, zmax, lev - 1);
  57:     }
  58:  
  59: }

 

2. 툰 텍스쳐링

 

(툰 텍스쳐링으로 렌더링 된 모델.)

 

모델 렌더링에서 툰 텍스쳐링을 사용한다. 텍스쳐를 이용한 툰 텍스쳐링을 하는데, 그라데이션 텍스쳐를 사용해서 빛이 밝은 부분은 흰색, 중간 빛은 회색, 빛이 없는 부분은 검은색 텍스쳐를 사용해서 위와 같이 표현하는 가장 간단한 기법이다.

이를 위해 텍스쳐 좌표인 s, t가 필요한데, 이 값을 구하는 것은 라이트 위치와 법선 벡터를 가지고 intensity를 계산하면 된다. 특별히 어렵진 않고 아래와 같이 3D 입문서에 나와있는 개념을 그대로 적용하면 된다.

 

1. 모델 버텍스들의 실제 좌표(변환이 끝난 좌표)를 구한다.

2. 이 실제 좌표를 기준으로 라이트 위치 벡터를 구한다. 이 벡터는 정규화 되야 한다.

3. 노멀벡터와 dot product를 계산하면 두 벡터의 cos값이 나온다.

4. 이 값이 intensity이며, 바로 텍스쳐 좌표로 사용할 수 있다.

 

사용한 텍스쳐는 아래와 같다. 3가지 이미지가 한꺼번에 있지만, 여기선 가장 효과가 극명한 맨 윗부분 이미지만 사용한다. 즉, t값은 0을 사용하고 s값으로 intensity를 사용한다.

 

아래 모델 렌더링 코드를 보면 쉽게 이해될 것이다. 참고로 여기서 intensity가 음수로 나오는 경우가 있는데, 이때는 노멀 벡터와 광원 벡터의 사이각이 90도 이상인 경우로, 당연히 좌표값을 0으로 지정해 줘야 한다. 하지만 실제로 0을 넣으면 텍스쳐가 깨져 나오는데, 코드를 보면 0이 아닌 BLACK_TEX_S_COORD로 지정되어있다. 이것은 오픈gl 텍스쳐 옵션을 GL_REPEAT으로 할 경우 텍스쳐의 양 끝에서 반복이 발생하는데 이때 좌표값이 float으로 주어지기 때문에 이와 관련된 오차가 발생하는 것으로 생각된다. 따라서 이 옵션을 꺼주던가 아니면 코드와 같이 약간의 오차 보정 상수가 필요하다.

 

   1: glBegin(GL_TRIANGLES);
   2: for(int i = 0; i < m_faces.size(); i++)
   3: {
   4:     const vector3 &idx = m_faces[i];
   5:     const vector3 &a = m_verts[idx.x];
   6:     const vector3 &b = m_verts[idx.y];
   7:     const vector3 &c = m_verts[idx.z];
   8:  
   9:     const vector3 &na = m_norms[idx.x];
  10:     const vector3 &nb = m_norms[idx.y];
  11:     const vector3 &nc = m_norms[idx.z];
  12:  
  13:     // calc intensity
  14:     vector3 reala = (m * a);
  15:     vector3 realb = (m * b);
  16:     vector3 realc = (m * c);
  17:  
  18:     vector3 lighta = lightpos - reala;
  19:     vector3 lightb = lightpos - realb;
  20:     vector3 lightc = lightpos - realc;
  21:  
  22:     lighta.norm();
  23:     lightb.norm();
  24:     lightc.norm();
  25:  
  26:     float intensitya = na.dot(lighta);
  27:     float intensityb = nb.dot(lightb);
  28:     float intensityc = nc.dot(lightc);
  29:  
  30:  
  31:     if(intensitya < 0)
  32:         intensitya = BLACK_TEX_S_COORD;
  33:     if(intensityb < 0)
  34:         intensityb = BLACK_TEX_S_COORD;
  35:     if(intensityc < 0)
  36:         intensityc = BLACK_TEX_S_COORD;
  37:  
  38:     //DEBUG_LOG("intensity: %f, %f, %f", intensitya, intensityb, intensityc);
  39:  
  40:     glTexCoord2f(intensitya, 0);
  41:     glNormal3f(na.x, na.y, na.z);
  42:     glVertex3f(a.x, a.y, a.z);
  43:  
  44:     glTexCoord2f(intensityb, 0);
  45:     glNormal3f(nb.x, nb.y, nb.z);
  46:     glVertex3f(b.x, b.y, b.z);
  47:  
  48:     glTexCoord2f(intensityc, 0);
  49:     glNormal3f(nc.x, nc.y, nc.z);
  50:     glVertex3f(c.x, c.y, c.z);
  51:     
  52: }
  53: glEnd();
댓글
  • 프로필사진 초보자 좋은 정보 정말 감사합니다. 코드까지 놀랍네요. 2013.01.03 11:55 신고
  • 프로필사진 대다나다.. 와우.. 정말 이코드만 보고 공부해도 되겠어요 ㅠㅠ
    근데 왜 전 지형이 선으로만 나오져??
    2013.06.27 15:08 신고
  • 프로필사진 나그네 소스를 보니깐 디버그때는 선으로 표시하고 릴리즈일때는 텍스쳐가 나오게 해 놓으셨네요.. ㅎㅎ 릴리즈로 바꿔서 실행해 보세요 2013.07.03 11:07 신고
댓글쓰기 폼