← 개념 · 데모 가이드 | 구현 허브 | 1장. 기초 수학 | 벡터 — Vec3
IMPLEMENTATION · CHAPTER 1

벡터 — Vec3 — 한 줄 한 줄 직접 구현

그래픽스에서 위치(점이 어디 있나), 방향(어느 쪽을 보나), (RGB) — 이 세 가지는 모두 숫자 세 개로 표현됩니다. 이 세 개를 하나로 묶은 것이 3차원 벡터 Vec3입니다. 이 페이지에서는 Vec3 구조체를 C++로 직접 만들고, 덧셈·뺄셈·스칼라 곱·단항 부호·내적·외적·길이·정규화코드 한 줄, 성분 하나까지 해부한 뒤, 실무 표준인 DirectXMath로 똑같이 옮겨 봅니다.

규약 이 가이드는 D3D 규약으로 통일합니다: 행벡터 v' = v * M(HLSL mul(v, M)), 왼손 좌표계. 이 페이지의 Vec3 연산(덧셈·내적·길이 등)은 규약과 무관하게 동일하지만, 외적의 방향만은 좌표계(왼손/오른손)에 따라 의미가 달라집니다. 아래 외적 절에서 다시 짚습니다.

0. 무엇을, 왜 만드나

Vec3float 세 개(x, y, z)를 담는 아주 작은 구조체입니다. 하지만 이 위에 덧셈·내적·외적 같은 연산을 얹는 순간, 그래픽스의 거의 모든 계산을 표현할 수 있게 됩니다.

같은 세 숫자라도 "점"이냐 "방향"이냐에 따라 의미가 다릅니다. 예를 들어 두 점을 더하는 것은 대개 의미가 없지만 (뉴욕 좌표 + 서울 좌표 = ?), 점 + 방향은 "그 점에서 그 방향으로 이동한 새 점"이라는 뚜렷한 의미가 있습니다. 구조체 하나로 이 셋을 모두 표현하되, 의미는 우리가 코드로 구분해서 씁니다.

1. 구조체와 생성자 — 뼈대 만들기

먼저 세 개의 float을 담을 그릇을 정의합니다. 그리고 그 그릇을 "빈 채로" 또는 "값을 채워서" 만드는 두 가지 방법(생성자)을 마련합니다.

  1. float 세 개 x, y, z를 멤버로 선언하고 0으로 기본 초기화합니다.
  2. 인자 없는 기본 생성자(0,0,0) 벡터를 만들 수 있게 합니다.
  3. 값 세 개를 받는 생성자Vec3 a(1,2,3) 처럼 채워서 만들 수 있게 합니다.
math/Vec3.h — ①구조체 · 생성자cpp
#pragma once
#include <cmath>

struct Vec3 {
    float x = 0.0f, y = 0.0f, z = 0.0f;
    Vec3() = default;
    Vec3(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {}
};

한 줄씩 해부

#pragma once — 이 헤더가 여러 .cpp에서 #include돼도 딱 한 번만 컴파일에 포함되게 막아 주는 지시문입니다. 없으면 같은 구조체가 두 번 정의됐다는 오류가 납니다. #include <cmath>는 뒤에서 쓸 std::sqrt(제곱근) 때문에 필요합니다.

struct Vec3 { ... }; — 새 자료형 Vec3를 정의합니다. C++에서 structclass는 기본 접근 권한만 다릅니다(struct는 기본 public). 데이터를 그대로 노출하는 수학 타입에는 struct가 관례입니다.

float x = 0.0f, y = 0.0f, z = 0.0f; — 세 멤버 변수입니다. 뒤의 = 0.0f기본 멤버 초기화(default member initializer)로, 어떤 생성자를 타든 값을 안 주면 자동으로 0이 됩니다. "쓰레기 값(초기화 안 된 메모리)"이 들어가는 실수를 원천 차단합니다. f 접미사는 이 리터럴이 double이 아니라 float임을 뜻합니다(GPU/그래픽스는 대부분 32비트 float을 씁니다).

Vec3() = default;기본 생성자를 컴파일러가 자동으로 만들게 명시합니다. 위에서 멤버에 = 0.0f를 줬으므로 이 생성자로 만든 벡터는 (0,0,0)입니다. 사실 아래에 인자 있는 생성자를 하나라도 직접 선언하면 컴파일러가 기본 생성자를 자동으로 안 만들어 주기 때문에, = default;되살려 두는 것입니다. 이게 없으면 Vec3 v;가 컴파일 오류가 납니다.

Vec3(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {} — 값 세 개를 받는 생성자입니다. : x(x_), y(y_), z(z_) 부분이 멤버 초기화 리스트로, 멤버 x를 인자 x_로 초기화합니다. 본문({}) 안에서 x = x_;로 대입하는 것보다 초기화 리스트가 더 효율적이고 정석입니다 (대입은 "일단 기본값 생성 → 다시 대입" 2단계, 초기화는 1단계). 인자 이름에 밑줄(x_)을 붙인 건 멤버 x와 이름이 겹쳐 헷갈리는 것을 피하기 위한 흔한 관례입니다.

함정 멤버 초기화 = 0.0f가 없으면 Vec3 v;v.x정의되지 않은 값(쓰레기)입니다. 특히 스택에 잡힌 로컬 변수는 0으로 초기화되지 않습니다. "가끔 검은 화면, 가끔 정상"처럼 재현이 어려운 버그의 단골 원인이니, 수학 타입은 반드시 기본값을 0으로 두세요.

집합 초기화(aggregate init) 한 마디

아래 연산자들에서 return { x + b.x, y + b.y, z + b.z }; 처럼 중괄호로 값 세 개를 나열하는 문법을 자주 씁니다. 이건 Vec3의 세 멤버를 순서대로(x, y, z) 채워 새 객체를 만드는 중괄호 초기화입니다. 반환 타입이 Vec3임을 컴파일러가 알기에 return Vec3{...}return {...}로 줄여 쓸 수 있습니다.

2. 덧셈과 뺄셈 — 이동과 "차이"

무엇을 만드나: 두 벡터를 성분끼리 더하거나 빼는 연산입니다. 왜 필요한가: 덧셈은 "점을 방향만큼 이동"(새위치 = 위치 + 속도), 뺄셈은 "A에서 B로 가는 방향"(목표 - 현재)을 구하는 데 매일 쓰입니다. 벡터 뺄셈 b - a는 그래픽스에서 "a에서 b를 향하는 화살표"라고 외워 두면 두고두고 편합니다.

math/Vec3.h — ②+ , -cpp
    Vec3 operator+(const Vec3& b) const { return { x + b.x, y + b.y, z + b.z }; }
    Vec3 operator-(const Vec3& b) const { return { x - b.x, y - b.y, z - b.z }; }

시그니처 해부

Vec3 operator+(const Vec3& b) const — 앞에서부터 뜯어봅니다.

성분별 계산 — 수학 → 코드

수학적으로 (x, y, z) + (bx, by, bz) = (x+bx, y+by, z+bz) 입니다. 즉 같은 축끼리만 더합니다 (x는 x끼리, y는 y끼리). 코드의 세 항이 정확히 이에 대응합니다:

결과 성분코드의미
x'x + b.x내 x + 상대 x
y'y + b.y내 y + 상대 y
z'z + b.z내 z + 상대 z

기하 직관: 덧셈은 화살표를 이어 붙이기입니다. 벡터 a의 화살촉 끝에서 벡터 b를 다시 그리면, 출발점에서 b의 화살촉까지가 a + b입니다(평행사변형 법칙). 뺄셈 a - bb의 화살촉에서 a의 화살촉으로 향하는 화살표입니다.

성능 b - a는 "a에서 b로 가는 방향"입니다. 순서가 바뀌면 정반대 방향이 되니 주의하세요. 카메라에서 물체로 향하는 시선은 물체위치 - 카메라위치, 반대는 아닙니다.

손계산 예제

a = (1, 2, 3), b = (4, 0, 0) 이면 a + b = (1+4, 2+0, 3+0) = (5, 2, 3), a - b = (1-4, 2-0, 3-0) = (-3, 2, 3) 입니다.

3. 스칼라 곱 — 길이 늘이고 줄이기

무엇을 만드나: 벡터에 실수 하나(스칼라)를 곱해 세 성분 모두를 같은 배율로 키우거나 줄이는 연산입니다. 왜 필요한가: 속도 * deltaTime(프레임당 이동량), 방향 * 거리(그 방향으로 얼마만큼), 색 밝기 조절 등 끝없이 쓰입니다. 스칼라를 곱해도 방향은 그대로, 길이만 바뀝니다(음수를 곱하면 방향이 뒤집힙니다).

math/Vec3.h — ③스칼라 곱 (멤버 + 자유 함수)cpp
    // 멤버: v * s  (벡터 * 스칼라)
    Vec3 operator*(float s) const { return { x * s, y * s, z * s }; }
};  // struct 끝

// 자유 함수: s * v  (스칼라 * 벡터) — v * s 로 되돌려 재사용
inline Vec3 operator*(float s, const Vec3& v) { return v * s; }

왜 두 개나 필요한가 — v*s 와 s*v

수학에서는 v * ss * v든 같은 뜻이지만, C++에서는 둘이 서로 다른 함수 호출입니다.

inline Vec3 operator*(float s, const Vec3& v) { return v * s; } — 이 자유 함수는 스스로 계산하지 않고 return v * s;로 멤버 버전을 재사용합니다. 로직을 한 곳(멤버)에만 두어 중복과 실수를 줄이는 좋은 습관입니다. inline은 이 함수가 헤더에 정의돼 여러 .cpp에 포함될 때 "중복 정의" 링크 오류를 막아 줍니다 (헤더에 함수 본문을 쓸 때는 붙이는 게 안전합니다).

참고로 멤버 operator*는 구조체 이라 inline이 필요 없습니다(클래스 안 정의 함수는 자동으로 inline 취급).

성분별 계산 & 기하 직관

(x, y, z) * s = (x·s, y·s, z·s). 세 성분에 똑같은 배율 s를 곱합니다. s = 2면 화살표가 두 배 길어지고, s = 0.5면 절반, s = -1이면 길이는 그대로인데 정반대 방향이 됩니다.

손계산: a = (1,2,3), s = 2a * 2 = (2, 4, 6). 2 * a도 결과는 같은 (2,4,6).

4. 단항 마이너스 — 방향 뒤집기

무엇을 만드나: -v처럼 벡터 하나 앞에 마이너스를 붙여 모든 성분의 부호를 바꾸는 연산입니다. 왜 필요한가: 반대 방향(카메라의 "뒤", 빛의 반대 방향 등)을 구할 때 씁니다. 사실상 v * -1.0f과 같지만, -v가 훨씬 읽기 좋습니다.

math/Vec3.h — ④단항 -cpp
    Vec3 operator-() const { return { -x, -y, -z }; }

이항 뺄셈과 어떻게 구분되나

앞서 operator-(const Vec3& b)(인자 1개)는 이항 뺄셈 a - b였습니다. 여기 operator-()인자가 0개입니다. 컴파일러는 인자 개수로 둘을 구분합니다: a - b는 이항(뺄셈), -a는 단항(부호 반전). 같은 기호 -지만 다른 함수입니다.

{ -x, -y, -z } — 세 성분 각각에 마이너스를 붙여 새 벡터를 만듭니다. a = (1,2,3)이면 -a = (-1, -2, -3). 길이는 그대로, 방향만 정반대입니다. 맨 끝 const는 앞서와 같은 이유로 "자기 자신을 바꾸지 않는다"는 약속입니다.

5. 내적 dot — 두 벡터가 얼마나 "같은 쪽"을 보나

무엇을 만드나: 두 벡터의 성분끼리 곱해 모두 더한 하나의 실수(스칼라)를 돌려주는 연산입니다. 결과가 벡터가 아니라 숫자 하나라는 점이 핵심입니다. 왜 필요한가: 내적은 그래픽스의 심장입니다. 두 방향의 사잇각, 표면이 빛을 얼마나 받는지(램버트 조명), 앞/뒤 판별, 투영 등 거의 모든 조명·기하 계산의 출발점입니다.

math/Vec3.h — ⑤dotcpp
inline float dot(const Vec3& a, const Vec3& b) {
    return a.x*b.x + a.y*b.y + a.z*b.z;
}

시그니처: 왜 float을 돌려주나

inline float dot(const Vec3& a, const Vec3& b) — 반환 타입이 Vec3가 아니라 float입니다. 내적의 결과는 스칼라 하나이기 때문입니다. 두 인자 모두 const Vec3&로 받아 읽기 전용·복사 없이 처리합니다. 구조체 바깥의 자유 함수이며(멤버가 아니어도 두 인자를 대칭으로 받는 편이 자연스러움), 헤더 정의라 inline을 붙였습니다.

수식 → 기하 직관 → 코드 3단

① 수식: a · b = ax·bx + ay·by + az·bz. 같은 축끼리 곱해서 전부 더합니다.

② 기하 직관: 내적은 또한 a · b = |a| |b| cos θ 와 같습니다(θ는 두 벡터 사잇각, |a|는 길이). 여기서 어마어마하게 유용한 사실이 나옵니다:

특히 두 벡터가 모두 단위 길이(정규화됨)라면 |a| = |b| = 1이므로 a · b = cos θ가 되어, 내적 값이 곧 코사인입니다. 조명에서 "표면 법선 N 과 빛 방향 Ldot(N, L)"이 바로 이 원리로 "정면으로 받으면 1(가장 밝음), 옆이면 0, 뒤면 음수(빛 안 닿음 → 0으로 클램프)"를 계산합니다.

③ 코드 매핑: a.x*b.x(x축 곱) + a.y*b.y(y축 곱) + a.z*b.z(z축 곱). 수식과 1:1입니다.

손계산 예제

a = (1, 2, 3), b = (4, 0, 0) 이면 dot(a, b) = 1·4 + 2·0 + 3·0 = 4. b가 x축만 가리키므로, 내적은 결국 a의 x성분(1)에 b의 크기(4)를 곱한 값과 같습니다. 이는 "a를 b(=x축) 방향으로 투영했을 때의 크기"라는 내적의 또 다른 의미를 그대로 보여 줍니다.

함정 dot(N, L)로 조명을 계산할 때 두 벡터를 반드시 정규화해야 = cos θ가 성립합니다. 정규화하지 않으면 길이가 값에 섞여 들어가 조명이 이상하게 밝거나 어두워집니다. 또 음수가 나올 수 있으니 max(dot(N, L), 0.0f)0 아래를 잘라 주는 것이 보통입니다.

6. 외적 cross — 두 벡터에 수직인 벡터

무엇을 만드나: 두 벡터 a, b에 동시에 수직새 벡터를 돌려줍니다. 내적과 달리 결과가 벡터입니다. 왜 필요한가: 삼각형의 두 변에서 면의 법선(수직 방향)을 구할 때, 카메라의 오른쪽·위 축을 만들 때 등 "수직 방향이 필요한 모든 곳"에 씁니다. 조명·컬링·좌표계 구축의 핵심입니다.

math/Vec3.h — ⑥crosscpp
inline Vec3 cross(const Vec3& a, const Vec3& b) {
    return {
        a.y*b.z - a.z*b.y,   // x 성분
        a.z*b.x - a.x*b.z,   // y 성분
        a.x*b.y - a.y*b.x    // z 성분
    };
}

왜 이렇게 복잡한가 — 성분별로 하나씩

외적의 각 성분은 "자기 축을 뺀 나머지 두 축"의 교차 곱셈이라는 규칙을 따릅니다. 외우기보다 패턴으로 보세요. 각 결과 성분마다, 나머지 두 축을 순환 순서(x→y→z→x)로 "앞·뒤"를 곱해 뺍니다.

결과 성분코드사용하는 두 축규칙(앞·뒤 곱 − 뒤·앞 곱)
xa.y*b.z - a.z*b.yy, z (x를 뺀 나머지)aybz − azby
ya.z*b.x - a.x*b.zz, x (y를 뺀 나머지)azbx − axbz
za.x*b.y - a.y*b.xx, y (z를 뺀 나머지)axby − aybx

x 성분 a.y*b.z - a.z*b.y: x를 만들 때는 x를 빼고 남은 y와 z만 씁니다. 순환 순서 x→yz를 따라 "앞(y)·뒤(z)"를 곱한 a.y*b.z에서 "뒤(z)·앞(y)"를 곱한 a.z*b.y를 뺍니다.

y 성분 a.z*b.x - a.x*b.z: y를 빼고 남은 z와 x를 씁니다. 순환에서 y 다음은 z→x이므로 "앞(z)·뒤(x)" a.z*b.x에서 "뒤(x)·앞(z)" a.x*b.z를 뺍니다. 주의: 여기서 순서가 z, xx, z가 아니라는 점이 초보자가 가장 많이 틀리는 곳입니다(부호가 뒤집혀 법선이 반대로 나옵니다).

z 성분 a.x*b.y - a.y*b.x: z를 빼고 남은 x와 y를 씁니다. 순환에서 z 다음은 x→y이므로 "앞(x)·뒤(y)" a.x*b.y에서 "뒤(y)·앞(x)" a.y*b.x를 뺍니다. 이 z 성분만 따로 떼면 2D 외적 (감김 방향/삼각형 넓이 부호 판정)에 그대로 쓰입니다.

기하 직관과 방향(오른손/왼손)

결과의 방향: cross(a, b)는 a와 b가 이루는 평면에 수직입니다. 구체적으로 어느 쪽(위/아래)인지는 손 규칙으로 정해집니다. 수학의 기본 공식(위 코드)은 오른손 법칙을 따릅니다: 오른손 손가락을 a에서 b로 감아쥐면 엄지가 cross(a, b) 방향입니다.

결과의 길이: |cross(a, b)| = |a| |b| sin θ로, 이는 a, b가 만드는 평행사변형의 넓이와 같습니다 (삼각형 넓이는 그 절반). 두 벡터가 평행하면 sin θ = 0이라 외적이 영벡터가 됩니다 — "수직 방향이 정의되지 않음".

주의 이 공식은 오른손 좌표계 기준입니다. 우리 가이드는 왼손 좌표계(D3D)를 쓰므로, 삼각형 법선을 만들 때 "정점 감김 순서(CW/CCW)"에 따라 a, b를 어떤 변으로 잡을지로 최종 방향을 맞춰야 합니다. 법선이 뒤집혀 보이면 cross(a, b)cross(b, a)를 바꿔 보세요(두 결과는 부호만 반대: cross(b,a) = -cross(a,b)). 외적은 교환법칙이 성립하지 않습니다.

손계산 예제

a = (1, 2, 3), b = (4, 0, 0) 로 계산해 봅니다.

cross(a, b) = (0, 12, -8). 검산으로 내적이 0이면 수직임을 확인합니다: dot((0,12,-8), a) = 0·1 + 12·2 + (-8)·3 = 24 - 24 = 0 ✓, dot((0,12,-8), b) = 0·4 + 0 + 0 = 0 ✓. 결과가 a, b 둘 다에 수직임이 확인됩니다.

7. 길이 length — 벡터의 크기

무엇을 만드나: 벡터의 크기(원점에서 화살촉까지의 거리)를 실수로 돌려줍니다. 왜 필요한가: 두 점 사이 거리(length(b - a)), 속력, 정규화의 재료 등에 쓰입니다.

math/Vec3.h — ⑦lengthcpp
inline float length(const Vec3& v) { return std::sqrt(dot(v, v)); }

dot(v, v)인가

피타고라스로 |v| = √(x² + y² + z²) 입니다. 그런데 dot(v, v) = x·x + y·y + z·z = x² + y² + z²이므로, 제곱합이 곧 자기 자신과의 내적입니다. 그래서 dot(v, v)std::sqrt(제곱근)를 씌우면 길이가 됩니다. 이미 만든 dot를 재사용하니 코드가 짧고 실수가 줄어듭니다.

std::sqrt<cmath>에 있는 제곱근 함수입니다(그래서 맨 위에서 #include <cmath> 했습니다). 손계산: a = (1,2,3)dot(a,a) = 1+4+9 = 14length(a) = √14 ≈ 3.742.

성능 std::sqrt는 상대적으로 비싼 연산입니다. "가장 가까운 물체 찾기"처럼 거리끼리 비교만 하는 경우엔 실제 거리 대신 거리의 제곱 dot(v, v)으로 비교하세요. 는 단조 증가라 "제곱이 더 작으면 실제로도 더 가깝다"가 항상 성립하므로, sqrt를 통째로 생략할 수 있습니다. 수천~수만 번 반복되는 루프에서 체감 차이가 큽니다.

8. 정규화 normalize — 길이만 1로, 방향은 그대로

무엇을 만드나: 방향은 유지하되 길이를 정확히 1로 맞춘 단위 벡터를 돌려줍니다. 왜 필요한가: 조명(dot(N, L) = cos θ가 성립하려면 둘 다 단위여야 함), 방향 비교, 이동 방향 등 "크기는 필요 없고 방향만 필요한" 거의 모든 곳에서 먼저 정규화합니다.

math/Vec3.h — ⑧normalizecpp
inline Vec3 normalize(const Vec3& v) {
    float len = length(v);
    if (len < 1e-8f) return { 0.0f, 0.0f, 0.0f };
    return v * (1.0f / len);
}

한 줄씩

float len = length(v); — 먼저 현재 길이를 구합니다. 이걸로 나눠 주면 길이가 1이 됩니다.

if (len < 1e-8f) return { 0.0f, 0.0f, 0.0f };0으로 나누기 방어입니다. 영벡터(또는 거의 영)에는 "방향"이 없어 정규화가 정의되지 않습니다. 그대로 나누면 len이 0이라 결과가 inf/NaN이 되어 화면 전체가 깨질 수 있습니다. 1e-8f(= 0.00000001)은 "이보다 작으면 사실상 0으로 본다"는 엡실론(허용 오차)입니다. 정확히 == 0.0f로 비교하지 않는 이유는 float 계산에는 미세 오차가 있어, 이론상 0이어야 할 값이 아주 작은 값으로 남을 수 있기 때문입니다. 엡실론으로 "충분히 작으면 0 취급"하는 것이 안전합니다.

return v * (1.0f / len); — 각 성분을 길이로 나눠 단위 벡터를 만듭니다. v / len 대신 1.0f / len을 한 번 구해 곱하는 이유는 성능입니다: 나눗셈은 곱셈보다 느리므로, 나눗셈 한 번 + 곱셈 세 번이 나눗셈 세 번보다 빠릅니다. (여기서 앞서 만든 v * s 스칼라 곱이 재사용됩니다.)

손계산 예제

b = (4, 0, 0)length(b) = 4normalize(b) = (4/4, 0/4, 0/4) = (1, 0, 0). 방향(x축 양의 방향)은 그대로, 길이만 1이 됐습니다.

함정 엡실론 방어를 빼먹고 영벡터를 정규화하면 NaN이 퍼져 이후 모든 계산이 오염됩니다. (NaN은 어떤 연산과 만나도 NaN이 되고, 렌더 결과가 검거나 사라집니다.) 방향이 확실치 않은 벡터를 정규화할 때는 항상 길이 0 방어를 넣으세요.

9. 전체를 한 번에 — 사용 예

main.cpp — 사용 예cpp
Vec3 a{1, 2, 3};
Vec3 b{4, 0, 0};

Vec3 sum = a + b;         // (5, 2, 3)
Vec3 dir = normalize(b);  // (1, 0, 0)  — x축 단위 벡터
float d  = dot(a, b);     // 4
Vec3 c   = cross(a, b);   // (0, 12, -8) — a, b 에 수직
float la = length(a);     // sqrt(14) ≈ 3.742

앞에서 손으로 계산한 값들이 그대로 나옵니다. 여기까지가 직접 구현입니다. 내부 동작을 손으로 확인했으니, 이제 실무 표준으로 넘어갑니다.

10. 실무 버전 — DirectXMath로 옮기기

실제 D3D 프로젝트에서는 Vec3를 직접 만들지 않고 DirectXMath를 씁니다. SIMD(한 번에 4개 float을 병렬 계산)로 최적화돼 있고, 행렬·쿼터니언까지 일관된 API를 제공합니다. 직접 구현은 원리 학습용, DirectXMath는 실전용이라고 생각하세요.

핵심 개념 하나만 잡으면 됩니다: 저장은 XMFLOAT3, 계산은 XMVECTOR.

그래서 흐름이 항상 "로드(XMLoadFloat3) → 계산 → 저장(XMStoreFloat3)" 입니다.

DirectXMath 버전cpp
#include <DirectXMath.h>
using namespace DirectX;

XMFLOAT3 aS{1, 2, 3};      // 저장용(storage)
XMFLOAT3 bS{4, 0, 0};

// 1) 로드: 저장용 → 계산용
XMVECTOR a = XMLoadFloat3(&aS);
XMVECTOR b = XMLoadFloat3(&bS);

// 2) 계산: 전부 XMVECTOR 로
XMVECTOR sum  = XMVectorAdd(a, b);          // a + b
XMVECTOR diff = XMVectorSubtract(a, b);     // a - b
XMVECTOR sc   = XMVectorScale(a, 2.0f);     // a * 2
XMVECTOR ndir = XMVector3Normalize(b);      // normalize(b)
XMVECTOR cx   = XMVector3Cross(a, b);       // cross(a, b)

// 내적/길이는 결과가 스칼라지만 XMVECTOR(4채널에 복제)로 나옴 → Get 으로 추출
float d  = XMVectorGetX(XMVector3Dot(a, b));    // dot(a, b) = 4
float la = XMVectorGetX(XMVector3Length(a));    // length(a) ≈ 3.742

// 3) 저장: 계산용 → 저장용 (메모리에 다시 담기)
XMFLOAT3 sumS;
XMStoreFloat3(&sumS, sum);   // 이제 sumS.x = 5, ...

원본 ↔ DirectXMath 1:1 매핑

직접 구현DirectXMath메모
Vec3 v{...} (저장)XMFLOAT3메모리 저장용 구조체
(계산할 벡터)XMVECTOR + XMLoadFloat3(&s)계산 전 반드시 로드
a + bXMVectorAdd(a, b)성분별 덧셈
a - bXMVectorSubtract(a, b)성분별 뺄셈
a * sXMVectorScale(a, s)스칼라 곱
-vXMVectorNegate(v)부호 반전
dot(a, b)XMVector3Dot(a, b)XMVectorGetX(...)결과가 XMVECTOR라 X채널 추출
cross(a, b)XMVector3Cross(a, b)결과는 XMVECTOR(방향)
length(v)XMVector3Length(v)XMVectorGetX(...)스칼라 추출 필요
normalize(v)XMVector3Normalize(v)영벡터 방어는 내부 처리(구현별 상이)
결과를 메모리에 저장XMStoreFloat3(&s, v)계산 끝나면 다시 저장

한 가지 낯선 점: XMVector3Dot·XMVector3Length의 결과가 float이 아니라 XMVECTOR라는 것입니다. SIMD 특성상 스칼라 하나도 4개 채널(x, y, z, w)에 같은 값으로 복제해 담기 때문입니다. 그래서 XMVectorGetX(...)로 첫 채널을 꺼내 진짜 float으로 씁니다. 우리 직접 구현이 곧바로 float을 돌려주던 것과의 유일한 사용법 차이입니다.

규약 DirectXMath는 행벡터 규약입니다: v' = v * M, HLSL에서 mul(v, M). GLM(OpenGL)의 열벡터 v' = M * v와 곱 순서가 반대이니, 다른 자료의 행렬 곱 순서를 그대로 옮기지 마세요. 이 차이는 다음 행렬 — Mat4 페이지에서 본격적으로 다룹니다(그리고 cbuffer 업로드 시 전치가 필요한 이유도).
성능 XMLoadFloat3/XMStoreFloat3는 SIMD 레지스터와 메모리 사이 이사 비용이 있습니다. 루프 안에서 여러 연산을 한다면 한 번 로드해서 XMVECTOR로 쭉 계산하고, 필요할 때만 저장하세요. 성분마다 로드/저장을 반복하면 SIMD의 이점이 사라집니다.