본문 바로가기

Programming/OpenGL

OpenGL Transformation_Matrix

실질적인 OpenGL 코딩 시 처리되는 행렬에 관한 포스팅 이에요. 여태까지 왜 행렬에 대해서 공부했는지, 행렬이 왜 필요한지에 대해서 엄청 많이 언급 했어요. 이해가 안 되시는 분들은 지난 포스팅을 참조해 주세요 :)

단순히 하나의 행렬이 아닌, 여러 개의 행렬이 사용 된다는 것은 봐왔어요. 그럼 이게 내부적으로 어떤식으로 동작하는 지에 대해서 설명 할게요.

행렬은 앞으로 연산이 될 순번에 맞춰서 스택에 쌓이게 됩니다. (스택에 대해서 모르시는 분들은 구글링 ㄱㄱㄱ!!) 스택은 아래와 같이 세가지 종류가 있으며, 각기 연산된 행렬에 의해서 우리의 화면에 결과가 뿌려지는 거죠.

  • The modelview matrix stack
  • The projection matrix stack
  • The texture matrix stack

스택의 필수 불가결 적인 함수인 push와 pop은 OpenGL에서 아래의 함수로 동작 한답니다.
void glPushMatrix(void);
void glPopMatrix(void);


인자와 반환 값은 없으므로 처리에 대해서만 설명을 드릴게요. 그림을 보면서 설명 하는게 이해가 더 빠르겠죠? :D


OpenGL내부적으로 구현되어 있는 행렬 이에요. 언제 버전에 관련된 건지는 모르겠지만… 책의 출판년도가 2000년도 인걸 감안하면… 음.. :)

처음에 불러오게 되면 모든 정점(또는 벡터)들의 정보는 (0, 0, 0)을 기준으로 놓이게 되요. 저번 포스팅(Understanding Coordiante Transformations)에서 나무랑 별을 예제로 든 설명이 바로 그거에요. 변환 행렬을 이용해서 정점들이나 카메라 등을 원하는 위치에 놓아 주는 거죠. 그때 설명이 미흡했던 부분이 묶어 준다라는 개념 있었죠. 그게 push와 pop 입니다.


너무 많은 스택을 쌓게(Push) 되면, OpenGL은 GL_STACK_OVERFLOW 에러를 알려 주게 되고,
스택이 없음에도 불구하고 빼내(Pop)려고 하면GL_STACK_UNDERFLOW 에러를 알려 줍니다:D

이런 Push와 Pop은 각각의 오브젝트들이 별개로 위치하거나 움직여야 할 때 사용 되는 거에요. 그건 단순히 단일 오브젝트에만 적용 되는게 아니라 하나의 오브젝트가 여러 개의 오브젝트를 포함 할 때에도 적용 되는 거죠. 레고 로봇으로 예를 들어 볼게요.

로봇은 머리, 몸통, 팔(2개), 다리(2개) 로 이루어진 오브젝트에요. 각각의 디테일한 형태는 현재 넣지 말고 그냥 정육면체라고 할게요.그럼 정육면체 1개를 그리는 것만 가지고 있으면 로봇전체를 표현 할 수 있겠죠?

일단 정육면체 하나를 만들어야겠죠? 어떠한 방법을 써서 만들어도 아직은 상관 없지만, 저는 Polygon을 이용해서 만들어 볼게요.
서적에서는 main하나에서 통째로 프로그램을 만들었는데, 지금 저는 각기 클래스로 별도 분할에서 만들고 있으므로 전체 코드를 보는 건 추후에 진행하도록 할게요. 아마 이 포스팅을 볼 수 있는 배경지식 정도면 이미 자신의 코드를 만들고 필요한 부분만 붙여 넣는 것만으로도 충분히 만들 수 있을거라고 생각됩니다. 그래도 이해가 안되면 일단은 댓글을 달아주세요 :D


일단 정육면체 하나를 만드는 코드는 아래와 같이 작성 했어요.
void DrawCube(const float _color[3] = {1.0f, 1.0f, 1.0f})
{
	glColor3f(_color[0], _color[1], _color[2]);
	glBegin(GL_POLYGON);
	glVertex3f(0.0f, 0.0f, 0.0f); // top face
	glVertex3f(0.0f, 0.0f, -1.0f);
	glVertex3f(-1.0f, 0.0f, 0.0f);
	glVertex3f(-1.0f, 0.0f, 0.0f);

	glVertex3f(0.0f, 0.0f, 0.0f); // front face
	glVertex3f(-1.0f, 0.0f, 0.0f);
	glVertex3f(-1.0f, -1.0f, 0.0f);
	glVertex3f(0.0f, -1.0f, 0.0f);

	glVertex3f(0.0f, 0.0f, 0.0f); // right face
	glVertex3f(0.0f, -1.0f, 0.0f);
	glVertex3f(0.0f, -1.0f, -1.0f);
	glVertex3f(0.0f, 0.0f, -1.0f);

	glVertex3f(0.0f, 0.0f, 0.0f); // right face
	glVertex3f(0.0f, -1.0f, 0.0f);
	glVertex3f(0.0f, -1.0f, -1.0f);
	glVertex3f(0.0f, 0.0f, -1.0f);

	glVertex3f(-1.0f, 0.0f, 0.0f); // left face
	glVertex3f(-1.0f, 0.0f, -1.0f);
	glVertex3f(-1.0f, -1.0f, -1.0f);
	glVertex3f(-1.0f, -1.0f, 0.0f);

	glVertex3f(0.0f, 0.0f, 0.0f); // bottom face
	glVertex3f(0.0f, -1.0f, -1.0f);
	glVertex3f(-1.0f, -1.0f, -1.0f);
	glVertex3f(-1.0f, -1.0f, 0.0f);

	glVertex3f(0.0f, 0.0f, 0.0f); // back face
	glVertex3f(-1.0f, 0.0f, -1.0f);
	glVertex3f(-1.0f, -1.0f, -1.0f);
	glVertex3f(0.0f, -1.0f, -1.0f);

	glEnd();
}


그리고 실제로 그려지는 부분에서 아래와 같이 코드를 삽입하면 되겠죠.
float col_body[3] = {1.0f, 0.0f, 0.0f}; // 몸 색
float col_head[3] = {0.0f, 1.0f, 0.0f}; // 머리 색
float col_left_arm[3] = {0.0f, 0.0f, 1.0f}; // 왼팔 색
float col_right_arm[3] = {0.0f, 1.0f, 1.0f}; // 오른팔 색
float col_left_leg[3] = {1.0f, 0.0f, 1.0f}; // 왼다리 색
float col_right_leg[3] = {1.0f, 1.0f, 0.0f}; // 오른다리 색

DrawCube(col_body); // 몸통
DrawCube(col_head); // 머리
DrawCube(col_left_arm); // 왼팔
DrawCube(col_right_arm); // 오른팔
DrawCube(col_left_leg); // 왼다리
DrawCube(col_right_leg); // 오른다리


하나의 정육면체를 여섯 번 그리면 일단 6개의 정육면체를 그릴 수가 있어요. 별다른 변환을 취하지 않으면 각기 다른 정육면체 6개가 같은 위치에 겹쳐서 나오게 되니까 아래와 같은 모습이 될 거에요.


이제 각각의 위치를 달리 해 볼까요? 이때 사용하는 게 MatrixPush, Pop 입니다. 서로 다른 위치에 적용 되게끔 묶어 주는 거죠. 몸통을 기준으로 해야 하니까 몸통은 가만히 두도록 하죠.

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
DrawCube(col_body); // 몸통

glPushMatrix();
glTranslatef(0.0f, 1.2f, 0.0f);
DrawCube(col_head); // 머리
glPopMatrix();

glPushMatrix();
glTranslatef(-2.2f, 0.0f, 0.0f);
DrawCube(col_left_arm); // 왼팔
glPopMatrix();

glPushMatrix();
glTranslatef(1.2f, 0.0f, 0.0f);
DrawCube(col_right_arm); // 오른팔
glPopMatrix();

glPushMatrix();
glTranslatef(-0.8f, -1.2f, 0.0f);
DrawCube(col_left_leg); // 왼다리
glPopMatrix();

glPushMatrix();
glTranslatef(0.8f, -1.2f, 0.0f);
DrawCube(col_right_leg); // 오른다리
glPopMatrix();


적당한 값으로 설정해 주시면 되요 :D 이렇게 설정된 박스들은 아래 처럼 보이게 되요.


카메라의 위치도 원하는 곳으로 옮겨 보아요. 현재 저는 상하좌우앞뒤로 이동하는 카메라 클래스를 별도로 구현해 놓아서, Runtime중에 조정하고 있어요. 아직 회전에 대한 부분은 적용시키지 않아서 그냥 쿼터뷰 시점만 지원 하고 있구요ㅋㅋㅋ


너무 1:1:1의 비율은 맞지 않으니까 비율을 좀 조정해 볼까요? 확대 변환에 관련된 함수가 있었죠. glScalef(..) 함수! 이제 이 함수를 사용해서 다시 각각의 부품(?!)들의 크기를 바꿔 볼게요. 이 과정에 있어서 물체가 너무 커지거나 작아지게 되면 다시 위치를 수정할 필요가 있을 거에요. :D
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
DrawCube(col_body); // 몸통

glPushMatrix();
glScalef(0.5f, 0.5f, 0.5f);
glTranslatef(0.0f, 1.2f, 0.0f);
DrawCube(col_head); // 머리
glPopMatrix();

glPushMatrix();
glScalef(0.5f, 1.5f, 0.5f);
glTranslatef(-2.2f, 0.0f, 0.0f);
DrawCube(col_left_arm); // 왼팔
glPopMatrix();

glPushMatrix();
glScalef(0.5f, 1.5f, 0.5f);
glTranslatef(1.2f, 0.0f, 0.0f);
DrawCube(col_right_arm); // 오른팔
glPopMatrix();

glPushMatrix();
glScalef(0.5f, 1.5f, 0.5f);
glTranslatef(-0.8f, -1.2f, 0.0f);
DrawCube(col_left_leg); // 왼다리
glPopMatrix();

glPushMatrix();
glScalef(0.5f, 1.5f, 0.5f);
glTranslatef(0.8f, -1.2f, 0.0f);
DrawCube(col_right_leg); // 오른다리

glPopMatrix();


어느정도 모양새는 갖춰 졌네요. 뭐… 제가 일일이 찾아서 고치면 되는 거긴 한대… 서적에서 제시하는 예시의 상자 모양이 (0, 0, 0)을 중심으로 균일하게 뻗은 모양의 상자가 아니라 (-1, -1, -1)쪽으로 향하는 상자라 이동하고 확대 하는데 있어서 의도하는 모양이 나오지 않네요 :D(제가 완벽 주의자는 아니지만 괜시리 어긋나는거 보면 딱히 기분이 개운치는 않기 때문에요ㅋㅋ)

로봇이 움직이지 않고 마네킹 처럼 서 있다는 가정하에 고정된 오브젝트를 보이는 건 완성했어요~ 단 한 개의 큐브로 말이죠! (+ _+)

가만히 멀뚱 서있는거 재미 없으니까 움직여 볼까요? :D

서적을 따라 가고 있기는 하지만 애니메이션화 시키는 부분이 정말 맘에 안 들게 main에서 모든 변수를 집어넣고 만들어야 하는 부분이 정말 맘에 들지는 않아요. 이후 챕터의 제목을 쭉 훑어 봤는데 애니메이션에 관련된 부분이 언급된 건 없더라구요..
저는 일단 클래스화 시켜서 만들고는 있는데 이 부분 또한 확장성이 용이 하도록 수정해야 해서 지금 당장 공개는 하지 않으려고 합니다. 원리에 대한 설명만 하고 어떻게 할지는 고민을 하셔야 될거 같아요 ㅠ


일단 적용시킬 행동은 "걷다" 에요. 일반적으로 걸을 때는 (왼팔 - 오른다리), (오른팔 - 왼다리)가 한 쌍이 되어서 앞(또는 뒤)으로 x축, 즉 오브젝트의 왼쪽 방향을 기준으로 회전을 하게 됩니다. 무한전 회전하게 되면 팔과 다리가 계속 돌아가면서 보이게 되므로 한계치가 정해져야 겠죠? 저는 앞, 뒤 30도로 제한 시켰습니다.



만들었던 정육면체는 위와 같은 모습을 하고 있는데 이를 x축을 기준으로 +0.5도씩(저는 이렇게 했어요) 앞쪽으로 가다가 30도 이상 회전 됐으면 다시 -0.5도씩 회전시키다가 다시 -30도 이상 회전 됐으면 다시 +0.5도씩, … 반복되는 형식으로 했어요.

아까 말했던 팔과 다리의 쌍은 서로 반대로 움직여야 겠죠? 쌍1이 +회전이면, 쌍2는 -회전이 되어야 하는 거죠.

전역 변수로 아래와 같은 변수들을 추가 합니다.
const float rotate_max = 30.0f;
const float rotate_min = -30.0f;
bool leftArm_positive = true; // true면, 왼팔-오른다리 쌍이 증가 회전, 반대면 감소 회전
float leftArm_rotate_angle = 0.0f; // 왼팔-오른다리 기준 총 회전된 각도
// OpenGL은 총 회전된 각도가 아니라 단순히 한번에 회전시킬 각도만을 주게 되므로
// 그에 대해서 얼마나 "총(total)"에 대해 얼마나 가지고 있었는지가 필요하다.

이제 로봇을 그렸던 각 부분에 조건부를 넣고 그만큼 회전시키는 코드를 중간중간 삽입하는 거죠.
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
DrawCube(col_body); // 몸통

glPushMatrix();
glScalef(0.5f, 0.5f, 0.5f);
glTranslatef(0.0f, 1.2f, 0.0f);
DrawCube(col_head); // 머리
glPopMatrix();

glPushMatrix();
if(leftArm_positive)
	glRotatef(0.5f, 1.0f, 0.0f, 0.0f); // x축 기준으로  0.5도 회전
else
	glRotatef(-0.5f, 1.0f, 0.0f, 0.0f); // x축 기준으로 -0.5도 회전
	
glScalef(0.5f, 1.5f, 0.5f);
glTranslatef(-2.2f, 0.0f, 0.0f);
DrawCube(col_left_arm); // 왼팔
glPopMatrix();

glPushMatrix();
if(leftArm_positive)
	glRotatef(-0.5f, 1.0f, 0.0f, 0.0f); // 오른팔은 왼팔과 다른 방향으로 움직여야 하므로
else
	glRotatef(0.5f, 1.0f, 0.0f, 0.0f);
	
glScalef(0.5f, 1.5f, 0.5f);
glTranslatef(1.2f, 0.0f, 0.0f);
DrawCube(col_right_arm); // 오른팔
glPopMatrix();

glPushMatrix();
if(leftArm_positive)
	glRotatef(-0.5f, 1.0f, 0.0f, 0.0f); // 왼다리는 오른팔과 쌍이므로 그것과 같은 회전
else
	glRotatef(0.5f, 1.0f, 0.0f, 0.0f);
	
glScalef(0.5f, 1.5f, 0.5f);
glTranslatef(-0.8f, -1.2f, 0.0f);
DrawCube(col_left_leg); // 왼다리
glPopMatrix();

glPushMatrix();
if(leftArm_positive)
	glRotatef(0.5f, 1.0f, 0.0f, 0.0f); // 오른다리는 왼팔과 쌍이므로 그것과 같은 회전
else
	glRotatef(-0.5f, 1.0f, 0.0f, 0.0f);
	
glScalef(0.5f, 1.5f, 0.5f);
glTranslatef(0.8f, -1.2f, 0.0f);
DrawCube(col_right_leg); // 오른다리
glPopMatrix();

// 한번 그리는게 다 끝났으므로 왼팔 기준 앞뒤가 바뀌는지 확인
if(leftArm_positive)
{
	leftArm_rotate_angle += 0.5;
	if(leftArm_rotate_angle >= rotate_max)
	{
		leftArm_positive = !leftArm_positive;
	}
}
else
{
	leftArm_rotate_angle -= 0.5f;
	if(leftArm_rotate_angle <= rotate_min)
	{
		leftArm_positive = !leftArm_positive;
	}

}


이렇게 하면 로봇이 걷게 되는 모습이 보이게 되죠!!


일단, 제가 생각하는 애니메이션 오브젝트를 만들 때 문제점이 두 가지가 있어요.
  • 복잡한 오브젝트가 아님에도 불구하고 재생되는 속도가 다르다.
  • 애니메이션을 만들 때는 일일이 저런 코드를 삽입해야 한다.

첫 번째 문제에 대해서는 지금 실행되고 있는 프로세스가 CPU를 얼마나 점유하고 있느냐에 따라서 다르게 되는 건데, 현재에서는 크게 문제 되지는 않는 내용이에요. 아무리 슈퍼컴퓨터에서 게임을 돌리더라도 너무 빠르게 되면 플레이를 하기가 어렵게 되잖아요? 그래서 그걸 적정 프레임으로 맞춰주는 작업을 하게 됩니다.(서적에서 그걸 할지 안 할지는 모르겠지만요;;;) 물론 느린 컴퓨터에서도 마찮가지로 느리게 보이더라도 적정프레임을 유지해야 플레이가 원활 하겠죠? :)

두 번째 문제는 찾아보니까 매우 많은 방법론이 있더라구요. 물론 구현에 대한 것은 잘 나오지 않는 걸로 봤을 때, 제가 잘 못 찾는 가능성과 기술자체가 돈인지라 공개를 안하는 것에 대한 가능성이 있어서요ㅎ 이 부분은 조금 더 생각을 해봐야 할 거 같아요ㅠㅠㅠ


허접하지만 그럴싸한 오브젝트도 만들어 봤고, 그에 대한 애니메이션도 넣어 봤어요. 여기에서 가장 키포인트는 행렬에 대한 부분이에요. 코드상에서 몸통~머리를 그리는 부분까지만 그림과 함께 설명을 할게요.

  1. 처음에는 ModelView 행렬 스택을 사용한다고 하고 나서, 가장 기본적인 단위행렬이 들어가요. 이후에는 나오는 정점들의 정보는 모두 그 자체의 좌표값들을 표현하게 되는 거죠.


  2. 아직까지 단위 행렬만 들어가 있으니까 DrawCube(..)(함수 내부는 위쪽에서 보세요 :D)를 하게 되도 그 값이 그대로 나오겠죠.


  3. 이제 부터 나오는 행렬관련 함수들은 스택에 "Push" 할거라는 의미 에요.


  4. 확대 변환 행렬을 사용했군요! 이게 이제 스택에 들어 갑니다.


  5. 이동에 대한 변환도 했네요~ 이거 역시 스택에 들어가요.


  6. 이제 상자를 그려보라는 명령이 들어 왔어요. 이때에는 들어가게 되는 정점들의 정보는 행렬 스택에 쌓여있는 결과 행렬과 계산된 좌표로 바뀌어서 나오게 되죠.


  7. pop 하라는 명령이 들어오면, push 하기 시작한 부분부터의 행렬들이 모두 나가게 됩니다.


  8. 이후 과정 반복 :D

이 메커니즘이 OpenGL이 돌아가는(DirectX 도 그런진 모르겠지만ㅋ) 과정이에요. 사실 사용법은 함수 콜로 이루어져 있어서 어렵지는 않지만, 내부적으로 돌아가는 과정을 알아 두면 추후에 단순히 함수콜을 이용해서 하는 거 말고도 행렬 자체를 조작하는 부분에서 진가를 발휘할 거에요:D(전 아직 못해요ㅋㅋㅋㅋ)

참조 : OPENGL GAME PROGRAMMING(Foreword by Mark J.Kilgard)