작성일 :

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

게임 화면에 보이는 모든 것은 수학으로 표현됩니다.

캐릭터의 위치는 좌표로, 캐릭터가 바라보는 방향은 방향 벡터로, 이동은 벡터의 덧셈으로, 조명의 밝기는 벡터의 내적으로 계산됩니다. 3D 오브젝트를 회전시키는 것은 행렬 곱셈이고, 3D 공간을 2D 화면에 투영하는 것도 행렬 연산입니다.

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

이 과정에서 사용되는 수학이 벡터, 행렬, 좌표 변환, 투영입니다. 이 수학적 기반이 없으면 렌더링 파이프라인이나 셰이더의 동작을 정확히 파악하기 어렵습니다.


Unity API를 호출하면 내부적으로 이 수학 연산이 실행되므로, 수학을 모르더라도 게임을 만들 수는 있습니다.

그러나 성능 병목을 분석하거나 셰이더를 직접 수정해야 할 때, 내부에서 어떤 계산이 일어나는지 파악하지 못하면 최적화 방향을 잡기 어렵습니다.

이 시리즈는 셰이더 최적화와 렌더링 파이프라인 분석이 전제하는 3D 그래픽스 수학을 다루며, 이 글에서는 가장 기본이 되는 벡터와 벡터 연산을 살펴봅니다.


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

벡터란 무엇인가

수학에서 다루는 양은 스칼라(Scalar)벡터(Vector) 두 가지로 나뉩니다.

스칼라는 크기만 가진 양입니다. 온도 25도, 질량 5kg, 속력 60km/h 같은 값이 스칼라입니다. 하나의 숫자로 완전히 표현되며, 방향이라는 개념이 없습니다.


벡터는 크기와 방향을 동시에 가진 양입니다. “북쪽으로 60km/h”는 속력(크기)과 방향이 결합된 벡터입니다. “오른쪽으로 3미터 이동”도 크기(3미터)와 방향(오른쪽)을 함께 담고 있으므로 벡터입니다.


2D 벡터는 x, y 두 개의 성분으로 표현됩니다. 3D 벡터는 x, y, z 세 개의 성분으로 표현됩니다. 게임 개발에서는 주로 3D 벡터를 사용합니다.

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)


게임에서 벡터는 용도에 따라 다양하게 활용됩니다.

위치 벡터(Position Vector)는 공간에서 한 점의 좌표를 나타냅니다. 캐릭터의 위치 (5, 0, 3)은 원점에서 x 방향으로 5, y 방향으로 0, z 방향으로 3만큼 떨어진 점을 가리키는 벡터입니다.


방향 벡터(Direction Vector)는 특정 방향을 나타내며, 캐릭터가 바라보는 전방 방향이나 표면에서 수직으로 뻗어나가는 법선 방향 등이 이에 해당합니다. 방향 벡터는 보통 크기를 1로 맞춰서(정규화하여) 사용하고, 이렇게 크기가 1인 벡터를 단위 벡터라 부릅니다.


속도 벡터(Velocity Vector)는 이동의 방향과 빠르기를 동시에 담고 있습니다. (2, 0, -1)이라는 속도 벡터는 매 초마다 x 방향으로 2, z 방향으로 -1만큼 이동한다는 뜻입니다.


벡터의 기본 연산

벡터에 대해 수행할 수 있는 기본 연산은 덧셈, 뺄셈, 스칼라 곱입니다.


덧셈

두 벡터의 덧셈은 각 성분을 더하는 것입니다.

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

기하학적으로 벡터의 덧셈은 화살표를 이어 붙이는 것입니다. 벡터 a의 끝점에 벡터 b의 시작점을 놓으면, 원점에서 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을 향하는 방향 벡터가 나옵니다.

결과 벡터의 크기는 두 점 사이의 거리이고, 방향은 origin에서 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}\]

스칼라 곱

벡터에 스칼라(숫자 하나)를 곱하는 연산입니다. 각 성분에 그 스칼라를 곱합니다.

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

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

양수를 곱하면 같은 방향으로 늘어나거나 줄어들고, 음수를 곱하면 방향이 반전됩니다.


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


게임에서 스칼라 곱은 속도 조절에 활용됩니다.

방향 벡터에 속력(스칼라)을 곱하면 속도 벡터가 됩니다. 같은 방향으로 더 빠르게 이동하려면 더 큰 값을 곱하면 됩니다.


벡터의 크기와 정규화

앞의 기본 연산에서 “크기가 2배”, “크기가 1인 벡터”, “정규화한 뒤” 같은 표현이 등장했습니다.

벡터의 크기정규화는 이 표현들의 정확한 의미를 정의하는 개념입니다.

크기 (Magnitude)

벡터의 크기(Magnitude)는 벡터가 나타내는 화살표의 길이입니다. 2D 벡터 (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 벡터는 같은 공식을 한 차원 확장한 형태입니다.

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}\]


피타고라스 정리는 2D에서 직각삼각형의 빗변을 구하는 공식이고, 3D 벡터의 크기는 이 정리를 3차원으로 확장한 것입니다.

먼저 x축과 z축으로 바닥면 대각선 $\sqrt{x^2 + z^2}$을 구하고, 이 대각선과 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)는 임의의 벡터를 단위 벡터로 변환하는 연산으로, 벡터의 각 성분을 벡터의 크기로 나누면 됩니다.


정규화 공식

\[\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$만 계산하고 제곱근을 생략합니다.

제곱근 연산은 CPU에서 곱셈이나 덧셈보다 수 배 이상 비용이 높습니다. 거리의 정확한 값이 필요하지 않고 두 거리의 대소만 비교하면 되는 경우에는 sqrMagnitudemagnitude보다 효율적입니다. 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

sqrMagnitude와 비교 대상의 제곱값을 사용하면 결과는 동일하면서 연산 비용이 줄어듭니다. 프레임마다 수십~수백 개의 오브젝트에 대해 거리 비교를 수행하는 경우, 이 차이가 누적되어 체감 가능한 성능 개선으로 이어질 수 있습니다.


내적 (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$는 두 벡터 사이의 각도입니다.


두 공식은 수학적으로 동일한 결과를 냅니다.

성분별 곱의 합이 $\lvert\mathbf{a}\rvert \times \lvert\mathbf{b}\rvert \times \cos\theta$ 와 같다는 점은 삼각함수의 성질로 증명됩니다.

프로그램에서 내적을 계산할 때는 성분별 곱의 합을 사용하고, 내적의 기하학적 의미를 해석할 때는 코사인 관계를 활용합니다.


내적의 기하학적 의미

두 벡터가 모두 단위 벡터(크기 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 θ

내적의 부호만으로 두 벡터가 같은 쪽을 가리키는지, 수직인지, 반대 쪽인지를 판별할 수 있습니다.


예를 들어, 플레이어의 전방 벡터와 적을 향한 방향 벡터의 내적이 양수이면, 적이 플레이어 시야의 앞쪽에 있다는 뜻입니다. 이를 통해 적이 시야 안에 있는지 판별할 수 있습니다.

반대로, 오브젝트의 전방 벡터와 카메라를 향한 방향의 내적이 음수이면, 오브젝트가 카메라에 등을 보이고 있다는 뜻입니다.

이처럼 내적의 부호 하나로 방향 관계를 판별할 수 있어, 시야 판정이나 앞뒤 구분 등 다양한 게임 로직에 활용됩니다.


내적과 조명 계산

내적이 사용되는 대표적인 곳은 조명(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}\]


이 계산 방식을 램버트 반사(Lambertian Reflectance)라 부릅니다. 램버트 반사는 가장 기본적인 확산(Diffuse) 조명 모델이며, 대부분의 셰이더에 포함되어 있습니다. 내적 한 번으로 표면이 빛을 받는 정도를 구할 수 있어, 3D 그래픽스에서 가장 빈번하게 수행되는 수학 연산 중 하나입니다.

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$이며, 이는 두 벡터가 이루는 평행사변형의 넓이와 같습니다. 두 벡터가 평행하면 $\sin\theta = 0$이므로 외적의 크기도 0이 됩니다. 두 벡터가 수직이면 $\sin\theta = 1$이므로 외적의 크기가 최대입니다.

a b a × b 넓이 = |a × b| 방향: a와 b 모두에 수직 · 크기: |a|·|b|·sinθ

외적의 방향

외적의 결과 벡터가 “위로” 향할지 “아래로” 향할지는 좌표계의 손잡이 규칙으로 결정됩니다. 수학과 OpenGL에서는 오른손 법칙(Right-Hand Rule)을 사용합니다.

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


a b a × 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는 왼손 좌표계를 사용합니다. 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)를 가리킵니다. 오른손 법칙을 적용하면 엄지는 -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가 뒤바뀌고, 앞서 확인한 것처럼 $\mathbf{a} \times \mathbf{b} = -(\mathbf{b} \times \mathbf{a})$이므로 법선의 방향이 반전됩니다.

GPU는 화면 공간에서의 감기 순서를 기준으로 삼각형의 앞면과 뒷면을 구분합니다. 래스터화 단계에서 투영된 삼각형의 정점이 시계 방향(CW)인지 반시계 방향(CCW)인지를 확인하여 판별하며, 뒷면으로 판정된 삼각형은 백페이스 컬링(Backface Culling)으로 제거됩니다.

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


Unity에서 외적은 Vector3.Cross(a, b)로 계산합니다. 표준 외적 공식과 동일하므로, 시계 방향으로 감긴 삼각형에 적용하면 앞면 방향의 법선을 얻습니다.

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

Unity에서의 Vector3

지금까지 다룬 벡터의 기본 연산, 크기와 정규화, 내적, 외적은 Unity에서 Vector3라는 구조체를 통해 사용됩니다. Vector3는 3D 벡터를 표현하며, 위의 연산들이 메서드와 연산자로 구현되어 있습니다.


위치와 방향

Unity에서 오브젝트의 위치와 방향은 Transform 컴포넌트를 통해 접근합니다.

Transform.position은 오브젝트의 월드 공간 위치를 나타내는 위치 벡터입니다.


Transform.forward는 오브젝트가 바라보는 전방 방향의 단위 벡터이고, Transform.up위쪽 방향, Transform.right오른쪽 방향의 단위 벡터입니다. 이 세 벡터는 서로 수직이며, 오브젝트의 로컬 좌표축을 구성합니다.

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


위 다이어그램은 회전이 적용되지 않은 기본 상태의 방향 벡터입니다. 오브젝트가 회전하면 이 세 벡터도 함께 회전하여 항상 오브젝트 기준의 방향을 나타냅니다.

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은 해당 오브젝트가 회전한 상태에서의 위쪽 방향을 가리킵니다. 오브젝트가 45도 기울어져 있다면 transform.up은 (0, 1, 0)이 아닌 기울어진 방향을 가리킵니다.


Unity의 좌표계

외적의 방향이 좌표계에 따라 달라진다는 점을 앞에서 확인했습니다. Unity는 왼손 좌표계(Left-Handed Coordinate System)를 사용하며, Y축이 위쪽(Up), Z축이 전방(Forward), X축이 오른쪽(Right)입니다.

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

수학 교과서와 일부 3D 소프트웨어(Blender, OpenGL)는 오른손 좌표계를 사용합니다. 오른손 좌표계에서는 Z축이 화면 밖(시점 쪽, 즉 카메라를 향하는 방향)을 향합니다. Unity의 왼손 좌표계에서는 Z축이 화면 안쪽(전방, 즉 카메라가 바라보는 방향)을 향합니다.


왼손 좌표계 (Unity, DirectX) Y X Z (전방) 오른손 좌표계 (OpenGL, Blender) Y X Z (후방)


Blender 같은 오른손 좌표계 도구에서 만든 모델을 Unity로 가져올 때 축 변환이 발생하는 이유가 이 좌표계 차이입니다. Unity는 임포트 과정에서 자동으로 축을 변환합니다.

다만, 스크립트에서 수동으로 벡터 연산을 수행할 때는 Unity가 왼손 좌표계라는 점을 기억해야 합니다. 앞서 외적에서 언급했듯이, 같은 외적 공식이라도 왼손 좌표계에서는 결과 방향을 왼손 법칙으로 해석해야 합니다.


주요 Vector3 메서드 정리

위에서 다룬 벡터 연산들을 Unity API 기준으로 정리하면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
연산                   Unity API                     결과
─────────────────────────────────────────────────────────────
덧셈                   a + b                         Vector3
뺄셈                   a - b                         Vector3
스칼라 곱              v * s 또는 s * v              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$이면 두 벡터의 정확한 중간점입니다.

이동이나 카메라 추적에서 부드러운 전환을 만들 때 사용됩니다.


마무리

  • 스칼라는 크기만 가진 양이고, 벡터는 크기와 방향을 동시에 가진 양입니다. 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)를 사용하며, 외적의 방향은 왼손 법칙을 따릅니다. 시계 방향 감기(CW)가 앞면입니다.

벡터가 공간에서 점과 방향을 표현하는 도구라면, 행렬(Matrix)은 벡터를 변환하는 도구입니다. 이동, 회전, 스케일 같은 변환은 모두 행렬 곱셈으로 수행됩니다.

그래픽스 수학 (2) - 행렬과 변환에서 행렬의 구조와 변환의 원리를 이어 설명합니다.



관련 글

시리즈

전체 시리즈

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

Categories: ,