# Transparence

La transparence dans three.js est à la fois facile et difficile.

Nous allons d'abord aborder la partie facile. Créons une scène avec 8 cubes placés sur une grille 2x2x2.

Nous allons commencer par l'exemple de l'article sur le rendu à la demande qui contenait 3 cubes et le modifier pour en avoir 8. Modifions d'abord notre fonction `makeInstance` pour qu'elle prenne x, y et z

    -function makeInstance(geometry, color) {
    +function makeInstance(geometry, color, x, y, z) {
      const material = new THREE.MeshPhongMaterial({color});

      const cube = new THREE.Mesh(geometry, material);
      scene.add(cube);

    -  cube.position.x = x;
    +  cube.position.set(x, y, z);

      return cube;
    }

Ensuite, nous pouvons créer 8 cubes.

    +function hsl(h, s, l) {
    +  return (new THREE.Color()).setHSL(h, s, l);
    +}

    -makeInstance(geometry, 0x44aa88,  0);
    -makeInstance(geometry, 0x8844aa, -2);
    -makeInstance(geometry, 0xaa8844,  2);

    +{
    +  const d = 0.8;
    +  makeInstance(geometry, hsl(0 / 8, 1, .5), -d, -d, -d);
    +  makeInstance(geometry, hsl(1 / 8, 1, .5),  d, -d, -d);
    +  makeInstance(geometry, hsl(2 / 8, 1, .5), -d,  d, -d);
    +  makeInstance(geometry, hsl(3 / 8, 1, .5),  d,  d, -d);
    +  makeInstance(geometry, hsl(4 / 8, 1, .5), -d, -d,  d);
    +  makeInstance(geometry, hsl(5 / 8, 1, .5),  d, -d,  d);
    +  makeInstance(geometry, hsl(6 / 8, 1, .5), -d,  d,  d);
    +  makeInstance(geometry, hsl(7 / 8, 1, .5),  d,  d,  d);
    +}

J'ai aussi ajusté la caméra.

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

Définissez le fond en blanc.

    const scene = new THREE.Scene();
    +scene.background = new THREE.Color('white');

Et ajouté une deuxième lumière pour que toutes les faces des cubes reçoivent de l'éclairage.

    -{
    +function addLight(...pos) {
      const color = 0xFFFFFF;
      const intensity = 1;
      const light = new THREE.DirectionalLight(color, intensity);
    -  light.position.set(-1, 2, 4);
    +  light.position.set(...pos);
      scene.add(light);
    }
    +addLight(-1, 2, 4);
    +addLight( 1, -1, -2);

Pour rendre les cubes transparents, il suffit de définir le drapeau `transparent` et de définir un niveau d'`opacity`, 1 étant complètement opaque et 0 étant complètement transparent.

    function makeInstance(geometry, color, x, y, z) {
    -  const material = new THREE.MeshPhongMaterial({color});
    +  const material = new THREE.MeshPhongMaterial({
    +    color,
    +    opacity: 0.5,
    +    transparent: true,
    +  });

      const cube = new THREE.Mesh(geometry, material);
      scene.add(cube);

      cube.position.set(x, y, z);

      return cube;
    }

et avec cela, nous obtenons 8 cubes transparents.

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

Faites glisser sur l'exemple pour faire pivoter la vue.

Cela semble donc facile mais... regardez de plus près. Les cubes n'ont pas de faces arrière.

pas de faces arrière

Nous avons découvert la propriété de matériau `side` dans l'article sur les matériaux. Définissons-la donc sur `THREE.DoubleSide` pour que les deux faces de chaque cube soient dessinées.

    const material = new THREE.MeshPhongMaterial({
      color,
      map: loader.load(url),
      opacity: 0.5,
      transparent: true,
    +  side: THREE.DoubleSide,
    });

Et nous obtenons

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

Faites-le tourner. Cela semble fonctionner car nous pouvons voir les faces arrière, sauf qu'en y regardant de plus près, parfois ce n'est pas le cas.

la face arrière gauche de chaque cube est manquante

Cela se produit en raison de la manière dont les objets 3D sont généralement dessinés. Pour chaque géométrie, chaque triangle est dessiné un par un. Lorsque chaque pixel du triangle est dessiné, 2 choses sont enregistrées. Premièrement, la couleur de ce pixel, et deuxièmement, la profondeur de ce pixel. Lorsque le triangle suivant est dessiné, pour chaque pixel, si la profondeur est plus importante que la profondeur précédemment enregistrée, aucun pixel n'est dessiné.

Cela fonctionne très bien pour les objets opaques, mais échoue pour les objets transparents.

La solution consiste à trier les objets transparents et à dessiner ceux situés à l'arrière avant de dessiner ceux à l'avant. THREE.js le fait pour les objets comme les `Mesh`, sinon le tout premier exemple aurait échoué entre les cubes, certains cubes bloquant les autres. Malheureusement, pour les triangles individuels, le tri serait extrêmement lent.

Le cube a 12 triangles, 2 pour chaque face, et l'ordre dans lequel ils sont dessinés est le même que celui dans lequel ils sont construits dans la géométrie. Ainsi, selon la direction dans laquelle nous regardons, les triangles les plus proches de la caméra peuvent être dessinés en premier. Dans ce cas, les triangles à l'arrière ne sont pas dessinés. C'est pourquoi parfois nous ne voyons pas les faces arrière.

Pour un objet convexe comme une sphère ou un cube, une sorte de solution consiste à ajouter chaque cube à la scène deux fois. Une fois avec un matériau qui dessine uniquement les triangles orientés vers l'arrière, et une autre fois avec un matériau qui dessine uniquement les triangles orientés vers l'avant.

    function makeInstance(geometry, color, x, y, z) {
    +  [THREE.BackSide, THREE.FrontSide].forEach((side) => {
        const material = new THREE.MeshPhongMaterial({
          color,
          opacity: 0.5,
          transparent: true,
    +      side,
        });

        const cube = new THREE.Mesh(geometry, material);
        scene.add(cube);

        cube.position.set(x, y, z);
    +  });
    }

Et avec cela, cela semble fonctionner.

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

Cela suppose que le tri de three.js est stable. C'est-à-dire que parce que nous avons ajouté le mesh `side: THREE.BackSide` en premier et parce qu'il est exactement à la même position, il sera dessiné avant le mesh `side: THREE.FrontSide`.

Créons 2 plans qui se croisent (après avoir supprimé tout le code relatif aux cubes). Nous allons ajouter une texture à chaque plan.

    const planeWidth = 1;
    const planeHeight = 1;
    const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight);

    const loader = new THREE.TextureLoader();

    function makeInstance(geometry, color, rotY, url) {
      const texture = loader.load(url, render);
      const material = new THREE.MeshPhongMaterial({
        color,
        map: texture,
        opacity: 0.5,
        transparent: true,
        side: THREE.DoubleSide,
      });

      const mesh = new THREE.Mesh(geometry, material);
      scene.add(mesh);

      mesh.rotation.y = rotY;
    }

    makeInstance(geometry, 'pink',       0,             'resources/images/happyface.png');
    makeInstance(geometry, 'lightblue',  Math.PI * 0.5, 'resources/images/hmmmface.png');

Cette fois, nous pouvons utiliser `side: THREE.DoubleSide` car nous ne pouvons jamais voir qu'une seule face d'un plan à la fois. Notez également que nous passons notre fonction `render` à la fonction de chargement de texture afin que lorsque la texture a fini de charger, nous rendions à nouveau la scène. C'est parce que cet exemple effectue un rendu à la demande au lieu d'un rendu continu.

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

Et encore une fois, nous voyons un problème similaire.

la moitié d'une face est manquante

La solution ici est de diviser manuellement chaque plan en 2 plans afin qu'il n'y ait réellement aucune intersection.

    function makeInstance(geometry, color, rotY, url) {
    +  const base = new THREE.Object3D();
    +  scene.add(base);
    +  base.rotation.y = rotY;

    +  [-1, 1].forEach((x) => {
        const texture = loader.load(url, render);
    +    texture.offset.x = x < 0 ? 0 : 0.5;
    +    texture.repeat.x = .5;
        const material = new THREE.MeshPhongMaterial({
          color,
          map: texture,
          opacity: 0.5,
          transparent: true,
          side: THREE.DoubleSide,
        });

        const mesh = new THREE.Mesh(geometry, material);
    -    scene.add(mesh);
    +    base.add(mesh);

    -    mesh.rotation.y = rotY;
    +    mesh.position.x = x * .25;
      });
    }

La manière d'y parvenir dépend de vous. Si j'utilisais un logiciel de modélisation comme Blender, je le ferais probablement manuellement en ajustant les coordonnées de texture. Ici cependant, nous utilisons `PlaneGeometry` qui, par défaut, étire la texture sur tout le plan. Comme nous l'avons vu précédemment, en définissant `texture.repeat` et `texture.offset`, nous pouvons mettre à l'échelle et déplacer la texture pour obtenir la bonne moitié de la texture de face sur chaque plan.

Le code ci-dessus crée également un `Object3D` et lui attache les 2 plans en tant qu'enfants. Il semblait plus facile de faire pivoter un `Object3D` parent que de faire les calculs nécessaires pour le faire sans.

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

Cette solution ne fonctionne vraiment que pour des choses simples comme 2 plans dont la position d'intersection ne change pas.

Pour les objets texturés, une autre solution consiste à définir un test alpha.

Un test alpha est un niveau d'alpha en dessous duquel three.js ne dessinera pas le pixel. Si nous ne dessinons pas du tout un pixel, alors les problèmes de profondeur mentionnés ci-dessus disparaissent. Pour les textures aux bords relativement nets, cela fonctionne assez bien. Les exemples incluent les textures de feuilles sur une plante ou un arbre, ou souvent une parcelle d'herbe.

Essayons sur les 2 plans. Utilisons d'abord des textures différentes. Les textures ci-dessus étaient 100% opaques. Ces 2 utilisent la transparence.

Retournons aux 2 plans qui se croisent (avant de les diviser) et utilisons ces textures et définissons un `alphaTest`.

    function makeInstance(geometry, color, rotY, url) {
      const texture = loader.load(url, render);
      const material = new THREE.MeshPhongMaterial({
        color,
        map: texture,
    -    opacity: 0.5,
        transparent: true,
    +    alphaTest: 0.5,
        side: THREE.DoubleSide,
      });

      const mesh = new THREE.Mesh(geometry, material);
      scene.add(mesh);

      mesh.rotation.y = rotY;
    }

    -makeInstance(geometry, 'pink',       0,             'resources/images/happyface.png');
    -makeInstance(geometry, 'lightblue',  Math.PI * 0.5, 'resources/images/hmmmface.png');
    +makeInstance(geometry, 'white', 0,             'resources/images/tree-01.png');
    +makeInstance(geometry, 'white', Math.PI * 0.5, 'resources/images/tree-02.png');

Avant d'exécuter cela, ajoutons une petite interface utilisateur pour pouvoir jouer plus facilement avec les paramètres `alphaTest` et `transparent`. Nous utiliserons lil-gui comme nous l'avons présenté dans l'article sur le graphe de scène de three.js.

Nous allons d'abord créer une aide pour lil-gui qui définit une valeur pour chaque matériau de la scène.

    class AllMaterialPropertyGUIHelper {
      constructor(prop, scene) {
        this.prop = prop;
        this.scene = scene;
      }
      get value() {
        const {scene, prop} = this;
        let v;
        scene.traverse((obj) => {
          if (obj.material && obj.material[prop] !== undefined) {
            v = obj.material[prop];
          }
        });
        return v;
      }
      set value(v) {
        const {scene, prop} = this;
        scene.traverse((obj) => {
          if (obj.material && obj.material[prop] !== undefined) {
            obj.material[prop] = v;
            obj.material.needsUpdate = true;
          }
        });
      }
    }

Ensuite, nous allons ajouter l'interface graphique.

    const gui = new GUI();
    gui.add(new AllMaterialPropertyGUIHelper('alphaTest', scene), 'value', 0, 1)
        .name('alphaTest')
        .onChange(requestRenderIfNotRequested);
    gui.add(new AllMaterialPropertyGUIHelper('transparent', scene), 'value')
        .name('transparent')
        .onChange(requestRenderIfNotRequested);

et bien sûr, nous devons inclure lil-gui.

    import * as THREE from 'three';
    import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
    +import {GUI} from 'three/addons/libs/lil-gui.module.min.js';

et voici les résultats.

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

Vous pouvez voir que cela fonctionne, mais zoomez et vous verrez qu'un plan a des lignes blanches.

C'est le même problème de profondeur qu'auparavant. Ce plan a été dessiné en premier, donc le plan situé derrière n'est pas dessiné. Il n'y a pas de solution parfaite. Ajustez l'`alphaTest` et/ou désactivez `transparent` pour trouver une solution qui correspond à votre cas d'utilisation.

La conclusion de cet article est que la transparence parfaite est difficile. Il y a des problèmes, des compromis et des solutions de contournement.

Par exemple, disons que vous avez une voiture. Les voitures ont généralement des pare-brise sur les 4 côtés. Si vous voulez éviter les problèmes de tri ci-dessus, vous devrez faire de chaque fenêtre son propre objet afin que three.js puisse trier les fenêtres et les dessiner dans le bon ordre.

Si vous créez des plantes ou de l'herbe, la solution du test alpha est courante.

La solution que vous choisissez dépend de vos besoins.