-05 삼각함수
어찌된 게 개념원리보다 설명을 잘해논 것 같다.
기억왜곡인가.
3장에서는 벡터 공간의 덧셈과 곱셈의 연산을 활용해 벡터의 움직임을 구현했다.
4장에서는 3장에서 표현할 수 없었던 회전에 대해 살펴본다.
회전은 원의 궤적을 따라 이동하는 움직임이기 때문에 이를 이해하려면 원과 밀접하게 연결되어 있는 삼각함수를 알아야 한다.
삼각함수
빗변 밑변 높이
한 각이 직각인 직각삼각형을 이루는 세 변은 각 위치에 따라 빗변(직각의 대변), 밑변, 높이라고 부른다.
삼각비
직각 삼각형을 구성하는 세 변에서 두 변을 뽑아 각각의 비례관계를 나타낸 것을 삼각비라고 한다.
삼각비에는 여러 종류가 있지만 사인 코사인 탄젠트가 제일 대표적이다.
밑변의 길이를 a, 높이의 길이를 b, 빗변의 길이를 c, 빗변과 밑변과의 사잇각을 O(세타기호라고 생각하자)라고 할 때, 각 삼각비의 관계는 다음과 같이 분수식으로 표현할 수 있다.
sin O = b/c
cos O = a/c
tan O = b/a
삼각함수
직각삼각형에서 측정할 수 있는 사잇각은 0도보다 크거나 90도보다 작아야 한다.
직각삼각형을 데카르트 좌표계 상에 배치하고 사잇각의 범위를 R 집합(실수 전체)로 확장한 대응 관계를 삼각함수라고 한다.
가장 많이 사용하는 삼각함수인 sin 함수와 cos 함수의 개념은 직각삼각형에서 출발했다.
그러나 원점을 중심으로 반지름이 1인 평면 위의 단위 원Unit circle을 사용해 나타내면 좀 더 쉽게 파악할 수 있다.
원의 반지름의 길이는 1이므로 이 빗변의 길이는 항상 1이다.
그리고 x축과 해당 빗변이 이루는 각을 사잇각(O(세타기호라고 생각))으로 지정하자.
그런 다음에는 빗변에서 x축으로 수직선을 내려 직각삼각형을 그려보자.
이 직각삼각형으로부터 삼각비를 계산할 수 있을 것이다.
빗변 c의 길이가 1이므로 삼각비 sinO의 값은 높이 b와 같고 cosO 값은 밑변 a와 같다.
따라서 데카르트 좌표계에서 빗변이 가리키는 원의 좌표는 (cosO, sinO)로 표현할 수 있는데, 이를 삼각함수로 확장하면 원주(원 둘레) 위의 모든 좌표는 (cosO, sinO)에 대응한다고 할 수 있다.
그렇다면 밑변 a의 x좌표는 cos O가 되고 높이 b의 y좌표는 sin O가 되는데 이를 피타고라스 정리(a^2 + b^2 = c^2)에 대입하면 다음과 같은 공식을 얻을 수 있다.
cos ^2 O + sin ^2 O = 1
이번에는 반지름 길이를 r로 일반화시켜 생각해보자.
반지름이 r인 원 안에서 그은 직각삼각형의 빗변은 벡터의 개념으로 보았을 때 길이가 1인 벡터와 평행하다.
길이는 r배만큼 증가했으므로 스칼라곱에 의해 r * (cos O, sin O)라는 좌표를 갖게 된다.
이로부터 빗변의 길이가 r인 직각삼각형 밑변의 길이는 r * cos O가 되고 높이의 길이는 r * sin O가 됨을 알 수 있다.
앞서 구한 식은 cos ^2 O + sin ^2 O = 1은 따라서 반지름의 길이와 무관하게 성립함을 알 수 있다.
r^2 cos ^2 O + r^2 sin ^2 O = r^2
이 식은 삼각함수의 기본을 이루는 중요한 공식으로, 이후 회전과 관련된 계산에 매우 유용하게 사용된다.;
삼각함수의 성질
진폭과 주기
데카르트 좌표계에서 각도는 x축에서 원의 궤적을 따라 반시계 방향으로 회전한 크기를 의미한다.
반지름이 1인 단위원에서 반시계 방향의 회전을 생각해보자.
아직 회전하지 않아 x축 상에 위치한 빗변 v의 좌표는 (1, 0)인데, 이 각도는 0도에 대응한다고 할 수 있다.
따라서 각도 0도에 대한 sin 함수와 cos 함수의 값은 다음과 같다.
v = (vx, vy) = (cos 0, sin 0) = (1, 0)
sin 0 = 0, cos 0 = 1
이제부터 각도를 0도에서 90도까지 서서히 증가시키면서 회전하는 빗변의 좌표 vx와 vy의 변화를 살펴보자.
각도가 증가할수록 vx 값은 감소하고 vy 값은 증가한다.
그리고 목적지인 90도에 도달하면 y축 상에 위치한 좌표 (0, 1)과 일치하는 벡터가 만들어진다.
x 값과 y값의 변화를 추적하면 그림 4-7과 같은 부드러운 곡선이 만들어진다.
각도가 90도를 넘어서면 vx 값은 0을 지나 음수가 되고, vy 값은 다시 0을 향해 감소하기 시작한다.
계속해서 한바퀴에 해당하는 360도까지 빗변의 좌표 변화를 계속 관찰하면, 그림 4-8과 같이 -1에 도달할 때까지 계속 감소하다가 -1에 도달하면 방향을 바꿔서 1을 향해 증가하며, 1에 도달하면 다시 -1을 향해 감소하는 패턴을 반복한다.
이러한 값의 변화는 [-1, 1] 범위 내에서 360도마다 반복되는데, 변화 값의 범위를 진폭Amplitude
반복되는 각도를 주기Period라고 한다.
sin 함수와 cos 함수의 특징
그림 4-9의 그래프로부터 sin 함수와 cos 함수의 성질을 정리하면 다음과 같다.
- sin 함수와 cos 함수는 항상 -1에서 1 사이를 일정하게 반복하는 패턴을 띤다.
- sin 함수와 cos 함수의 값은 360도 주기로 반복된다.
- y축을 기준으로 좌우를 접어 포갰을 때 cos 함수 그래프는 좌우 대칭인 반면 sin 함수 그래프는 상하가 반전된 원점 대칭의 형태를 띤다. cos 함수와 같이 좌우 대칭의 성질을 가진 함수를 짝함수Even function 또는 우함수라고 부르며, sin 함수와 같이 원점 대칭의 성질을 가진 함수를 홀함수Odd function 또는 기함수라고 부른다.
특히 3번에서 언급한 sin 함수와 cos 함수 그래프가 지니는 홀함수와 짝함수의 성질은 다음 식과 같이 정리할 수 있다.
이 역시 향후 회전에 관련된 계산에 유용하게 사용되므로 숙지해야한다.
cos -O = cos O
sin -O = -sinO
tan 함수의 특징
이번에는 tan 함수의 특징을 알아보자.
tan 함수는 빗변과 무관하게 밑변과 높이의 관계만을 나타낸다.
이 식의 분자와 분모를 모두 빗변 값으로 각각 나누면 다음 수식과 같이 cos과 sin으로 tan 함수를 표현할 수 있다.
tan O = (b/c) / (a/c) = sin O / cos O
그림 4-10은 tan 함수의 그래프며 sin 함수와 동일하게 홀함수의 성질을 지님을 알 수 있다.
호도법
우리는 일상 생활에서 각의 크기를 잴 때 0에서 360까지의 수를 사용하는 각도법을 사용한다.
각도법에서 기준으로 삼는 360이라는 수는 약수가 많아 원을 다양한 방식으로 쪼개 활용할 수 있기 때문이다.
그러나 이는 일상생활에서의 편리를 위한 것일 뿐, 360은 표준으로 사용되기에는 너무 크다.
각도법 대신 호의 기준을 기준으로 각을 측정하는 법을 호도법Radian이라고 부른다.
호도법은 호의 길이가 1이 되는 부채꼴의 각을 기준으로 각을 측정한다.
단위원의 반원 호 길이는 단위 벡터의 길이 1보다 대략적으로 3.14배 더 큰데, 정확한 값을 구할 수는 없다.
바로 이것이 원주율 파이다.
위의 원에서 호의 길이가 1인 부채꼴의 중심각은 몇 도인지 생각해보자.
이 부채꼴의 각이 바로 호도법에서 사용하는 각의 기준인 1rad이다.
1라디안은 각도로 환산하면 약 57.2958도가 되며 이도 역시 무리수다.
위에 정리된 사실을 바탕으로 다음 식이 성립함을 알 수 있다.
pi(rad) = 180
이 수식을 응용해 다음의 변환식을 만들 수 있다.
1 = pi / 180 (rad)
1(rad) = 180 / pi
삼각함수로 하트 그리기 4-1
다음과 같이 좌표 x와 y에 각각 진폭과 주기가 다른 삼각함수를 결합한 수식을 사용하고, 수식에 0부터 2pi까지의 각을 대입해 나온 좌표마다 점을 찍으면 데카르트 좌표계에서 예쁜 하트를 표현할 수 있다.
x = 16 sin ^ 3 O
y = 13 cos O - 5 cos 2O - 2 cos 3O - cos 4O
일명 하트 방정식이라고도 불리는 이 식을 이용해 하트를 그리는 프로그램을 만들어보자.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | // 게임 로직과 렌더링 로직이 공유하는 변수 Vector2 currentPosition; float currentScale = 10.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; //입력에 따른 변화속도 Vector2 vectorInput = Vector2( input.GetAxis(InputAxis::XAxis), input.GetAxis(InputAxis::YAxis) ).GetNormalize(); Vector2 posDelta = vectorInput * moveSpeed * InDeltaSeconds; float deltaScale = input.GetAxis(InputAxis::ZAxis) * scaleSpeed * InDeltaSeconds; // 현재 프레임에서 Z축 입력에 따른 크기의 변화량을 계산한다. //물체의 최종상태 설정 currentPosition += posDelta; currentScale = Math::Clamp(currentScale + deltaScale, scaleMin, scaleMax); //최종 크기 값에 변화량을 반영하되 Clamp 함수를 사용해 지정한 최댓값과 최솟값 사이를 넘지 못하도록 한다. } // 렌더링 로직을 담당하는 함수 void SoftRenderer::Render2D() { // 렌더링 로직에서 사용하는 모듈 내 주요 레퍼런스 auto& r = GetRenderer(); const auto& g = Get2DGameEngine(); // 배경에 격자 그리기 DrawGizmo2D(); // 렌더링 로직의 로컬 변수 float rad = 0.f; static float increment = 0.001f; static std::vector<Vector2> heart; // 하트를 구성하는 점 생성 if (heart.empty()) { for (rad = 0.f; rad < Math::TwoPI; rad += increment) { //하트방정식 float sin1 = sinf(1 * rad); float cos1 = cosf(1 * rad); float cos2 = cosf(2 * rad); float cos3 = cosf(3 * rad); float cos4 = cosf(4 * rad); float x = 16.f * sin1 * sin1 * sin1; float y = 13 * cos1 - 5 * cos2 - 2 * cos3 - cos4; heart.push_back(Vector2(x, y)); } } for (auto const& v : heart) { r.DrawPoint(v * 10.f, LinearColor::Blue); } } | cs |
두근대는 하트 애니메이션 만들기 4-3
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 | // 게임 로직을 담당하는 함수 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 duration = 1.5f; static float timeElapsed = 0.f; Vector2 inputVector = Vector2(input.GetAxis(InputAxis::XAxis), input.GetAxis(InputAxis::YAxis)).GetNormalize(); Vector2 deltaPosition = inputVector * moveSpeed * InDeltaSeconds; // 경과 시간과 sin 함수를 활용한 [0, 1] 값의 생성 timeElapsed += InDeltaSeconds; //현재 경과시간 갱신 timeElapsed = Math::FMod(timeElapsed, duration); // 나머지 함수를 사용해 현재 경과시간이 duration 값을 넘으면 0으로 초기화 float radCurrent = (timeElapsed / duration) * Math::TwoPI; // 현재 시간에 대응되는 각 계산 float alpha = (sinf(radCurrent) + 1) * 0.5f; // radCurrent에 대한 sin 함수 값을 계산한다. // 물체의 최종 상태 설정 currentPosition += deltaPosition; currentScale = Math::Lerp(scaleMin, scaleMax, alpha); } | cs |
삼각함수를 활용한 물체의 회전
물체를 이동시키거나 크기를 늘리는 동작은 수직인 x축과 y축이 서로 독립적으로 적용된다.
따라서 x축과 y축을 분리해 따로따로 계산한 후 두 결과를 결합한 것과 동일하다.
하지만 회전이라는 동작은 x와 y값이 함께 영향을 미치기 때문에 그림 4-19처럼 x축과 y축을 분리해 독립적으로 계산할 수 없다. 회전을 구현하기 위해 기저벡터의 개념을 활용해본다.
우리가 사용하는 실벡터 공간 R2는 두 표준기저벡터 e1, e2를 기저로 둔 공간이고, 공간에 속한 모든 벡터는 e1과 e2의 선형 결합에 의해 생성된다.
회전을 위해 실벡터 공간 R2 전체를 각 O만큼 회전시켜본다.
그러면 두 표준기저벡터 e1과 e2의 좌표는 다음 그림과 같이 변화될 것이다.
표준기저벡터 e1이 각 O만큼 회전한 좌표는 그림 4-5에서 확인했듯이 (cos O, sin O)가 된다.
이를 다음과 같이 e1' 로 표시하자.
e1' = (cos O, sin O)
그렇다면 이번에는 표준기저벡터 e2가 각 O만큼 변한 좌표를 확인해보자.
그림 4-20을 관찰해보면 e1'의 y좌표만큼 음의 x값을 가지고, x좌표만큼 양의 y값을 가지고 있음을 알 수 있다.
이를 e2'으로 표시하면 좌표는 다음과 같다.
e2' = (-sin O , cos O)
그렇다면 실벡터 공간 R2의 벡터가 각 O만큼 회전하면 어떻게 변화되는지 수식으로 확인해보자.
이를 위해 좌표 (1, 1)의 값을 가진 벡터 v를 사용해보겠다.
회전되기 전의 벡터 v는 표준기저벡터 e1과 e2를 사용해 다음의 선형 결합식으로 표현할 수 있다.
v = 1 * e1 + 1 * e2
여기서 벡터 v가 각 O만큼 회전한 벡터 v'은 위 선형 결합식의 e1과 e2를 회전된 표준기저벡터 e1'과 e2'로 치환해 얻을 수 있다.
v' = 1 * (cos O, sin O) + 1 * (-sin O, cos O)
따라서 좌표 (1, 1)이 각 O만큼 회전한 벡터 v'의 좌표는 다음과 같이 얻을 수 있다.
v' = (cos O - sin O, sin O + cos O)
실벡터공간 R2의 두 표준기저벡터를 e1, e2라고 할 때 각 O만큼 회전한 벡터 u는 다음의 수식으로 정리할 수 있다.
u = (x, y) = x * e1 + y * e1 = x * (1, 0) + y * (0, 1)
u' = (x', y') = x * (cos O, sin O) + y * (-sin O, cos O)
= (x cos O - y sin O, x sin O + y cos O)
따라서 임의의 벡터 (x, y)가 각 O만큼 회전한 결과 (x', y')은 다음과 같다.
x' = x cos O - y sin O
y' = x sin O + y cos O
삼각함수에 대해서는 따로 찾아봐야 할 것 같다..
고등학교 때 수학을 놓았어서 자세한 내용 찾아보고 암기해야 할 것 같다.
삼각함수의 역함수
지금까지 삼각함수를 사용해 주어진 각에 대응하는 벡터의 좌표를 얻는 방법을 알아보았다.
그런데 게임 제작 과정에서는 거꾸로 주어진 벡터의 좌표로부터 이에 대응하는 각도를 얻어내는 작업도 필요하다.
이를 계산하려면 삼각함수의 역함수와 이에 대한 성질을 알아야 한다.
임의의 각 x에 대응되는 sin 함수는 다음과 같이 표기할 수 있다.
y = f(x) = sin(x)
만일 공역의 범위를 실수 집합 전체가 아닌 [-1, 1] 구간으로 한정해 정의한다면, sin 함수는 전사함수의 성질을 띤다.
여기서 정의역의 범위를 좁혀 정의역의 한 요소가 공역의 한 요소에 대응되도록 전단사 함수를 만들어보자.
이 때 정의역의 범위는 [-90, 90] 구간이 된다.
이렇게 어떤 함수를 전단사 함수로 만든다면 저저번 포스팅에서 배운 내용에 따라 sin x값이 주어졌을 때 거꾸로 각 x를 구할 수 있는 역함수가 존재하게 된다.
이와같이 정의역과 공역의 범위를 제한시켜 얻은 sin 함수의 역함수를 arcsin(아크사인) 함수라고 부른다.
위는 아크사인함수 그래프
이와 동일하게 cos 함수의 역함수를 구하기 위해 cos 함수가 전단사 함수가 되도록 정의역과 공역을 제한해보자.
전단사함수가 되기 위한 cos 함수의 정의역은 [0, 180] 공역은 [-1, 1]이다.
이로부터 생성한 역함수를 arccos(아크코사인) 함수라고 한다.
arccos 함수의 그래프는 다음 그림과 같다.
마지막으로 tan 함수의 역함수를 구할 차례다.
tan 함수의 역함수를 구하기 위해서는 tan 함수가 전단사함수가 되어야 한다고 했다.
tan 함수의 치역은 실수 영역 전체인 데 반해, 정의역이 존재하지 않는 구간이 존재하는 것이 뭄ㄴ제다.
따라서 tan 함수의 정의역 구간을 한정해야 할 것이다.
tan 함수는 x 값이 -90도와 90도일 때의 y값이 존재하지 않으므로 전단사함수가 되기 위한 정의역 구간은 -90과 90 값을 제외한 (-90, 90)이 되어야 한다.
arctan 함수는 벡터의 각도를 구하는 데 유용하게 사용된다.
임의의 벡터를 v = (x, y)라고 할 때 분수식 y/x를 계산해 벡터로부터 tan 함수 값을 얻을 수 있다.;
이 tan 값을 arctan 함수에 넣으면 해당 벡터가 x축과 이루는 사잇각을 얻어낼 수 있다.
그런데 arctan 함수의 치역은 (-90, 90)구간이므로 arctan로 얻을 수 있는 각의 범위에는 한계가 있다.
이는 arcsin 함수와 arccos 함수도 동일한 상황이다.
하지만 arctan 함수의 경우, 인자에 분수 y/x 값을 넣지 않고 x와 y의 두 값을 분리해 전달하면 4사분면 전체에 해당하는 각의 정보를 얻을 수 있다.
3사분면에서 x값의 부호가 음이고 y값의 부호도 음인 경우를 생각해보자.
그런데 분수값 y/x의 결과는 양수기 때문에, 이를 arctan 함수에 전달하면 3사분면의 정보는 사라지고 1사분면에 해당하는 각의 정보가 나와 처음에 사용한 벡터를 얻어낼 수 없다.
하지만 arctan 함수에 x와 y 두 값을 따로 전달한다면 두 값이 가지는 부호를 파악해 벡터가 1사분면에 있었는지 3사분면에 있었는지를 파악할 수 있다.
그래서 arctan 함수에는 분수값 y/x를 계산해 전달하는 함수와 x와 y를 따로 전달하는 함수의 두 종류가 있다.
x와 y를 따로 전달하는 arctan 함수를 atan2 함수라고 부르며, 이는 분수 y/x를 연상하기 쉽게 y값에 이어 x값을 전달하도록 설계되어 있다. atan2를 사용하면 평면의 모든 사분면에 대응하는 각도를 얻을 수 있다.
극좌표계
앞선 절에서 물체를 이동시키고 크기를 놀리는 동작은 x와 y가 독립적으로 적용되는 움직임인 데 반해, 회전은 x와 y가 함께 영향받는 동작이라고 설명했다.
따라서 데카르트 좌표계로 회전을 구현하면 회전에 따른 x와 y의 변화를 매번 계산하는 번거로움이 발생한다.
회전 동작을 기반으로 설계된 좌표계를 고안해 사용한다면 이로부터 편리하게 회전을 관리하고 구현할 수 있을 것이다.
이를 위해 고안된 좌표계가 바로 극좌표계Polar coordinate system이다.
극좌표계는 원점으로부터의 거리 r과 각O의 두 요소로 구성되며 극좌표계의 좌표는 (r, O)로 표시한다.;
극좌표계는 다음 그림과 같이 동심원의 형태로 평면의 모든 점을 표현하며, 주로 시간에 따른 회전의 움직임을 구현하거나 회전에 관련된 효과를 연출할 때 활용된다.
데카르트 좌표계로 표현된 벡터 (x, y)는 벡터의 크기와 arctan 함수를 사용해 다음과 같이 극좌표계로 변환될 수 있다.
r = 루트(x^2 + y^2)
O = atan2(y, x)
반대로 극좌표계의 좌표 (r, O)를 데카르트 좌표계 (x, y)로 변환하는 식은 삼각함수를 사용해 구할 수 있다.
x = r * cos O
y = r * sin O
극좌표계가 지니는 원형 공간의 성질은 회전에 관련된 여러 효과를 줄 때 유용하게 사용된다.
예를 들어 스킬아이콘의 쿨타임 효과는 대표적인 극좌표계의 활용 예다.
주어진 벡터의 각 정보를 알려주는 atan2 함수가 왜 인자를 두 개 받을 수밖에 없는지 설명할 수 있다면, 삼각함수에 대한 이해가 한 단계 높아진 자신을 발견할 수 있을 것이다.