OpenGL 쉐이더 프로그래밍 - 쿼터니언 기초

타입·2025년 8월 12일

컴퓨터 그래픽스

목록 보기
17/24

외적 (Cross Product)

  • 내적 (Dot Product)
    row 벡터 * column 벡터
  • 외적 (Cross Product)
    일종의 determinant 계산 (i,j,k는 basis 벡터)
    a x b = (ay*bz - az*by) i + (az*bx - ax*bz) j + (ax*by - ay*bx) k

a x b: 2개의 3D 벡터에 대한 외적
a, b 모두에 수직인 새로운 3D 벡터를 구함

a x b의 방향은 오른속 법칙으로 결정
b x a = -a x b

|a x b| = |a||b|sinθ
a x a = 0 (θ가 0이므로)

법선 벡터 (Surface Normal Vector)

3차원 물체의 면에 대해 수직인 벡터

삼각형을 이루는 세 점을 p0, p1, p2라고 했을 때
n = (p1 - p0) x (p2 - p0) = (a,b,c)
평면의 방정식: ax + by + cz + d = 0

정규화 (Normalize)

벡터의 길이가 1인 단위 벡터로 만드는 방법

  • 벡터의 길이
    v: (vx,vy,vz)라고 하면
    |v| = √(vx^2 + vy^2 + vz^2)
    |v|^2 = vx^2 + vy^2 + vz^2 = v∘v

  • 정규화
    주어진 벡터와 방향이 일치하는 단위 벡터를 구함
    u = v/|v|

사원수 (Quaternion)

복소수 (Complex Number)

허수 i = √-1, i^2 = -1

복소수 = 실수부 + 허수부
z = x + i y = r cosΦ + i r sinΦ = r e^(i * Φ)
(길이 r인 벡터를 Φ만큼 회전한 2차원 좌표)

이런 식으로 복소수를 2차원 좌표에 대응시킴
복소수 끼리의 곱으로 2차원 회전을 구현 가능

2차원 회전

단위 벡터 z' = e^(i * θ)
z ∘ z' = z ∘ e^(i * θ) = (x + i y)(cosθ + i sinθ)
= (x cosθ - y sinθ) + i(x sinθ + y cosθ)

쿼터니언

복소수를 확장
쿼터니언은 회전축이 되는 3D 벡터와 회전각도가 되는 스칼라 값으로 이루어져 있음

q = qx i + qy j + qz k + qw = (qx,qy,qz,qw) = (v, qw)
v = qx i + qy j + qz k = (qx,qy,qz)
v는 허수부, qw가 실수부

  • i,j,k는 서로 직교하는 허수 단위 벡터 (허수 개념을 가짐)
    i^2 = j^2 = k^2 = ijk = -1

3차원 회전

  • 3차원 좌표를 쿼터니언으로 표현
    실수 부분을 0으로 표현
    p = (p,0) = px i + py j + pz k + 0

  • 3차원 회전을 쿼터니언으로 표현
    회전축 v, 회전각도 θ

  • 3차원 좌표를 회전하는 식
    점 p에 회전식 r과 r 인버스를 앞뒤에 곱해주면 점 p를 θ만큼 회전시킨 결과가 나옴

Virtual Trackball 프로그램

윈도우 앞에 가상의 구가 있다고 가정
윈도우에서 마우스를 클릭했을 때 구 상의 좌표로 매핑
2차원에서의 마우스 트래킹을 3차원의 회전으로 변환

  • matrix의 분리
    한 번에 회전시키는 것이 아닌 축을 기준으로 여러번 반복해서 조금씩 회전

    • matUpdated: 물체가 이미 회전된 상태
    • matDrag: 현재 적용되는 dragging matrix
  • p' = matDrag * matUpdated * p
    dragging 도중에는 matDrag만 수정
    dragging이 끝나면 matUpdated에 모두 적용

  • 단위 벡터 계산 함수
    윈도우 상의 (x,y) 좌표를 구에 대응하는 3차원 좌표로 반환

glm::vec3 calcUnitVec(const glm::vec2& raw) {
	glm::vec2 scr;
	// 윈도우 밖을 벗어나지 않도록 예외처리
	scr.x = std::clamp(raw.x, 0.0F, (float)WIN_W);
	scr.y = std::clamp(raw.y, 0.0F, (float)WIN_H);
	// normal processing
	const GLfloat radius = sqrtf(WIN_W * WIN_W + WIN_H * WIN_H) / 2.0F;
	glm::vec3 v;
	v.x = (scr.x - WIN_W / 2.0F) / radius;
	v.y = (WIN_H / 2.0F - scr.y) / radius;
	v.z = sqrtf(1.0F - v.x * v.x - v.y * v.y);
	return v;
}
  • 마우스 회전 계산
glm::mat4 calcTrackball(const glm::vec2& start, const glm::vec2& cur) {
	glm::vec3 org = calcUnitVec(start);
	glm::vec3 dst = calcUnitVec(cur);
	glm::quat q = glm::rotation(org, dst); // 회전 쿼터니언 반환
	glm::mat4 m = glm::toMat4(q); // 회전 행렬로 변환
	return m;
}

void cursorPosFunc(GLFWwindow* win, double xscr, double yscr) {
	if (mousePressed == GL_TRUE) {
		glm::vec2 dragCur = glm::vec2((GLfloat)xscr, (GLfloat)yscr);
		matDrag = calcTrackball(dragStart, dragCur);
		mat = matDrag * matUpdated; // vertex shader에 전달할 회전 행렬
	}
}

// mat 배열을 uniform 레지스터에 등록
void drawFunc(void) {
	...
	GLuint locMat = glGetUniformLocation(prog, "uMat");
	glUniformMatrix4fv(locMat, 1, GL_FALSE, glm::value_ptr(mat));
    ...
}
  • 마우스 입력 콜백 함수
void mouseButtonFunc(GLFWwindow* win, int button, int action, int mods) {
	GLdouble x, y;
	switch (action) {
	case GLFW_PRESS:
		mousePressed = GL_TRUE;
		glfwGetCursorPos(win, &x, &y);
		dragStart = glm::vec2((GLfloat)x, (GLfloat)y); // 시작점 설정
		break;
	case GLFW_RELEASE:
		mousePressed = GL_FALSE;
		glfwGetCursorPos(win, &x, &y);
		glm::vec2 dragCur = glm::vec2((GLfloat)x, (GLfloat)y);
		matDrag = calcTrackball(dragStart, dragCur);
		mat = matDrag * matUpdated;
		matDrag = glm::mat4(1.0F); // 단위 행렬로 초기화 (나중을 위해)
		matUpdated = mat; // update to the object matrix
		break;
	}
	fflush(stdout);
}
  • vertex shader 프로그램
    mat 배열을 받아 vertex를 회전하여 위치 갱신
    (2차원 회전 게시글의 orbit 프로그램과 동일한 코드)
#version 330 core

in vec4 aPos; // vertex position: attribute
in vec4 aColor; // vertex color: attribute
out vec4 vColor; // varying color: varying
uniform mat4 uMat; // matrix: uniform

void main(void) {
	gl_Position = uMat * aPos; // transformation
	gl_Position.z *= -1.0F; // negation
	vColor = aColor;
}

마우스로 드래그하여 피라미드를 회전시키는 것을 확인

키프레임 애니메이션

keyframes: 애니메이터가 설정하는 중요한 장면
in-betweens: 컴퓨터가 자동 생성하는 중간 장면

  • Lerp (Linear Interpolation)
    선형 보간
    주로 2D 애니메이션에서 사용
    • 오일러 각에 대해 회전 시 문제 발생
      (0°,90°,0°) -> (90°,45°,90°) 회전
      중간 프레임에서 (45°,67.5°,45°)가 되며 yz 평면을 벗어나버림

Slerp (Spherical Linear Interpolation)

3차원 회전의 보간 방법
쿼터니언을 활용한 회전 보간

slerp(q0, q1; t) = q0 (q0^(-1) * q1)^t = q1 (q1^(-1) * q0)^(1-t)
= (q1 * q0^(-1))^t q0

(시간 t는 0.0~1.0)

profile
언리얼 프로그래머

0개의 댓글