카테고리 없음

23. 프레임워크 제작_Transform

devRiripong 2024. 1. 8.
반응형

유니티에서 어떤 큐브를 만들고 그 산하에 캐릭터를 넣으면 World 좌표에서 그 큐브를 원점으로 하는 좌표로 바뀐다. 그리고 그 큐브를 회전하거나 이동시키면 캐릭터의 좌표 또한 바뀐다. 다시 캐릭터를 큐브 산하에서 빼면 월드 좌표를 기준으로 한 좌표로 바뀐다. 이렇게 왔다 갔다 하는 것을 구현할 것이다.

 

스자이공부는

Scale, Rotation, Translation, 공전은 무시, Parent를 말한다.

 

큐브를 기준으로 하는 캐릭터의 변환행렬을 알고 싶다고 한다면?

SRT를 하면 그 변환 행렬이 만들어진다.

이 행렬을 캐릭터의 Position에 곱하면 큐브를 기준으로 한 좌표계의 좌표가 나온다.

 

그럼 큐브는 큐브의 로컬좌표에서 월드 좌표로 넘어갈 때 변형행렬은 무엇일까? 큐브의 SRT가 되는 것이다.

SRT를 하면 직속상관의 좌표로 넘어갈 수 있다.

 

그럼 캐릭터의 월드 좌표를 구하려면 어떻게 해야 할까?

스자이 하고, 부모의 행렬을 곱하면 된다.

즉 SRT를 곱할 때마다 부모로 넘어갈 수 있다는 게 된다.

 

Transform이란 걸 만들어 줄 건데, 부모가 없다면 SRT 곱하면 로컬에서 월드 변환 행렬이 된다.

계층 구조가 있을 때가 복잡한 것이다.

유니티에서 작업할 때도 좌표를 왔다 갔다 할 필요가 생길 수 있다.

이걸 만들어 보자.

 

1. Component 클래스

00.Engine 필터에 Component 필터를 만든다.

Component는 말 그대로 부품이라 GameObject에 이런저런 부품을 끼어 넣을 수 있다.

 

Coponent 필터에 Component 클래스를 생성한다.

#pragma once

class GameObject; 

class Component
{
public:
	Component(); 
	~Component(); 

	virtual void Init() abstract; 
	virtual void Update() abstract; 

	// 유니티에선 gameobject로 되어 있다. 
	shared_ptr<GameObject> GetGameObject() { return _owner.lock(); } 

protected: 
	weak_ptr<GameObject> _owner;
};

 

2. Component를 상속받은 Transform 클래스

첫 번째로 등장하는 부품은 Transform이다.

Component필터에 Transform 클래스를 추가한다.

계층 구조도 얘가 관리한다.

GameObject에서 갖고 있었던 SRT를 Transform으로 옮겨준다.

#pragma once
#include "Component.h"

class Transform : public Component
{
public:
	Transform(); 
	~Transform(); 

	virtual void Init() override; 
	virtual void Update() override;

	void UpdateTransform(); 

	// Local
	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 아직 안 만든 상태
	Vec3 GetWorldScale() { return ; }
	void SetWorldScale(const Vec3& localScale) {  }
	Vec3 GetWorldRotation() { return _localRotation; }
	void SetWorldRotation(const Vec3& localRotation) {  }
	Vec3 GetWorldPosition() { }
	void SetWorldPosition(const Vec3& localPosition) {  }

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 };

	// Cache
	Matrix _matLocal = Matrix::Identity;
	Matrix _matWorld = Matrix::Identity;

	Vec3 _scale; 
	Vec3 _rotation;  
	Vec3 _position; 

	Vec3 _right; 
	Vec3 _up; 
	Vec3 _look; 
};

 

 

1) UpdateTransform에서 World 변환행렬 구현

일단 UpdateTransform을 구현한다.

GameObject::Update의

 	Matrix matScale = Matrix::CreateScale(_localScale / 3);
	Matrix matRotation = Matrix::CreateRotationX(_localRotation.x);
	matRotation *= Matrix::CreateRotationY(_localRotation.y);
	matRotation *= Matrix::CreateRotationZ(_localRotation.z); // x,y,z 축 회전 행렬 곱하는 순서는 상관 없다. 
	Matrix matTranslation = Matrix::CreateTranslation(_localPosition);

 SRT를 구하는 이 부분을 복사해서

UpdateTransform에 넣어 놓고 시작을 한다.

 

_matLocal이란 부모님을 기준으로 하는 나의 변환 행렬이라 했을 때 어떻게 하면 될까?

Cube 산하에 있는 캐릭터의 STR를 구하면 그게 무슨 변환 행렬이었을까?

캐릭터의 로컬에서 부모인 Cube의 좌표계로 넘어가는 변환 행렬인 것이다.

그걸 지금 코드에서 _matLocal이라고 부르고 있어.

부모를 기준으로 하는 좌표계다.

 

부모가 없는 상황이라면 어떻게 되는 걸까?

로컬 자체가 월드 변환 행렬이기도 한 게 된다.

부모님이 있을 때가 문제다.

 

Transform.h에

private:
	shared_ptr<Transform> _parent; 
	vector<shared_ptr<Transform>> _children;

이게 추가하고

// 계층 관계
	bool HasParent() { return _parent != nullptr; }
	shared_ptr<Transform> GetParent() { return _parent; }

부모의 유무와, 부모의 클래스를 가져오는 함수를 정의하고,

이걸 이용해서 UpdateTransform를 구현한다.

void Transform::UpdateTransform()
{
	Matrix matScale = Matrix::CreateScale(_localScale);
	Matrix matRotation = Matrix::CreateRotationX(_localRotation.x);
	matRotation *= Matrix::CreateRotationY(_localRotation.y);
	matRotation *= Matrix::CreateRotationZ(_localRotation.z); // x,y,z 축 회전 행렬 곱하는 순서는 상관 없다. 
	Matrix matTranslation = Matrix::CreateTranslation(_localPosition);

	_matLocal = matScale * matRotation * matTranslation; // 스자이까지 한 것, 아직 부모로까진 못 간 것이다. 

        if (HasParent())
	{
		_matWorld = _matLocal * _parent->GetWorldMatrix(); 
	}
	else
	{
		_matWorld = _matLocal; // _matLocal이 _matWorld가 된다. 
	}
}

_matWorld를 구하기 위해서는 _matLocal에 _parent->GetWorldMatrix();를 곱해줘야 한다.

 

Transform.h에 GetWorldMatrix를 추가한다.

Matrix GetWorldMatrix() { return _matWorld; }

_matWorld란 어떤 물체의 월드 변환 행렬이다.

부모를 기준으로 하는 월드 매트릭스를 마지막으로 곱하는 이유는 스자이공부에서 부모님의 행렬을 곱해주는 것이다.

캐릭터에서 Cube로 넘어가는 게 SRT였고,

큐브의 SRT를 하면 더 직속상관으로 넘어가는데 그게 world 좌표계인 것으로 케어가 된다.

SRT를 중첩한 게 월드변환이라고 할 수 있다.

 

part2.

 

2) 계층 관계 설정 함수들

계층관계 나타내려면 Get뿐만 아니라 SetParent도 있을 것이다. Transform.h에 그걸 선언하고 구현한다.

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

const vector<shared_ptr<Transform>>& GetChildren() { return _children;  }
void AddChild(shared_ptr<Transform> child) { _children.push_back(child); }

 

3) _matWorld로부터 _scale, _rotation, _position, _right, _up, _look  구하기

UpdateTransform에서

_scale, _rotation, _position, _right, _up, _look까지 구해주고 싶다고 가정한다.

고치는 김에 얘네들까지 다 연산해서 들고 있겠다는 것이다.

 

월드 변환행렬 _matWorld 가 만들어져 있었다고 가정을 하면,

[01 02 03 04]

[05 06 07 08]

[09 10 11 12]

[13 14 15 16]

 

_right를 구하고 싶을 때,

캐릭터를 45도 회전을 하면, 유니티에서 보면 캐릭터의 오른쪽 빨간색이 월드 좌표계 기준으로 했을 때 무엇이 되나?

월드 변형 행렬에서 꺼내서 구할 수 있다. 애당초 SRT 등등을 이용해서 구한 행렬이기 때문이다.

바로 꺼내 쓸 때 S가 없다고 가정할 때 가능하다. S가 끼어 있다면 한번 걸러내야 한다.

 

3_1) scale, rotation, translation을 구하려 한다면, 

 

_matWorld.Decompose()라는 매트릭스를 반대로 쪼개주는 함수가 라이브러리에 들어가 있다.

 

inline bool Matrix::Decompose(Vector3& scale, Quaternion& rotation, Vector3& translation) noexcept

rotation을 Quaternion으로 받아 주고 있다는 것을 볼 수 있다.

 

왜 굳이 이런 걸까?

Vec3로 rotation을 관리하면 짐벌락 현상이 발생한다.

회전 시 x, y, z 축을 다 사용하면 Quaternion을 사용해야 한다.

x, y, z, w 4개로 관리하는 게 핵심이다.

 

예를 들어 UpdateTransform()에서

Vec3 scale; 
Quaternion rotation; 
Vec3 translation;

을 선언하고, 

 

Quaternion은 Types.h. 에 가서

using Quaternion = DirectX::SimpleMath::Quaternion;

추가해 주고,

_matWorld.Decompose(scale, rotation, translation);

UpdateTransform()에서 이렇게 해주면, _matWorld행렬을 scale, rotation, translation으로 분해해 준다.

 

Quaternion값을 이용해서 Vec3 _rotation을 구하고 싶다면? 

Quaternion quat; 
_matWorld.Decompose(_scale, quat, _position);

이렇게 변수 이름을 rotation에서 quat으로 바꿔준다.

 

오일러 방식으로 원하는 x, y, z 축 회전으로 뭔가를 만들어 주는 것도 함수에 있다. 만약 없으면 검색을 해서 구현을 하면 된다.

Quaternion to Euler로 검색을 하면, 나오는 소스코드를 왼손 좌표계로 수정해서 가져와서 사용할 수 있다.

Vec3 ToEulerAngles(Quaternion q) 
{
	Vec3 angles;

	// 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);
	angles.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));
	angles.y = 2 * std::atan2(sinp, cosp) - 3.14159f / 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);
	angles.z = std::atan2(siny_cosp, cosy_cosp);

	return angles;
}

Quaternion에서 분해해서 x, y, z 축에 해당하는 거로 만들 수 있다.

 

이걸 이용해서 Vec3 _rotation을 구하고 싶다면?

Quaternion quat; 
_matWorld.Decompose(_scale, quat, _position);

_rotation = ToEulerAngles(quat);

이런 식으로 하면 된다. 이런식으로 구하고 싶은게 있을 대 검색을 통해 알맞은 함수를 찾아서 구현을 해주면 된다.

 

유니티에서도 Rotation이 x y z로 되어 있지만 내부적으론 quaternion으로 되어 있다.

 

3_2) _right, _up. _look도 마찬가지로 구해준다. 

_right 벡터를 만약에 구하고 싶다면,

// v[x y z ?] M : ?가 무엇이냐에 따라 가지고 Translation까지 적용 시킬지 말지 판단하는데 
// 방향만 알고 싶다면 Normal 버전을 사용한다. 
// TransformCoord(Translation도 적용한 버전)
// TransformNormal
_right = Vec3::TransformNormal(Vec3::Right, _matWorld); 
// 월드 기준으로 한 오른쪽이 나온다.

 

_up, _look 도 마찬가지로 구해줄 수 있다.

_up = Vec3::TransformNormal(Vec3::Up, _matWorld);  
_look = Vec3::TransformNormal(Vec3::Backward, _matWorld); // Backward인 건 오른손 좌표계라서

이건 당장 중요한 건 아니다.

 

	if (HasParent())
	{
		_matWorld = _matLocal * _parent->GetWorldMatrix(); 
	}

지금은 이게 중요하다.

 

지금은 구해서 변수로 들고 있긴 하지만, // Cache에 변수들을 들고 있기보다는 실시간으로 필요할 때마다 구하는 게 일반적이긴 하다.

 

4) 자식에게도 영향 주게 하기 

UpdateTransform에서 뭔가 바뀌었다 하면, 자식들에게도 영향을 줘야 한다.

// Children
	for (const shared_ptr<Transform>& child : _children)
		child->UpdateTransform();

자식도 WorldMatrix를 고쳐야 하기 때문에 재귀적으로 호출해 줄 필요가 있다고 보면 된다.

 

5) SetLocal~하면, UpdateTransform 호출해서 _matWorld 업데이트하게 하기

Transform.h에서

// Local
	Vec3 GetLocalScale() { return _localScale; }
	void SetLocalScale(const Vec3& localScale) { _localScale = localScale; UpdateTransform(); }
	Vec3 GetLocalRotation() { return _localRotation; }
	void SetLocalRotation(const Vec3& localRotation) { _localRotation = localRotation; UpdateTransform(); }
	Vec3 GetLocalPosition() { return _localPosition; } 
	void SetLocalPosition(const Vec3& localPosition) { _localPosition = localPosition; UpdateTransform(); }

이렇게 SetLocal~에 UpdateTransform();을 넣어서 고치자마자 업데이트를 하게 할 수 있다.

 

6) World의 S, R, T가 변했을 때 부모가 있을 경우 부모의 S, R, T를 적용시켜 SetLoacalS, R, T 해주는 SetS, R, T 함수 정의하기

// World
	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);

world의 SRT가 변했으면 어떻게 할지 풀어야 한다. Set 함수들의 구현부를 만들어 주자.

Scale 같은 경우도 부모가 있냐 없냐에 따라 달라진다.

캐릭터를 Scale을 2, 2, 2, 큐브의 스케일을 4, 4, 4로 한 뒤 캐릭터를 큐브 산하에 넣으면 캐릭터는 0.5, 0.5, 0.5가 된다. 스케일을 4로 한다는 건 안에 있는 애들도 4,4,4로 하라는 암묵적인 규칙이 있기 때문에 안에 들어간 캐릭터가 0.5가 되는 거다. 하지만 World를 기준으로 했을 때는 2,2,2가 되는 건 변함이 없다. 이렇게 부모에 따라서 스케일도 영향을 받는 것이다.

 

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); 
	}
}

규칙은 같다 부모님의 영향을 받아 보정을 해줘야 하는 거.

 

SetPosition에서

부모가 있을 때 어떻게 구해주면 될까? 구하고 싶은 건, 캐릭터의 월드 좌표를 변경했을 때 부모를 기준으로 한 좌표이다. 

전체 월드를 기반으로 한 좌표를 변경했을 때 큐브를 기반으로 한 좌표를 구하는 것이다.

부모의 좌표를 월드 좌표로 변경하는 행렬의 역행렬을 변경한 월드 좌표에 곱하게 된다면, 부모의 좌표를 기준으로 한 좌표가 나오게 된다.  

 

void Transform::SetPosition(const Vec3& worldPosition)
{
	if (HasParent())
	{
	    Matrix worldToParentLocalMatrix = _parent->GetWorldMatrix().Invert(); // world에서 부모의 좌표계로 가는 행렬
	
		Vec3 position; 
		position.Transform(worldPosition, worldToParentLocalMatrix); 

		SetLocalPosition(position); 
	}
	else
	{
		SetLocalPosition(worldPosition); 
	}
}

이렇게 부모의 월드 행렬의 역행렬을 이용해 부모 좌표로 변환할 수 있다.

 

SetRotation도 비슷한 느낌으로 할 수 있다.

void Transform::SetRotation(const Vec3& worldRotation)
{
	if (HasParent())
	{
		Matrix inverseMatrix = _parent->GetWorldMatrix().Invert(); // world에서 부모의 좌표계로 가는 행렬

		Vec3 rotation;
		rotation.TransformNormal(worldRotation, inverseMatrix);

		SetLocalRotation(rotation);
	}
	else
	{
		SetLocalRotation(worldRotation);
	}
}

이런 느낌으로 작업을 해주면 된다. 

SRT을 하는 게 월드로 가는 게 아닌 부모를 기준으로 하는 변환행렬이라는 게 결론이다.

3. Transform 클래스 적용

pch.h에 가서

#include "Transform.h"

를 넣어준다.

그리고 GameObject.h에 가서

Vec3 _localPosition = { 0.f, 0.f, 0.f };
Vec3 _localRotation = { 0.f, 0.f, 0.f };
Vec3 _localScale = { 1.f, 1.f, 1.f };

이걸 삭제한다.

shared_ptr<Transform> _transform = make_shared<Transform>();

그리고 이걸 추가한다.

이것이 첫 컴포넌트다.

나중에는 GameObject 산하에서 관리하기 편하게 만들어 주겠지만, 지금 상황에서는 쉽게 만들기 위해 이렇게 만든다.

 

GameObject::Update를 한다 했을 때도,

void GameObject::Update()
{
	Vec3 pos = _transform->GetPosition(); 
	pos.x += 0.001f;
	_transform->SetPosition(pos);

이런 식으로 SetPosition을 호출하면 하면

SetPosition안의 SetLocalPosition으로 position이 업데이트되면서,

SetLocalPosition안의 UpdateTransform이 호출되면서,

UpdateTransform안에서 나머지 좌표들이 다 계산이 된다.

 

그러므로 GameObject::Update의

	Matrix matScale = Matrix::CreateScale(_localScale / 3);
	Matrix matRotation = Matrix::CreateRotationX(_localRotation.x);
	matRotation *= Matrix::CreateRotationY(_localRotation.y);
	matRotation *= Matrix::CreateRotationZ(_localRotation.z); // x,y,z 축 회전 행렬 곱하는 순서는 상관 없다. 
	Matrix matTranslation = Matrix::CreateTranslation(_localPosition);

	Matrix matWorld = matScale * matRotation * matTranslation; // SRT
	_transformData.matWorld = matWorld;

이 코드를 삭제하고,

_transformData.matWorld = _transform->GetWorldMatrix();

이 코드를 써준다.

그럼

void GameObject::Update()
{
	Vec3 pos = _transform->GetPosition(); 
	pos.x += 0.001f;
	_transform->SetPosition(pos); 

	_transformData.matWorld = _transform->GetWorldMatrix(); 

	_constantBuffer->CopyData(_transformData);
}

이렇게 된다.

빌드를 해보면 문제없이 되는 걸 볼 수 있다.

실행을 하면 물체가 똑같이 움직이는 것을 볼 수 있다.

 

4. 부모 변경하면 자식도 영향받는지 테스트

GameObject.h에서

shared_ptr<Transform> _transform = make_shared<Transform>(); 

shared_ptr<Transform> _parent = make_shared<Transform>();

이런 식으로 나와 부모님을 억지로 만들었다고 가정을 하고,

GameObject 생성자에서 세팅을 할 때

// TEST
	_parent->AddChild(_transform); 
	_transform->SetParent(_parent);

이렇게 서로 연결을 시켜줘 보자.

GameObject::Update에서

_transform을 지금처럼 직접 건드리는 게 아니라,

부모님을 건드린다고 가정을 해보자.

void GameObject::Update()
{
	Vec3 pos = _parent->GetPosition();
	pos.x += 0.001f;
	_parent->SetPosition(pos);

	//Vec3 pos = _transform->GetPosition(); 
	//pos.x += 0.001f;
	//_transform->SetPosition(pos); 

	_transformData.matWorld = _transform->GetWorldMatrix(); 

	_constantBuffer->CopyData(_transformData);
}

이런 식으로 _parent만 건드렸다고 하면,

_parenrt만 움직여도 _transform이 움직이는 것을 볼 수 있다.

더 나아가서,

Vec3 rot = _parent->GetRotation(); 
rot.z += 0.01f; 
_parent->SetRotation(rot);

이 코드를 추가해서 부모를 회전시킨다 하더라도,

child까지 영향을 받아서 회전하는 것을 볼 수 있다.

이런 식으로 자식, 부모와 관련된 계층관계에 있어서 영향을 주고 있고,

Transform 컴포넌트가 중요하다.

유니티에 드래그 앤 드롭으로 큐브 산하에 캐릭터를 넣거나 하는 걸 모작할 수 있게 된다.

반응형

댓글