OpenGL 쉐이더 프로그래밍 - 뷰잉 변환

타입·2025년 9월 12일

컴퓨터 그래픽스

목록 보기
20/24

View Transform

OpenGL 초기 설정

카메라의 좌표계인 View Frame은 World Frame의 원점에 일치
-z 방향을 바라봄
z좌표의 -1.0 ~ 1.0 내의 물체 촬영

View Transform

View Transform: World Frame 전체를 View Frame으로 변환
카메라의 이동 가능

  • View Transform의 필요성
    카메라와 물체 사이의 거리나 각도를 조정이 필요한 경우
    수많은 물체를 움직이는 방법보단 카메라 하나만 옮기는 것이 훨씬 효율적임

Viewing API 용어 정리

  • p: VRP (View Reference Point)
    카메라의 위치
  • n: VPN (View Plane Normal)
    카메라의 방향, 카메라 뒤쪽으로 View Plane에 수직인 벡터
  • v: VUP (View Up Vector)
    카메라의 위쪽 방향, View Plane의 y방향 설정
  • u
    x축을 나타내는 벡터
    하지만 v벡터와 n벡터를 외적하여 u벡터를 구할 수 있음

Viewing Matrix Calculation

M_view: View Transform 행렬
q_view = M_view q_world
q_world = M_model q_model

M_model: Model Transform 행렬
즉, q_view = M_view M_model q_model
Model Frame의 좌표는 Model Transform과 View Transform을 거쳐 View Frame의 좌표로 변환됨

View Transform의 계산

좌표계 변환 - World Frame 관점

  • VRP
    p_world = (px, py, pz, 1)
    호모지니어스 좌표계에서 좌표의 w 값은 1
  • VPN
    n_world = (nx, ny, nz, 0)
    호모지니어스 좌표계에서서 벡터의 w 값은 0
    정규화된 유닛벡터라 가정
  • VUP
    v_world = (vx, vy, vz, 0)
    정규화된 유닛벡터라 가정

좌표계 변환 - View Frame 관점

  • VRP
    p_view = (0, 0, 0, 1)
  • VPN
    n_view = (0, 0, 1, 0)
    카메라의 방향이 -z 방향이므로, 그 반대인 +z 방향을 가짐
  • VUP
    v_view = (0, 1, 0, 0)
    y축 방향
  • u 벡터 계산
    u_view = v_view x n_view
    x축 방향

Viewing Matrix Calculation

  • 조건식 추출

    qview=Mviewqworldq_{view} = M_{view} \, q_{world}

    아래 네 행렬식을 만족하는 View Transform 행렬을 구해야 함

    pview=(0,0,0,1)T=Mviewpworlduview=(1,0,0,0)T=Mviewuworldvview=(0,1,0,0)T=Mviewvworldnview=(0,0,1,0)T=Mviewnworld\mathbf{p}_{view} = (0,0,0,1)^T = M_{view} \, \mathbf{p}_{world} \\ \mathbf{u}_{view} = (1,0,0,0)^T = M_{view} \, \mathbf{u}_{world} \\ \mathbf{v}_{view} = (0,1,0,0)^T = M_{view} \, \mathbf{v}_{world} \\ \mathbf{n}_{view} = (0,0,1,0)^T = M_{view} \, \mathbf{n}_{world}

  • Viewing Transform은 Affine Transformation의 일종
    카메라 세팅 시 Rotation과 Translation은 가능
    Sclaing은 불가능
    Affine Transformation의 특성 상 Rotation과 Translation은 별도로 계산 가능

  • View Transform은 3D Rotation의 역변환
    • basis vector를 회전 변환하는 행렬
      xyz의 basis vector를 uvn으로 변환
      R=[uxvxnxuyvynyuzvznz]R = \begin{bmatrix} u_x & v_x & n_x \\ u_y & v_y & n_y \\ u_z & v_z & n_z \end{bmatrix}
    • basis vector로 돌리는 역행렬
      World Frame 상 정의된 uvn 벡터를 View Frame으로 변환
      R1=[uxuyuzvxvyvznxnynz]R^{-1} = \begin{bmatrix} u_x & u_y & u_z \\ v_x & v_y & v_z \\ n_x & n_y & n_z \end{bmatrix}
      View Transform 행렬의 회전 행렬 부분을 구함
  • World Frame 상의 카메라의 위치를 View Frame 상 원점으로 이동 변환

    pview=(0,0,0,1)T=Mviewpworld\mathbf{p}_{view} = (0,0,0,1)^T = M_{view} \, \mathbf{p}_{world}
    • 미지수 3개를 구할 수 있음
      [uxuyuz?1vxvyvz?2nxnynz?30001][pxpypz1]=[up+?1vp+?2np+?31]=[0001]\begin{bmatrix} u_x & u_y & u_z & ?_1 \\ v_x & v_y & v_z & ?_2 \\ n_x & n_y & n_z & ?_3 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} p_x \\ p_y \\ p_z \\ 1 \end{bmatrix} = \begin{bmatrix} \mathbf{u} \cdot \mathbf{p} + ?_1 \\ \mathbf{v} \cdot \mathbf{p} + ?_2 \\ \mathbf{n} \cdot \mathbf{p} + ?_3 \\ 1 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \\ 0 \\ 1 \end{bmatrix}
    • 최종 결과
      Mview=[uxuyuzupvxvyvzvpnxnynznp0001]=[uxuyuz(uxpx+uypy+uzpz)vxvyvz(vxpx+vypy+vzpz)nxnynz(nxpx+nypy+nzpz)0001]M_{view} = \begin{bmatrix} u_x & u_y & u_z & -\mathbf{u} \cdot \mathbf{p} \\ v_x & v_y & v_z & -\mathbf{v} \cdot \mathbf{p} \\ n_x & n_y & n_z & -\mathbf{n} \cdot \mathbf{p} \\ 0 & 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} u_x & u_y & u_z & -(u_xp_x + u_yp_y + u_zp_z) \\ v_x & v_y & v_z & -(v_xp_x + v_yp_y + v_zp_z) \\ n_x & n_y & n_z & -(n_xp_x + n_yp_y + n_zp_z) \\ 0 & 0 & 0 & 1 \end{bmatrix}
      View Transform 행렬의 이동 행렬 부분을 구함
  • 행렬 분해
    View Transform 행렬은 회전/이동 행렬로 분해할 수 있음

    qview=Mviewqworld=RTqworld\mathbf{q}_{view} = M_{view} \, \mathbf{q}_{world} = \mathbf{R} \, \mathbf{T} \, \mathbf{q}_{world}
Mview=[uxuyuz0vxvyvz0nxnynz00001][100px010py001pz0001]M_{view} = \begin{bmatrix} u_x & u_y & u_z & 0 \\ v_x & v_y & v_z & 0 \\ n_x & n_y & n_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & -p_x \\ 0 & 1 & 0 & -p_y \\ 0 & 0 & 1 & -p_z \\ 0 & 0 & 0 & 1 \end{bmatrix}

OpenGL default setting

  • 카메라 위치

    pworld=(0,0,0)\mathbf{p}_{world} = (0,0,0)
  • View Plane Normal

    nworld=(0,0,1)\mathbf{n}_{world} = (0,0,1)
  • View Up Vector

    vworld=(0,1,0)\mathbf{v}_{world} = (0,1,0)
  • u = v x n

    uworld=(1,0,0)\mathbf{u}_{world} = (1,0,0)
  • 디폴트 세팅 상태의 카메라를 View Transform 변환
    View Transform 행렬은 아무런 변화가 없는 단위행렬로 나타남

    Mview=[uxuyuzupvxvyvzvpnxnynznp0001]=[1000010000100001]M_{view} = \begin{bmatrix} u_x & u_y & u_z & -\mathbf{u} \cdot \mathbf{p} \\ v_x & v_y & v_z & -\mathbf{v} \cdot \mathbf{p} \\ n_x & n_y & n_z & -\mathbf{n} \cdot \mathbf{p} \\ 0 & 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}

Rotating View 프로그램

View Panning 설정

zx평면 상에서 원점을 기준으로 카메라가 원궤도를 돌게 함
카메라의 y축 좌표는 고정

  • 세팅

    pworld=(rsinθ,0,rcosθ)nworld=(sinθ,0,cosθ)vworld=(0,1,0)uworld=(cosθ,0,sinθ)\mathbf{p}_{world} = (r \, sinθ, 0, r \, cosθ) \\ \mathbf{n}_{world} = (sinθ, 0, cosθ) \\ \mathbf{v}_{world} = (0, 1, 0) \\ \mathbf{u}_{world} = (cosθ, 0, -sinθ)
  • View Transform 행렬에 대입

    Mview=[uxuyuzupvxvyvzvpnxnynznp0001]=[cosθ0sinθ00100sinθ0cosθr0001]M_{view} = \begin{bmatrix} u_x & u_y & u_z & -\mathbf{u} \cdot \mathbf{p} \\ v_x & v_y & v_z & -\mathbf{v} \cdot \mathbf{p} \\ n_x & n_y & n_z & -\mathbf{n} \cdot \mathbf{p} \\ 0 & 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} \cos \theta & 0 & -\sin \theta & 0 \\ 0 & 1 & 0 & 0 \\ \sin \theta & 0 & \cos \theta & -r \\ 0 & 0 & 0 & 1 \end{bmatrix}

예제 프로그램

  • Vertex Shader 프로그램
    qview=MviewMmodelqmodel\mathbf{q}_{view} = \mathbf{M}_{view} \, \mathbf{M}_{model} \, \mathbf{q}_{model}
#version 330 core

in vec4 aPos; // vertex position: attribute
in vec4 aColor; // vertex color: attribute
out vec4 vColor; // varying color: varying
uniform mat4 uModel; // model matrix: uniform
uniform mat4 uView; // view matrix: uniform

void main(void) {
	gl_Position = uView * uModel * aPos; // transformation
	gl_Position.z *= -1.0F; // negation
	vColor = aColor;
}
  • Fragment Shader 프로그램
    그대로 사용
#version 330 core

in vec4 vColor; // varying color: varing
out vec4 FragColor; // fragment color: framebuffer

void main(void) {
	FragColor = vColor;
}
  • updateFunc()
    View Transform 행렬 초기화
    (column-major 주의)
glm::mat4 matView = glm::mat4(1.0F);

void updateFunc(void) {
	...
    
	// viewing transform
	GLfloat radius = 0.1F;
	matView[0][0] = cosf(theta); matView[0][2] = sinf(theta);
	matView[2][0] = -sinf(theta); matView[2][2] = cosf(theta);
	matView[3][2] = -radius;
}
  • drawFunc()
    레지스터에 View Transform 행렬 세팅
void drawFunc(void) {
	...
	// matrix settings
	GLuint locView = glGetUniformLocation(prog, "uView");
	glUniformMatrix4fv(locView, 1, GL_FALSE, glm::value_ptr(matView));
    // 이후 피라미드, 큐브 draw
    ...
}

y축을 기준으로 카메라가 반시계방향으로 회전하는 것을 확인

Look-At 방법

View Transform 설정의 문제점

  • 설정에 필요한 정보
    • 카메라 위치: 점 p_world
    • View Plane Normal: n_world 벡터
    • View Up Vector: v_world 벡터

어떻게 n, v 벡터가 단위 벡터여야 하며 서로 직교하게 만들어야 함
3차원 공간 상에서 어떻게 설정하고 계산해야할 지 문제
서로 직교하는 x,y,z basis vector를 변환 후 u,v,n basis vector의 직교 좌표계로 만들어야 함
u, v, n을 직접 계산하지 않고 직관적인 방법이 필요

Look-At Approach

카메라의위치:eyeworld=(eyex,eyey,eyez)바라보는지점:atworld=(atx,aty,atz)카메라의윗방향:upworld=(upx,upy,upz)카메라의\, 위치: \mathbf{eye}_{world} = (eye_x,eye_y,eye_z) \\ 바라보는\, 지점: \mathbf{at}_{world} = (at_x,at_y,at_z) \\ 카메라의\, 윗방향: \mathbf{up}_{world} = (up_x,up_y,up_z)

이 때 카메라의 윗방향은 다시 계산할거라 대강 결정해도 괜찮음

  • VRP (View Reference Point)
    p = eye
  • VPN (View Plane Normal)
    n = -normalize(at - eye)
  • u 벡터
    u = normalize(up x n)
  • View Up Vector
    v = normalize(n x u)

이렇게 u,v,n 벡터가 직교 좌표계를 이룸

Look-At 프로그램

View Panning 설정

아까의 카메라가 원궤도를 돌게 하는 프로그램
단 u,v,n 벡터를 직접 계산하지 않음

  • 세팅
    eye=(rsinθ,0,rcosθ)at=(0,0,0)up=(0,1,0)eye = (r \, sinθ, 0, r \, cosθ) \\ at = (0, 0, 0) \\ up = (0, 1, 0)

예제 프로그램

  • updateFunc()
    View Transform 행렬을 세팅하는 부분을 glm의 lookAt 함수로 대체
    (오른손 좌표계의 함수 사용)
// eye, at, up을 순서대로 전달
void updateFunc(void) {
	...
	// viewing transform
	GLfloat radius = 0.1F;
	matView = glm::lookAtRH(
	              glm::vec3( radius * sinf(theta), 0.0F, radius * cosf(theta) ),
	              glm::vec3( 0.0F, 0.0F, 0.0F ),
	              glm::vec3( 0.0F, 1.0F, 0.0F )
	          );
}
  • 디버깅
    View Transform 행렬의 값이 잘 나오는 것을 확인
// 코드
std::cout << glm::to_string( matView ) << std::endl;

// 출력 결과 (column-major)
mat4x4(
	(0.964142, 0.000000, 0.265388, 0.000000),
	(0.000000, 1.000000, -0.000000, 0.000000),
    (-0.265388, 0.000000, 0.964142, 0.000000),
    (-0.000000, -0.000000, -0.100000, 1.000000)
)

LowLevel 함수

void updateFunc(void) {
	...
	const GLfloat radius = 0.1F;
	glm::vec3 eye( radius * sinf(theta), 0.05F, radius * cosf(theta) );
	glm::vec3 at( 0.02F, 0.0F, 0.0F );
	glm::vec3 up( 0.0F, 1.0F, 0.0F );
    
	glm::vec3 p = eye;
	glm::vec3 n = -glm::normalize(at - eye);
	glm::vec3 u = glm::normalize( glm::cross(up, n) );
	glm::vec3 v = glm::cross( n, u );
    
	matView[0][0] = u.x; matView[1][0] = u.y; matView[2][0] = u.z; matView[3][0] = -glm::dot(u, p);
	matView[0][1] = v.x; matView[1][1] = v.y; matView[2][1] = v.z; matView[3][1] = -glm::dot(v, p);
	matView[0][2] = n.x; matView[1][2] = n.y; matView[2][2] = n.z; matView[3][2] = -glm::dot(n, p);
	matView[0][3] = 0.0F; matView[1][3] = 0.0F; matView[2][3] = 0.0F; matView[3][3] = 1.0F;
}

살짝 비스듬하게 내려다보며 카메라가 회전하는 것을 확인

Other Viewing Methods

Yaw-Pitch-Roll 방식

  • Yaw (z축)
    수직 방향 회전
  • Pitch (y축)
    옆 방향 회전
  • Roll (x축)
    전진 방향 회전

  • Euler Angle과의 관계
    Euler Angle 중에서 특별히 z-y-x축 순서로 회전하는 경우 정확히 일치함

Polar Coordinate (극좌표) 방식

  • Azimuth (방위각)
    xy평면 상의 각도
  • Elevation (고각)
    높이에 대한 각도

2개의 회전 행렬로 구현 가능

왼손 좌표계의 고려

  • 장점
    VPN 벡터가 Look-At과 유사
    DirectX에서 사용
  • 단점
    오른손 좌표계와 z축이 반대로 계산
profile
주니어 언리얼 프로그래머

0개의 댓글