이전 글에서 다루었던 이 책에 처음 내용은 기초 필수 수학으로 시작한다. 그 내용 중에서도 가장 먼저 등장하는 것이 벡터이다. 유니티에서 벡터는 굉장히 중요하고 자주 다루는 개념이다. 위치를 나타내거나 이동, 회전, 다양한 부분에 사용된다.
책에서는 벡터의 개념이나 기본적인 연산 방법, 단위 벡터, 내적, 외적, 직교화, 위치 벡터, 마지막으로 DirectXMath 라이브러리의 벡터에 대한 내용을 다룬다. 기본적이고 찾아보면 나오는 내용들이기 때문에 따로 정리하지는 않고, 이런 내용들을 보면서 생각한 내용과 추가로 찾아본 것들에 대해서 적어보고자 한다.
같은 벡터라도 기준계가 다르면 좌표 표현이 달라진다는 것이다.
...
우리가 어떤 벡터를 좌표로 규정하거나 식별할 때 그 좌표가 절대적인 수치들이 아니라 항상 어떤 기준계에 상대적인 수치이고, 3차원 컴퓨터 그래픽에서는 여러 기준계들을 사용하는 경우가 많으므로, 벡터를 다룰 때에는 주어진 벡터의 좌표가 현재 어떤 기준계에 상대적인지를 기억할 필요가 있다. 또한, 한 기준계에서의 벡터 좌표를 다른 기준계로 변환하는 방법도 알아야 한다.
먼저 이 내용을 보면서 유니티의 월드 좌표와 로컬 좌표, 그리고 스크린 좌표가 떠올랐다. 월드 좌표는 월드 내에서 물체의 절대적인 위치를 나타내고, 로컬 좌표는 객체의 부모를 기준으로 한 좌표를 나타낸다. 스크린 좌표는 화면 내에서 표현되는 좌표를 나타낸다. 다른 좌표계도 있을지는 모르겠는데, 주로 이 3가지를 많이 이용하는 것 같다. 분류를 나누기 위한 객체들의 부모는 특별한 이유가 없으면 (0, 0, 0)의 좌표로 설정해주는 것은 지금와서는 자연스러운 행동이지만 이런 것들을 염두에 두고 하는 행동인 셈이다. 스크린 좌표는 보통 UI를 나타낼 때 많이 사용하는 것 같은데, 월드 좌표로 치환하기 위한 ScreenToWorldPoint 등의 함수도 유니티에서 지원한다. 자주 사용하진 않지만 로컬 좌표를 월드 좌표를 변환하기 위한 TransformPoint라는 함수도 있다.
_moveDirection = new Vector3(_inputX, 0, _inputZ).normalized;
그리고 벡터의 정규화에 대한 내용을 보면서 유니티에서 벡터의 정규화를 어떻게 하는지 궁금해졌다. 이전에도 이동 속도를 항상 동일하게 맞추기 위해서 등의 이유로 벡터를 정규화하곤 했다. 여기서 normalized가 어떻게 구현되어있는지를 살펴보았다.
// 요약:
// Returns this vector with a magnitude of 1 (Read Only).
public Vector3 normalized
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return Normalize(this);
}
}
Vector3의 경우, 내부 함수인 Normalize를 호출하는 것을 알 수 있었다.
public static Vector3 Normalize(Vector3 value)
{
float num = Magnitude(value);
if (num > 1E-05f)
{
return value / num;
}
return zero;
}
public static float Magnitude(Vector3 vector)
{
return (float)Math.Sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z);
}
Normalize 함수를 살펴보면 이렇다. Magnitude 함수를 이용하여 Vector의 크기를 구해주고, 전달받은 Vector의 value를 Vector의 크기로 나눈다. 위에서 본 식과 동일한 방법을 이용하고 있는 셈이다. 단, 이 과정에서 Vector의 크기가 1E-05f보다 커야 한다. 1E-05f는 0.00001를 다르게 표현한 것이다.
What’s wrong with Vector3.normalized, or my understanding of it? - Unity Answers
What’s wrong with Vector3.normalized, or my understanding of it? - Unity Answers
answers.unity.com
포럼을 살펴보면 이에 관한 내용이 나와있다. 내용을 정리하자면 부동소수점 오류로 인해 두 벡터의 값이 일정값 이상 차이가 나지 않으면 같다고 정의를 해놓은 것이라고 볼 수 있겠다. 실제로 유니티의 인스펙터에서 벡터 값을 15로 변경하면 15.0000001이라거나 14.99999999 등이 되어있는 모습을 자주 볼 수 있다. 따라서 이런 예외를 둔 것으로 보인다.
이 정규화에서 추가로 든 생각은, normalized는 결국 Normalize 함수를 호출하는데, 한 번 더 과정을 거치기 때문에 바로 Normalize 함수를 호출하는 것이 성능 면에서 더 좋지 않을까? 라는 생각이었다.
using UnityEngine;
public class VectorTest : MonoBehaviour
{
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
Vector3 testVector1 = new();
Vector3 testVector2 = new();
float runTimeNormalized;
float runTimeNormalize;
const long REPEAT_TIME = 10000000000;
private void Start()
{
Debug.Log("Calculating Normalized Time: " + REPEAT_TIME);
stopwatch.Start();
ExecuteNormalized(REPEAT_TIME);
stopwatch.Stop();
runTimeNormalized = stopwatch.ElapsedMilliseconds;
Debug.Log("Calculating Normalize Time: " + REPEAT_TIME);
stopwatch.Restart();
ExecuteNormalize(REPEAT_TIME);
stopwatch.Stop();
runTimeNormalize = stopwatch.ElapsedMilliseconds;
Debug.Log("Normalized: " + runTimeNormalized + "ms, Normalize: " + runTimeNormalize + "ms");
}
private void ExecuteNormalized(long repeat)
{
for (long i = 0; i < repeat; ++i)
{
testVector1 = testVector2.normalized;
}
}
private void ExecuteNormalize(long repeat)
{
for (long i = 0; i < repeat; ++i)
{
testVector1 = Vector3.Normalize(testVector2);
}
}
}
실제로 각각 100억번씩 호출하고 소요되는 시간을 확인해보았다.
normalized를 호출한 경우는 161017ms, Normalize를 호출한 경우는 145271ms가 소요되었다.
Normalize를 바로 호출하는 것이 성능상으로 약 9.8%정도 빨랐다. 아마 Normalize에서도 Magnitude함수를 호출하지 않고 바로 계산한다면 성능을 더 향상시킬 수 있을 것이다. 하지만 이렇게 하지 않는 이유는 아마 각각의 함수가 하는 역할들을 나눠놓기 위함이 아닐까 싶다. 이런 방법을 통한 성능 향상은 분명 있겠지만, 굉장히 미미하다. 내 컴퓨터에서 사용한 경우, normalized 대신에 Normalize를 1번 사용하면 약 0.0000015746ms라는 성능 상의 이점이 생긴다. 1초에 60프레임 이라고 생각하면, 매 프레임마다 10000번 호출한다고 가정했을 때 1초(1000ms)에 0.94476ms 정도를 절약할 수 있다.
솔직히 실질적인 성능 상의 이점은 거의 없다고도 말할 수 있을 것 같다. 그래도 normalized 대신에 Normalize를 사용하는 것이 크게 어려운 것은 아니기 때문에 앞으로는 Normalize를 사용하도록 노력해야겠다. 만약 혹시라도 호출이 극단적으로 저렇게 많이 늘어나는 경우에는 함수를 커스텀해서 사용함으로써 추가로 성능 향상을 도모해볼 수도 있겠다.
유니티 Vector3 new 는 스택에 생성된다 :: 3DMP (tistory.com)
유니티 Vector3 new 는 스택에 생성된다
http://cafe.naver.com/unityhub/25942 아래 개미개발자님의 코드 스타일의 최적화 이슈의 문제에 질답이 오가던 중, 메테오님의 지적으로 그동안 제가 잘 못 알고 있었던게 있었더군요. 그래서 많은 분들
3dmpengines.tistory.com
또, 위에서 Vector3를 사용할 때 new를 호출할 때마다 가비지가 생성되는 것은 아닌가? 하는 생각에 찾아보면서 새롭게 알게된 것이 있다. 결론을 먼저 말하면 Vector3는 new로 생성해도 가비지가 생기지 않는다. Vector3는 구조체이고, C#에서 구조체는 new로 할당해도 힙이 아니라 스택에 생성되기 때문이라고 한다. 직접 프로파일러를 이용하여 테스트를 해보니 확실히 new string()을 호출할 때는 가비지가 생겼지만, new Vector3()의 경우는 가비지가 생기지 않는 것을 확인할 수 있었다.
오늘은 책을 통해 벡터에 관한 개념을 다시 복습하고, 새로 알게 된 지식으로부터 생긴 궁금증을 2가지 해결할 수 있었다. 유니티에서 정규화를 하는 방법은 특별하지 않고 일반적인 방법을 사용하며, normalized로 정규화를 하는 것보다는 Normalize로 정규화를 하는 것이 정말 미세하게지만 성능이 조금 더 좋다. 그리고 Vector는 구조체이고, C#에서 구조체는 new로 할당해도 힙이 아닌 스택에 생성되기 때문에 가비지가 생기지 않는다는 것이다. 그리고 프로파일러의 존재는 알고 있었지만 사용해본 것은 처음인 것 같다. 앞으로도 종종 사용하면서 익숙해질 필요가 있을 것 같다.
마지막으로, new Vector()를 사용해도 가비지가 생성되지 않는다는 것을 새롭게 알게 되면서 여전히 가비지 컬렉터에 대한 이해가 모자란 부분이 있다고 느꼈다. 내일은 가비지와 가비지 컬렉터에 대해서 더 자세히 알아봐야겠다.
'개발 > 공부' 카테고리의 다른 글
메모리 관리에 대한 탐구 (3) - 가비지 컬렉터(C#) (0) | 2023.05.15 |
---|---|
메모리 관리에 대한 탐구 (2) - 가비지란 무엇인가? (스택 영역/힙 영역, 값 형식/참조 형식) (1) | 2023.05.13 |
모바일 게임과 PC 게임의 차이 (0) | 2023.05.04 |
Race Condition, 동기화, 임계 영역, 스핀락, 뮤텍스, 세마포어 (0) | 2023.05.02 |
게임 네트워킹의 이해 3 - Socket Programming, MMORPG 서버 구조 (0) | 2023.05.01 |