Pourquoi utilise-t-on .normalize() — Comprendre la normalisation des vecteurs avec Three.js et Blender
Une analyse de l'essence de la normalisation à travers la logique de mouvement dans les jeux et l'application de l'échelle dans Blender.
Une analyse de l'essence de la normalisation à travers la logique de mouvement dans les jeux et l'application de l'échelle dans Blender.
Le mot de passe saisi sert à ouvrir, modifier et supprimer les commentaires secrets.
Lorsque vous étudiez Three.js, vous rencontrez souvent .normalize() accolé presque comme une formule derrière les vecteurs. Dans les cours, ils passent rapidement en disant que c'est "utile", ce qui a éveillé ma curiosité. Pourquoi prendre un vecteur qui fonctionne bien pour le réduire à une longueur de 1 ? Quand est-ce indispensable et quand peut-on s'en passer ? En y réfléchissant, ce n'était pas un concept nouveau. Si vous utilisez souvent Blender, vous entendez toujours dire qu'il faut appliquer « Apply Scale » après avoir ajusté l'échelle. La raison était toujours "pour éviter des problèmes ultérieurs", mais je l'appliquais machinalement sans vraiment comprendre pourquoi cela causait des problèmes. En approfondissant la normalisation dans Three.js, j'ai commencé à comprendre que ce que je faisais sous Blender était, en fait, résoudre le même problème différemment.
La normalisation est le processus qui consiste à . On renonce à "la distance parcourue" pour ne garder que "la direction indiquée". Ainsi, les vecteurs normalisés sont principalement utilisés lorsque la direction est plus importante que la taille, comme pour exprimer une direction (direction), une direction de vue (view direction), un normal (normal) ou un vecteur de déplacement.
En termes de formule, c'est le vecteur v divisé par sa magnitude |v|. Un vecteur comme (3, 4, 0) a une longueur de 5, donc une fois normalisé, il devient (0.6, 0.8, 0). Sa longueur est de 1 tandis que sa direction est conservée.
Dans les jeux en grille comme les échecs, les anciens jeux Pokémon ou les simulations stratégiques au tour par tour, la normalisation n'est pas nécessaire. Le monde de ces jeux n'est pas un espace continu. Un pas à gauche, un pas à droite, un pas en diagonale sont mathématiquement à des distances différentes, mais selon les règles du jeu, ils sont tous considérés comme "un déplacement". Même si une diagonale est mathématiquement √2 ≈ 1.41, si la règle la définit comme "un pas", c'est la référence. Ce qui est important, ce n'est pas la distance précise mais la règle, donc des questions comme "la diagonale est-elle plus rapide ?" ne se posent tout simplement pas.
L'espace que manipule Three.js est totalement différent. Les coordonnées sont (x, y, z), toutes les valeurs sont des réels (Float), et le déplacement est calculé non pas en "cases" mais par la distance. Ici, l'absence de normalisation pose problème immédiatement.
Supposons que vous appuyiez simultanément sur les touches W + D. Le vecteur d'entrée est (1, 1) et sa longueur est √2 ≈ 1.41. Cela signifie qu'au cours du même cadre, un déplacement en ligne droite se fait sur une distance de 1 tandis que le déplacement en diagonale est de 1,41. Ce n'est pas seulement une question de perception de vitesse, cela change réellement la position dans une plus grande proportion.
Cette différence peut avoir des conséquences plus graves que prévu. Si vous vous déplacez en diagonale à travers un mur mince ou un coin, un bug de collision peut vous faire traverser le mur en un seul cadre. De plus, un personnage se déplaçant en diagonale arrive toujours en premier au même point de destination. Ce n'est pas que la vitesse soit mal configurée, mais qu'il y a un gain de distance.
Dans les jeux à déplacement libre, on suit généralement ce flux. D'abord, l'entrée est interprétée comme une intention de direction. (1, 1) signifie « Je veux aller en diagonale ». On applique ensuite .normalize() pour obtenir (0.707, 0.707). Ensuite, on y applique la vitesse (speed).
const direction = inputVector.clone().normalize();
mesh.position.add(direction.multiplyScalar(speed));En fin de compte, que ce soit le déplacement vers la droite (1, 0), vers le haut (0, 1), ou en diagonale (1, 1), après normalisation, la longueur est de 1. Bien que visuellement vous vous déplaciez en diagonale, d'un point de vue valeur de position, vous vous déplacez toujours sur la même distance.
Ici, Blender rejoint à nouveau la discussion. Le vecteur normal (Normal Vector) de Blender indique la « direction de la face » dans le calcul de la lumière. Ce normal est toujours calculé en supposant une longueur de 1.
Si vous augmentez l'échelle sans appliquer « Apply Scale », la longueur interne du vecteur est également augmentée. Le normal n'est plus seulement une « valeur de direction ».
Le calcul de la lumière utilise des produits scalaires (Dot Product). Lorsque la normale de face et le vecteur de direction de la lumière ont tous deux une longueur de 1, le résultat du produit scalaire donne une valeur comprise entre -1 et 1, mappée sur la luminosité attendue. Cependant, si la longueur de la normale de face est de 100 et que celle de la direction de la lumière est de 50, le résultat du produit scalaire devient 5000. Cela peut rendre l'écran blanc incandescent, donner des couleurs étranges ou faire apparaître le shading comme s'il était décomposé. « Apply Scale » signifie en fin de compte "ramener ces vecteurs à une longueur de 1 comme référence ».
.normalize() modifie directement le vecteur d'origine.
const v = new THREE.Vector3(3, 4, 0);
v.normalize();
console.log(v); // Vector3 { x: 0.6, y: 0.8, z: 0 }Si vous devez conserver la position ou la magnitude, vous devez absolument utiliser une copie.
const direction = v.clone().normalize();Beaucoup rapportent avoir fait l'expérience de voir un objet s'envoler près de l'origine parce qu'ils ont appelé .normalize() directement sur le vecteur de position sans le savoir.
Il n'est pas nécessaire de l'appliquer partout. Cependant, dès que le mot « direction » surgit, on l'utilise presque systématiquement.
Lorsque vous utilisez un contrôleur comme OrbitControls, tout est géré à l'interne, donc au début vous sentez que vous n'avez presque jamais besoin d'utiliser .normalize(). Cependant, si vous créez une logique de déplacement en recevant des entrées de clavier ou de coordonnées de clic, c'est une autre histoire. Un déplacement simple de coordonnées comme mesh.position.x += 1 a un "combien" clair et donc pas besoin de normaliser. Cependant, pour des déplacements avec une vitesse donnée basés sur une direction spécifique, la direction de la caméra ou un vecteur d'entrée, c'est indispensable.
const direction = target.clone().sub(mesh.position).normalize();
mesh.position.add(direction.multiplyScalar(speed));Sans normalisation ici, la valeur de speed est appliquée différemment selon la distance. Plus l'objectif est proche, plus on va lentement, et plus il est éloigné, plus on va vite, ce qui entraîne des résultats étranges.
Dans la création d'interactions Web 3D avec Three.js, on utilise quasiment à coup sûr Raycaster. Le rayon invisible projeté depuis la position de la souris vers l'intérieur de l'écran est précisément un vecteur de direction. À l'intérieur de raycaster.setFromCamera(mouse, camera), il est supposé que le vecteur de direction est normalisé. Si vous lui transmettez un vecteur non normalisé, la détection de clics peut se produire à des emplacements incorrects ou le rayon peut être trop long, nuisant ainsi aux performances.
Lorsque vous utilisez les matériaux intégrés comme MeshStandardMaterial, MeshPhongMaterial, Three.js normalise tous les normaux et vecteurs de direction en interne. Cependant, lorsqu’on applique des déformations d'échelle intenses comme mesh.scale.set(10, 1, 1) ou qu’on écrit directement ShaderMaterial, c'est à vous d’en prendre soin.
vec3 lightDir = normalize(lightPosition - vPosition);
float diffuse = dot(normal, lightDir);Dans un shader, les nombres se reflètent directement dans le résultat à l'écran. Si la normalisation est omise, la lumière peut devenir excessivement brillante, les couleurs peuvent être cassées ou le résultat peut vaciller à chaque frame.
La normalisation vous permet de maintenir une référence équitable de distance dans le mouvement et une référence de luminosité dans les calculs de lumière. Elle sépare intentionnellement la direction et la taille, indiquant que "cette valeur représente uniquement la direction". En somme, .normalize() joue le rôle de contrôle de la grandeur pour maintenir une référence constante dans les calculs. Lorsqu'il est question uniquement de direction, on peut comprendre qu'il s'agit d'un traitement qui accompagne naturellement.
Enregistrement des problèmes rencontrés lors de l'intégration de personnages dans Three.js pour le projet funda et mon portfolio personnel
Le récit d'une aventure intense de 7 semaines : de l'amélioration de l'onboarding et du monitoring d'infrastructure à la communication temps réel et l'optimisation de personnages 3D.
Le récit d'une aventure intense de 7 semaines : de l'amélioration de l'onboarding et du monitoring d'infrastructure à la communication temps réel et l'optimisation de personnages 3D.
De l'idéation à l'implémentation du MVP en passant par les retours seniors : récit de la mise en place des fondations du projet et des défis techniques relevés
Une analyse de l'essence de la normalisation à travers la logique de mouvement dans les jeux et l'application de l'échelle dans Blender.
Retour sur les 6 semaines de sprint de groupe : défis d'architecture, dynamiques de collaboration et progression technique suite aux retours seniors.
Retour sur dix semaines d’apprentissage au Boostcamp : technologies étudiées, défis de conception, fatigue mentale et enseignements tirés de l’utilisation de l’IA.
Un retour sur la phase Challenge du Boostcamp : missions quotidiennes, apprentissage CS, feedback entre pairs, travail d’équipe et évolution de ma façon d’apprendre avec l’IA.
Exploration des avantages et inconvénients de la programmation fonctionnelle et orientée objet, et des raisons du choix fonctionnel dans React
Retour d’expérience sur la préparation, la phase Basic et le test de résolution de problèmes du Boostcamp Web·Mobile 10.
Un résumé de mon expérience de contribution au projet Githru pendant la phase Master de l’OSCCA.
Mon expérience durant la phase Challenge de l'OSCCA : propositions d’issues et première Pull Request sur le projet Githru.