Notice
Recent Posts
Recent Comments
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Today
Total
Archives
관리 메뉴

cyphen156

게임 수학 10장 : 3차원 공간(입체 공간의 생성)-3차원 트랜스폼과 오일러각 본문

수학/게임수학

게임 수학 10장 : 3차원 공간(입체 공간의 생성)-3차원 트랜스폼과 오일러각

cyphen156 2025. 3. 24. 19:41

3차원 공간은 원근감이 존재하기 때문에 구현 방법이 복잡해지고, 예상밖의 상황이 많이 생긴다고 한다.

이것을 오일러의 각도법과 벡터의 외적으로 하나씩 해결해 나가면서 여러 수학적 지식과 게임 엔진 개발에 대한 지식들을 배울 수 있을 것 같다.

좌표계 기준

이 전에 한번 언급한적 있었던 왼손 좌표계오른손 좌표계에 대한 내용이다. 

이 둘은 관점에 따라 3차원 공간을 설계하는 큰 차이를 만들어내는 기준들이 되는데 그 기준이 물체를 바라보는데 있어서 나를 기준으로 오는지, 또는 앞으로 가는지를 결정한다.

대표적인 특징은 다음과 같다. 하지만 상대적인 기준일 뿐 절대적이지는 않다.

  • 왼손 좌표계 : 배경을 관찰하는데 적합하다
  • 오른손 좌표계 : 물체를 관찰하는데 더 익숙하다.

밑의 그림은 대표적인 게임, 모델링 툴, 엔진과 같은 프로그램들이 사용하는 좌표계 기준점이다.

CK소프트 렌더러에서는 오른손 좌표계를 기반으로 축을 다음과 같이 정의하고 있다.

앞서 배웠던 내용들은 모두 2차원 공간을 다루고 있었기 때문에 실제 사용한 행렬식이 모두 3*3 Matrix였다. 

하지만 이제는 3차원 공간을 다루어야 하기 때문에 4 * 4 행렬식을 사용하기 시작할 것이고, 이것은 우리가 인식할 수 없는 가상차원인 4차원 공간과 수학적으로는 허수 공간을 사용해야 한다.

여기서 회전을 위해 주로 사용되는 것이 사원수( Quaternion )라는 개념이다.

그 전에 먼저 3차원 공간에서 회전을 표현하는 대표적인 방법인 오일러 각에 대해 알아보겠다.

오일러 각(Euler's Angle) 

오일러 각은 3차원 공간에서 물체가 놓인 방향(3개의 차원)을 중심축으로 활용해 회전을 표현하는 방법이다.

우리가 바라보는 세계와 동일하게 좌표계를 사용하기 때문에 물체가 한쪽 방향으로만 회전한다면 직관적으로 어느정도 회전했는 지를 알 기 쉽다는 장점이 있다.

오일러 각에서의 회전 운동은 다음과 같이 항공기의 기본 3축에 대응하여 정의된다.

게임 엔진에서는 오일러 각으로 표현된 회전 운동 벡터를 각각의 회전을 각 축에 맞게 한번씩, 총 세번의 회전 행렬을 곱하여 처리하기 때문에 실질적으로 세 번의 회전이 발생한다.

이것은 다시말해 회전 순서에 따라 결과가 달라질 수 있다는 것이다.

  • Yaw → Pitch → Roll
  • Yaw → Roll → Pitch
  • Pitch → Roll → Yaw
  • Pitch → Yaw → Roll
  • Roll → Yaw → Pitch
  • Roll → Pitch → Yaw

일반적인 경우 가장 많이 쓰이는 방식은 마지막에 소개된 Roll → Pitch → Yaw의 회전 행렬 곱인데, 결과만 보자면 다음과 같다.

위의 행렬 처럼 3차원 공간의 회전을 다루려면 최종적으로 9개의 실수(float / double) 데이터가 필요하다.

하지만 오일러 각을 통한 회전의 표현방식에서는 3축 회전 정보만 가지고 있다면 회전변환을 통해 표현할 수 있기 때문에 실제로는 필요한 데이터가 압축되는 효과가 있다.

예제 10_1 머리 바로 위에서 공간감 없이 직교(Orthographic) 투영 방식으로 바라보기

SoftRenderer3D.cpp

LoadScene3D()함수 수정내용

GameObject& goPlayer = g.CreateNewGameObject(PlayerGo);
goPlayer.SetMesh(GameEngine::CubeMesh);
goPlayer.GetTransform().SetPosition(Vector3::Zero);
goPlayer.GetTransform().SetScale(Vector3::One * playerScale);
goPlayer.GetTransform().SetRotation(Rotator(0.f, 0.f, 0.f));

goPlayer.SetColor(LinearColor::Blue);

CameraObject& mainCamera = g.GetMainCamera();

// 0, 0, 0으로 지정하면플레이어 못본다
// why? 플레이어 머리 위에서 봐야 하기 때문에
// && 회전도 해야한다. 
mainCamera.GetTransform().SetPosition(Vector3(0.f, 0.f, 500.f));
mainCamera.GetTransform().SetRotation(Rotator(180.f, 0.f, 0.f));

예제 10_2 플레이어 이동 코드 추가하기(카메라 종속시키기)

Update3D()함수

/// 예제 10_2 플레이어 이동 코드 추가하기(카메라 종속시키기)
goPlayer.GetTransform().AddPosition(Vector3::UnitZ * input.GetAxis(InputAxis::ZAxis) * moveSpeed * InDeltaSeconds);
goPlayer.GetTransform().AddPitchRotation(-input.GetAxis(InputAxis::WAxis) * rotateSpeed * InDeltaSeconds);

camera.GetTransform().AddYawRotation(-input.GetAxis(InputAxis::XAxis) * rotateSpeed * InDeltaSeconds);
camera.GetTransform().AddPitchRotation(-input.GetAxis(InputAxis::YAxis) * rotateSpeed * InDeltaSeconds);

예제 10_3 3차원 공간에서 렌더링하기(원근 투영하기)

Render3D()함수(실제로는 이미 구현되어있음)

// 렌더링 로직을 담당하는 함수
void SoftRenderer::Render3D()
{
	// 렌더링 로직에서 사용하는 모듈 내 주요 레퍼런스
	const GameEngine& g = Get3DGameEngine();
	auto& r = GetRenderer();
	const CameraObject& mainCamera = g.GetMainCamera();

	// 배경에 기즈모 그리기
	DrawGizmo3D();

	// 렌더링 로직의 로컬 변수
	const Matrix4x4 vMatrix = mainCamera.GetViewMatrix();

	for (auto it = g.SceneBegin(); it != g.SceneEnd(); ++it)
	{
		const GameObject& gameObject = *(*it);
		if (!gameObject.HasMesh() || !gameObject.IsVisible())
		{
			continue;
		}

		// 렌더링에 필요한 게임 오브젝트의 주요 레퍼런스를 얻기
		const Mesh& mesh = g.GetMesh(gameObject.GetMeshKey());
		const TransformComponent& transform = gameObject.GetTransform();

		Matrix4x4 finalMatrix = vMatrix * transform.GetModelingMatrix();

		// 메시 그리기
		DrawMesh3D(mesh, finalMatrix, gameObject.GetColor());

		// 뷰 공간에서의 플레이어 위치를 화면에 표시
		if (gameObject == PlayerGo)
		{
			Vector3 viewPosition = vMatrix * transform.GetPosition();
			r.PushStatisticText("View : " + viewPosition.ToString());
		}
	}
}

그러나 실제로 객체를 한번에 세 방향 모두를 회전시키는 경우는 그렇게 많지 않은데, 그 이유는 오일러 각에서 세 축이 특정 상황에서 발생하게 되는 짐벌락이라는 현상이 존재하기 때문이다.

짐벌락(Gimbal lock)

짐벌락이란 회전 순서에 따라 회전하다가 특정 축이 일치하게 되는 순간 회전운동의 자유도가 사라져 하나의 회전운동이 무시되는 현상이 발생하는데 이것을 짐벌락이라고 부른다.

 

  • 세 개의 회전축 중 두 축이 같은 방향을 바라보게 되면,
    → 하나의 회전이 무시되는 상황 발생
  • 이는 비행기 시뮬레이션, 로봇 관절, 3D 게임 회전 로직에서는 치명적

 

짐벌락이 발생하는 이유는 각 축의 회전운동이 서로 별개로 표현되었다 할 지라도 서로 영향을 주는 행렬 곱의 관계에 있기 때문이다.  

짐벌

강의중에 소개된 짐벌락 현상

예제 10_4 짐벌락 체험해보기

Update3D()함수

 

/// 예제 10_4 짐벌락 체험해보기
Rotator r = goPlayer.GetTransform().GetRotation();
if (input.IsPressed(InputButton::Space))
{
	r.Pitch = -90;
}
else
{
	r.Pitch += input.GetAxis(InputAxis::ZAxis) * rotateSpeed * InDeltaSeconds;
}

r.Roll += input.GetAxis(InputAxis::YAxis) * rotateSpeed * InDeltaSeconds;
goPlayer.GetTransform().SetRotation(r);

제한 없는 회전 운동

피치 회전이 -90이 됨에 따라 한 축 운동이 제한된 회전운동

보간

시작 동작과 마무리 동작 사이의 중간 과정이 끊기는 일 없이 부드럽게 표현하기 위해 중간값을 찾아 임시로 반영하는 것을 보간이라 부른다.

예를 들어 캐릭터가 칼을 들었다가 내리는 동작을 할 때 두 프레임으로 계산한다면 중간에 위치한 칼이 몸 가운데 위치한다 라는 움직임이 생략된다. 이러한 끊긴 움직임은 플레이어에게 렉에 걸린것 과 같은 이상한 움직임을 느끼도록 만들 수 있다. 그래서 캐릭터가 움직이고, 커지고, 회전하는 트랜스폼의 변형이 필요한 작업에는 항상 보간이 필요하다.

이러한 애니메이션을 위해 여러 처리들이 필요한데, 대표적인 것이 프레임연산 시간에 따른 보정과, 두 움직임 사이의 트랜스폼 보간이 대표적이다.

보간함수는 대개 Lerp함수(Linear Interpolation / 선형 보간)로 지정되어 있다. 수식은 동작의 시작점을 0, 마무리 지점을 1로 비율삼아 진행에 두 벡터의 아핀 결합으로 나타낼 수 있다. 

Lerp(a, b, t) = (1 - t) * a + t * b

한 축만을 회전시킬때는 오일러 각을 사용해 보간하는 것이 가능하지만, 앞서 말했듯 회전운동은 서로 영향을 주기 때문에 행렬곱의 법칙에 의해 두 축의 회전을 사용한다면 보간 값에 의해 회전 변환이 수시로 변하기 때문에 일반적인 보간을 사용할 경우 후보정이 필요해지기 때문에 오히려 연산량이 늘어난다. 

이 문제를 해결한 것이 축각 방식의 회전인 SLerp(Spear Linear InterPolation / 구형 보간)함수이다.

※ 이 내용에 대한 것은 후에 다루도록 하겠다.

모든 예제 코드의 소스파일은 

GitHub - onlybooks/gamemath: <이득우의 게임 수학> 공식 깃허브 페이지

 

GitHub - onlybooks/gamemath: <이득우의 게임 수학> 공식 깃허브 페이지

<이득우의 게임 수학> 공식 깃허브 페이지. Contribute to onlybooks/gamemath development by creating an account on GitHub.

github.com

또한 제 개인 깃허브 레포지토리 에 있습니다.

Workspace/C++/GameMath at main · cyphen156/Workspace · GitHub

 

Workspace/C++/GameMath at main · cyphen156/Workspace

Studying . Contribute to cyphen156/Workspace development by creating an account on GitHub.

github.com

※ 이득우 교수님의 인프런 게임수학강의를 참고하여 작성되었습니다.