Notice
Recent Posts
Recent Comments
«   2025/03   »
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 31
Today
Total
Archives
관리 메뉴

cyphen156

게임 수학 7장-1 : 내적(벡터 공간의 분석과 응용)-벡터 내적과 시야 본문

수학/게임수학

게임 수학 7장-1 : 내적(벡터 공간의 분석과 응용)-벡터 내적과 시야

cyphen156 2025. 3. 13. 20:09

드디어 몇 번이나 나를 다시 공부하게 만들었던 내적이다. 

여태까지가 벡터를 통해 가상공간에서의 물체를 그리고, 움직이고, 변환하는 것에 집중했다면 앞으로 배울 내용들은 더욱 빠르고, 적게 연산하여 물체와 물체간의 상호작용 등을 배워나갈 것이다.

벡터의 내적

내적은 같은 차원의 두 벡터가 주어졌을 때 각 성분을 곱한 후 더해 스칼라를 만들어내는 연산이다.

가령 2차원 벡터와 3차원 벡터의 경우를 확인해보자.

이차원 벡터의 내적

A-> = (a, b)
B-> = (c, d)

Dot(A->, B->)
	=> (ac + bd)

삼차원 벡터의 내적

A-> = (a, b, c)
B-> = (d, e, f)

Dot(A->, B->)
	=> (ad + be + cf)

벡터의 내적은 교환 법칙은 성립하지만 결과가 벡터가 아닌 스칼라로 변환되기 때문에 결합 법칙은 성립하지 않는다.

다만 덧셈에 대한 분배 법칙은 성립한다. 그리고 이것은 다시 말해 벡터 간의 결합 법칙은 성립하지 않지만, 스칼라 곱에 대해서는 결합 법칙과 분배 법칙이 성립한다는 것을 의미한다.

이러한 특징들 때문에 두 벡터의 내적의 결과는 사잇각에 대한 코사인 함수와 직접적인 관계를 가진다.

벡터 내적과 사잇각의 관계

두 벡터 A->, B->가 있을 때, 내적은 다음과 같이 각도와 연관된다:

여기서 는 두 벡터 사이의 각이다. 따라서 벡터의 내적을 통해 사잇각을 구할 수 있으며, 식을 변형하면 다음과 같다:

이를 통해 벡터의 크기를 정규화하면 내적의 결과가 단순히 코사인 값과 동일해진다

Dot(A->, B->)
	=>	|A->||B->|Cos

즉, 두 벡터를 단위 벡터로 변환하면 내적의 결과가 두 벡터 사이의 코사인 값이 되며, 이를 통해 두 벡터의 방향성을 쉽게 비교할 수 있다.

굉장히 난잡하게 써놨는데 핵심은 다음과 같다.

벡터 내적의 삼각함수적 유도

삼각함수를 이용한 내적 유도

  • 벡터들을 단위 원 위의 좌표로 표현하면, A-> = ( cosθ, sinθ) B-> = b(cosα,sinα)
  • 벡터 B의 x-축 투영을 찾으면, Bx=bcosα
  • 벡터 B의 y-축 투영은, By=bsinα

초록색 벡터를 통한 내적의 유도

이제 초록색 벡터를 이용하여 내적을 유도해보겠습니다.

  • 초록색 벡터는 B\mathbf{B}의 방향을 바꾸어 놓은 벡터로 볼 수 있습니다.
  • 그림에서 볼 수 있듯이, 각도 π−θcos⁡(π−θ)를 이용하여 내적을 유도
    cos⁡(π − θ) = −cos⁡θ
  • 따라서, 초록색 벡터의 x축 투영Bx = bcos(π θ) = b(cosθ) = bcosθ

결과적으로, 두 벡터의 내적은  A B = a1b1 + a2b2 = A ∣∣ B cosθ

즉, 내적이 두 벡터의 크기와 사잇각의 코사인 값의 곱이다.

따라서 

(B->**2)
	=>	{(A->) - (C->)}**2
	==>	(A->**2) + (C->**2) - 2(A->)(C->)cosθ
    ==> cosθ

 

벡터 내적의 직교성 판별

벡터 내적을 활용하면, 두 벡터가 서로 직교(orthogonal)하는지 판단할 수 있다.

 

코사인은 항상 -1 ~ 1 사이의 값을 가지는 주기 함수이다. 

- 코사인이 0이 되는 경우는 각이 90도 또는 -90도일 때이다.
- 즉, 벡터 내적이 0이면 두 벡터는 서로 직교(orthogonal) 한다는 것을 의미한다.

이를 통해 물체가 얼마나 회전했든 상관없이, 강체(Rigid Body)의 직교성을 판단할 수 있다.

 

여기서 회전 변환을 다시 생각해보자. 회전변환은 ad - bc, 행렬식은 Cos**2 + Sin**2 = 1이다. 

벡터가 변환되었다고 할 지라도 형태가 원본 그대로 유지되기 때문에 이것을 리지드 트랜스폼(강체 변환)이라고 한다.

그렇다면 벡터의 내적은 어디에 사용될까?

주로 시야 판별에 사용된다. 

앞서 벡터의 내적의 결과가 0이 되는 경우를 판단해보았다. 

다시한번 코사인 그래프를 확인해 보면 다음과 같은 결과값이 나타남을 볼 수 있다. 

뭔가 알것 같지 않은가?

두 벡터의 내적의 결과가 양수라면 각도 범위가 -90도 ~ 90도 사이이고, 

음수라면 90 ~ 270도 사이 를 의미한다.

이것은 물체가 내가 바라보는 방향(양수 벡터)보다 뒤에 존재하고 있음을 나타낸다.

이것을 더욱 제한하여 시야각이라는 개념을포함한다면 전방위를 바라보는 것이 아닌 실제 사람이 바라보는 세상과 유사하게 시야를 표현하고 물체를 표현할 수 있게 된다.

그렇기 때문에 두 벡터의 내적의 결과가 1에서 점점 작아지다가 시야범위 벡터와 내적한 것보다 작아진다면 내 시야에서 벗어난 물체라고 판단 할 수 있게 된다.

예제에서 핵심 추가 코드는 다음과 같다.

타겟의 색상을 변경시켜줄 색상변수

// 게임 로직과 렌더링 로직이 공유하는 변수
LinearColor nonVisibleColor = LinearColor::Blue;
LinearColor targetColor = nonVisibleColor;
LinearColor visibleColor = LinearColor::Red;

 

플레이어가 바라보는 방향의 1/2에 해당하는 시야각

//	게임 로직의 로컬 변수
	static float halfFovCos = cosf(Math::Deg2Rad(fovAngle * 0.5f));

시야각 안에 들어왔을 때를 판단해주는 변수

// 게임 로직의 로컬 변수
// 물체의 최종 상태 설정

Vector2 f = Vector2::UnitY;
Vector2 v = (targetPosition - playerPosition).GetNormalize();
if (v.Dot(f) >= halfFovCos || v.Dot(f) <= -halfFovCos)
{
	targetColor = visibleColor;
}
else
{
	targetColor = nonVisibleColor;
}
playerPosition += deltaPosition;

최종 코드는 다음과 같다.

7-2 글이 작성됨에 따라 최종 코드가 변경될 수 있다.

#include "Precompiled.h"
#include "SoftRenderer.h"
#include <random>
using namespace CK::DD;

// 격자를 그리는 함수
void SoftRenderer::DrawGizmo2D()
{
	auto& r = GetRenderer();
	const auto& g = Get2DGameEngine();

	// 그리드 색상
	LinearColor gridColor(LinearColor(0.8f, 0.8f, 0.8f, 0.3f));

	// 뷰의 영역 계산
	Vector2 viewPos = g.GetMainCamera().GetTransform().GetPosition();
	Vector2 extent = Vector2(_ScreenSize.X * 0.5f, _ScreenSize.Y * 0.5f);

	// 좌측 하단에서부터 격자 그리기
	int xGridCount = _ScreenSize.X / _Grid2DUnit;
	int yGridCount = _ScreenSize.Y / _Grid2DUnit;

	// 그리드가 시작되는 좌하단 좌표 값 계산
	Vector2 minPos = viewPos - extent;
	Vector2 minGridPos = Vector2(ceilf(minPos.X / (float)_Grid2DUnit), ceilf(minPos.Y / (float)_Grid2DUnit)) * (float)_Grid2DUnit;
	ScreenPoint gridBottomLeft = ScreenPoint::ToScreenCoordinate(_ScreenSize, minGridPos - viewPos);

	for (int ix = 0; ix < xGridCount; ++ix)
	{
		r.DrawFullVerticalLine(gridBottomLeft.X + ix * _Grid2DUnit, gridColor);
	}

	for (int iy = 0; iy < yGridCount; ++iy)
	{
		r.DrawFullHorizontalLine(gridBottomLeft.Y - iy * _Grid2DUnit, gridColor);
	}

	ScreenPoint worldOrigin = ScreenPoint::ToScreenCoordinate(_ScreenSize, -viewPos);
	r.DrawFullHorizontalLine(worldOrigin.Y, LinearColor::Red);
	r.DrawFullVerticalLine(worldOrigin.X, LinearColor::Green);
}

// 게임 오브젝트 목록


// 최초 씬 로딩을 담당하는 함수
void SoftRenderer::LoadScene2D()
{
	// 최초 씬 로딩에서 사용하는 모듈 내 주요 레퍼런스
	auto& g = Get2DGameEngine();

}

// 게임 로직과 렌더링 로직이 공유하는 변수
float fovAngle = 60.f;	// 시야각	
Vector2 playerPosition(0.f, 0.f);	// 위치값
LinearColor playerColor = LinearColor::Gray;
Vector2 targetPosition(0.f, 100.f);	// 목표 위치값
bool isVisible = false;
LinearColor nonVisibleColor = LinearColor::Blue;
LinearColor targetColor = nonVisibleColor;
LinearColor visibleColor = LinearColor::Red;

// 게임 로직을 담당하는 함수
void SoftRenderer::Update2D(float InDeltaSeconds)
{
	// 게임 로직에서 사용하는 모듈 내 주요 레퍼런스
	auto& g = Get2DGameEngine();
	const InputManager& input = g.GetInputManager();

	// 게임 로직의 로컬 변수
	static float moveSpeed = 100.f;	
	//	랜덤 위치 변환 범위
	static std::random_device rd;
	static std::mt19937 mt(rd());
	static std::uniform_real_distribution<float> randomPosX(-300.f, 300.f);
	static std::uniform_real_distribution<float> randomPosY(-200.f, 200.f);
	
	static float duration = 3.f;
	static float elapsedTime = 0.f;
	static Vector2 targetStart = targetPosition;
	static Vector2 targetDestination = Vector2(randomPosX(mt), randomPosY(mt));

	static float halfFovCos = cosf(Math::Deg2Rad(fovAngle * 0.5f));
	elapsedTime = Math::Clamp(elapsedTime + InDeltaSeconds, 0.f, duration);

	// 지정한 시간이 경과하면 새로운 이동 지점을 랜덤하게 설정
	if (elapsedTime == duration)
	{
		targetStart = targetDestination;
		targetPosition = targetDestination;
		targetDestination = Vector2(randomPosX(mt), randomPosY(mt));

		elapsedTime = 0.f;
	}
	else // 비율에 따라 목표지점까지 선형보간하면서 이동
	{
		float ratio = elapsedTime / duration;
		targetPosition = Vector2(
			Math::Lerp(targetStart.X, targetDestination.X, ratio),
			Math::Lerp(targetStart.Y, targetDestination.Y, ratio)
		);
	}

	Vector2 inputVector = Vector2(input.GetAxis(InputAxis::XAxis), input.GetAxis(InputAxis::YAxis)).GetNormalize();
	Vector2 deltaPosition = inputVector * moveSpeed * InDeltaSeconds;

	// 물체의 최종 상태 설정

	Vector2 f = Vector2::UnitY;
	Vector2 v = (targetPosition - playerPosition).GetNormalize();
	if (v.Dot(f) >= halfFovCos || v.Dot(f) <= -halfFovCos)
	{
		targetColor = visibleColor;
	}
	else
	{
		targetColor = nonVisibleColor;
	}
	playerPosition += deltaPosition;
}

// 렌더링 로직을 담당하는 함수
void SoftRenderer::Render2D()
{
	// 렌더링 로직에서 사용하는 모듈 내 주요 레퍼런스
	auto& r = GetRenderer();
	const auto& g = Get2DGameEngine();

	// 렌더링 로직의 로컬 변수
	static float radius = 5.f;
	static std::vector<Vector2> sphere;
	static float sightLength = 300.f;

	if (sphere.empty())
	{
		for (float x = -radius; x <= radius; ++x)
		{
			for (float y = -radius; y <= radius; ++y)
			{
				Vector2 target(x, y);
				float sizeSquared = target.SizeSquared();
				float rr = radius * radius;
				if (sizeSquared < rr)
				{
					sphere.push_back(target);
				}
			}
		}
	}

	// 플레이어 렌더링. 
	// 플레이어 렌더링. 
	float halfFovSin = 0.f, halfFovCos = 0.f;
	Math::GetSinCos(halfFovSin, halfFovCos, fovAngle * 0.5f);

	r.DrawLine(playerPosition, playerPosition + Vector2(sightLength * halfFovSin, sightLength * halfFovCos), playerColor);
	r.DrawLine(playerPosition, playerPosition + Vector2(-sightLength * halfFovSin, sightLength * halfFovCos), playerColor);
	r.DrawLine(playerPosition, playerPosition + Vector2::UnitY * sightLength * 0.2f, playerColor);
	for (auto const& v : sphere)
	{
		r.DrawPoint(v + playerPosition, playerColor);
	}

	// 타겟 렌더링
	for (auto const& v : sphere)
	{
		r.DrawPoint(v + targetPosition, targetColor);
	}

	// 주요 정보 출력
	r.PushStatisticText(std::string("Player Position : ") + playerPosition.ToString());
	r.PushStatisticText(std::string("Target Position : ") + targetPosition.ToString());
}

// 메시를 그리는 함수
void SoftRenderer::DrawMesh2D(const class DD::Mesh& InMesh, const Matrix3x3& InMatrix, const LinearColor& InColor)
{
}

// 삼각형을 그리는 함수
void SoftRenderer::DrawTriangle2D(std::vector<DD::Vertex2D>& InVertices, const LinearColor& InColor, FillMode InFillMode)
{
}

모든 예제 코드의 소스파일은 

GitHub - onlybooks/gamemath: <이득우의 게임 수학> 공식 깃허브 페이지

 

GitHub - onlybooks/gamemath: <이득우의 게임 수학> 공식 깃허브 페이지

<이득우의 게임 수학> 공식 깃허브 페이지. Contribute to onlybooks/gamemath development by creating an account on GitHub.

github.com

또한 제 개인 깃허브 레포지토리 에 있습니다.

Workspace/C++/GameMath at main · cyphen156/Workspace · GitHub

 

Workspace/C++/GameMath at main · cyphen156/Workspace

Studying . Contribute to cyphen156/Workspace development by creating an account on GitHub.

github.com

※ 이득우 교수님의 인프런 게임수학강의를 참고하여 작성되었습니다.