ViewPort
우리가 유니티를 사용하던 어느 특정 엔진을 사용하던 공통적으로 제공되는 기술이 하나 있다. 바로 화면을 클릭했을 때, 특정 물체가 픽킹되거나 좌표가 픽킹되는 것이다.
가령 우리가 롤 이라는 게임을 즐길 때도 마우스 우클릭으로 땅바닥을 클릭하면 그 좌표로 캐릭터가 움직이는 것을 볼 수 있다.
이는 쉬운 작업이 아니다.
왜냐하면 캐릭터가 존재하는 세상은 3D 세상이고 유저 입장에서는 화면이 2D이기 때문이다. 우리는 캐릭터가 카메라를 넘어 스크린화면 까지 넘어오기 위해 다음과 같은 작업을 했다.
wolrd 변환 -> camera 변환 -> projcetion 변환 -> 좌표 정규화 -> screen 변환
이런 과정을 온전히 거쳤을 때야 어떤 오브젝트가 카메라를 기준으로 그려지게 된다.
그렇다면 2D 화면인 스크린에서 바닥 좌표나 캐릭터 좌표를 얻기 위해선 어떻게 해야할까?
단순하다. 카메라를 기준으로 레이저를 쏴 부딪치는 오브젝트가 있다면 그 오브젝트의 좌표를 얻어오면 될것이다.
그러나 어려운 것은 카메라가 바라보는 방향중 "어디로" 레이저를 쏴야할까? 그 레이저를 쏘기 위한 방향벡터는 어떻게 구해야할까? 가 근본적인 고민인 것이다.
결론적으로 위의 과정을 거꾸로 다시 거슬러 올라가면 된다. Unproject를 통해 거꾸로 가는 것이다.
즉, 800 * 600 사이즈의 스크린 좌표계에서 400, 300의 좌표를 유저가 클릭했을 경우 그 좌표를 다시 역으로 정규화를 한다. 그리고 wvp의 역행렬을 곱해 world 좌표를 구해내는 것이다.
코드로 나타내면 다음과 같다.
Vec3 Viewport::Unproject(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P)
{
Vec3 p = pos;
// 정규화 하기
p.x = 2.f * (p.x - _vp.TopLeftX) / _vp.Width - 1.f; // -1 ~ 1 사이의 값으로 맵핑
p.y = -2.f * (p.y - _vp.TopLeftY) / _vp.Height + 1.f; // -1 ~ 1 사이의 값으로 맵핑
p.z = ((p.z - _vp.MinDepth) / (_vp.MaxDepth - _vp.MinDepth)); // 깊이값에 따른 설정
// WVP 역행렬
Matrix wvp = W * V * P;
Matrix wvpInv = wvp.Invert();
// 곱해서 구하기
p = Vec3::Transform(p, wvpInv);
return p;
}
게임에서는 내가 보고 있는 화면에서 3D 게임 세상으로, 3D 게임 세상에서 내가 보고 있는 화면으로의 전환이 아주 빈번하게 발생하기 때문에 잘 숙지하고 있어야만 한다.
Shpere Collider
아까 말했듯이 그러면 레이저를 쏴 물체를 판별하기 위해선 레이저와 부딪쳤는지 확인할 것이 필요하다. 그리고 그것뿐만 아니라 물체들끼리 부딪치는 상황이 있을 것이다. 즉, 충돌의 개념이 필요한 것이다.
물론 오브젝트에 이미 Vertex들이 존재하기 때문에 이를 가지고 바로 충돌을 계산할 수야 있겠지만 물체 하나마다 Vertex가 너무 많기 때문에 연산량이 정말 많아질 것이다. 그렇기 때문에 Collider를 만들어보자.
Collider는 여러개 있을 수 있으니 Base를 만들고 상속 받자. 다행히도 Ray는 DirectX에 이미 존재하기에 이는 구현하지 않는다.
#pragma once
#include "Component.h"
enum class ColliderType
{
Sphere,
AABB,
OBB,
};
class BaseCollider : public Component
{
public:
BaseCollider(ColliderType colliderType);
virtual ~BaseCollider();
virtual bool Intersects(Ray& ray, OUT float& distance) = 0;
virtual bool Intersects(shared_ptr<BaseCollider>& other) = 0;
ColliderType GetColliderType() { return _colliderType; }
protected:
ColliderType _colliderType;
};
#pragma once
#include "BaseCollider.h"
class SphereCollider : public BaseCollider
{
public:
SphereCollider();
virtual ~SphereCollider();
virtual void Update() override;
virtual bool Intersects(Ray& ray, OUT float& distance) override;
virtual bool Intersects(shared_ptr<BaseCollider>& other) override;
void SetRadius(float radius) { _radius = radius; }
BoundingSphere& GetBoundingSphere() { return _boundingSphere; }
private:
float _radius = 1.f;
BoundingSphere _boundingSphere;
};
#include "pch.h"
#include "SphereCollider.h"
#include "AABBBoxCollider.h"
#include "OBBBoxCollider.h"
SphereCollider::SphereCollider()
: BaseCollider(ColliderType::Sphere)
{
}
SphereCollider::~SphereCollider()
{
}
void SphereCollider::Update()
{
_boundingSphere.Center = GetGameObject()->GetTransform()->GetPosition();
Vec3 scale = GetGameObject()->GetTransform()->GetScale();
_boundingSphere.Radius = _radius * max(max(scale.x, scale.y), scale.z);
}
bool SphereCollider::Intersects(Ray& ray, OUT float& distance)
{
return _boundingSphere.Intersects(ray.position, ray.direction, OUT distance);
}
bool SphereCollider::Intersects(shared_ptr<BaseCollider>& other)
{
ColliderType type = other->GetColliderType();
switch (type)
{
case ColliderType::Sphere:
return _boundingSphere.Intersects(dynamic_pointer_cast<SphereCollider>(other)->GetBoundingSphere());
case ColliderType::AABB:
return _boundingSphere.Intersects(dynamic_pointer_cast<AABBBoxCollider>(other)->GetBoundingBox());
case ColliderType::OBB:
return _boundingSphere.Intersects(dynamic_pointer_cast<OBBBoxCollider>(other)->GetBoundingBox());
}
return false;
}
이제 코드상에서 마우스의 인풋을 넣어서 확인해보자.
void CollisionDemo::Update()
{
if (INPUT->GetButtonDown(KEY_TYPE::LBUTTON))
{
int32 mouseX = INPUT->GetMousePos().x;
int32 mouseY = INPUT->GetMousePos().y;
// Picking
auto pickObj = CUR_SCENE->Pick(mouseX, mouseY);
if (pickObj)
{
CUR_SCENE->Remove(pickObj);
}
}
}
Pick 함수가 매우 중요하다.
std::shared_ptr<class GameObject> Scene::Pick(int32 screenX, int32 screenY)
{
shared_ptr<Camera> cameara = GetCameara()->GetCamera();
float width = GRAPHICS->GetViewPort().GetWidth();
float height = GRAPHICS->GetViewPort().GetHeight();
Matrix projectionMatrix = cameara->GetProjectionMatrix();
float viewX = (+2.f * screenX / width - 1.0f) / projectionMatrix(0, 0);
float viewy = (-2.f * screenY / height + 1.0f) / projectionMatrix(1, 1);
Matrix viewMatrix = cameara->GetViewMatrix();
Matrix viewMatrixInv = viewMatrix.Invert();
const auto& gameObjects = GetObject();
float minDistance = FLT_MAX;
shared_ptr<GameObject> picked;
for (auto& gameObject : gameObjects)
{
if (gameObject->GetCollider() == nullptr)
continue;
// ViewSpace에서 Ray 정의
Vec4 rayOrigin = Vec4(0.f, 0.f, 0.f, 1.f);
Vec4 rayDir = Vec4(viewX, viewy, 1.0f, 0.f);
Vec3 worldRayOrigin = XMVector3TransformCoord(rayOrigin, viewMatrixInv);
Vec3 worldRayDir = XMVector3TransformNormal(rayDir, viewMatrixInv);
worldRayDir.Normalize();
// WorldSpace에서 연산
Ray ray = Ray(worldRayOrigin, worldRayDir);
float distance = 0.f;
if (gameObject->GetCollider()->Intersects(ray, OUT distance) == false)
continue;
if (distance < minDistance)
{
minDistance = distance;
picked = gameObject;
}
}
return picked;
}
클릭된 화면의 좌표를 먼저 -1~1 사이의 값, 정규화된 좌표로 변환한다. 그리고 모든 오브젝트에 대해 ray를 검사하게된다.
여기서 ray는 카메라의 중점으로 시작되고 방향은 정규화된 그 좌표이다. 그렇기 때문에 0.f,0.f,0.f,0.f 에서 시작하게 된다.
world 좌표로 변환하기 위해서는 world에서 view로 변환하는 행렬의 역행렬을 곱해서 구한다.
결국 view를 기준으로 하는 ray를 world를 기준으로하는 것으로 바꿔주고 그 방향으로 충돌을 감지해 가장 가까운 오브젝트를 pick 하게 되는 것이다.
Picking
sphere collider 말고 추가적인 collider를 만들어 볼 것인데 타입이 두가지가 있다.
그중 AABB Collider는 Collider의 축의 게임 오브젝트가 사용하는 x, y, z축과 동일 한 경우를 의미한다. 이때가 더 연산이 쉽다.
축이 틀어져 있는 경우 OBB Collider 라고 한다.
#pragma once
#include "BaseCollider.h"
class AABBBoxCollider : public BaseCollider
{
public:
AABBBoxCollider();
virtual ~AABBBoxCollider();
virtual void Update() override;
virtual bool Intersects(Ray& ray, OUT float& distance) override;
virtual bool Intersects(shared_ptr<BaseCollider>& other) override;
BoundingBox& GetBoundingBox() { return _boundingBox; }
private:
BoundingBox _boundingBox;
};
#pragma once
#include "BaseCollider.h"
class OBBBoxCollider : public BaseCollider
{
public:
OBBBoxCollider();
virtual ~OBBBoxCollider();
virtual void Update() override;
virtual bool Intersects(Ray& ray, OUT float& distance) override;
virtual bool Intersects(shared_ptr<BaseCollider>& other) override;
BoundingOrientedBox& GetBoundingBox() { return _boundingBox; }
private:
BoundingOrientedBox _boundingBox;
};
void Scene::CheckCollision()
{
vector<shared_ptr<BaseCollider>> colliders;
for (auto object : _objects)
{
if (object->GetCollider() == nullptr)
continue;
colliders.push_back(object->GetCollider());
}
for (int32 i = 0; i < colliders.size(); i++)
{
for (int j = i + 1; j < colliders.size(); j++)
{
shared_ptr<BaseCollider>& other = colliders[j];
if (colliders[i]->Intersects(other))
{
// 충돌 판정 추가
}
}
}
}
Terrain
일반적인 물체가 아니라 Terrain 같이 지형지물은 어떻게 처리할까? 일단 Terrain 대신 일반 Gird를 사용해도 지금 당장 문제는 없다. 하지만 만약 산처럼 높은 공간이 있거나 특정 부분만 텍스처가 다르거나 할 수 있기 때문에 이를 처리해 주기 위해서는 무조건 Terrain 이라는 새로운 클래스가 필요할 것이다.
#pragma once
#include "Component.h"
class Terrain : public Component
{
using Super = Component;
public:
Terrain();
~Terrain();
void Create(int32 sizeX, int32 sizeZ, shared_ptr<Material> material);
int32 GetSizeX() { return _sizeX; }
int32 GetSizeZ() { return _sizeZ; }
bool Pick(int32 screenX, int32 screenY, Vec3& pickPos, float& distance);
private:
shared_ptr<Mesh> _mesh;
int32 _sizeX = 0;
int32 _sizeZ = 0;
};
#include "pch.h"
#include "Terrain.h"
#include "MeshRenderer.h"
#include "Camera.h"
Terrain::Terrain() : Super(ComponentType::Terrain)
{
}
Terrain::~Terrain()
{
}
void Terrain::Create(int32 sizeX, int32 sizeZ, shared_ptr<Material> material)
{
_sizeX = sizeX;
_sizeZ = sizeZ;
auto go = _gameObject.lock();
go->GetOrAddTransform();
if (go->GetMeshRenderer() == nullptr)
go->AddComponent(make_shared<MeshRenderer>());
_mesh = make_shared<Mesh>();
_mesh->CreateGrid(sizeX, sizeZ);
go->GetMeshRenderer()->SetMesh(_mesh);
go->GetMeshRenderer()->SetPass(0);
go->GetMeshRenderer()->SetMaterial(material);
}
bool Terrain::Pick(int32 screenX, int32 screenY, Vec3& pickPos, float& distance)
{
Matrix W = GetTransform()->GetWorldMatrix();
Matrix V = Camera::S_MatView;
Matrix P = Camera::S_MatProjection;
Viewport& vp = GRAPHICS->GetViewport();
Vec3 n = vp.Unproject(Vec3(screenX, screenY, 0), W, V, P);
Vec3 f = vp.Unproject(Vec3(screenX, screenY, 1), W, V, P);
Vec3 start = n;
Vec3 direction = f - n;
direction.Normalize();
Ray ray = Ray(start, direction);
const auto& vertices = _mesh->GetGeometry()->GetVertices();
for (int32 z = 0; z < _sizeZ; z++)
{
for (int32 x = 0; x < _sizeX; x++)
{
uint32 index[4];
index[0] = (_sizeX + 1) * z + x;
index[1] = (_sizeX + 1) * z + x + 1;
index[2] = (_sizeX + 1) * (z + 1) + x;
index[3] = (_sizeX + 1) * (z + 1) + x + 1;
Vec3 p[4];
for (int32 i = 0; i < 4; i++)
p[i] = vertices[index[i]].position;
// [2]
// | \
// [0] - [1]
if (ray.Intersects(p[0], p[1], p[2], OUT distance))
{
pickPos = ray.position + ray.direction * distance;
return true;
}
// [2] - [3]
// \ |
// [1]
if (ray.Intersects(p[3], p[1], p[2], OUT distance))
{
pickPos = ray.position + ray.direction * distance;
return true;
}
}
}
return false;
}
'C++ > DirectX 11' 카테고리의 다른 글
[DirectX] 물방울 책 - 조명, 텍스처 (0) | 2024.10.24 |
---|---|
[DirectX] 물방울 책 - 랜더링 파이프라인, 그리기 연산 (0) | 2024.10.23 |
[DirectX] GPU - RawBuffer, System Value 분석, TextureBuffer, StructureBuffer (0) | 2024.10.15 |
[DirectX] 인스턴싱 - 드로우 콜 (1) | 2024.10.10 |
[DirectX] 애니메이션 - 모델 애니메이션 (0) | 2024.10.09 |