작성일 :

3D 그래픽스에서 수학이 필요한 이유

Unity에서 위치를 바꾸거나 카메라를 회전시키는 코드는 대부분 API 호출로 끝납니다. 하지만 그 API 안에서는 좌표, 벡터, 행렬, 투영 같은 수학 연산이 계속 실행됩니다.

캐릭터의 위치는 좌표로 저장되고, 바라보는 방향은 방향 벡터로 표현됩니다. 이동은 위치에 이동량을 더하는 연산이고, 조명 밝기는 표면 방향과 빛 방향의 관계로 계산됩니다. 오브젝트의 회전과 3D 공간을 2D 화면으로 옮기는 과정에는 행렬 연산이 들어갑니다.

렌더링 기초 (1) - 메쉬의 구조에서 법선 벡터가 빛 계산에 사용된다고 했고, 렌더링 기초 (3) - 머티리얼과 셰이더 기초에서 셰이더가 정점 변환과 조명 계산을 수행한다고 했습니다.

이때 등장하는 벡터, 행렬, 좌표 변환, 투영을 이해해야 렌더링 파이프라인과 셰이더 코드가 어떤 값을 다루는지 따라갈 수 있습니다.


Unity API가 많은 계산을 대신 처리하므로, 수학을 깊게 몰라도 게임을 만들 수는 있습니다.

다만 성능 병목을 분석하거나 셰이더를 직접 수정해야 할 때는 이야기가 달라집니다. 내부에서 어떤 벡터를 만들고 어떤 값을 비교하는지 알아야 불필요한 계산을 줄이거나 잘못된 좌표 변환을 찾을 수 있기 때문입니다.

이 시리즈에서는 렌더링과 셰이더를 읽는 데 필요한 그래픽스 수학을 다룹니다. 첫 글의 주제는 벡터입니다. 벡터가 위치, 방향, 이동량을 어떻게 표현하는지 확인한 뒤, 덧셈, 뺄셈, 정규화, 내적, 외적이 Unity와 렌더링에서 어떤 의미를 갖는지 연결합니다.


벡터 위치 · 방향 · 속도 (현재 글) 행렬 이동 · 회전 · 스케일 (Part 2) 좌표 공간 오브젝트 · 월드 · 뷰 · 클립 (Part 3) 투영 원근 · 직교 (Part 4)

벡터란 무엇인가

게임에서 위치를 다룰 때는 숫자 하나만으로는 충분하지 않습니다. 캐릭터가 얼마나 이동했는지도 중요하지만, 어느 방향으로 이동했는지도 함께 필요하기 때문입니다.

숫자 하나로 표현되는 값은 스칼라(Scalar)입니다. 온도 25도, 질량 5kg, 속력 60km/h처럼 크기만 있는 값이 여기에 해당합니다. 스칼라는 값의 크기를 나타낼 수 있지만, 방향은 담지 않습니다.

반대로 벡터(Vector)는 크기와 방향을 함께 담습니다. “북쪽으로 60km/h”는 얼마나 빠른지와 어느 쪽으로 움직이는지를 함께 나타내고, “오른쪽으로 3미터 이동”도 이동 거리와 방향을 함께 나타냅니다.

그래서 벡터는 좌표축별 성분으로 표현합니다. 2D 벡터는 x, y 두 성분을 사용하고, 3D 벡터는 x, y, z 세 성분을 사용합니다. Unity의 Vector3도 이 세 성분으로 위치, 방향, 이동량을 표현합니다.

2D 벡터 y x 1 2 3 1 2 v = (3, 2) 3D 벡터 y x z 1 2 3 1 2 4 v = (3, 2, 4)


같은 x, y, z 성분이라도 어떤 값으로 쓰느냐에 따라 위치, 방향, 속도처럼 다른 의미를 가집니다.

위치 벡터(Position Vector)로 사용할 때는 공간의 한 점을 가리킵니다. 캐릭터 위치가 (5, 0, 3)이라면, 원점에서 x 방향으로 5, y 방향으로 0, z 방향으로 3만큼 떨어진 지점에 있다는 뜻입니다.

방향 벡터(Direction Vector)로 사용할 때는 어느 쪽을 향하는지만 나타냅니다. 캐릭터의 전방 방향이나 표면의 법선 방향처럼 위치보다 방향 자체가 중요한 값입니다. 이런 경우에는 거리 정보가 섞이지 않도록 크기를 1로 맞춘 단위 벡터를 주로 사용합니다.

속도 벡터(Velocity Vector)로 사용할 때는 방향에 빠르기까지 포함됩니다. (2, 0, -1)이라는 속도 벡터는 1초 동안 x 방향으로 2, z 방향으로 -1만큼 이동한다는 뜻입니다.


벡터의 기본 연산

벡터를 실제 코드에서 사용하려면 위치를 더하고, 두 위치의 차이를 구하고, 방향에 속력을 곱하는 계산이 필요합니다. 방향 벡터의 길이를 1로 맞출 때는 벡터를 자신의 크기로 나누기도 합니다. 이때 사용하는 기본 연산이 덧셈, 뺄셈, 스칼라 곱과 스칼라 나눗셈입니다.


덧셈

두 벡터를 더할 때는 같은 자리의 성분끼리 더합니다.

\[\mathbf{a} = (2, \; 1), \quad \mathbf{b} = (1, \; 3)\] \[\mathbf{a} + \mathbf{b} = (2{+}1, \; 1{+}3) = (3, \; 4)\]

벡터 덧셈은 이동량을 차례대로 적용하는 것과 같습니다. 먼저 벡터 a만큼 이동하고, 그 끝점에서 다시 벡터 b만큼 이동하면, 처음 출발점에서 마지막 도착점까지의 이동량이 a + b입니다.


y x 1 2 3 1 2 3 4 a = (2, 1) b = (1, 3) (3, 4) = a + b


게임에서는 현재 위치에 이동량을 더할 때 이 연산을 사용합니다. 캐릭터의 현재 위치에 이동 벡터를 더하면 새 위치가 됩니다.

\[\begin{aligned} \text{현재 위치} &= (5, \; 0, \; 3) \\ \text{이동 벡터} &= (1, \; 0, \; {-}2) \\[6pt] \text{새 위치} &= (5{+}1, \; 0{+}0, \; 3{+}({-}2)) = (6, \; 0, \; 1) \end{aligned}\]

뺄셈

두 벡터를 뺄 때도 같은 자리의 성분끼리 뺍니다.

\[\mathbf{a} = (4, \; 3), \quad \mathbf{b} = (1, \; 1)\] \[\mathbf{a} - \mathbf{b} = (4{-}1, \; 3{-}1) = (3, \; 2)\]

두 위치를 빼면 한 위치에서 다른 위치까지의 차이가 벡터로 나옵니다.

target - origin을 계산하면 origin에서 target까지 이동하려면 어느 방향으로 얼마나 가야 하는지 알 수 있습니다. 이 벡터의 방향은 target 쪽이고, 크기는 두 점 사이의 거리입니다.


y x 1 2 3 4 1 2 origin = (1, 1) target = (4, 3) target − origin = (3, 2)


적 캐릭터를 플레이어 쪽으로 움직일 때도 같은 계산을 사용합니다. 플레이어 위치에서 적 위치를 빼면, 적에서 플레이어로 향하는 벡터를 얻을 수 있습니다.

이 벡터를 정규화한 뒤 이동 속력을 곱하면 실제 이동에 사용할 속도 벡터가 됩니다.

\[\begin{aligned} \text{플레이어 위치} &= (10, \; 0, \; 5) \\ \text{적 위치} &= (3, \; 0, \; 2) \\[6pt] \text{방향 벡터} &= \text{플레이어 위치} - \text{적 위치} \\ &= (10{-}3, \; 0{-}0, \; 5{-}2) \\ &= (7, \; 0, \; 3) \end{aligned}\]

스칼라 곱과 나눗셈

벡터에 숫자 하나를 곱하거나 나누면 벡터의 길이를 조절할 수 있습니다. 계산할 때는 x, y, z 각 성분을 같은 숫자로 곱하거나 나눕니다.

\[\mathbf{v} = (3, \; 2)\] \[\begin{aligned} 2\mathbf{v} &= (6, \; 4) & &\text{— 크기가 2배} \\ 0.5\mathbf{v} &= (1.5, \; 1) & &\text{— 크기가 절반} \\ \frac{\mathbf{v}}{2} &= (1.5, \; 1) & &\text{— 0.5를 곱한 것과 같음} \\ {-}\mathbf{v} &= (-3, \; -2) & &\text{— 방향 반전} \end{aligned}\]

기하학적으로 스칼라 곱과 나눗셈은 벡터의 크기를 변경합니다.

양수를 곱하거나 나누면 같은 방향으로 늘어나거나 줄어들고, 음수를 곱하면 방향이 반전됩니다. 나눗셈은 0이 아닌 값에 대해서만 사용할 수 있으며, v / 2v * 0.5와 같은 의미입니다.


y x 2v = (6, 4) v = (3, 2) 0.5v = (1.5, 1) −v = (−3, −2)


게임에서는 방향에 속력을 붙일 때 스칼라 곱을 사용합니다. 방향 벡터에 속력 값을 곱하면 속도 벡터가 되고, 같은 방향으로 더 빠르게 이동하려면 더 큰 값을 곱하면 됩니다. 반대로 벡터를 일정한 크기로 줄이거나 정규화할 때는 스칼라 나눗셈이 사용됩니다.


벡터의 크기와 정규화

벡터는 방향만 담는 것이 아니라 길이도 함께 가집니다. 두 위치 사이의 거리를 구하려면 벡터의 길이를 알아야 합니다. 반대로 방향만 필요할 때는 길이가 계산에 섞이지 않도록 벡터의 길이를 1로 맞춥니다.

크기 (Magnitude)

벡터의 크기(Magnitude)는 벡터가 나타내는 화살표의 길이입니다. 2D 벡터 (x, y)는 x축 이동량과 y축 이동량이 직각을 이루므로, 피타고라스 정리로 길이를 구할 수 있습니다.

2D 벡터의 크기

\[|\mathbf{v}| = \sqrt{x^2 + y^2}\] \[\begin{aligned} \mathbf{v} &= (3, \; 4) \\[4pt] |\mathbf{v}| &= \sqrt{3^2 + 4^2} \\ &= \sqrt{9 + 16} \\ &= \sqrt{25} = 5 \end{aligned}\]


3D 벡터도 같은 원리를 사용합니다. x, y, z 세 성분이 서로 직교하는 축의 이동량이므로, 세 성분의 제곱을 더한 뒤 제곱근을 구합니다.

3D 벡터의 크기

\[|\mathbf{v}| = \sqrt{x^2 + y^2 + z^2}\] \[\begin{aligned} \mathbf{v} &= (1, \; 2, \; 2) \\[4pt] |\mathbf{v}| &= \sqrt{1^2 + 2^2 + 2^2} \\ &= \sqrt{1 + 4 + 4} \\ &= \sqrt{9} = 3 \end{aligned}\]


3D에서 이 공식을 떠올리기 어렵다면, 바닥면의 대각선을 먼저 구한 뒤 그 대각선과 y축 높이를 다시 조합한다고 보면 됩니다.

2D x y 빗변 3D x z y √(x²+z²) |v|
\[\text{2D:} \quad |\mathbf{v}| = \sqrt{x^2 + y^2} \qquad \text{3D:} \quad |\mathbf{v}| = \sqrt{x^2 + y^2 + z^2}\]


게임에서 벡터의 크기는 거리 계산에 직접 쓰입니다. 두 오브젝트의 위치를 빼면 한 오브젝트에서 다른 오브젝트로 향하는 벡터가 나오고, 그 벡터의 크기가 두 점 사이의 거리입니다.

\[\begin{aligned} \text{플레이어 위치} &= (10, \; 0, \; 5) \\ \text{적 위치} &= (3, \; 0, \; 2) \\[6pt] \text{차이 벡터} &= (10{-}3, \; 0{-}0, \; 5{-}2) = (7, \; 0, \; 3) \\[4pt] \text{거리} &= |\text{차이 벡터}| = \sqrt{7^2 + 0^2 + 3^2} \\ &= \sqrt{49 + 0 + 9} \\ &= \sqrt{58} \approx 7.62 \end{aligned}\]

단위 벡터와 정규화

단위 벡터(Unit Vector)는 크기가 정확히 1인 벡터입니다. 단위 벡터는 순수한 방향만을 나타내며, 크기 정보를 담지 않습니다.

임의의 벡터를 단위 벡터로 바꾸는 연산을 정규화(Normalization)라고 합니다. 벡터의 각 성분을 벡터의 크기로 나누면 방향은 유지되고 크기만 1이 됩니다.


정규화 공식

\[\hat{\mathbf{v}} = \frac{\mathbf{v}}{|\mathbf{v}|}\] \[\begin{aligned} \mathbf{v} &= (3, \; 0, \; 4) \\[4pt] |\mathbf{v}| &= \sqrt{9 + 0 + 16} = \sqrt{25} = 5 \\[4pt] \hat{\mathbf{v}} &= \left(\frac{3}{5}, \; \frac{0}{5}, \; \frac{4}{5}\right) = (0.6, \; 0, \; 0.8) \\[4pt] |\hat{\mathbf{v}}| &= \sqrt{0.36 + 0 + 0.64} = \sqrt{1} = 1 \quad \leftarrow \text{크기가 1} \end{aligned}\]

정규화된 벡터는 원래 벡터와 같은 방향을 가리키지만 크기는 1입니다. 즉, 방향 정보만 남기고 거리나 속력 같은 크기 정보는 제거합니다.


게임에서 정규화는 방향과 속력을 분리할 때 사용합니다. 적에서 플레이어를 향하는 벡터를 구한 뒤, 이 벡터를 정규화하고 원하는 속력을 곱하면 이동 속도 벡터가 됩니다.

정규화하지 않은 벡터를 그대로 이동에 사용하면, 두 오브젝트 사이가 멀수록 벡터의 크기도 커집니다. 그러면 방향만 달라져야 하는 상황에서 이동 속도까지 함께 달라집니다.

\[\begin{aligned} \text{방향 벡터} &= (7, \; 0, \; 3) & &\leftarrow |\mathbf{v}| \approx 7.62 \\ \text{정규화} &= (0.92, \; 0, \; 0.39) & &\leftarrow |\hat{\mathbf{v}}| = 1 \\[6pt] \text{이동 속도} &= \hat{\mathbf{v}} \times \text{속력} \\ &= (0.92, \; 0, \; 0.39) \times 5 \\ &= (4.6, \; 0, \; 1.95) \end{aligned}\]

Unity에서의 크기와 정규화

Unity의 Vector3에는 크기와 정규화에 대응하는 속성이 준비되어 있습니다.

Vector3.magnitude는 벡터의 크기를 반환합니다. 내부적으로 $\sqrt{x^2 + y^2 + z^2}$를 계산합니다.

Vector3.normalized는 정규화된 벡터를 반환합니다. 내부적으로 크기를 먼저 구한 뒤 각 성분을 나눕니다.

Vector3.sqrMagnitude는 크기의 제곱 값을 반환합니다. 내부적으로 $x^2 + y^2 + z^2$만 계산하고 제곱근을 생략합니다.

제곱근 연산은 덧셈이나 곱셈보다 비용이 큽니다. 거리의 정확한 값이 필요하지 않고 기준 거리보다 가까운지만 확인하면 된다면 sqrMagnitude를 사용할 수 있습니다. 양수 거리에서는 a > b이면 a² > b²이므로, 제곱값끼리 비교해도 대소 관계가 유지되기 때문입니다.

1
2
3
4
5
6
7
// magnitude 사용 (제곱근 포함)
float dist = (target - origin).magnitude;
if (dist < 10f) { ... }

// sqrMagnitude 사용 (제곱근 생략)
float sqrDist = (target - origin).sqrMagnitude;
if (sqrDist < 100f) { ... }    // 10의 제곱 = 100

예를 들어 반경 10 안에 들어왔는지만 확인한다면, 실제 거리와 10을 비교할 필요가 없습니다. 거리의 제곱값을 구한 뒤 10의 제곱인 100과 비교하면 같은 판단을 할 수 있습니다. 프레임마다 많은 오브젝트의 거리를 검사하는 코드에서는 이런 방식으로 제곱근 계산을 줄일 수 있습니다.


내적 (Dot Product)

게임에서는 두 방향이 얼마나 비슷한지 판단해야 할 때가 많습니다. 플레이어가 적을 바라보고 있는지, 표면이 빛을 정면으로 받고 있는지, 오브젝트가 카메라를 향하고 있는지 같은 판단이 여기에 해당합니다.

내적(Dot Product)은 이런 방향 관계를 숫자 하나로 바꾸는 연산입니다. 두 벡터를 입력으로 받아 스칼라 값을 반환하며, 이 값의 부호와 크기를 보면 두 벡터가 같은 쪽을 향하는지, 수직에 가까운지, 반대쪽을 향하는지 알 수 있습니다.


내적의 공식

내적은 코드에서는 성분별 곱의 합으로 계산합니다.


\[\mathbf{a} \cdot \mathbf{b} = a_x b_x + a_y b_y + a_z b_z\] \[\begin{aligned} \mathbf{a} &= (2, \; 3, \; 1), \quad \mathbf{b} = (4, \; {-}1, \; 2) \\[6pt] \mathbf{a} \cdot \mathbf{b} &= 2 \times 4 + 3 \times ({-}1) + 1 \times 2 \\ &= 8 + ({-}3) + 2 \\ &= 7 \end{aligned}\]


내적은 두 벡터 사이의 각도와도 연결됩니다.

\[\mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| \; |\mathbf{b}| \; \cos\theta\]

여기서 $\lvert\mathbf{a}\rvert$는 벡터 a의 크기, $\lvert\mathbf{b}\rvert$는 벡터 b의 크기, $\theta$는 두 벡터 사이의 각도입니다.


이 식 때문에 내적 값으로 두 벡터의 방향 차이를 읽을 수 있습니다. 특히 두 벡터의 크기가 1이면 식이 더 단순해집니다.


내적의 기하학적 의미

두 벡터가 모두 단위 벡터(크기 1)라면, 앞의 식에서 $ \mathbf{a} $와 $ \mathbf{b} $가 모두 1이 됩니다.
\[|\mathbf{a}| = 1, \; |\mathbf{b}| = 1 \; \text{일 때:} \quad \mathbf{a} \cdot \mathbf{b} = 1 \times 1 \times \cos\theta = \cos\theta\]

두 단위 벡터의 내적은 두 벡터 사이 각도의 코사인 값과 같습니다. 그래서 벡터를 정규화해 두면 내적 결과만으로 두 방향이 얼마나 비슷한지 판단할 수 있습니다.


$\cos\theta$ 의 부호와 방향 관계

\[\begin{aligned} \theta = 0^\circ &\rightarrow \cos\theta = 1.0 & &\text{같은 방향} \\ \theta = 60^\circ &\rightarrow \cos\theta = 0.5 & &\text{같은 방향 쪽} \\ \theta = 90^\circ &\rightarrow \cos\theta = 0.0 & &\text{수직} \\ \theta = 120^\circ &\rightarrow \cos\theta = -0.5 & &\text{반대 방향 쪽} \\ \theta = 180^\circ &\rightarrow \cos\theta = -1.0 & &\text{정반대} \end{aligned}\]


내적 > 0 (같은 방향) a b θ 내적 = 0 (수직) a b 90° 내적 < 0 (반대 방향) a b θ

내적이 양수이면 두 벡터의 각도가 90도보다 작고, 음수이면 90도보다 큽니다. 값이 0에 가까우면 두 방향은 거의 수직입니다.

이 성질은 앞뒤 판정에 자주 사용됩니다. 플레이어의 전방 벡터와 적을 향한 방향 벡터를 내적했을 때 값이 양수이면, 적은 플레이어가 바라보는 앞쪽에 있습니다. 값이 음수이면 플레이어 뒤쪽에 있는 것으로 볼 수 있습니다.

카메라와 오브젝트의 앞뒤 관계를 판단할 때도 같은 방식으로 사용할 수 있습니다. 별도의 각도 계산 없이 내적의 부호만으로 시야 판정이나 앞뒤 구분을 처리할 수 있습니다.


내적과 조명 계산

내적은 조명(Lighting) 계산에서 자주 등장합니다. 렌더링 기초 (1) - 메쉬의 구조에서 법선(Normal)과 빛의 각도에 따라 표면 밝기가 달라진다고 했습니다. 이 각도 관계를 수치로 바꿀 때 내적을 사용합니다.

표면의 법선 벡터 N은 표면에서 수직으로 뻗어나가는 방향이고, 광원 방향 벡터 L은 표면에서 광원을 향하는 방향입니다. 이 두 벡터의 내적 $\mathbf{N} \cdot \mathbf{L}$은 빛이 표면에 얼마나 직접적으로 닿는지를 나타냅니다.


$\mathbf{N}$과 $\mathbf{L}$이 모두 단위 벡터일 때, $\mathbf{N} \cdot \mathbf{L} = \cos\theta$입니다. 빛이 표면 뒤쪽에서 오는 경우($\theta > 90°$)에는 음수가 되므로, 실제 조명 계산에서는 0 이하를 잘라냅니다.

표면 N L θ
\[\text{밝기} = \max(0, \;\mathbf{N} \cdot \mathbf{L}) = \max(0, \;\cos\theta)\] \[\begin{aligned} \theta = 0^\circ &\rightarrow \cos(0^\circ) = 1.0 & &\text{(가장 밝음)} \\ \theta = 45^\circ &\rightarrow \cos(45^\circ) \approx 0.71 & &\text{(중간 밝기)} \\ \theta = 90^\circ &\rightarrow \cos(90^\circ) = 0.0 & &\text{(빛이 닿지 않음)} \\ \theta > 90^\circ &\rightarrow \cos\theta < 0 & &\text{(표면 뒤쪽, 0으로 처리)} \end{aligned}\]


확산(Diffuse) 조명에서는 빛이 표면에 정면으로 들어올수록 밝고, 옆에서 스치듯 들어올수록 어둡게 보입니다. 법선과 빛 방향의 내적은 이 차이를 1에서 0 사이의 값으로 바꿔 주므로, 표면 밝기를 계산하는 기본 값으로 사용할 수 있습니다. 이 방식이 램버트 반사(Lambertian Reflectance)의 기본 형태입니다.

Unity에서는 이 내적 계산을 Vector3.Dot(a, b)로 작성합니다.

1
2
3
4
5
Vector3 N = transform.up;          // 표면 법선
Vector3 L = lightDirection;        // 광원 방향 (단위 벡터)

float brightness = Vector3.Dot(N, L);
brightness = Mathf.Max(brightness, 0f);  // 음수는 0으로

외적 (Cross Product)

내적이 두 벡터에서 숫자 하나를 얻는 연산이라면, 외적(Cross Product)은 두 벡터에서 새로운 벡터를 얻는 연산입니다. 외적의 결과는 입력 두 벡터에 모두 수직인 방향을 가리키며, 3D 공간에서만 정의됩니다.


외적의 공식

외적은 다음 공식으로 계산합니다.

\[\mathbf{a} \times \mathbf{b} = (a_y b_z - a_z b_y, \;\; a_z b_x - a_x b_z, \;\; a_x b_y - a_y b_x)\] \[\begin{aligned} \mathbf{a} &= (1, \; 0, \; 0), \quad \mathbf{b} = (0, \; 1, \; 0) \\[6pt] \mathbf{a} \times \mathbf{b} &= (0 \cdot 0 - 0 \cdot 1, \;\; 0 \cdot 0 - 1 \cdot 0, \;\; 1 \cdot 1 - 0 \cdot 0) \\ &= (0, \; 0, \; 1) \end{aligned}\]

x축 방향 벡터 (1, 0, 0)과 y축 방향 벡터 (0, 1, 0)의 외적은 z축 방향 벡터 (0, 0, 1)입니다. x축과 y축에 모두 수직인 방향이 z축이므로, 결과가 직관적으로 일치합니다.


외적의 기하학적 의미

외적의 결과는 두 입력 벡터가 놓인 평면에서 수직으로 뻗는 벡터입니다. 그래서 삼각형이나 표면의 방향을 나타내는 법선 벡터(Normal Vector)를 구할 때 외적을 사용합니다.

결과 벡터의 길이도 의미를 가집니다. 외적의 크기는 다음과 같고, 이 값은 두 벡터가 만드는 평행사변형의 넓이와 같습니다.

\[\lvert\mathbf{a} \times \mathbf{b}\rvert = \lvert\mathbf{a}\rvert \; \lvert\mathbf{b}\rvert \; \sin\theta\]

두 벡터가 평행하면 평행사변형의 넓이가 0이므로 외적의 크기도 0입니다. 두 벡터가 수직에 가까워질수록 넓이가 커지고, 외적의 크기도 함께 커집니다.

a b a × b 넓이 = |a × b|

외적의 방향

외적의 결과는 두 벡터가 이루는 평면에 수직이지만, 그 수직 방향은 두 가지가 가능합니다. 어느 쪽을 결과로 볼지는 좌표계의 손잡이 규칙으로 정합니다. 수학과 OpenGL에서는 오른손 법칙(Right-Hand Rule)을 사용합니다.

오른손의 네 손가락을 벡터 a 방향으로 뻗은 뒤, 벡터 b 방향으로 감아쥡니다. 이때 엄지가 가리키는 방향이 $\mathbf{a} \times \mathbf{b}$의 방향입니다.


a b a × b θ

외적에서는 입력 순서가 결과 방향을 결정합니다. $\mathbf{a} \times \mathbf{b}$와 $\mathbf{b} \times \mathbf{a}$는 길이는 같지만 서로 반대 방향을 가리킵니다.

\[\mathbf{a} \times \mathbf{b} = -(\mathbf{b} \times \mathbf{a})\]


Unity는 왼손 좌표계를 사용하므로, Unity의 x, y, z축에서 외적 방향을 예상할 때는 왼손 법칙으로 확인해야 합니다. 왼손의 네 손가락을 첫 번째 벡터 방향에서 두 번째 벡터 방향으로 감아쥐면, 엄지가 가리키는 방향이 Vector3.Cross(a, b)의 결과 방향입니다.


Unity의 기본 축으로 확인하면 다음과 같습니다.

1
2
Vector3.Cross(Vector3.right, Vector3.up);      // (0, 0, 1) = forward
Vector3.Cross(Vector3.right, Vector3.forward);  // (0, -1, 0) = down

첫 번째 예시에서 왼손 손가락을 right(+x)에서 up(+y) 방향으로 감아쥐면, 엄지가 forward(+z)를 가리킵니다. Unity에서 외적의 방향을 예상할 때는 이 축 방향을 기준으로 확인해야 합니다.


외적의 활용: 면의 법선 계산

외적은 삼각형의 법선 벡터를 구할 때 자주 사용합니다. 렌더링 기초 (1) - 메쉬의 구조에서 메쉬가 삼각형으로 구성되며, 각 표면에 법선이 있다고 했습니다. 삼각형의 세 정점 v0, v1, v2가 주어지면 두 변을 벡터로 만들 수 있고, 두 변의 외적이 삼각형 면에 수직인 방향을 줍니다.

v₀ v₁ v₂ edge₁ edge₂
\[\begin{aligned} \mathbf{edge_1} &= \mathbf{v_1} - \mathbf{v_0} \\ \mathbf{edge_2} &= \mathbf{v_2} - \mathbf{v_0} \\[6pt] \mathbf{N} &= \mathbf{edge_1} \times \mathbf{edge_2} \\[4pt] \hat{\mathbf{N}} &= \frac{\mathbf{N}}{|\mathbf{N}|} \quad \leftarrow \text{정규화} \end{aligned}\]

법선의 방향은 정점의 감기 순서(Winding Order)에 따라 달라집니다. 감기 순서는 삼각형의 세 정점을 나열하는 순서입니다. 정점 순서가 바뀌면 edge1과 edge2의 순서도 바뀌고, 외적은 입력 순서가 바뀔 때 방향이 반전되므로 법선 방향도 반대로 뒤집힙니다.

GPU는 이 감기 순서를 사용해 삼각형의 앞면과 뒷면을 구분합니다. 래스터화 단계에서 화면에 투영된 삼각형의 정점 순서가 시계 방향(CW)인지 반시계 방향(CCW)인지 확인하고, 뒷면으로 판정된 삼각형은 백페이스 컬링(Backface Culling)으로 제거할 수 있습니다.

Unity는 시계 방향(CW)을 앞면으로 판정합니다. OpenGL 기반 엔진은 반시계 방향(CCW)을 앞면으로 사용하므로, 엔진에 따라 규칙이 다릅니다.


Unity에서 외적은 Vector3.Cross(a, b)로 계산합니다. 삼각형의 정점 순서를 Unity의 앞면 규칙에 맞추면, 이 연산으로 앞면 방향의 법선을 얻을 수 있습니다.

1
2
3
Vector3 edge1 = v1 - v0;
Vector3 edge2 = v2 - v0;
Vector3 normal = Vector3.Cross(edge1, edge2).normalized;

Unity에서의 Vector3

앞에서 다룬 연산은 Unity에서 대부분 Vector3를 통해 사용합니다. Vector3는 x, y, z 성분을 가진 3D 벡터 구조체이며, 덧셈과 뺄셈은 연산자로, 내적과 외적은 정적 메서드로 제공됩니다.


위치와 방향

Unity에서 오브젝트가 어디에 있고 어느 쪽을 향하는지는 Transform 컴포넌트에서 읽습니다.

Transform.position은 오브젝트의 월드 공간 위치입니다. 씬의 원점을 기준으로 오브젝트가 어디에 있는지를 나타내는 위치 벡터입니다.

방향은 Transform.forward, Transform.up, Transform.right로 읽습니다. 이 값들은 오브젝트가 회전한 상태를 반영한 전방, 위쪽, 오른쪽 방향입니다. 모두 길이가 1인 단위 벡터이므로, 이동 방향이나 내적 계산에 바로 사용할 수 있습니다.

up (0, 1, 0) right (1, 0, 0) forward (0, 0, 1)


위 다이어그램은 오브젝트가 회전하지 않았을 때의 기본 방향입니다. 오브젝트를 회전시키면 forward, up, right도 그 회전에 맞춰 바뀌므로, 현재 오브젝트가 바라보는 방향을 기준으로 사용할 수 있습니다.

1
2
3
4
Vector3 pos = transform.position;       // 위치 벡터
Vector3 fwd = transform.forward;        // 전방 단위 벡터
Vector3 up  = transform.up;             // 위쪽 단위 벡터
Vector3 rt  = transform.right;          // 오른쪽 단위 벡터

미리 정의된 벡터

Vector3에는 자주 쓰는 월드 방향 벡터가 정적 속성으로 정의되어 있습니다.

1
2
3
4
5
6
7
8
9
10
// 미리 정의된 Vector3 상수

  Vector3.zero     = (0, 0, 0)      // 원점
  Vector3.one      = (1, 1, 1)      // 모든 성분이 1
  Vector3.up       = (0, 1, 0)      // 월드 위쪽
  Vector3.down     = (0, -1, 0)     // 월드 아래쪽
  Vector3.forward  = (0, 0, 1)      // 월드 전방
  Vector3.back     = (0, 0, -1)     // 월드 후방
  Vector3.right    = (1, 0, 0)      // 월드 오른쪽
  Vector3.left     = (-1, 0, 0)     // 월드 왼쪽

Vector3.uptransform.up은 이름이 비슷하지만 의미가 다릅니다. Vector3.up은 월드 공간의 고정된 방향 (0, 1, 0)을 가리키고, transform.up은 오브젝트가 회전한 뒤의 위쪽 방향을 가리킵니다. 오브젝트가 기울어져 있다면 transform.up도 그 기울어진 방향을 따라갑니다.


Unity의 좌표계

앞에서 Transform.forward와 외적의 방향을 설명하면서 Unity의 축 방향을 여러 번 사용했습니다. 여기서는 그 기준만 정리합니다. Unity는 왼손 좌표계(Left-Handed Coordinate System)를 사용하며, X축은 오른쪽, Y축은 위쪽, Z축은 전방을 가리킵니다.

Y (위) X (오른쪽) Z (전방)

수학 교과서나 일부 3D 도구는 z축 방향을 Unity와 다르게 잡는 경우가 있습니다. 그래서 외부 도구에서 만든 모델을 가져오거나, 스크립트에서 외적과 방향 벡터를 직접 계산할 때는 Unity의 기준인 +Z = Forward를 먼저 확인해야 합니다.


주요 Vector3 메서드 정리

앞에서 다룬 벡터 연산을 Unity API 기준으로 모으면 다음과 같습니다.

연산 Unity API 결과
덧셈 a + b Vector3
뺄셈 a - b Vector3
스칼라 곱 v * s 또는 s * v Vector3
스칼라 나눗셈 v / s Vector3
크기 v.magnitude float
크기의 제곱 v.sqrMagnitude float
정규화 v.normalized Vector3
내적 Vector3.Dot(a, b) float
외적 Vector3.Cross(a, b) Vector3
두 점 사이 거리 Vector3.Distance(a, b) float
선형 보간 Vector3.Lerp(a, b, t) Vector3


Vector3.Distance(a, b)(a - b).magnitude와 같은 의미입니다. 거리의 대소만 비교한다면 (a - b).sqrMagnitude를 사용해 제곱근 계산을 피할 수 있습니다.

Vector3.Lerp(a, b, t)는 두 벡터 a와 b 사이를 t 비율(0~1)로 선형 보간(Linear Interpolation)합니다.

내부적으로 $\mathbf{a} + (\mathbf{b} - \mathbf{a}) \cdot t$를 계산하므로, $t = 0$이면 $\mathbf{a}$, $t = 1$이면 $\mathbf{b}$, $t = 0.5$이면 두 벡터의 중간점이 됩니다. 이동이나 카메라 추적처럼 두 값 사이를 부드럽게 이어야 할 때 사용합니다.


마무리

이번 글에서는 Unity와 렌더링에서 자주 만나는 벡터 연산을 기본 개념부터 API 사용까지 연결했습니다.

  • 스칼라는 크기만 가진 양이고, 벡터는 크기와 방향을 동시에 가진 양입니다. 3D 벡터는 x, y, z 세 성분으로 표현됩니다.
  • 벡터의 덧셈은 이동의 합성, 뺄셈은 두 점 사이의 방향 벡터, 스칼라 곱과 나눗셈은 크기 변경이나 방향 반전입니다.
  • 벡터의 크기는 $\sqrt{x^2 + y^2 + z^2}$로 계산되며, 정규화는 크기 1인 단위 벡터로 변환하는 연산입니다. sqrMagnitude는 제곱근을 생략하여 거리 비교에서 성능 이점을 줍니다.
  • 내적(Dot Product)은 두 벡터 사이의 각도 관계를 스칼라로 표현하며, 단위 벡터의 내적은 $\cos\theta$로 조명 계산($\mathbf{N} \cdot \mathbf{L}$)의 수학적 기반입니다.
  • 외적(Cross Product)은 두 벡터에 수직인 새 벡터를 만들며, 삼각형의 법선 벡터 계산에 사용됩니다. 외적 결과의 크기는 $\lvert\mathbf{a}\rvert\lvert\mathbf{b}\rvert\sin\theta$로 평행사변형의 넓이와 같습니다.
  • Unity는 왼손 좌표계(Y-up, Z-forward)를 사용하므로, 외적 방향과 삼각형 감기 순서를 Unity의 좌표계 기준으로 확인해야 합니다.

벡터는 공간의 위치와 방향을 표현하지만, 오브젝트를 이동하거나 회전시키려면 벡터를 다른 벡터로 바꾸는 변환이 필요합니다. 이 변환을 체계적으로 다루는 도구가 행렬(Matrix)입니다.

그래픽스 수학 (2) - 행렬과 변환에서는 이동, 회전, 스케일이 행렬 곱셈으로 표현되는 방식을 다룹니다.



관련 글

전체 시리즈

Tags: Unity, 그래픽스, 모바일, 벡터, 수학

Categories: ,