본문 바로가기
전공/컴퓨터 그래픽스

모델변환과 시점변환 (4)지엘의 시점변환 & (5)3차원 회전

by 임 낭 만 2023. 4. 23.

지엘(GL) 의 시점변환

  • 시점 좌표계 (VCS : View Coordinate System)

더보기
더보기

시점 좌표계카메라 좌표계라고도 한다. 물체는 항상 모델 좌표계에서 설계되어 모델 좌표로 전시된다. 전역 좌표계를 모델 좌표계와 일치 시키는 변환이 바로 모델 행렬이다. 이와 같이 전역 좌표계에 뷰 행렬을 곱하면 시점 좌표가 된다. 역으로 말하면, 시점 좌표와 전역 좌표계를 일치시키는 변환이 바로 뷰 행렬이다. 시점 좌표계에서 전역 좌표계로 가는 변환 행렬을 만들기 위해서는 먼저 시점 좌표계를 설정해야 한다. 


시점 좌표계 설정

단순 시스템

  • 카메라 위치 = 시점 좌표계 원점
  • 전역 좌표계 원점을 향한 방향이 시점 좌표계의 z축
  • z축에 수직으로 서 있는 면 = 투상면 (Projection Plane, View Plane)
  • 투상면 내부에 뷰 윈도우 (View Window) = 카메라 필름
  • 시점 좌표계 y축 = 뷰 윈도우의 y축과 평행
  • y-z 평면에 수직인 방향으로 x축
더보기
더보기

가장 단순한 방법. 카메라 위치가 시점 좌표계의 원점. 카메라는 전역 좌표계의 원점을 바라보고 있으며, 이 방향이 VCS의 z방향이 된다. 카메라와 원점 사이에, z축에 수직으로 서 있는 면이 투상면이며 여기에 나중에 물체 영상이 맺히게 된다. 투상면 자체의 크기는 무한대이므로, 투상면 안에 뷰 윈도우라는 사각형을 만들어서 그 내부에 맺힌 영상만을 출력하게 된다. 따라서 뷰 윈도우는 카메라의 필름에 해당한다.

픽사에서 개발한 렌더맨(Renderman) 시스템

  • 카메라가 바라보는 점 (물체 위의 한 점) = 초점 (Focus, Target)
  • 초점을 향한 방향이 z축
  • 나머지는 단순 시스템과 유사함
  • z축을 중심으로 카메라가 회전 가능
    • 롤링 (Roll, Rolling)
    • 초점은 고정한 채 카메라가 시선 방향인 z축을 기준으로 회전
더보기
더보기

픽사(Pixar)에서 개발한 렌더맨 소프트웨어는 이보다 자유도를 높인 것이다. 단순 시스템에서는 카메라가 WCS의 원점을 바라보고 있기 때문에, 물체가 원점에서 멀어지면 이를 잡아낼 수 없다. 랜더맨의 카메라는 물체의 한 점을 향한다. 카메라가 바라보는 이 점을 초점이라고 한다. 시점 좌표계의 z, y, x축, 투상면, 뷰 윈도우는 단순시스템에서와 같이 설정된다. 렌더맨 시스템에서는 VCS의 z축을 중심으로 카메라가 회전할 수 있다. 이를 롤링이라 한다. 카메라의 위치와, 초점의 위치는 고정한 채 카메라를 한 바퀴 돌리는 것이다.

렌더맨(Renderman) 시스템

비행 시뮬레이션

롤 (Roll) = z축을 기준으로 x축이 회전하는 각도로 무게중심을 좌우로 이동

피치 (Pitch) = x축을 기준으로 z축이 회전하는 각도로 고도를 위아래로 조절

요 (Yaw) = y축을 기준으로 z축이 회전하는 각도로서 진행방향을 좌우로 조절

  • 조종사는 단지 3개의 각만을 사용하여 비행 방향을 지정
  • 각도 변화에 따라서 새로운 축을 x, y, z축으로 하는 시점 좌표계가 형성
  • 조종사의 눈에 보이는 모든 물체는 변화된 새로운 시점 좌표계를 기준으로 변환
더보기
더보기

비행 시뮬레이션에서 조종사는 3개의 각을 사용하여 비행 방향을 지정하며, 조종사 12시 방향이 z축, 날개가 x축, 머리 방향이 y축이 된다. 이 각들을 변화시킴으로써 비행기 진로를 원하는 곳으로 변경할 수 있다. 카메라로 말하자면 카메라가 3가지 자유도를 가지고 회전함을 의미한다.


지엘의 시점 좌표계

void gluLookAt(GLdouble eyex, GLdouble eyey, GLdouble eyez, GLdouble atx, GLdouble aty,
	GLdouble atz, GLdouble upx, GLdouble upy, GLdouble upz);
  • 파라미터
    • 카메라 위치
    • 카메라가 바라보는 점, 즉 초점의 위치
    • 카메라 기울임 (Orientation)
void gluLookAt(0.0, 0.0, 0.0, 0.0, 0.0, -100.0, 0.0, 1.0, 0.0);
//시점 좌표계는 전역 좌표계와 일치 (기본값)
더보기
더보기

지엘의 시점 좌표계는 위 3가지 파라미터에 의해 정의된다. 또한, eye는 카메라 위치, at은 초점의 좌표, up은 기울기, 카메라의 상향벡터에 해당한다. gluLookAt 함수의 파라미터는 전역 좌표로 표시해야 한다. default값의 시점 좌표계는 전역 좌표계와 일치하며, 카메라 방향은 -z 방향을 바라보되, 기울어지지 않고 정면을 주시한다.

따라서 카메라 위를 향하는 상향벡터는 (0, 1, 0)으로 표시된다.

시점 좌표계의 z축은 eye좌표에서 at좌표로 가는 방향의 벡터 (시선 방향의 벡터), y축은 카메라 상향 벡터를 시선에 수직인 평면에 투상한 것이며, x축은 이 z축과 y축에 수직으로 정의된다.

뷰 행렬 : 시점 좌표계(VCS)를 전역 좌표계(WCS)로 일치시키는 행렬

모델 행렬 : 전역좌표계(WCS)를 모델좌표계(MCS)로 일치시키는 행렬

PWCS = MㆍPMCS

PVCS = VㆍPWCS = VㆍMㆍPMCS

  • 시점 변환 함수의 위치
    • glMatrixMode(GL_MODELVIEW);
    • glLoadIdentiy();                                                                      I
    • gluLookAt(0.2, 0.0, 0.0, 0.0, 0.0, -100.0, 1.0, 1.0, 0.0);         V : 시점 좌표계 분리
    • glRotatef(45, 0.0, 1.0, 0.0);                                                    M : 모델 좌표계 분리
    • glutWireCube(1.0);                                                                 PMCS
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(0.2, 0.0, 0.0, 0.0, 0.0, -100.0, 1.0, 1.0, 0.0);
glRotatef(45, 0.0, 1.0, 0.0);
glutWireCube(1.0);
더보기
더보기

뷰 행렬과 모델 행렬과의 관계를 살펴보자. 뷰 행렬은 gluLookAt 함수에 의해 정의된 시점 좌표계를 전역 좌표계에 일치시키는 행렬이다. 한편 모델 행렬을 전역 좌표계를 모델 좌표계에 일치시키는 행렬이기 때문에, 뷰 행렬에 모델 행렬을 곱한 것. V*M이 모델 뷰 행렬이 된다. 결국 뷰 행렬은 모델 행렬 왼쪽에 곱해져야 한다.

gluLookAt 함수는 모델 뷰 행렬 초기화 이후에 호출. 시점 좌표계가 따로 설정되지 않아다면, 뷰행렬이 항등행렬 I라는 것.

#include <GL/freeglut.h>

void InitLight() {	//조명 설정 함수
    GLfloat mat_diffuse[]    = { 0.5, 0.4, 0.3, 1.0 };	//물체의 난반사 색상 (물체가 빛을 받아서 일반적으로 반사하는 빛)
    GLfloat mat_specular[]   = { 1.0, 1.0, 1.0, 1.0 };	//물체의 정반사 색상 (빛을 반사하는 각도에 따라서 반사되는 빛)
    GLfloat mat_ambient[]    = { 0.5, 0.4, 0.3, 1.0 };	//물체의 주변광 색상 (주변에서 반사되어 들어오는 빛)
    GLfloat mat_shininess[]  = { 15.0 };		//물체의 광택 정도를 나타내는 값 설정
    GLfloat light_diffuse[]  = { 0.8, 0.8, 0.8, 1.0 };	//조명의 난반사 색상
    GLfloat light_specular[] = { 1.0, 1.0, 1.0, 1.0 };	//조명의 정반사 색상
    GLfloat light_ambient[]  = { 0.3, 0.3, 0.3, 1.0 };	//조명의 주변광 색상
    GLfloat light_position[] = { -3, 6, 3.0, 0.0 };		//조명의 위치 설정

    glShadeModel(GL_SMOOTH);	//부드러운 쉐이딩 (그래픽 오브젝트의 표면색을 부드럽게 처리)을 사용하도록 설정
    glEnable(GL_DEPTH_TEST);	//깊이 테스트를 활성화함
    glEnable(GL_LIGHTING);		//조명을 사용하도록 설정
    glEnable(GL_LIGHT0);		//첫 번째 조명을 사용하도록 설정
    glLightfv(GL_LIGHT0, GL_POSITION, light_position);	//조명의 위치를 설정
    glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);	//조명의 난반사 색상 설정
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);	//조명의 정반사 색상 설정
    glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);	//조명의 주변광 색상 설정
    glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);	//물체의 난반사 색상 설정
    glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);	//물체의 정반사 색상 설정
    glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);	//물체의 주변광 색상 설정
    glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess); //물체의 광택 정도 설정
}
void MyDisplay() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 1.0, 1.0, 0.0);
    glutSolidTeapot(0.5);	//0.5크기의 주전자 그리기
    glFlush();
}
void MyReshape(int w, int h) {
    glViewport(0, 0, (GLsizei) w, (GLsizei) h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);
}
int main(int argc, char** argv) {
    glutInit(&argc, argv);			//glut라이브러리 초기화
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA | GLUT_DEPTH);
    glutInitWindowSize(400, 400);		//윈도우의 초기 크기를 400x400픽셀로 설정
    glutInitWindowPosition(0, 0);		//윈도우 초기 위치 지정 (좌측 상단)
    glutCreateWindow("OpenGL Sample Drawing");	//실제 윈도우 생성
    glClearColor(0.4, 0.4, 0.4, 0.0);		//화면 색상 설정
    InitLight();
    glutDisplayFunc(MyDisplay);			//디스 플레이 콜백 함수 등록
    glutReshapeFunc(MyReshape);			//리셰이프 콜백 함수 등록
    glutMainLoop();				//이벤트 실행
    return 0;
}


시점 설정에 의한 애니메이션

  • 오비팅 (Orbiting)
    • 물체를 중심으로 카메라를 회전시키는 것
    • gluLookAt() 함수를 사용할 경우에 초점과 일치하게 되면 시선벡터는 (0, 0, 0)으로 정의되지 않음
  • 극 좌표 (Polar Coordinate System)

 

P(radius, azimuth, elevation)

z축을 기준으로 반시계 방향으로 회전하는 각도가 방위각 (azimuth)

x축을 기준으로 회전하는 각도가 고도각 (Elevation)

void PolarView(GLfloat radius,  GLfloat elevation, Glfloat azimuth, Glfloat twist) {
      glTranslatef(0.0, 0.0, -radius);        // 시점좌표계를 전역좌표계로 일치시키기 위함
      glRotatef(-twist, 0.0, 0.0, 1.0);       // 이동된 시점 좌표계의 z축을 기준으로 x-y축 방향이 회전 
      glRotatef(-elevation, 1.0, 0.0, 0.0);   // 두 좌표계의 z축이 일치
      glRotatef(-azimuth, 0.0, 0.0, 1.0);     // y축이 일치함으로써 나머지 x축은 자동으로 일치
}

카메라 위치가 직교 좌표계가 아닌 극좌표로 표시되어 있다면 극 좌표계를 시점 좌표계로 바꿀 때 위와 같은 함수를 사용해서 극좌표로 표시된 시점의 위치를 직접 모델 뷰 행렬에 반영할 수 있다.

  • 비행 시뮬레이션
void PilotView(GLfloat roll,  GLfloat pitch, Glfloat yaw) {
      glRotatef(roll, 0.0, 0.0, 1.0);             
      glRotatef(pitch, 0.0, 1.0, 0.0); 
      glRotatef(yaw, 1.0, 0.0, 0.0);    
      glTranslatef(-x, -y, -z);      
}

비행 시뮬레이션에서 조종사 눈에 보이는 광경을 촬영하려면 이 함수를 사용하면 된다. 여기서 x, y, z는 원점으로부터 비행기까지의 거리를 의미한다

더보기
더보기

애니메이션에는 물체를 움직이거나, 카메라를 움직이는 방법이 사용된다. 특히 물체를 중심으로 카메라를 회전시키는 것을 오비팅이라고 한다. gluLookAt 함수로 카메라를 이리저리 움직이다 보면, 초점과 카메라 좌표가 일치해버릴 때가 있고, 시선 벡터가 (0, 0, 0)이 되어버리기 때문이다. 따라서 일부 애니메이션에서는 gluLookAt함수 대신 직접 시점 변환 함수를 설정하기도 한다.

응용 프로그램에 따라서는 카메라의 위치를 잡을 때 직교 좌표계 대신 극좌표계를 사용하기도 한다. 극좌표계는 반지금, 방위각, 고도각으로 표시된다. 반지름이 결정되면 물체는 원점을 중심으로 해당 반지름을 지닌 구면상에 존재한다. 따라서 극좌표계상의 점 P는 원점, 반지름, 방위각, 고도각으로 표시할 수 있다. 애니메이션에서 카메라의 상하 회전을 틸팅 (Tilting), 좌우 회전을 팬(Panning), 물체는 가만 있고, 카메라가 시선을 따라 물체의 앞뒤로 움직이는 것을 돌리(Dolly)라고 한다.


3차원 회전

3차원 회전 - 오일러 각 (Euler angles)

임의 회전을 수행하려면 서로 다른 축을 중심으로 차례로 회전할 수 있음. 여기서 원칙적으로 임의의 시퀀스는 예를 들어, 처음에는 x축 주위, 다음에는 정적인 y축 주위, 마지막으로 정적인 z축 주위에서 가능하다. 참고로 OpenGL 코드의 순서는 오른쪽에서 행렬 곱셈 때문에 정확히 반대임.

  • 장점
    • x, y, z 세 개의 축을 기준으로 회전하기에 직관적이며 조작하기 쉬움
    • 180도가 넘는 회전도 표현할 수 있음
  • 한계
    • 오일러 각을 계산하는 데 드는 비용이 큼
    • 짐벌 락(Gimbal Lock)이 발생
The loss of one degree of freedom in a three-dimensional, three-gimbal mechanism that occurs when the axes of two of the three gimbals are driven into a parallel configuration, "locking" the system into rotation in a degenerate two-dimensional space.
3차원 3짐벌 기구에서 하나의 자유도 상실은 3개의 짐벌 중 두 축이 평행한 구성으로 구동될 때 발생하며 퇴화된 2차원 공간에서 시스템이 회전하도록 "잠금"함.

  • 세 개의 축을 동시에 생각하지 않고, 각 축을 독립적으로 판단하기에 어쩌다 겹쳐버리는 현상이 발생
  • 이렇게 축이 겹쳐 버리면 한 축에 대해서는 계산이 불가능해서 정확한 각도 계산이 불가능함
  • 2D 게임에서는 오일러 각으로 각도 계산이 가능하지만, 3D 게임에서는 오일러 각 만으로는 한계가 있음.
// This code example is created for educational purpose
// by Thorsten Thormaehlen (contact: www.thormae.de).
// It is distributed without any warranty.

#include <GL/freeglut.h> // we use glut here as window manager

#include <math.h>
#ifndef M_PI 
#define M_PI 3.1415926535897932385f 
#endif

#include <sstream>
#include <fstream>
#include <iostream>
#include <vector>
using namespace std;

class Renderer {
public:
    Renderer() : rot1(0.0), rot2(0.0), rot3(0.0) {}

public:
    void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();

        // view scene from the side
        glTranslatef(0.0f, 0.0f, -3.0f);
        glRotatef(-45.0f, 0.0f, 0.0f, 1.0f);
        glRotatef(-45.0f, 0.0f, 1.0f, 0.0f);
        glRotatef(135.0f, 1.0f, 0.0f, 0.0f);

        // rotate around z
        glRotatef(rot1, 0.0f, 0.0f, 1.0f);
        glColor3f(0.0f, 0.0f, 1.0f);
        drawCoordinateAxisZ();

        // rotate around local y
        glRotatef(rot2, 0.0f, 1.0f, 0.0f);
        glColor3f(0.0f, 1.0f, 0.0f);
        drawCoordinateAxisY();

        // rotate around local x
        glRotatef(rot3, 1.0f, 0.0f, 0.0f);
        glColor3f(1.0f, 0.0f, 0.0f);
        drawCoordinateAxisX();

        // draw the plane in the local coordinate system
        drawToyPlane();
    }
    void init() {

        // read vertex data from file
        std::ifstream input("ToyPlaneData.txt");
        if (!input) {
            cout << "Can not find vertex data file \"ToyPlaneData.txt\"" << endl;
        }
        else {
            int vertSize;
            double vertData;
            if (input >> vertSize) {
                if (vertSize > 0) {
                    toyPlaneData.resize(vertSize);
                    int i = 0;
                    while (input >> vertData && i < vertSize) {
                        // store it in the vector.
                        toyPlaneData[i] = vertData;
                        i++;
                    }
                    if (i != vertSize || vertSize % 3) toyPlaneData.resize(0);
                }
            }
            input.close();
        }


        // set opengl states
        glEnable(GL_DEPTH_TEST);
    }
    void resize(int w, int h) {
        // ignore this for now
        glViewport(0, 0, w, h);
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        gluPerspective(45.0, (float)w / (float)h, 0.1, 10.0);
    }
    void dispose() { }
public:
    float rot1;
    float rot2;
    float rot3;
    std::vector <double> toyPlaneData;

private:
    void drawCoordinateAxisZ() {
        glBegin(GL_LINE_LOOP); // circle in x-y plane
        for (int a = 0; a < 360; a += 10) {
            float angle = M_PI / 180.0f * a;
            glVertex3f(cos(angle), sin(angle), 0.0);
        }
        glEnd();

        glBegin(GL_LINES);
        glVertex3f(0.9f, 0.0f, 0.0f); // x-axis
        glVertex3f(1.0f, 0.0f, 0.0f);
        glVertex3f(0.0f, 0.9f, 0.0f); // y-axis
        glVertex3f(0.0f, 1.0f, 0.0f);
        glVertex3f(0.0f, 0.0f, -1.0f); // z-axis
        glVertex3f(0.0f, 0.0f, 1.0f);
        glEnd();

        glBegin(GL_TRIANGLES); // z-axis tip
        glVertex3f(0.0f, -0.1f, 0.9f);
        glVertex3f(0.0f, 0.0f, 1.0f);
        glVertex3f(0.0f, 0.1f, 0.9f);
        glEnd();
    }
    void drawCoordinateAxisX() {
        glPushMatrix();
        glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
        drawCoordinateAxisZ();
        glPopMatrix();
    }
    void drawCoordinateAxisY() {
        glPushMatrix();
        glRotatef(-90.0f, 1.0f, 0.0f, 0.0f);
        drawCoordinateAxisZ();
        glPopMatrix();
    }
    void drawToyPlane() {
        glColor3f(0.5f, 0.5f, 0.5f);
        glBegin(GL_TRIANGLES);
        for (unsigned i = 0; i < toyPlaneData.size(); i += 3) {
            glVertex3d(toyPlaneData[i], toyPlaneData[i + 1], toyPlaneData[i + 2]);
        }
        glEnd();
    }
};
//this is a static pointer to a Renderer used in the glut callback functions
static Renderer* renderer;

//glut static callbacks start
static void glutResize(int w, int h)
{
    renderer->resize(w, h);
}
static void glutDisplay()
{
    renderer->display();
    glutSwapBuffers();
}
static void glutKeyboard(unsigned char key, int x, int y) {
    bool redraw = false;
    float offset = 2.5f;

    if (glutGetModifiers() & GLUT_ACTIVE_ALT) {
        offset = -offset;
    }
    switch (key) {
    case '1':
        renderer->rot1 += offset;
        redraw = true;
        break;
    case '2':
        renderer->rot2 += offset;
        redraw = true;
        break;
    case '3':
        renderer->rot3 += offset;
        redraw = true;
        break;
    case '0':
        renderer->rot1 = 0.0f;
        renderer->rot2 = 0.0f;
        renderer->rot3 = 0.0f;
        redraw = true;
        break;
    }
    if (redraw) {
        std::stringstream ss;
        ss << "Yaw " << renderer->rot1 << ", Pitch " << renderer->rot2 << ", Roll " << renderer->rot3;
        glutSetWindowTitle(ss.str().c_str());
        glutDisplay();
    }
}
static void glutClose()
{
    renderer->dispose();
}
int main(int argc, char** argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(320, 320);

    glutCreateWindow("Use 1, 2, and 3 keys to rotate (ALT+key rotates in other direction)");

    glutDisplayFunc(glutDisplay);
    //glutIdleFunc(glutDisplay);
    glutReshapeFunc(glutResize);
    glutKeyboardFunc(glutKeyboard);
    glutCloseFunc(glutClose);

    renderer = new Renderer;
    renderer->init();

    glutMainLoop();
}

짐벌락 예제. Toy Plane Data txt는 필요하시면 보내드릴게용

3차원 회전 - 쿼터니언 (quaternion)

  • 쿼터니언 (Quaternion, 사원수)
    • 4개의 수 (x, y, z, w)로 이루어지며 각 성분은 축이나 각도를 의미하는 게 아니라, 하나의 벡터 (x, y, z)와 하나의 스칼라 (w, roll을 표현)를 의미함
    • 오일러 각이 회전순서에 기반하는 반면에 쿼터니언은 세 축을 동시에 회전시키기에 짐벌 락 현상이 발생하지 않음. (x, y, z 성분은 항상 동시에 변화)
    • 벡터가 원점과 특정 위치를 비교함으로써 방향을 측정하듯이, 쿼터니언은 회전의 원점과 특정 방향을 비교함으로써 회전을 측정함
    • Direction (두 점을 이용해서 나타낼 수 있는 방향, ~로 향하는 움직임)
    • Orientation (세 가지 오일러 각 또는 쿼터니언을 이용해서 나타낼 수 있는 방향, ~를 향하고 있는 상태)
  • 장점
    • 짐벌 락 문제가 없음
    • 계산하는데 드는 비용이 적음
  • 한계
    • 쿼터니언을 이용한 회전은 하나의 방향에서 다른 방향으로 측정되기에 180보다 큰 회전은 표현 안 됨
    • 직관적으로 이해하기가 힘듦

Spherical interpolation between the quaternions, q1 and q2. If simple linear interpolation was used then the interpolated point would be based on a line passing through the hypershere (the red colored chord). Using spherical interpolation, the predicted point is on the surface of the hypersphere

댓글