Débogage JavaScript

La majeure partie de cet article ne concerne pas directement THREE.js, mais plutôt le débogage de JavaScript en général. Cela semblait important car de nombreuses personnes qui débutent avec THREE.js commencent également avec JavaScript, j'espère donc que cela pourra les aider à résoudre plus facilement les problèmes qu'ils rencontrent.

Le débogage est un vaste sujet et je ne peux probablement pas couvrir tout ce qu'il y a à savoir, mais si vous débutez en JavaScript, voici quelques pistes. Je vous suggère fortement de prendre le temps de les apprendre. Elles vous aideront énormément dans votre apprentissage.

Apprenez à utiliser les outils de développement de votre navigateur

Tous les navigateurs disposent d'outils de développement. Chrome, Firefox, Safari, Edge.

Dans Chrome, vous pouvez cliquer sur l'icône , choisir Plus d'outils -> Outils de développement pour accéder aux outils de développement. Un raccourci clavier est également affiché.

Dans Firefox, vous cliquez sur l'icône , choisissez "Développeur Web", puis "Activer/Désactiver les outils"

Dans Safari, vous devez d'abord activer le menu Développement depuis les Préférences avancées de Safari.

Puis, dans le menu Développement, vous pouvez choisir "Afficher/Connecter l'inspecteur web".

Avec Chrome, vous pouvez également utiliser Chrome sur votre ordinateur pour déboguer des pages web s'exécutant dans Chrome sur votre téléphone ou tablette Android. De même avec Safari, vous pouvez utiliser votre ordinateur pour déboguer des pages web s'exécutant dans Safari sur des iPhones et iPads.

Je suis plus familier avec Chrome, donc ce guide utilisera Chrome comme exemple pour faire référence aux outils, mais la plupart des navigateurs ont des fonctionnalités similaires, il devrait donc être facile d'appliquer ce qui est dit ici à tous les navigateurs.

Désactiver le cache

Les navigateurs essaient de réutiliser les données qu'ils ont déjà téléchargées. C'est excellent pour les utilisateurs, car si vous visitez un site web une deuxième fois, de nombreux fichiers utilisés pour afficher le site n'auront pas à être téléchargés à nouveau.

D'autre part, cela peut être problématique pour le développement web. Vous modifiez un fichier sur votre ordinateur, rechargez la page, et vous ne voyez pas les changements car le navigateur utilise la version qu'il a obtenue la dernière fois.

Une solution pendant le développement web est de désactiver le cache. Ainsi, le navigateur obtiendra toujours les versions les plus récentes de vos fichiers.

Choisissez d'abord les paramètres dans le menu du coin

Puis choisissez "Désactiver le cache (lorsque les outils de développement sont ouverts)".

Utiliser la console JavaScript

Dans tous les outils de développement se trouve une console. Elle affiche les avertissements et les messages d'erreur.

LISEZ LES MESSAGES !!

Typiquement, il ne devrait y avoir que 1 ou 2 messages.

Si vous en voyez d'autres, LISEZ-LES. Par exemple :

J'ai mal orthographié "three" en "threee"

Vous pouvez également afficher vos propres informations dans la console avec console.log, comme ceci :

console.log(someObject.position.x, someObject.position.y, someObject.position.z);

Encore mieux, si vous affichez un objet, vous pouvez l'inspecter. Par exemple, si nous affichons l'objet scène racine de l'article sur les gLTF

  {
    const gltfLoader = new GLTFLoader();
    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
      const root = gltf.scene;
      scene.add(root);
+      console.log(root);

Nous pouvons ensuite développer cet objet dans la console JavaScript

Vous pouvez également utiliser console.error qui rapporte le message en rouge et inclut une trace de pile.

Afficher des données à l'écran

Une autre méthode évidente mais souvent négligée est d'ajouter des balises <div> ou <pre> et d'y insérer des données.

La manière la plus évidente est de créer des éléments HTML

<canvas id="c"></canvas>
+<div id="debug">
+  <div>x:<span id="x"></span></div>
+  <div>y:<span id="y"></span></div>
+  <div>z:<span id="z"></span></div>
+</div>

Stylez-les pour qu'ils restent au-dessus du canevas. (en supposant que votre canevas remplisse la page)

<style>
#debug {
  position: absolute;
  left: 1em;
  top: 1em;
  padding: 1em;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  font-family: monospace;
}
</style>

Puis recherchez les éléments et définissez leur contenu.

// au moment de l'initialisation
const xElem = document.querySelector('#x');
const yElem = document.querySelector('#y');
const zElem = document.querySelector('#z');

// au moment du rendu ou de la mise à jour
xElem.textContent = someObject.position.x.toFixed(3);
yElem.textContent = someObject.position.y.toFixed(3);
zElem.textContent = someObject.position.z.toFixed(3);

C'est plus utile pour les valeurs en temps réel

Une autre façon d'afficher des données à l'écran est de créer un logger à effacement. Je viens d'inventer ce terme, mais de nombreux jeux sur lesquels j'ai travaillé ont utilisé cette solution. L'idée est d'avoir un tampon qui affiche des messages pour une seule image. Toute partie de votre code qui souhaite afficher des données appelle une fonction pour ajouter des données à ce tampon à chaque image. C'est beaucoup moins de travail que de créer un élément par donnée comme ci-dessus.

Par exemple, modifions le HTML ci-dessus pour qu'il soit juste ceci :

<canvas id="c"></canvas>
<div id="debug">
  <pre></pre>
</div>

Et créons une classe simple pour gérer ce tampon d'effacement arrière.

class ClearingLogger {
  constructor(elem) {
    this.elem = elem;
    this.lines = [];
  }
  log(...args) {
    this.lines.push([...args].join(' '));
  }
  render() {
    this.elem.textContent = this.lines.join('\n');
    this.lines = [];
  }
}

Ensuite, créons un exemple simple qui, chaque fois que nous cliquons avec la souris, crée un maillage qui se déplace dans une direction aléatoire pendant 2 secondes. Nous commencerons avec l'un des exemples de l'article sur rendre les choses réactives

Voici le code qui ajoute un nouveau Mesh chaque fois que nous cliquons avec la souris

const geometry = new THREE.SphereGeometry();
const material = new THREE.MeshBasicMaterial({color: 'red'});

const things = [];

function rand(min, max) {
  if (max === undefined) {
    max = min;
    min = 0;
  }
  return Math.random() * (max - min) + min;
}

function createThing() {
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);
  things.push({
    mesh,
    timer: 2,
    velocity: new THREE.Vector3(rand(-5, 5), rand(-5, 5), rand(-5, 5)),
  });
}

canvas.addEventListener('click', createThing);

Et voici le code qui déplace les maillages que nous avons créés, les enregistre, et les supprime lorsque leur minuterie est écoulée

const logger = new ClearingLogger(document.querySelector('#debug pre'));

let then = 0;
function render(now) {
  now *= 0.001;  // convertir en secondes
  const deltaTime = now - then;
  then = now;

  ...

  logger.log('fps:', (1 / deltaTime).toFixed(1));
  logger.log('num things:', things.length);
  for (let i = 0; i < things.length;) {
    const thing = things[i];
    const mesh = thing.mesh;
    const pos = mesh.position;
    logger.log(
        'timer:', thing.timer.toFixed(3),
        'pos:', pos.x.toFixed(3), pos.y.toFixed(3), pos.z.toFixed(3));
    thing.timer -= deltaTime;
    if (thing.timer <= 0) {
      // supprimer cet élément. Notez que nous n'avançons pas `i`
      things.splice(i, 1);
      scene.remove(mesh);
    } else {
      mesh.position.addScaledVector(thing.velocity, deltaTime);
      ++i;
    }
  }

  renderer.render(scene, camera);
  logger.render();

  requestAnimationFrame(render);
}

Cliquez maintenant plusieurs fois avec la souris dans l'exemple ci-dessous

Paramètres de requête

Une autre chose à retenir est que les pages web peuvent se voir passer des données soit via des paramètres de requête, soit via l'ancre, parfois appelée la recherche et le hachage.

https://domain/path/?query#anchor

Vous pouvez l'utiliser pour rendre des fonctionnalités optionnelles ou passer des paramètres.

Par exemple, prenons l'exemple précédent et faisons en sorte que les informations de débogage n'apparaissent que si nous mettons ?debug=true dans l'URL.

Nous avons d'abord besoin de code pour analyser la chaîne de requête

/**
  * Renvoie les paramètres de requête sous forme d'objet clé/valeur.
  * Exemple : Si les paramètres de requête sont
  *
  *    abc=123&def=456&name=gman
  *
  * Alors `getQuery()` renverra un objet comme
  *
  *    {
  *      abc: '123',
  *      def: '456',
  *      name: 'gman',
  *    }
  */
function getQuery() {
  return Object.fromEntries(new URLSearchParams(window.location.search).entries());
}

Nous pourrions ensuite faire en sorte que l'élément de débogage ne s'affiche pas par défaut

<canvas id="c"></canvas>
+<div id="debug" style="display: none;">
  <pre></pre>
</div>

Ensuite, dans le code, nous lisons les paramètres et choisissons de rendre visible les informations de débogage si et seulement si ?debug=true est passé en paramètre

const query = getQuery();
const debug = query.debug === 'true';
const logger = debug
   ? new ClearingLogger(document.querySelector('#debug pre'))
   : new DummyLogger();
if (debug) {
  document.querySelector('#debug').style.display = '';
}

Nous avons également créé un DummyLogger qui ne fait rien et avons choisi de l'utiliser si ?debug=true n'a pas été passé en paramètre.

class DummyLogger {
  log() {}
  render() {}
}

Vous pouvez voir si nous utilisons cette URL :

debug-js-params.html

il n'y a pas d'informations de débogage, mais si nous utilisons cette URL :

debug-js-params.html?debug=true

il y a des informations de débogage.

Plusieurs paramètres peuvent être passés en les séparant par '&', comme dans somepage.html?someparam=somevalue&someotherparam=someothervalue. En utilisant des paramètres comme ceci, nous pouvons passer toutes sortes d'options. Peut-être speed=0.01 pour ralentir notre application afin de faciliter la compréhension de quelque chose, ou showHelpers=true pour indiquer s'il faut ajouter des helpers qui affichent les lumières, les ombres ou le frustum de la caméra vus dans d'autres leçons.

Apprenez à utiliser le Débogueur

Chaque navigateur dispose d'un débogueur où vous pouvez mettre votre programme en pause, l'exécuter pas à pas ligne par ligne et inspecter toutes les variables.

Vous apprendre à utiliser un débogueur est un sujet trop vaste pour cet article, mais voici quelques liens

Vérifiez la présence de NaN dans le débogueur ou ailleurs

NaN est l'abréviation de Not A Number (Pas un Nombre). C'est la valeur que JavaScript attribuera lorsque vous faites quelque chose qui n'a pas de sens mathématique.

Voici un exemple simple :

Souvent, quand je crée quelque chose et que rien n'apparaît à l'écran, je vérifie certaines valeurs et si je vois NaN, j'ai instantanément un point de départ pour chercher.

À titre d'exemple, lorsque j'ai commencé à créer le chemin pour l'article sur le chargement de fichiers gLTF, j'ai créé une courbe en utilisant la classe SplineCurve qui crée une courbe 2D.

J'ai ensuite utilisé cette courbe pour déplacer les voitures comme ceci :

curve.getPointAt(zeroToOnePointOnCurve, car.position);

En interne, curve.getPointAt appelle la fonction set sur l'objet passé comme deuxième argument. Dans ce cas, ce deuxième argument est car.position qui est un Vector3. La fonction set de Vector3 nécessite 3 arguments (x, y et z) mais SplineCurve est une courbe 2D, elle appelle donc car.position.set avec juste x et y.

Le résultat est que car.position.set définit x sur x, y sur y, et z sur undefined.

Un rapide coup d'œil dans le débogueur sur la matrixWorld de la voiture a montré un tas de valeurs NaN.

Voir que la matrice contenait des NaN suggérait que quelque chose comme position, rotation, scale ou une autre fonction qui affecte cette matrice avait de mauvaises données. En remontant à partir de là, il a été facile de trouver le problème.

En plus de NaN, il y a aussi Infinity qui est un signe similaire qu'il y a un bug mathématique quelque part.

Regardez le code !

THREE.js est Open Source. N'ayez pas peur de regarder le code ! Vous pouvez regarder à l'intérieur sur github. Vous pouvez également regarder à l'intérieur en entrant dans les fonctions du débogueur.

Mettez requestAnimationFrame en bas de votre fonction de rendu.

Je vois souvent ce schéma

function render() {
   requestAnimationFrame(render);

   // -- faire des choses --

   renderer.render(scene, camera);
}
requestAnimationFrame(render);

Je suggérerais de mettre l'appel à requestAnimationFrame en bas, comme ceci :

function render() {
   // -- faire des choses --

   renderer.render(scene, camera);

   requestAnimationFrame(render);
}
requestAnimationFrame(render);

La raison principale est que cela signifie que votre code s'arrêtera si vous avez une erreur. Mettre requestAnimationFrame en haut signifie que votre code continuera de s'exécuter même si vous avez une erreur puisque vous avez déjà demandé une autre image. À mon avis, il vaut mieux trouver ces erreurs que les ignorer. Elles pourraient facilement être la raison pour laquelle quelque chose n'apparaît pas comme vous l'attendez, mais à moins que votre code ne s'arrête, vous pourriez même ne pas le remarquer.

Vérifiez vos unités !

Cela signifie essentiellement savoir, par exemple, quand utiliser des degrés plutôt que des radians. Il est regrettable que THREE.js n'utilise pas uniformément les mêmes unités partout. De mémoire, le champ de vision de la caméra est en degrés. Tous les autres angles sont en radians.

L'autre point à surveiller est la taille de vos unités mondiales. Jusqu'à récemment, les applications 3D pouvaient choisir la taille d'unité qu'elles voulaient. Une application pouvait choisir 1 unité = 1 cm. Une autre pouvait choisir 1 unité = 1 pied. Il est toujours vrai que vous pouvez choisir les unités que vous voulez pour certaines applications. Cela dit, THREE.js suppose 1 unité = 1 mètre. C'est important pour des choses comme le rendu basé sur la physique qui utilise des mètres pour calculer les effets d'éclairage. C'est également important pour la RA et la RV qui doivent gérer des unités du monde réel, comme l'emplacement de votre téléphone ou des contrôleurs VR.

Créer un Exemple Minimal, Complet et Vérifiable pour Stack Overflow

Si vous décidez de poser une question sur THREE.js, il est presque toujours requis de fournir un MCVE, qui signifie Exemple Minimal, Complet et Vérifiable.

La partie Minimale est importante. Disons que vous avez un problème avec le mouvement le long du chemin dans le dernier exemple de l'article sur le chargement de gLTF. Cet exemple contient de nombreuses parties. En les listant, il y a :

  1. Beaucoup de HTML
  2. Du CSS
  3. Lumières
  4. Ombres
  5. Code lil-gui pour manipuler les ombres
  6. Code pour charger un fichier .GLTF
  7. Code pour redimensionner le canevas.
  8. Code pour déplacer les voitures le long des chemins

C'est assez énorme. Si votre question ne concerne que la partie suivant le chemin, vous pouvez retirer la majeure partie du HTML car vous n'avez besoin que d'une balise <canvas> et d'une balise <script> pour THREE.js. Vous pouvez retirer le code CSS et le code de redimensionnement. Vous pouvez retirer le code .GLTF car vous ne vous souciez que du chemin. Vous pouvez retirer les lumières et les ombres en utilisant un MeshBasicMaterial. Vous pouvez certainement retirer le code lil-gui. Le code crée un plan de sol avec une texture. Il serait plus simple d'utiliser un GridHelper. Enfin, si notre question concerne le déplacement d'objets sur un chemin, nous pourrions simplement utiliser des cubes sur le chemin au lieu de modèles de voitures chargés.

Voici un exemple plus minimal prenant en compte tout ce qui précède. Il est passé de 271 lignes à 135. Nous pourrions envisager de le réduire encore plus en simplifiant notre chemin. Peut-être qu'un chemin avec 3 ou 4 points fonctionnerait aussi bien que notre chemin avec 21 points.

J'ai gardé l'OrbitController simplement parce qu'il est utile pour que d'autres personnes puissent déplacer la caméra et comprendre ce qui se passe, mais en fonction de votre problème, vous pourriez également être en mesure de le supprimer.

La meilleure chose à propos de la création d'un MCVE est que nous résolvons souvent notre propre problème. Le processus consistant à supprimer tout ce qui n'est pas nécessaire et à créer le plus petit exemple possible reproduisant le problème nous conduit le plus souvent à notre bug.

De plus, c'est respectueux du temps de toutes les personnes à qui vous demandez de regarder votre code sur Stack Overflow. En créant l'exemple minimal, vous leur facilitez grandement la tâche de vous aider. Vous apprendrez également au cours du processus.

Il est également important, lorsque vous allez sur Stack Overflow pour poster votre question, de mettre votre code dans un extrait (snippet). Bien sûr, vous êtes libre d'utiliser JSFiddle ou Codepen ou un site similaire pour tester votre MCVE, mais une fois que vous postez réellement votre question sur Stack Overflow, vous êtes tenu de mettre le code pour reproduire votre problème dans la question elle-même. En créant un extrait, vous remplissez cette exigence.

Notez également que tous les exemples en direct sur ce site devraient fonctionner comme des extraits. Copiez simplement les parties HTML, CSS et JavaScript dans les sections respectives de l'éditeur d'extraits. N'oubliez pas d'essayer de retirer les parties qui ne sont pas pertinentes pour votre problème et d'essayer de rendre votre code le plus minimal possible.

Suivez ces suggestions et vous aurez beaucoup plus de chances d'obtenir de l'aide pour votre problème.

Utilisez un MeshBasicMaterial

Étant donné que le MeshBasicMaterial n'utilise pas de lumières, c'est un moyen d'éliminer les raisons pour lesquelles quelque chose pourrait ne pas s'afficher. Si vos objets s'affichent en utilisant le MeshBasicMaterial mais pas avec les matériaux que vous utilisiez, alors vous savez que le problème vient probablement des matériaux ou des lumières et non d'une autre partie du code.

Vérifiez les paramètres near et far de votre caméra

Une PerspectiveCamera a des paramètres near et far qui sont couverts dans l'article sur les caméras. Assurez-vous qu'ils sont définis pour correspondre à l'espace qui contient vos objets. Vous pourriez même les définir temporairement à quelque chose de grand comme near = 0.001 et far = 1000000. Vous rencontrerez probablement des problèmes de résolution de profondeur, mais vous pourrez au moins voir vos objets à condition qu'ils soient devant la caméra.

Vérifiez que votre scène est devant la caméra

Parfois, les choses n'apparaissent pas parce qu'elles ne sont pas devant la caméra. Si votre caméra n'est pas contrôlable, essayez d'ajouter un contrôle de caméra comme l'OrbitController afin de pouvoir regarder autour de vous et trouver votre scène. Ou, essayez de cadrer la scène en utilisant du code, comme décrit dans cet article. Ce code trouve la taille d'une partie de la scène, puis déplace la caméra et ajuste les paramètres near et far pour la rendre visible. Vous pouvez ensuite regarder dans le débogueur ou ajouter des messages console.log pour afficher la taille et le centre de la scène.

Mettez quelque chose devant la caméra

C'est juste une autre façon de dire que si tout le reste échoue, commencez par quelque chose qui fonctionne, puis ajoutez progressivement des éléments. Si vous obtenez un écran vide, essayez de mettre quelque chose directement devant la caméra. Créez une sphère ou une boîte, donnez-lui un matériau simple comme le MeshBasicMaterial et assurez-vous que vous pouvez l'afficher à l'écran. Puis commencez à ajouter des éléments petit à petit et à tester. Finalement, vous reproduirez votre bug ou vous le trouverez en chemin.


Voici quelques conseils pour le débogage de JavaScript. Passons également en revue quelques conseils pour le débogage de GLSL.