/ PROGRAMMING, GRAPHICS, GUI, OPENGL

Viewing in OpenGL

(COSE436 인터렉티브시각화 과제 수행 중 알게 된 내용 정리)

LookAt 및 View Matrix

과제 소스코드에서 제공되는 헤더 파일에 구현된 LookAt 함수를 사용하여 카메라의 위치를 지정할 수 있는데, LookAt 함수가 리턴하는 매트릭스를 어떻게 적용해야 하는지 잘 몰랐다.

LookAt 함수가 반환하는 매트릭스는 View Matrix에 해당하며, 이 매트릭스를 vertex에 곱하면 카메라의 시점 조정이 반영된다. 여기서는 View Matrix를 쉐이더 프로그램에 넘겨주어, 쉐이더가 해당 매트릭스 곱을 수행하도록 하였다.

View Matrix 이외에도 Model Matrix, Projection Matrix가 있는데 Model Matrix와 View Matrix를 곱한 매트릭스 MV를 Model-view Matrix라고 부르기도 하며 세 매트릭스를 모두 곱한 매트릭스를 MVP라고 하기도 한다. 이때 곱하는 순서가 중요한데, Model-view Matrix를 먼저 곱하고, 그 다음 Projection Matrix를 곱해야 한다. 카메라 위치 및 모델들의 위치 등이 정의된 이후에 최종적으로 Projection을 수행하는 것이기 때문이다.

Projection Matrix는 뒷부분에서 Projection을 다룰 때 같이 살펴볼 것이다. 그리고 여기서는 Model Matrix는 등장하지 않는다. 오브젝트에 대한 변형(Transformation)을 수행하지 않고 우선 View Matrix를 통해 시점만 변경할 것이기 때문이다.

inline
mat4 LookAt( const vec4& eye, const vec4& at, const vec4& up )
{
    vec4 n = normalize(eye - at);
    vec4 u = vec4(normalize(cross(up,n)), 0.0);
    vec4 v = vec4(normalize(cross(n,u)), 0.0);
    vec4 t = vec4(0.0, 0.0, 0.0, 1.0);
    mat4 c = mat4(u, v, n, t);
    return c * Translate( -eye );
}

제공된 LookAt함수의 구현은 위와 같다. 여기서 eye는 카메라의 위치를 의미한다. at은 카메라가 바라보는 지점을 의미한다. up은 카메라의 위쪽 방향을 의미한다. 이 세 매개변수를 통해 카메라의 시점을 조정할 수 있다. 3차원 좌표 공간에서 4차원 벡터를 사용하는 이유는 좌표계가 homogeneous system이기 때문이다. 벡터의 마지막 요소인 w는 좌표가 점인지 벡터인지 구분하는 데 사용되며, 0이면 벡터, 1이면 점을 의미한다.

주의할 점은 여기서 반환되는 매트릭스는 row major order라는 것이다. 우리가 잘 알듯이, C, C++의 multidimensional array는 row major order로 저장된다. 즉, 2차원 배열 arrarr[i][j]arr[i * n + j]와 같이 저장된다. 그러나 OpenGL은 column major order로 매트릭스를 저장한다. 즉, 2차원 배열 arrarr[i][j]arr[j * n + i]와 같이 저장된다. 따라서, OpenGL 어플리케이션을 C++로 구현하고 있다면, LookAt 함수가 반환하는 매트릭스(View Matrix)를 OpenGL에 넘겨주기 위해서는 transpose를 수행해야 한다.

디스플레이 콜백에서 다음 함수를 호출해서 주전자를 그렸다고 하자.

glutWireTeapot(0.5);

이때 argument는 주전자의 크기에 해당한다. 그리고 기본적으로 주전자의 위치는 원점에 해당한다.

이 주전자를 바라보는 카메라의 시점을 나타내는 View Matrix를 얻어내려면 다음과 같이 LookAt 함수를 호출한다.

viewMatrix = LookAt(vec4(1, 2, 5, 1), vec4(0.0, 0.0, 0.0, 1), vec4(0.0, 1.0, 0.0, 0.0));

위 코드는 카메라가 (1,2,5) 위치에서 (0,0,0) 위치(주전자의 위치)를 바라보며, (0,1,0) 방향을 ‘위쪽(up)’으로 간주하겠다는 뜻이다. 하지만 이것만으로는 시점이 완전하게 정의되지 않으며, Projection Matrix를 통해 시점을 완전히 정의할 수 있다.

Projection

3차원 공간을 2차원 화면에 표현한다는 것은 즉 Projection을 수행한다는 의미이다. 이때 Projection은 두 가지 방식으로 수행할 수 있는데, Orthographic Projection과 Perspective Projection이다. Orthographic Projection은 원근감이 없는 투영을 의미하며, Perspective Projection은 원근감이 있는 투영을 의미한다.

각자 정의되는 parameter들이 다르다. Orthographic Projection은 left, right, bottom, top, near, far를 parameter로 받는다. Perspective Projection은 fovy, aspect, near, far를 parameter로 받는다. 여기서 fovy는 y축을 기준으로 한 field of view angle이며, aspect는 width/height를 의미한다.

near과 far은 각각 near plane과 far plane의 상대 좌표를 의미한다. 참고로 OpenGL의 기본 projection 설정은 Ortho(-1,1,-1,1,-1,1)이다. 즉, near plane은 z=-1, far plane은 z=1에 해당한다. 이때 near plane과 far plane 사이의 공간이 viewing volume이다. 이 viewing volume을 projection surface에 투영하는 것이 projection이다.

OrthoPerspective 함수를 호출하여 전달한 argument로 정의되는 투영과 대응되는, Projection Matrix를 생성할 수 있다. 각 함수의 구현은 다음과 같다.

inline
mat4 Ortho( const GLfloat left, const GLfloat right,
	    const GLfloat bottom, const GLfloat top,
	    const GLfloat zNear, const GLfloat zFar )
{
    mat4 c;
    c[0][0] = 2.0/(right - left);
    c[1][1] = 2.0/(top - bottom);
    c[2][2] = 2.0/(zNear - zFar);
    c[3][3] = 1.0;
    c[0][3] = -(right + left)/(right - left);
    c[1][3] = -(top + bottom)/(top - bottom);
    c[2][3] = -(zFar + zNear)/(zFar - zNear);
    return c;
}

inline
mat4 Perspective( const GLfloat fovy, const GLfloat aspect,
		  const GLfloat zNear, const GLfloat zFar)
{
    GLfloat top   = tan(fovy*DegreesToRadians/2) * zNear;
    GLfloat right = top * aspect;

    mat4 c;
    c[0][0] = zNear/right;
    c[1][1] = zNear/top;
    c[2][2] = -(zFar + zNear)/(zFar - zNear);
    c[2][3] = -2.0*zFar*zNear/(zFar - zNear);
    c[3][2] = -1.0;
    c[3][3] = 0.0;
    return c;
}

이때도 마찬가지로 반환되는 matrix는 row major order이니 주의하자.

View Matrix와 Projection Matrix 반영

이제 카메라의 위치 및 방향과 Projection 기준이 정의되었으니 vertex에 매트릭스 곱을 하면 된다. 만약 쉐이더 프로그램을 사용 중이라면 쉐이더 프로그램에 View Matrix와 Projection Matrix를 각각 넘겨줘야 한다. 쉐이더 프로그램을 참조하는 변수가 p라고 한다면, 다음과 같이 매트릭스들을 넘겨줄 수 있다.

	glUniformMatrix4fv(glGetUniformLocation(p, "Proj"), 1, GL_TRUE, projMatrix);
	glUniformMatrix4fv(glGetUniformLocation(p, "Mv"), 1, GL_TRUE, viewMatrix); // C++ multidimensional array is stored in row major order
	//If transpose is GL_TRUE, each matrix is assumed to be supplied in row major order.

glUniform API 문서를 참조하면, 3번째 변수는 매트릭스의 transpose 여부인데, 우리가 얻어낸 매트릭스는 row major order이므로 GL_TRUE로 설정해야 한다.

쉐이더 프로그램 작성

#version 140
#extension GL_ARB_compatibility: enable

out vec4 color;

uniform mat4 Mv;  // Model-view matrix
uniform mat4 Proj; // Projection matrix

void main() 
{
    // Combine model-view and projection matrices
    gl_Position =  Proj * Mv * gl_Vertex; // vertices -> Mv -> Proj
    color = gl_Color;
}

위 코드에서 p가 참조하는 쉐이더 프로그램의 예시(vshader.vert)는 위와 같다. gl_Position은 vertex shader의 output으로, transformation이 반영된 vertex의 위치를 나타낸다. gl_Vertex는 vertex shader의 input으로, vertex의 위치를 나타낸다. gl_Color는 vertex shader의 input으로, vertex의 색상을 나타낸다. 주목할 부분은 Proj * Mv * gl_Vertex 이다. 곱셈 순서에 주의하자.