Computer Graphics

Chapter07. GL-Viewing : 행렬&좌표계&모델 변환

CodeJB 2021. 6. 1. 16:52

Homogeneous 좌표계

우리는 3차원 공간상의 오브젝트의 위치를 나타낼 때 (x,y,z)좌표를 이용한다. 그리고 그 오브젝트는 수많은 정점(vertex)로 이루어져 있기 때문에 하나하나의 정점 모두가 3차원 좌표를 가지고 있을 것이다. 하지만 호모지니어스 좌표계에서는 또 하나의 좌표인 w를 제시하였으며 이제 (x,y,z,w)벡터를 이용해야 한다. 아래를 머릿속에 박아두자

  • w == 1이면, 벡터(x,y,z,1)은 공간상에서의 위치이다.
  • w == 0이면, 벡터(x,y,z,0)은 방향이다.

이제부터 하나하나 살펴보면서, 이 둘의 차이도 함께 알아보자


변환 행렬

3D 그래픽스에서는 4x4행렬을 주로 사용하며, 이들은 (x,y,z,w)버텍스들을 변형하게 해준다. 이는 버텍스를 행렬로 곱함으로써 이루어진다.
행렬 x 버텍스 (순서 중요) = 변형된 버텍스


평행이동 행렬

평행이동 행렬은 버텍스들의 위치를 이동시키기 위한 행렬이다. 평행이동 행렬은 아래와 같이 생겼다.

예를 들어, 정점의 벡터(10,10,10,1)을 X축 방향으로 10유닛(unit : 이동 단위, 개발자 마음)만큼 이동시키려면 아래와 같이 계산한다.

X에 10을 대입하고 나머지 Y,Z에 0을 대입한 다음 정점 좌표와 곱하게되면 결국 정점 좌표는 (20,10,10,1)이라는 호모지니어스 벡터를 얻게된다. 더 쉽게 생각하면 결국 X+10(덧셈)을 해준거나 다름이 없다
이번엔, 위치가 아니라 -z축으로의 방향을 나타내는 벡터를 표현하기 위해 (0,0,-1,0)을 이용해보자

오리지널 (0,0,-1,0)에서 값이 변환되지 않고 그대로 유지되는 모습을 볼 수 있다. 따라서, 위치 값이 변하지 않았다는 사실을 알 수 있다. 결국 -z축의 오리지날 방향 벡터를 그대로 유지하고 있는 모습을 볼 수 있다.


스케일링 매트릭스(크기변환 행렬)

스케일링 매트릭스는 아래와 같이 생겼다.

 
어떤 오브젝트의 크기를 2배 크게 변환시켜주고 싶다면 어떻게 해야할까? 

크기 변환은 위치와 방향이 상관 없기 때문에 그냥 w로 표기해도 무리가 없다고 생각한 것 같다. 어쨋든, 이동에서는 특정 축을 기준으로 덧셈을 해준 것과 같았으나 스케일링은 특정 축에 값을 곱셈해준 것과 같다고 볼 수 있다.


회전 매트릭스

회전에 대한 기하학적 원리는 매우 복잡하기 때문에 구체적으로 들어가면 끝도 없다.. 일단 기본적으로 삼각함수, 삼각함수의 합차공식등을 기초로 계산되어진다.


GL-Viewing Pipeline

렌더링시에 Object가 우리가 보는 화면에 그려지기 까지 몇가지 과정을 거치게 되는데, 이는 렌더링 파이프라인 혹은 그래픽스 파이프라인이라고도 한다. 그리고 우리는 이 파이프라인대로 코딩을 직접 하기 위해 OpenGL을 사용하고 있는데, OpenGL을 이용해서 특정 오브젝트를 화면에 그리기 위해서는 여러 함수를 호출해야하며 이를 GL-Vewing 파이프라인이라고 한다. 사실 렌더링, 그래픽스 파이프라인과 흡사하지만 OpenGL은 개발자가 직접 함수를 호출할 필요 없이 API상에서 처리해주는 몇몇 과정들이 있기 때문에, 비교적 간단해진다.(Rasterization 등이 생략되어있다.)

 
이 파이프라인대로 하나하나 알아볼 예정이며, 이번 시간에는 모델 변환에 대해서 알아볼 것이다. 일단 이 파이프라인의 흐름은 아래와 같다 :

  • 특정 Object를 기하변환 시키는 것이 모델변환이다.
  • 변환된 물체를 관찰하기 위해 카메라의 위치나 방향을 설정하는 것이 시점 변환이다
  • 카메라의 렌즈를 선택하고 촬영하여 물체의 2차원 영상을 필름에 맺히게 하는 것이 투상변환이다
  • 그렇게 해서 찍혀진 사진을 출력장치 좌표계로 매핑하여 2d디스플레이에 만들어낸다
  • 행렬의 내용은 프로그램에 의해 결정되지만 지정된 값에 의거하여 최종적인 기하변환을 가하는 것은 GL파이프라인 프로세서의 몫이다

좌표계

  • 모델 좌표계(MCS: Modeling Coordinate System = Local Coordinate System) : 물체별로 설정된 좌표계
  • 전역 좌표계(WCS : World Coordinate System) : 각각의 물체들에 대한 상대적 좌표계
  • 시점 좌표계(VCS : View Coordinate System) : 카메라를 기준으로 한 좌표계로 카메라의 초점(렌즈의 중심)을 원점, 카메라의 정면 광학축 방향을 Z축, 카메라 아래쪽 방향을 Y축, 오른쪽 방향을 X축으로 잡는다.

GL프로그램이 처음 시작될 때에는 MCS, WCS, VCS가 모두 같은 위치에 일치되어 있다.
여기서 필요에 따라 물체에 변환(기하 변환)이 가해지는데, 변환이 지니는 가장 큰 의미는  WCS와 MCS의 분리이다.
물체가 이동변환을 한다면, 이동과 동시에 전역 좌표계와 모델 좌표계는 별개의 좌표계로 분리되어 있는 것이다.
(본인의 손은 월드좌표로는 위도경도 상으로 존재하지만,  MCS좌표로는 위도경도가 아닌 좌표값으로 표현 가능한 것처럼)

예를 들어 물체가 이동변환을 했을 때, WCS = T * MCS로 행렬변환 T를 만들 수 있다. 결국 T는 물체를 T만큼 이동시켰다라는 것을 의미하는데, GL 입장에서는 "물체의 이동"이 아니라 "좌표계의 이동"으로 해석한다. 즉, "MCS가 T만큼 이동했다"가 아니라 "전역 좌표를 T만큼 이동시키면 새로운 모델좌표가 된다"라는 식으로 해석한다.

  • 변환과 동시에 WCS와 MCS가 분리됨
  • 변환 후에도 MCS 기준의 정점 좌표는 불변
  • 좌표계의 이동으로 간주함
  • 전역 좌표계를 (3,2,0)만큼 이동하면 모델 좌표계와 일치됨.
  • 전역 좌표계를 모델 좌표계로 일치시키기 위한 것이 변환행렬이다. == 모델 좌표계에 모델 행렬을 곱하면 전역좌표

즉, GL은 모델이 변환될 때, 전역좌표계의 원점을 모델 좌표계의 원점에 일치 시킨 뒤, 변환을 가한다.

모델변환 함수

모델 변환은 결국, 어떤 모델의 기하 변환을 가하고, 전역 좌표계를 모델 좌표계와 일치시시키는 과정이다. 이를 위한 함수는 아래와 같다.

void glMatrixMode(GLenum mode);
GL_MODELVIEW
: 모델 변환은 모델좌표에 모델행렬(변환행렬T)를 곱해 전역 좌표로 표현하는 것이며, 전역 좌표에 뷰 행렬(변환행렬)
: 을 곱해 시점 좌표로 바꾼다.
: GL은 모델 변환과 뷰 변환을 모델-뷰 변환으로 정의하였으며, 모델행렬*뷰행렬 = 모델 뷰 행렬로 취급한다.

GL_PROJECTION
: 투영 변환을 위한 인자로 추후에 다룬다

GL_TEXTURE
: 추후에 다룬다.
void glLoadIdentity()
: 상태 변수를 사용하는 GL에서는 항상 현재의 상태 변수, 즉 현 상태 변수 값이 중요하다.
: 위 함수를 호출하면 현 변환 행렬이 I(항등 행렬)로 바뀐다.
: 초기화 결과로, 모델 좌표계 전역 좌표계 시점 좌표계가 일치된다.
void glTranslatef(GLfloat dx,GLfloat dy,GLfloat dz);
void glScalef(GLfloat sx, GLfloat sy, GLloat sz);
void glRotatef(GLfloat angle, GLfloat x, GLfloat y);
:기하 변환 함수

모델변환  함수호출 순서 : 물체변환과 좌표계변환

glLoadIdentity();//CTM = I
glRotatef(Angle,x,y,z);// CM = I * R
glTranslatef(dx,dy,dz);// CTM = I * R * T
glutWireCube(0.1);

GL에서 모델 변환을 하기 위해 함수를 호출하는데, GL은 호출된 함수를 순서에 따라 두 가지로 관점으로 해석한다.

  • 코딩한 순서대로 => 좌표계 변환 : 모델 좌표계를 움직이는 방법임
  • 코딩 역순서대로 => 물체 변환 : 고정된 전역 좌표계를 중심으로 물체를 움직임.

위의  작성한 코드를 순서대로  해석하면  CTM = I, CTM = IR, CTM=IRT이된다.
하지만, 물체변환은 P' = CTM * P이므로, P' = IRTP가된다. 이렇게 점 P를 점 P'로 변환하는 것을 물체 변환이라고 한다.
반대로 좌표계 변환 관점으로 생각하면, 일단 전역좌표계와 모델좌표계가 처음엔 일치해있다. 전역 좌표계의 원점으로부터 모델좌표계를 회전하고 평행이동 시킨 후 얻게된 변환된 모델좌표계에 p를 그려내면 물체변환을 한 것과 같이 물체가 그려진다. 

행렬스택

CTM은 하나의 행렬이다. 복합적인 변환과정 R,T,S 등을 통하여 CTM = IRTS라는 행렬이 얻어졌다고 생각하면 CTM만 봐서는 어떠한 과정을 거쳤는지 전혀 알 수 없다. GL은 행렬이 변환됨에 따라 계속해서 변환되는 CTM의 현 상태를 스택구조로 저장하는 행렬스택을 제공한다.

drawArrow();
glRotatef(90,0,0,1);
drawArrow();
glRotatef(-90,0,0,1); //이건 뭐지
glRotatef(-90,0,1,0);

만약 위와 같은 코드가 있다고 가정하자, 위 코드에서 하고자하는 것은 아래와 같다.

  • 화살표를 그리고 z축을 기준으로 90도 회전한다.
  • 회전하지않은 오리지널 화살표를 그린다
  • 이번엔 y축을 기준으로 -90도 회전한다

그런데 주석 처리 한 부분에 "이건 뭐지"한 줄을 보자. 굳이 저것을 해준 이유는 무엇일까? 
그 이유는 좌표계를 z축 방향으로 90도 회전을 한 상태가 CTM이 되기 때문에 다시 원래대로 -90만큼 회전시켜 본래의 CTM으로 돌아가기 위함이다. 본래의 상태에서 다시 y축을 기준으로 -90만큼 회전시키고 싶어서이다. 즉, CTM은 내가 Rotation시키거나 Translate시킴에 따라 변환되기 때문에 다시 원래대로 돌려놓을 필요가 있다. 그때마다 저짓거리를 하기에는 너무 힘드니까 자료구조의 힘을 빌리는 것이다.

glPushMatrix();//행렬 스택에 push
    glRotatef(90, 0, 0, 1);
    drawXAxis();
glPopMatrix();

drawXAxis();

이렇게하면 90도만큼 회전된 CTM이 스택에 push됐다가 다시 Pop되면서 원래 상태의 CTM을 유지시킬 수 있다. 자료구조를 사용했기 때문에 속도측면에서도 효율이 좋고 가독성측면에서도 훨씬 좋다.
 

OpenGL 실습코드 : 로봇 그리기와 팔 움직이기

#include <OpenGL/OpenGL.h>
#include <GLUT/GLUT.h>
#include <iostream>

#define _WINDOW_WIDTH 800
#define _WINDOW_HEIGHT 800

int angle_upper = 0;
int angle_lower = 0;

int dir_upper = 1;
int dir_lower = 1;

void MyReshape(int width, int height){
    glViewport(0, 0, width, height);
    GLfloat f_w = (GLfloat)width / (GLfloat)_WINDOW_WIDTH;
    GLfloat f_h = (GLfloat)height / (GLfloat)_WINDOW_HEIGHT;
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-1.0*f_w, 1.0*f_w, -1.0*f_h, 1.0*f_h, -2, 2);
    gluLookAt(0.5, 0.5, 1, 0, 0, 0, 0, 1, 0);
}

void drawXAxis(){
    glBegin(GL_LINES);
        //선
        glVertex3f(0, 0, 0);
        glVertex3f(0.3, 0, 0);
        //선머리
        glVertex3f(0.3, 0, 0);
        glVertex3f(0.21, 0.09, 0);
        //선머리
        glVertex3f(0.3, 0, 0);
        glVertex3f(0.21, -0.09, 0);
    
    glEnd();
}

void drawAxis(){
    glColor3f(1, 1, 1);
   // glMatrixMode(GL_MODELVIEW);
   // glLoadIdentity(); // 초기화

    drawXAxis();
    glPushMatrix();//행렬 스택에 push
        glRotatef(90, 0, 0, 1);
        drawXAxis();
    glPopMatrix(); //모델 좌표계의 행렬스택중 Top Data삭제
    
    glPushMatrix();
        glRotatef(-90, 0, 1, 0);
        drawXAxis();
    glPopMatrix();
}

void drawCuboid(GLfloat sx, GLfloat sy, GLfloat sz){
    glPushMatrix();
        glScalef(sx, sy, sz);
        glutWireCube(1);
    glPopMatrix();
}

void drawBody(){
    drawAxis();
    drawCuboid(0.5,1,0.2);
}

void drawHead(){
    glPushMatrix();
    glTranslatef(0, 0.55, 0);
    drawAxis();
    drawCuboid(0.3, 0.1, 0.2);
    glPopMatrix();
}

void drawUpperArm(GLfloat Angle){
    glTranslatef(0.25,0.3, 0);
    glRotatef(Angle, 0, 0, 1);
    glTranslatef(0.25,0, 0);
    drawCuboid(0.5, 0.2, 0.2);
}

void drawLowerArm(GLfloat Angle){
    drawAxis();//Axis확인하면서 평행이동하면 편함
    glTranslatef(0.25,0, 0);
    glRotatef(Angle, 0, 0, 1);
    glTranslatef(0.25,0, 0);
    drawAxis();
    drawCuboid(0.5, 0.2, 0.2);
}

void drawHand(){
    glTranslatef(0.35, 0, 0);
    glutWireSphere(0.1, 15, 15);
}

void drawFinger1(){
    glPushMatrix();
        glTranslatef(0.15, 0, 0);
        drawCuboid(0.1, 0.05, 0.05);
    glPopMatrix();
}
void drawFinger2(){
    glPushMatrix();
        glRotatef(30, 0, 0, 1);
        glTranslatef(0.15, 0, 0);
        drawCuboid(0.1, 0.05, 0.05);
    glPopMatrix();
}

void MyDisplay(){
    glClear(GL_COLOR_BUFFER_BIT);
    //drawAxis();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    
    drawBody();
    drawHead();
    drawUpperArm(angle_upper);//timer에 따라 angle 변함
    drawLowerArm(angle_lower);//timer에 따라 angle 변함
    drawHand();
    drawFinger1();
    drawFinger2();
    //glFlush();
    glutSwapBuffers(); //애니메이션 사용을 위해서 Flush없애고 스왑버퍼 호출
}

void MyTimer(int value){
    angle_upper += dir_upper; //1도씩 움직이기
    angle_lower += dir_lower; //5도씩 움직이기
    
    if(angle_upper <= 0)
        dir_upper = 1;
    else if(angle_upper >= 60)
        dir_upper = 0;
    
    //std::cout << angle_upper << std::endl;
    if(angle_lower <= -70)
        dir_lower = 5;
    else if(angle_lower >= 100)
        dir_lower = -5;
        
    glutTimerFunc(20, MyTimer, 1);
    glutPostRedisplay();
}

int main(int argc, char ** argv) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE);
    glutInitWindowSize(_WINDOW_WIDTH, _WINDOW_HEIGHT);
    glutCreateWindow("title");
    
    //콜백
    glutDisplayFunc(MyDisplay);
    glutReshapeFunc(MyReshape);
    
    glutTimerFunc(20, MyTimer, 1);
    
    glutMainLoop();
}
  • drawAxis에서 x,y,z축을 시각적으로 보여주고 있는데, 항등행렬로 초기화 시키지 않고 여러 메서드에서 기하변환을 가할 때마다 호출함으로써 좌표축의 변환을 확인할 수 있다. 이를 통해서 기하변환의 평행이동,회전을 더 순조롭게 파악할 수 있다.
  • drawAxis를 통해 행렬스택의 CTM을 push해줄지 pop해줄지를 결정해야한다. 
인사하듯이 팔을 흔든다
출처 :  http://www.opengl-tutorial.org/kr/beginners-tutorials/tutorial-3-matrices/
출처 : https://wjdgh283.tistory.com/entry/OpenGL%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EC%BB%B4%ED%93%A8%ED%84%B0-%EA%B7%B8%EB%9E%98%ED%94%BD%EC%8A%A4-Chapter-06-%EB%AA%A8%EB%8D%B8%EB%B3%80%ED%99%98%EA%B3%BC-%EC%8B%9C%EC%A0%90%EB%B3%80%ED%99%98
출처 : 
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=dnjsgk0206&logNo=198348769
출처 : http://lernen628.blogspot.com/2015/01/opengl.html