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.