Transfrom

Transform 컴포넌트는 3D 게임 오브젝트의 위치, 회전, 크기를 관리하며 로컬/월드 좌표계 간의 변환과 부모-자식 관계를 효과적으로 처리합니다. 쿼터니언을 사용한 회전 처리로 짐벌락 문제를 방지하고, 계층적 구조를 통해 복잡한 변환도 쉽게 구현할 수 있습니다.

Local과 World

Transform 클래스는 3D 공간에서 오브젝트의 위치, 회전, 크기를 관리하며, 두 가지 주요 좌표계를 다룹니다:

  1. 로컬 좌표계(Local Space): 부모 오브젝트 기준의 상대적 변환

  2. 월드 좌표계(World Space): 씬(Scene)의 원점 기준의 절대적 변환

Local Position, Rotation, Scale

  1. 저장 방식

private:
	Vec3 _localPosition = { 0, 0, 0 };
	Quaternion _qtLocalRotation = Quaternion::Identity;
	Vec3 _localScale = { 1,1,1 };
	Matrix _localMat = Matrix::Identity;
  1. 로컬 위치,크기 가져오기

Vec3 GetLocalPosition() { return _localPosition; }
Vec3 GetLocalScale() { return _localScale; }
  1. 로컬 위치, 크기 설정

void SetLocalPosition(Vec3 position) { _localPosition = position; UpdateTransform();}
void SetLocalScale(Vec3 scale) { _localScale = scale; UpdateTransform();}

단순히 로컬 위치, 크기 값을 설정하고 변환 행렬을 업데이트합니다.

  1. 로컬 회전 가져오기

Vec3 GetLocalRotation() { 
	Vec3 rotation = ToEulerAngles(_qtLocalRotation);
	// 라디안을 도(degree)로 변환
	return Vec3(
		XMConvertToDegrees(rotation.x),
		XMConvertToDegrees(rotation.y),
		XMConvertToDegrees(rotation.z)
	);
}
  • 내부 쿼터니언 값을 오일러각으로 변환해 반환

  • ToEulerAngles 함수로 쿼터니언 → 오일러각(라디안) 변환

  • 라디안 → 도(degree) 변환하여 반환

Vec3 Transform::ToEulerAngles(Quaternion q)
{
    Vec3 angles;

    // pitch (x-axis rotation)
    float pitch = asin(2 * (q.w * q.x - q.y * q.z));

    // yaw (y-axis rotation)
    float yaw = atan2(2 * (q.w * q.y + q.x * q.z), 1 - 2 * (q.x * q.x + q.y * q.y));

    // roll (z-axis rotation)
    float roll = atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.x * q.x + q.z * q.z));

    angles.x = pitch;
    angles.y = yaw;
    angles.z = roll;

    return angles;
}
  1. 로컬 회전 설정

void SetLocalRotation(Vec3 rotation) { 
	float pitch = XMConvertToRadians(rotation.x);
	float yaw = XMConvertToRadians(rotation.y);
	float roll = XMConvertToRadians(rotation.z);

	_qtLocalRotation = Quaternion::CreateFromYawPitchRoll(yaw, pitch, roll);
	UpdateTransform();
}

오일러각(도 단위)을 받아 내부적으로 쿼터니언으로 변환하여 저장합니다. 쿼터니언을 사용함으로써 짐벌 락 문제를 방지합니다.

void Transform::SetQTLocaslRotation(const Quaternion& rotation)
{
	_qtLocalRotation = rotation;
	UpdateTransform();
}

또는 쿼터니언을 직접 받아 설정할 수도 있습니다.

World Position, Rotation, Scale

  1. 저장 방식

private:
	Vec3 _worldPosition = { 0, 0, 0 };
	Vec3 _worldRotation = { 0, 0, 0 };
	Vec3 _worldScale = { 1, 1, 1 };
	Matrix _worldMat = Matrix::Identity;

  1. 월드 행렬 가져오기

Matrix GetWorldMatrix() { return _worldMat; }
  1. 월드 위치 가져오기

Vec3 GetWorldPosition() { return _worldPosition; }
  1. 월드 위치 설정

void Transform::SetPosition(const Vec3& position)
{
	if (HasParent())
	{
		Matrix inverseWorldMatrix = _parent->GetWorldMatrix().Invert();
		Vec3 convertedPosition = Vec3::Transform(position, inverseWorldMatrix);
		_localPosition = convertedPosition;
	}
	else
	{
		_localPosition = position;
	}
	UpdateTransform();
}

월드 좌표를 설정할 때 실제로는 해당하는 로컬 좌표를 계산하여 설정합니다:

  • 부모가 있는 경우: 부모의 월드 변환의 역행렬을 사용하여 월드 → 로컬 변환

  • 부모가 없는 경우: 월드 좌표 = 로컬 좌표

  1. 월드 회전 가져오기

Quaternion GetWorldRotation(){
	if (HasParent()) {
		return _parent->GetWorldRotation() * _qtLocalRotation;
	}
	return _qtLocalRotation;
}
  • 부모가 있을 경우 부모의 월드 회전에 로컬 회전을 결합

  • 부모가 없을 경우 로컬 회전이 곧 월드 회전

  1. 월드 회전 설정

오일러각(Euler Angles)을 직접 사용하는 대신 쿼터니언(Quaternion)을 사용하여 짐벌락 문제를 해결합니다:

void Transform::SetRotation(const Vec3& rotation)
{
	if (HasParent())
	{
		Matrix inverseWorldMatrix = _parent->GetWorldMatrix().Invert();
		Vec3 convertedRotation = Vec3::TransformNormal(rotation, inverseWorldMatrix);

		// 변환된 오일러 각도를 쿼터니언으로 변환
		float pitch = XMConvertToRadians(convertedRotation.x);
		float yaw = XMConvertToRadians(convertedRotation.y);
		float roll = XMConvertToRadians(convertedRotation.z);

		_qtLocalRotation = Quaternion::CreateFromYawPitchRoll(yaw, pitch, roll);
	}
	else
	{
		float pitch = XMConvertToRadians(rotation.x);
		float yaw = XMConvertToRadians(rotation.y);
		float roll = XMConvertToRadians(rotation.z);

		_qtLocalRotation = Quaternion::CreateFromYawPitchRoll(yaw, pitch, roll);
	}
	UpdateTransform();
}

월드 회전값을 설정할 때도 로컬 회전값으로 변환하여 저장합니다:

  • 부모가 있는 경우: 부모의 역행렬을 사용하여 월드 회전을 로컬 회전으로 변환

  • 회전값에는 TransformNormal을 사용하는데, 이는 위치 변환과 달리 방향 벡터 변환에 적합한 방식입니다.

  1. 월드 크기 가져오기

Vec3 GetWorldScale() { return _worldScale; }
  1. 월드 크기 설정

void Transform::SetScale(const Vec3& scale)
{
	if (HasParent())
	{
		Matrix inverseWorldMatrix = _parent->GetWorldMatrix().Invert();
		Vec3 convertedScale = Vec3::TransformNormal(scale, inverseWorldMatrix);
		_localScale = convertedScale;
	}
	else
	{
		_localScale = scale;
	}
    UpdateTransform();
}
  • 월드 스케일을 받아 로컬 스케일로 변환하여 저장합니다

  • 부모가 있는 경우: 부모의 역행렬로 월드 스케일을 로컬 스케일로 변환

  • 여기서도 스케일 벡터에 TransformNormal을 사용합니다

변환 행렬 계산 (UpdateTransform)

모든 변환 설정 후에는 UpdateTransform()을 호출하여 최종 변환 행렬을 계산합니다:

void Transform::UpdateTransform()
{
	// 1. 로컬 변환 계산
    Matrix scale = Matrix::CreateScale(_localScale);
    Matrix rotation = Matrix::CreateFromQuaternion(_qtLocalRotation);
    Matrix localTranslation = Matrix::CreateTranslation(_localPosition);
    Matrix localTransform = scale * rotation * localTranslation;
    _localMat = localTransform;

    // 2. 기본 월드 변환 계산
    if (HasParent())
    {
        _worldMat = _localMat * _parent->GetWorldMatrix();
    }
    else
    {
        _worldMat = _localMat;
    }


    // 3. 최종 월드 변환값 추출
    Quaternion worldQuat;
    _worldMat.Decompose(_worldScale, worldQuat, _worldPosition);
    _worldRotation = ToEulerAngles(worldQuat);

    // 자식 업데이트
    for (const shared_ptr<Transform>& child : _children)
    {
        child->UpdateTransform();
    }

    // 4. 셰이더에 전달할 상수 버퍼 업데이트
    _transformBuffer = make_shared<Buffer>();
    TransformBuffer _transformBufferData;
    _transformBufferData.worldMatrix = _worldMat;
    
    // 5. 상수 버퍼 생성 및 데이터 복사
    _transformBuffer->CreateConstantBuffer<TransformBuffer>();
    _transformBuffer->CopyData(_transformBufferData);
}

변환 과정:

  1. 로컬 변환 행렬 생성: Scale → Rotation → Translation 순서로 적용(기본 회전은 자전)

  2. 부모가 있다면 부모의 월드 행렬을 곱하여 최종 월드 변환 행렬 계산

  3. 최종 월드 행렬에서 위치, 회전, 크기 정보를 추출

  4. 모든 자식 오브젝트의 변환도 재귀적으로 업데이트

  5. 생성된 World Matrix를 상수 버퍼로 생성

회전 방향 벡터 계산

Vec3 GetLook() { return Vec3::TransformNormal(Vec3::Backward,_worldMat); }
Vec3 GetUp() { return Vec3::TransformNormal(Vec3::Up,_worldMat); }
Vec3 GetRight() { return Vec3::TransformNormal(Vec3::Right,_worldMat); }

부모-자식 관계와 변환

부모 설정

void SetParent(shared_ptr<Transform> parent) { _parent = parent; }

자식 추가

void AddChild(shared_ptr<Transform> child) {
	_children.push_back(child); 
	UpdateTransform();
}

부모로부터 분리

void DetachFromParent()
{
	if (_parent)
	{
		// 부모의 자식 Transform 목록에서 제거
		auto it = std::find(_parent->_children.begin(), _parent->_children.end(), shared_from_this());
		if (it != _parent->_children.end())
			_parent->_children.erase(it);

		// 월드 변환 유지하면서 로컬 변환 갱신
		_localPosition = GetWorldPosition();
		_qtLocalRotation = Quaternion::CreateFromRotationMatrix(GetWorldMatrix());
		_localScale = GetWorldScale();

		_parent = nullptr;
	}
	UpdateTransform();
}

공전(Revolution)

오브젝트가 특정 중심점을 기준으로 회전하는 공전 기능을 제공합니다:

void Transform::RotateAround(const Vec3& center, const Vec3& axis, float angle)
{
    // 1. 현재 월드 위치 저장
    Vec3 worldPos = GetWorldPosition();

    // 2. 중심점 기준 회전 행렬 생성
    Matrix toOrigin = Matrix::CreateTranslation(-center);
    Matrix rotation = Matrix::CreateFromAxisAngle(axis, XMConvertToRadians(angle));
    Matrix fromOrigin = Matrix::CreateTranslation(center);

    // 3. 위치에 회전 적용
    Matrix transform = toOrigin * rotation * fromOrigin;
    Vec3 newWorldPos = Vec3::Transform(worldPos, transform);

    // 4. 새로운 위치를 로컬 공간으로 변환
    if (HasParent())
    {
        Matrix parentInverse = _parent->GetWorldMatrix().Invert();
        _localPosition = Vec3::Transform(newWorldPos, parentInverse);
    }
    else
    {
        _localPosition = newWorldPos;  // 부모가 없으면 월드 위치가 곧 로컬 위치
    }

    // 5. Transform 업데이트
    UpdateTransform();
}

이 함수는 다음과 같은 과정으로 특정 중심점을 기준으로 오브젝트를 회전시킵니다:

  1. 현재 월드 위치 저장: 오브젝트의 현재 월드 위치를 가져옵니다.

  2. 중심점 기준 변환 행렬 계산:

    1. toOrigin: 중심점으로 이동하는 행렬 (중심점을 원점으로 만듦)

    2. rotation: 지정된 축을 기준으로 회전하는 행렬

    3. fromOrigin: 다시 중심점에서 원래 거리만큼 되돌아가는 행렬

  3. 위치 변환 적용: 위 세 행렬을 조합하여 새로운 월드 위치를 계산합니다.

  4. 로컬 좌표로 변환: 계산된 새 월드 위치를 로컬 좌표로 변환합니다.

    1. 부모가 있는 경우: 부모의 역행렬을 사용하여 월드 → 로컬 변환

    2. 부모가 없는 경우: 월드 위치 = 로컬 위치

  5. Transform 업데이트: 변경된 로컬 변환값으로 전체 변환 행렬을 업데이트합니다.

Last updated