유니티에서 어떤 큐브를 만들고 그 산하에 캐릭터를 넣으면 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 컴포넌트가 중요하다.
유니티에 드래그 앤 드롭으로 큐브 산하에 캐릭터를 넣거나 하는 걸 모작할 수 있게 된다.
댓글