이득우의 게임 수학
사원수를 활용해 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와 나머지 사원수 요소의 곱으로 정리된다.
책의 예제인 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 값을 구할 수 있다.
예외 상황을 감안해 회전 변환 행렬로부터 사원수를 구하는 방법은 켄 슈메이크(Ken Shoemake) 알고리즘이라고 한다.
트레이스 t의 값이 -1보다 크면 w 값 계산이 가능하지만, -1에 가까워질수록 제곱근 계산으로 인해 오차가 커지므로, 0 < t일 때만 w를 계산해 진행한다.
t <= 0일 때는 예외 상황으로 간주해 w를 제외하고 x, y, z 중 가장 큰 요소를 파악한다.
- 가장 큰 요소를 찾는 이유는 제곱근을 구하는 값이 음수가 되지 않도록 하기 위함이다.
가장 큰 요소는 대각행렬의 요소들을 비교해 파악할 수 있다.
이렇게 가장 큰 요소를 찾아 계산했다면, 나머지 두 요소는 대각 방향으로 대칭된 행렬의 요소를 서로 더하면 xy, yz, xz 값으로부터 얻을 수 있다.
x, y, z를 모두 구했다면 마지막 w 값은, w를 이용해 x, y, z를 구했던 방법을 사용해 대각 방향으로 대칭된 행렬의 요소를 서로 빼서 구할 수 있다.
책의 예제인 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 |