ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 강의 과제) 삼각 함수, 역 삼각 함수
    TIL 2024. 6. 27. 23:45

     

     > 8차 강의 과제인 삼각 함수와 역 삼각 함수의 정의와 게임 개발에서 어떻게 사용되는지에 대해 간단하게 알아보았다.

     

    학습 키워드: trigonometry, sin, cos, tan, asin, acos, atan

     

    1. 삼각 함수와 역 삼각 함수

    1) 삼각 함수 (Trigonometric Function):

    Figure 1. right triangle

     삼각 함수는 삼각형의 한 각(angle)의 크기를 통해 두 변(sides)의 비율을 구하는 함수, 줄여서 삼각비를 구하는 함수를 말한다. 가장 기본이 되는 것은 직각 삼각형을 이용한 정의다. 북미권에서는 이를 처음 알려줄 때 SOH CAH TOA(쏘-카-토아 라고 읽음)라는 문구로 배우며, 각각 SOH는 Sine = Opposite / Hypotenuse, CAH는 Cosine = Adjacent / Hypotenuse, TOA는 Tangent = Opposite / Adjacent를 뜻한다. 위 figure는 opposite과 adjacent가 각 A를 기준으로 적혀있는데, 실제로는 adjacent는 각과 직각 사이에 붙어있는 변, opposite은 각의 반대편에 있는 변, 그리고 hypotenuse는 빗변을 뜻한다. 예를 들면, sin(A) = a / h, cos(B) = a / h, cos(A) = b / h, tan(B) = b / a 가 되는 식이다.

     

    2) 역 삼각 함수 (Inverse Trigonometic Function):

     역 삼각 함수는 삼각 함수의 inverse, 즉 삼각비를 통해 각을 도출해내는 함수를 의미한다. 보통 sin-1(x) 같은 형태로 표현되는데, 이는 1 / sin(x) 하고는 다르다. 전자는 arcsin로 역 삼각 함수지만, 후자는 단순히 sin의 역수(reciprocal)이며  secant 라고 부르고 sec으로 표기한다. cos의 역수는 csc, tan의 역수는 cot으로 표기하며, 이 세 가지 역수 또한 inverse 인 arcsec, arccsc, arccot이 존재한다.

     

    3) Unity에서의 삼각 함수:

     Unity 엔진에서는 UnityEngine의 CoreModule에 속한 Mathf 에서 삼각 함수를 다룬다. 아래는 공식 문서에서 제공하는 Mathf.Cos()의 예시 코드다.

    using UnityEngine;
    using System.Collections;
    
    public class PolyDrawExample : MonoBehaviour
    {
        public int numberOfSides;
        public float polygonRadius;
        public Vector2 polygonCenter;
    
        void Update()
        {
            DebugDrawPolygon(polygonCenter, polygonRadius, numberOfSides);
        }
    
        // Draw a polygon in the XY plane with a specfied position, number of sides
        // and radius.
        void DebugDrawPolygon(Vector2 center, float radius, int numSides)
        {
            // The corner that is used to start the polygon (parallel to the X axis).
            Vector2 startCorner = new Vector2(radius, 0) + center;
    
            // The "previous" corner point, initialised to the starting corner.
            Vector2 previousCorner = startCorner;
    
            // For each corner after the starting corner...
            for (int i = 1; i < numSides; i++)
            {
                // Calculate the angle of the corner in radians.
                float cornerAngle = 2f * Mathf.PI / (float)numSides * i;
    
                // Get the X and Y coordinates of the corner point.
                Vector2 currentCorner = new Vector2(Mathf.Cos(cornerAngle) * radius, Mathf.Sin(cornerAngle) * radius) + center;
    
                // Draw a side of the polygon by connecting the current corner to the previous one.
                Debug.DrawLine(currentCorner, previousCorner);
    
                // Having used the current corner, it now becomes the previous corner.
                previousCorner = currentCorner;
            }
    
            // Draw the final side by connecting the last corner to the starting corner.
            Debug.DrawLine(startCorner, previousCorner);
        }
    }

     Mathf.Sin(), Mathf.Cos(), Mathf.Tan()이 삼각 함수, Mathf.Asin(), Mathf.Acos(), Mathf.Atan()이 역 삼각 함수인데, tangent의 경우 Atan2()라는 함수가 하나 더 있다. Atan() 함수에서 연산할 때 0으로 나뉘어 exception을 던지는 경우가 있기 때문에 존재하는 함수이며, Atan()과 다르게 0으로 나뉜 경우에도 올바른 값을 출력하도록 설계되어 있다.


    4) Node.js에서의 삼각 함수:

     Node.js에서는 Math라는 내장 객체의 method들을 통해 삼각 함수와 역 삼각 함수를 사용할 수 있다. 메서드 이름은 Math.sin() 처럼 모두 소문자로 작성하고, 유니티와 동일한 이유로 atan2()가 존재한다는 점만 기억하면 된다.

     

    2. 게임 프로그래밍에서의 삼각 함수

    1) Usage:

     삼각 함수는 게임 프로그래밍에서 정말 많이 사용되는 가장 핵심이 되는 요소 중 하나다. 단순히 캐릭터가 움직인 이후에 있어야 할 위치부터, 타겟팅 스킬의 투사체 위치, 물리 객체에 힘이 가해지는 방향, 특정 대상을 바라보기 위한 방향각 계산, FoV나 LoS 값의 연산, 심지어 애니메이션의 ease-in, ease-out 이펙트 등에도 사용된다.

     

     

    2) Unit Circle:

     앞서 삼각 함수의 정의와 Unity, Node.js에서 어떻게 사용하는지를 간단하게 알아봤는데, 그렇다면 실제 게임 개발에서는 삼각 함수가 어떠한 역할을 맡게 될까? 이에 대해 알기 전에 우선 단위원 (Unit Circle)이라는 개념에 대해 알아두면 좋다.

    Figure 2. unit circle (Wikipedia)

     단위원이란 좌표 평면에서 원점이 중심이고 반지름 길이가 1인 원을 의미한다. Figure 2를 보면 sin(θ) = opp / hyp 인데, x축이 기준인 단위원에서의 hyp 값은 항상 1으로, sin(θ) = opp / 1 = opp 이며 sin(θ) 값이 곧 opp 의 값이 된다. 마찬가지로 cos(θ) 또한 adj / hyp = adj / 1 = adj 이며 adj의 값이 항상 cos(θ)의 값이 된다.

     

     이것만 봤을 때는 삼각 함수가 대체 게임 프로그래밍이랑 무슨 상관인가 싶을 수 있으나, 좌표 평면이 2D Top-down view 게임의 캐릭터 위치를 나타낸다고 생각하고 보면 이해가 쉽다. 처음 위치인 점O에서 다음 위치인 점A 까지 1만큼 이동한다고 가정했을 때 y 좌표의 변화량 dy가  sin(θ) 값이며 x 좌표의 변화량 dx가 cos(θ) 값이 된다.

     

     

    3) Usage in game server:

     단위원 개념이 사용되는 대표적인 예시로 멀티 플레이 중 latency 차이로 인한 위치 정보 불일치를 해소하기 위해 서버가 캐릭터의 다음 위치를 추측하여 보내주는 latency 기반의 추측 항법이 있는데, 2D게임이라고 가정했을 때 유저의 원래 위치인 x, y 좌표에서 유저가 어느 방향으로 얼만큼 이동해야 하는지에 대한 정보를 알아내야 한다. 유저 캐릭터가 이동하는 방향은 클라이언트의 입력 값을 통해 얻어내게 되는데, 예를들어 키보드의 방향키로 이동하는 2D 게임의 경우 오른쪽 방향키를 누르면 x = 1, 왼쪽을 누르면 x = -1, 가만히 있으면 x = 0 이 되며, 마찬가지로 위 아래 방향키가 y 값을 결정하게 된다.

     

     이 input 정보를 토대로 방향을 알아낼 수 있는데, 여기서 사용되는 것이 바로 atan2 함수다. 위의 단위원 이미지를 다시 참고하면, 우리가 원하는 방향 값은 결국 θ 값을 의미하는데, 단위원에서 tan(θ) 값은 sin(θ) / cos(θ) 값과 같다고 설명한 바 있다. 또한 단위원의 sin(θ)와 cos(θ) 값은 결국 θ 각도의 방향으로 1 만큼의 거리를 이동할 시 최종 위치 값과 현재 위치 값의 차이이므로, 각각 좌표 값의 변화량인 dy, dx를 의미하게 된다. 따라서 클라이언트로부터 받은 input x y값으로 arc tangent를 적용하면 θ 값을 얻을 수 있다. 다시 앞의 2D 게임 예시로 설명하면 tan(θ) = sin(θ) / cos(θ) = dy / dx, 즉 atan(dy / dx) 값이 θ 값이 된다. 여기서 dx의 값이 0이 되는(좌우 방향으로 움직이지 않는) 경우가 있기 때문에 dy / dx 값 때문에 에러가 발생하게 되는데, 이 문제를 해결한 함수가 atan2라고 설명한 바 있다. 따라서 JS 기준으로 Math.atan2(dy, dx) 를 사용하면 θ 값을 얻을 수 있다.

     

    Figure 3. 이해를 돕기 위해 제작한 단위원 이미지

     

     각도(방향)을 알았다면 이제 얼만큼 이동해야 하는지도 알아야 한다. 앞서 atan 함수에 사용한 dy dx 값은 실제 이동할 dy dx 값이 아니라 인풋 값을 기준으로 한 dy dx로, 각도 계산만을 위해 사용된 값이다. 그렇다면 현재 상태에서 알고 있는 값은 유저 캐릭터가 움직일 방향인 각θ, 그리고 실제로 해당 방향으로 이동했을 때의 dy, dx인 sin(θ) 와 cos(θ) 다. 위 이미지에서 주황색으로 displacement를 작성해 두었는데, displacement는 distance (거리) 값에 방향이 적용된 vector 값을 의미하며, 이는 우리가 최종적으로 얻고싶어하는 (x + dx, y + dy) 좌표 값을 의미한다. 그럼 이 거리 값은 어떻게 구하느냐? 이 또한 유저 캐릭터가 얼마나 빠르게 이동하고 있는지(속도)에 대한 정보를 전달받고, 이 방향으로 몇 초간 움직일 것인지(시간)에 대한 정보를 알아야 한다. 멀티플레이어 게임의 경우 속도 정보 또한 DB에서 저장되고 있을테니 서버에서 쉽게 알아낼 수 있고, 시간 정보의 경우 latency 기반의 추측 항법 적용이므로 접속 중인 유저들의 latency 평균 혹은 최댓값을 이용하여 알아낼 수 있다. 따라서 거리 값은 거리 = 속도 * 시간 공식을 통해 계산하기만 하면 된다.

     

     이제 displacement의 dy dx 계산에 필요한 정보가 다 있다. 비율을 통한 연산을 하면 끝인데, displacement의 dy를 기준으로 설명하면 다음과 같다.

    Figure 4. dy 구하기

     

     dx에도 동일한 방식을 적용할 수 있고, Javascript로 구현한다면 아래와 같은 형태가 된다.

    // javascript example
    
    const time = maxLatency / 1000; // latency in seconds
    const distance = speed * time;
    
    const theta = Math.atan2(inputY, inputX);
    
    const dx = Math.cos(theta) * distance;
    const dy = Math.sin(theta) * distance;

     

     단위원의 dx와 dy (inputX와 inputY)를 이미 알고있다면 그 자체로 이미 방향을 의미하기 때문에 atan2를 통한 각도 계산이 필요 없이 해당 값을 cos와 sin 대신에 사용하기도 한다.

     

     

    4) Notes:

     추가로, 한 개의 위치만을 연산하면 되는 클라이언트와 달리 서버의 경우 여러 개의 유저 캐릭터의 위치를 연산해야 하는 문제가 있다. 매 프레임마다 서버에 위치 변화에 대한 요청을 보내게 되면 유저가 조금만 늘어나도 서버가 뻗어버릴 것이다. 이는 클라이언트에게도 문제가 되는데, 매 프레임마다 네트워크 요청을 보내는 것 또한 엄청난 리소스 부담이기 때문이다. 때문에 클라이언트에서는 특정 숫자 만큼의 프레임 혹은 몇 초 마다 한 번, 그리고 방향 전환마다 요청을 보내도록 하며, 서버 또한 클라이언트가 짧은 시간에 너무 많은 요청을 보내는 경우 처리를 거부하도록 설정한다. 아마 나중에 따로 자세히 다루게 될 것 같은데, 이를 rate limitting이라고 한다.

     

     

    --

     

    REFERENCES:

     

    https://ko.wikipedia.org/wiki/%EC%82%BC%EA%B0%81%ED%95%A8%EC%88%98

     > Wikipedia, "삼각함수", accessed: 2024-06-27, Figure 1, 2.

    https://allenchou.net/2019/08/trigonometry-basics-sine-cosine/

     > "Game Math: Trigonometry Basics" by Ming-Lun "Allen" Chou, accessed: 2024-06-27

    https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Mathf.html

     > Unity, "Scripting API: Mathf", version: 2022.3, accessed: 2024-06-27

    728x90
Designed by Tistory.