# Graphe de scène

Cet article fait partie d'une série d'articles sur three.js. Le premier article est les bases de three.js. Si vous ne l'avez pas encore lu, vous pourriez envisager de commencer par là.

Le cœur de Three.js est sans doute son graphe de scène. Un graphe de scène dans un moteur 3D est une hiérarchie de nœuds dans un graphe où chaque nœud représente un espace local.

C'est un peu abstrait, alors essayons de donner quelques exemples.

Un exemple pourrait être le système solaire : soleil, terre, lune.

La Terre tourne autour du Soleil. La Lune tourne autour de la Terre. La Lune se déplace en cercle autour de la Terre. Du point de vue de la Lune, elle tourne dans l'« espace local » de la Terre. Même si son mouvement par rapport au Soleil est une courbe folle ressemblant à un spirographe, du point de vue de la Lune, elle n'a qu'à se soucier de tourner autour de l'espace local de la Terre.

Pour le voir autrement, vous qui vivez sur Terre n'avez pas à penser à la rotation de la Terre sur son axe ni à sa rotation autour du Soleil. Vous marchez, conduisez, nagez ou courez comme si la Terre ne bougeait ni ne tournait pas du tout. Vous marchez, conduisez, nagez, courez et vivez dans l'« espace local » de la Terre, même si, par rapport au Soleil, vous tournez autour de la Terre à environ 1600 kilomètres par heure et autour du Soleil à environ 108 000 kilomètres par heure. Votre position dans le système solaire est similaire à celle de la Lune ci-dessus, mais vous n'avez pas à vous en soucier. Vous vous préoccupez simplement de votre position par rapport à la Terre dans son « espace local ».

Prenons les choses étape par étape. Imaginez que nous voulions faire un diagramme du soleil, de la terre et de la lune. Nous commencerons par le soleil en créant simplement une sphère et en la plaçant à l'origine. Remarque : Nous utilisons le soleil, la terre, la lune pour illustrer comment utiliser un graphe de scène. Bien sûr, le vrai soleil, la terre et la lune utilisent la physique, mais pour nos besoins, nous allons simuler cela avec un graphe de scène.

    // un tableau d'objets dont la rotation doit être mise à jour
    const objects = [];

    // utiliser une seule sphère pour tout
    const radius = 1;
    const widthSegments = 6;
    const heightSegments = 6;
    const sphereGeometry = new THREE.SphereGeometry(
        radius, widthSegments, heightSegments);

    const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
    const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
    sunMesh.scale.set(5, 5, 5);  // agrandir le soleil
    scene.add(sunMesh);
    objects.push(sunMesh);

Nous utilisons une sphère avec très peu de polygones. Seulement 6 subdivisions autour de son équateur. C'est pour qu'il soit facile de voir la rotation.

Nous allons réutiliser la même sphère pour tout, nous allons donc définir une échelle de 5x pour le maillage du soleil.

Nous définissons également la propriété `emissive` du matériau phong en jaune. La propriété emissive d'un matériau phong est essentiellement la couleur qui sera dessinée sans lumière frappant la surface. La lumière est ajoutée à cette couleur.

Plaçons également une seule lumière ponctuelle au centre de la scène. Nous reviendrons plus tard sur les détails des lumières ponctuelles, mais pour l'instant, la version simple est qu'une lumière ponctuelle représente la lumière qui émane d'un point unique.

    {
      const color = 0xFFFFFF;
      const intensity = 500;
      const light = new THREE.PointLight(color, intensity);
      scene.add(light);
    }

Pour faciliter la visualisation, nous allons placer la caméra juste au-dessus de l'origine, regardant vers le bas. La façon la plus simple de faire cela est d'utiliser la fonction `lookAt`. La fonction `lookAt` orientera la caméra depuis sa position pour « regarder » la position que nous lui passons. Cependant, avant de faire cela, nous devons indiquer à la caméra dans quelle direction se trouve le haut de la caméra, ou plutôt quelle direction est le « haut » pour la caméra. Dans la plupart des situations, Y positif vers le haut est suffisant, mais puisque nous regardons directement vers le bas, nous devons indiquer à la caméra que Z positif est vers le haut.

    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
    camera.position.set(0, 50, 0);
    camera.up.set(0, 0, 1);
    camera.lookAt(0, 0, 0);

Dans la boucle de rendu, adaptée des exemples précédents, nous faisons pivoter tous les objets de notre tableau `objects` avec ce code.

    objects.forEach((obj) => {
      obj.rotation.y = time;
    });

Comme nous avons ajouté le `sunMesh` au tableau `objects`, il va tourner.

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

Maintenant, ajoutons la Terre.

    const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
    const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
    earthMesh.position.x = 10;
    scene.add(earthMesh);
    objects.push(earthMesh);

Nous créons un matériau bleu, mais nous lui donnons une petite quantité de bleu émissif afin qu'il ressorte sur notre fond noir.

Nous utilisons la même `sphereGeometry` avec notre nouveau `earthMaterial` bleu pour créer un `earthMesh`. Nous le positionnons à 10 unités à gauche du soleil et l'ajoutons à la scène. Comme nous l'avons ajouté à notre tableau `objects`, il tournera aussi.

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

Vous pouvez voir que le soleil et la terre tournent, mais la terre ne tourne pas autour du soleil. Faisons de la terre un enfant du soleil

    -scene.add(earthMesh);
    +sunMesh.add(earthMesh);

et...

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

Que s'est-il passé ? Pourquoi la Terre a-t-elle la même taille que le Soleil et pourquoi est-elle si loin ? J'ai en fait dû déplacer la caméra de 50 unités au-dessus à 150 unités au-dessus pour voir la Terre.

Nous avons fait du `earthMesh` un enfant du `sunMesh`. L'échelle du `sunMesh` est réglée à 5x avec `sunMesh.scale.set(5, 5, 5)`. Cela signifie que l'espace local du `sunMesh` est 5 fois plus grand. Tout ce qui est placé dans cet espace sera multiplié par 5. Cela signifie que la terre est maintenant 5 fois plus grande et que sa distance par rapport au soleil (`earthMesh.position.x = 10`) est également multipliée par 5.

Notre graphe de scène ressemble actuellement à ceci

Pour corriger cela, ajoutons un nœud de graphe de scène vide. Nous allons rendre le soleil et la terre enfants de ce nœud.

    +const solarSystem = new THREE.Object3D();
    +scene.add(solarSystem);
    +objects.push(solarSystem);

    const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
    const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
    sunMesh.scale.set(5, 5, 5);
    -scene.add(sunMesh);
    +solarSystem.add(sunMesh);
    objects.push(sunMesh);

    const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
    const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
    earthMesh.position.x = 10;
    -sunMesh.add(earthMesh);
    +solarSystem.add(earthMesh);
    objects.push(earthMesh);

Ici, nous avons créé un `Object3D`. Comme un `Mesh`, c'est aussi un nœud dans le graphe de scène, mais contrairement à un `Mesh`, il n'a ni matériau ni géométrie. Il représente simplement un espace local.

Notre nouveau graphe de scène ressemble à ceci

Le `sunMesh` et le `earthMesh` sont tous deux enfants du `solarSystem`. Les 3 tournent et maintenant, parce que le `earthMesh` n'est pas un enfant du `sunMesh`, il n'est plus mis à l'échelle par 5x.

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

Bien mieux. La Terre est plus petite que le soleil et elle tourne autour du soleil tout en tournant sur elle-même.

En continuant sur ce même schéma, ajoutons une lune.

    +const earthOrbit = new THREE.Object3D();
    +earthOrbit.position.x = 10;
    +solarSystem.add(earthOrbit);
    +objects.push(earthOrbit);

    const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
    const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
    -earthMesh.position.x = 10; // notez que ce décalage est déjà défini dans l'objet THREE.Object3D parent "earthOrbit"
    -solarSystem.add(earthMesh);
    +earthOrbit.add(earthMesh);
    objects.push(earthMesh);

    +const moonOrbit = new THREE.Object3D();
    +moonOrbit.position.x = 2;
    +earthOrbit.add(moonOrbit);

    +const moonMaterial = new THREE.MeshPhongMaterial({color: 0x888888, emissive: 0x222222});
    +const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial);
    +moonMesh.scale.set(.5, .5, .5);
    +moonOrbit.add(moonMesh);
    +objects.push(moonMesh);

Encore une fois, nous avons ajouté d'autres nœuds de graphe de scène invisibles. Le premier, un `Object3D` appelé `earthOrbit` et auquel nous avons ajouté le `earthMesh` et le `moonOrbit`, également nouveau. Nous avons ensuite ajouté le `moonMesh` au `moonOrbit`. Le nouveau graphe de scène ressemble à ceci.

et voici le résultat

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

Vous pouvez voir que la lune suit le motif spirographe montré en haut de cet article, mais nous n'avons pas eu à le calculer manuellement. Nous avons simplement configuré notre graphe de scène pour qu'il le fasse pour nous.

Il est souvent utile de dessiner quelque chose pour visualiser les nœuds dans le graphe de scène. Three.js dispose de quelques ummmm, helpers utiles pour ummm, ... aider à cela.

L'un s'appelle `AxesHelper`. Il dessine 3 lignes représentant les axes locaux X, Y et Z. Ajoutons-en un à chaque nœud que nous avons créé.

    // ajouter un AxesHelper à chaque nœud
    objects.forEach((node) => {
      const axes = new THREE.AxesHelper();
      axes.material.depthTest = false;
      axes.renderOrder = 1;
      node.add(axes);
    });

Dans notre cas, nous voulons que les axes apparaissent même s'ils sont à l'intérieur des sphères. Pour ce faire, nous réglons la propriété `depthTest` de leur matériau sur false, ce qui signifie qu'ils ne vérifieront pas s'ils dessinent derrière quelque chose d'autre. Nous réglons également leur `renderOrder` à 1 (la valeur par défaut est 0) afin qu'ils soient dessinés après toutes les sphères. Sinon, une sphère pourrait dessiner par-dessus et les masquer.

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

Nous pouvons voir les axes x (rouge) et z (bleu). Comme nous regardons directement vers le bas et que chacun de nos objets ne tourne que autour de son axe y, nous ne voyons pas beaucoup les axes y (vert).

Il peut être difficile d'en voir certains car il y a 2 paires d'axes qui se chevauchent. Le `sunMesh` et le `solarSystem` sont tous deux à la même position. De même, le `earthMesh` et le `earthOrbit` sont à la même position. Ajoutons quelques contrôles simples pour nous permettre de les activer/désactiver pour chaque nœud. Tant qu'on y est, ajoutons également un autre helper appelé `GridHelper`. Il crée une grille 2D sur le plan XZ. Par défaut, la grille est de 10x10 unités.

Nous allons également utiliser lil-gui, une bibliothèque d'interface utilisateur très populaire dans les projets three.js. lil-gui prend un objet et le nom d'une propriété sur cet objet et, en fonction du type de la propriété, crée automatiquement une interface utilisateur pour manipuler cette propriété.

Nous voulons créer à la fois un `GridHelper` et un `AxesHelper` pour chaque nœud. Nous avons besoin d'une étiquette pour chaque nœud, nous allons donc nous débarrasser de l'ancienne boucle et passer à l'appel d'une fonction pour ajouter les helpers pour chaque nœud

    -// ajouter un AxesHelper à chaque nœud
    -objects.forEach((node) => {
    -  const axes = new THREE.AxesHelper();
    -  axes.material.depthTest = false;
    -  axes.renderOrder = 1;
    -  node.add(axes);
    -});

    +function makeAxisGrid(node, label, units) {
    +  const helper = new AxisGridHelper(node, units);
    +  gui.add(helper, 'visible').name(label);
    +}
    +
    +makeAxisGrid(solarSystem, 'solarSystem', 25);
    +makeAxisGrid(sunMesh, 'sunMesh');
    +makeAxisGrid(earthOrbit, 'earthOrbit');
    +makeAxisGrid(earthMesh, 'earthMesh');
    +makeAxisGrid(moonOrbit, 'moonOrbit');
    +makeAxisGrid(moonMesh, 'moonMesh');

`makeAxisGrid` crée un `AxisGridHelper`, une classe que nous allons créer pour rendre lil-gui heureux. Comme indiqué ci-dessus, lil-gui créera automatiquement une interface utilisateur qui manipule la propriété nommée de certains objets. Il créera une interface utilisateur différente en fonction du type de propriété. Nous voulons qu'il crée une case à cocher, nous devons donc spécifier une propriété `bool`. Mais, nous voulons que les axes et la grille apparaissent/disparaissent en fonction d'une seule propriété, nous allons donc créer une classe qui a un getter et un setter pour une propriété. De cette façon, nous pouvons laisser lil-gui penser qu'il manipule une seule propriété, mais en interne, nous pouvons définir la propriété visible à la fois de l'`AxesHelper` et du `GridHelper` pour un nœud.

    // Active/désactive la visibilité des axes et de la grille
    // lil-gui nécessite une propriété qui renvoie un booléen
    // pour décider de créer une case à cocher, nous créons donc un setter
    // et un getter pour `visible` que nous pouvons dire à lil-gui
    // de regarder.
    class AxisGridHelper {
      constructor(node, units = 10) {
        const axes = new THREE.AxesHelper();
        axes.material.depthTest = false;
        axes.renderOrder = 2;  // après la grille
        node.add(axes);

        const grid = new THREE.GridHelper(units, units);
        grid.material.depthTest = false;
        grid.renderOrder = 1;
        node.add(grid);

        this.grid = grid;
        this.axes = axes;
        this.visible = false;
      }
      get visible() {
        return this._visible;
      }
      set visible(v) {
        this._visible = v;
        this.grid.visible = v;
        this.axes.visible = v;
      }
    }

Une chose à noter est que nous avons défini le `renderOrder` de l'`AxesHelper` à 2 et celui du `GridHelper` à 1 afin que les axes soient dessinés après la grille. Sinon, la grille pourrait écraser les axes.

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

Activez le `solarSystem` et vous verrez que la terre se trouve exactement à 10 unités du centre, comme nous l'avons réglé ci-dessus. Vous pouvez voir comment la terre se trouve dans l'espace local du `solarSystem`. De même, si vous activez l'`earthOrbit`, vous verrez que la lune se trouve exactement à 2 unités du centre de l'espace local de l'`earthOrbit`.

Quelques autres exemples de graphes de scène. Une automobile dans un monde de jeu simple pourrait avoir un graphe de scène comme celui-ci

Si vous déplacez la carrosserie de la voiture, toutes les roues bougeront avec elle. Si vous vouliez que la carrosserie rebondisse séparément des roues, vous pourriez rendre la carrosserie et les roues enfants d'un nœud « châssis » qui représente le châssis de la voiture.

Un autre exemple est un humain dans un monde de jeu.

Vous pouvez voir que le graphe de scène devient assez complexe pour un humain. En fait, le graphe de scène ci-dessus est simplifié. Par exemple, vous pourriez l'étendre pour couvrir chaque doigt (au moins 28 nœuds supplémentaires) et chaque orteil (encore 28 nœuds) plus ceux pour le visage et la mâchoire, les yeux et peut-être plus.

Créons un graphe de scène semi-complexe. Nous allons faire un char. Le char aura 6 roues et une tourelle. Le char suivra un chemin. Il y aura une sphère qui se déplacera et le char ciblera la sphère.

Voici le graphe de scène. Les maillages sont colorés en vert, les `Object3D` en bleu, les lumières en or et les caméras en violet. Une caméra n'a pas été ajoutée au graphe de scène.

Regardez le code pour voir la configuration de tous ces nœuds.

Pour la cible, la chose que le char vise, il y a un `targetOrbit` (`Object3D`) qui tourne simplement comme l'`earthOrbit` ci-dessus. Un `targetElevation` (`Object3D`), qui est un enfant du `targetOrbit`, fournit un décalage par rapport au `targetOrbit` et une élévation de base. Un autre `Object3D` appelé `targetBob` est enfant de celui-ci et il monte et descend simplement par rapport au `targetElevation`. Enfin, il y a le `targetMesh` qui est juste un cube que nous faisons pivoter et dont nous changeons les couleurs

    // déplacer la cible
    targetOrbit.rotation.y = time * .27;
    targetBob.position.y = Math.sin(time * 2) * 4;
    targetMesh.rotation.x = time * 7;
    targetMesh.rotation.y = time * 13;
    targetMaterial.emissive.setHSL(time * 10 % 1, 1, .25);
    targetMaterial.color.setHSL(time * 10 % 1, 1, .25);

Pour le char, il y a un `Object3D` appelé `tank` qui est utilisé pour déplacer tout ce qui se trouve en dessous. Le code utilise une `SplineCurve` à laquelle il peut demander les positions le long de cette courbe. 0.0 est le début de la courbe. 1.0 est la fin de la courbe. Il demande la position actuelle où il place le char. Il demande ensuite une position légèrement plus loin le long de la courbe et l'utilise pour orienter le char dans cette direction en utilisant `Object3D.lookAt`.

    const tankPosition = new THREE.Vector2();
    const tankTarget = new THREE.Vector2();

    ...

    // déplacer le char
    const tankTime = time * .05;
    curve.getPointAt(tankTime % 1, tankPosition);
    curve.getPointAt((tankTime + 0.01) % 1, tankTarget);
    tank.position.set(tankPosition.x, 0, tankPosition.y);
    tank.lookAt(tankTarget.x, 0, tankTarget.y);

La tourelle sur le dessus du char est déplacée automatiquement en étant un enfant du char. Pour la pointer vers la cible, nous demandons simplement la position mondiale de la cible et ensuite utilisons à nouveau `Object3D.lookAt`.

    const targetPosition = new THREE.Vector3();

    ...

    // orienter la tourelle vers la cible
    targetMesh.getWorldPosition(targetPosition);
    turretPivot.lookAt(targetPosition);

Il y a une `turretCamera` qui est un enfant du `turretMesh`, donc elle montera et descendra et tournera avec la tourelle. Nous la faisons viser la cible.

    // faire pointer la turretCamera vers la cible
    turretCamera.lookAt(targetPosition);

Il y a aussi un `targetCameraPivot` qui est un enfant du `targetBob`, donc il flotte autour de la cible. Nous le faisons viser le char. Son but est de permettre à la `targetCamera` d'être décalée par rapport à la cible elle-même. Si nous avions fait de la caméra un enfant du `targetBob` et l'avions simplement fait pointer, elle serait à l'intérieur de la cible.

    // faire pointer le targetCameraPivot vers le char
    tank.getWorldPosition(targetPosition);
    targetCameraPivot.lookAt(targetPosition);

Enfin, nous faisons tourner toutes les roues

    wheelMeshes.forEach((obj) => {
      obj.rotation.x = time * 3;
    });

Pour les caméras, nous avons configuré un tableau de toutes les 4 caméras au moment de l'initialisation avec des descriptions.

    const cameras = [
      { cam: camera, desc: 'caméra détachée', },
      { cam: turretCamera, desc: 'sur tourelle regardant la cible', },
      { cam: targetCamera, desc: 'près de la cible regardant le char', },
      { cam: tankCamera, desc: 'au-dessus de l'arrière du char', },
    ];

    const infoElem = document.querySelector('#info');

et passons en revue nos caméras au moment du rendu.

    const camera = cameras[time * .25 % cameras.length | 0];
    infoElem.textContent = camera.desc;

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

J'espère que cela vous donne une idée du fonctionnement des graphes de scène et de la manière dont vous pourriez les utiliser. Créer des nœuds `Object3D` et les rendre parents d'autres objets est une étape importante pour bien utiliser un moteur 3D comme three.js. Souvent, il peut sembler qu'il soit nécessaire de faire des calculs mathématiques complexes pour faire bouger et pivoter quelque chose comme vous le souhaitez. Par exemple, sans graphe de scène, calculer le mouvement de la lune ou où placer les roues de la voiture par rapport à sa carrosserie serait très compliqué, mais en utilisant un graphe de scène, cela devient beaucoup plus facile.

Ensuite, nous aborderons les matériaux.