Three.js: disegnare gli assi geometrici

Chiunque abbia lavorato con Three.js a un modello tridimensionale sa quanto è facile fare casino con gli assi e a un certo punto non ricordarsi più da che parte è l'asse X. O era l'asse Y? ARGH!!! Se poi un poveraccio sta cercando di ragionare con angoli, orbite, velocità e rotazioni il risultato è il cervello in fiamme.

Non è più comodo avere qualcosa del genere, anche solo per la fase di sviluppo/debug?

Per prima cosa dobbiamo inserire l'axes helper, un oggetto 3d che disegna i tre assi con tre colori diversi. Il codice è molto semplice:

var axis = new THREE.AxesHelper(30);
scene.add(axis);

Il parametro numerico nel costruttore dell'axes helper è la lunghezza di ciascun segmento (nel mio caso 30 unità).

Un ulteriore miglioramento a questa idea è scrivere le etichette dei tre assi, per evitare di confonderli. Questo passaggio è un filo più complicato. Si inizia procurandosi un font in formato JSON (si trova nelle cartelle di esempio di three.js o in giro). Si scarica il file e lo si copia nel progetto, magari in una cartella "fonts".

Il caricamento del font nella pagina è effettuato in maniera asincrona da un oggetto FontLoader, a cui passiamo una funzione di callback dentro la quale avremo a disposizione il font caricato. In questa funzione procediamo a creare individualmente le tre etichette con la solita procedura:

  • Si crea la geometria, di tipo TextGeometry, impostando testo, dimensione dei caratteri eccetera.
  • Si crea il materiale, impostando il colore.
  • Si crea la mesh, combinando geometria e materiale.
  • Si settano posizione e rotazione della nuova geometria.
  • Si inserisce la geometria nella scena.

Il codice che realizza tutto questo è il seguente:

// Funzione per creare la singola label
// Crea la label, la inserisce nella scena e ne restituisce 
// il riferimento.
function createAxisLabel(label, scene, font, x, y, z, color){

  const xGeo = new THREE.TextGeometry( label, {
    font: font,
    size: 1,
    height: .1,
    curveSegments: 6,
  });

  let  xMaterial = new THREE.MeshBasicMaterial({ color: color });
  let  xText = new THREE.Mesh(xGeo , xMaterial);
  
  xText.position.x = x
  xText.position.y = y
  xText.position.z = z
  xText.rotation.x = camera.rotation.x;
  xText.rotation.y = camera.rotation.y;
  xText.rotation.z = camera.rotation.z;
  scene.add(xText);  

  return xText

}

// Font loader
const loader = new THREE.FontLoader();

// Riferimento etichette 
// (per averle a disposizione fuori dal callback)
var xLabel, yLabel, zLabel

// Caricamento font (da cartella fonts) e creazione etichette
loader.load( 'fonts/helvetiker_regular.typeface.json', function(font){
  xLabel = createAxisLabel('X', scene, font, 30, 0, 0, 'red')
  yLabel = createAxisLabel('Y', scene, font, 0, 30, 0, 'green')
  zLabel = createAxisLabel('Z', scene, font, 0, 0, 30, 'blue')
});

Nell'esempio qui sopra la creazione delle etichette è demandata a una funzione che, oltre a quanto visto prima (geometria, materiale, mesh, scena, ...) ne imposta la posizione (calcolata manualmente per mettere le etichette in cima agli assi che ho creati prima) e la rotazione. Quest'ultima è importante perchè deve coincidere con la rotazione della camera, viceversa il testo non sarà leggibile correttamente.

Come avrete intuito, l'ultimo aspetto da considerare è proprio questo: alla creazione il testo è perpendicolare alla camera e quindi è perfettamente leggibile, ma se ruoto la camera? Soluzione: nella funzione di rendering, a ogni frame reimposto la rotazione delle tre etichette. In questo modo saranno sempre orientate verso l'osservatore. Naturalmente bisogna aver esposto le etichette all'esterno del callback del FontLoader (dichiarando le variabili all'esterno), e verificare di volta in volta se l'etichetta è valida (viene creata in maniera asincrona, quindi per i primi rendering potrebbe essere nulla).

var render = function (actions) {
  ...
  // Allineamento etichette assi
  if (xLabel){
    xLabel.rotation.x = camera.rotation.x;
    xLabel.rotation.y = camera.rotation.y;
    xLabel.rotation.z = camera.rotation.z;
  }
  if (yLabel){
    yLabel.rotation.x = camera.rotation.x;
    yLabel.rotation.y = camera.rotation.y;
    yLabel.rotation.z = camera.rotation.z;
  }
  if (zLabel){
    zLabel.rotation.x = camera.rotation.x;
    zLabel.rotation.y = camera.rotation.y;
    zLabel.rotation.z = camera.rotation.z;
  }
  ...
}

Il risultato è quello mostrato nella prima immagine in questo post.

Alla prossima!