Home [Unity] 구면 좌표계로 3인칭 카메라 움직임 구현하기
Post
Cancel

[Unity] 구면 좌표계로 3인칭 카메라 움직임 구현하기

0. 서론

3인칭 게임에서 대부분의 카메라는 타켓을 중심으로 하는 구 형태를 그리면서 움직이고 있습니다.

이러한 카메라 움직임을 구현하는 방법은 다양합니다.

그중에서 “구 형태”를 그린다는 것에 집중하여 구면 좌표계로 구현해 보면 어떨까? 란 의문이 들었고

이를 직접 구현해 보면서 작성한 글입니다.


1. 구면 좌표계 구현

구면 좌표계에 대한 설명과 코드에 대한 설명은 아래 블로그를 통해 확인하시면 됩니다.

구면 좌표계로 어쌔신 크리드 동기화 연출 구현해보기

전체 코드는 아래와 같습니다.

  • Input Value에 따라 방위각, 앙각 값이 가감되어야 하므로
    AddAzimuth(float degree)와 AddElevation(float degree) 메서드를 추가했습니다.

  • 방위각(Φ, Azimuth)은
    좌 또는 우 한 방향으로 계속 회전할 수 있도록 하기 위해서 CYCLE_IN_RAD 상수를 추가했습니다.

    • CYCLE_IN_RAD: 360도를 라디안으로 변경한 값
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
using UnityEngine;

public class SphericalCoordinate
{
    float _radius, _minRadius, _maxRadius;
    float _azimuthInRad, _minAzimuthInRad, _maxAzimuthInRad;           // 방위각
    float _elevationInRad, _minElevationInRad, _maxElevationInRad;     // 앙각
    
    const float ONE_CYCLE_IN_RAD = 360f * Mathf.Deg2Rad;

    public SphericalCoordinate((float min, float init, float max) radius,(float min,float init, float max) azimuth, (float min, float init, float max) elevation)
    {
        _minRadius = radius.min;
        _maxRadius = radius.max;
        SetRadius(radius.init);

        _minAzimuthInRad = azimuth.min * Mathf.Deg2Rad;
        _maxAzimuthInRad = azimuth.max * Mathf.Deg2Rad;

        _minElevationInRad = elevation.min * Mathf.Deg2Rad;
        _maxElevationInRad = elevation.max * Mathf.Deg2Rad;

        SetAzimuth(azimuth.init * Mathf.Deg2Rad);
        SetElevation(elevation.init * Mathf.Deg2Rad);
    }

    public float GetAzimuth() => _azimuthInRad;

    public void SetAzimuth(float degree)
    {
        float value = degree * Mathf.Deg2Rad;

        if (value <= 0f)
        {
            value += ONE_CYCLE_IN_RAD;
        }

        if (value > ONE_CYCLE_IN_RAD)
        {
            value -= ONE_CYCLE_IN_RAD;
        }

        _azimuthInRad = Mathf.Clamp(value, _minAzimuthInRad, _maxAzimuthInRad);
    }

    public void AddAzimuth(float degree)
    {
        float addValue = _azimuthInRad + degree * Mathf.Deg2Rad;

        /**
         * 좌, 우 한쪽 방향으로 계속 회전할 수 있도록 처리
        */
        if (addValue <= 0f)
        {
            addValue += ONE_CYCLE_IN_RAD;
        }

        if (addValue > ONE_CYCLE_IN_RAD)
        {
            addValue -= ONE_CYCLE_IN_RAD;
        }

        _azimuthInRad = Mathf.Clamp(addValue, _minAzimuthInRad, _maxAzimuthInRad);
    }

    public float GetElevation() => _elevationInRad;

    public void SetElevation(float degree)
    {
        _elevationInRad = Mathf.Clamp(degree * Mathf.Deg2Rad, _minElevationInRad, _maxElevationInRad);
    }

    public void AddElevation(float degree)
    {
        _elevationInRad = Mathf.Clamp(_elevationInRad + degree * Mathf.Deg2Rad, _minElevationInRad, _maxElevationInRad);
    }

    public void SetRadius(float value)
    {
        _radius = Mathf.Clamp(value, _minRadius, _maxRadius);
    }

    public void AddRadius(float value)
    {
        _radius = Mathf.Clamp(_radius + value, _minRadius, _maxRadius);
    }

    public Vector3 ToCartesianPos()
    {
        return new Vector3(
            _radius * Mathf.Sin(_elevationInRad) * Mathf.Cos(_azimuthInRad),
            _radius * Mathf.Cos(_elevationInRad),
            _radius * Mathf.Sin(_elevationInRad) * Mathf.Sin(_azimuthInRad));
    }
}

2. 카메라 구현

변수 선언

구면좌표계를 사용하는 카메라 움직임 컴포넌트에서 필요한 것은 아래와 같습니다.

  • 방위각(azimuth) 최소, 최대 각도
  • 앙각(elevation) 최소, 최대 각도
  • 반지름 길이(radius)
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
[Header("Target")]
[SerializeField]
Transform _target;

[Header("Radius")]
[SerializeField]
float _minRadius = 2f;
[SerializeField]
float _maxRadius = 10f;

[Header("Mouse X(Degree)")]
[SerializeField]
float _minMouseXDegree;
[SerializeField]
float _maxMouseXDegree = 360f;

[Header("Mouse Y(Degree)")]
[SerializeField]
float _minMouseYDegree = 45f;
[SerializeField]
float _maxMouseYDegree = 135f;

/**
* 중간 생략...
*/

// 반지름 길이
float GetRadius()
{
    return Vector3.Distance(this.transform.position, _target.position);
}

구면 좌표계 인스턴스 설정

구면 좌표계 클래스의 인스턴스 또한 필요하고 Awake()에 초기화시켜 줍니다.

ValueTuple 값은 (min, init, max)입니다.
init 값은 일단 임시로 작성했습니다.

1
2
3
4
5
6
7
8
9
10
11
SphericalCoordinate _sphericalCoordinate;

private void Awake()
{
    _sphericalCoordinate = 
        new SphericalCoordinate(
            (_minRadius, GetRadius(), _maxRadius), 
            (_minMouseXDegree, 0f ,_maxMouseXDegree), 
            (_minMouseYDegree, 90f, _maxMouseYDegree)
            );
}

Mouse Input Data 처리

여기서  마우스 X, Y의 Input 값을 구면 좌표계 값으로 변환하여 처리하는 코드를 추가하면 끝입니다.

[CameraMovement.cs]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void LateUpdate()
{
    _sphericalCoordinate.AddAzimuth(GetMouseXDegree());
    _sphericalCoordinate.AddElevation(GetMouseYDegree());

    this.transform.position = _target.position + _sphericalCoordinate.ToCartesianPos();
}

float GetMouseXDegree()
{
    float inputValue = Input.GetAxis("Mouse X") * -1;

    return inputValue;
}

float GetMouseYDegree()
{
    float inputValue = Input.GetAxis("Mouse Y");

    return inputValue;
}

[SphericalCoordinate.cs]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 public void AddAzimuth(float degree)
{
    float addValue = _azimuthInRad + degree * Mathf.Deg2Rad;

    /**
     * 좌, 우 한쪽 방향으로 계속 회전할 수 있도록 처리
    */
    if (addValue <= 0f)
    {
        addValue += ONE_CYCLE_IN_RAD;
    }

    if (addValue > ONE_CYCLE_IN_RAD)
    {
        addValue -= ONE_CYCLE_IN_RAD;
    }

    _azimuthInRad = Mathf.Clamp(addValue, _minAzimuthInRad, _maxAzimuthInRad);
}

public void AddElevation(float degree)
{
    _elevationInRad = Mathf.Clamp(_elevationInRad + degree * Mathf.Deg2Rad, _minElevationInRad, _maxElevationInRad);
}

Zoom In / Out

마우스 휠 Input 값을 통해 Radius 값을 변경하여 쉽게 구현할 수 있습니다.

[CameraMovement.cs]

1
2
3
4
5
6
7
8
9
10
11
12
private void LateUpdate()
{
    // 다른 코드는 생략
    _sphericalCoordinate.AddRadius(GetZoomInOut());
}
    
float GetZoomInOut()
{
    float inputValue = Input.GetAxis("Mouse ScrollWheel") * -1;

    return inputValue;
}

[SphericalCoordinate.cs]

1
2
3
4
5
// class SphericalCoordinate
public void AddRadius(float value)
{
    _radius = Mathf.Clamp(_radius + value, _minRadius, _maxRadius);
}

[Debug] Gizmos

구면좌표계가 잘 적용되는지 확인을 위해 Gizmos도 추가해 주었습니다

1
2
3
4
5
6
7
8
9
private void OnDrawGizmos()
{
    if(_target != null)
    {
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(_target.position, 
            Mathf.Clamp(GetRadius(),_minRadius,_maxRadius));
    }
}

3. 적용 결과

위 내용을 모두 적용하면 아래와 같은 카메라 움직임을 구현할 수 있습니다.

구면좌표계로 변경하다 보니 시작 방위각, 앙각이 정확하게 맞지 않는 현상이 있으나

Unity Editor Script나 다른 방법을 통해 시작 값을 지정해 주면 간단하게 해결할 수 있습니다.

This post is licensed under CC BY 4.0 by the author.

[C++] r-value 참조

[Unity] Behavior Tree(BT) 만들어보기