Primitives

Cet article fait partie d'une série d'articles sur three.js. Le premier article traitait des notions fondamentales. Si vous ne l'avez pas encore lu, vous pourriez vouloir commencer par là.

Three.js dispose d'un grand nombre de primitives. Les primitives sont généralement des formes 3D qui sont générées au moment de l'exécution avec un ensemble de paramètres.

Il est courant d'utiliser des primitives pour des choses comme une sphère pour un globe ou un ensemble de boîtes pour dessiner un graphique 3D. Il est particulièrement courant d'utiliser des primitives pour expérimenter et commencer avec la 3D. Pour la majorité des applications 3D, il est plus courant qu'un artiste crée des modèles 3D dans un programme de modélisation 3D comme Blender ou Maya ou Cinema 4D. Plus tard dans cette série, nous aborderons la création et le chargement de données à partir de plusieurs programmes de modélisation 3D. Pour l'instant, passons en revue quelques-unes des primitives disponibles.

Beaucoup des primitives ci-dessous ont des valeurs par défaut pour tout ou partie de leurs paramètres, de sorte que vous pouvez les utiliser plus ou moins selon vos besoins.

Une Boîte
Un cercle plat
Un Cône
Un Cylindre
Un dodécaèdre (12 faces)
Une forme 2D extrudée avec biseautage optionnel. Ici, nous extrudons une forme de cœur. Notez que c'est la base de TextGeometry.
Un icosaèdre (20 faces)
Une forme générée en faisant tourner une ligne. Exemples : lampes, quilles de bowling, bougies, chandeliers, verres à vin, verres à boire, etc... Vous fournissez la silhouette 2D comme une série de points, puis vous indiquez à three.js combien de subdivisions créer en faisant tourner la silhouette autour d'un axe.
Un Octaèdre (8 faces)
Une surface générée en fournissant une fonction qui prend un point 2D d'une grille et renvoie le point 3D correspondant.
Un plan 2D
Prend un ensemble de triangles centrés autour d'un point et les projette sur une sphère
Un disque 2D avec un trou au centre
Un contour 2D qui est triangulé
Une sphère
Un tétraèdre (4 faces)
Texte 3D généré à partir d'une police 3D et d'une chaîne de caractères
Un tore (beignet)
Un nœud torique
Un cercle tracé le long d'un chemin
Un objet d'aide qui prend une autre géométrie en entrée et génère des arêtes seulement si l'angle entre les faces est supérieur à un certain seuil. Par exemple, si vous regardez la boîte en haut, elle montre une ligne traversant chaque face, montrant chaque triangle qui compose la boîte. En utilisant un EdgesGeometry à la place, les lignes du milieu sont supprimées. Ajustez le seuil `thresholdAngle` ci-dessous et vous verrez les arêtes en dessous de ce seuil disparaître.
Génère une géométrie qui contient un segment de ligne (2 points) par arête dans la géométrie donnée. Sans cela, il vous manquerait souvent des arêtes ou vous obtiendriez des arêtes supplémentaires car WebGL nécessite généralement 2 points par segment de ligne. Par exemple, si vous n'aviez qu'un seul triangle, il n'y aurait que 3 points. Si vous essayiez de le dessiner en utilisant un matériau avec wireframe: true, vous n'obtiendriez qu'une seule ligne. Passer cette géométrie de triangle à un WireframeGeometry générera une nouvelle géométrie qui a 3 segments de ligne utilisant 6 points.

Nous aborderons la création de géométries personnalisées dans un autre article. Pour l'instant, faisons un exemple créant chaque type de primitive. Nous commencerons avec les exemples de l'article précédent.

Près du haut, définissons une couleur de fond

const scene = new THREE.Scene();
+scene.background = new THREE.Color(0xAAAAAA);

Cela indique à three.js d'effacer avec un gris clair.

La caméra doit changer de position afin que nous puissions voir tous les objets.

-const fov = 75;
+const fov = 40;
const aspect = 2;  // the canvas default
const near = 0.1;
-const far = 5;
+const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-camera.position.z = 2;
+camera.position.z = 120;

Ajoutons une fonction, addObject, qui prend une position x, y et un Object3D et ajoute l'objet à la scène.

const objects = [];
const spread = 15;

function addObject(x, y, obj) {
  obj.position.x = x * spread;
  obj.position.y = y * spread;

  scene.add(obj);
  objects.push(obj);
}

Créons également une fonction pour créer un matériau de couleur aléatoire. Nous utiliserons une fonctionnalité de Color qui vous permet de définir une couleur basée sur la teinte, la saturation et la luminance.

hue (teinte) va de 0 à 1 autour de la roue chromatique avec le rouge à 0, le vert à 0.33 et le bleu à 0.66. saturation va de 0 à 1, 0 n'ayant pas de couleur et 1 étant la plus saturée. luminance va de 0 à 1 avec 0 étant le noir, 1 étant le blanc et 0.5 étant la quantité maximale de couleur. En d'autres termes, lorsque la luminance passe de 0.0 à 0.5, la couleur passe du noir à la hue (teinte). De 0.5 à 1.0, la couleur passe de la hue (teinte) au blanc.

function createMaterial() {
  const material = new THREE.MeshPhongMaterial({
    side: THREE.DoubleSide,
  });

  const hue = Math.random();
  const saturation = 1;
  const luminance = .5;
  material.color.setHSL(hue, saturation, luminance);

  return material;
}

Nous avons également passé side: THREE.DoubleSide au matériau. Cela indique à three de dessiner les deux côtés des triangles qui composent une forme. Pour une forme solide comme une sphère ou un cube, il n'y a généralement aucune raison de dessiner les côtés arrière des triangles car ils font tous face à l'intérieur de la forme. Dans notre cas cependant, nous dessinons quelques éléments comme le PlaneGeometry et le ShapeGeometry qui sont bidimensionnels et n'ont donc pas d'intérieur. Sans définir side: THREE.DoubleSide, ils disparaîtraient en regardant leurs côtés arrière.

Je dois noter qu'il est plus rapide de dessiner lorsque l'on ne définit pas side: THREE.DoubleSide, donc idéalement nous ne le définirions que sur les matériaux qui en ont vraiment besoin, mais dans ce cas, nous ne dessinons pas trop, donc il n'y a pas beaucoup de raison de s'en soucier.

Créons une fonction, addSolidGeometry, à laquelle nous passons une géométrie, et elle crée un matériau de couleur aléatoire via createMaterial et l'ajoute à la scène via addObject.

function addSolidGeometry(x, y, geometry) {
  const mesh = new THREE.Mesh(geometry, createMaterial());
  addObject(x, y, mesh);
}

Maintenant, nous pouvons l'utiliser pour la majorité des primitives que nous créons. Par exemple, pour créer une boîte

{
  const width = 8;
  const height = 8;
  const depth = 8;
  addSolidGeometry(-2, -2, new THREE.BoxGeometry(width, height, depth));
}

Si vous regardez le code ci-dessous, vous verrez une section similaire pour chaque type de géométrie.

Voici le résultat :

Il y a quelques exceptions notables au modèle ci-dessus. La plus importante est probablement la TextGeometry. Elle nécessite de charger les données de police 3D avant de pouvoir générer un maillage pour le texte. Ces données se chargent de manière asynchrone, nous devons donc attendre qu'elles soient chargées avant d'essayer de créer la géométrie. En "promisifiant" le chargement de la police, nous pouvons rendre les choses beaucoup plus faciles. Nous créons un FontLoader, puis une fonction loadFont qui renvoie une promesse qui, une fois résolue, nous donnera la police. Nous créons ensuite une fonction async appelée doit et chargeons la police en utilisant await. Et enfin, nous créons la géométrie et appelons addObject pour l'ajouter à la scène.

{
  const loader = new FontLoader();
  // promisify font loading
  function loadFont(url) {
    return new Promise((resolve, reject) => {
      loader.load(url, resolve, undefined, reject);
    });
  }

  async function doit() {
    const font = await loadFont('resources/threejs/fonts/helvetiker_regular.typeface.json');  /* threejs.org : URL */
    const geometry = new TextGeometry('three.js', {
      font: font,
      size: 3.0,
      depth: .2,
      curveSegments: 12,
      bevelEnabled: true,
      bevelThickness: 0.15,
      bevelSize: .3,
      bevelSegments: 5,
    });
    const mesh = new THREE.Mesh(geometry, createMaterial());
    geometry.computeBoundingBox();
    geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);

    const parent = new THREE.Object3D();
    parent.add(mesh);

    addObject(-1, -1, parent);
  }
  doit();
}

Il y a une autre différence. Nous voulons faire tourner le texte autour de son centre, mais par défaut, three.js crée le texte de manière à ce que son centre de rotation soit sur le bord gauche. Pour contourner ce problème, nous pouvons demander à three.js de calculer la boîte englobante (bounding box) de la géométrie. Nous pouvons ensuite appeler la méthode getCenter de la boîte englobante et lui passer l'objet position de notre maillage. getCenter copie le centre de la boîte dans la position. Elle renvoie également l'objet position afin que nous puissions appeler multiplyScalar(-1) pour positionner l'objet entier de sorte que son centre de rotation soit au centre de l'objet.

Si nous appelions simplement addSolidGeometry comme avec les exemples précédents, cela redéfinirait la position, ce qui n'est pas bon. Donc, dans ce cas, nous créons un Object3D qui est le nœud standard pour le graphe de scène de three.js. Mesh est également hérité de Object3D. Nous aborderons le fonctionnement du graphe de scène dans un autre article. Pour l'instant, il suffit de savoir que, comme les nœuds DOM, les enfants sont dessinés par rapport à leur parent. En créant un Object3D et en faisant de notre maillage un enfant de celui-ci, nous pouvons positionner l'Object3D où nous voulons tout en conservant le décalage central que nous avons défini précédemment.

Si nous ne faisions pas cela, le texte tournerait de manière décentrée.

Notez que celui de gauche ne tourne pas autour de son centre tandis que celui de droite le fait.

Les autres exceptions sont les 2 exemples basés sur des lignes pour EdgesGeometry et WireframeGeometry. Au lieu d'appeler addSolidGeometry, elles appellent addLineGeometry qui ressemble à ceci

function addLineGeometry(x, y, geometry) {
  const material = new THREE.LineBasicMaterial({color: 0x000000});
  const mesh = new THREE.LineSegments(geometry, material);
  addObject(x, y, mesh);
}

Elle crée un LineBasicMaterial noir et crée ensuite un objet LineSegments qui est un wrapper pour Mesh et aide three à savoir que vous rendez des segments de ligne (2 points par segment).

Chacune des primitives possède plusieurs paramètres que vous pouvez passer lors de sa création et il est préférable de consulter la documentation pour les voir tous plutôt que de les répéter ici. Vous pouvez également cliquer sur les liens ci-dessus à côté de chaque forme pour accéder directement à la documentation de cette forme.

Il existe une autre paire de classes qui ne correspondent pas vraiment aux modèles ci-dessus. Ce sont les classes PointsMaterial et Points. Points est similaire à LineSegments ci-dessus en ce sens qu'elle prend une BufferGeometry mais dessine des points à chaque sommet au lieu de lignes. Pour l'utiliser, vous devez également lui passer un PointsMaterial qui prend un paramètre size pour définir la taille des points.

const radius = 7;
const widthSegments = 12;
const heightSegments = 8;
const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
const material = new THREE.PointsMaterial({
    color: 'red',
    size: 0.2,     // in world units
});
const points = new THREE.Points(geometry, material);
scene.add(points);

Vous pouvez désactiver sizeAttenuation en le définissant à false si vous souhaitez que les points aient la même taille quelle que soit leur distance par rapport à la caméra.

const material = new THREE.PointsMaterial({
    color: 'red',
+    sizeAttenuation: false,
+    size: 3,       // in pixels
-    size: 0.2,     // in world units
});
...

Une autre chose importante à aborder est que presque toutes les formes ont divers paramètres pour déterminer combien les subdiviser. Un bon exemple pourrait être les géométries de sphères. Les sphères prennent des paramètres pour le nombre de divisions à faire autour et de haut en bas. Par exemple

La première sphère a 5 segments autour et 3 en hauteur, soit 15 segments ou 30 triangles. La deuxième sphère a 24 segments sur 10, soit 240 segments ou 480 triangles. La dernière a 50 sur 50, soit 2500 segments ou 5000 triangles.

C'est à vous de décider du nombre de subdivisions dont vous avez besoin. Il peut sembler que vous ayez besoin d'un grand nombre de segments, mais supprimez les lignes et l'ombrage plat, et nous obtenons ceci

Il n'est maintenant plus si clair que celle de droite avec 5000 triangles soit entièrement meilleure que celle du milieu avec seulement 480.

Si vous ne dessinez que quelques sphères, comme par exemple un seul globe pour une carte de la terre, alors une seule sphère de 10000 triangles n'est pas un mauvais choix. Si par contre vous essayez de dessiner 1000 sphères, alors 1000 sphères multipliées par 10000 triangles chacune donnent 10 millions de triangles. Pour animer fluidement, vous avez besoin que le navigateur dessine à 60 images par seconde, donc vous demanderiez au navigateur de dessiner 600 millions de triangles par seconde. C'est beaucoup de calcul.

Parfois, il est facile de choisir. Par exemple, vous pouvez également choisir de subdiviser un plan.

Le plan de gauche est composé de 2 triangles. Le plan de droite est composé de 200 triangles. Contrairement à la sphère, il n'y a vraiment aucun compromis sur la qualité pour la plupart des cas d'utilisation d'un plan. Vous ne subdiviseriez très probablement un plan que si vous vous attendiez à vouloir le modifier ou le déformer d'une manière ou d'une autre. Une boîte est similaire.

Alors, choisissez ce qui convient le mieux à votre situation. Moins vous choisissez de subdivisions, plus il est probable que les choses fonctionneront fluidement et moins elles consommeront de mémoire. Vous devrez décider vous-même quel est le bon compromis pour votre situation particulière.

Si aucune des formes ci-dessus ne correspond à votre cas d'utilisation, vous pouvez charger une géométrie, par exemple à partir d'un fichier .obj ou d'un fichier .gltf. Vous pouvez également créer votre propre BufferGeometry personnalisée.

Ensuite, passons en revue le fonctionnement du graphe de scène de three et comment l'utiliser.