DirectX

79_Sphere Collider

devRiripong 2024. 3. 28.
반응형

 

ray casting을 하거라 물체들끼리 충돌을 하거나 두가지 상황이 있을건데 두 가지 상황 다 Collision 이란 개념이 들어가야 한다.

 

1. CollisionDemo 클래스 만들기 

Client/Game필터에 Week3 필터를 만든다.

ViewportDemo 클래스를 복붙해서 이름을 CollisionDemo 라고 한다.

CollisionDemo::Update의 코드를 삭제한다.

코드를 수정하고 Main에 세팅한다.

 

2. BaseCollider, SphereCollider 클래스 만들기

Engine/04. Component 필터에 Collision 필터를 만든다.

그 안에 BaseCollider 라는 클래스를 만든다. 조상님이 될 아이다.

이거를 상속해서 여러개의 충돌 박스를 만들어 줄 것이다. 구형이랑 박스형을 만들 것이다.

박스형도 obb랑 aabb 두가지 방식이 있다. obb가 상위 호환이라 하나만 만들어주지만 차이를 보기 위해 두가지를 만들 것이다.

BaseCollider를 상속받은 SphereCollider 클래스를 만든다.

BaseCollider부터 채원준다.

 

3. BaseCollider를 구현하기 위해 필요한 코드 추가하기

Types.h에 가서

using Ray = DirectX::SimpleMath::Ray;

를 추가한다.

f12를 눌러 Ray를 보면

    class Ray
    {
    public:
        Vector3 position;
        Vector3 direction;
// Ray operations
bool Intersects(const BoundingSphere& sphere, _Out_ float& Dist) const noexcept;
bool Intersects(const BoundingBox& box, _Out_ float& Dist) const noexcept;
bool Intersects(const Vector3& tri0, const Vector3& tri1, const Vector3& tri2, _Out_ float& Dist) const noexcept;
bool Intersects(const Plane& plane, _Out_ float& Dist) const noexcept;

position과 direction이 있고, 그것에 따라서 많은 함수들이 준비되어 있다는 것을 볼 수 있다.

이걸 직접 구현하려면 어렵다. 최적화가 잘 되어 있다보니 가독성은 떨어진다.

 

 

Component.h의 ComponentType에

enum class ComponentType : uint8
{
	Transform, 
	MeshRenderer, 
	Camera, 
	ModelRenderer,
	Animator, 
	Light, 
	Collider, 
	// ...
	Script, 

	End, 
};

이렇게 Collider를 추가한다.

 

 

GameObject.h에

class BaseCollider; 

전방선언을 하고

	shared_ptr<BaseCollider> GetCollider(); 

를 추가한다.

 

GameObject.cpp에

#include "BaseCollider.h"

를 추가하고,

shared_ptr<BaseCollider> GameObject::GetCollider()
{
	shared_ptr<Component> component = GetFixedComponent(ComponentType::Collider);
	return static_pointer_cast<BaseCollider>(component);
}

이렇게 구현한다.

 

4. BaseCollider 구현하기

BaseCollider로 돌아가서

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

	ColliderType GetColliderType() { return _colliderType; }

protected:
	ColliderType _colliderType; 
};
#include "pch.h"
#include "BaseCollider.h"

BaseCollider::BaseCollider(ColliderType colliderType)
	: Component(ComponentType::Collider), _colliderType(colliderType)
{

}

BaseCollider::~BaseCollider()
{

}

 

이렇게 코드를 채운다.

 

5. BaseCollider 를 상속받은 SphereCollider를 구현하기

첫번째 실험할 것은 SphereCollider다. 

구형으로 되어 있는 Collider를 만들어 주고 싶다.

구형은 원점이 있을 것이고, 원점을 기준으로 radius로 구를 그려줄텐데 그거를 실질적으로 BoundingSphere로 제공하고 있다.

BoundingSphere를 살펴보면

 struct BoundingSphere
 {
     XMFLOAT3 Center;            // Center of the sphere.
     float Radius;               // Radius of the sphere.
ContainmentType    XM_CALLCONV     Contains(_In_ FXMVECTOR Point) const noexcept;
ContainmentType    XM_CALLCONV     Contains(_In_ FXMVECTOR V0, _In_ FXMVECTOR V1, _In_ FXMVECTOR V2) const noexcept;
ContainmentType Contains(_In_ const BoundingSphere& sh) const noexcept;
ContainmentType Contains(_In_ const BoundingBox& box) const noexcept;
ContainmentType Contains(_In_ const BoundingOrientedBox& box) const noexcept;
ContainmentType Contains(_In_ const BoundingFrustum& fr) const noexcept;

bool Intersects(_In_ const BoundingSphere& sh) const noexcept;
bool Intersects(_In_ const BoundingBox& box) const noexcept;
bool Intersects(_In_ const BoundingOrientedBox& box) const noexcept;
bool Intersects(_In_ const BoundingFrustum& fr) const noexcept;

이런 기능을 사용하면 raycasting을 할 때 물체를 피격한다거나 하는 걸 쉽게 처리할 수 있다.

손수 구현하면 실수 했을 때 찾기가 어려워서 라이브러리를 사용한다.

BoundingSphere가 충돌 영역을 표현한다고 보면 된다.

 

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

	void SetRadius(float radius) { _radius = radius;  }
	BoundingSphere& GetBoundingSphere() { return _boundingSphere; } 

private: 
	float _radius = 1.f; 
	BoundingSphere _boundingSphere; 
};
#include "pch.h"
#include "SphereCollider.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); 
}

SphereCollider를 이렇게 정의 하고,

Engine을 빌드하면 빌드가 된다.

 

6. CollisionDemo에 SphereCollision 세팅하기

복기를 하면

CollisionDemo에서 Collider를 붙여주게 될 것이다. 그걸 이용해서 마우스 판정을 하는게 목표다.

CollisionDemo.cpp에

#include "SphereCollider.h"

를 추가하고,

CollisionDemo::Init()의 //mesh 부분에

		{
			auto collider = make_shared<SphereCollider>(); 
			collider->SetRadius(0.5f); 
			obj->AddComponent(collider); 
		}

이렇게 SphereCollision을 obj에 붙여주면 veigar에 충돌체라는 개념이 들어가게 된다.

그 다음에 하고 싶은건 picking test다.

CollisionDemo::Update에서

void CollisionDemo::Update()
{
	if (INPUT->GetButtonDown(KEY_TYPE::LBUTTON))
	{
		int32 mouseX = INPUT->GetMousePos().x; 
		int32 mouseY = INPUT->GetMousePos().y; 

		// Picking

	}

}

이 mouseX와 mouseY를 이용해서 피킹작업을 진행하게 된다.

 

7. Scene에서 Pick 함수 정의하기

Picking 코드를 어디에 넣을지는 고민을 해봐야한다.

CollisionManager를 파서 관리하는 경우도 있을 것이고,

모든 물체들의 존재를 Scene에서 알고 있다 보니까 Scene에서 피킹 코드를 넣어준다거나 이렇게 하는 것도 방식이 될 것이다.

 

Scene에다 넣기로 하고, Scene.h에 이어서 작업을 해본다.

	shared_ptr<class GameObject> Pick(int32 screenX, int32 screenY); 

screenX, screenY에 따라 피킹하는 코드를 넣어줄 예정이다. 

 

Scene.cpp로 가서

#include "GameObject.h"
#include "BaseCollider.h"
#include "Camera.h"

를 추가하고,

 

std::shared_ptr<class GameObject> Scene::Pick(int32 screenX, int32 screenY)
{
	shared_ptr<Camera> camera = GetCamera()->GetCamera(); // Get카메라 오브젝트->Get카메라 컴포넌트

	float width = GRAPHICS->GetViewport().GetWidth(); 
	float height = GRAPHICS->GetViewport().GetHeight(); 

	Matrix projectionMatrix = camera->GetProjectionMatrix(); 

	float viewX = (+2.0f * screenX / width - 1.0f) / projectionMatrix(0, 0); // Scrren좌표를 View 좌표로 치환, 공식은 예전 문서 참조
	float viewY = (-2.0f * screenY / height + 1.0f) / projectionMatrix(1, 1); 

	Matrix viewMatrix = camera->GetViewMatrix(); 
	Matrix viewMatrixInv = viewMatrix.Invert(); 

	const auto& gameObjects = GetObjects(); // 모든 물체들 중에서 피격이 되는게 있는지 확인 하기 위해

	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); // ViewSpace라는게 카메라의 원점을 기준으로 하는 좌표계이다 보니 0,0,0이 카메라의 위치가 된다.
		Vec4 rayDir = Vec4(viewX, viewY, 1.0f, 0.f); // 클릭했던 좌표를 이용해서 시작점과 끝점을 구해주고 있는 상황이다. 
		
		// WorldSpace에서의 Ray 정의
		// World 기준으로 되돌린다
		Vec3 worldRayOrigin = XMVector3TransformCoord(rayOrigin, viewMatrixInv);  // 위치까지
		Vec3 worldRayDir = XMVector3TransformNormal(rayDir, viewMatrixInv); // 방향만
		worldRayDir.Normalize(); 

		// WorldSpace에서 연산
		Ray ray = Ray(worldRayOrigin, worldRayDir); 
		// World에서 했지만, View 스페이스에서 해도 말이 된다. 기준이 되는 세상만 고정을 해서 만들어 주면 된다.
		
		float distance = 0.f; 
		if (gameObject->GetCollider()->Intersects(ray, OUT distance) == false)
			continue; 

		if (distance < minDistance)
		{
			minDistance = distance; 
			picked = gameObject; 
		}
	}

	return picked; 
}

이게 첫번쨰 방법이었고, 여기서 만든 방법은 Camera의 위치에서 클릭한 좌표를 다시 View space 좌표로 바꾼 다음에 Camera의 원점에서 방금 했던 위치까지 레이저를 쏴가지고 피격하는 그 부분을 만들어 본 것이다.

지난 번에 만들었던 Viewport라는 projection을 이용해서 만들 수도 있다.

여러가지 방식으로 만들 수 있다.

중요한 건 레이저를 만들어서 world space로 만들어준 다음에 world space끼리 연산을 하고 있는 것이다.

이렇게 피격 판정 에서 true가 뜨면 된다.

여기까지 기본적인 코드라고 볼 수 있다.

 

Engine을 빌드한다.

 

8. CollisionDemo::Update에서 Scene의 Pick을 사용하 테스트하기

이 Picking 코드가 실행이 되면서 결국 우리가 작업하던 CollisionDemo로 돌아가서

Collision::Update의 //Picking 부분을 넣어주면 된다.

CollisionDemo.cpp에

#include "Scene.h"

을 넣어주고,

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

이렇게 picking 되면 제거되게 코드를 만들어 준 뒤 실행을 하면

아무대나 클릭해도 작동을 하는데

Camera::UpdateMatrix에서

void Camera::UpdateMatrix()
{
	Vec3 eyePosition = GetTransform()->GetPosition();
	Vec3 focusPosition = eyePosition + GetTransform()->GetLook();
	Vec3 upDirection = GetTransform()->GetUp();

	_matView = S_MatView = ::XMMatrixLookAtLH(eyePosition, focusPosition, upDirection);
	_matProjection = S_MatProjection = ::XMMatrixPerspectiveFovLH(_fov, _width / _height, _near, _far);
}

이렇게 _matView와 _matProjection을 Update하면

Sphere를 클릭한 경우에만 Sphere가 사라지고 주위를 클릭하면 사라지지 않는다.

 

 

 

반응형

'DirectX' 카테고리의 다른 글

81_Terrain Picking  (0) 2024.03.29
80_Picking  (0) 2024.03.28
78_Viewport  (0) 2024.03.27
77_RenderManager 구조정리  (0) 2024.03.24
76_StructureBuffer  (0) 2024.03.23

댓글