Création de labels 2D multi-lignes avec Three.js pour positionnement en temps réel

Pour intégrer des labels 2D multi-lignes qui restent alignés avec les modèles 3D dans une scène Three.js, notamment après des interactions de zoom ou de rotation, on utilise CSS2DObject et CSS2DRenderer. Cette approche permet de superposer des éléments HTML sur le canvas WebGL et de synchroniser leur position en écouter les événements des contrôles.

1. Configuration du renderer CSS2D

Initialisez un renderer dédié aux labels et positionnez-le en absolu pour qu'il chevauceh le canvas principal. Désactivez les interactions souris pour que les labels ne bloquent pas les événements de la scène 3D.

private initLabelOverlay() {
    this.labelRenderer = new CSS2DRenderer();
    this.labelRenderer.setSize(this.container.clientWidth, this.container.clientHeight);

    // Style pour superposition sur le canvas WebGL
    this.labelRenderer.domElement.style.position = "absolute";
    this.labelRenderer.domElement.style.top = "0";
    this.labelRenderer.domElement.style.left = "0";
    this.labelRenderer.domElement.style.pointerEvents = "none"; // Permet le travers des événements souris

    this.container.appendChild(this.labelRenderer.domElement);
}

2. Rendu dans la boucle d'animation

Ajoutez l'appel de rendu du renderer CSS2D dans votre fonction d'animation pour mettre à jour les labels à chaque frame.

this.labelRenderer.render(this.scene, this.camera);

3. Gestion du redimensionnement

Ajustez la taille du renderer CSS2D lors des changements de taille de la fenêtre pour maintenir la cohérence.

this.labelRenderer.setSize(this.width, this.height);

4. Génération d'éléments DOM pour labels

Créez une fonction pour construire des éléments HTML multi-lignes avec des styles personnalisables. Utilisez des paramètres pour contrôler l'apparence de chaque ligne et du conteneur.

export const buildMultiLineLabel = (
  textLines: Array<{
    content: string;
    style?: {
      textColor?: string;
      textSize?: string;
      bold?: boolean;
      bgColor?: string;
      spacing?: string;
      alignment?: string;
    };
  }>,
  containerStyle?: {
    bgColor?: string;
    padding?: string;
    rounded?: boolean;
    border?: string;
    shadow?: string;
  }
): HTMLElement => {
  const wrapper = document.createElement("div");
  wrapper.style.display = "flex";
  wrapper.style.flexDirection = "column";
  wrapper.style.pointerEvents = "none";
  wrapper.style.userSelect = "none";

  // Appliquer les styles du conteneur
  if (containerStyle) {
    if (containerStyle.bgColor) wrapper.style.backgroundColor = containerStyle.bgColor;
    if (containerStyle.padding) wrapper.style.padding = containerStyle.padding;
    if (containerStyle.rounded) wrapper.style.borderRadius = "4px";
    if (containerStyle.border) wrapper.style.border = containerStyle.border;
    if (containerStyle.shadow) wrapper.style.boxShadow = containerStyle.shadow;
  } else {
    wrapper.style.backgroundColor = "rgba(0, 50, 0, 0.7)";
    wrapper.style.padding = "5px 10px";
    wrapper.style.borderRadius = "3px";
  }

  // Ajouter les lignes de texte
  textLines.forEach((line) => {
    const lineDiv = document.createElement("div");
    lineDiv.textContent = line.content;

    // Style par défaut pour la ligne
    lineDiv.style.color = "#fff";
    lineDiv.style.fontSize = "13px";

    if (line.style) {
      if (line.style.textColor) lineDiv.style.color = line.style.textColor;
      if (line.style.textSize) lineDiv.style.fontSize = line.style.textSize;
      if (line.style.bold) lineDiv.style.fontWeight = "bold";
      if (line.style.bgColor) lineDiv.style.backgroundColor = line.style.bgColor;
      if (line.style.spacing) lineDiv.style.margin = line.style.spacing;
      if (line.style.alignment) lineDiv.style.textAlign = line.style.alignment;
    }

    wrapper.appendChild(lineDiv);
  });

  return wrapper;
};

5. Attachement de labels aux modèles

Associez des labels à des objets 3D en utilisant CSS2DObject. Stockez les références pour permettre des mises à jour ultérieures de position.

public attachLabelToObject(
  object3D: THREE.Object3D,
  htmlElement: HTMLElement,
  verticalOffset: number = 0.25
) {
  const labelObj = new CSS2DObject(htmlElement);
  const labelData = {
    target: object3D,
    offset: verticalOffset,
  };

  this.labelMap.set(labelObj, labelData);
  this.repositionAllLabels();
  object3D.add(labelObj);
}

6. Fonctions utilitaires pour le positionnement

Implémentez des helpers pour convertir les pixels en unités mondiales et recalculer les positions des labels en fonction de la géométrie des modèles.

private convertPixelsToWorld(pixelValue: number, labelRef: CSS2DObject): number {
  const worldPosition = labelRef.getWorldPosition(new THREE.Vector3());
  const distToCamera = this.camera.position.distanceTo(worldPosition);
  const verticalFovRad = THREE.MathUtils.degToRad(this.camera.fov);
  const worldHeightInView = 2 * distToCamera * Math.tan(verticalFovRad / 2);
  const pixelsPerWorldUnit = this.container.clientHeight / worldHeightInView;
  return pixelValue / pixelsPerWorldUnit;
}

private repositionAllLabels() {
  this.labelMap.forEach((data, labelObj) => {
    const boundingBox = new THREE.Box3().setFromObject(data.target);
    requestAnimationFrame(() => {
      const labelWorldHeight = this.convertPixelsToWorld(labelObj.element.offsetHeight, labelObj);
      const topPosition = boundingBox.max.y + data.offset + labelWorldHeight / 2;
      labelObj.position.set(0, topPosition, 0);
    });
  });
}

Pour garantir la synchronisation des labels lors des interactions utilisateur, écoutez les événements des contrôles et appelez la fonction de repositionnement.

this.controls.addEventListener("change", () => {
   this.repositionAllLabels();
});

Cette méthode assure que les labels restent ancrés aux objets 3D malgré les changements de vue, en mettant à jour leur position basée sur la boîte englobante des modèles.

Étiquettes: Three.js CSS2DObject CSS2DRenderer 3D labels real-time positioning

Publié le 3 août à 01h27