cyphen156

게임 수학 4장 : 삼각함수(회전을 위한 수학) 본문

수학/게임수학

게임 수학 4장 : 삼각함수(회전을 위한 수학)

cyphen156 2023. 8. 18. 15:27

앞서 배웠던 벡터 공간에서 그려진 물체를 이동시킬때는 벡터의 덧셈과 곱셈을 이용해 직선 상의 움직임을 통해 표현하였습니다.  이제 4장에서는 벡터 공간에서의 물체의 회전 이동을 표현하기 위해 원을 그리고, 이 궤적을 따라 점(Point)을 이동시키기 위해 삼각함수에 대해 알아야 합니다.

삼각함수

고등학교때 배웠던 내용을 끄집어 내보면 뭐였는지 하나도 기억안나는 삼각비에 대한 내용 [얼싸안고(All, Sin, Tan, Cos)], 피타고라스의 정리(빗변과 끼인 각, 직각과의 관계)등이 있었던 것 같습니다.

삼각함수의 기본은 피타고라스의 정리입니다.

[각주:1]피타고라스의 정리는 직각삼각형을 이루는 세 변(빗변, 밑변, 높이)사이의 관계[밑변과 높이, 그리고 둘 사이의 끼인 각(90˚ / 직각)빗변 사이의 관계]에 대한 정리입니다.

삼각형

도형에서 존재하는 모든 각의 수가 3개이고, 이 각들의 합이 180˚(π)인 도형을 삼각형이라고 합니다.

삼각비

  • 빗변(Hypotenuse)
  • 밑변(Adjacent)
  • 높이(Opposite)

앞선 장에서 간단하게 피타고라스의 정리에 대해서 이야기하고, 피타고라스의 정리를 통해 이차원 벡터 공간에서의 특정 벡터의 크기를 계산했었습니다. 

이제 이 피타고라스의 정리를 응용하여 삼각비에 대해 배워야 합니다. 

이 그림을 데카르트 좌표계에서 반지름(r)이 1인 단위 원을 사용해서 나타내면 

다음과 같은 형태로 표시할 수 있습니다. 

여기서 c=n이라 가정하면 밑변 a의 길이는 ncosθ, 높이 b의 길이는 nsinθ가 됩니다. 

다시 말하면 단위 원에서는 sin²θ+cos²θ=1², n²sin²θ+n²cos²θ=n²라는 빗변의 길이 공식이 도출된다는 것입니다.

위에서 나왔던 삼각비를 약간 변형하면 다시 삼각형의 각 변의 길이를 구할 수 있습니다.

높이 b는 c * sinθ, 밑변 a는 c * cosθ, 빗변 c는 b / sinθ = a / cosθ로 나타낼 수 있습니다.

삼각함수의 성질

 

각도에 따른 sin, cos, tan그래프

출처 : 삼함수 - 위키백과, 우리 모두의 백과사전 (wikipedia.org)

여기서 π는 180˚라고 했었습니다. 기억해야 할 점은 라는 주기를 갖고 그래프가 반복되고 있으며, x축을 기준으로 cos그래프는 좌우 대칭을, sin과 tan그래프는 원점을 중심으로 대칭을 이루고 있다는 것, 그리고 sin과 cos그래프의 관계는 서로를 +90˚(π/2) 또는 270˚(π * 3/2)만큼 대칭 이동시킨 결과라는 것과 tan그래프는 예외적으로 π주기를 갖고 반복되고 있으며, 그 값이 순회하지 않는 극한에 수렴하는 값이라는 것과, 각 주기가 서로 만나지 않는다는 것(이어지지 않는 그래프)입니다.

정리하자면 다음과 같습니다.

  • π = 180˚
  • sin함수과 cos함수는 2π를 주기로 반복한다.
  • tan함수는 π를 주기로 반복한다.
  • sin함수과 cos함수는 항상 -1부터 1사이의 값을 갖는다. == [-1, 1]의 진폭을 갖는다.
  • tan함수는 π/2, π * 3/2의 각도에서 +-로의 극한값을 갖는다. 왜냐하면 분모가 0인 경우는 존재하지 않기 때문이다.
  • sin함수는 cos함수를 +π/2만큼 평행이동한 것과 같다.
  • cos함수는 x축을 기준으로 좌우 대칭이며, 이를 짝함수 또는 우함수라고 부른다.(cos(-θ) = cosθ)
  • sin함수와 tan함수는 원점(0, 0)을 기준으로 대칭이며, 이를 홀함수 또는 기함수라고 부른다. sin(-θ) = -sinθ
  • tanθ는 b/a 즉, sinθ / cosθ == (b/c) / (a/c)이다.

각의 측정

각도법

각도법은 원을 ˚(도)라는 단위를 이용해 360개로 쪼개어 표현하는 방법입니다.

왜 360개냐 하면 360이라는 수가 약수가 많이 나오는 수이기 때문에 사용했다고 합니다.

호도법

호도법은 360이라는 너무 큰 수가 아닌 일상생활에서 사용하기 편한 단위인 π(1radian = 180˚/π == 57.29....)를 기준으로 각을 측정하여 표시한 것입니다. 

물체의 회전

 벡터 공간에서의 물체가 θ만큼 회전했을 때 렌더링될 좌표의 연산은 단위 원을 기준으로 기저벡터를 활용하여 계산합니다.

여기에서 e1과 e2의 길이는 각각 (1*1 + 1*1) = √2이고, 좌표는 (1, 1), (-1, 1)입니다 여기서 e1을 회전하여 v'으로 이동시킨다고 할때 

단순히 회전만 시킨다고 보면 이런 그림이 될 겁니다. 

여기서 v'의 길이는 √2, 좌표는 (0, √2)가 될겁니다.

이 좌표를 도출하기 위한 수식은 

v-> = 1 * e1(1, 0) + 1*e2(0, 1),

v'-> = 1 * e'1(cosθ, sinθ) + 1*e'2(-sinθ, cosθ)가 될 겁니다.

다시말하면 v'->의 좌표는 (cosθ-sinθ, sinθ + cosθ)가 될 겁니다. 단위원을 기준으로 결론을 도출했으니 일반화 한 식

x' = xcosθ - ysinθ

y' = xsinθ + ycosθ이 됩니다.

삼각함수의 역수

삼각함수에는 역원이 존재합니다

  • sin의 역원 1/sin은 csc(코시컨트)
  • cos의 역원 1/cos는 sec(시컨트)
  • tan의 역원은 1/tan인데 tan은 cos/sin으로 표기할 수 있으니 sin/cos로 cot(코탄젠트)

삼각함수의 역함수

삼각함수의 역함수를 아크(arc)함수라고 부릅니다.

아크함수는 각 삼각함수마다 arcsin, arccos, arctan로 불립니다. 

하지만 삼각함수는 일정 주기마다 반복되기 때문에 전단사함수로 만들기 위해 범위를 제한하면 [-90˚, 90˚](sin, tan)또는 [0˚, 180˚](cos)사이의 값만을 공역으로, [-1, 1]치역으로 설정하여 역함수를 구한다면

arcsin, arccos, arctan

그런데 분모에는 0이 올 수 없으므로 역함수를 통해서 얻을 수 있는 각은 범위에 제한이 존재한다. 바로 3사분면에 위치한 값에 대한 정보는 얻을 수 없다는 것입니다.

그래서 acrtan함수를 응용하여 인자값에 y/x를 전달하는 것이 아닌 따로 분리하여 x와 y 두 인자를 전달하면 4사분면 전체 범위에 해당하는 각의 정보를 얻을 수 있습니다. 

즉, arctan함수의 치역의 범위를 [-90˚, 90˚]에서 [-180˚, 180˚]로 확장하여 두 인자를 각각 다른 그래프에 적용시켜 표현하는것 입니다.

이를 acrtan2(y, x) = atan2(y, x)

atan2함수

극좌표계

물체를 이동시키고 크기를 늘리는 동작은 x와 y가 독립적으로 적용되는 움직임인데 반해 회전은 두 변수가 함께 영향을 받는 동작입니다. 따라서 회전 움직임을 연산시 매번 x와 y의 변화를 계산하는 번거로움이 있습니다.

이걸 편하게 관리할 수 있지 않을까? 라는 고민에서 나온 결과가 극좌표계라는 체계입니다.

r은 x² + y²의 제곱근이고, θ는 atan2(y, x)입니다.

다음에 배울 내용은 행렬에 대한 내용입니다. 이산수학과 같은 과목에서도 배우겠지만 챕터에 있는 내용이고, 컴퓨터 공학의 핵심적인 내용을 다룰 수 있으므로 여기서도 나름대로 정리해보겠습니다.

실습하기

다음의 코드는 본문 내용중 CH4 삼각함수 : 회전을 위한 수학 내의 예제 코드 1~5까지를 모두 합친 코드입니다.

중간에 하트를 색칠하는 부분은 chatgpt4의 도움을 받아 완성하였습니다.

  1. Home/End버튼을 통해 하트그래프와 사각형 그래프를 "회전"시킬 수 있습니다
  2. 화살표 키를 입력하여 하트그래프를 평행이동 시킬 수 있습니다.
  3. 하트 그래프는 일정 주기를 반복하며 펌핑(커졋다/작아졋다)를 반복하고, 펌핑시 그래프 안의 영역이 색칠되거나 빈 공간으로 표현됩니다.
#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();

}

// 게임 로직과 렌더링 로직이 공유하는 변수
Vector2 currentPosotion;
float currentScale = 10.0f;
float currentDegree = 0.f;

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

	// 게임 로직의 로컬 변수
	static float moveSpeed = 100.f;
	static float scaleMin = 5.f;
	static float scaleMax = 20.f;
	static float scaleSpeed = 20.f;
	static float duration = 1.5f;
	static float elapsedTime2 = 0.f;
	static float rotateSpeed = 180.f;

	Vector2 inputVector = Vector2(input.GetAxis(InputAxis::XAxis), input.GetAxis(InputAxis::YAxis)).GetNormalize();
	Vector2 deltaPosition = inputVector * moveSpeed * InDeltaSeconds;
	float deltaScale = input.GetAxis(InputAxis::ZAxis) * scaleSpeed * InDeltaSeconds;
	float deltaDegree = input.GetAxis(InputAxis::WAxis) * rotateSpeed * InDeltaSeconds;

	elapsedTime2 += InDeltaSeconds;
	elapsedTime2 = Math::FMod(elapsedTime2, duration);
	float currentRad = (elapsedTime2 / duration) * Math::TwoPI;
	float alpha = (sinf(currentRad) + 1) * 0.5f;

	currentPosotion += deltaPosition;
	//currentScale = Math::Clamp(currentScale + deltaScale, scaleMin, scaleMax);
	currentScale = Math::Lerp(scaleMin, scaleMax, alpha);
	currentDegree += deltaDegree;
}

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

	// 배경에 격자 그리기
	DrawGizmo2D();

	// 렌더링 로직의 로컬 변수
	float rad = 0.f;
	static float increment = 0.001f;
	static std::vector<Vector2> hearts;
	HSVColor hsv(0.f, 1.f, 0.85f);

	// 시공의 폭풍 구현
	static float halfSize = 100.f;
	static std::vector<Vector2> squares;

	if (squares.empty())
	{
		for (float x = -halfSize; x <= halfSize; x += 0.25f)
		{
			for (float y = -halfSize; y <= halfSize; y += 0.25f)
			{
				squares.push_back(Vector2(x, y));
			}
		}
	}

	// 하트를 구성하는 점 생성
	if (hearts.empty())
	{
		for (rad = 0.f; rad < Math::TwoPI; rad += increment)
		{
			// 하트 방정식
			// x와 y를 구하기.
			// hearts.push_back(Vector2(x, y));
			float sin = sinf(rad);
			float cos = cosf(rad);
			float cos2 = cosf(2 * rad);
			float cos3 = cosf(3 * rad);
			float cos4 = cosf(4 * rad);
			float x = 16.f * sin * sin * sin;
			float y = 13 * cos - 5 * cos2 - 2 * cos3 - cos4;
			hearts.push_back(Vector2(x, y));
		}
	}
	// 0.5초 주기로 깜빡이기
	static float elapsedTime = 0.0f;
	static bool isVisible = true;

	elapsedTime += 1.0f / 60.0f; // 업데이트 빈도에 따라 조정 필요

	if (elapsedTime >= 0.1f)
	{
		isVisible = !isVisible;
		elapsedTime = 0.0f;
	}
	//각도에 해당하는 사인과 코사인값 얻기 
	float sin = 0.f, cos = 0.f;
	Math::GetSinCos(sin, cos, currentDegree);

	rad = 0.f;
	for (auto const& v : hearts)
	{
		rad += increment;
		// 1. 점에 크기를 적용
		Vector2 scaledV = v * currentScale;

		// 2. 크기가 변한 점을 회전
		Vector2 rotateV = Vector2(scaledV.X * cos - scaledV.Y * sin, scaledV.X * sin + scaledV.Y * cos);

		// 3. 회전시킨 점을 이동
		Vector2 translatedV = rotateV + currentPosotion;

		hsv.H = rad / Math::TwoPI;

		// 윤곽선 그리기 
		//r.DrawPoint(v * currentScale + currentPosotion, hsv.ToLinearColor());
		r.DrawPoint(translatedV, hsv.ToLinearColor());

		if (isVisible)  // 색칠할 부분만 isVisible 상태에 따라 그리기
		{
			Vector2 origin = Vector2(0, 0);
			Vector2 direction = (v - origin).GetNormalize();
			for (float t = 0; t <= 1; t += 0.01f)
			{
				Vector2 pointOnLine = origin + direction * t * v.Size();

				// 1. 점에 크기를 적용
				Vector2 scaledPoint = pointOnLine * currentScale;

				// 2. 크기가 변한 점을 회전
				Vector2 rotatedPoint = Vector2(scaledPoint.X * cos - scaledPoint.Y * sin, scaledPoint.X * sin + scaledPoint.Y * cos);

				// 3. 회전시킨 점을 이동
				Vector2 translatedPoint = rotatedPoint + currentPosotion;

				r.DrawPoint(translatedPoint, hsv.ToLinearColor());
			}
		}


		/*
		r.PushStatisticText(std::string("Position : ") + currentPosotion.ToString());
		r.PushStatisticText(std::string("scale : ") + std::to_string(currentScale));
		r.PushStatisticText(std::string("Degree : ") + std::to_string(currentDegree));
		*/
	}

	// 시공의 폭풍 구현하기
	// 현재 화면의 크기로부터 길이를 비교할 기준양 정하기
	static float maxLength = Vector2(_ScreenSize.X, _ScreenSize.Y).Size() * 0.5f;

	// 원을 구성하는 점을 그린다.
	for (auto const& v : squares)
	{
		// r극 좌표계 변경
		Vector2 polarV = v.ToPolarCoordinate();

		// 극좌표계의 각 정보로부터 색상을 결정한다.
		if (polarV.Y < 0.f)
		{
			polarV.Y += Math::TwoPI;
		}
		hsv.H = polarV.Y / Math::TwoPI;

		// 극좌표계의 크기 정보로부터 회전량 결정
		float ratio = polarV.X / maxLength;
		float weight = Math::Lerp(1.f, 5.f, ratio);

		polarV.Y += Math::Deg2Rad(currentDegree) * weight;

		// 최종 값을 데카르트 좌표계로 변환하기
		Vector2 cartesianV = polarV.ToCartesianCoordinate();
		r.DrawPoint(cartesianV, hsv.ToLinearColor());
	}
}

// 메시를 그리는 함수
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

※ 이미지는 게임수학 서적 내에서 발췌했습니다.

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

 

 

 

 

 

  1. 피타고라스 : 고대 그리스의 수학이자 철학 [본문으로]