Why Use .normalize()? — Understanding Vector Normalization through Three.js and Blender
An in-depth look at why we normalize vectors, connecting game movement logic with Blender's "Apply Scale."
An in-depth look at why we normalize vectors, connecting game movement logic with Blender's "Apply Scale."
The password you enter is used to open, edit, and delete secret comments.
Studying Three.js, you often come across .normalize() attached to vectors almost like a formula. In lectures, it was just mentioned to be «useful», leaving me even more curious. Why do we need to make an intact vector length 1? When is it absolutely necessary, and when is it not? I realized this wasn't a new concept. Frequent Blender users learn to always use Apply Scale after adjusting scale. The usual explanation is «it might break later», but I never really understood why and did it out of habit. Delving into normalization in Three.js, I began to see that this action in Blender was actually solving the same problem in a different way.
Normalization is the process of maintaining the direction of a vector unchanged while making its length 1. It discards «how far you go» and retains only «which direction you point». Thus, normalized vectors are used when representing information where direction is more important than magnitude, such as direction, view direction, normal, and movement input vectors.
Mathematically, it is the vector v divided by its magnitude |v|. A vector like (3, 4, 0) has a length of 5, so when normalized, it becomes (0.6, 0.8, 0). The length becomes 1, but the direction remains the same.
Grid-based games like chess, old Pokémon games, and turn-based strategy simulations don't require normalization. Such game worlds are not truly continuous spaces. One square left, one square right, one diagonal square each «distance differently» mathematically, but within the game's rules, they all count as «one move». Even if the diagonal measures √2 ≈ 1.41 mathematically, if the rule defines it as «one square», that's the standard. The exact distance value isn't what's crucial here, it's the rule that's important, eliminating concerns like «isn't diagonal faster?»
The space in Three.js is entirely different. Coordinates are (x, y, z), values are all Floats, and movement is by distance computation, not «squares». The issue becomes clear without normalization.
Consider pressing W + D keys simultaneously. The input vector is (1, 1), and the length of this vector is √2 ≈ 1.41. Within one frame, linear movement travels 1 unit, but diagonal movement travels 1.41 units. This isn't just a perceivable speed issue; it's an actual change in positional values.
This difference can lead to significant issues. Moving diagonally across thin walls or corners can cause collision detection bugs where walls are passed through in one frame, and characters moving diagonally to the same destination always arriving first. This is not a speed setting error but a gain in distance.
In free movement games, this is often the flow. First, input is interpreted as directional intent. (1, 1) means «want to go diagonally». Applying .normalize() to this results in (0.707, 0.707). Then, you multiply it by speed.
After normalization, rightward movement (1, 0), upward movement (0, 1), and diagonal movement (1, 1) all have a length of 1. You appear to move diagonally, but by position value standards, you always travel the same distance.
Here's where Blender reconnects. The Normal Vector in Blender indicates the direction a face is pointing during lighting calculations. This normal is calculated under the premise that its length is always 1.
If you increase scale and don't Apply Scale, internal vector lengths increase too. The normal no longer only represents «directional value».
Lighting calculations utilize the Dot Product. When both the face normal and light direction vector have a length of 1, the dot product results in a value between -1 ~ 1, which maps to the brightness we expect. However, if a face's normal length is 100 and the light's direction length is 50, the dot product results in 5000. This causes the screen to turn white, colors to pop bizarrely, or shading to appear disrupted. Apply Scale effectively means «reset these vectors to a length of 1 standard».
.normalize() alters the original vector directly.
If you need to maintain the position or force magnitude, you must copy it first.
const direction = v.At first, not knowing this, many experience their object flying towards the origin when calling .normalize() directly on position vectors.
You don't need to apply it everywhere. However, when the word «direction» arises, it's almost mandatory.
Using controllers like OrbitControls, it's easy to think there's almost no need to use .normalize() initially as it's handled internally. However, the story changes when directly crafting movement logic accepting keyboard input or click coordinates. Simple coordinate movements like clearly define «how much to move», so normalization isn't required. However, movements with specific directions or movements based on camera directions or input vectors must use it.
Without normalization, the speed value varies with distance. The closer the goal, the slower it moves, resulting in strange speeds where objects farther away move faster.
When crafting 3D web interactions with Three.js, Raycaster is almost always used. A seemingly invisible laser shot from the mouse position into the screen is the directional vector. Internally, raycaster.setFromCamera(mouse, camera) assumes the direction vector is normalized. Insert an unnormalized vector, and click detection occurs in peculiar places or ray extends excessively affecting performance.
When using built-in materials like MeshStandardMaterial or , Three.js internally normalizes all normals and directional vectors. However, strong distortions like or writing directly means you must manage it yourself.
In shaders, numbers directly translate to screen results. Missing normalization causes excessive brightness, color distortion, or flickering outcomes frame by frame.
Normalization maintains a fair distance standard for movement and a brightness standard for lighting. It intentionally separates direction and magnitude, effectively indicating «this value only represents direction». Ultimately, .normalize() serves to control the values' size, maintaining a consistent calculation standard. If a situation requires only direction, it's naturally accompanied by this process.
const direction = inputVector.clone().normalize();
mesh.position.add(direction.multiplyScalar(speed));const v = new THREE.Vector3(3, 4, 0);
v.normalize();
console.log(v); // Vector3 { x: 0.6, y: 0.8, z: 0 }mesh.position.x += 1const direction = target.clone().sub(mesh.position).normalize();
mesh.position.add(direction.multiplyScalar(speed));mesh.scale.set(10, 1, 1)ShaderMaterialvec3 lightDir = normalize(lightPosition - vPosition);
float diffuse = dot(normal, lightDir);A record of the intense 7-week journey: from improving onboarding and infrastructure monitoring to implementing real-time sockets and 3D character optimization.
A record of building the foundation: from ideation and prototyping to navigating senior feedback and implementing the MVP
An in-depth look at why we normalize vectors, connecting game movement logic with Blender's "Apply Scale."
A reflection on a 6-week team sprint: covering architectural challenges, the nuances of collaboration, and technical growth through senior feedback.
A reflection on the 10-week Boostcamp membership sprint: technical learning, design challenges, burnout, and lessons about using AI effectively.
A personal retrospective on the Boostcamp Web·Mobile Challenge phase: daily missions, CS learning, peer feedback, teamwork, and how I learned to grow alongside AI
A record of exploring the pros and cons of functional programming and object-oriented programming, and the reasons for choosing functional programming in React
My experience preparing for Naver Boostcamp Web·Mobile 10, completing the Basic course, and taking the problem-solving test.
A summary of my contributions to the Githru project during the OSCCA master phase, including UI improvements, issues, and Pull Requests.
My experience during the OSCCA challenge phase, including raising issues and making first Pull Request while exploring the Githru project.
A record of the issues faced when putting characters on Three.js for the Funda project and personal portfolio
A record of the intense 7-week journey: from improving onboarding and infrastructure monitoring to implementing real-time sockets and 3D character optimization.