DirectX 11 스터디 - GameObject 의 첫 컴포넌트 Transform
GameObject 의 Scale, Rotation, Translation 을 처리할 컴포넌트를 생성해서 코드를 모듈화 해본다. Component 클래스를 만들고, 이 클래스를 상속받아 Transform 컴포넌트를 생성해줌. 유니티의 그것과 동일한 네이밍. 동일한 기능을 만들어본다.
Transform 클래스를 만들고, GameObject 가 들고있던 _localScale, _localRotation, _localPosition 을 옮겨줌. 관련 Get/Set 메소드들도 모두 만들어준다. 로컬좌표계의 SRT 변환을 처리해줄 행렬 Matrix _matLocal 을 생성해주고, 이 행렬을 활용해서 월드좌표를 계산할 메소드 UpdateTransform() 함수를 만들어준다.
#include "Component.h"class Transform : public Component
{
using Super = Component;
public:
Transform() {}
virtual ~Transform() {}
virtual void Init() override;
virtual void Update() override;
void UpdateTransform();
Vec3 GetLocalScale() { return _localScale; }
void SetLocalScale(const Vec3& localScale) { _localScale = localScale; }
Vec3 GetLocalRotation() { return _localRotation; }
void SetLocalRotation(const Vec3& localRotation) { _localRotation = localRotation; }
Vec3 GetLocalPosition() { return _localPosition; }
void SetLocalPosition(const Vec3& localPosition) { _localPosition = localPosition; }
// World SRT 벡터 관련 메소드를 만들어야 함.
private:
Vec3 _localScale = { 1.f, 1.f, 1.f };
Vec3 _localRotation = { 0.f, 0.f, 0.f };
Vec3 _localPosition = { 0.f, 0.f, 0.f };
// 로컬 SRT 변환 행렬.
Matrix _matLocal = Matrix::Identity;
Matrix _matWorld = Matrix::Identity;
Vec3 _scale;
Vec3 _rotation;
Vec3 _position;
Vec3 _right;
Vec3 _up;
Vec3 _look;
};
이 GameObject, Transform 에서 월드좌표를 구하려면 부모의 Transform 을 참조해야 함. 부모의 SRT 를 알아와서 곱해야 월드좌표를 알 수 있기 때문에.
shared_ptr<Transform> _parent; // 부모
vector<shared_ptr<Transform>> _children; // 자식들
UpdateTransform() 함수는 GameObject 하위의 Update() 함수 내용을 옮긴 뒤 World Transform 이 적용되도록 적절히 수정. 만약 이 Transform 이 항상 월드에 있다면, _matWorld = _matLocal 이 됨. 만약 부모 Transform 이 존재한다면 부모의 WorldMatrix를 곱해주는 추가 연산이 필요.
void Transform::UpdateTransform()
{
Matrix matScale = Matrix::CreateScale(_localScale);
Matrix matRotation = Matrix::CreateRotationX(_localRotation.x);
matRotation *= Matrix::CreateRotationY(_localRotation.y);
matRotation *= Matrix::CreateRotationZ(_localRotation.z);
Matrix matTranslation = Matrix::CreateTranslation(_localPosition);
_matLocal = matScale * matRotation * matTranslation; // SRT
if (!HasParent())
{
_matWorld = _matLocal;
}
else
{
_matWorld = _matLocal * _parent->GetWorldMatrix();
}
}
다음은 현재 Transform 의 Scale, Rotion, Translation 벡터를 각각 Local Matrix 로부터 구해보기. SimpleMath의 Matrix::Decompose() 함수로 구할 수 있는데, Rotation은 Euler 형식의 벡터가 아닌 Quaternion 사원수로만 받을 수 있음. 사원수 Quaternion 은 struct Quaternion : public XMFLOAT4 타입.
Quaternion 으로 받은 Rotation 은 인터넷 검색으로 Euler 각으로 변환하는 함수를 통해 Vec3 Rotation 으로 변환. 아래 QuaternionToEulerAngles 함수는 아래에서 긁어옴.
// https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
{
//...
Quaternion quaternion;
_matWorld.Decompose(_scale, quaternion, _position); // SRT 요소 획득.
// Quaternion 을 Euler 로 변환
_rotation = QuaternionToEulerAngles(quaternion);
}
Vec3 QuaternionToEulerAngles(Quaternion q) {
const double M_PI = 3.14159f;
Vec3 retAngles;
// roll (x-axis rotation)
double sinr_cosp = 2 * (q.w * q.x + q.y * q.z);
double cosr_cosp = 1 - 2 * (q.x * q.x + q.y * q.y);
retAngles.x = std::atan2(sinr_cosp, cosr_cosp);
// pitch (y-axis rotation)
double sinp = std::sqrt(1 + 2 * (q.w * q.y - q.x * q.z));
double cosp = std::sqrt(1 - 2 * (q.w * q.y - q.x * q.z));
retAngles.y = 2 * std::atan2(sinp, cosp) - M_PI / 2;
// yaw (z-axis rotation)
double siny_cosp = 2 * (q.w * q.z + q.x * q.y);
double cosy_cosp = 1 - 2 * (q.y * q.y + q.z * q.z);
retAngles.z = std::atan2(siny_cosp, cosy_cosp);
return retAngles;
}
다음은 right, up, look 벡터를 구해준다.
유의해야할 점은. SimpleMath::Vector3::Transform 함수. 위치값 적용은 필요 없으니 TransformNormal 을 사용하면 되고,
_look 벡터를 구할 때, SimpleMath 가 오른손 좌표계 기준이기 때문에 Forward 가 0,0,-1 이라서 _look 벡터는 Backward 를 기준으로 구해줘야 함.
_right = Vec3::TransformNormal(Vec3::Right, _matWorld);
_up = Vec3::TransformNormal(Vec3::Up, _matWorld);
_look = Vec3::TransformNormal(Vec3::Backward, _matWorld);
마지막으로 자식 Transform 들도 모두 업데이트 호출.
for (const shared_ptr<Transform>& child : _children)
{
child->UpdateTransform();
}
이렇게 작성한 UpdateTransform() 함수는 Transform 이 변경될 때마다 한 번씩 호출해줘서 부모 자식간의 계층구조에 필요한 값들을 모두 갱신 처리 해줘야 함.
이제 Transform 에서 world의 Scale, Roation, Position을 Get/Set 하는 메소드가 필요. Get 메소드들은 위에서 구한 _scale, _rotation, _position 을 그냥 뱉어주면 되는데 Set~ 메소드들은 내부에서 부모와의 관계를 고려해서 처리 해줘야 함.
Vec3 GetScale() { return _scale; }
void SetScale(const Vec3& scale);
Vec3 GetRotation() { return _rotation; }
void SetRotation(const Vec3& rotation);
Vec3 GetPosition() { return _position; }
void SetPosition(const Vec3& position);
SetScale, SetRotation, SetPosition 은 parent 가 있을 경우 parent 의 Scale 값이나 Matrix 의 역을 취해서 연산을 해줘야 함.
void Transform::SetScale(const Vec3& worldScale)
{
if (HasParent())
{
Vec3 parentScale = _parent->GetScale();
Vec3 scale = worldScale;
scale.x /= parentScale.x;
scale.y /= parentScale.y;
scale.z /= parentScale.z;
SetLocalScale(scale);
}
else
{
SetLocalScale(worldScale);
}
}
void Transform::SetRotation(const Vec3& worldRotation)
{
if (HasParent())
{
Matrix matParentInverse = _parent->GetWorldMatrix().Invert();
Vec3 rotation;
rotation = Vec3::TransformNormal(worldRotation, matParentInverse);
SetLocalRotation(rotation);
}
else
{
SetLocalRotation(worldRotation);
}
}
void Transform::SetPosition(const Vec3& worldPosition)
{
if (HasParent())
{
Matrix matParentInverse = _parent->GetWorldMatrix().Invert();
Vec3 position;
position = Vec3::Transform(worldPosition, matParentInverse);
SetLocalPosition(position);
}
else
{
SetLocalPosition(worldPosition);
}
}
Transform 컴포넌트는 여기까지.
<리얼-타임 렌더링(REAL-TIME RENDERING) 4/e> 구입 링크
https://link.coupang.com/a/8VWas
<DirectX 12를 이용한 3D 게임 프로그래밍 입문> 구입 링크
https://link.coupang.com/a/8V4Wq
<이득우의 게임 수학:39가지 예제로 배운다! 메타버스를 구성하는 게임 수학의 모든 것> 구입 링크
https://link.coupang.com/a/9BqLd
유니티 에셋 스토어 링크
https://assetstore.unity.com?aid=1011lvz7h
(링크를 통해 도서/에셋 구입시 일정액의 수수료를 지급받습니다.)