이득우의 게임 수학
3차원 공간에서의 회전 변환에 대해 생각해보자.
회전 변환은 물체의 형태를 보존해주는 강체 변환의 성질을 갖는다.
- 따라서, 세 표준 기저 벡터는 동일한 크기와 직교성을 유지한 상태로 함께 움직여야 한다.
회전 변환으로 달라진 세 표준 기저 벡터를 알 수 있다면, 이를 열벡터로 만들어 회전 변환 행렬 R을 만들 수 있다.
하지만 이러한 방법은 매번 3개의 변화된 표준 기저 벡터 값을 계산해야 하기 때문에 비효율적이다.
따라서, 3차원 공간에서 회전을 지정할 때는 일반적으로 회전하는 중심축과 각으로 표현된 회전량을 지정하는 방식을 사용한다.
- 2차원 공간의 극좌표계 방식과 유사하다.
하나의 평면만 존재하는 2차원과 달리 3차원은 무수히 많은 평면으로 구성되기 때문에, 새로운 오일러 각 방식이 필요하다.
오일러 각은 3차원 공간에서 물체가 놓인 방향을 3개의 각을 사용해 표시하는 방법이다.
표준 기저 벡터를 중심으로 회전하는 각의 크기로 지정된다.
그런데 프로그램마다 x, y, z축의 용도가 다르기 때문에, 축 대신 회전의 움직임으로 회전 동작을 구분해 각을 지정하는 Pitch, Roll, Yaw 방식을 사용한다.
- 이 방식을 사용하면 서로 다른 프로그램 간에도 데이터를 쉽게 변환할 수 있다.
책의 예제인 CK소프트렌더러에는 Rotator 구조체를 통해 오릴러 각을 관리한다.
// 책의 예제인 CK소프트렌더러에 정의된 오일러 각
struct Rotator
{
public:
FORCEINLINE void Clamp()
{
Yaw = GetAxisClampedValue(Yaw);
Roll = GetAxisClampedValue(Roll);
Pitch = GetAxisClampedValue(Pitch);
}
// 각의 범위를 [0°, 360°]로 제한한다.
FORCEINLINE float GetAxisClampedValue(float InRotatorValue)
{
float angle = Math::FMod(InRotatorValue, 360.f);
if (angle < 0.f)
{
angle += 360.f;
}
return angle;
}
public:
float Yaw = 0.f;
float Roll = 0.f;
float Pitch = 0.f;
};
오일러 각은 표준 기저 벡터를 중심으로 진행되는 세 번의 연속적인 회전을 뜻한다.
x축 회전은 yz 평면의 회전을 의미하는데, 이 때 x값은 변하지 않고 오직 y, z축의 값만 변한다.
이때 y축 회전만 sinα의 부호가 반대인 이유는, 3차원 공간에서는 x - y - z - x - y 순으로 순환되기 때문에 y축의 직교 평면은 xz 평면이 아니라 zx 평면이다.
- x축의 직교 평면: yz평면
- y축의 직교 평면: zx평면
- z축의 직교 평면: xy평면
각 기저 축의 회전 행렬을 구했으니 이제 순서를 적용해 최종 회전 행렬을 만들어야 한다.
- 책의 예제인 CK소프트렌더러에서는 언리얼 엔진의 오일러 각과 동일한 Roll - Pitch - Yaw 순서를 사용한다.
이렇게 계산된 회전 행렬 R의 열벡터는 표준 기저 벡터 xₑ, yₑ, zₑ가 회전 변환된 로컬 축 xₑ', yₑ', zₑ'을 의미한다.
책의 예제인 CK소프트렌더러에서는 위 식을 사용해 오일러 각으로부터 세 개의 로컬 축을 계산한다.
// 책의 예제인 CK소프트렌더러에서 오일러 각으로부터 로컬 축을 계산하는 함수
FORCEINLINE void GetLocalAxes(Vector3& OutRight, Vector3& OutUp, Vector3& OutForward)
{
float cy = 0.f, sy = 0.f, cp = 0.f, sp = 0.f, cr = 0.f, sr = 0.f;
Math::GetSinCos(sy, cy, Yaw);
Math::GetSinCos(sp, cp, Pitch);
Math::GetSinCos(sr, cr, Roll);
OutRight = Vector3(cy * cr + sy * sp * sr, cp * sr, -sy * cr + cy * sp * sr);
OutUp = Vector3(-cy * sr + sy * sp * cr, cp * cr, sy * sr + cy * sp * cr);
OutForward = Vector3(sy * cp, -sp, cy * cp);
}
이렇게 얻은 로컬 축 벨터를 열벡터로 만든 회전 행렬 R은 다음과 같다.
책의 예제인 CK소프트렌더러에서 매 프레임마다 오일러 각으로부터 로컬 축 정보를 갱신하는 부분이다.
// 책의 예제인 CK소프트렌더러에서 매 프레임마다 오일러 각에 대응하는 로컬 축을 갱신하는 부분
class TransformComponent
{
public:
void AddYawRotation(float InDegree) { _Rotation.Yaw += InDegree; Update(); }
void AddRollRotation(float InDegree) { _Rotation.Roll += InDegree; Update(); }
void AddPitchRotation(float InDegree) { _Rotation.Pitch += InDegree; Update(); }
void SetRotation(const Rotator & InRotation) { _Rotation = InRotation; Update(); }
const Vector3& GetLocalX() const { return _Right; }
const Vector3& GetLocalY() const { return _Up; }
const Vector3& GetLocalZ() const { return _Forward; }
void SetLocalAxes(const Vector3 & InRight, const Vector3 & InUp, const Vector3 & InForward)
{
_Right = InRight;
_Up = InUp;
_Forward = InForward;
}
private:
FORCEINLINE void Update()
{
_Rotation.Clamp();
_Rotation.GetLocalAxes(_Right, _Up, _Forward);
}
Vector3 _Position = Vector3::Zero;
Rotator _Rotation;
Vector3 _Scale = Vector3::One;
Vector3 _Right = Vector3::UnitX;
Vector3 _Up = Vector3::UnitY;
Vector3 _Forward = Vector3::UnitZ;
};
아래 변환 행렬들을 크기 - 회전 - 이동 순으로 곱해 만든 모델링 행렬 M은 다음과 같다.
책의 예제인 CK소프트렌더러에서 변환 행렬들의 값을 통해 모델링 행렬을 생성하는 부분이다.
// 책의 예제인 CK소프트렌더러에서 모델링 행렬의 생성
FORCEINLINE Matrix4x4 GetModelingMatrix() const;
{
return Matrix4x4(
Vector4(_Right * _Scale.X, false),
Vector4(_Up * _Scale.Y, false),
Vector4(_Forward * _Scale.Z, false),
Vector4(_Position, true)
);
}
'게임 수학 > 이득우의 게임 수학' 카테고리의 다른 글
짐벌 락(Gimbal Lock) (0) | 2023.05.07 |
---|---|
카메라 공간 (0) | 2023.05.06 |
3차원 공간의 설계 (0) | 2023.05.04 |
카메라 시스템 (0) | 2023.05.03 |
렌더링 파이프라인 (0) | 2023.05.02 |