# Chargement d'un fichier .GLTF

Dans une leçon précédente, nous avons chargé un fichier .OBJ. Si vous ne l'avez pas lue, vous pourriez vouloir la consulter d'abord.

Comme indiqué là-bas, le format de fichier .OBJ est très ancien et assez simple. Il ne fournit aucun graphe de scène, donc tout ce qui est chargé est un seul grand maillage. Il a été conçu principalement comme un moyen simple de passer des données entre des éditeurs 3D.

Le format gLTF est en réalité un format conçu dès le départ pour être utilisé pour afficher des graphiques. Les formats 3D peuvent être divisés en 3 ou 4 types de base.

- Formats d'éditeurs 3D

  Ce sont des formats spécifiques à une seule application. .blend (Blender), .max (3d Studio Max), .mb et .ma (Maya), etc...

- Formats d'échange

  Ce sont des formats comme .OBJ, .DAE (Collada), .FBX. Ils sont conçus pour aider à échanger des informations entre les éditeurs 3D. En tant que tels, ils sont généralement beaucoup plus volumineux que nécessaire avec des informations supplémentaires utilisées uniquement à l'intérieur des éditeurs 3D.

- Formats d'application

  Ceux-ci sont généralement spécifiques à certaines applications, généralement des jeux.

- Formats de transmission

  gLTF pourrait être le premier véritable format de transmission. Je suppose que VRML pourrait être considéré comme tel, mais VRML était en réalité un très mauvais format.

  gLTF est conçu pour bien faire certaines choses que tous ces autres formats ne font pas

  1. Être petit pour la transmission

     Par exemple, cela signifie qu'une grande partie de leurs données volumineuses, comme les sommets, est stockée en binaire. Lorsque vous téléchargez un fichier .gLTF, ces données peuvent être téléchargées sur le GPU sans aucun traitement. Elles sont prêtes telles quelles. C'est en contraste avec, par exemple, VRML, .OBJ ou .DAE où les sommets sont stockés sous forme de texte et doivent être analysés. Les positions de sommets en texte peuvent facilement être 3 à 5 fois plus volumineuses qu'en binaire.

  2. Être prêt à être rendu

     C'est encore différent des autres formats, sauf peut-être les formats d'application. Les données d'un fichier glTF sont destinées à être rendues, pas éditées. Les données qui ne sont pas importantes pour le rendu ont généralement été supprimées. Les polygones ont été convertis en triangles. Les matériaux ont des valeurs connues qui sont censées fonctionner partout.

gLTF a été spécifiquement conçu pour que vous puissiez télécharger un fichier glTF et l'afficher avec un minimum de problèmes. Croisons les doigts pour que ce soit vraiment le cas, car aucun autre format n'a été capable de faire cela.

Je n'étais pas vraiment sûr de ce que je devais montrer. À un certain niveau, le chargement et l'affichage d'un fichier gLTF sont plus simples qu'un fichier .OBJ. Contrairement à un fichier .OBJ, les matériaux font directement partie du format. Cela dit, j'ai pensé que je devais au moins en charger un et je pense qu'examiner les problèmes que j'ai rencontrés pourrait fournir de bonnes informations.

En cherchant sur le net, j'ai trouvé cette ville low-poly par antonmoek qui semblait, si nous avons de la chance, faire un bon exemple.

En partant d'un exemple de l'article sur les fichiers .OBJ, j'ai supprimé le code de chargement de .OBJ et je l'ai remplacé par le code de chargement de .GLTF.

L'ancien code .OBJ était

    const mtlLoader = new MTLLoader();
    mtlLoader.loadMtl('resources/models/windmill/windmill-fixed.mtl', (mtl) => {
      mtl.preload();
      mtl.materials.Material.side = THREE.DoubleSide;
      objLoader.setMaterials(mtl);
      objLoader.load('resources/models/windmill/windmill.obj', (event) => {
        const root = event.detail.loaderRootNode;
        scene.add(root);
        ...
      });
    });

Le nouveau code .GLTF est

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

J'ai gardé le code de cadrage automatique comme auparavant.

Nous devons également inclure le `GLTFLoader` et nous pouvons nous débarrasser du `OBJLoader`.

    -import {LoaderSupport} from 'three/addons/loaders/LoaderSupport.js';
    -import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
    -import {MTLLoader} from 'three/addons/loaders/MTLLoader.js';
    +import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';

Et en exécutant cela, nous obtenons

cliquez ici pour ouvrir dans une fenêtre séparée

Magie ! Ça fonctionne tout seul, textures comprises.

Ensuite, je voulais voir si je pouvais animer les voitures qui circulent, j'ai donc eu besoin de vérifier si la scène avait les voitures comme entités séparées et si elles étaient configurées d'une manière que je pouvais utiliser.

J'ai écrit du code pour afficher le graphe de scène dans la console JavaScript.

Voici le code pour imprimer le graphe de scène.

    function dumpObject(obj, lines = [], isLast = true, prefix = '') {
      const localPrefix = isLast ? '└─' : '├─';
      lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`);
      const newPrefix = prefix + (isLast ? '  ' : '│ ');
      const lastNdx = obj.children.length - 1;
      obj.children.forEach((child, ndx) => {
        const isLast = ndx === lastNdx;
        dumpObject(child, lines, isLast, newPrefix);
      });
      return lines;
    }

Et je l'ai appelée juste après avoir chargé la scène.

    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(dumpObject(root).join('\n'));

En exécutant cela, j'ai obtenu cette liste

    OSG_Scene [Scene]
      └─RootNode_(gltf_orientation_matrix) [Object3D]
        └─RootNode_(model_correction_matrix) [Object3D]
          └─4d4100bcb1c640e69699a87140df79d7fbx [Object3D]
            └─RootNode [Object3D]
              │ ...
              ├─Cars [Object3D]
              │ ├─CAR_03_1 [Object3D]
              │ │ └─CAR_03_1_World_ap_0 [Mesh]
              │ ├─CAR_03 [Object3D]
              │ │ └─CAR_03_World_ap_0 [Mesh]
              │ ├─Car_04 [Object3D]
              │ │ └─Car_04_World_ap_0 [Mesh]
              │ ├─CAR_03_2 [Object3D]
              │ │ └─CAR_03_2_World_ap_0 [Mesh]
              │ ├─Car_04_1 [Object3D]
              │ │ └─Car_04_1_World_ap_0 [Mesh]
              │ ├─Car_04_2 [Object3D]
              │ │ └─Car_04_2_World_ap_0 [Mesh]
              │ ├─Car_04_3 [Object3D]
              │ │ └─Car_04_3_World_ap_0 [Mesh]
              │ ├─Car_04_4 [Object3D]
              │ │ └─Car_04_4_World_ap_0 [Mesh]
              │ ├─Car_08_4 [Object3D]
              │ │ └─Car_08_4_World_ap8_0 [Mesh]
              │ ├─Car_08_3 [Object3D]
              │ │ └─Car_08_3_World_ap9_0 [Mesh]
              │ ├─Car_04_1_2 [Object3D]
              │ │ └─Car_04_1_2_World_ap_0 [Mesh]
              │ ├─Car_08_2 [Object3D]
              │ │ └─Car_08_2_World_ap11_0 [Mesh]
              │ ├─CAR_03_1_2 [Object3D]
              │ │ └─CAR_03_1_2_World_ap_0 [Mesh]
              │ ├─CAR_03_2_2 [Object3D]
              │ │ └─CAR_03_2_2_World_ap_0 [Mesh]
              │ ├─Car_04_2_2 [Object3D]
              │ │ └─Car_04_2_2_World_ap_0 [Mesh]
              ...

À partir de cela, nous pouvons voir que toutes les voitures se trouvent sous un parent appelé `"Cars"`.

    *          ├─Cars [Object3D]
              │ ├─CAR_03_1 [Object3D]
              │ │ └─CAR_03_1_World_ap_0 [Mesh]
              │ ├─CAR_03 [Object3D]
              │ │ └─CAR_03_World_ap_0 [Mesh]
              │ ├─Car_04 [Object3D]
              │ │ └─Car_04_World_ap_0 [Mesh]

Donc, comme test simple, j'ai pensé que j'essaierais juste de faire tourner tous les enfants du nœud "Cars" autour de leur axe Y.

J'ai cherché le nœud "Cars" après avoir chargé la scène et j'ai sauvegardé le résultat.

    +let cars;
    {
      const gltfLoader = new GLTFLoader();
      gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
        const root = gltf.scene;
        scene.add(root);
    +    cars = root.getObjectByName('Cars');

Ensuite, dans la fonction `render`, nous pouvons simplement définir la rotation de chaque enfant de `cars`.

    +function render(time) {
    +  time *= 0.001;  // convertir en secondes

      if (resizeRendererToDisplaySize(renderer)) {
        const canvas = renderer.domElement;
        camera.aspect = canvas.clientWidth / canvas.clientHeight;
        camera.updateProjectionMatrix();
      }

    +  if (cars) {
    +    for (const car of cars.children) {
    +      car.rotation.y = time;
    +    }
    +  }

      renderer.render(scene, camera);

      requestAnimationFrame(render);
    }

Et nous obtenons

cliquez ici pour ouvrir dans une fenêtre séparée

Hmmm, il semble que malheureusement cette scène n'a pas été conçue pour animer les voitures, car leurs origines ne sont pas configurées à cette fin. Les camions tournent dans la mauvaise direction.

Cela soulève un point important : si vous allez faire quelque chose en 3D, vous devez planifier à l'avance et concevoir vos éléments de manière à ce que leurs origines soient aux bons endroits, qu'ils aient la bonne échelle, etc.

Comme je ne suis pas un artiste et que je ne connais pas très bien Blender, je vais bricoler cet exemple. Nous allons prendre chaque voiture et la faire appartenir à un autre `Object3D`. Nous allons ensuite déplacer ces objets `Object3D` pour déplacer les voitures, mais séparément, nous pouvons définir l'`Object3D` d'origine de la voiture pour la réorienter afin qu'elle soit à peu près là où nous en avons vraiment besoin.

En regardant à nouveau la liste du graphe de scène, il semble qu'il n'y ait en réalité que 3 types de voitures : "Car_08", "CAR_03" et "Car_04". Espérons que chaque type de voiture fonctionnera avec les mêmes ajustements.

J'ai écrit ce code pour passer en revue chaque voiture, la faire appartenir à un nouvel `Object3D`, faire appartenir ce nouvel `Object3D` à la scène, appliquer des paramètres par *type* de voiture pour corriger son orientation, et ajouter le nouvel `Object3D` à un tableau `cars`.

    -let cars;
    +const cars = [];
    {
      const gltfLoader = new GLTFLoader();
      gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
        const root = gltf.scene;
        scene.add(root);

    -    cars = root.getObjectByName('Cars');
    +    const loadedCars = root.getObjectByName('Cars');
    +    const fixes = [
    +      { prefix: 'Car_08', rot: [Math.PI * .5, 0, Math.PI * .5], },
    +      { prefix: 'CAR_03', rot: [0, Math.PI, 0], },
    +      { prefix: 'Car_04', rot: [0, Math.PI, 0], },
    +    ];
    +
    +    root.updateMatrixWorld();
    +    for (const car of loadedCars.children.slice()) {
    +      const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
    +      const obj = new THREE.Object3D();
    +      car.getWorldPosition(obj.position);
    +      car.position.set(0, 0, 0);
    +      car.rotation.set(...fix.rot);
    +      obj.add(car);
    +      scene.add(obj);
    +      cars.push(obj);
    +    }
         ...

Cela corrige l'orientation des voitures.

cliquez ici pour ouvrir dans une fenêtre séparée

Maintenant, faisons-les rouler.

Faire même un simple système de conduite est trop pour ce post, mais il semble qu'au lieu de cela, nous pourrions simplement créer un chemin alambiqué qui parcourt toutes les routes, puis placer les voitures sur ce chemin. Voici une image de Blender à mi-chemin de la construction du chemin.

J'avais besoin d'un moyen d'obtenir les données de ce chemin depuis Blender. Heureusement, j'ai pu sélectionner juste mon chemin et exporter en .OBJ en cochant "write nurbs".

En ouvrant le fichier .OBJ, j'ai pu obtenir une liste de points que j'ai formatée ainsi

    const controlPoints = [
      [1.118281, 5.115846, -3.681386],
      [3.948875, 5.115846, -3.641834],
      [3.960072, 5.115846, -0.240352],
      [3.985447, 5.115846, 4.585005],
      [-3.793631, 5.115846, 4.585006],
      [-3.826839, 5.115846, -14.736200],
      [-14.542292, 5.115846, -14.765865],
      [-14.520929, 5.115846, -3.627002],
      [-5.452815, 5.115846, -3.634418],
      [-5.467251, 5.115846, 4.549161],
      [-13.266233, 5.115846, 4.567083],
      [-13.250067, 5.115846, -13.499271],
      [4.081842, 5.115846, -13.435463],
      [4.125436, 5.115846, -5.334928],
      [-14.521364, 5.115846, -5.239871],
      [-14.510466, 5.115846, 5.486727],
      [5.745666, 5.115846, 5.510492],
      [5.787942, 5.115846, -14.728308],
      [-5.423720, 5.115846, -14.761919],
      [-5.373599, 5.115846, -3.704133],
      [1.004861, 5.115846, -3.641834],
    ];

THREE.js possède des classes de courbes. La `CatmullRomCurve3` semblait pouvoir fonctionner. L'intérêt de ce type de courbe est qu'elle essaie de créer une courbe lisse passant par les points.

En fait, l'insertion directe de ces points générera une courbe comme celle-ci

mais nous voulons des coins plus marqués. Il semblait que si nous calculions des points supplémentaires, nous pourrions obtenir ce que nous voulons. Pour chaque paire de points, nous allons calculer un point à 10 % du chemin entre les 2 points et un autre à 90 % du chemin entre les 2 points, et passer le résultat à `CatmullRomCurve3`.

Cela nous donnera une courbe comme celle-ci

Voici le code pour créer la courbe

    let curve;
    let curveObject;
    {
      const controlPoints = [
        [1.118281, 5.115846, -3.681386],
        [3.948875, 5.115846, -3.641834],
        [3.960072, 5.115846, -0.240352],
        [3.985447, 5.115846, 4.585005],
        [-3.793631, 5.115846, 4.585006],
        [-3.826839, 5.115846, -14.736200],
        [-14.542292, 5.115846, -14.765865],
        [-14.520929, 5.115846, -3.627002],
        [-5.452815, 5.115846, -3.634418],
        [-5.467251, 5.115846, 4.549161],
        [-13.266233, 5.115846, 4.567083],
        [-13.250067, 5.115846, -13.499271],
        [4.081842, 5.115846, -13.435463],
        [4.125436, 5.115846, -5.334928],
        [-14.521364, 5.115846, -5.239871],
        [-14.510466, 5.115846, 5.486727],
        [5.745666, 5.115846, 5.510492],
        [5.787942, 5.115846, -14.728308],
        [-5.423720, 5.115846, -14.761919],
        [-5.373599, 5.115846, -3.704133],
        [1.004861, 5.115846, -3.641834],
      ];
      const p0 = new THREE.Vector3();
      const p1 = new THREE.Vector3();
      curve = new THREE.CatmullRomCurve3(
        controlPoints.map((p, ndx) => {
          p0.set(...p);
          p1.set(...controlPoints[(ndx + 1) % controlPoints.length]);
          return [
            (new THREE.Vector3()).copy(p0),
            (new THREE.Vector3()).lerpVectors(p0, p1, 0.1),
            (new THREE.Vector3()).lerpVectors(p0, p1, 0.9),
          ];
        }).flat(),
        true,
      );
      {
        const points = curve.getPoints(250);
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        const material = new THREE.LineBasicMaterial({color: 0xff0000});
        curveObject = new THREE.Line(geometry, material);
        scene.add(curveObject);
      }
    }

La première partie de ce code crée une courbe. La deuxième partie de ce code génère 250 points à partir de la courbe, puis crée un objet pour afficher les lignes formées en connectant ces 250 points.

En exécutant l'exemple, je n'ai pas vu la courbe. Pour la rendre visible, je l'ai fait ignorer le test de profondeur et la rendre en dernier.

        curveObject = new THREE.Line(geometry, material);
    +    material.depthTest = false;
    +    curveObject.renderOrder = 1;

Et c'est là que j'ai découvert qu'elle était beaucoup trop petite.

En vérifiant la hiérarchie dans Blender, j'ai découvert que l'artiste avait mis à l'échelle le nœud parent de toutes les voitures.

La mise à l'échelle est mauvaise pour les applications 3D en temps réel. Elle cause toutes sortes de problèmes et finit par être une source de frustration infinie lors de la création d'applications 3D en temps réel. Les artistes ne le savent souvent pas car il est si facile de mettre à l'échelle une scène entière dans un programme d'édition 3D, mais si vous décidez de créer une application 3D en temps réel, je vous suggère de demander à vos artistes de ne jamais rien mettre à l'échelle. S'ils modifient l'échelle, ils devraient trouver un moyen d'appliquer cette échelle aux sommets afin que, lorsqu'elle arrive dans votre application, vous puissiez ignorer l'échelle.

Et, pas seulement l'échelle, dans ce cas, les voitures sont tournées et décalées par leur parent, le nœud `Cars`. Cela rendra difficile au moment de l'exécution de déplacer les voitures dans l'espace mondial. Pour être clair, dans ce cas, nous voulons que les voitures circulent dans l'espace mondial, c'est pourquoi ces problèmes se posent. Si quelque chose est destiné à être manipulé dans un espace local, comme la lune tournant autour de la terre, c'est moins problématique.

Pour en revenir à la fonction que nous avons écrite ci-dessus pour afficher le graphe de scène, affichons la position, la rotation et l'échelle de chaque nœud.

    +function dumpVec3(v3, precision = 3) {
    +  return `${v3.x.toFixed(precision)}, ${v3.y.toFixed(precision)}, ${v3.z.toFixed(precision)}`;
    +}

    function dumpObject(obj, lines, isLast = true, prefix = '') {
      const localPrefix = isLast ? '└─' : '├─';
      lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`);
    +  const dataPrefix = obj.children.length
    +     ? (isLast ? '  │ ' : '│ │ ')
    +     : (isLast ? '    ' : '│   ');
    +  lines.push(`${prefix}${dataPrefix}  pos: ${dumpVec3(obj.position)}`);
    +  lines.push(`${prefix}${dataPrefix}  rot: ${dumpVec3(obj.rotation)}`);
    +  lines.push(`${prefix}${dataPrefix}  scl: ${dumpVec3(obj.scale)}`);
      const newPrefix = prefix + (isLast ? '  ' : '│ ');
      const lastNdx = obj.children.length - 1;
      obj.children.forEach((child, ndx) => {
        const isLast = ndx === lastNdx;
        dumpObject(child, lines, isLast, newPrefix);
      });
      return lines;
    }

Et le résultat de l'exécution

    OSG_Scene [Scene]
      │   pos: 0.000, 0.000, 0.000
      │   rot: 0.000, 0.000, 0.000
      │   scl: 1.000, 1.000, 1.000
      └─RootNode_(gltf_orientation_matrix) [Object3D]
        │   pos: 0.000, 0.000, 0.000
        │   rot: -1.571, 0.000, 0.000
        │   scl: 1.000, 1.000, 1.000
        └─RootNode_(model_correction_matrix) [Object3D]
          │   pos: 0.000, 0.000, 0.000
          │   rot: 0.000, 0.000, 0.000
          │   scl: 1.000, 1.000, 1.000
          └─4d4100bcb1c640e69699a87140df79d7fbx [Object3D]
            │   pos: 0.000, 0.000, 0.000
            │   rot: 1.571, 0.000, 0.000
            │   scl: 1.000, 1.000, 1.000
            └─RootNode [Object3D]
              │   pos: 0.000, 0.000, 0.000
              │   rot: 0.000, 0.000, 0.000
              │   scl: 1.000, 1.000, 1.000
              ├─Cars [Object3D]
    *          │ │   pos: -369.069, -90.704, -920.159
    *          │ │   rot: 0.000, 0.000, 0.000
    *          │ │   scl: 1.000, 1.000, 1.000
              │ ├─CAR_03_1 [Object3D]
              │ │ │   pos: 22.131, 14.663, -475.071
              │ │ │   rot: -3.142, 0.732, 3.142
              │ │ │   scl: 1.500, 1.500, 1.500
              │ │ └─CAR_03_1_World_ap_0 [Mesh]
              │ │       pos: 0.000, 0.000, 0.000
              │ │       rot: 0.000, 0.000, 0.000
              │ │       scl: 1.000, 1.000, 1.000

Cela nous montre que l'`Cars` dans la scène originale a vu sa rotation et son échelle supprimées et appliquées à ses enfants. Cela suggère que soit l'exportateur utilisé pour créer le fichier .GLTF a fait un travail spécial ici, soit plus probablement l'artiste a exporté une version différente du fichier que le fichier .blend correspondant, ce qui explique pourquoi les choses ne correspondent pas.

La morale de l'histoire est que j'aurais probablement dû télécharger le fichier .blend et exporter moi-même. Avant d'exporter, j'aurais dû inspecter tous les nœuds principaux et supprimer toute transformation.

Tous ces nœuds en haut

    OSG_Scene [Scene]
      │   pos: 0.000, 0.000, 0.000
      │   rot: 0.000, 0.000, 0.000
      │   scl: 1.000, 1.000, 1.000
      └─RootNode_(gltf_orientation_matrix) [Object3D]
        │   pos: 0.000, 0.000, 0.000
        │   rot: -1.571, 0.000, 0.000
        │   scl: 1.000, 1.000, 1.000
        └─RootNode_(model_correction_matrix) [Object3D]
          │   pos: 0.000, 0.000, 0.000
          │   rot: 0.000, 0.000, 0.000
          │   scl: 1.000, 1.000, 1.000
          └─4d4100bcb1c640e69699a87140df79d7fbx [Object3D]
            │   pos: 0.000, 0.000, 0.000
            │   rot: 1.571, 0.000, 0.000
            │   scl: 1.000, 1.000, 1.000

sont également un gaspillage.

Idéalement, la scène devrait se composer d'un seul nœud "racine" sans position, rotation ou échelle. Au moment de l'exécution, je pourrais alors retirer tous les enfants de cette racine et les faire appartenir à la scène elle-même. Il pourrait y avoir des enfants de la racine comme "Cars" qui m'aideraient à trouver toutes les voitures, mais idéalement, il n'aurait pas non plus de translation, rotation ou échelle afin que je puisse rattacher les voitures à la scène avec un minimum de travail.

En tout cas, la solution la plus rapide, même si ce n'est peut-être pas la meilleure, est de simplement ajuster l'objet que nous utilisons pour visualiser la courbe.

Voici ce que j'ai obtenu au final.

D'abord, j'ai ajusté la position de la courbe et trouvé des valeurs qui semblaient fonctionner. Je l'ai ensuite cachée.

    {
      const points = curve.getPoints(250);
      const geometry = new THREE.BufferGeometry().setFromPoints(points);
      const material = new THREE.LineBasicMaterial({color: 0xff0000});
      curveObject = new THREE.Line(geometry, material);
    +  curveObject.scale.set(100, 100, 100);
    +  curveObject.position.y = -621;
    +  curveObject.visible = false;
      material.depthTest = false;
      curveObject.renderOrder = 1;
      scene.add(curveObject);
    }

Ensuite, j'ai écrit du code pour déplacer les voitures le long de la courbe. Pour chaque voiture, nous choisissons une position de 0 à 1 le long de la courbe et calculons un point dans l'espace mondial en utilisant l'`curveObject` pour transformer le point. Nous choisissons ensuite un autre point légèrement plus loin sur la courbe. Nous définissons l'orientation de la voiture en utilisant `lookAt` et plaçons la voiture au point médian entre les 2 points.

    // créer 2 Vector3 que nous pouvons utiliser pour les calculs de chemin
    const carPosition = new THREE.Vector3();
    const carTarget = new THREE.Vector3();

    function render(time) {
      ...

    -  for (const car of cars) {
    -    car.rotation.y = time;
    -  }

    +  {
    +    const pathTime = time * .01;
    +    const targetOffset = 0.01;
    +    cars.forEach((car, ndx) => {
    +      // un nombre entre 0 et 1 pour espacer uniformément les voitures
    +      const u = pathTime + ndx / cars.length;
    +
    +      // obtenir le premier point
    +      curve.getPointAt(u % 1, carPosition);
    +      carPosition.applyMatrix4(curveObject.matrixWorld);
    +
    +      // obtenir un deuxième point légèrement plus loin sur la courbe
    +      curve.getPointAt((u + targetOffset) % 1, carTarget);
    +      carTarget.applyMatrix4(curveObject.matrixWorld);
    +
    +      // placer la voiture au premier point (temporairement)
    +      car.position.copy(carPosition);
    +      // orienter la voiture vers le deuxième point
    +      car.lookAt(carTarget);
    +
    +      // placer la voiture entre les 2 points
    +      car.position.lerpVectors(carPosition, carTarget, 0.5);
    +    });
    +  }

et quand je l'ai exécuté, j'ai découvert que pour chaque type de voiture, leur hauteur au-dessus de leurs origines n'est pas définie de manière cohérente, et j'ai donc dû décaler chacune un peu.

    const loadedCars = root.getObjectByName('Cars');
    const fixes = [
    -  { prefix: 'Car_08', rot: [Math.PI * .5, 0, Math.PI * .5], },
    -  { prefix: 'CAR_03', rot: [0, Math.PI, 0], },
    -  { prefix: 'Car_04', rot: [0, Math.PI, 0], },
    +  { prefix: 'Car_08', y: 0,  rot: [Math.PI * .5, 0, Math.PI * .5], },
    +  { prefix: 'CAR_03', y: 33, rot: [0, Math.PI, 0], },
    +  { prefix: 'Car_04', y: 40, rot: [0, Math.PI, 0], },
    ];

    root.updateMatrixWorld();
    for (const car of loadedCars.children.slice()) {
      const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
      const obj = new THREE.Object3D();
      car.getWorldPosition(obj.position);
    -  car.position.set(0, 0, 0);
    +  car.position.set(0, fix.y, 0);
      car.rotation.set(...fix.rot);
      obj.add(car);
      scene.add(obj);
      cars.push(obj);
    }

Et le résultat.

cliquez ici pour ouvrir dans une fenêtre séparée

Pas mal pour quelques minutes de travail.

La dernière chose que je voulais faire est d'activer les ombres.

Pour ce faire, j'ai pris tout le code GUI de l'exemple d'ombres `DirectionalLight` dans l'article sur les ombres et l'ai collé dans notre dernier code.

Ensuite, après le chargement, nous devons activer les ombres sur tous les objets.

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

    +    root.traverse((obj) => {
    +      if (obj.castShadow !== undefined) {
    +        obj.castShadow = true;
    +        obj.receiveShadow = true;
    +      }
    +    });

J'ai ensuite passé près de 4 heures à essayer de comprendre pourquoi les helpers d'ombre ne fonctionnaient pas. C'était parce que j'avais oublié d'activer les ombres avec

    renderer.shadowMap.enabled = true;

😭

J'ai ensuite ajusté les valeurs jusqu'à ce que la caméra d'ombre de notre `DirectionLight` ait un frustum qui couvrait toute la scène. Voici les paramètres avec lesquels j'ai fini.

    {
      const color = 0xFFFFFF;
      const intensity = 1;
      const light = new THREE.DirectionalLight(color, intensity);
    +  light.castShadow = true;
    *  light.position.set(-250, 800, -850);
    *  light.target.position.set(-550, 40, -450);

    +  light.shadow.bias = -0.004;
    +  light.shadow.mapSize.width = 2048;
    +  light.shadow.mapSize.height = 2048;

      scene.add(light);
      scene.add(light.target);
    +  const cam = light.shadow.camera;
    +  cam.near = 1;
    +  cam.far = 2000;
    +  cam.left = -1500;
    +  cam.right = 1500;
    +  cam.top = 1500;
    +  cam.bottom = -1500;
    ...

et j'ai défini la couleur de fond en bleu clair.

    const scene = new THREE.Scene();
    -scene.background = new THREE.Color('black');
    +scene.background = new THREE.Color('#DEFEFF');

Et ... les ombres

cliquez ici pour ouvrir dans une fenêtre séparée

J'espère que parcourir ce projet a été utile et a montré de bons exemples de résolution de certains des problèmes liés au chargement d'un fichier avec un graphe de scène.

Une chose intéressante est qu'en comparant le fichier .blend au fichier .gltf, le fichier .blend a plusieurs lumières mais elles ne sont pas des lumières après avoir été chargées dans la scène. Un fichier .GLTF est juste un fichier JSON, vous pouvez donc facilement regarder à l'intérieur. Il se compose de plusieurs tableaux de choses et chaque élément dans un tableau est référencé par index ailleurs. Bien qu'il y ait des extensions en cours de développement, elles soulignent un problème commun à presque tous les formats 3D. Ils ne peuvent jamais couvrir tous les cas.

Il y a toujours un besoin de plus de données. Par exemple, nous avons exporté manuellement un chemin pour les voitures à suivre. Idéalement, cette information aurait pu être dans le fichier .GLTF, mais pour ce faire, nous aurions besoin d'écrire notre propre exportateur et de marquer d'une manière ou d'une autre les nœuds pour la façon dont nous voulons qu'ils soient exportés, ou utiliser un schéma de nommage ou quelque chose de similaire pour obtenir les données de l'outil que nous utilisons pour créer les données dans notre application.

Tout cela est laissé comme un exercice pour le lecteur.