주제
- 이 강의의 주제는 유니티에서 제공하는 Transform 시스템을 C++로 직접 구현하는 것이다.
- 객체의 위치(Position), 회전(Rotation), 크기(Scale) 를 SRT 행렬 기반으로 처리하고, 부모-자식 계층 구조에 따라 좌표계를 전환하며, 특히 World ↔ Local 변환, Quaternion 회전 처리, Update 재귀 호출 등의 핵심 로직을 모두 구현하는 것이 목표이다.
개념
- 게임 오브젝트(GameObject)의 위치, 회전, 크기를 담당하는 컴포넌트(Component) 이며, 3D 공간에서 객체의 위치와 방향성을 결정한다.
- 유니티처럼 계층 구조를 통해 부모의 변환이 자식에게 전달되도록 설계된다.
2. SRT란?
- Scale → Rotation → Translation 순서로 곱하는 4x4 변환 행렬 구성법이다.
- 이 SRT 행렬은 객체의 크기, 회전, 위치 정보를 포함하며, 이를 통해 좌표를 변환할 수 있다.
3. Local vs World 좌표계
- Local Transform: 부모 좌표계를 기준으로 한 상대적 위치/회전/크기
- World Transform: 전역(0,0,0)을 기준으로 계산된 절대 위치/회전/크기
- 부모가 없으면 Local = World
4. Quaternion과 짐벌락
- 오일러 회전(x, y, z축의 회전)을 그대로 쓰면 Gimbal Lock(짐벌락) 현상이 발생할 수 있음.
- 이를 방지하기 위해 Quaternion(4차원 벡터) 기반 회전 방식을 사용하고, 필요 시 다시 Euler로 변환한다.
용어정리
| 용어 | 설명 |
|---|
| SRT | Scale, Rotation, Translation 행렬 조합 |
| Quaternion | 4차원 회전 벡터로 짐벌락을 방지함 |
| Transform | 위치/회전/크기를 저장 및 계산하는 컴포넌트 |
| Local Transform | 부모를 기준으로 한 좌표계에서의 상태 |
| World Transform | 전역 좌표계 기준의 상태 |
| Parent / Child | 계층 관계에서 상위/하위 객체 |
| Matrix::Decompose | 행렬을 S, R, T로 분해하는 함수 |
| TransformNormal | 방향 벡터만 변환하는 함수 |
| TransformCoord | 위치 + 방향을 모두 변환하는 함수 |
코드 분석
class Transform : public Component
{
public:
void UpdateTransform();
Vec3 GetLocalScale();
void SetLocalScale(const Vec3& Scale);
Vec3 GetLocalRotation();
void SetLocalRotation(const Vec3& Rotation);
Vec3 GetLocalPosition();
void SetLocalPosition(const Vec3& Position);
Vec3 GetScale();
void SetScale(const Vec3& Scale);
Vec3 GetRotation();
void SetRotation(const Vec3& Rotation);
Vec3 GetPosition();
void SetPosition(const Vec3& Position);
bool HasParent();
shared_ptr<Transform> GetParent();
void SetParent(shared_ptr<Transform> Parent);
const vector<shared_ptr<Transform>>& GetChildren();
void AddChild(shared_ptr<Transform> Child);
Matrix GetWorldMatrix();
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 };
Matrix _matLocal = Matrix::Identity;
Matrix _matWorld = Matrix::Identity;
Vec3 _scale;
Vec3 _rotation;
Vec3 _position;
Vec3 _right;
Vec3 _up;
Vec3 _look;
shared_ptr<Transform> _parent;
vector<shared_ptr<Transform>> _children;
};
_matLocal: 객체 자신의 로컬 공간에서의 SRT 변환 행렬
_matWorld: 부모를 기준으로 한 월드 변환 결과
_parent, _children: 계층 구조 표현
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;
if (HasParent())
_matWorld = _matLocal * _parent->GetWorldMatrix();
else
_matWorld = _matLocal;
Quaternion quat;
_matWorld.Decompose(_scale, quat, _position);
_rotation = ToEulerAngles(quat);
_right = Vec3::TransformNormal(Vec3::Right, _matWorld);
_up = Vec3::TransformNormal(Vec3::Up, _matWorld);
_look = Vec3::TransformNormal(Vec3::Backward, _matWorld);
for (const shared_ptr<Transform>& child : _children)
child->UpdateTransform();
}
- SRT 연산 순서는
Scale → Rotation → Translation이며, 회전은 X → Y → Z 순으로 곱해도 무방하다.
Decompose()로 _scale, _position, _rotation(quaternion)을 추출하고, 쿼터니언을 오일러 각으로 변환
Vec3::TransformNormal()을 통해 방향 벡터를 월드 기준으로 계산함
- 자식은 부모의 월드 좌표에 영향을 받기 때문에, 변경 시 반드시 재귀 호출
3. Quaternion → Euler 변환 함수
Vec3 Transform::ToEulerAngles(Quaternion q)
{
Vec3 angles;
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);
angles.x = std::atan2(sinr_cosp, cosr_cosp);
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));
angles.y = 2 * std::atan2(sinp, cosp) - 3.14159f / 2;
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);
angles.z = std::atan2(siny_cosp, cosy_cosp);
return angles;
}
Quaternion은 회전을 안정적으로 표현하지만, 사람이 이해하거나 디버깅하기에는 불편하므로 오일러 각도로 변환
x, y, z 각 축별 회전을 수식으로 분리
4. SetLocal~ 함수 구현: 내부 값 수정 후 자동 갱신
void Transform::SetLocalScale(const Vec3& scale)
{
_localScale = scale;
UpdateTransform();
}
void Transform::SetLocalRotation(const Vec3& rotation)
{
_localRotation = rotation;
UpdateTransform();
}
void Transform::SetLocalPosition(const Vec3& position)
{
_localPosition = position;
UpdateTransform();
}
- Local 값이 바뀔 때마다 UpdateTransform 호출하여 내부 행렬을 자동 갱신하게 구성
- 객체 상태를 항상 최신으로 유지
5. SetWorld~ 함수 구현: 부모 기준으로 역변환
SetScale()
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);
}
}
- 부모가 있으면 자식의 로컬 스케일은 부모의 영향을 역으로 보정해서 저장
- 그렇지 않으면 그냥 worldScale을 localScale로 사용
SetPosition()
void Transform::SetPosition(const Vec3& worldPosition)
{
if (HasParent())
{
Matrix inverseParent = _parent->GetWorldMatrix().Invert();
Vec3 localPos;
localPos.Transform(worldPosition, inverseParent);
SetLocalPosition(localPos);
}
else
{
SetLocalPosition(worldPosition);
}
}
- 부모의 월드 행렬의 역행렬을 곱해 로컬 좌표 추출
- 자식 좌표는 부모의 기준으로 상대 좌표여야 하므로, world → local 변환을 거침
SetRotation()
void Transform::SetRotation(const Vec3& worldRotation)
{
if (HasParent())
{
Matrix inverseParent = _parent->GetWorldMatrix().Invert();
Vec3 localRot;
localRot.TransformNormal(worldRotation, inverseParent);
SetLocalRotation(localRot);
}
else
{
SetLocalRotation(worldRotation);
}
}
- 회전 역시 TransformNormal을 이용해 부모 기준 회전 벡터로 환산
6. 계층 관계 함수
bool Transform::HasParent()
{
return _parent != nullptr;
}
shared_ptr<Transform> Transform::GetParent()
{
return _parent;
}
void Transform::SetParent(shared_ptr<Transform> parent)
{
_parent = parent;
}
const vector<shared_ptr<Transform>>& Transform::GetChildren()
{
return _children;
}
void Transform::AddChild(shared_ptr<Transform> child)
{
_children.push_back(child);
}
- 부모와 자식 간 계층 구조를 설정하고, 부모의 변환이 자식에 반영되도록 연결
7. GameObject에 적용
shared_ptr<Transform> _transform = make_shared<Transform>();
GameObject::Update 구현
void GameObject::Update()
{
Vec3 pos = _parent->GetPosition();
pos.x += 0.001f;
_parent->SetPosition(pos);
Vec3 rot = _parent->GetRotation();
rot.z += 0.01f;
_parent->SetRotation(rot);
_transformData.World = _transform->GetWorldMatrix();
_constantBuffer->CopyData(_transformData);
}
_parent를 움직이면 _transform이 자동으로 따라 움직이는 걸 확인할 수 있음
UpdateTransform의 재귀 호출을 통해 변화가 계층 전체로 전파됨
핵심
| 항목 | 설명 |
|---|
| Transform의 역할 | 위치, 회전, 크기를 표현하고 SRT 행렬로 변환 |
| 계층 구조 | 부모가 있을 경우, 자식의 행렬은 부모의 월드 행렬을 기반으로 계산됨 |
| UpdateTransform | SRT 계산 + 부모 월드 곱셈 + 쿼터니언 분해 + 방향벡터 계산 + 자식 갱신 |
| World Set 함수 | 부모가 있을 경우, 역행렬을 통해 로컬 값으로 변환 후 저장 |
| Quaternion | 회전의 짐벌락 문제를 해결하기 위한 회전 표현 방식 |
| 방향 벡터 | 오른쪽/위쪽/정면 방향을 TransformNormal로 월드 기준으로 계산 |
| GameObject 연동 | Transform을 통해 위치, 회전, 크기 등을 자동 갱신하고 적용 가능 |