투영 변환
지난 시간까지는 모델에 기하변환을 가하여 모델변환을 시키고, 전역 좌표계와 모델 좌표계에 대해서 공부했으며 카메라 좌표를 변환시켜서 시점을 변환시켰다. 이제는 카메라로 세계를 촬영했을 때, 촬영되지 않는 부분과 촬영되어지는 부분에 대해서 학습할 것이다. 카메라를 가까이서 촬영하여 보이지 않을 수도 있고 특정 오브젝트가 촬영되어지는 부분에서 벗어나서 촬영되지 않을 수 있다. 따라서, 촬영되지 않는 부분들을 삭제해주어야 하는데, 이 것이 투영변환 과정에 포함된다.
이 투영 변환은 크게 정사투영(Orthographics Projection), 원근투영(Perspective Projection)에 따라 달라진다.
정사투영(Othographics Projection)
정사투영은 쉽게말해서 원근감을 고려하지 않고 직선상으로 투영되는 물체를 촬영하는 것이다.
위의 그림을 보면 카메라가 맨 왼쪽에 있다고 가정하고 가시부피를 향해 정사투영을 하고 있다. 이 때, 특징은 투상선이 평행하다는 것이다. 그리고 3차원 상의 가시부피가 실제로 우리가 보는 윈도우 평면인 뷰 윈도우에 투영되고 있으며, 전방절단면과 후방절단면이 존재한다. 간단하게 해석하자면, 원래는 크디큰 세계가 존재하지만, 우리가 촬영하고자 하는 제한적인 영역이며 그 영역은 가시부피이다. 이 가시부피는 뷰 윈도우의 비율에 따라 너비(Width), 높이(Height)가 결정되며, 전방절단면(near)과 후방절단면(far)에 따라 결정된다. 즉, 아무리 세계는 넓더라도 정해져있는 너비와 높이에 따라서만 촬영할 것이며 그 밖의 영역에 있는 물체는 대상에서 제외된다. 그리고 전방 절단면과 후방절단면 사이의 영역에 있지 않은(너무 가까이 있거나 너무 멀리있는)물체 또한 대상에서 제외된다. 이렇게 가시 부피 내부에 있는 제한된 영역의 세계만 촬영하게 된다. 그리고 정사투영은 투상선이 평행하기 때문에( = 카메라 렌즈와의 거리를 고려하지 않기 때문에) 크기가 같은 두 오브젝트가 3차원 상으로 멀리있던 가까이에 있던 투영되는 크기는 동일하게된다. 이렇게 원근감이 고려되지 않은 투영이 가능하게된다.
만약 3차원 상의 좌표를 갖고 있는 네개의 Vertex가 있다고 가정하겠다. 현재 점들은 x,y,z축의 값들을 모두 가지고 있다. 여기서 x,y는 뷰 윈도우 상에 그려질 2차원상의 좌표라고 생각하고 z는 카메라 렌즈와의 거리라고 생각하자. z축이 클 수록 카메라에는 더 멀리 있는 것이다. 따라서, 원근법이 고려되어 2차원 뷰 윈도우에 투영된다면
이렇게 찌그러질 것이다. 하지만,
이를 정사투영하게 되면 이 상태 그대로, z값만 0으로 바꿔버리거나 아예 고려를 안해버리면 된다. 그러면 점이 이 상태 그대로 찍히게 될 것이다.
glOrtho(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top,
GLdouble near, GLdouble far);
원근투영
원근투영은 정사변환에서 원근법을 고려한 것으로, 가까이 있는 물체는 크게하고 멀리 있는 물체는 작게 처리하는 것이다.
정사투영과는 다르게 카메라 렌즈로부터 시작되는 투상선이 평행하지 않기 때문에 FOV라고 하는 상하좌우 각도가 존재하게되고, 전방 절단면과 후방 절단면의 크기가 달라지게된다. 뷰 윈도우(투상면)과 전방절단면, 후방절단면의 크기는 모두 다르지만 비율은 모두 동일하다. 크기가 다르다는 의미는 즉, 멀리있으면 멀리 있을 수록 더 넓은 세계를 촬영하겠다라는 의미이다. 아무리 같은 크기의 꽃병이라도 더 넓은 세계에서 꽃병을 본다면 작아보일 것이고, 좁은 세계에서의 꽃병은 크게보일 것이다. 산 위에서 넓은 시야로 사람을 보면 개미처럼 보이는 것과 똑같다.
그런데 문제는 가시부피가 사각뿔 형태로 바뀌게 되면서 정사투영과는 다른 파이프라인 처리가 요구되고, 가시 부피 밖의 물체를 절단함에 있어서 더 복잡하고 많은 양의 연산이 발생하게 된다. 따라서 정사투영과 동일한 파이프라인 구조, 훨씬 단순한 연산을 위해서 정육면체로 정규화 시키는 변환 과정이 내부적으로 존재한다. 즉, 뷰 윈도우(뷰 포트)상의 크기로 변환시켜주는 것이다.
자, 이제 정사투영에서 봤던 네개의 Vertex를 다시보면 원근투영은 카메라 렌즈와의 거리(z축이라고 가정)를 고려하기 때문에 이를 실제로 2D평면(뷰 윈도우)에 투영하게 되면 찌그러지는 모습을 볼 수 있다. 여기서 특징이 드러나는데, 아무리 물체가 카메라와의 거리차이가 있더라도 결국에는 2D평면에 투영하는 것이기 때문에 z축이 모두 같아져야 한다. 따라서, 각 Vertex의 3차원 좌표를 z축의 값으로 나누어주어야 한다.(z가 0이 아니라는 가정하에). 이렇게 간단해 보이지만 사실은 훨씬 복잡한 수학으로 이루어져 있는데, 이는 색상과 빛 재질 등의 여러 요소들이 포함되어 있기 때문이다. 이에 대해선 나중에 알아보자.
void glFrustum(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top,
GLdouble near, GLdouble far);
void glPerspective(GLdouble FOV, GLdouble aspect, GLdouble near, GLdouble far);
: aspect == 폭/높이 뷰포트의 비율과 동일해야함
: FOV개념이 추가된 함수며 더 많이 쓰이고 High Level에서도 FOV를 많이 씀
: 내부적으로는 glFrustum을 이용하고 있음
OpenGL 실습 : 투영변환
#include <OpenGL/OpenGL.h>
#include <GLUT/GLUT.h>
#include <math.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;
GLfloat camPos_x = 0.5, camPos_y = 0.5, camPos_z = 6; //위치
GLfloat camAt_x = 0, camAt_y = 0, camAt_z = 0; //바라보는 곳
GLfloat camUp_x = 0, camUp_y = 1, camUp_z = 0; //방향
void MyReshape(int width, int height){
glViewport(0, 0, width, height);
//GLfloat f_w = (GLfloat)width / (GLfloat)_WINDOW_WIDTH;
//glMatrixMode(GL_PROJECTION);
glMatrixMode(GL_PROJECTION);//투영변환 모드 초기화
glLoadIdentity();
//glOrtho(-1.0*f_w, 1.0*f_w, -1.0*f_h, 1.0*f_h, -2, 2);
GLfloat ratio = (float)width/height; //비율 값
gluPerspective(40, ratio, 0.1, 10); //원근 투영
}
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();
gluLookAt(camPos_x, camPos_y, camPos_z, camAt_x, camAt_y, camAt_z, camUp_x,camUp_y, camUp_z);
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();
}
void CameraAnimationTimer(int value){
GLfloat Theta = 0.01;
GLfloat tmp_cam_x = camPos_x;
//모델의 y축 기준으로 카메라 회전시키기 회전공식 참고
camPos_x = camPos_x * cos(Theta) + camPos_z * sin(Theta);
camPos_z = tmp_cam_x * -sin(Theta) + camPos_z * cos(Theta);
glutTimerFunc(20, CameraAnimationTimer, 2);
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);
glutTimerFunc(20, CameraAnimationTimer, 2);
glutMainLoop();
}
뷰포트 변환
투상변환이 끝나면 이제 뷰 윈도우의 2차원 공간 상의 위치에 실질적으로 그림을 그려주어야 한다. 즉, 사용자의 장치 좌표계에 출력시켜주어야 한다. 이 때, 출력할 윈도우(window)와 윈도우 내에서의 출력 공간(viewport)가 필요하다. 윈도우 크기와 뷰포트 크기가 동일할 때도 있지만, 언제나 그렇지만은 않다.
따라서, 이와 같이 출력 공간(viewport)를 설정하여 실질적으로 출력되는 공간을 설정해줄 수 있다.
void glViewport(GLint left, GLint bottom, GLsizei width, GLsizei height);
OpenGL 실습코드 : 뷰포트
void MyReshape(int width, int height){
GLfloat left = 250, bottom = 300;
glViewport(left, bottom, width-left, height-bottom); //윈도우 사이즈에 맞게 뷰포트 지정
glMatrixMode(GL_PROJECTION);//투영변환 모드 초기화
glLoadIdentity();
GLfloat ratio = (float)(width-left)/(height-bottom); //비율 값 + //윈도우 사이즈에 맞게 뷰포트 지정
gluPerspective(40, ratio, 0.1, 10); //원근 투영
}
위 예제 코드대로 하니까 안돼서, 일단 display 콜백함수에서 glViewport호출해서 결과물을 내보았다.. reshape콜백함수에서 돌렸을 때 왜 안되는지에 대해서 해결해보아야 될 것 같다 ㅠ.. 참고로 window size width, height는 각각 500으로 수정하였다.
'Computer Graphics' 카테고리의 다른 글
Chapter11. 래스터 변환 (0) | 2021.06.07 |
---|---|
Chapter10. 가시성 판단 (0) | 2021.06.06 |
Chapter08. 시점변환(gluLookat) (0) | 2021.06.05 |
Chapter07. GL-Viewing : 행렬&좌표계&모델 변환 (0) | 2021.06.01 |
Chapter06. 디스플레이 리스트 (0) | 2021.05.31 |