태초에 벡터가 있었다

등록일: 2014. 10. 16

시작하세요! 안드로이드 게임 프로그래밍

  • 마리오 제흐너 지음
  • 유윤선 옮김
  • 832쪽
  • 36,000원
  • 2011년 09월 30일

앞 장에서는 벡터를 위치와 혼동해서는 안 된다고 언급한 바 있다. 이 말은 꼭 맞는 말은 아니다. 어떤 공간에서는 벡터를 통해 위치를 표현할 수 있기 때문이다. 실제로 벡터는 다양하게 해석할 수 있다.

  • 위치: 위치는 이전 장들에서 좌표계의 원점을 기준으로 엔티티의 좌표를 인코딩할 때 사용한 바 있다.
  • 속도와 가속도: 이들 값은 물리적인 양으로 다음 절에서 자세히 언급한다. 보통 속도와 가속도는 단일 값으로 생각하기 쉽지만 실제로는 2D 또는 3D 벡터로 표현한다. 속도와 가속도는 객체의 속력을 나타낼 뿐 아니라(예를 들어 시속 100킬로미터로 달리는 차) 객체가 이동하는 방향도 나타낸다. 이와 같은 벡터 해석값은 원점을 기준으로 한 값이 아니라는 점에 주의하자. 이 말은 차의 속력과 방향이 위치와 무관하다는 점을 생각하면 이해하기 더 쉽다. 차가 북서쪽 방향으로 고속도로에서 직진으로 시속 100킬로미터로 이동한다고 가정하자. 차의 속력과 방향이 변하지 않는 한 속도 벡터도 변하지 않는다.
  • 방향과 거리: 방향은 속도와 유사하지만 일반적으로 물리적인 양이 결여돼 있다. 이런 벡터 해석값은 이 객체가 남동쪽을 향한다 등의 정보를 나타날 때 사용한다. 거리는 한 위치가 다른 위치와 얼마만큼 어느 방향으로 떨어져 있는지를 알려준다.

그림 8-1은 이들 해석값이 사용되는 상황을 보여준다.

그림 8-1 | 위치, 속도, 방향, 거리를 벡터로 표현한 Bob

물론 그림 8-1은 모든 경우를 다 다룬 것은 아니다. 벡터는 이보다 훨씬 다양한 해석값을 가질 수 있다. 하지만 게임 개발을 대상으로 할 때는 이 정도 기본 해석값만 사용하더라도 충분하다.

그림 8-1에서 빠진 내용 중 하나는 벡터가 사용하는 단위다. 벡터의 단위는 항상 민감하게 사용해야 한다(예를 들어 Bob의 속도를 초당 미터 단위로 움직인 거리로 표현해, 초당 왼쪽으로 2미터, 위로 3미터 움직인다고 할 수 있다). 마찬가지로 위치와 거리도 이를테면 미터로 표현할 수 있다. 하지만 Bob의 방향은 조금 특별하다. 단위가 없기 때문이다. 단위가 없는 이런 방향 벡터는 방향의 물리적인 특성을 별도로 유지한 채 객체의 일반적인 방향을 지정하려고 할 때 편리하다. Bob의 속도와 관련해 속도의 방향을 방향 벡터로 저장하고 속력을 단일 값으로 저장하면 이와 같은 표현을 할 수 있다. 단일 값은 스칼라라고 부른다. 이 경우 방향 벡터는 잠시 후 설명하겠지만 길이가 1이어야 한다.

벡터의 활용

벡터는 쉽게 수정하고 조합할 수 있다는 점에서 매우 유용하다. 하지만 이 작업을 하려면 먼저 벡터를 표현하는 방법부터 정의해야 한다.

v = (x,y)

이 부분은 어렵지 않았을 것이다. 이미 앞에서 수도 없이 했기 때문이다. 모든 벡터는 2D 공간에서 x, y 요소를 갖고 있다(이 장에서도 2차원을 다룬다). 다음과 같이 벡터는 서로 더할 수도 있다.

c = a + b = (a.x, a.y) + (b.x, b.y) = (a.x + b.x, a.y + b.y)

이때는 벡터 요소를 서로 더해 최종 벡터를 구하면 된다. 예제 8-1에 나와 있는 벡터를 사용해 이를 계산해 보자. 예를 들어 Bob의 위치가 p = (3,2)이고 여기에 속도 벡터 v = (–2,3)을 더해보자. 그럼 다음과 같은 새로운 위치를 얻을 수 있다 p' = (3 + –2, 2 + 3) = (1,5). 이때 p 오른쪽 위에 있는 어퍼스트로피는 신경 쓰지 않아도 된다. 이 표시는 단지 새로운 p 벡터를 구했음을 나타내는 표시일 뿐이다. 물론 이 연산은 위치와 방향의 벡터 단위가 서로 일치할 때만 말이 된다. 여기서는 위치가 미터(m)이고 속도가 초당 미터(m/s)라고 가정하므로 벡터 단위가 정확히 일치한다.

물론 벡터의 뺄셈도 가능하다.

c = a – b = (a.x, a.y) – (b.x, b.y) = (a.x – b.x, a.y – b.y)

이때도 두 벡터의 요소를 서로 합치기만 하면 된다. 하지만 벡터의 뺄셈을 감행할 때는 벡터를 빼는 순서가 중요하다. 예를 들어 그림 8-1에서 제일 우측에 있는 그림을 살펴보자. 녹색 Bob은 pg = (1,4)에 있고 빨간색 Bob은 pr = (6,1)에 있다. 여기서 pg와 pr은 각각 녹색 위치와 빨간색 위치를 나타낸다. 녹색 Bob에서 빨간색 Bob의 거리 벡터를 빼면 다음과 같은 계산이 나온다.

d = pg – pr = (1, 4) – (6, 1) = (-5, 3)

그런데 결과가 좀 이상하다. 이 벡터는 빨간색 Bob에서 녹색 Bob을 가리키는 벡터이기 때문이다. 녹색 Bob에게서 빨간색 Bob으로의 방향 벡터를 구하려면 뺄셈의 순서를 반대로 해야 한다.

d = pr – pg = (6, 1) – (1, 4) = (5, -3)

위치 a에서 위치 b로의 방향 벡터를 구하려면 다음과 같은 일반 공식을 사용해야 한다.

d = b – a

다시 말해 항상 끝 위치에서 시작 위치를 빼야 하는 것이다. 이런 공식은 처음에는 조금 헷갈리지만 좀 더 자세히 생각해 보면 이치에 맞음을 알 수 있다. 모눈 종이에서 직접 한번 시험해 보자.

벡터는 스칼라만큼 곱할 수도 있다(스칼라는 단일 값이라는 사실을 기억하자).

a' = a * scalar = (a.x * scalar, a.y * scalar)

이때는 벡터의 각 요소를 스칼라만큼 곱해준다. 이렇게 하면 벡터의 길이 스케일을 조절할 수 있다. 그림 8-1의 방향 벡터를 예로 들어 살펴보자. 이 벡터는 d = (0,–1)로 지정했다. 이 벡터에 s = 2 스칼라를 곱하면 벡터의 길이를 두 배로 만들 수 있다 d × s = (0,–1 × 2) = (0,–2). 물론 1보다 작은 스칼라를 사용하면 길이를 더 줄일 수도 있다. 예를 들어 d에 s = 0.5를 곱하면 새로운 벡터는 d' = (0,–0.5)가 된다.

길이 얘기가 나온 김에 설명하자면 벡터의 길이도 (주어진 단위를 사용해) 계산할 수 있다.

|a| = sqrt(a.x*a.x + a.y*a.y)

이때 |a| 표기는 이 값이 벡터의 길이를 나타냄을 뜻한다. 학창시절 선형 대수 수업 시간에 졸지 않았다면 벡터의 길이를 구하는 공식을 알고 있을 것이다. 이 공식은 2D 벡터에 피타고라스 공식을 적용한 것이다. 벡터의 x, y 요소는 오른쪽 삼각형의 두 면을 형성하고 세 번째 면은 벡터의 길이가 된다. 그림 8-2는 이를 보여주는 그림이다.

그림 8-2 | 피타고라스도 벡터를 사랑했을 것이다.

벡터의 길이는 루트의 특성상 항상 양수 또는 0이다. 이를 빨간색과 녹색 Bob 사이의 거리 벡터에 적용하면 다음과 같이

|pr – pg| = sqrt(5*5 + -3*-3) = sqrt(25 + 9) = sqrt(34) ~= 5.83m

두 Bob이 서로 떨어져 있음을 알 수 있다(Bob의 위치가 모두 미터로 주어졌다고 가정한다). 길이는 벡터의 방향과 무관하므로 |pg – pr|를 계산할 때는 항상 같은 값에 도달한다는 사실에 주의하자. 이 사실은 또 다른 사실을 암시하는데, 바로 벡터에 스칼라를 곱하면 이에 따라 길이가 바뀐다는 것이다. 본래 길이가 1 단위인 d = (0,–1) 벡터가 있을 때 이 벡터에 2.5를 곱하면 길이가 2.5 단위인 새로운 벡터를 얻게 된다.

앞에서는 방향 벡터가 보통 연계된 단위를 전혀 갖지 않는다고 설명한 바 있다. 하지만 방향 벡터에 스칼라를 곱하면 단위를 갖게 할 수 있다. 예를 들어 d = (0,1) 벡터에 속력 상수 s = 100 m/s를 곱하면 속도 벡터 v = (0 × 100,1 × 100) = (0,100)를 얻게 된다. 따라서 방향 벡터는 항상 길이를 1로 지정하는 게 좋다. 길이가 1인 벡터는 단위 벡터라고 부른다. 임의의 벡터를 길이로 나누면 단위 벡터를 얻을 수 있다.

d' = (d.x/|d|, d.y/|d|)

|d|는 벡터 d의 길이를 나타낸다는 사실을 기억하자. 이를 한번 시험해 보자. 예를 들어 정확히 북동쪽을 향하는 방향 벡터 d = (1,1)이 필요하다고 가정하자. 얼핏 보기에는 두 벡터 요소 모두 1이므로 이 벡터는 이미 단위 벡터처럼 보인다. 하지만 이는 잘못된 생각이다.

|d| = sqrt(1*1 + 1*1) = sqrt(2) ~= 1.44

이 벡터를 단위 벡터로 만들면 문제를 쉽게 해결할 수 있다.

d' = (d.x/|d|, d.y/|d|) = (1/|d|, 1/|d|) ~= (1/1.44, 1/1.44) = (0.69, 0.69)

이를 벡터 정규화라고 부른다. 벡터 정규화는 벡터가 길이 값 1을 갖게 만듦을 뜻한다. 이 트릭을 사용하면 예를 들어 거리 벡터로부터 단위 길이 방향 벡터를 생성할 수 있다. 물론 0 길이 벡터에 대해서는 주의해야 하다. 이 경우 0으로 벡터를 나누게 되기 때문이다.

간단한 삼각함수 이론

이번에는 잠깐 삼각함수를 들여다 보자. 삼각함수에는 핵심이 되는 두 가지 함수가 있다. 바로 코사인과 사인 함수다. 두 함수 모두 단일 인자로 각도를 받는다. 우리는 각도를 45° 또는 360°처럼 표시하는 것에 익숙하다. 하지만 대부분의 수학 라이브러리에서 삼각함수 함수는 라디언으로 각도 값을 받는다. 다음 공식을 사용하면 각도와 라디언을 손쉽게 변환할 수 있다.

degreesToRadians(angleInDegrees) = angleInDegrees / 180 * pi
radiansToDegrees(angle) = angleInRadians / pi * 180

여기서 pi는 대략 3.14159265 값을 갖는 상수다. pi 라디언은 180도이므로 이로부터 앞의 공식을 도출할 수 있다.

그럼 각도가 주어졌을 때 코사인과 사인이 실제로 계산하는 값은 무엇일까? 사인과 코사인은 원점을 기준으로 단위 길이 벡터의 x, y 요소를 계산한다. 그림 8-3은 이를 잘 보여준다.

그림 8-3 | 코사인과 사인은 끝점이 단위 원에 머무는 단위 벡터를 만든다.

따라서 각도가 주어지면 다음과 같이 단위 길이 방향을 쉽게 계산할 수 있다.

v = (cos(angle), sin(angle))

반대로 x축을 기준으로 벡터의 각도를 계산할 수도 있다.

angle = atan2(v.y, v.x)

atan2 함수는 실제로는 인공 구조체다. 이 함수는 아크 탄젠트 함수(이 함수는 삼각함수의 또 다른 기본 함수인 탄젠트 함수의 역함수다)를 사용해 -180도부터 180도 사이의 각도를 생성한다(또는 각도가 라디언으로 반환될 경우 –pi부터 pi). 이런 내부 로직은 조금 복잡하며 책의 설명에서도 크게 중요하지 않다. 이 함수의 인자는 벡터의 y, x 요소다. 이때 atan2 함수가 제대로 동작하기 위해 벡터가 꼭 단위 벡터일 필요는 없다는 사실에 주의하자. 또 대부분의 경우 y 요소가 먼저 주어지고 x 요소가 나중에 주어진다는 점도 주의하자. 이는 사용하는 수학 라이브러리에 따라 다른데 이로 인해 많은 오류가 생기기도 한다.

간단한 예제를 몇 개 실험해 보자. v = (cos(97°),sin(97°)) 벡터가 주어졌을 때 atan2(sin(97°),cos(97°))는 97°다. 이 부분은 쉽다. v = (1,–1) 벡터가 주어진 경우 atan2(–1,1) = –45°이다. 따라서 벡터의 y 요소가 음수이면 0°부터 –180° 사이의 음수 각도를 결과로 얻게 된다. 이런 결과는 atan2의 값이 음수일 경우 360° (또는 2pi)를 더해줌으로써 수정할 수 있다. 앞의 예제의 경우 이렇게 하면 결과 값이 315°가 된다.

벡터에 적용해볼 마지막 연산은 벡터를 일정 각도만큼 회전하는 것이다. 이어지는 식의 도출 과정도 조금 복잡하다. 하지만 이 식은 정사영 기본 벡터에 대한 지식 없이도 바로 사용할 수 있다(내부적으로 진행되는 로직이 궁금하다면 웹에서 정사영 기본 벡터를 검색해 보자). 이 식을 사용한 의사 코드는 다음과 같다.

v.x' = cos(angle) * v.x - sin(angle) * v.y
v.y' = sin(angle) * v.x + cos(angle) * v.y

생각보다 복잡하지 않아서 다행이라는 생각이 들 것이다. 이 공식을 사용하면 벡터의 해석값이 무엇이든 상관없이 원점을 중심으로 벡터를 반시계 방향으로 회전할 수 있다.

벡터 덧셈, 뺄셈, 스칼라에 의한 곱셈을 함께 사용하면 실제로 오픈GL 매트릭스 연산을 모두 구현할 수 있다. 이 내용은 앞 장의 BobTest의 성능을 조금 더 끌어올리기 위한 해결책 중 하나다. 이 부분에 대해서는 이어지는 절 가운데 한 곳에서 좀 더 자세히 언급할 것이다. 지금은 지금까지 설명한 내용에만 집중하고 이를 코드로 옮겨보겠다.