주제

  • 이 강의의 주제는 유니티에서 제공하는 Transform 시스템을 C++로 직접 구현하는 것이다.
  • 객체의 위치(Position), 회전(Rotation), 크기(Scale) 를 SRT 행렬 기반으로 처리하고, 부모-자식 계층 구조에 따라 좌표계를 전환하며, 특히 World ↔ Local 변환, Quaternion 회전 처리, Update 재귀 호출 등의 핵심 로직을 모두 구현하는 것이 목표이다.

개념

1. Transform 컴포넌트란?

  • 게임 오브젝트(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로 변환한다.

용어정리

용어설명
SRTScale, Rotation, Translation 행렬 조합
Quaternion4차원 회전 벡터로 짐벌락을 방지함
Transform위치/회전/크기를 저장 및 계산하는 컴포넌트
Local Transform부모를 기준으로 한 좌표계에서의 상태
World Transform전역 좌표계 기준의 상태
Parent / Child계층 관계에서 상위/하위 객체
Matrix::Decompose행렬을 S, R, T로 분해하는 함수
TransformNormal방향 벡터만 변환하는 함수
TransformCoord위치 + 방향을 모두 변환하는 함수

코드 분석

1. Transform 클래스의 구조

class Transform : public Component
{
public:
    void UpdateTransform();

    // Local
    Vec3 GetLocalScale();
    void SetLocalScale(const Vec3& Scale);

    Vec3 GetLocalRotation();
    void SetLocalRotation(const Vec3& Rotation);

    Vec3 GetLocalPosition();
    void SetLocalPosition(const Vec3& Position);

    // World
    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: 계층 구조 표현

2. UpdateTransform(): SRT 계산 + 계층 전파

void Transform::UpdateTransform()
{
    // 1. SRT 행렬 구성
    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;

    // 2. 부모가 있으면 월드 행렬에 부모의 변환 곱해줌
    if (HasParent())
        _matWorld = _matLocal * _parent->GetWorldMatrix();
    else
        _matWorld = _matLocal;

    // 3. 월드 행렬 분해 (Decompose): scale, rotation(Quaternion), position
    Quaternion quat;
    _matWorld.Decompose(_scale, quat, _position);
    _rotation = ToEulerAngles(quat);

    // 4. 방향 벡터(right, up, look) 갱신
    _right = Vec3::TransformNormal(Vec3::Right, _matWorld);
    _up    = Vec3::TransformNormal(Vec3::Up, _matWorld);
    _look  = Vec3::TransformNormal(Vec3::Backward, _matWorld);

    // 5. 자식 Transform에도 재귀 호출
    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에 적용

Transform을 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 행렬로 변환
계층 구조부모가 있을 경우, 자식의 행렬은 부모의 월드 행렬을 기반으로 계산됨
UpdateTransformSRT 계산 + 부모 월드 곱셈 + 쿼터니언 분해 + 방향벡터 계산 + 자식 갱신
World Set 함수부모가 있을 경우, 역행렬을 통해 로컬 값으로 변환 후 저장
Quaternion회전의 짐벌락 문제를 해결하기 위한 회전 표현 방식
방향 벡터오른쪽/위쪽/정면 방향을 TransformNormal로 월드 기준으로 계산
GameObject 연동Transform을 통해 위치, 회전, 크기 등을 자동 갱신하고 적용 가능

profile
李家네_공부방

0개의 댓글