프로그래밍 공부
작성일
2023. 11. 27. 16:51
작성자
WDmil
728x90

 

캡슐형 오브젝트와 구, 캡슐, 사각형, 레이 오브젝트 끼리 충돌과정을 구현해보자.

 

CapsuleCollider는 구체형 Collider에서 선분 Vector를 추가하여 충돌확인을 한다.

CapsuleCollider는 위와같이 생겼다.

 

선분의 양 끝단에 구체가 생겨있고.

 

모든 선분이 중심점으로 이루어진. 무한히 많은 구가 선분에 끼어져 있다고 생각하면 된다.

대충 위와같이. 무한히 많은 구체가 선분을 따라 생겨있다고 이해하면된다.

 

그래서, 대부분의 충돌은 구체와 다른 오브젝트와의 충돌이라고 이해하면 된다.

 

이제 Collider의 정의에 대해 알아보자.


CapsuleCollider

class CapsuleCollider : public Collider
{
public:
	CapsuleCollider(float radius = 1.0f, float height = 2.0f, UINT stackCount = 9, UINT sliceCount = 16);
	~CapsuleCollider() = default;

	// Collider을(를) 통해 상속됨
	bool IsRayCollision(IN const Ray& ray, OUT Contact* contact) override;
	bool IsBoxCollision(BoxCollider* collider) override;
	bool IsSphereCollision(SphereCollider* collider) override;
	bool IsCapsuleCollision(CapsuleCollider* collider) override;
	

	float Radius() {
		return radius * max(GetGlobalScale().x,
			max(GetGlobalScale().y, GetGlobalScale().z));
	}

	float Height() { return height * GetGlobalScale().y; }
private:
	void MakeMesh() override;
private:
	Vector3 hitpoint;
	float distance = 0;
	float radius, height;
	UINT stackCount, sliceCount;
};

 

void CapsuleCollider::MakeMesh()
{
	float thetaStep = XM_2PI / sliceCount;
	float phiStep = XM_PI / stackCount;

	vector<Vertex>& vertices = mesh->GetVertices();
	vertices.reserve((sliceCount + 1) * (stackCount + 1));

	for (UINT i = 0; i <= stackCount; i++)
	{
		float phi = i * phiStep;

		for (UINT j = 0; j <= sliceCount; j++)
		{
			float theta = j * thetaStep;

			Vertex vertex;
			vertex.pos.x = sin(phi) * cos(theta) * radius;
			vertex.pos.y = cos(phi) * radius;
			vertex.pos.z = sin(phi) * sin(theta) * radius;

			vertex.pos = Vector3(vertex.pos) * radius;

			if (vertex.pos.y > 0.0f)
				vertex.pos.y += height * 0.5f;
			else if (vertex.pos.y < 0.0f)
				vertex.pos.y -= height * 0.5f;

			vertices.push_back(vertex);
		}
	}

	// Indices
	vector<UINT>& indices = mesh->GetIndices();

	indices.reserve(sliceCount * stackCount * 4);

	for (UINT i = 0; i < stackCount; i++) {
		for (UINT j = 0; j < sliceCount; j++) {
			indices.push_back((sliceCount + 1) * i + j);	// 0
			indices.push_back((sliceCount + 1) * (i + 1) + j);	// 1

			indices.push_back((sliceCount + 1) * i + j);	// 0
			indices.push_back((sliceCount + 1) * i + j + 1);	// 2
		}
	}
}

MakeMesh 에서 Vectices가 만들어질 떄, Pos값이 0보다 크거나 작을경우, 위로 또는 밑으로 내려버린다.

 

그렇게 하여 높이값에 따라 vertices의 값을 수정한다.

 

끌어올린다음에 선을 그으면 캡슐콜라이더가 만들어진다.


IsSphereCollision

bool CapsuleCollider::IsSphereCollision(SphereCollider* collider)
{
	Vector3 direction = GetUp();
	Vector3 pa = GetGlobalPosition() - direction * Height() * 0.5f; // 시작점
	Vector3 pb = GetGlobalPosition() + direction * Height() * 0.5f; // 끝점

	Vector3 P = collider->GetGlobalPosition();

	Vector3 pointOnLine = MATH->ClosestPointOnLine(pa, pb, P);

	float diestance = (P - pointOnLine).Length();
    return diestance <= (Radius() + collider->Radius());
}

 

구체와 구체의 충돌과정과 같은 방식으로 처리된다.

 

다른점은, 선분을 사용해서. 해당 선분과 collider의 중심점의 가장 가까운 점을 찾는방식인데,

 

시작점과 끝점을 구한다음.

 

시작점과 끝점을 

Vector3 GameMath::ClosestPointOnLine(const Vector3& start, const Vector3& end, const Vector3& point) const
{
    Vector3 line = end - start;
    Vector3 A = point - start;


    float x = Vector3::Dot(line, A);
    float y = Vector3::Dot(line, line);

    float t = Clamp(0.0f, 1.0f, x / y);
    return start + line * t;
}

위와같은 방식으로, 선분과 점 한개에 대한 가장 가까운점을 찾는다.

 

Clamp하는 이유는, start나 end점을 넘어서는 Point가 있을경우, 음수가 나타나기 때문에,

 

가장 큰 값을 벗어날 경우 1 작은값을 벗어날경우 0으로 하여.

 

가장 가까운 점이 시작점과 끝점으로 하게 해야한다.

 

아무튼, 가장 가까운 점이 확인되었으면.  가장 가까운 점과 객체간의 거리, 반지름거리를 비교해서

 

충돌했는지 아닌지를 검사하면 된다.

 


IsCapsuleCollision

캡슐과 캡슐의 거리계산은 조금 복잡하게 이루어진다.

 

선분과 선분의 충돌거리 계산이라고 생각하면 이해가 편한데,

 

선분 한개와 다른 선분 한개 간의 가장 가까운 점 을 각각 한개씩 찾고.

 

그 점을 기준으로 구체를 전개해서. 충돌계산을 한다.

 

이렇게 찾은다음에. A와 B를 기준으로 구체를 전개한다음 구체 Collision을 계산하면 된다!

 

// 두 캡슐 간의 충돌 여부를 검사하는 함수
bool CapsuleCollider::IsCapsuleCollision(CapsuleCollider* collider)
{
	// 첫 번째 캡슐의 방향 벡터와 시작점, 끝점 계산
	Vector3 aDirection = GetUp();
	Vector3 aA = GetGlobalPosition() - aDirection * Height() * 0.5f; // 시작점
	Vector3 aB = GetGlobalPosition() + aDirection * Height() * 0.5f; // 끝점

	// 두 번째 캡슐의 방향 벡터와 시작점, 끝점 계산
	Vector3 bDirection = collider->GetUp();
	Vector3 bA = collider->GetGlobalPosition() - bDirection * collider->Height() * 0.5f; // 시작점
	Vector3 bB = collider->GetGlobalPosition() + bDirection * collider->Height() * 0.5f; // 끝점

	// 각 캡슐의 네 개의 꼭짓점에 대한 벡터 계산
	Vector3 v0 = bA - aA;
	Vector3 v1 = bB - aA;
	Vector3 v2 = bA - aB;
	Vector3 v3 = bB - aB;

	// 각 꼭짓점 사이의 거리의 제곱 계산
	float d0 = Vector3::Dot(v0, v0);
	float d1 = Vector3::Dot(v1, v1);
	float d2 = Vector3::Dot(v2, v2);
	float d3 = Vector3::Dot(v3, v3);

	// 각 꼭짓점에서 가장 가까운 캡슐과의 교차점 계산
	Vector3 bestA;
	if (d2 < d0 || d2 < d1 || d3 < d0 || d3 > d1)
		bestA = aB;
	else
		bestA = aA;

	Vector3 bestB = MATH->ClosestPointOnLine(bA, bB, bestA);
	bestA = MATH->ClosestPointOnLine(aA, aB, bestB);
	bestB = MATH->ClosestPointOnLine(bA, bB, bestA);

	// 두 캡슐 사이의 최소 거리 계산
	float distance = (bestA - bestB).Length();

	// 계산된 거리가 두 캡슐의 반지름 합 이하이면 충돌이 발생한 것으로 판단
	return distance <= (Radius() + collider->Radius());
}

 

각 꼭지점별로 위치좌표를 계산해서.

 

가장 가까운 점을 투영해서 찾고.

 

해당 점을 이용해서 구체를 전개한 다음

 

그 구체 두개로 거리계산을 한다고 이해하면 편하다.

 


IsBoxCollision

Box와 CapsuleCollision은 구체와 Box의 충돌과 같다.

 

선분과 상자의 거리에 비례해 선분에서 가장 가까운 점 한개를 찾고.

 

그 점을 기준으로. 구체를 전개하고.

 

그 구체와 Box간의 충돌검사를 진행하면 된다.

 

// 주어진 상자(BoxCollider)와 캡슐 간의 충돌 여부를 검사하는 함수
bool CapsuleCollider::IsBoxCollision(BoxCollider* collider)
{
	// 주어진 상자(BoxCollider)의 정보 얻기
	BoxCollider::ObbDesc box;
	collider->GetObb(box);

	// 현재 캡슐의 방향 벡터와 시작점, 끝점 계산
	Vector3 direction = GetUp();
	Vector3 pa = GetGlobalPosition() - direction * Height() * 0.5f; // 시작점
	Vector3 pb = GetGlobalPosition() + direction * Height() * 0.5f; // 끝점

	// 캡슐 충돌선상에서 상자에 가장 가까운 점과 해당 점이 속한 축 찾기
	Vector3 closestPointToSphere = box.pos;
	Vector3 pointOnLine = MATH->ClosestPointOnLine(pa, pb, box.pos);
	Vector3 boxToSphere = pointOnLine - box.pos;

	// 각 축에 대해 가장 가까운 지점 찾기
	FOR(3)
	{
		float length = Vector3::Dot(box.axis[i], boxToSphere);
		float mult = (length < 0.0f) ? -1.0f : 1.0f;
		length = min(abs(length), box.halfSize[i]);
		closestPointToSphere += box.axis[i] * length * mult;
	}

	// 캡슐 충돌선상의 가장 가까운 점과 상자의 가장 가까운 점 사이의 거리 계산
	float distance = (pointOnLine - closestPointToSphere).Length();

	// 계산된 거리가 캡슐의 반지름 이하이면 충돌이 발생한 것으로 판단
	return distance <= Radius();
}

 

기본적으로 다른 Collision과 같이. 캡슐의 시작점 끝점을 확인한 다음. 가장 가까운점을 구하고.

 

 

해당 점을 기준으로 사각형 각 변에 대해 비율을 찾고.

 

모든 변에 대해 해당 선분에 대한 길이를 정규화 해주고.

 

정규화된 데이터를 반지름과 거리계산을하면 충돌계산을 할 수 있다.

 

 


IsRayCollision

 

Ray와 Capsul의 충돌처리는 구체와 Ray의 충돌처리와 비슷하다.

 

여기에 가장 가까운 점을 찾는 함수만 추가해주면 된다.

 

// 캡슐(CapsuleCollider)과 광선(Ray) 간의 충돌 여부를 검사하는 함수
bool CapsuleCollider::IsRayCollision(IN const Ray& ray, OUT Contact* contact)
{
	// 캡슐 충돌선의 방향 벡터와 시작점, 끝점 계산
	Vector3 direction = GetUp();
	Vector3 pa = GetGlobalPosition() - direction * Height() * 0.5f;
	Vector3 pb = GetGlobalPosition() + direction * Height() * 0.5f;

	float r = Radius(); // 캡슐의 반지름

	// 광선의 원점과 방향 벡터
	Vector3 ro = ray.pos;
	Vector3 rd = ray.dir;

	// 캡슐 충돌선 벡터 및 기타 계산
	Vector3 ba = pb - pa;
	Vector3 oa = ro - pa;

	float baba = Vector3::Dot(ba, ba);
	float bard = Vector3::Dot(ba, rd);
	float baoa = Vector3::Dot(ba, oa);
	float rdoa = Vector3::Dot(rd, oa);
	float oaoa = Vector3::Dot(oa, oa);

	float a = baba - bard * bard;
	float b = baba * rdoa - baoa * bard;
	float c = baba * oaoa - baoa * baoa - r * r * baba;

	float h = b * b - a * c;

	if (h >= 0.0f)
	{
		// 충돌이 발생한 경우
		float t = (-b - sqrt(h)) / a;

		float distance = baoa + t * bard;

		if (distance > 0.0f && distance < baba)
		{
			// 충돌이 발생하고 충돌 지점 계산
			if (contact)
			{
				contact->distance = distance;
				contact->hitPoint = ray.pos + ray.dir * t;
			}

			return true;
		}

		// 충돌이 출발점의 뒤쪽에서 발생한 경우
		Vector3 oc = (distance <= 0.0f) ? oa : ro - pb;
		b = Vector3::Dot(rd, oc);
		c = Vector3::Dot(oc, oc) - r * r;
		h = b * b - c;

		if (h > 0.0f)
		{
			// 충돌이 발생하고 충돌 지점 계산
			if (contact)
			{
				contact->distance = distance;
				contact->hitPoint = ray.pos + ray.dir * t;
			}

			return true;
		}
	}

	// 충돌이 발생하지 않은 경우
	return false;
}

 

Sphere에서  RayCollision을 했던것처럼. Ray와 Obejct가 충돌했는지 아닌지를 판별한다.

 

Ray와 Capsule간의 충돌처리과정인데

 

이차방정식 형태로 광선의 충돌, 방향, 원점, 충돌선을 계산해서. 판별식을 만든다.

 

해당 판별식을 사용해서 충돌을 했는지 아닌지 확인하고.

 

충돌했을 시. Out과 In이 두개가 나타남으로 가장 바깥쪽을 사용한다.

 

 

728x90