# Textures sur Canvas

Cet article fait suite à l'article sur les textures. Si vous ne l'avez pas encore lu, vous devriez probablement commencer par là.

Dans l'article précédent sur les textures, nous avons principalement utilisé des fichiers image pour les textures. Cependant, il arrive parfois que nous souhaitions générer une texture à l'exécution. Une façon de procéder est d'utiliser une `CanvasTexture`.

Une texture sur canvas prend un `<canvas>` en entrée. Si vous ne savez pas comment dessiner avec l'API canvas 2D sur un canvas, il existe un bon tutoriel sur MDN.

Créons un simple programme canvas. En voici un qui dessine des points à des endroits aléatoires et dans des couleurs aléatoires.

    const ctx = document.createElement('canvas').getContext('2d');
    document.body.appendChild(ctx.canvas);
    ctx.canvas.width = 256;
    ctx.canvas.height = 256;
    ctx.fillStyle = '#FFF';
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

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

    function drawRandomDot() {
      ctx.fillStyle = `#${randInt(0x1000000).toString(16).padStart(6, '0')}`;
      ctx.beginPath();

      const x = randInt(256);
      const y = randInt(256);
      const radius = randInt(10, 64);
      ctx.arc(x, y, radius, 0, Math.PI * 2);
      ctx.fill();
    }

    function render() {
      drawRandomDot();
      requestAnimationFrame(render);
    }
    requestAnimationFrame(render);

C'est assez simple.

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

Utilisons-le maintenant pour texturer quelque chose. Nous allons commencer avec l'exemple de texturation d'un cube tiré de l'article précédent. Nous allons supprimer le code qui charge une image et utiliser à la place notre canvas en créant une `CanvasTexture` et en lui passant le canvas que nous avons créé.

    const cubes = [];  // juste un tableau que nous pouvons utiliser pour faire tourner les cubes
    -const loader = new THREE.TextureLoader();
    -
    +const ctx = document.createElement('canvas').getContext('2d');
    +ctx.canvas.width = 256;
    +ctx.canvas.height = 256;
    +ctx.fillStyle = '#FFF';
    +ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    +const texture = new THREE.CanvasTexture(ctx.canvas);

    const material = new THREE.MeshBasicMaterial({
    -  map: loader.load('resources/images/wall.jpg'),
    +  map: texture,
    });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);
    cubes.push(cube);  // add to our list of cubes to rotate

Puis appelez le code pour dessiner un point aléatoire dans notre boucle de rendu.

    function render(time) {
      time *= 0.001;

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

    +  drawRandomDot();
    +  texture.needsUpdate = true;

      cubes.forEach((cube, ndx) => {
        const speed = .2 + ndx * .1;
        const rot = time * speed;
        cube.rotation.x = rot;
        cube.rotation.y = rot;
      });

      renderer.render(scene, camera);

      requestAnimationFrame(render);
    }

La seule chose supplémentaire que nous devons faire est de définir la propriété `needsUpdate` de la `CanvasTexture` pour indiquer à three.js de mettre à jour la texture avec le contenu le plus récent du canvas.

Et avec cela, nous avons un cube texturé sur canvas.

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

Notez que si vous souhaitez utiliser three.js pour dessiner dans le canvas, vous êtes il est préférable d'utiliser un `RenderTarget`, ce qui est abordé dans cet article.

Un cas d'utilisation courant pour les textures sur canvas est d'afficher du texte dans une scène. Par exemple, si vous vouliez afficher le nom d'une personne sur le badge de son personnage, vous pourriez utiliser une texture sur canvas pour texturer le badge.

Créons une scène avec 3 personnes et donnons à chaque personne un badge ou une étiquette.

Reprenons l'exemple ci-dessus et supprimons tout ce qui est lié au cube. Ensuite, mettons le fond en blanc et ajoutons deux lumières.

    const scene = new THREE.Scene();
    +scene.background = new THREE.Color('white');
    +
    +function addLight(position) {
    +  const color = 0xFFFFFF;
    +  const intensity = 1;
    +  const light = new THREE.DirectionalLight(color, intensity);
    +  light.position.set(...position);
    +  scene.add(light);
    +  scene.add(light.target);
    +}
    +addLight([-3, 1, 1]);
    +addLight([ 2, 1, .5]);

Écrivons du code pour créer une étiquette en utilisant le canvas 2D.

    +function makeLabelCanvas(size, name) {
    +  const borderSize = 2;
    +  const ctx = document.createElement('canvas').getContext('2d');
    +  const font =  `${size}px bold sans-serif`;
    +  ctx.font = font;
    +  // mesurer la longueur du nom
    +  const doubleBorderSize = borderSize * 2;
    +  const width = ctx.measureText(name).width + doubleBorderSize;
    +  const height = size + doubleBorderSize;
    +  ctx.canvas.width = width;
    +  ctx.canvas.height = height;
    +
    +  // besoin de redéfinir la police après avoir redimensionné le canvas
    +  ctx.font = font;
    +  ctx.textBaseline = 'top';
    +
    +  ctx.fillStyle = 'blue';
    +  ctx.fillRect(0, 0, width, height);
    +  ctx.fillStyle = 'white';
    +  ctx.fillText(name, borderSize, borderSize);
    +
    +  return ctx.canvas;
    +}

Ensuite, nous allons créer des personnages simples à partir d'un cylindre pour le corps, d'une sphère pour la tête et d'un plan pour l'étiquette.

Commençons par créer la géométrie partagée.

    +const bodyRadiusTop = .4;
    +const bodyRadiusBottom = .2;
    +const bodyHeight = 2;
    +const bodyRadialSegments = 6;
    +const bodyGeometry = new THREE.CylinderGeometry(
    +    bodyRadiusTop, bodyRadiusBottom, bodyHeight, bodyRadialSegments);
    +
    +const headRadius = bodyRadiusTop * 0.8;
    +const headLonSegments = 12;
    +const headLatSegments = 5;
    +const headGeometry = new THREE.SphereGeometry(
    +    headRadius, headLonSegments, headLatSegments);
    +
    +const labelGeometry = new THREE.PlaneGeometry(1, 1);

Ensuite, créons une fonction pour construire une personne à partir de ces éléments.

    +function makePerson(x, size, name, color) {
    +  const canvas = makeLabelCanvas(size, name);
    +  const texture = new THREE.CanvasTexture(canvas);
    +  // parce que notre canvas n'est probablement pas une puissance de 2
    +  // dans les deux dimensions, définissez le filtrage de manière appropriée.
    +  texture.minFilter = THREE.LinearFilter;
    +  texture.wrapS = THREE.ClampToEdgeWrapping;
    +  texture.wrapT = THREE.ClampToEdgeWrapping;
    +
    +  const labelMaterial = new THREE.MeshBasicMaterial({
    +    map: texture,
    +    side: THREE.DoubleSide,
    +    transparent: true,
    +  });
    +  const bodyMaterial = new THREE.MeshPhongMaterial({
    +    color,
    +    flatShading: true,
    +  });
    +
    +  const root = new THREE.Object3D();
    +  root.position.x = x;
    +
    +  const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
    +  root.add(body);
    +  body.position.y = bodyHeight / 2;
    +
    +  const head = new THREE.Mesh(headGeometry, bodyMaterial);
    +  root.add(head);
    +  head.position.y = bodyHeight + headRadius * 1.1;
    +
    +  const label = new THREE.Mesh(labelGeometry, labelMaterial);
    +  root.add(label);
    +  label.position.y = bodyHeight * 4 / 5;
    +  label.position.z = bodyRadiusTop * 1.01;
    +
    +  // si les unités sont des mètres, alors 0.01 ici ajuste la taille
    +  // de l'étiquette en centimètres.
    +  const labelBaseScale = 0.01;
    +  label.scale.x = canvas.width  * labelBaseScale;
    +  label.scale.y = canvas.height * labelBaseScale;
    +
    +  scene.add(root);
    +  return root;
    +}

Vous pouvez voir ci-dessus que nous plaçons le corps, la tête et l'étiquette sur un `Object3D` racine et ajustons leurs positions. Cela nous permettrait de déplacer l'objet racine si nous voulions déplacer les personnages. Le corps mesure 2 unités de haut. Si 1 unité équivaut à 1 mètre, alors le code ci-dessus tente de créer l'étiquette en centimètres, de sorte qu'elle mesurera 'size' centimètres de haut et la largeur nécessaire pour contenir le texte.

Nous pouvons ensuite créer des personnages avec des étiquettes.

    +makePerson(-3, 32, 'Purple People Eater', 'purple');
    +makePerson(-0, 32, 'Green Machine', 'green');
    +makePerson(+3, 32, 'Red Menace', 'red');

Il ne reste plus qu'à ajouter des `OrbitControls` afin de pouvoir déplacer la caméra.

    import * as THREE from 'three';
    +import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

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

    +const controls = new OrbitControls(camera, canvas);
    +controls.target.set(0, 2, 0);
    +controls.update();

et nous obtenons des étiquettes simples.

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

Quelques points à noter.

- Si vous zoomez, les étiquettes deviennent de faible résolution.

Il n'y a pas de solution simple. Il existe des techniques de rendu de police plus complexes, mais je ne connais pas de solutions sous forme de plugin. De plus, elles nécessiteraient que l'utilisateur télécharge les données de la police, ce qui serait lent.

Une solution consiste à augmenter la résolution des étiquettes. Essayez de doubler la taille passée en paramètre et de diviser par deux la valeur actuelle de `labelBaseScale`.

- Les étiquettes s'allongent plus le nom est long.

Si vous vouliez résoudre ce problème, vous choisiriez plutôt une étiquette de taille fixe et compresseriez le texte.

C'est assez facile. Passez une largeur de base et adaptez le texte à cette largeur comme ceci :

    -function makeLabelCanvas(size, name) {
    +function makeLabelCanvas(baseWidth, size, name) {
      const borderSize = 2;
    -  const ctx = document.createElement('canvas').getContext('2d');
      const font =  `${size}px bold sans-serif`;
      ctx.font = font;
      // mesurer la longueur du nom
    +  const textWidth = ctx.measureText(name).width;

      const doubleBorderSize = borderSize * 2;
    -  const width = ctx.measureText(name).width + doubleBorderSize;
    +  const width = baseWidth + doubleBorderSize;
      const height = size + doubleBorderSize;
      ctx.canvas.width = width;
      ctx.canvas.height = height;

      // besoin de redéfinir la police après avoir redimensionné le canvas
      ctx.font = font;
    -  ctx.textBaseline = 'top';
    +  ctx.textBaseline = 'middle';
    +  ctx.textAlign = 'center';

      ctx.fillStyle = 'blue';
      ctx.fillRect(0, 0, width, height);

    +  // adapter à la taille mais ne pas étirer
    +  const scaleFactor = Math.min(1, baseWidth / textWidth);
    +  ctx.translate(width / 2, height / 2);
    +  ctx.scale(scaleFactor, 1);
      ctx.fillStyle = 'white';
      ctx.fillText(name, borderSize, borderSize);

      return ctx.canvas;
    }

Nous pouvons ensuite passer une largeur pour les étiquettes.

    -function makePerson(x, size, name, color) {
    -  const canvas = makeLabelCanvas(size, name);
    +function makePerson(x, labelWidth, size, name, color) {
    +  const canvas = makeLabelCanvas(labelWidth, size, name);

    ...

    }

    -makePerson(-3, 32, 'Purple People Eater', 'purple');
    -makePerson(-0, 32, 'Green Machine', 'green');
    -makePerson(+3, 32, 'Red Menace', 'red');
    +makePerson(-3, 150, 32, 'Purple People Eater', 'purple');
    +makePerson(-0, 150, 32, 'Green Machine', 'green');
    +makePerson(+3, 150, 32, 'Red Menace', 'red');

et nous obtenons des étiquettes dont le texte est centré et adapté à la taille.

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

Ci-dessus, nous avons utilisé un nouveau canvas pour chaque texture. Utiliser ou non un canvas par texture dépend de vous. Si vous avez besoin de les mettre à jour souvent, avoir un canvas par texture est probablement la meilleure option. Si elles sont rarement ou jamais mises à jour, vous pouvez choisir d'utiliser un seul canvas pour plusieurs textures en forçant three.js à utiliser la texture. Modifions le code ci-dessus pour faire exactement cela.

    +const ctx = document.createElement('canvas').getContext('2d');

    function makeLabelCanvas(baseWidth, size, name) {
      const borderSize = 2;
    -  const ctx = document.createElement('canvas').getContext('2d');
      const font =  `${size}px bold sans-serif`;

      ...

    }

    +const forceTextureInitialization = function() {
    +  const material = new THREE.MeshBasicMaterial();
    +  const geometry = new THREE.PlaneGeometry();
    +  const scene = new THREE.Scene();
    +  scene.add(new THREE.Mesh(geometry, material));
    +  const camera = new THREE.Camera();
    +
    +  return function forceTextureInitialization(texture) {
    +    material.map = texture;
    +    renderer.render(scene, camera);
    +  };
    +}();

    function makePerson(x, labelWidth, size, name, color) {
      const canvas = makeLabelCanvas(labelWidth, size, name);
      const texture = new THREE.CanvasTexture(canvas);
      // parce que notre canvas n'est probablement pas une puissance de 2
      // dans les deux dimensions, définissez le filtrage de manière appropriée.
      texture.minFilter = THREE.LinearFilter;
      texture.wrapS = THREE.ClampToEdgeWrapping;
      texture.wrapT = THREE.ClampToEdgeWrapping;
    +  forceTextureInitialization(texture);

      ...

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

Un autre problème est que les étiquettes ne font pas toujours face à la caméra. Si vous utilisez les étiquettes comme des badges, c'est probablement une bonne chose. Si vous utilisez les étiquettes pour afficher les noms des joueurs dans un jeu en 3D, vous pourriez vouloir que les étiquettes fassent toujours face à la caméra. Nous aborderons comment faire cela dans un article sur les billboards.

Pour les étiquettes en particulier, une autre solution consiste à utiliser le HTML. Les étiquettes dans cet article sont à l'intérieur du monde 3D, ce qui est bien si vous voulez qu'elles soient cachées par d'autres objets, tandis que les étiquettes HTML sont toujours au-dessus.