[Three.js] 벡터의 정규화(Normalize), 왜 굳이 길이를 1로 만들까?
벡터의 길이를 1로 만드는 정규화의 본질을 게임 이동 로직과 블렌더의 Apply Scale 사례를 통해 분석했습니다. 방향과 크기를 분리하는 이유와 실무 활용 팁을 정리했습니다.
- #Three.js
댓글
입력한 비밀번호는 비밀 댓글 열람, 수정, 삭제에 사용됩니다.
아직 등록된 댓글이 없습니다.
벡터의 길이를 1로 만드는 정규화의 본질을 게임 이동 로직과 블렌더의 Apply Scale 사례를 통해 분석했습니다. 방향과 크기를 분리하는 이유와 실무 활용 팁을 정리했습니다.
입력한 비밀번호는 비밀 댓글 열람, 수정, 삭제에 사용됩니다.
아직 등록된 댓글이 없습니다.
Three.js를 공부하다 보면 벡터 뒤에 거의 공식처럼 붙어 있는 .normalize()를 자주 보게 된다. 강의에서는 “유용하다”는 말만 남기고 넘어갔는데, 오히려 그게 더 궁금해졌다.
왜 멀쩡한 벡터를 굳이 길이 1로 만들까?
언제 꼭 필요하고, 언제는 굳이 안 해도 되는 걸까?
생각해 보니 이건 처음 겪는 개념이 아니었다.
나는 평소 블렌더를 자주 쓰는데, 스케일을 조정한 뒤에는 항상 Apply Scale을 해주라는 말을 들어왔다. 이유는 늘 “나중에 깨질 수 있어서”였지만, 왜 깨지는지는 제대로 이해하지 못한 채 습관처럼 눌러왔던 것 같다.
Three.js의 정규화를 파고들다 보니, 블렌더에서 하던 그 행동이 사실은 같은 문제를 다른 방식으로 해결하고 있었다는 게 보이기 시작했다.
정규화는 벡터의 방향은 유지한 채, 길이만 1로 만드는 작업이다.
“얼마나 멀리 가는지”는 버리고
“어느 방향을 가리키는지”만 남긴다
그래서 정규화된 벡터는 보통 이렇게 쓰인다.
모두 공통점이 있다. 크기보다 ‘방향’이 중요한 정보라는 점이다.
먼저 격자 게임을 떠올려봤다.
이런 게임의 세계는 사실상 연속 공간이 아니다.
대각선 이동은 수학적으로 보면 √2 ≈ 1.41이지만,
게임 규칙상으로는 그냥 “한 번 이동” 이다.
📌 중요한 건 위치의 정확한 거리 값이 아니라 규칙
그래서 “대각선이 더 빠르지 않나?” 같은 문제가 애초에 발생하지 않는다.
Three.js에서 다루는 공간은 완전히 다르다.
(x, y, z)여기서 정규화를 하지 않으면 문제가 바로 드러난다.
W + D 입력 → (1, 1)
이 벡터의 길이는 √2 ≈ 1.41
즉, 같은 프레임 동안:
이 차이는 단순한 체감 속도 문제가 아니다.
💥 실제 위치 값이 더 많이 이동한다
🧱 충돌 판정 버그
얇은 벽이나 코너를 대각선으로 이동하면서 한 프레임에 통과해버리는 현상
⏱ 도착 시간 불균형
같은 목적지인데 대각선 이동 캐릭터가 항상 먼저 도착함
(속도 문제가 아니라 거리 이득)
자유 이동 게임에서는 보통 이런 흐름을 따른다.
결과적으로:
| 이동 | 입력 벡터 | 실제 길이 | 정규화 후 |
|---|---|---|---|
| 오른쪽 | (1, 0) | 1.0 | 1.0 |
| 위쪽 | (0, 1) | 1.0 | 1.0 |
| 대각선 | (1, 1) | 1.41 | 1.0 |
👀 눈에는 대각선으로 가지만
📐 위치 값 기준으로는 항상 같은 거리만 이동
여기서 블렌더가 다시 연결된다.
블렌더의 노멀 벡터(Normal Vector) 는 조명 계산에서 “면의 방향”을 나타낸다. 그리고 이 노멀은 항상 길이 1이라는 전제로 계산된다.
그런데 스케일을 늘리고 Apply Scale을 하지 않으면:
조명 계산은 보통 내적(Dot Product) 을 쓴다.
0 ~ 1 (우리가 기대하는 밝기)이 값은:
👉 Apply Scale은 결국 “이 벡터들 다시 길이 1 기준으로 맞춰라” 라는 의미였다.
.normalize()는 원본 벡터를 직접 바꾼다.
위치나 힘의 크기를 유지해야 한다면
반드시 복사해서 써야 한다.
const direction = v.clone().normalize();모든 곳에 붙일 필요는 없다
하지만 ‘방향’이라는 단어가 떠오르는 순간, 거의 무조건 사용한다
아래는 Three.js에서 실제로 자주 마주치는 대표적인 3가지 상황이다.
OrbitControls 같은 컨트롤러를 쓰면 내부에서 알아서 처리된다. 그래서 처음엔 .normalize()를 쓸 일이 거의 없다고 느끼기 쉽다.
하지만 키보드 입력이나 클릭 좌표를 받아 직접 이동 로직을 만든다면 이야기가 달라진다.
mesh.position.x += 1;여기서 정규화를 하지 않으면 speed 값이 거리마다 다르게 적용된다.
즉, 속도를 일정하게 만들고 싶다면 → 방향 벡터는 항상 normalize
Three.js로 3D 웹 인터랙션을 만들다 보면 거의 반드시 Raycaster를 쓰게 된다.
마우스 위치 → 화면 안쪽으로 쏘는 보이지 않는 레이저
이 레이저가 바로 방향 벡터다.
raycaster.setFromCamera(mouse, camera);내부에서는 정규화된 방향 벡터가 들어올 것을 전제로 계산한다.
만약 정규화되지 않은 벡터를 직접 넣게 되면:
📌 Ray = 방향 벡터 → 방향이면 normalize
입문 단계에서는 보통 MeshStandardMaterial, MeshPhongMaterial 같은 내장 재질을 사용한다.
이 경우엔 Three.js가 내부에서 노멀과 방향 벡터를 전부 정규화해준다
하지만 아래 순간부터는 직접 개입해야 한다.
mesh.scale.set(10Apply Scale을 안 한 상태와 동일이때는 정규화된 벡터를 다시 만들어서 사용해야 한다.
“어느 쪽으로?”라는 정보만 필요하고 “얼마나 멀리”는 무시하고 싶을 때
dot(), reflect(), cross() 같은 연산 전에 표준 규격(길이 1) 을 맞추는 습관
lookAt(), OrbitControls, 단순 position 변경 등은 엔진이 내부에서 이미 처리한다
정규화는 단순한 수학 트릭이 아니다.
.normalize()를 사용해야 하는 이유가 이제는 꽤 명확해졌다.
입력을 방향 의도로 해석
→ (1, 1) = “대각선으로 가고 싶다”
.normalize()
→ (0.7, 0.7)
여기에 속도(speed) 를 곱한다
const v = new THREE.Vector3(3, 4, 0);
v.normalize();
console.log(v); // (0.6, 0.8, 0)const direction = target.clone().sub(mesh.position).normalize();
mesh.position.add(direction.multiplyScalar(speed));vec3 lightDir = normalize(lightPosition - vPosition);
float diffuse = dot(normal, lightDir);