오일러각과 회전행렬
언리얼 엔진에서 회전각을 역회전행렬로 변환하는 FInverseRotationMatrix 클래스의 생성자는 다음과 같다.
역행렬의 성질에 따라
Y − 1 P − 1 R − 1 = ( R P Y ) − 1 Y^{-1}P^{-1}R^{-1}=(RPY)^{-1} Y − 1 P − 1 R − 1 = ( R P Y ) − 1
임을 생각해본다면, 본디 축회전연산의 순서는 Roll-Pitch-Yaw 순임을 알 수 있다.
또한, 이들 회전행렬은 직교행렬이기에
A − 1 = A T A^{-1} = A^T A − 1 = A T
이다. 따라서, 언리얼엔진에서 쓰는 Roll, Pitch, Yaw의 회전행렬을 알 수 있다.
x축 회전 - RollR = ( 1 0 0 0 0 c o s R − s i n R 0 0 s i n R c o s R 0 0 0 0 1 ) R=\begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & cosR & -sinR & 0 \\ 0 & sinR & cosR & 0 \\ 0 & 0 & 0 & 1 \\ \end{pmatrix} R = ⎝ ⎜ ⎜ ⎜ ⎛ 1 0 0 0 0 c o s R s i n R 0 0 − s i n R c o s R 0 0 0 0 1 ⎠ ⎟ ⎟ ⎟ ⎞
y축 회전 - PitchP = ( c o s P 0 s i n P 0 0 1 0 0 − s i n P 0 c o s P 0 0 0 0 1 ) P=\begin{pmatrix} cosP & 0 & sinP & 0 \\ 0 & 1 & 0 & 0 \\ -sinP & 0 & cosP & 0 \\ 0 & 0 & 0 & 1 \\ \end{pmatrix} P = ⎝ ⎜ ⎜ ⎜ ⎛ c o s P 0 − s i n P 0 0 1 0 0 s i n P 0 c o s P 0 0 0 0 1 ⎠ ⎟ ⎟ ⎟ ⎞
z축 회전 - YawY = ( c o s Y s i n Y 0 0 − s i n Y c o s Y 0 0 0 0 1 0 0 0 0 1 ) Y=\begin{pmatrix} cosY & sinY & 0 & 0 \\ -sinY & cosY & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{pmatrix} Y = ⎝ ⎜ ⎜ ⎜ ⎛ c o s Y − s i n Y 0 0 s i n Y c o s Y 0 0 0 0 1 0 0 0 0 1 ⎠ ⎟ ⎟ ⎟ ⎞
이제 모두 곱하여 회전 복합행렬을 만들 수 있다.
R P Y = ( c o s P c o s Y c o s P s i n Y s i n P 0 s i n R s i n P c o s Y − c o s R s i n Y s i n R s i n P s i n Y + c o s R c o s Y − s i n R c o s P 0 − c o s R s i n P c o s Y − s i n R s i n Y − c o s R s i n P s i n Y + s i n R c o s Y c o s R c o s P 0 0 0 0 1 ) RPY = \begin{pmatrix} cosPcosY & cosPsinY & sinP & 0 \\ sinRsinPcosY-cosRsinY & sinRsinPsinY+cosRcosY & -sinRcosP & 0 \\ -cosRsinPcosY-sinRsinY & -cosRsinPsinY+sinRcosY & cosRcosP & 0 \\ 0 & 0 & 0 & 1 \\ \end{pmatrix} R P Y = ⎝ ⎜ ⎜ ⎜ ⎛ c o s P c o s Y s i n R s i n P c o s Y − c o s R s i n Y − c o s R s i n P c o s Y − s i n R s i n Y 0 c o s P s i n Y s i n R s i n P s i n Y + c o s R c o s Y − c o s R s i n P s i n Y + s i n R c o s Y 0 s i n P − s i n R c o s P c o s R c o s P 0 0 0 0 1 ⎠ ⎟ ⎟ ⎟ ⎞
실제로 FRotationTranslationMatrix 클래스의 생성자 구현코드를 보면, 각 요소들의 계산이 위 행렬과 같음을 확인해볼 수 있다.
FORCEINLINE FRotationTranslationMatrix::FRotationTranslationMatrix(const FRotator& Rot, const FVector& Origin)
{
#if PLATFORM_ENABLE_VECTORINTRINSICS
const VectorRegister Angles = MakeVectorRegister(Rot.Pitch, Rot.Yaw, Rot.Roll, 0.0f);
const VectorRegister HalfAngles = VectorMultiply(Angles, GlobalVectorConstants::DEG_TO_RAD);
union { VectorRegister v; float f[4]; } SinAngles, CosAngles;
VectorSinCos(&SinAngles.v, &CosAngles.v, &HalfAngles);
const float SP = SinAngles.f[0];
const float SY = SinAngles.f[1];
const float SR = SinAngles.f[2];
const float CP = CosAngles.f[0];
const float CY = CosAngles.f[1];
const float CR = CosAngles.f[2];
#else
...
#endif // PLATFORM_ENABLE_VECTORINTRINSICS
M[0][0] = CP * CY;
M[0][1] = CP * SY;
M[0][2] = SP;
M[0][3] = 0.f;
M[1][0] = SR * SP * CY - CR * SY;
M[1][1] = SR * SP * SY + CR * CY;
M[1][2] = - SR * CP;
M[1][3] = 0.f;
M[2][0] = -( CR * SP * CY + SR * SY );
M[2][1] = CY * SR - CR * SP * SY;
M[2][2] = CR * CP;
M[2][3] = 0.f;
M[3][0] = Origin.X;
M[3][1] = Origin.Y;
M[3][2] = Origin.Z;
M[3][3] = 1.f;
}
Pitch, Roll 축의 반시계 회전
다음은 msdn dx9 축 회전행렬 부분을 스크린샷한 것이다. 자세히보면 아래 x, y축 회전행렬이 위 언리얼의 x, y축 회전행렬과 전치되어 있음을 알 수 있다.
이는 곧 역행렬이라, 회전에서 그 의미는 반대방향으로의 회전을 의미한다.
언리얼 엔진을 켜고 어떤 액터의 축이 나아가는 방향의 정면으로 카메라를 이동한 뒤 회전을 시켜보자. 아래 그림처럼 왼손좌표계를 그대로 따른다면 시계방향으로 회전하겠지만.. 실제로 해보면 Pitch, Roll이 반시계로 회전됨을 확인할 수 있다.
그렇다. 언리얼 엔진은 z-up의 왼손좌표계를 사용하지만, Yaw는 시계방향으로 Roll, Pitch축은 반시계 방향으로 회전한다. 이는 의도된 언리얼 엔진의 회전축 정책으로, 그 이유는 모르겠다.
최근 일관적이지 못한 축회전에 대해 많은 사람들이 의문을 제기하였는지, UE 5.1 버전으로 올라오면서 FRotator 클래스의 주석이 변경되었다.
각도는 Yaw, Pitch, Roll 순서로 적용되는 본질적인 회전으로 해석됩니다. 즉, 객체는 먼저 지정된 Yaw로 up-z(위에서 볼 때 시계 방향이 양의 각도로 해석됨, -Z 방향)을 중심으로 회전하고, 그 다음으로 (새로운) 오른쪽 축을 중심으로 Pitch(양의 각도는 nose-up으로 해석되며, +Y 방향을 따라 볼 때 시계 방향), 마지막으로 (최종) 전방 축을 중심으로 Roll(양의 각도는 +X 방향을 따라 볼 때 시계 방향의 회전으로 해석됨)합니다.
이러한 관례는 쿼터니언 축/각도와 다릅니다. UE Quat는 항상 양의 각도를 왼손 회전으로 간주하지만, Rotator는 Yaw를 왼손으로, Pitch와 Roll을 오른손으로 처리합니다.
Rotator는 Yaw를 왼손좌표계로, Pitch와 Roll을 오른손좌표계로 처리합니다.
이에 회전연산을 직접 구현해 처리할 적엔 이 요상한 축회전 규칙에 주의하여야 한다.
오일러각과 쿼터니언
오일러각으로 쿼터니언 만들기
위 오일러각과 회전행렬처럼 각 x,y,z(Roll-Pitch-Yaw) 축 순서로 회전행렬을 곱해 복합행렬을 만든 것처럼, 각 오일러각을 단계적으로 적용해 쿼터니언을 만들 수 있다.
x ′ = x ∗ P I / 180 x'=x*PI/180 x ′ = x ∗ P I / 1 8 0 , y ′ = y ∗ P I / 180 y'=y*PI/180 y ′ = y ∗ P I / 1 8 0 , z ′ = z ∗ P I / 180 z'=z*PI/180 z ′ = z ∗ P I / 1 8 0
x축(Roll)으로 회전하는 쿼터니언 q x = c r + s r i q_x = cr+sri q x = c r + s r i , ( c r = c o s x ′ 2 , s r = s i n x ′ 2 ) (cr = cos\frac{x'}{2}, sr = sin\frac{x'}{2}) ( c r = c o s 2 x ′ , s r = s i n 2 x ′ )
y축(Roll)으로 회전하는 쿼터니언 q y = c p + s p j q_y = cp+spj q y = c p + s p j , ( c p = c o s y ′ 2 , s p = s i n y ′ 2 ) (cp = cos\frac{y'}{2}, sp = sin\frac{y'}{2}) ( c p = c o s 2 y ′ , s p = s i n 2 y ′ )
z축(Yaw)으로 회전하는 쿼터니언 q z = c y + s y k q_z = cy+syk q z = c y + s y k , ( c y = c o s z ′ 2 , s y = s i n z ′ 2 ) (cy = cos\frac{z'}{2}, sy = sin\frac{z'}{2}) ( c y = c o s 2 z ′ , s y = s i n 2 z ′ )
그런데, 우리는 위에서 Pitch와 Roll이 방향이 언리얼엔진에선 오른손좌표 즉, 시계반대방향으로 회전함을 알고 있다. 따라서, x, y 축으로 회전하는 쿼터니언 허수부의 부호를 반대로 한다.
q x = c r − s r i q_x = cr-sri q x = c r − s r i
q y = c p − s p j q_y = cp-spj q y = c p − s p j
q z = c y + s y k q_z = cy+syk q z = c y + s y k
이렇게 만들어진 x,y,z축 회전 쿼터니언들을 역순(q z q y q x q_z q_y q_x q z q y q x )으로 곱해야한다.
q z q y q x q_z q_y q_x q z q y q x
= ( c y + s y k ) ( c p − s p j ) ( c r − s r i ) =(cy+syk)(cp-spj)(cr-sri) = ( c y + s y k ) ( c p − s p j ) ( c r − s r i )
= ( c p c y − s p c y j + c p s y k + s p s y i ) ( c r + s r i ) =(cpcy-spcyj+cpsyk+spsyi)(cr+sri) = ( c p c y − s p c y j + c p s y k + s p s y i ) ( c r + s r i )
= c r c p c y − s r c p c y i − c r s p c y j − s r s p c y k + c r c p s y k − s r c p s y j + c r s p s y i − s r s p s y =crcpcy-srcpcyi-crspcyj-srspcyk+crcpsyk-srcpsyj+crspsyi-srspsy = c r c p c y − s r c p c y i − c r s p c y j − s r s p c y k + c r c p s y k − s r c p s y j + c r s p s y i − s r s p s y
= ( c r s p s y − s r c p c y ) i + ( − c r s p c y − s r c p s y ) j + ( c r c p s y − s r s p c y ) k + ( c r c p c y − s r s p s y ) =(crspsy-srcpcy)i+(-crspcy-srcpsy)j+(crcpsy-srspcy)k+(crcpcy-srspsy) = ( c r s p s y − s r c p c y ) i + ( − c r s p c y − s r c p s y ) j + ( c r c p s y − s r s p c y ) k + ( c r c p c y − s r s p s y )
곱한 결과, 위와 같이 나오게 되고 i j k ijk i j k 의 상수와 실수부를 사용해 쿼터니언을 만들면된다.
q x = c r s p s y − s r c p c y q_x = crspsy-srcpcy q x = c r s p s y − s r c p c y
q y = − c r s p c y − s r c p s y q_y = -crspcy-srcpsy q y = − c r s p c y − s r c p s y
q z = c r c p s y − s r s p c y q_z = crcpsy-srspcy q z = c r c p s y − s r s p c y
q w = c r c p c y − s r s p s y q_w=crcpcy-srspsy q w = c r c p c y − s r s p s y
이제 아래의 FRotator::Quaternion 함수를 보면, 위 결과와 동일하게 구현되어 있음을 확인할 수 있다.
FQuat FRotator::Quaternion() const
{
...
const float DEG_TO_RAD = PI/(180.f);
const float RADS_DIVIDED_BY_2 = DEG_TO_RAD/2.f;
float SP, SY, SR;
float CP, CY, CR;
const float PitchNoWinding = FMath::Fmod(Pitch, 360.0f);
const float YawNoWinding = FMath::Fmod(Yaw, 360.0f);
const float RollNoWinding = FMath::Fmod(Roll, 360.0f);
FMath::SinCos(&SP, &CP, PitchNoWinding * RADS_DIVIDED_BY_2);
FMath::SinCos(&SY, &CY, YawNoWinding * RADS_DIVIDED_BY_2);
FMath::SinCos(&SR, &CR, RollNoWinding * RADS_DIVIDED_BY_2);
FQuat RotationQuat;
RotationQuat.X = CR*SP*SY - SR*CP*CY;
RotationQuat.Y = -CR*SP*CY - SR*CP*SY;
RotationQuat.Z = CR*CP*SY - SR*SP*CY;
RotationQuat.W = CR*CP*CY + SR*SP*SY;
...
return RotationQuat;
}
쿼터니언을 사용한 회전
다음은 언리얼 엔진에서 쿼터니언 구조체를 사용하여 벡터 V를 회전된 벡터로 리턴하는 함수이다.
왜 이런 코드로 작성되었는지 한번 알아보고자 한다.
FORCEINLINE FVector FQuat::RotateVector(FVector V) const
{
const FVector Q(X, Y, Z);
const FVector T = 2.f * FVector::CrossProduct(Q, V);
const FVector Result = V + (W * T) + FVector::CrossProduct(Q, T);
return Result;
}
먼저, 단위 쿼터니언 q q q 그리고 회전할 벡터 V V V 주어지고, 회전된 벡터를 R e s u l t Result R e s u l t 라고 한다면 쿼터니언 회전식은 다음과 같이 정의된다.
R e s u l t = q p q − 1 Result=qpq^{-1} R e s u l t = q p q − 1 = [ W , Q ] [ 0 , V ] [ W , − Q ] =[W,Q][0,V][W,-Q] = [ W , Q ] [ 0 , V ] [ W , − Q ] W W W 는 쿼터니언 실수부, Q Q Q 는 벡터부, V V V 는 회전할 벡터
사원수의 곱은 다음과 같고,
q 0 q 1 = [ s 0 s 1 − v 0 ⋅ v 1 , s 0 v 1 + s 1 v 0 + v 0 × v 1 ] q_0q_1=[s_0s_1-v_0\cdot v_1,s_0v_1+s_1v_0+v_0\times v_1] q 0 q 1 = [ s 0 s 1 − v 0 ⋅ v 1 , s 0 v 1 + s 1 v 0 + v 0 × v 1 ]
이를 활용해 [ W , Q ] [ 0 , V ] [ W , − Q ] [W,Q][0,V][W,-Q] [ W , Q ] [ 0 , V ] [ W , − Q ] 식을 전개해보면,
[ W , Q ] [ 0 , V ] [ W , − Q ] [W,Q][0,V][W,-Q] [ W , Q ] [ 0 , V ] [ W , − Q ]
= [ − Q ⋅ V , W V + Q × V ] [ W , − Q ] =[-Q \cdot V, WV + Q \times V][W,-Q] = [ − Q ⋅ V , W V + Q × V ] [ W , − Q ]
= [ W ( − Q ⋅ V ) − ( W V + Q × V ) ⋅ − Q ) , Q ( Q ⋅ V ) + W ( W V + Q × V ) + ( ( W V + Q × V ) × ( − Q ) ) ] =[W(-Q \cdot V) - (WV + Q \times V) \cdot -Q), Q(Q \cdot V) + W(WV + Q \times V) + ((WV + Q \times V) \times (-Q))] = [ W ( − Q ⋅ V ) − ( W V + Q × V ) ⋅ − Q ) , Q ( Q ⋅ V ) + W ( W V + Q × V ) + ( ( W V + Q × V ) × ( − Q ) ) ]
까지 된다.
여기서 먼저 실수부부터 전개하면
W ( − Q ⋅ V ) − ( W V + Q × V ) ⋅ − Q ) W(-Q \cdot V) - (WV + Q \times V) \cdot -Q) W ( − Q ⋅ V ) − ( W V + Q × V ) ⋅ − Q )
= [ − W ( Q ⋅ V ) + W ( Q ⋅ V ) + Q ⋅ ( Q × V ) ] =[-W(Q \cdot V) + W(Q \cdot V) + Q \cdot (Q \times V)] = [ − W ( Q ⋅ V ) + W ( Q ⋅ V ) + Q ⋅ ( Q × V ) ]
= [ Q ⋅ ( Q × V ) ] =[Q \cdot (Q \times V)] = [ Q ⋅ ( Q × V ) ]
위와 같이 된다. 그런데, 외적의 성질에 따라 Q × V Q \times V Q × V 벡터는 Q Q Q 와 직각임으로, 서로 내적하면
Q ⋅ ( Q × V ) = 0 Q \cdot (Q \times V) = 0 Q ⋅ ( Q × V ) = 0 이 된다. 따라서, 실수부는 0이 된다.
이제, 벡터부를 전개해보자. 벡터부는 좀 복잡하다.
= [ 0 , Q ( Q ⋅ V ) + W ( W V + Q × V ) + ( W V + Q × V ) × ( − Q ) ] =[0 , Q(Q \cdot V) + W(WV + Q \times V) + (WV + Q \times V) \times (-Q)] = [ 0 , Q ( Q ⋅ V ) + W ( W V + Q × V ) + ( W V + Q × V ) × ( − Q ) ]
외적의 성질 ( c x + y ) × z = c ( x × z ) + y × z (cx + y) \times z = c(x \times z) + y \times z ( c x + y ) × z = c ( x × z ) + y × z 에 따라,
( W V + Q × V ) × ( − Q ) = − W ( V × Q ) − ( Q × V ) × Q (WV + Q \times V) \times (-Q)= - W(V \times Q) - (Q \times V) \times Q ( W V + Q × V ) × ( − Q ) = − W ( V × Q ) − ( Q × V ) × Q
= [ 0 , Q ( Q ⋅ V ) + W 2 V + W ( Q × V ) − W ( V × Q ) − ( ( Q × V ) × Q ) ] =[0, Q(Q \cdot V) + W^2V + W(Q \times V) -W(V \times Q) - ((Q \times V) \times Q)] = [ 0 , Q ( Q ⋅ V ) + W 2 V + W ( Q × V ) − W ( V × Q ) − ( ( Q × V ) × Q ) ]
단위 쿼터니언은 크기가 1로 정규화된 사원수이며, 다음 식으로 정의된다.
∣ q ∣ = s 2 + ∣ v ∣ 2 = 1 |q| = \sqrt{s^2 + |v|^2} = 1 ∣ q ∣ = s 2 + ∣ v ∣ 2 = 1
따라서, 다음식이 성립할 수 있다.
s 2 = 1 − ∣ v ∣ 2 s^2=1-|v|^2 s 2 = 1 − ∣ v ∣ 2
= 1 − v ⋅ v =1-v \cdot v = 1 − v ⋅ v
마찬가지로 W 2 = 1 − Q ⋅ Q W^2=1-Q \cdot Q W 2 = 1 − Q ⋅ Q 임으로, 위 식에서 W 2 W^2 W 2 에 1 − Q ⋅ Q 1-Q \cdot Q 1 − Q ⋅ Q 를 삽입하면,
= [ 0 , Q ( Q ⋅ V ) + ( 1 − Q ⋅ Q ) V + W ( Q × V ) − W ( V × Q ) − ( ( Q × V ) × Q ) ] =[0, Q(Q \cdot V) + (1-Q \cdot Q)V + W(Q \times V) - W(V \times Q) - ((Q \times V) \times Q)] = [ 0 , Q ( Q ⋅ V ) + ( 1 − Q ⋅ Q ) V + W ( Q × V ) − W ( V × Q ) − ( ( Q × V ) × Q ) ]
= [ 0 , Q ( Q ⋅ V ) + ( V − ( V ( Q ⋅ Q ) ) + W ( Q × V ) − W ( V × Q ) − ( ( Q × V ) × Q ) ] =[0, Q(Q \cdot V) + (V-(V(Q \cdot Q)) + W(Q \times V) -W(V \times Q) - ((Q \times V )\times Q)] = [ 0 , Q ( Q ⋅ V ) + ( V − ( V ( Q ⋅ Q ) ) + W ( Q × V ) − W ( V × Q ) − ( ( Q × V ) × Q ) ]
벡터삼중곱은 외적한 두 벡터에 다시 다른 벡터를 외적한 것을 말하는데 이는 다음과 같다.
a × ( b × c ) = b ( a ⋅ c ) − c ( a ⋅ b ) a \times (b \times c) = b(a \cdot c) - c(a \cdot b) a × ( b × c ) = b ( a ⋅ c ) − c ( a ⋅ b )
( a × b ) × c = − c × ( a × b ) = − a ( b ⋅ c ) + b ( a ⋅ c ) (a \times b) \times c = -c \times (a \times b) = - a(b \cdot c) + b(a \cdot c) ( a × b ) × c = − c × ( a × b ) = − a ( b ⋅ c ) + b ( a ⋅ c )
이를 활용해 아래 파트를 다음과 같이 정리할 수 있다.
( ( Q × V ) × Q ) ((Q \times V) \times Q) ( ( Q × V ) × Q )
= − Q ( Q ⋅ V ) + V ( Q ⋅ Q ) =-Q(Q \cdot V ) + V(Q \cdot Q) = − Q ( Q ⋅ V ) + V ( Q ⋅ Q )
이를 식에 삽입하면 다음과 같다.
= [ 0 , Q ( Q ⋅ V ) + V − V ( Q ⋅ Q ) + W ( Q × V ) − W ( V × Q ) + Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ] =[0, Q(Q \cdot V) + V-V(Q \cdot Q) + W(Q \times V) -W(V \times Q) + Q(Q \cdot V ) - V(Q \cdot Q)] = [ 0 , Q ( Q ⋅ V ) + V − V ( Q ⋅ Q ) + W ( Q × V ) − W ( V × Q ) + Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ]
이제 같은 거 끼리 잘 묶어주면
= [ 0 , V + W ( Q × V ) − W ( V × Q ) + 2 ( Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ) ] =[0, V + W(Q \times V) -W(V \times Q) + 2(Q(Q \cdot V) - V(Q \cdot Q))] = [ 0 , V + W ( Q × V ) − W ( V × Q ) + 2 ( Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ) ]
외적의 성질에 따라, A × B = − B × A A \times B = - B \times A A × B = − B × A 임으로 아래와 같이 정리할 수 있다.
= [ 0 , V + 2 W ( Q × V ) + 2 ( Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ) ] =[0, V + 2W(Q \times V) + 2(Q(Q \cdot V) - V(Q \cdot Q))] = [ 0 , V + 2 W ( Q × V ) + 2 ( Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ) ]
그리고 다시 다음 파트를 벡터삼중곱을 사용하면
Q ( Q ⋅ V ) − V ( Q ⋅ Q ) = Q × ( Q × V ) Q(Q \cdot V) - V(Q \cdot Q) = Q \times (Q \times V) Q ( Q ⋅ V ) − V ( Q ⋅ Q ) = Q × ( Q × V )
최종적으로 다음으로 정리할 수 있겠다.
= [ 0 , V + 2 W ( Q × V ) + 2 ( Q × ( Q × V ) ) ] =[0, V + 2W(Q \times V) + 2(Q \times (Q \times V))] = [ 0 , V + 2 W ( Q × V ) + 2 ( Q × ( Q × V ) ) ]
언리얼엔진은 여기에 2 ( Q × V ) = T 2(Q \times V) = T 2 ( Q × V ) = T 로 두어 다음과 같이 사용한다.
R e s u l t = V + W T + Q × T Result=V + WT + Q \times T R e s u l t = V + W T + Q × T
const FVector Result = V + (W * T) + FVector::CrossProduct(Q, T);
쿼터니언으로 회전행렬 만들기
이제 마지막으로 쿼터니언을 회전행렬로 변환하는 방식과 이를 구현한 언리얼엔진의 소스코드를 보고자 한다.
참고로, 위 글 내용처럼 FRotator -> FQuat로 변환되는 과정에서 Pitch, Roll 축이 역회전된 상태로 저장되기에, 쿼터니언에서의 축 회전방향은 모두 시계방향으로 통일된다. 따라서, FQuat -> FMatrix로 변환할 땐 축 방향은 신경쓰지 않아도 된다.
위 글 "쿼터니언을 사용한 회전" 파트 중
= [ 0 , V + 2 W ( Q × V ) + 2 ( Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ) ] =[0, V + 2W(Q \times V) + 2(Q(Q \cdot V) - V(Q \cdot Q))] = [ 0 , V + 2 W ( Q × V ) + 2 ( Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ) ]
이 전개식에서
= [ 0 , V + 2 W ( Q × V ) + 2 ( Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ) ] =[0, V + 2W(Q \times V) + 2(Q(Q \cdot V) - V(Q \cdot Q))] = [ 0 , V + 2 W ( Q × V ) + 2 ( Q ( Q ⋅ V ) − V ( Q ⋅ Q ) ) ]
= [ 0 , V + 2 W ( Q × V ) + 2 Q ( Q ⋅ V ) − 2 V ( Q ⋅ Q ) ] =[0, V + 2W(Q \times V) + 2Q(Q \cdot V) - 2V(Q \cdot Q)] = [ 0 , V + 2 W ( Q × V ) + 2 Q ( Q ⋅ V ) − 2 V ( Q ⋅ Q ) ]
= [ 0 , V − 2 V ( Q ⋅ Q ) + 2 W ( Q × V ) + 2 Q ( Q ⋅ V ) ] =[0, V - 2V(Q \cdot Q) + 2W(Q \times V) + 2Q(Q \cdot V)] = [ 0 , V − 2 V ( Q ⋅ Q ) + 2 W ( Q × V ) + 2 Q ( Q ⋅ V ) ]
이며, Q ⋅ Q = Q 2 Q \cdot Q = Q^2 Q ⋅ Q = Q 2 임으로 V − 2 V ( Q ⋅ Q ) = V ( 1 − 2 Q 2 ) = V ( 1 − Q 2 − Q 2 ) V-2V(Q\cdot Q)= V(1-2Q^2)=V(1-Q^2-Q^2) V − 2 V ( Q ⋅ Q ) = V ( 1 − 2 Q 2 ) = V ( 1 − Q 2 − Q 2 ) 로 표현할 수 있다. 여기서 1 − Q 2 = W 2 1-Q^2=W^2 1 − Q 2 = W 2 와 같음으로
= [ 0 , ( W 2 − Q 2 ) V + 2 W ( Q × V ) + 2 ( Q ⋅ V ) Q =[0, (W^2 - Q^2)V + 2W(Q \times V) + 2(Q \cdot V)Q = [ 0 , ( W 2 − Q 2 ) V + 2 W ( Q × V ) + 2 ( Q ⋅ V ) Q
로 정리할 수 있다.
이제, 쿼터니언 [ W , Q ] [W, Q] [ W , Q ] 를 [ w , x , y , z ] [w, x, y, z] [ w , x , y , z ] 로 두고, V V V 를 [ v x , v y , v z ] [v_x, v_y, v_z] [ v x , v y , v z ] 로 두고 회전행렬로 변환해보자.
1. ( W 2 − Q 2 ) V (W^2-Q^2)V ( W 2 − Q 2 ) V 파트
W 2 − Q 2 = ( 1 − Q 2 ) − Q 2 W^2-Q^2=(1-Q^2)-Q^2 W 2 − Q 2 = ( 1 − Q 2 ) − Q 2
= 1 − 2 Q 2 =1-2Q^2 = 1 − 2 Q 2
= 1 − 2 ( x 2 + y 2 + z 2 ) =1-2(x^2+y^2+z^2) = 1 − 2 ( x 2 + y 2 + z 2 )
= 1 − 2 x 2 − 2 y 2 − 2 z 2 =1-2x^2-2y^2-2z^2 = 1 − 2 x 2 − 2 y 2 − 2 z 2
그리고 V와의 아다마르곱을 행렬곱으로 표현해보면 다음과 같다.
( 1 − 2 x 2 − 2 y 2 − 2 z 2 0 0 0 1 − 2 x 2 − 2 y 2 − 2 z 2 0 0 1 − 2 x 2 − 2 y 2 − 2 z 2 ) V \begin{pmatrix} 1-2x^2-2y^2-2z^2 & 0 & 0 \\ 0 & 1-2x^2-2y^2-2z^2 \\ 0 & 0 & 1-2x^2-2y^2-2z^2 \\ \end{pmatrix}V ⎝ ⎜ ⎛ 1 − 2 x 2 − 2 y 2 − 2 z 2 0 0 0 1 − 2 x 2 − 2 y 2 − 2 z 2 0 0 1 − 2 x 2 − 2 y 2 − 2 z 2 ⎠ ⎟ ⎞ V
2. 2 W ( Q × V ) 2W(Q \times V) 2 W ( Q × V ) 파트
Q × V Q \times V Q × V 외적은 행렬로 다음과 같이 표현할 수 있다.
( 0 − 2 w z 2 w y 2 w z 0 − 2 w x − 2 w y 2 w x 0 ) V \begin{pmatrix} 0 & -2wz & 2wy \\ 2wz & 0 & -2wx \\ -2wy & 2wx & 0 \\ \end{pmatrix}V ⎝ ⎜ ⎛ 0 2 w z − 2 w y − 2 w z 0 2 w x 2 w y − 2 w x 0 ⎠ ⎟ ⎞ V
3. 2 ( Q ⋅ V ) Q 2(Q \cdot V) Q 2 ( Q ⋅ V ) Q 파트
Q ⋅ V Q \cdot V Q ⋅ V 를 내적한 결과에 V V V 벡터를 분리한다는 생각으로 전개
2 ( Q ⋅ V ) Q = ( ( x , y , z ) ⋅ ( v x , v y , v z ) ) Q 2(Q \cdot V)Q = ((x, y, z) \cdot (v_x, v_y, v_z))Q 2 ( Q ⋅ V ) Q = ( ( x , y , z ) ⋅ ( v x , v y , v z ) ) Q
= 2 ( x v x + y v y + z v z ) Q =2(xv_x+yv_y+zv_z)Q = 2 ( x v x + y v y + z v z ) Q
이제 Q Q Q 와의 스칼라곱을 전개하면,
( 2 x 2 v x + 2 x y v y + 2 x z v z 2 x y v x + 2 y 2 v y + 2 y z v z 2 x z v x + 2 y z v y + 2 z 2 v z ) \begin{pmatrix} 2x^2v_x + 2xyv_y + 2xzv_z \\ 2xyv_x + 2y^2v_y + 2yzv_z \\ 2xzv_x + 2yzv_y + 2z^2v_z \\ \end{pmatrix} ⎝ ⎜ ⎛ 2 x 2 v x + 2 x y v y + 2 x z v z 2 x y v x + 2 y 2 v y + 2 y z v z 2 x z v x + 2 y z v y + 2 z 2 v z ⎠ ⎟ ⎞
그리고 V [ v x , v y , v z ] V [v_x, v_y, v_z] V [ v x , v y , v z ] 를 행렬곱의 형태로 분리하면 다음과 같다.
( 2 x 2 2 x y 2 x z 2 x y 2 y 2 2 y z 2 x z 2 y z 2 z 2 ) V \begin{pmatrix} 2x^2 & 2xy & 2xz \\ 2xy & 2y^2 & 2yz \\ 2xz & 2yz & 2z^2 \\ \end{pmatrix}V ⎝ ⎜ ⎛ 2 x 2 2 x y 2 x z 2 x y 2 y 2 2 y z 2 x z 2 y z 2 z 2 ⎠ ⎟ ⎞ V
4. ( W 2 − Q 2 ) V + 2 W ( Q × V ) + 2 ( Q ⋅ V ) Q (W^2 - Q^2)V + 2W(Q \times V) + 2(Q \cdot V)Q ( W 2 − Q 2 ) V + 2 W ( Q × V ) + 2 ( Q ⋅ V ) Q 파트 합치기
이제, 최종적으로 각 파트의 행렬을 모두 더해주면 회전행렬M M M 이 완성된다.
( 1 − 2 x 2 − 2 y 2 − 2 z 2 0 0 0 1 − 2 x 2 − 2 y 2 − 2 z 2 0 0 1 − 2 x 2 − 2 y 2 − 2 z 2 ) V + ( 0 − 2 w z 2 w y 2 w z 0 − 2 w x − 2 w y 2 w x 0 ) V + ( 2 x 2 2 x y 2 x z 2 x y 2 y 2 2 y z 2 x z 2 y z 2 z 2 ) V \begin{pmatrix} 1-2x^2-2y^2-2z^2 & 0 & 0 \\ 0 & 1-2x^2-2y^2-2z^2 \\ 0 & 0 & 1-2x^2-2y^2-2z^2 \\ \end{pmatrix}V+ \begin{pmatrix} 0 & -2wz & 2wy \\ 2wz & 0 & -2wx \\ -2wy & 2wx & 0 \\ \end{pmatrix}V+ \begin{pmatrix} 2x^2 & 2xy & 2xz \\ 2xy & 2y^2 & 2yz \\ 2xz & 2yz & 2z^2 \\ \end{pmatrix}V ⎝ ⎜ ⎛ 1 − 2 x 2 − 2 y 2 − 2 z 2 0 0 0 1 − 2 x 2 − 2 y 2 − 2 z 2 0 0 1 − 2 x 2 − 2 y 2 − 2 z 2 ⎠ ⎟ ⎞ V + ⎝ ⎜ ⎛ 0 2 w z − 2 w y − 2 w z 0 2 w x 2 w y − 2 w x 0 ⎠ ⎟ ⎞ V + ⎝ ⎜ ⎛ 2 x 2 2 x y 2 x z 2 x y 2 y 2 2 y z 2 x z 2 y z 2 z 2 ⎠ ⎟ ⎞ V
= ( 1 − 2 y 2 − 2 z 2 2 ( x y − w z ) 2 ( x z + w y ) 2 ( x y + w z ) 1 − 2 x 2 − 2 z 2 2 ( y z − w x ) 2 ( x z − w y ) 2 ( y z + w x ) 1 − 2 x 2 − 2 y 2 ) V =\begin{pmatrix} 1-2y^2-2z^2 & 2(xy-wz) & 2(xz+wy) \\ 2(xy+wz) & 1-2x^2-2z^2 & 2(yz-wx) \\ 2(xz-wy) & 2(yz+wx) & 1-2x^2-2y^2 \\ \end{pmatrix}V = ⎝ ⎜ ⎛ 1 − 2 y 2 − 2 z 2 2 ( x y + w z ) 2 ( x z − w y ) 2 ( x y − w z ) 1 − 2 x 2 − 2 z 2 2 ( y z + w x ) 2 ( x z + w y ) 2 ( y z − w x ) 1 − 2 x 2 − 2 y 2 ⎠ ⎟ ⎞ V
아래는 쿼터니언을 회전행렬로 변환하는 FQuatRotationTranslationMatrix클래스의 생성자 소스코드이다. 우리가 위에서 계산한 회전행렬과 동일하게 구현됨을 확인할 수 있다.
FORCEINLINE FQuatRotationTranslationMatrix::FQuatRotationTranslationMatrix(const FQuat& Q, const FVector& Origin)
{
...
const float x2 = Q.X + Q.X; const float y2 = Q.Y + Q.Y; const float z2 = Q.Z + Q.Z;
const float xx = Q.X * x2; const float xy = Q.X * y2; const float xz = Q.X * z2;
const float yy = Q.Y * y2; const float yz = Q.Y * z2; const float zz = Q.Z * z2;
const float wx = Q.W * x2; const float wy = Q.W * y2; const float wz = Q.W * z2;
M[0][0] = 1.0f - (yy + zz); M[1][0] = xy - wz; M[2][0] = xz + wy; M[3][0] = Origin.X;
M[0][1] = xy + wz; M[1][1] = 1.0f - (xx + zz); M[2][1] = yz - wx; M[3][1] = Origin.Y;
M[0][2] = xz - wy; M[1][2] = yz + wx; M[2][2] = 1.0f - (xx + yy); M[3][2] = Origin.Z;
M[0][3] = 0.0f; M[1][3] = 0.0f; M[2][3] = 0.0f; M[3][3] = 1.0f;
}