Make Unreal REAL.
article thumbnail
이득우의 게임 수학

 

사원수를 활용해 3차원 벡터를 회전시키는 기능을 구현했지만, 사원수를 구성하는 실부수와 허수부 요소 값은 직관적이지 않아 회전을 설계할 때 어려움이 따른다.

 

3차원 공간에서 물체의 회전을 설정할 때에는 사원수보다 오일러 각 방식이 직관적이고 편리하기 때문에, 오일러 각 값을 사원수로 변환해주는 기능을 만들면 게임 제작에 유용하게 활용할 수 있다.

 

오일러 각의 회전은 x, y, z 기저 축이 회전축의 역할을 수행하며, 예를 들어 3차원 공간에서 x축을 중심으로 각 θ만큼 회전시키는 데 사용하는 회전 사원수는 다음과 같다.

 

 

따라서, 오일러 각을 구성하는 Roll, Pitch, Yaw 회전에 대응하는 각 사원수는 다음과 같이 표기된다.

 

 

오일러 각의 회전 순서를 Roll - Pitch - Yaw 순으로 적용했었는데, 이에 대응하는 사원수를 동일한 순서에 맞춰 곱하면 오일러 각에 대응하는 사원수가 만들어진다.

 

 

책의 예제인 CK소프트렌더러에서 오일러 각을 입력받아 사원수를 생성하는 함수는 다음과 같다.

 

// 책의 예제인 CK소프트렌더러에서 오일러 각을 입력받아 사원수의 요소를 설정하는 함수
FORCEINLINE constexpr void Quaternion::FromRotator(const Rotator& InRotator)
{
    float sp = 0.f, sy = 0.f, sr = 0.f;
    float cp = 0.f, cy = 0.f, cr = 0.f;

    Math::GetSinCos(sp, cp, InRotator.Pitch * 0.5f);
    Math::GetSinCos(sy, cy, InRotator.Yaw * 0.5f);
    Math::GetSinCos(sr, cr, InRotator.Roll * 0.5f);

    W = sy * sp * sr + cy * cp * cr;
    X = sy * sr * cp + sp * cy * cr;
    Y = sy * cp * cr - sp * sr * cy;
    Z = -sy * sp * cr + sr * cy * cp;
}

 

이번에는 반대로 사원수로부터 오일러 각을 변환해본다.

 

실수부를 w, 각 허수부를 x, y, z로 치환한다.

 

Yaw, Roll, Pitch의 절반각을 y, r, p로 표기하고, 이들의 sin 값을 sy, sr, sp, cos 값을 cy, cr, cp로 지정한다.

 

 

사원수로부터 오일러 각을 구하려면 사원수를 잘 조합해 단일 각에 대한 삼각함수가 나오도록 식을 설정해야 한다.

 

Roll 회전에 대한 삼각함수를 만들어주는 wz + xy의 값을 계산해보자.

 

 

따라서, 2(wz + xy)의 값은 다음과 같다.

 

 

같은 방법으로 1 - 2(z² + x²)를 계산하면 다음과 같다.

 

 

이 둘을 나누면 tanθ.roll 값을 구할 수 있다.

 

 

이제 Roll 회전 값은 arctan 함수를 이용해 구할 수 있다.

 

 

이번에는 wx - yz 값을 계산해 Pitch 회전각을 구해본다.

 

 

따라서, Pitch 회전각은 arcsin 함수를 이용해 구할 수 있다.

  • 이때 주의할 점은 arcsin 함수의 정의역은 [-1, 1] 범위로 제한되어 있으므로, wx - yz 값이 [-0.5, 0.5] 범위를 벗어나지 않는지 확인하고, 벗어나는 경우에는 범위 내 가장 가까운 값으로 설정해야 한다.

 

 

마지막 남은 Yaw 회전은 2(wy + xz) 값을 계산해, Roll 회전과 유사한 방식으로 구할 수 있다.

 

 

아래는 책의 예제인 CK소프트렌더러에서 회전 사원수를 오일러 각으로 변환하는 함수이다.

 

// 책의 예제인 CK소프트렌더러에서 회전 사원수를 오일러 각으로 변환하는 함수
FORCEINLINE Rotator Quaternion::ToRotator() const
{
    Rotator result;
    
    float sinrCosp = 2 * (W * Z + X * Y);
    float cosrCosp = 1 - 2 * (Z * Z + X * X);
    result.Roll = Math::Rad2Deg(atan2f(sinrCosp, cosrCosp));

    float pitchTest = W * X - Y * Z;
    float asinThreshold = 0.4999995f;
    float sinp = 2 * pitchTest;
    if (pitchTest < -asinThreshold)
    {
        result.Pitch = -90.f;
    }
    else if (pitchTest > asinThreshold)
    {
        result.Pitch = 90.f;
    }
    else
    {
        result.Pitch = Math::Rad2Deg(asinf(sinp));
    }

    float sinyCosp = 2 * (W * Y + X * Z);
    float cosyCosp = 1.f - 2 * (X * X + Y * Y);
    result.Yaw = Math::Rad2Deg(atan2f(sinyCosp, cosyCosp));
	
    return result;
}

 

이번에는 사원수에서 회전 변환 행렬로 변환하는 식을 계산해본다.

  • 회전 변환 행렬은 로컬 축으로 구성되어 있기 때문에, 사원수를 사용해 회전된 세 로컬 축의 값을 구하면 쉽게 해결할 수 있다.

 

사원수를 구성하는 네 요소를 x, y, z, w로 하고, 허수부 벡터 r의 값을 (x, y, z)로 할 때, 사원수를 이용해 주어진 벡터 v를 회전시키는 식음 다음과 같았다.

  • t = 2(r x v)

 

 

먼저 벡터 v에 x축의 기저 벡터 (1, 0, 0)을 대입해 사원수에 의해 회전된 로컬 x축을 구해본다.

 

 

같은 방법으로 y축의 기저 벡터 (0, 1, 0), z 축의 기저 벡터 (0, 0, 1)을 대입해 회전된 로컬 축을 구하면 다음과 같다.

 

 

계산한 세 로컬 축을 행렬에 열 벡터로 꽂아 넣으면 회전 변환 행렬을 만들 수 있다.

 

 

책의 예제인 CK소프트렌더러에서 임의의 사원수가 만들어내는 세 로컬 축 벡터를 구하는 부분은 다음과 같다.

 

// 책의 예제인 CK소프트렌더러에서 기저 벡터를 회전시켜 사원수가 만들어내는 로컬 축을 구하는 함수
Vector3 GetLocalX(Quaternion& InQuaternion) { return InQuaternion * Vector3::UnitX; }
Vector3 GetLocalY(Quaternion& InQuaternion) { return InQuaternion * Vector3::UnitY; }
Vector3 GetLocalZ(Quaternion& InQuaternion) { return InQuaternion * Vector3::UnitZ; }

 

마지막으로, 회전 변환 행렬에서 사원수로 변환하는 방법을 알아본다.

 

회전 변환 행렬로부터 사원수를 구성하는 x, y, z, w 값을 개별로 구해야 하는데, 이는 정방행렬의 모든 대각 성분을 더한 트레이스(Trace)로부터 실마리를 찾을 수 있다.

 

회전 변환 행렬의 대각행렬 값을 모두 더한 트레이스 t의 값은 다음과 같다.

 

 

크기가 1인 단위 사원수인 회전 사원수의 성질에 의해 x² + y² + z² = 1 - w²이므로, 계산 결과에 1을 더하면 제곱근을 씌울 수 있는 형태가 나온다.

 

 

여기에 제곱근을 씌운 값 중 양수를 사용하면 사원수의 w 값은 다음과 같이 구할 수 있다.

  • 항상 양수 제곱근을 사용해야 한다.

 

 

여기서 구한 w값을 이용해 나머지 x, y, z 값을 구할 수 있다.

 

회전 변환 행렬을 살펴보면 대각 방향으로 대칭된 행렬의 요소를 서로 빼면, w와 나머지 사원수 요소의 곱으로 정리된다.

 

xw 값을 구하기 위한 행렬의 성분
yw 값을 구하기 위한 행렬의 성분
zw 값을 구하기 위한 행렬의 성분

 

책의 예제인 CK소프트렌더러에서 트레이스 값을 토대로 사원수의 모든 요소를 구하는 로직의 일부는 다음과 같다.

 

// 책의 예제인 CK소프트렌더러에 행렬 값으로 사원수를 구하는 기본 로직
float root = 0.f;
float trace = InMatrix[0][0] + InMatrix[1][1] + InMatrix[2][2];

// W 요소를 구하고 나머지 X, Y, Z를 계산
root = sqrtf(trace + 1.f);
W = 0.5f * root;
root = 0.5f / root;

X = (InMatrix[1][2] - InMatrix[2][1]) * root;
Y = (InMatrix[2][0] - InMatrix[0][2]) * root;
Z = (InMatrix[0][1] - InMatrix[1][0]) * root;

 

이 방식으로 사원수를 구할 때는 예외 사항이 있다.

 

트레이스 t의 값이 -1보다 작거나 같으면, t + 1의 값이 0보다 작거나 같아져 w를 구하는 데 필요한 r의 해가 존재하지 않는다.

 

예를 들어, y축으로 180도 회전하는 행렬은 x 기저와 z 기저를 반대 방향으로 돌리므로 다음과 같이 구성된다.

 

 

이 행렬을 사용하는 경우 트레이스 t의 값은 -1이 되고 w = 0이 되므로, 0의 역수는 존재하지 않기에 나머지 값들을 구할 수 없게 된다.

 

이러한 경우, w가 아닌 다른 요소부터 계산한 후에 이로부터 다른 성분을 구하도록 계산 방법을 우회해야 한다.

 

대각 성분에서 하나의 요소에서 나머지 요소들을 빼면 x, y, z 값을 구할 수 있다.

 

예외 상황에서 x 값을 구하기 위한 대각 성분
예외 상황에서 y 값을 구하기 위한 대각 성분
예외 상황에서 z 값을 구하기 위한 대각 성분

 

예외 상황을 감안해 회전 변환 행렬로부터 사원수를 구하는 방법은 켄 슈메이크(Ken Shoemake) 알고리즘이라고 한다.

 

트레이스 t의 값이 -1보다 크면 w 값 계산이 가능하지만, -1에 가까워질수록 제곱근 계산으로 인해 오차가 커지므로, 0 < t일 때만 w를 계산해 진행한다.

 

t <= 0일 때는 예외 상황으로 간주해 w를 제외하고 x, y, z 중 가장 큰 요소를 파악한다.

  • 가장 큰 요소를 찾는 이유는 제곱근을 구하는 값이 음수가 되지 않도록 하기 위함이다.

 

가장 큰 요소는 대각행렬의 요소들을 비교해 파악할 수 있다.

 

x, y의 크기를 비교하는 방법

 

이렇게 가장 큰 요소를 찾아 계산했다면, 나머지 두 요소는 대각 방향으로 대칭된 행렬의 요소를 서로 더하면 xy, yz, xz 값으로부터 얻을 수 있다.

 

xy 값을 구하기 위한 행렬의 성분
xz 값을 구하기 위한 행렬의 성분
yz 값을 구하기 위한 행렬의 성분

 

x, y, z를 모두 구했다면 마지막 w 값은, w를 이용해 x, y, z를 구했던 방법을 사용해 대각 방향으로 대칭된 행렬의 요소를 서로 빼서 구할 수 있다.

 

x가 가장 큰 요소인 경우 w를 구하는 방법

 

책의 예제인 CK소프트렌더러에 구현된 켄 슈메이크 알고리즘은 다음과 같다.

 

// 책의 예제인 CK소프트렌더러에 구현된 켄 슈메이크 알고리즘
FORCEINLINE void Quaternion::FromMatrix(const Matrix3x3& InMatrix)
{
    float root = 0.f;
    float trace = InMatrix[0][0] + InMatrix[1][1] + InMatrix[2][2];

    if (0.f < trace)
    {
        // W 요소를 구하고 나머지 X, Y, Z를 계산
        root = sqrtf(trace + 1.f);
        W = 0.5f * root;
        root = 0.5f / root;

        X = (InMatrix[1][2] - InMatrix[2][1]) * root;
        Y = (InMatrix[2][0] - InMatrix[0][2]) * root;
        Z = (InMatrix[0][1] - InMatrix[1][0]) * root;
    }
    else
    {
        BYTE i = 0;

        // X, Y, Z 중에서 가장 큰 요소를 파악
        if (InMatrix[1][1] > InMatrix[0][0]) { i = 1; }
        if (InMatrix[2][2] > InMatrix[i][i]) { i = 2; }

        // i, j, k 의 순서 지정
        static const BYTE next[3] = { 1, 2, 0 };
        BYTE j = next[i];
        BYTE k = next[j];

        // 가장 큰 요소의 값을 구하기
        root = sqrtf(InMatrix[i][i] - InMatrix[j][j] - InMatrix[k][k] + 1.f);

        float* qt[3] = { &X, &Y, &Z };
        *qt[i] = 0.5f * root;

        root = 0.5f / root;

        // 나머지 두 요소의 값을 구하기
        *qt[j] = (InMatrix[i][j] + InMatrix[j][i]) * root;
        *qt[k] = (InMatrix[i][k] + InMatrix[k][i]) * root;

        // 마지막 W 값 구하기
        W = (InMatrix[j][k] - InMatrix[k][j]) * root;
    }
}

'게임 수학 > 이득우의 게임 수학' 카테고리의 다른 글

스켈레탈 애니메이션  (0) 2023.06.09
사원수의 보간  (0) 2023.06.08
사원수의 회전  (0) 2023.06.06
사원수와 오일러 공식  (0) 2023.06.05
사원수 대수  (0) 2023.06.04
profile

Make Unreal REAL.

@diesuki4

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그