상세 컨텐츠

본문 제목

HumanGL: 카메라 제어 - (5)

Computer Graphics/HumanGL

by Banjosh 2024. 10. 14. 17:24

본문

이번에는 Human Model을 다양한 각도에서 보고 판단할 수 있도록 카메라 제어 기능을 추가했다. 마우스랑 키보드를 사용해서 모델을 자유롭게 움직이면서 볼 수 있게 만들 었고, OpenGL 강좌에서 배운 내용을 바탕으로 아래 기능들을 추가했다.

  1. 마우스 우클릭을 누르고 있는 동안 카메라 제어 모드로 전환
  2. 카메라 제어 모드에서 마우스 움직임에 따라 카메라 회전
  3. W, S, A, D, Q, E 키를 눌러서 카메라를 3축 방향으로 이동

Camera 클래스 설계

이 기능들을 위해 기존의 context 클래스에서 카메라 관련 부분을 분리해서 Camera라는 클래스를 새로 만들었다. 이 클래스는 카메라의 위치와 회전 상태를 관리하면서 움직임이나 회전을 처리하는 데 필요한 함수들을 포함하고 있다.

#ifndef CAMERA_H
# define CAMERA_H

# include "humanGL.h"

class Camera {
public:
	Camera() = default;
	void move(bool isPressW, bool isPressS, bool isPressD,
			  bool isPressA, bool isPressE, bool isPressQ);
	void rotate(glmath::vec2& pos);
	void saveCurrentPos(float x, float y);
	glmath::mat4 getViewMatrix();

private:
	float m_cameraPitch { 0.0f };
	float m_cameraYaw { 0.0f };
	glmath::vec2 m_prevMousePos { glmath::vec2(0.0f)};
	glmath::vec3 m_cameraPos {0.0f, 0.0f, 20.0f};
	glmath::vec3 m_cameraFront {0.0f, 0.0f, -1.0f};
	glmath::vec3 m_cameraUp {0.0f, 1.0f, 0.0f};
	const float m_cameraSpeed { 0.04f };
	const float m_cameraRotSpeed { 0.2f };
};

#endif

 

 

Camera 클래스 함수들

1. saveCurrentPos() 함수

void Camera::saveCurrentPos(float x, float y) {
	m_prevMousePos = glmath::vec2((float)x, (float)y);
}

 

이 함수는 현재 마우스 위치를 저장하는 역할을 한다. Context::mouseButton() 함수에서 마우스 우클릭이 감지되면 호출되고, 마우스의 좌표가 m_prevMousePos에 저장된다.

void Context::mouseButton(int button, int action, double x, double y) {
	if (button == GLFW_MOUSE_BUTTON_RIGHT)
	{
		if (action == GLFW_PRESS)
		{
			m_camera.saveCurrentPos((float)x, (float)y);
			m_cameraControl = true;
		}
		else if (action == GLFW_RELEASE)
			m_cameraControl = false;
	}
}

 

mouseButton() 함수에서는 마우스 우클릭 press 시 카메라 제어 모드가 활성화되고, release되면 비활성화된다.

 

2. rotate() 함수

void Camera::rotate(glmath::vec2& pos) {
	glmath::vec2 deltaPos = pos - m_prevMousePos;

	m_cameraYaw -= deltaPos.x * m_cameraRotSpeed;
	m_cameraPitch -= deltaPos.y * m_cameraRotSpeed;

	if (m_cameraYaw < 0.0f) { m_cameraYaw += 360.0f; }
	if (m_cameraYaw > 360.0f) { m_cameraYaw -= 360.0f; }

	if (m_cameraPitch > 89.0f) { m_cameraPitch = 89.0f; }
	if (m_cameraPitch < -89.0f) { m_cameraPitch = -89.0f; }

	m_prevMousePos = pos;
}

 

YawPitch를 마우스 위치 차이(deltaPos)를 이용해 계산하는 함수이다. m_cameraYaw는 Y축 기준 회전(좌우 회전), m_cameraPitch는 X축 기준 회전(상하 회전)을 나타내고, 이 함수는 Context::mouseMove() 함수에 의해 호출 된다.

void Context::mouseMove(double x, double y) {

	if (!m_cameraControl) { return ; }

	glmath::vec2 pos ((float)x, (float)y);
	m_camera.rotate(pos);
}

 

마우스가 움직이는 이벤트에 의해 호출되고 카메라 제어 모드인 경우 현재 마우스의 위치를 가지고 rotate() 함수를 호출한다.

 

3. move() 함수

void Camera::move(bool isPressW, bool isPressS, bool isPressD,
					bool isPressA, bool isPressE, bool isPressQ) {

	glmath::vec3 cameraRight = glmath::normalize(glmath::cross(m_cameraFront, m_cameraUp));
	glmath::vec3 cameraUp = glmath::normalize(glmath::cross(cameraRight, m_cameraFront));

	if (isPressW) {
		m_cameraPos = m_cameraPos + m_cameraSpeed * m_cameraFront;
	}

	if (isPressS) {
		m_cameraPos = m_cameraPos - m_cameraSpeed * m_cameraFront;
	}

	if (isPressD) {
		m_cameraPos = m_cameraPos + m_cameraSpeed * cameraRight;
	}

	if (isPressA) {
		m_cameraPos = m_cameraPos - m_cameraSpeed * cameraRight;
	}

	if (isPressE) {
		m_cameraPos = m_cameraPos + m_cameraSpeed * cameraUp;
	}

	if (isPressQ) {
		m_cameraPos = m_cameraPos - m_cameraSpeed * cameraUp;
	}
}

 

이 함수는 카메라를 W, S, A, D, Q, E 키 입력에 맞게 3축 방향으로 이동시키는 함수이다. 카메라의 정면 벡터(m_cameraFront), 우측 벡터(cameraRight), 수직 벡터(cameraUp)를 이용해서 움직임을 처리한다.

 

 

카메라의 3개의 축을 찾는 과정

 

 1. 카메라 정면 벡터 (m_cameraFront)

     m_cameraFront 벡터를 그대로 사용한다. (getViewMatrix() 함수에서 Yaw, Pitch를 통해 계속 계산해 놓음)

 2. 카메라 우측 벡터 (cameraRight)

      m_cameraFront 벡터와 카메라 기준 천장 방향인 m_cameraUp (0, 1, 0) 벡터를 외적후 정규화 하여 구한다.

 3. 카메라 수직 벡터 (cameraUp)

    cameraRight 벡터와 m_cameraFront 벡터를 외적하여 정규화하면 cameraUp 벡터가 나온다.

 

W, S, A, D, Q, E 키중 눌린 것에 따라 구해놓은 3축을 기준으로 m_cameraSpeed 만큼 m_cameraPos를 이동시킨다.

이 함수는 Context::processInput() 함수에 의해 호출된다.

 

void Context::processInput(GLFWwindow *window) {

	if (!m_cameraControl) { return ; }

	bool isPressW = glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS;
	bool isPressS = glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS;
	bool isPressD = glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS;
	bool isPressA = glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS;
	bool isPressE = glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS;
	bool isPressQ = glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS;

	m_camera.move(isPressW, isPressS, isPressD, isPressA, isPressE, isPressQ);
}

 

processInput() 함수는 프레임마다 현재 어떤 키가 눌렸는지 확인하고, 그에 맞춰 카메라를 이동시킨다.

 

 

4. getViewMatrix() 함수 

glmath::mat4 Camera::getViewMatrix() {
	glmath::vec3 eulerAngle(m_cameraPitch, m_cameraYaw, 0);
	glmath::vec4 cameraFront = glmath::mat4_cast(glmath::quat(eulerAngle)) * glmath::vec4(0.0f, 0.0f, -1.0f, 0.0f);
	m_cameraFront = glmath::vec3(cameraFront.x, cameraFront.y, cameraFront.z);
	
	return glmath::lookAt(m_cameraPos, m_cameraPos + m_cameraFront, m_cameraUp);
}

 

 이 함수는 Yaw와 Pitch로 카메라의 방향을 계산한 다음, lookAt() 함수를 이용해 카메라의 뷰 매트릭스를 반환한다. 오일러 회전 대신 쿼터니언을 사용해서 짐벌락 문제를 해결했다.

짐벌락(Gimbal Lock)과 쿼터니언

 짐벌락은 오일러 각을 사용할 때 발생하는 문제로, 회전 축이 겹쳐서 회전 자유도가 제한되는 현상이다. 이를 해결하기 위해 쿼터니언을 사용했는데, 쿼터니언은 3D 회전을 4차원 벡터로 표현해서, 회전 순서에 상관없이 자유롭게 회전할 수 있고, 짐벌락 문제가 발생하지 않는다.

 쿼터니언을 사용하여 회전 변환을 구하기 위해 glmath 라이브러리에 quat 클래스와 쿼터니언을 회전 변환으로 만들어주는 mat4_cast 함수를 구현하였다.

 


구현 결과

위 함수들을 통해 카메라 제어를 구현하였고 다음과 같은 결과를 얻을 수 있었다.

 


앞으로의 계획

다음에는 Human model의 움직임을 구현해 볼 것이다.

관련글 더보기