티스토리 뷰

SFML

SAT 이론 및 구현 스터디(2)

권벡터 2025. 3. 5. 21:30

개요

본 장에서는 이전에 포스팅한 SAT 이론 및 구현 스터디(1) 한 개념들을 SFML 환경의 코드로 구현한 내용들을 다룬다. 

코드 구현전에 전반적인 흐름을 파악하기 위해서 플로우 차트를 작성해 보았다. 

 

 

SAT 알고리즘 플로우차트

 

1. Convex Polygon의 Edge Vector 구하기

먼저 Edge Vector를 구하기 위해서는 두 개의 정점의 x, y 성분을 알아야 한다.  

두 정점은 좌표가 아닌 벡터 성분이고 벡터 뺄셈을 통해서 Edge Vector를 구하게 된다. 

연산은 존재하는 Convex Polygon의 정점의 갯수만큼 반복된다.

 

해당 코드는 다음과 같다. 

sf::Vector2f Collision::DetermineEdgeVector(sf::Vector2f* vertices, uint32_t index, uint32_t count) const
{
	uint32_t i0 = index + 1;  // B
	uint32_t i1 = index;      // A 

	if (i0 == count)
	{
		i0 = 0;
		i1 = count - 1;
	}
    
    // Vector AB is B - A
	return vertices[i0] - vertices[i1];
}

 

 

2. 모서리 벡터의 Normal Vector(Axis)구하기 

Convex Polygon에서 산출한 Edge Vector의 수직 벡터를 구한다. 

수직벡터를 구하기 위해서는 회전행렬을 사용한다. 회전 행렬 방정식은 아래와 같다.

 

 \begin{align}
a &= \begin{bmatrix}
  a_x \\
  x_y \\
\end{bmatrix}  \\
  
a^\perp  &=\begin{bmatrix}
 cos\frac{\pi}{2}& -sin\frac{\pi}{2}  \\
 sin\frac{\pi}{2}& cos\frac{\pi}{2} \\
\end{bmatrix}
\begin{bmatrix}
   a_x\\
   a_y\\
\end{bmatrix} \\
&= \begin{bmatrix}
0 & -1  \\
1 & 0 \\
\end{bmatrix}
\begin{bmatrix}
a_x \\
a_y \\
\end{bmatrix} \\

&= \begin{bmatrix}
-a_y \\
a_x
\end{bmatrix}
\end{align} 

 

추가적으로  Normal Vector는 방향 성분만 있어도 된다. 그러므로 Normal Vector를 구할 때
Normalize를 해서 벡터의 방향 성분만 남긴다. 해당 식을 토대로 Normal Vector를 구하면 다음과 같다.

sf::Vector2f Collision::DetermineNormalVector(sf::Vector2f* edgeVectors, int index) const
{
	sf::Vector2f normalVector = sf::Vector2f{ -edgeVectors[index].y,edgeVectors[index].x };

	//normalize
	const float length = sqrtf(
				  	(normalVector.x * normalVector.x) +
				  	(normalVector.y * normalVector.y));

	normalVector = normalVector / length;

	return normalVector;
}

 

 

Normal Vector가 구해졌으면 해당 벡터를 별도의 배열에 저장한다.

여기서 중요한 점은 중복이 되는 벡터는 배열에 저장하지 않는다. 오버랩 체크를 할 때  똑같은 축에 투영을 한다면 

연산 비용만 낭비되므로 Normal Vector 배열 저장전에 중복체크는 꼭 하도록 하자.

 

3.  Axis Projection and Overlap Check(축 투영 및 겹침 체크)

도형이 겹쳐졌는지 체크하기 위해서는 하나의 Axis에 Convex Polygon의 정점들을 투영시키고 각 도형의 투영된 선분(투영 값의 최대, 최솟값)을 산출한 후, 두 개 이상의 투영값에서 겹쳐짐 여부를 체크한다. 

Axis Projection 

일반적으로 벡터 성분의 투영은 Dot Product(내적법) 을 사용한다. 

 

투영한 결괏값은 각 Convex Polygon에 해당되는 배열에 별도로 저장하고 이들 배열에서 최솟값(min), 최댓값(max)

을 결정한다.

 

그럼  Convex Polygon 도형 하나에 min, max 값 두 개가 나오게 된다. 

연산된 min, max 값은 Vector2f에 저장했다.

 

해당 로직을 코드로 표현하면 다음과 같다. 

sf::Vector2f Collision::ProjectOnto(sf::Vector2f* vertices, uint32_t count, sf::Vector2f axis) const
{
	// 초기 최대, 최소값 설정
	float min = INF;
	float max = -INF;

	// 도형의 정점 갯수만큼 반복
	for (uint32_t i = 0; i < count; i++)
	{
		float projection = DotProduct(vertices[i], axis);

		if (projection < min)
			min = projection;

		if (projection > max)
			max = projection;
	}
    
	// 최대, 최소값 반환 
	return
	{
		min,
		max
	};
}

 

Overlap Check

도형의 겹쳐짐 체크는 두 개의 Convex polygon의 투영된 값들을 비교하여 겹쳐진 길이를 연산하고 이에 따른 판단을 한다.

 

만약 두 투영값들이 서로 겹쳐지지 않을 경우, 두 도형은 서로 겹쳐진 상태가 아닌 것으로 그냥 로직에서 리턴 시킨다. 

반대로 겹치는 구간이 있는 경우, 검사 축(Axis) 벡터에 겹쳐진 길이를 곱하여 MTV(Minimum Translation Vector)를 구한다. MTV는 검사한 Axis 중에서 겹쳐진 길이가 가장 큰 값으로 한다. 

 

해당 로직은 아래와 같이 구현할 수 있겠다. 

float Collision::Overlap(sf::Vector2f v0, sf::Vector2f v1) const
{
	// 두 투영값이 서로 겹쳐지지 않은 경우 0을 반환 
    // 만약 겹치는 경우, 겹친 길이를 반환 
	return !(v0.x <= v1.y && v0.y >= v1.x) ? .0f : std::min(v0.y, v1.y) - std::max(v0.x, v1.x);
}

void Collision::CheckOverlap()
{
	float overlap = Overlap(bodyProtection, otherProtection);
	
    // 안겹치면 해당 로직 리턴 
    if (!overlap) // =>!0 이므로 true 즉 오버랩이 없다는 뜻 
    {
        MTV = sf::Vector2f(.0f, .0f);

        delete[] axis;
        return false;
    }
    // 겹치면 검사 Axis에 겹친 길이만큼 곱셈
    else
    {
        if (overlap < minOverlap)
        {
            minOverlap = overlap;
            MTV = a * overlap;
        }
    }
}



 

4. MTV 연산 

산출된 MTV를 토대로 도형을 회피시켜야 한다. 우선 MTV의 방향과 두 도형 중심 사이의 벡터의 방향이 서로 일치하는지 체크한다. 도형의 중심을 구하는 수식은 다음과 같다

 

$$ C_x = \frac{1}{6A}\sum_{i=0}^{n-1}(x_i+x_{i+1})(x_iy_{i+1}-x_{i+1}y_i)$$ 

$$ C_y = \frac{1}{6A}\sum_{i=0}^{n-1}(y_i+y_{i+1})(x_iy_{i+1}-x_{i+1}y_i)$$ 

$$  A = \frac{1}{2}\sum_{i=0}^{n-1}(x_iy_{i+1}-x_{i+1}y_i)$$ 

 

이를 코드로 구현하면 다음과 같다.

sf::Vector2f Collision::GetCenter(const Collider& body) const
{
	sf::Vector2f center;
	uint32_t count = body.m_verticesCount;

	float A = 0.0f;
	float area = 0.0f;

	for (uint32_t i = 0; i < count; i++)
	{
		auto& v0 = body.m_vertices[i];
		auto& v1 = body.m_vertices[i + 1 != count ? i + 1 : 0];

		float b = CrossProduct(v0, v1);

		A += b;

		center += (v0 + v1) * b;
	}

	center *= 1.0f / (3.0f * A);
	return center;
}

 

 

그리고 두 도형의 벡터와 MTV의 방향을 검사하는 로직은 다음과 같다. 

만약 두 도형의 벡터와 MTV의 내적이 음수면 이는 두 벡터의 방향이 정반대이므로 MTV에

-1을 스칼라 곱 해준다. 

if (DotProduct(GetCenter(body) - GetCenter(other), MTV) < 0.0f)
{
	MTV *= -1.0f;
}

 

5. MTV만큼 도형 회피 

이제 정말로  MTV가 결정됐으니 MTV만큼 도형을 이동시켜 준다. 

해당 코드는 다음과 같다.

void CGameState::CheckCollision()
{
	for (auto& c : m_Colliders) {
		sf::Vector2f MTV;
		Collision::Instance.SATCollision(m_box, *c, MTV);

		m_box.Move(MTV);
	}
}

 

위 과정은 하나의 주기에 레벨 내에 존재하는 모든 Collider를 검사한다.

 


참고한 사이트 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/09   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함