강의

https://inf.run/Ju6ZN

 

학습 페이지

 

www.inflearn.com

 

1.Intersection

물체들끼리의 충돌했을 때 상호작용을 해주기 위해서는 우선 충돌을 탐지해야한다. 탐지하는 것을 Intersection이라고 한다.

 

구와 구

만약 교점이 있는지 확인하려면 각 원의 원점간의 거리를 계산했을 그 크기가 각 반지름의 합과 같거나 작다면 붙어있거나 

구가 충돌이 있거나 겹쳐져있는 것이다. 이때 계산의 

bool MathUtils::SphereSphere(const Sphere3D& s1, const Sphere3D& s2)
{
    float sum = s1.radius + s2.radius;
    //제곱으로 비교
    float sqDistance = (s1.position - s2.position).LengthSquared();
    return sqDistance <= sum * sum;
}

 

구와 AABB

구와 AABB는 원과 원을 비교할 때와 비슷한 느낌으로 원과 AABB의 최근접점까지의 거리와 반지름을 비교해서 최근접점까지의 거리가 더 짧으면 충돌이 있다는 것을 알 수 있다.

bool MathUtils::SphereAABB(const Sphere3D& sphere, const AABB3D& aabb)
{
    Point3D closestPoint = ClossetPoint(aabb, sphere.position);
    float distSq = (sphere.position - closestPoint).LengthSquared();
    float radiusSq = sphere.radius * sphere.radius;
    return distsq < radiusSq;
}

 

구와 OBB

구와 obb는 AABB와 같이 최근접점과의 거리와 반지름을 비교해주면 된다.

bool MathUtils::SphereOBB(const Sphere3D& sphere, const OBB3D& obb)
{

    Point3D closestPoint = ClossetPoint(obb, sphere.position);
    float distSq = (sphere.position - closestPoint).LengthSquared();
    float radiusSq = sphere.radius * sphere.radius;
    return distsq < radiusSq;
}

 

구와Plane

구와 Plane도 동일하게 최근접점과의거리와 반지름을 비교해주면 된다.

// 구와 평면의 충돌 검사
bool MathUtils::SphereToPlane(const Sphere3D& sphere, const Plane3D& plane)
{
    Point3D closestPoint = ClosestPoint(plane, sphere.position); 
    float distSq = (sphere.position - closestPoint).LengthSquared(); 
    float radiusSq = sphere.radius * sphere.radius; 
    return distSq < radiusSq; 
}

AABB와 AABB

각 큐브의 min max를 통해 비교해준다. 사이에 있는 값이 있는지 검사해준다.

// AABB와 AABB의 충돌 검사
bool MathUtils::AABBToAABB(const AABB3D& aabb1, const AABB3D& aabb2)
{
    Point3D aMin = AABB3D::GetMin(aabb1); // aabb1의 최소 좌표
    Point3D aMax = AABB3D::GetMax(aabb1); // aabb1의 최대 좌표
    Point3D bMin = AABB3D::GetMin(aabb2); // aabb2의 최소 좌표
    Point3D bMax = AABB3D::GetMax(aabb2); // aabb2의 최대 좌표

    return (aMin.x <= bMax.x && aMax.x >= bMin.x) &&
        (aMin.y <= bMax.y && aMax.y >= bMin.y) &&
        (aMin.z <= bMax.z && aMax.z >= bMin.z); // 모든 축에 대해 겹치면 true
}

AABB와 OBB

각 선분에 대해 기준이 되는 축을 하나 정하고 프로젝션(SAT알고리)을 모든 선분에 해줬을 때 선분이 모든 축에서 겹치는 부분이 있다면 충돌이 있는 것이다. 이때 정확한 판별을 위하여 외적한 축을 바탕으로도 이 연산을 해주어야 한다.

// AABB의 주어진 축에 대한 구간을 계산
Interval3D MathUtils::GetInterval(const AABB3D& aabb, const Vec3& axis)
{
    Vec3 i = AABB3D::GetMin(aabb); // 최소 좌표
    Vec3 a = AABB3D::GetMax(aabb); // 최대 좌표

    Vec3 vertex[8] =
    {
        Vec3(i.x, a.y, a.z), Vec3(i.x, a.y, i.z), Vec3(i.x, i.y, a.z), Vec3(i.x, i.y, i.z),
        Vec3(a.x, a.y, a.z), Vec3(a.x, a.y, i.z), Vec3(a.x, i.y, a.z), Vec3(a.x, i.y, i.z),
    };

    Interval3D result; // 구간 초기화
    result.min = result.max = axis.Dot(vertex[0]); // 첫 정점을 기준으로 초기화

    for (int i = 1; i < 8; ++i)
    {
        float projection = axis.Dot(vertex[i]);
        result.min = min(result.min, projection); // 최소값 갱신
        result.max = max(result.max, projection); // 최대값 갱신
    }
    return result; // 계산된 구간 반환
}

// OBB와 주어진 축에 대한 구간(Interval) 계산
Interval3D MathUtils::GetInterval(const OBB3D& obb, const Vec3& axis)
{
    Vec3 vertex[8]; // OBB의 꼭짓점을 저장할 배열

    Vec3 C = obb.position; // OBB의 중심 위치
    Vec3 E = obb.size; // OBB의 크기(각 축에 대한 반 길이)

    vector<Vec3> A; // OBB의 축
    A.push_back(obb.orientation.Right()); // OBB의 오른쪽 방향 축
    A.push_back(obb.orientation.Up()); // OBB의 위쪽 방향 축
    A.push_back(obb.orientation.Backward()); // OBB의 뒤쪽 방향 축

    // OBB의 8개 꼭짓점 계산
    vertex[0] = C + A[0] * E.x + A[1] * E.y + A[2] * E.z;
    vertex[1] = C - A[0] * E.x + A[1] * E.y + A[2] * E.z;
    vertex[2] = C + A[0] * E.x - A[1] * E.y + A[2] * E.z;
    vertex[3] = C + A[0] * E.x + A[1] * E.y - A[2] * E.z;
    vertex[4] = C - A[0] * E.x - A[1] * E.y - A[2] * E.z;
    vertex[5] = C + A[0] * E.x - A[1] * E.y - A[2] * E.z;
    vertex[6] = C - A[0] * E.x + A[1] * E.y - A[2] * E.z;
    vertex[7] = C - A[0] * E.x - A[1] * E.y + A[2] * E.z;

    // 주어진 축에 대해 OBB의 꼭짓점들을 투영하여 최소/최대 값 계산
    Interval3D result;
    result.min = result.max = axis.Dot(vertex[0]); // 첫 꼭짓점으로 초기화

    for (int i = 1; i < 8; ++i)
    {
        float projection = axis.Dot(vertex[i]); // 꼭짓점을 축에 투영
        result.min = min(result.min, projection); // 최소값 갱신
        result.max = max(result.max, projection); // 최대값 갱신
    }
    return result; // 계산된 구간 반환
}

// AABB와 OBB가 주어진 축에 대해 겹치는지 검사
bool MathUtils::OverlapOnAxis(const AABB3D& aabb, const OBB3D& obb, const Vec3& axis)
{
    Interval3D a = GetInterval(aabb, axis); // AABB의 구간 계산
    Interval3D b = GetInterval(obb, axis); // OBB의 구간 계산
    return ((b.min <= a.max) && (a.min <= b.max)); // 구간이 겹치면 true 반환
}

// AABB와 OBB의 충돌 검사
bool MathUtils::AABBToOBB(const AABB3D& aabb, const OBB3D& obb)
{
    Vec3 test[15] = // 충돌 검사에 사용될 축
    {
        Vec3(1,0,0), // AABB 축 1
        Vec3(0,1,0), // AABB 축 2
        Vec3(0,0,1), // AABB 축 3
        obb.orientation.Right(), // OBB 축 1
        obb.orientation.Up(), // OBB 축 2
        obb.orientation.Backward(), // OBB 축 3
        // 외적으로 생성된 추가 축
    };

    // 추가 축 계산 (AABB 축과 OBB 축의 외적)
    for (int i = 0; i < 3; ++i)
    {
        test[6 + i * 3 + 0] = test[i].Cross(test[3]);
        test[6 + i * 3 + 1] = test[i].Cross(test[4]);
        test[6 + i * 3 + 2] = test[i].Cross(test[5]);
    }

    // 모든 축에 대해 겹치는지 검사
    for (int i = 0; i < 15; ++i)
    {
        if (!OverlapOnAxis(aabb, obb, test[i])) // 하나라도 겹치지 않으면 false 반환
            return false;
    }

    return true; // 모두 겹치면 true 반환
}

 

AABB와 PLANE

평명은 쭉 뻗어나가는 것으로 거리 계산을 통해 해주면 된다.

// AABB와 평면의 충돌 검사
bool MathUtils::AABBToPlane(const AABB3D& aabb, const Plane3D& plane)
{
    float pLen = aabb.size.x * fabsf(plane.normal.x) + 
        aabb.size.y * fabsf(plane.normal.y) +
        aabb.size.z * fabsf(plane.normal.z);

    float dot = plane.normal.Dot(aabb.position); // 평면의 법선과 AABB 중심의 내적
    float dist = dot - plane.distance; // 평면 상수와의 차이 계산

    return fabsf(dist) <= pLen; // 겹치는지 여부 반환
}

 

OBB와 OBB

이것도 동일하게 축을 하나 정하고 이를 모든 선분을 검사해주면 된다.

// 두 OBB가 주어진 축에 대해 겹치는지 검사
bool MathUtils::OverlapOnAxis(const OBB3D& obb1, const OBB3D& obb2, const Vec3& axis)
{
    Interval3D a = GetInterval(obb1, axis); // obb1의 구간 계산
    Interval3D b = GetInterval(obb2, axis); // obb2의 구간 계산
    return ((b.min <= a.max) && (a.min <= b.max)); // 구간이 겹치면 true 반환
}

// 두 OBB의 충돌 검사
bool MathUtils::OBBToOBB(const OBB3D& obb1, const OBB3D& obb2)
{
    Vec3 test[15] = // 충돌 검사에 사용될 축
    {
        obb1.orientation.Right(), // OBB1 축 1
        obb1.orientation.Up(), // OBB1 축 2
        obb1.orientation.Backward(), // OBB1 축 3
        obb2.orientation.Right(), // OBB2 축 1
        obb2.orientation.Up(), // OBB2 축 2
        obb2.orientation.Backward(), // OBB2 축 3
        // 외적으로 생성된 추가 축은 여기에서 계산됨
    };

    // 추가 축 계산 (OBB1 축과 OBB2 축의 외적)
    for (int i = 0; i < 3; ++i)
    {
        for (int j = 0; j < 3; ++j) {
            test[6 + i * 3 + j] = test[i].Cross(test[3 + j]);
        }
    }

    // 모든 축에 대해 겹치는지 검사
    for (int i = 0; i < 15; ++i)
    {
        if (!OverlapOnAxis(obb1, obb2, test[i])) // 하나라도 겹치지 않으면 false 반환
            return false;
    }

    return true; // 모두 겹치면 true 반환
}

 

Plane과 Plane

각 평면의 법선벡터를 외적해서 평행한지 검사하고 평행하지 않으면 충돌이 있는것이다.

// 두 평면의 충돌 검사
bool MathUtils::PlaneToPlane(const Plane3D& plane1, const Plane3D& plane2)
{
    Vec3 d = plane1.normal.Cross(plane2.normal); // 두 평면의 법선 벡터의 외적
    return d.Dot(d) != 0; // 외적의 결과가 0이 아니면 두 평면은 평행하지 않은 것으로 간주
}

 

 

2.RayCasting

Sphere

Sphere와 레이의 충돌여부 판정은 레이의 시작점과 구의 중심점까지의 거리와 반지름을 비교해서 판정한다. 

// 구와 레이의 충돌 검사
bool MathUtils::Raycast(const Sphere3D& sphere, const Ray3D& ray, OUT float& distance)
{
	Vec3 e = sphere.position - ray.origin; // 레이의 시작점에서 구의 중심까지의 벡터

	float rSq = sphere.radius * sphere.radius; 
	float eSq = e.LengthSquared(); // e 벡터의 길이의 제곱

	float a = e.Dot(ray.direction); // 레이 방향과 e 벡터의 내적

	//피타고라스 공식
	float bSq = eSq - (a * a); // b의 제곱(삼각형의 한 변의 제곱)
	float f = sqrt(rSq - bSq); // f는 삼각형의 다른 변(구의 반지름에서 b를 뺀 값)

	// 실제 충돌이 발생하지 않는 경우 -> 내적값으로 구한 반지름이랑 차이가 나면
	if (rSq - (eSq - (a * a)) < 0.0f)
		return false;

	// 레이의 시작점이 구 내부에 있는 경우
	if (eSq < rSq)
	{
		distance = a + f; // 구를 뚫고 나가는 지점까지의 거리
		return true;
	}
	// 구 외부에서 시작하여 구에 닿지 않는 경우
	distance = a - f; // 구에 가장 가까운 점까지의 거리
	return false;
}

 

AABB

 AABB와의 충돌계산은 위 사진과 같이 큐브를 위에서 봤다고 가정하고 2차원으로 생각하고 보면 평면에 대해 교차점을 찾아내면 되는데 이는 Cyrus-Beck 클리핑 알고리즘을 변형한 형태로 수행되며, tmin과 tmax를 통해 충돌이 발생하는지 판단한다. 

이 테스트는 3번에 걸쳐서 해주면 된다.

// AABB와 레이의 충돌 검사 (Cyrus-Beck clipping algorithm을 사용)
bool MathUtils::Raycast(const AABB3D& aabb, const Ray3D& ray, OUT float& distance)
{
	Vec3 min = AABB3D::GetMin(aabb); // AABB의 최소 좌표
	Vec3 max = AABB3D::GetMax(aabb); // AABB의 최대 좌표

	// 각 축에 대해 레이가 AABB의 두 평면(최소값, 최대값)과 만나는 t 값을 계산
	float t1 = (min.x - ray.origin.x) / ray.direction.x;
	float t2 = (max.x - ray.origin.x) / ray.direction.x;

	float t3 = (min.y - ray.origin.y) / ray.direction.y;
	float t4 = (max.y - ray.origin.y) / ray.direction.y;

	float t5 = (min.z - ray.origin.z) / ray.direction.z;
	float t6 = (max.z - ray.origin.z) / ray.direction.z;

	// 가장 큰 최소 t 값(tmin)과 가장 작은 최대 t 값(tmax)을 계산
	float tmin = fmaxf(fmaxf(fminf(t1, t2), fminf(t3, t4)), fminf(t5, t6));
	float tmax = fminf(fminf(fmaxf(t1, t2), fmaxf(t3, t4)), fmaxf(t5, t6));

	// tmax가 0보다 작으면 레이는 AABB의 뒤쪽을 향함
	if (tmax < 0)
		return false;

	// tmin이 tmax보다 크면 레이는 AABB를 교차하지 않음
	if (tmin > tmax)
		return false;

	// 실제 충돌 거리 계산
	distance = (tmin < 0.0f) ? tmax : tmin;
	return true;
}

Plane

평면과의 충돌 검사는 레이가 평행한지 검사하고 아니라면 교점을 찾아내는 과정을 수행해주면 된다. 

// 평면과 레이의 충돌 검사
bool MathUtils::Raycast(const Plane3D& plane, const Ray3D& ray, OUT float& distance)
{
	float nd = ray.direction.Dot(plane.normal); // 레이 방향과 평면의 법선의 내적
	float pn = ray.origin.Dot(plane.normal); // 레이의 시작점과 평면의 법선의 내적

	// nd가 0보다 크거나 같으면 레이와 평면은 평행하거나 레이가 평면에서 멀어짐
	if (nd >= 0.0f)
		return false;

	// 실제 충돌 거리 계산
	float t = (plane.distance - pn) / nd;
	if (t >= 0.0f) {
		distance = t; // 충돌 지점까지의 거리
		return true;
	}

	return false;
}

 

 

https://www.acmicpc.net/problem/11050

조합의 경우의 수를 구하는 코드를 만들어주면 된다. n을 k번 1씩 줄이면서 곱해준 값과 k를 k번 1씩 줄이면서 곱해준 값을 나눠주면 된다.

 

정답코드

#include <iostream>

using namespace std;

int main()
{
	int n, k;

	cin >> n >> k;

	if (k > n - k) {
		k = n - k;  // nCk == nC(n-k), 계산량 줄이기 위해 k를 더 작은 값으로 설정
	}

	int a = 1, b = 1;

	for (int i = 0; i < k; i++)
	{
		a *= (n - i);
		b *= (k - i);
	}

	cout << a / b << endl;
}

https://www.acmicpc.net/problem/10872

 

팩토리얼을 구현하면 되는 문제이다. 나는 재귀함수를 사용하여 구현해주었다. 1보다 작다면 1을 반환해주고 아니라면

n* fac(n-1)을 통해 n~ 1까지의 팩토리얼 값을 계산하도록 했다.

이때 팩토리얼 결과값이 클 수 있어서 long값을 반환하도록 해주었다. 

 

 

정답코드

#include <iostream>

using namespace std;

long fac(int n)
{
	if (n <= 1)return 1;
	else return n * fac(n-1);
}

int main()
{
	int n;
	cin >> n;


	cout << fac(n) << endl;
}

https://www.acmicpc.net/problem/24723

 

간단하게 생각해봤을 때, 한층이 올라갈때마다 모든 경로는 피라미드 구조상 각 블록마다 두 가지 선택지를 갖게되기 때문에 전체 경우의 수는 2^n이라고 볼 수 있을 것 같아서 이렇게 계산하고 출력해주었다.

 

정답코드

#include <iostream>
#include <cmath>

using namespace std;

int main()
{
	int n;
	cin >> n;

	cout << pow(2, n) << endl;
}

https://www.acmicpc.net/problem/15439

 

이 문제는 간단하게 경우의수를 생각해서 각 상의에 대해 (N - 1)개의 다른 색상의 하의를 선택할 수 있기 때문에 계산된 결과를 출력해주는 것으로 풀어보았다.

 

정답코드

#include <iostream>

using namespace std;

int main()
{
	int n;
	cin >> n;

	cout << n * (n - 1) << endl;
}

 

 

1. 기본도형

오늘은 3d 기본 도형에 대해 알아보고 구현해보자 

알아볼 기본도형은 점, 선, 구등이 있다.

점은 말그대로 하나의 좌표이다.

 

선은 시작과 끝이 있는 선분이다. 

 

레이는 시작점이 있으면서 한쪽 방향으로 쭉 뻗어나가는 광선이다.

 

구는 중심점과 반지름을 가진 도형으로 중심에서 반지름까지의 모든 점을 합쳐주면 하나의 구가 되는 것이다.

 

AABB는 x,y,z축이 정렬되어(평행한) 만들어진 Box모양이다. 

 

OBB는 x,y,z축이 정렬되어 있지않은 채 만들어 Box모양이다. 

 

Plane은 평면으로 만들 때 점3개를 이용하여 만들거나 노멀벡터와 한점이나 노멀벡터와 평면과 원점까지의 거리를 활용 하여 만들어줄 수 있다.

 

Triangle은 3개의 점으로 이루어진 도형으로 이때 Union이라는 것을 사용해볼 것인데 이는 자료형이 3개의 각기 다른 자료형으로 표현가능하다는 것이다.

 

Primitive3D.h

#pragma once

// *************
// Point3D
// *************
using Point3D = Vec3;

// *************
// Line3D
// *************

struct Line3D
{
	Point3D start = Point3D(0.f);
	Point3D end = Point3D(0.f);

	float Length() { return Vec3::Distance(start, end); }
	//제곱급
	float LengthSq() { return Vec3::DistanceSquared(start, end); }
};

// *************
// Ray3D
// *************

struct Ray3D
{
	Point3D origin = Point3D(0.f);
	Vec3 direction = Vec3(0.f);

	void NormalizeDirection() { direction.Normalize(); }

	//Ray3D생성
	static Ray3D FromPoints(const Point3D& from, const Point3D& to) { return Ray3D{ from, to - from }; }
};

// *************
// Sphere3D
// *************

struct Sphere3D
{
	Point3D position;
	float radius;
};

// *************
// AABB
// *************

struct AABB3D
{
	Point3D position = Vec3(0.f);
	Vec3 size = Vec3(1.f, 1.f, 1.f);

	//최소점
	static Vec3 GetMin(const AABB3D& aabb)
	{
		Vec3 p1 = aabb.position + aabb.size;
		Vec3 p2 = aabb.position - aabb.size;
		return Vec3(fminf(p1.x, p2.x), fminf(p1.y, p2.y), fminf(p1.z, p2.z));
	}

	//최대점
	static Vec3 GetMax(const AABB3D& aabb)
	{
		Vec3 p1 = aabb.position + aabb.size;
		Vec3 p2 = aabb.position - aabb.size;
		return Vec3(fmaxf(p1.x, p2.x), fmaxf(p1.y, p2.y), fmaxf(p1.z, p2.z));
	}

	//최소 최대주면 AABB 생성해주는
	static AABB3D FromMinMax(const Vec3& min, const Vec3& max)
	{
		return AABB3D((min + max) / 2, (max - min) / 2);
	}
};

// *****************
// OBB
// *****************

struct OBB3D
{
	Point3D position;
	Vec3 size;
	Matrix orientation;
	// Vec4 quaternion;
	// Vec3 rotation;
};

// *****************
// Plane3D
// *****************

// 삼각형 (정점3개)
// 노멀 + 정점 1개
// 노멀 + 거리
struct Plane3D
{
	Vec3 normal;
	float distance;
};

// *****************
// Triangle3D
// *****************

struct Triangle3D
{
	//3개모두 사용할 수 있다.
	union
	{
		struct
		{
			Point3D a;
			Point3D b;
			Point3D c;
		};
		Point3D points[3];
		float values[9];
	};
};

 

 

2.Point Test

이제 만약 어떤 점이 있다면 그것이 도형에 포함이 되어있는지 확인하는 이론에 대해 배워보고 코드로 구현해보자. 

먼저 구에 대해 Point Test를 해보자. 중심점과의 거리를 통해 판별할 수 있다.

bool MathUtils::PointInSphere(const Point3D& point, const Sphere3D& sphere) {
    float magSq = (point - sphere.position).LengthSquared();
    float radSq = sphere.radius * sphere.radius;
    return magSq <= radSq;
}

Point3D MathUtils::ClossetPoint(const Sphere3D& sphere, const Point3D& point) {
    Vec3 sphereToPointDir = (point - sphere.position);
    sphereToPointDir.Normalize();
    return sphere.position + sphereToPointDir * sphere.radius;
}

AABB도 안에 포함되어있는지 표면에 있는거에서 가장 가까운 점이 어느것인지 찾아보자. 이때 위에서 만들어준 min과 max값을 활용하여 검사해줄 수 있다.

bool MathUtils::PointInAABB(const Point3D& point, const AABB3D& aabb) {
    Point3D min = AABB3D::GetMin(aabb);
    Point3D max = AABB3D::GetMax(aabb);

    if (point.x < min.x || point.y < min.y || point.z < min.z)
        return false;
    if (point.x > max.x || point.y > max.y || point.z > max.z)
        return false;

    return true;
}

Point3D MathUtils::ClossetPoint(const AABB3D& aabb, const Point3D& point) {
    Point3D result = point;
    Point3D minPt = AABB3D::GetMin(aabb);
    Point3D maxPt = AABB3D::GetMax(aabb);

    result.x = max(result.x, minPt.x);
    result.y = max(result.y, minPt.y);
    result.z = max(result.z, minPt.z);

    result.x = min(result.x, minPt.x);
    result.y = min(result.y, minPt.y);
    result.z = min(result.z, minPt.z);

    return result;
}

OBB도 동일하게 검사해주는데 이때 AABB로 변환해준 다음에 테스트를 해줄 수 도 있고 아니면 매축에 프로젝션 연산을 해주면 된다.

bool MathUtils::PointInOBB(const Point3D& point, const OBB3D& obb) {
    Vec3 dir = point - obb.position;

    vector<Vec3> axis = { obb.orientation.Right(), obb.orientation.Up(), obb.orientation.Backward() };
    vector<float> size = { obb.size.x, obb.size.y, obb.size.z };

    for (int i = 0; i < 3; i++) {
        float distance = dir.Dot(axis[i]);

        if (distance > size[i] || distance < -size[i])
            return false;
    }

    return true;
}

Point3D MathUtils::ClossetPoint(const OBB3D& obb, const Point3D& point) {
    Vec3 dir = point - obb.position;
    Point3D result;
    vector<Vec3> axis = { obb.orientation.Right(), obb.orientation.Up(), obb.orientation.Backward() };
    vector<float> size = { obb.size.x, obb.size.y, obb.size.z };

    for (int i = 0; i < 3; i++) {
        float distance = dir.Dot(axis[i]);

        distance = clamp(distance, -size[i], size[i]);
        result = result + (axis[i] * distance);
    }

    return obb.position + result;
}

Plane은 위에서 구해준 방법대로 점과 노멀벡터를 내적해주는 것으로 Distance를 구할 수 있는데 이 값을 검사해주면 된다.

bool MathUtils::PointInPlane(const Point3D& point, const Plane3D& plane) {
    float dot = point.Dot(plane.normal);
    return fabs(dot - plane.distance) < FLT_EPSILON;
}

Point3D MathUtils::ClossetPoint(const Plane3D& plane, const Point3D& point) {
    float dot = point.Dot(plane.normal);
    float distance = dot - plane.distance;
    return point - plane.normal * distance;
}

 

Line은 내적을 통해서 구해줄텐데 내적했을 때 거리가 0인지 검사하는 것으로 내부에 있는지 파악할 수 있다. 가장 가까운 점은 내적을 통한 크기와 기존 크기를 통해 비율을 구해서 활용해주면 된다.

bool MathUtils::PointInLine(const Point3D& point, const Line3D& line) {
    Point3D closest = ClossetPoint(line, point);
    return (closest - point).LengthSquared() == 0.f;
}

Point3D MathUtils::ClossetPoint(const Line3D& line, const Point3D& point) {
    Vec3 lineVec = line.end - line.start;
    float t = (point - line.start).Dot(lineVec) / lineVec.Dot(lineVec);
    t = clamp(t, 0.0f, 1.0f);
    return line.start + lineVec * t;
}

 

Ray는 내적을 통해 구해줄텐데 수직을 내렸을 때의 좌표를 구해주면 되는 것이기 때문에 방향벡터와 정점의 좌표 - 시작점을 내적해주고 이를 방향벡터에 곱해주면 위치를 구해줄 수 있다. 그리고 내부에 있는지 확인하려면 시작좌표와 비교하고 아니라면 시작좌표와의 방향벡터와 기존 레이의 방향벡터가 내적했을 때 1이면 같은것이기 때문에 내부에 있는것으로 판단해주면 된다.

bool MathUtils::PointInRay(const Point3D& point, const Ray3D& ray) {
    if (point == ray.origin)
        return true;
    Vec3 norm = point - ray.origin;
    norm.Normalize();
    return fabs(norm.Dot(ray.direction) - 1.0f) < FLT_EPSILON;
}

Point3D MathUtils::ClossetPoint(const Ray3D& ray, const Point3D& point) {
    float t = (point - ray.origin).Dot(ray.direction);
    t = fmaxf(t, 0.0f);
    return ray.origin + ray.direction * t;
}


지금까지는 물체간의 충돌만을 구현해보았다. 만약 물체가 아니라 땅을 설치하고 이것과의 충돌을 어떤식으로 구현할지가 문제이다. 

땅은 그리드 방식으로 만들어주자.

CollisionDemo.cpp

//Terrain
{
	auto obj = make_shared<GameObject>();
	obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
	obj->AddComponent(make_shared<MeshRenderer>());
	{
		auto mesh = make_shared<Mesh>();
		mesh->CreateGrid(10, 10);
		obj->GetMeshRenderer()->SetMesh(mesh);
		obj->GetMeshRenderer()->SetPass(0);
	}
	{
		obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
	}
}

 

이제 이땅에 어떻게 Collider를 넣어줄지 생각해보자. 기존에 넣어줬던대로 큰 Collider를 그냥 넣어주면 언덕과 같은 부분과 같이 높이가 반영되어있는 땅에는 제대로 적용이 되지않을 것이다. 그렇기 때문에 이 땅을 삼각형 단위로 좌표를 연산해서 이것을 이용해서 세분화된 상태로 충돌이 처리될 수 있도록 해야한다.

 

이런기능을 만들어주기 위하여 Terrain클래스 컴포넌트로 만들어서 관리할 수 있도록 하자.

Terrain.h

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

Terrain.cpp

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

	//Weak포인터라서 변환필요
	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);
}

 

사용은 다음과 같이 하면된다.

CollisionDemo.cpp

	//Terrain
	{
		auto obj = make_shared<GameObject>();
		obj->AddComponent(make_shared<Terrain>());
		obj->GetTerrain()->Create(10, 10, RESOURCES->Get<Material>(L"Veigar"));

		CUR_SCENE->Add(obj);
	}

 

이제 피킹기능을 넣어주자. 중요한점은 사각형을 구성하는 2개의 삼각형을 기준으로 피킹검사를 하고 이를 모든 사각형에 반복하면 된다.

Terrain.h

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

	//near far
	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;
}

 

이렇게 해주고 Scene에서 Picking을 검사하는 부분에 이 Pick함수를 실행하도록 만들어주자.

Scene.cpp

shared_ptr<class GameObject> Scene::Pick(int32 screenX, int32 screenY)
{
	shared_ptr<Camera> camera = GetCamera()->GetCamera();

	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);
	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 정의 ViewSpace 0 0 0 -> 카메라
		Vec4 rayOrigin = Vec4(0.0f, 0.0f, 0.0f, 1.0f);
		Vec4 rayDir = Vec4(viewX, viewY, 1.0f, 0.0f);

		// WorldSpace에서의 Ray 정의
		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;
		}
	}

	for (auto& gameObjcect : gameObjects)
	{
		if (gameObjcect->GetTerrain() == nullptr)
			continue;

		Vec3 pickPos;
		float distance = 0.f;
		if (gameObjcect->GetTerrain()->Pick(screenX, screenY, OUT pickPos, OUT distance) == false)
			continue;

		if (distance < minDistance)
		{
			minDistance = distance;
			picked = gameObjcect;

		}
	}

	return picked;
}

 

이렇게 해준다음 실행해보면 땅을 클릭했을 때 잘 없어지는 것을 볼 수 있다.

 

만약 땅이 아니라 복잡한 매쉬와 충돌이나 레이케스팅을 한다면 이런 삼각형으로 나눠서 하는방법을 응용할 수 있을 것이다. 하지만 이렇게하면 부하가 엄청나다. 그렇기 때문에 간단한 도형으로 된 충돌 판정용 매쉬나 Collider 가지고 충돌을 판별해주는게 나을 수 있다.

조금 더 효율적으로 하려면 레이어를 구분해서 레이어끼리 공식을 만들어서 작동시켜주면 된다.

https://www.acmicpc.net/problem/2346

 

풍선이 원형으로 놓여져있고 왼쪽,오른쪽으로 둘다 이동할 수 있기 때문에 deque자료형을 활용해보았다.

deque자료형과 pair 자료형을 활용하여 풍선의 인덱스 풍선종이에 적힌 숫자를 매치시켜주었다.

만약 오른쪽으로이동한다면 앞에꺼를 뒤에 넣어주고 앞에꺼는 빼주면 된다.

만약 왼쪽으로 이동한다면 뒤에꺼를  앞에 넣어주고 뒤에꺼는 빼주면 된다.

이 과정을 통하여 나온 인덱스값을 정답 vector에 넣어주고 이를 출력해주면 된다.

 

이때 불필요한 이동을 줄이기 위하여 나눗셈을 사용해줄 수 있다. 

 

정답코드

#include <iostream>
#include <deque>
#include <vector>

using namespace std;

vector<int> answer;
int main()
{
	int n, tmp = 0;

	cin >> n;

	deque<pair<int, int>> balloons; 

	for (int i = 0; i < n; i++) 
	{
		int value;
		cin >> value;
		balloons.push_back({ i + 1, value }); // 풍선 번호 / 종이에 적힌 값
	}


	while (!balloons.empty())
	{
		auto current = balloons.front();
		balloons.pop_front();
		answer.push_back(current.first);

		if (balloons.empty()) break;

		int steps = current.second;

		if (steps > 0)
		{
			for (int i = 0; i < steps - 1; i++) { // 오른쪽으로 이동
				balloons.push_back(balloons.front());
				balloons.pop_front();
			}
		}
		else
		{
			for (int i = 0; i < -steps; i++) { // 왼쪽으로 이동
				balloons.push_front(balloons.back());
				balloons.pop_back();
			}
		}
	}

	for (int i = 0; i < answer.size(); i++) {
		if (i != 0) cout << " ";
		cout << answer[i];
	}

	return 0;

}

 

 

정답코드2(나눗셈연산을 통해 이동최적화)

#include <iostream>
#include <deque>
#include <vector>

using namespace std;

int main() {
    int n;
    cin >> n;

    deque<pair<int, int>> balloons;

    for (int i = 0; i < n; i++) {
        int value;
        cin >> value;
        balloons.push_back({i + 1, value});  // 풍선 번호와 적힌 값을 저장
    }

    vector<int> result;

    while (!balloons.empty()) {
        auto current = balloons.front();
        result.push_back(current.first);  // 풍선 번호 저장
        balloons.pop_front();

        if (balloons.empty()) break;

        int steps = current.second;

        if (steps > 0) {
            steps = (steps - 1) % balloons.size();  // 오른쪽으로 이동, 크기 안에서 회전
        } else {
            steps = steps % balloons.size();  // 왼쪽으로 이동
        }

        if (steps > 0) {  // 오른쪽 이동
            for (int i = 0; i < steps; i++) {
                balloons.push_back(balloons.front());
                balloons.pop_front();
            }
        } else if (steps < 0) {  // 왼쪽 이동
            for (int i = 0; i < -steps; i++) {
                balloons.push_front(balloons.back());
                balloons.pop_back();
            }
        }
    }

    for (int i = 0; i < result.size(); i++) {
        if (i != 0) cout << " ";
        cout << result[i];
    }

    return 0;
}

'코딩테스트 > 백준' 카테고리의 다른 글

[백준][C++]24723번. 녹색거탑  (1) 2024.10.08
[백준][C++]15439번. 베라의 패  (3) 2024.10.08
[백준][C++]28279번. 덱 2  (0) 2024.10.04
[백준][C++]11866번. 요세푸스 문제 0  (1) 2024.10.04
[백준][C++]2164번. 카드2  (1) 2024.10.03



 

이제 BoxCollider를 만들어보자. Cube Collider는 OBB와 AABB로 나눠진다. 

실제 Unity 상에서 Cube를 만들어보면 Box collider가 Component로 붙어있는채로 나온다.

이때 AABB는 이 Box Collider가 월드 좌표의 축과 동일한 경우이다. 이렇게 하는 경우 연산이 쉽다

OBB는 이와 반대로 축이 일치하지 않는 경우라고 볼 수 있다.

성능을 우선으로 할지 미세한 판정을  우선으로 할지에 따라 사용하는 방식이 다르다. 만약 비스듬하게 있는 물체가 

있다고 했을 때, 이 물체에 Box Collider를 씌울 때, 정말 물체에 맞게 Collider영역을 만들면 물체에 충돌 판정을 잘할 수 있겠지만 성능이 떨어질 것이다. 만약 축에 맞게 영역을 맞춰주면 성능은 좋아지겠지만 판정에 문제가 생길 수도 있다.

 

이론상으로는 이렇게 나뉘지만, 보통 오브젝트가 회전이나 변형이 많이 이루어지기 때문에 일반적으로 많이 쓰이는 것은 

OBB라고 보면 된다.

지금은 공부하는 차원에서 2가지 다 만들고 통합해주는 방식으로 만들어주자.

우선 레이케스팅하는 부분은 이전에 만들어준 Sphere부분과 동일하게 만들어주면 된다.

AABBoxCollider.h

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

	BoundingBox& GetBoundingBox() { return _boundingBox; }

private:
	BoundingBox _boundingBox;
};

 

AABoxCollider.cpp

#include "pch.h"
#include "AABBBoxCollider.h"

AABBBoxCollider::AABBBoxCollider() : BaseCollider(ColliderType::AABB)
{

}

AABBBoxCollider::~AABBBoxCollider()
{

}

void AABBBoxCollider::Update()
{

}

bool AABBBoxCollider::Intersects(Ray& ray, OUT float& distance)
{
	return _boundingBox.Intersects(ray.position, ray.direction, OUT distance);
}

 

OBBBoxCollider.h

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

	BoundingOrientedBox& GetBoundingBox() { return _boundingBox; }

private:
	BoundingOrientedBox _boundingBox;
};

OBBBoxCollider.cpp

#include "pch.h"
#include "OBBBoxCollider.h"

OBBBoxCollider::OBBBoxCollider() : BaseCollider(ColliderType::OBB)
{

}

OBBBoxCollider::~OBBBoxCollider()
{

}

void OBBBoxCollider::Update()
{

}

bool OBBBoxCollider::Intersects(Ray& ray, OUT float& distance)
{
	return _boundingBox.Intersects(ray.position, ray.direction, OUT distance);
}

 

이렇게 해주고 Collider를 테스트하도록 메인코드에 새로운 오브젝트를 하나생성하고 테스트해보자 테스트해보기 위해 

오브젝트가 움직이는 코드를 넣어주자.

CollisionDemo.cpp

#include "pch.h"
#include "CollisionDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "SceneDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"
#include "SphereCollider.h"
#include "Scene.h"
#include "AABBBoxCollider.h"
#include "OBBBoxCollider.h"

void CollisionDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		CUR_SCENE->Add(camera);
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}

	// Mesh
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(3.f, 0.f, 0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		{
			auto collider = make_shared<SphereCollider>();
			collider->SetRadius(0.5f);
			obj->AddComponent(collider);
		}
		{
			obj->AddComponent(make_shared<MoveScript>());
		}

		CUR_SCENE->Add(obj);
	}

	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Cube");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		{
			auto collider = make_shared<AABBBoxCollider>();
			collider->GetBoundingBox().Extents = Vec3(0.5f);
			obj->AddComponent(collider);
		}

		CUR_SCENE->Add(obj);
	}
}

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

void CollisionDemo::Render()
{

}

void MoveScript::Update()
{
	auto pos = GetTransform()->GetPosition();
	pos.x -= DT * 1.0f;
	GetTransform()->SetPosition(pos);
}

 

이렇게 해주면 둘다 클릭하면 없어지는 것을 볼 수 있다.

 

OBB를 사용해서도 테스트를 해보자 이때 큐브를 살짝 돌려주고. OBB에 회전값을 전달해주어야하는데 이때 잠깐보자면 BoundingOrientedBox에서 Orientation으로 물체의 회전을 관리해주고 있는데 이는 쿼터니언 값으로 Float4값을 가지고있다. 이를 쿼터니언 값으로 전달해주어야 한다.

CollisionDemo.cpp

#include "pch.h"
#include "CollisionDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "SceneDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"
#include "SphereCollider.h"
#include "Scene.h"
#include "AABBBoxCollider.h"
#include "OBBBoxCollider.h"

void CollisionDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		CUR_SCENE->Add(camera);
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}

	// Mesh
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(3.f, 0.f, 0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		{
			auto collider = make_shared<SphereCollider>();
			collider->SetRadius(0.5f);
			obj->AddComponent(collider);
		}
		{
			obj->AddComponent(make_shared<MoveScript>());
		}

		CUR_SCENE->Add(obj);
	}

	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Cube");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		/*{
			auto collider = make_shared<AABBBoxCollider>();
			collider->GetBoundingBox().Extents = Vec3(0.5f);
			obj->AddComponent(collider);
		}*/
		{
		obj->GetOrAddTransform()->SetRotation(Vec3(0, 45, 0));

		auto collider = make_shared<OBBBoxCollider>();
		collider->GetBoundingBox().Extents = Vec3(0.5f);
		collider->GetBoundingBox().Orientation = Quaternion::CreateFromYawPitchRoll(45, 0, 0);
		obj->AddComponent(collider);
		}

		CUR_SCENE->Add(obj);
	}
}

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

void CollisionDemo::Render()
{

}

void MoveScript::Update()
{
	auto pos = GetTransform()->GetPosition();
	pos.x -= DT * 1.0f;
	GetTransform()->SetPosition(pos);
}

 

이렇게 되어있으면 회전되어있는 큐브와 구 클릭했을 때 잘 없어진다.

 

이제 여기에 Collider끼리 충돌했을 때를 이벤트를 구현해보자

SphereCollider.cpp

bool SphereCollider::Intersects(shared_ptr<BaseCollider>& other)
{
	ColliderType type = other->GetColliderType();

	//어떤 Collider인지 판별
	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;
}

 

AABBBoxCollider.cpp

bool AABBBoxCollider::Intersects(shared_ptr<BaseCollider>& other)
{
	ColliderType type = other->GetColliderType();

	switch (type)
	{
	case ColliderType::Sphere:
		return _boundingBox.Intersects(dynamic_pointer_cast<SphereCollider>(other)->GetBoundingSphere());
	case ColliderType::AABB:
		return _boundingBox.Intersects(dynamic_pointer_cast<AABBBoxCollider>(other)->GetBoundingBox());
	case ColliderType::OBB:
		return _boundingBox.Intersects(dynamic_pointer_cast<OBBBoxCollider>(other)->GetBoundingBox());
	}
}

OBBBoxCollider.cpp

bool OBBBoxCollider::Intersects(shared_ptr<BaseCollider>& other)
{
	ColliderType type = other->GetColliderType();

	switch (type)
	{
	case ColliderType::Sphere:
		return _boundingBox.Intersects(dynamic_pointer_cast<SphereCollider>(other)->GetBoundingSphere());
	case ColliderType::AABB:
		return _boundingBox.Intersects(dynamic_pointer_cast<AABBBoxCollider>(other)->GetBoundingBox());
	case ColliderType::OBB:
		return _boundingBox.Intersects(dynamic_pointer_cast<OBBBoxCollider>(other)->GetBoundingBox());
	}

	return false;
}

 

이렇게 해주고 이제 충돌검사를 하는 코드를 Scene클래스에 넣어줘서 물체들의 상태가 Update된 이후인 LateUpdate에서 충돌체크를 해주자.

Scene.cpp

void Scene::CheckCollision()
{
	vector<shared_ptr<BaseCollider>> colliders;

	//Collider 가진애들 모두 찾아주기
	for (shared_ptr<GameObject> object : _objects)
	{
		if (object->GetCollider() == nullptr)
			continue;

		colliders.push_back(object->GetCollider());
	}

	// BruteForce
	for (int32 i = 0; i < colliders.size(); i++)
	{
		for (int32 j = i + 1; j < colliders.size(); j++)
		{
			shared_ptr<BaseCollider>& other = colliders[j];
			//만약 i번째랑 j번째랑 충돌했다면
			if (colliders[i]->Intersects(other))
			{
				int a = 3;
			}
		}
	}
}

 

이렇게 해주고 Break Point를 int a=3에 두면  충돌했을 때 Break가 걸리는 것을 볼 수 있다.


강의

https://inf.run/LBTDz

 

학습 페이지

 

www.inflearn.com

 

오늘은 충돌에 대해 알아보자. 보통 충돌은 Mesh가 아니라 Collider를 통해 이루어진다. 만약 Mesh로 충돌 연산을 하게 된다면 수많은 삼각형에 대한 연산을 해야하기 때문에 연산량을 줄이기 위하여 간략한 기본 도형으로 이루어진 Collider를 통해 연산을 한다.

 

Collider 클래스는 Base Collider라는 클래스를 만들어서 상속받게하자. 지금은 Box, Sphere형태의 Collider만 만들어주도록 하자. Box형 Collider에는 OBB랑 AABB 두가지 방식이 있는데 두가지 다 만들어보자.

 

우선 BaseCollider에서 모든 Collider가 가지고 있어야하는 기능을 넣어주자. 이 기능에는 Get함수와 기본적인 Collider 종류, 레이케스팅을 통해 Collider를  관통했는지 와 다른 물체들 끼리 충돌이 일어났는 지 체크하는 기능이 필요하다.

레이케스팅은 Math라이브러리 이용하여 구현해주면 된다. 앞으로 갈 수 있다거나 땅에 붙어있는 상태를 관찰할 때는 충돌체를 사용하는 것도 방법이지만 레이케스팅을 사용해주는 게 더 좋을 수도 있다. 

BaseCollider.h

#pragma once
#include "Component.h"

enum class ColliderType
{
	Sphere,
	AABB,
	OBB,
};

class BaseCollider : public Component
{
public:
	BaseCollider(ColliderType colliderType);
	virtual ~BaseCollider();

	//레이케스팅 OUT은 명시용 기능 x 
	virtual bool Intersects(Ray& ray, OUT float& distance) = 0;
	ColliderType GetColliderType() { return _colliderType; }

protected:
	ColliderType _colliderType;
};

BaseCollider.cpp

#include "pch.h"
#include "BaseCollider.h"

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

}

BaseCollider::~BaseCollider()
{

 

기능을 완성한 다음에 ComponentType과 GameObject에서 가져오는 부분을 추가해주자.

GameObject.cpp

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

이를 바탕으로Sphere Collider를 만들어주자. Sphere 모양은 BoudingSphere 기능을 사용해주자.이 BoudingSphere는

중심점 위치와 반지름으로 구성되어있고 충돌 및 레이케스팅 관련 기능들을 지원해준다. 이 BoudingSpehre의  세부옵션을 설정할 수 있게 Get함수를 만들어주자.

SphereCollider.h

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

SphereCollider.cpp

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

 

이제 메인함수에서 Sphere Collider를 만들어서 Component로 넣어주자. 

#include "pch.h"
#include "CollisionDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "SceneDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"
#include "SphereCollider.h"

void CollisionDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		CUR_SCENE->Add(camera);
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}

	// Mesh
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		{
			auto collider = make_shared<SphereCollider>();
			collider->SetRadius(0.5f);
			obj->AddComponent(collider);
		}

		CUR_SCENE->Add(obj);
	}
}

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

		//Picking
	}
}

void CollisionDemo::Render()
{

}

그리고 테스트하기 위해 피킹 코드를 만들어주자. 피킹함수는 Scene 클래스에서 만들어주자. 

피킹함수에서는 스크린 좌표에서 클릭한 부분을 3D좌표로 바꿔주어야한다.

Scene.cpp

shared_ptr<class GameObject> Scene::Pick(int32 screenX, int32 screenY)
{
	shared_ptr<Camera> camera = GetCamera()->GetCamera();

	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);
	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 정의 ViewSpace 0 0 0 -> 카메라
		Vec4 rayOrigin = Vec4(0.0f, 0.0f, 0.0f, 1.0f);
		Vec4 rayDir = Vec4(viewX, viewY, 1.0f, 0.0f);

		// WorldSpace에서의 Ray 정의
		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;
}

 

이렇게 만들어준 함수를 메인에서 사용하도록하자. 테스트를 위해 피킹된 물체는 없어지는 걸로 해두자.

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

 

이렇게하면 피킹된 오브젝트가 없어진다.

+ Recent posts