최근 면접자리에서 짐벌락 관련 질문이 나온적이 있다.

 

정확히는, 내가 경력기술서에 작성한 내용 중

 

- 짐벌락 이슈에 대응하기 위해, 회전 정보 획득시 GetActorRotation이 아닌 GetComponentRotation 함수를 Root Component를 대상으로 호출하여 이용함.

 

라는 문장에 대한 질문이었는데, 내가 답변을 좀 에매모호하게 한거 같아서 정리를 좀 하고자 한다.

 

 


짐벌락?

 

짐벌락(Gimbal lock) 은 말 그대로 짐벌 (물체가 회전 가능하도록 중심축을 가진 물체)에 락이 걸린 상황을 의미한다.

 

보통, 3D 회전은 오일러 좌표계 (혹은 오일러 각 / Eular Angle)을 따르는데, 이때 3개의 축을 이용한다.

 

각각을 Roll, Pitch, Yaw로 표현하고, 보통은 각각의 수치가 X축 회전, Y축 회전, Z축 회전에 대응한다.

 

 

 

그래서 짐벌락은 언제, 왜 생기는가.

 

간단히 요약하는 짐벌락

 

 

간단히 요약하면, 축이 겹쳐서 발생하는 현상이다.

 

물체가 회전할때 명심해야 할 것이 있는데, 회전축이 회전할때는 다른 축의 회전에 영향을 받는다.

 

특히, 명심해야 하는 사항이 회전에는 순서가 있고, 순서에 따라 다른 결과값을 갖고온다는 점이다.

 

이러다보니 발생하는 문제가 바로 짐벌락. 임의의 축을 기준으로 회전했을때 회전한 축이 아닌 다른 축이 겹치는 현상이 발생하는데, 이렇게 되면 특정 축이 연산에 전혀 영향을 못주게 되면 문제가 발생한다.

 

예시로, X = 빨강, Y = 초록, Z = 파랑이라 생각해보자.

예를들어, X, Y축이 겹치는 현상이 발생했다면,  X축을 회전하던, Y축을 회전하던 결국 Z축을 기준으로 똑같이 빙빙 도는 결과가 발생한다.

 

짐벌락이 문제가 되는 경우는, 해당 상황과 같이 특정 축이 락이 걸려버린경우 해당 축의 연산값이 전혀 영향을 못 미치게 된다는것인데, 이로 인해 정상적인 회전을 보장할 수가 없게된다.

 

 

 

쿼터니언

 

선형대수학에서는 오일러 각을 이용할때 생기는 이러한 문제를 해결하기 위해 4개의 축을 이용하는 쿼터니언 이라는것을 이용하여 연산을 한다.

 

쿼터니언 표현식이 좀 많은데, 제일 많이 쓰는 식은

 

q = xi + yj + zk + w

 

의 식으로 기억하고 있다. 표현대로 각각 x축, y축, z축과.. 가상의 w축을 포함하여 4개의 축을 이용한다.

 

이를 이용해서 회전 처리를 할때 짐벌락이 걸리지 않도록 하는것인데... 쿼터니언 설명 자체는 다음에 필요하면 하도록 하겠다.

 

 


언리얼에서는

 

언리얼에서도 당연히 짐벌락을 막기 위해 많은 방법을 선택한다.

 

주로 사용되는 방식은 2가지인데,

 

첫번째. 회전시 쿼터니언을 사용한다. 

두번째. 회전값 표현시 축 분리를 한다.

 

 

우선 첫번째부터 설명하면, 언리얼에서 물체의 회전 및 위치, 스케일을 가지고 있는 "FTransform" 구조체를 뜯어보면 바로 답이 나온다.

 

protected:
	/** Rotation of this transformation, as a quaternion */
	TPersistentVectorRegisterType<T> Rotation;
	/** Translation of this transformation, as a vector */
	TPersistentVectorRegisterType<T> Translation;
	/** 3D scale (always applied in local space) as a vector */
	TPersistentVectorRegisterType<T> Scale3D;
public:
	/**
	 * The identity transformation (Rotation = TQuat<T>::Identity, Translation = TVector<T>::ZeroVector, Scale3D = (1,1,1))
	 */
	CORE_API static const TTransform<T> Identity;

그냥 대놓고 쿼터니언으로 변환해서 쓴다고 얘기를 한다.

 

언리얼에선 TQuat라는 쿼터니언 자료형이 있는데, 내부적으로는 해당 값을 사용하고, 오일러 좌표계로 표현되는 FRotator 값이 내부 함수에서 쿼터니언으로 변환되어 들어온다.

 

	FORCEINLINE explicit TTransform(const TRotator<T>& InRotation) 
	{
		TQuat<T> InQuatRotation = TQuat<T>::MakeFromRotator(InRotation);
		// Rotation = InRotation
		Rotation =  VectorLoadAligned( &InQuatRotation );
		// Translation = {0,0,0,0)
		Translation = GlobalVectorConstants::FloatZero;
		// Scale3D = {1,1,1,0);
		Scale3D = GlobalVectorConstants::Float1110;

		DiagnosticCheckNaN_All();
	}

 

 

두번째가 조금 설명하기 까다로운데,
우선 설명해야할것은, 쿼터니언 회전값은 해당 식만 보고는 이게 어떤 회전값을 가지고 있는지 바로 알기가 힘들다.
물론, FQuat와 FRotator를 변환해주는 함수가 없다는 뜻은 아니고, 쿼터니언 값만 가지고 원하는 오일러 각으로 변환하기가 쉽지 않다는 뜻이다. (물론, 지금은 시키면 할 수 있을거 같긴한데)

 

아무튼 그런 이유로, 언리얼 에디터에서는 회전값을 쿼터니언이 아닌 오일러 좌표계로 표현한다.

하지만, 오일러 좌표계를 이용하면 짐벌락을 피하기가 쉽지 않은데, 이를 "자동으로" 각도를 분할시키는 방식으로 해결하려고 했다.

 

무슨소리냐

 

예를들어, 멀쩡한 물체의 각도를 회전시키면

 

처음에는 0,0,0임

 

Y축 (Pitch)를 회전시키려고 하면

 

짜잔! 건들지도 않은 Yaw도 바뀌어버려요!

 

이처럼, 짐벌락을 피하기 위해 임의의 각을 연산하여 추가로 회전시키는 로직이 들어간다.

 

이러한 연산이 일반적인 회전 처리에서는 당연히 도움이 되지만...

회전이 들어간 이후 "인위적으로 회전값을 변경해야 하는 경우"에 문제가 발생했다.


그래서 뭐 한건데?


문제가 뭐였나면요...

 

 

위에서 말한것에서 이어서, 언리얼에서는 짐벌락을 피하기 위해 임의의 각 회전을 추가로 하는 경우가 있다고 했다.

하지만, 다음과 같은 경우에서 문제가 발생했었다.

 

 

예를들어, 특정 이동에서 X축값 (Roll)이 회전이 들어간 경우, Roll값을 0으로 만들고싶으면 Roll을 0으로 만드는 회전값 오버라이드를 하면 그만일것이다.

 

처음엔 당연히 그렇게 생각했다. 하지만 Roll을 회전 시킨 결과가 과연 Roll에만 있을까?

 

여기서 해당 회전 수치를 보장 할 수 없는 문제가 생겨버린다. 내가 임의의 축을 0으로 돌렸을때, 나머지 회전값은 내가 원하던 회전이 맞는가?

 

여기서 의문이 들 수 있다.

첫번째. "그냥 모든 축을 0으로 바꾸면 안됐나요?"

두번째. "내가 필요한 회전축을 제외하고 0으로 만들면 안되나요?"

 

둘다 답은 X다. 당연히 첫번째는 말도 안되고, 두번째의 경우, 처음 회전한 Yaw 값을 유지하는것이 목적이었는데
(언리얼에서 캐릭터를 조금이라도 만져보면 알겠지만, 서 있는 상태에서 회전은 Yaw값을 사용하여 움직인다)

 

임의의 축에서 회전이 발생 -> 짐벌락을 피하겠다고 언리얼에서 자체적으로 회전 처리 -> Yaw값에 손상 -> 나머지 회전값을 처리해도 Yaw값이 원하는 회전값이 아니게 되는 문제가 발생

 

이라는 문제가 발생해버리고 있었다.

 

 

 


해결방안

 

우선, Actor에서 회전값을 받아오는 GetActorRotation() 함수는 다음과 같은 구조로 작동한다.

template<class T>
static FORCEINLINE FRotator TemplateGetActorRotation(const T* RootComponent)
{
	return (RootComponent != nullptr) ? RootComponent->GetComponentRotation() : FRotator::ZeroRotator;
}

//---

/** Return rotation of the component, in world space */
FORCEINLINE FRotator GetComponentRotation() const
{
	return WorldRotationCache.NormalizedQuatToRotator(GetComponentTransform().GetRotation());
}

 

보통은 RootComponent의 회전값을 갖고와서 리턴해주는 방식인데, 당연히 대체로는 문제가 없다.

 

그런데 갖고오는 부분 어딘가에서 문제가 생기는지, RootComponent의 회전값과 다른 무언가를 갖고오는 현상이 있었는데, 이를 막기 위해 RootComponent에서 갖고오는 (정확히는 엔진 코멘트에 "해당 값은 RootComponent의 값과 동일함" 이라는 코멘트가 있는 부분이 있었다...) 방안을 선택했는데...

 

.....근데 분명 작업할때는 그런 코멘트를 본 기억이 있는데 (심지어 내부 문서에도 다 기록해놨다) 지금와서 찾으려니까 안보인다...???

 

그냥 해당 기록은 안쓰는거로 해야할거같다... 증거가 없으면 못쓰지요

 

사실 이제와서 해결하려면 그냥 쿼터니언 연산으로 해결하면 되긴 하는데 (...)

쿼터니언 연산으로 X축 성분, Y축 성분 제거하고 Z축 성분만 리턴해주면 될거다 아마도...

 

 


자료 출처

 

1.https://en.wikipedia.org/wiki/Euler_angles

2.https://en.wikipedia.org/wiki/Gimbal_lock

 

+ Recent posts