Three.js — Geometries

Gianluca Lomarco
9 min readJul 8, 2023

--

Fino ad ora abbiamo utilizzato solo la BoxGeometry con cui abbiamo creato un cubo e questo lo abbiamo manipolato e animato in diversi modi. Three.js però contiene molte classi diverse con le quali possiamo ottenere vari tipi di geometrie come:

  • sfere
  • cilindri
  • coni
  • tetraedri
  • dodecaedri
  • estrusioni

e molte altre ancora. Ma prima di approfondire queste geometrie abbiamo bisogno di capire che cosa sia una geometria e quali informazioni ci troviamo al suo interno.

Cos'è una geometria?

Le geometrie, in three.js sono composte da vertici, cioè punti nello spazio ognuno con le proprie coordinate, e da facce, di solito sono triangoli che uniscono tre vertici per creare una parte di superficie.

L'insieme di vertici e facce costituisce un reticolo geometrico che definisce un oggetto nello spazio e prende il nome di mesh (maglia).

Esiste anche la possibilità di avere delle geometrie formate solamente da vertici, indipendenti gli uni dagli altri, che quindi non compongono una vera e propria superficie, bensì essi costituiscono un particles system, ovvero un sistema di particelle. Analizzeremo questo argomento in maniera approfondita nei prossimi articoli.

Oltre a vertici e facce, nelle geometrie possono essere memorizzate altre informazioni, come per esempio le coordinate UV e le normali.

Coordinate UV

Le coordinate UV servono, nel caso applicassimo una texture alla geometria, per dire a three.js come mappare i pixel della texture sulla superficie della geometria stessa. Ad ogni vertice sono associate le coordinate uv corrispondenti ad un punto della texture.

Normali

Invece le normali sono dei vettori di lunghezza unitaria associati ad ogni singolo vertice appartenente ad un faccia e sono perpendicolari ad esse. Servono per lo più a indicare l'orientamento delle facce di una geometria e vengono utilizzati soprattuto per capire se una faccia viene colpita dalla luce e con che incidenza il vettore, che rappresenta la direzione della luce, colpisce le facce della geometria.

Ognuna di queste informazioni, vertici, coordinate UV e normali prendono il nome attributi della geometria e svolgono un ruolo molto importante all’interno della pipeline di rendering, in particolar modo all’interno del Vertex Shader del quale parleremo in un prossimo articolo.

Three.js built in geometries

Sulla documentazioni di three.js trovi l’elenco di tutte le geometrie che puoi utilizzare senza doverle creare a mano.

Per ognuna di esse vengono indicati i parametri da passare alla classe, e ti viene mostrata una finestra con l’anteprima della geometria, con un comodissimo pannello di controllo che ti aiuta a capire cosa fa ognuno dei parametri che la definiscono.

In un prossimo articolo impareremo anche noi ad aggiungere questo pannello di controllo alle nostre scene 3D.

Proprietà e metodi

Tutte queste estendono una classe chiamata BufferGeometry, ereditandone metodi e proprietà.

I metodi di questa classe sono molto interessanti, perché ci aiutano a manipolare le nostre geometrie.

Per esempio i metodi rotateX(…), rotateY(…), rotateZ(…) o applyQuaternion(), ci permettono di applicare delle rotazioni alla geometria modificando di conseguenza le coordinate dei vertici della stessa. Questo è diverso dall'applicare una rotazione alla classe Mesh. Infatti ruotare la mesh vuol dire modificare la matrice di trasformazione, cosa che lascerebbe invariate le coordinate dei vertici memorizzate nella geometria.

In maniera simile il metodo translate(x, y, z) ci permettere di applicare una traslazione ai vertici, modificandone le coordinate.

Inoltre abbiamo un metodo computeVertexNormals(), che ci permette di calcolare le normali della geometria a partire dalle coordinate dei vertici (nel caso fossero assenti, o nel caso in cui le coordinate dei vertici fossero cambiate nel tempo). Un altro metodo ancora computeTangents(), ci aiuta a calcolare le tangenti (vettori perpendicolari alle normali),ma per quest’ ultimo è necessario che siano presenti tutti e tre gli attributi, position, normal e uv.

Abbiamo anche metodi per aggiungere o rimuovere gli attributi dalla geometria, operazioni necessarie quando creiamo geometrie personalizzate alle quali applichiamo degli ShaderMaterial custom.

Box geometry

Adesso che sappiamo un pò di più sulle geometrie, analizziamo nel dettaglio la BoxGeometry e vediamo dove e come vengono creati gli attributi di tale geometria.

Avevamo già visto che per creare la BoxGeometry passavano 3 parametri:

  • width
  • height
  • depth
const geometry = new THREE.BoxGeometry(1, 1, 1)

Ottenendo questo risultato.

Facciamo una piccola modifica al materiale in modo da poter distinguere meglio il reticolo della mesh che viene creata.

const material = new THREE.MeshBasicMaterial({
wireframe: true,
color: 'green',
})

Con la proprietà wireframe impostata a true, diciamo a three.js di non renderizzare le facce della geometria, ma solamente il reticolo di triangoli che la compongono. Questo sarà il risultato.

Vediamo che ogni faccia del cubo è in realtà composta da 2 triangoli rettangoli.

Però la classe BoxGeometry poi può ricevere altri 3 parametri:

  • widthSegments
  • heightSegments
  • depthSegments

che ci permettono di aumentare il numero di vertici della geometria suddividendo in più parti il lati del cubo. Vediamo un esempio.

const geometry = new THREE.BoxGeometry(1, 1, 1, 2, 2, 2)

Come possiamo vedere ogni lato del cubo è stato suddiviso in 2 segmenti. Così facendo ognuna delle 6 facce viene divisa in 4 quadrati, ciascuno composta da 2 triangoli, per un totale di 8 triangoli per ognuna delle 6 facce.

Ci sono moltissimi motivi per cui può avere senso suddividere una superficie piana, come la faccia di un cubo, in più segmenti. Uno di questi può essere la creazione di terreni procedurali, o per esempio per simulazioni del moto ondoso.

Ma per adesso torniamo al caso più semplice in cui i lati del cubo non sono suddivisi in segmenti, e poniamoci una domanda.

const geometry = new THREE.BoxGeometry(1, 1, 1)

Quanti vertici ha questa geometria?

Da un punto di geometrico un cubo è composto da 8 vertici, 12 spigoli e 6 facce. Quindi vediamo se è vero.

I vertici delle geometrie vengono memorizzati in un attributo chiamato position, che possiamo trovare dentro a tutte le geometrie. Facciamoci un log della geometria nella console.

console.log(geometry)

All'interno della proprietà attributes troviamo i nostri 3 BufferAttribute:

  • normal
  • position
  • uv

Se espandiamo anche la proprietà position, notiamo diverse informazioni all'interno.

Nella chiave array ci sono memorizzate tutte le coordinate dei vertici sotto forma di Float32Array, che un TypedArray di javascript in cui vengono inseriti solo numeri di tipo float a 32 bit.

In questo array i numeri, a gruppi di tre in tre, rappresentano le coordinare x, y e z dei vertici.

Per cui se avessimo una geometria composta da 3 vertici, in questo array ci troveremmo 9 valori.

Se il cubo avesse 8 vertici, questo array dovrebbe contenere 24 valori.

Invece, possiamo vedere che il position attribute della nostra BoxGeometry contiene 72 valori. Quindi dividendo 72 per 3, che è la dimensione dei nostri vertici, otteniamo il numero 24. La nostra BoxGeometry ha 24 vertici, che corrisponde alla proprietà count. Mentre la proprietà itemSize contiene l'indicazione della dimensione delle entità rappresentate dal BufferAttribure, dato che sono vettori 3D, itemSize = 3. quindi è vera la seguente equazione.

count * itemSize = array.length

3 * 24 = 72

Come mai ci sono 24 vertici e non 8 come ci aspetteremmo? Abbiamo detto che un mesh è un reticolo di triangoli e ogni triangolo ha 3 vertici. Da quanti triangoli è composto il nostro cubo? Se ogni faccia è divisa in 2 triangoli e abbiamo 6 facce, scopriamo che il cubo è composto da 12 triangoli, che moltiplicati per 3 fa in totale 36 vertici. Ma ancora non ci siamo, perché noi abbiamo solo 24 vertici.

Per capire quale sia il problema analizziamo gli altri attributi, partendo dal normal attribute.

Per ogni vertice viene memorizzata la normale, una soltanto, cioè il vettore perpendicolare alla faccia a cui quel vertice appartiene. Già solo pensando a questa frase dovremmo poter incominciare a capire dove sta il problema.

In ogni caso, anche il normal attribute è un array e contiene tutte le componenti x, y e z delle normali.

L’uv attribute funziona nello stesso modo, ma in questo caso itemSize = 2, in quanto le coordinate uv sono bidimensionali dato che corrispondono alle coordinate di un pixel di una texture.

Riassumendo. Tutte le nostre geometrie sono composte da questi 3 attributi schematizzati qui sotto.

Esempio di geometria composta da solo 3 vertici.

È importante notare che la proprietà count di tutti gli attributi di una stessa geometria deve coincidere!

Index

Oltre agli attributi le geometrie di solito hanno anche una proprietà index, questa volta è un Uint16Array un array di numeri interi unsigned a 16 bit, che dice a three.js in che modo i vertici sono organizzati per formare i triangoli. Questo meccanismo ci permette di poter riutilizzare lo stesso vertice per più triangoli, riducendo le dimensioni dei buffer e migliorando le performance.

Ipotizzando di avere solo 3 vertici, i cui indici saranno 0, 1 e 2, la proprietà index sarà un array lungo 3, e conterrà i valori 0, 1 e 2 rappresentando quindi il triangolo composta da questi 3 vertici.

Ma se i vertici aumentano, aumentano anche i triangoli. Come potete vedere qui sotto, abbiamo 4 vertici che formano 2 triangoli (a e b) che condividono i vertici 1 e 2.

Con ancora più vertici il gioco si ripete.

Quindi potenzialmente gli spigoli del nostro cubo possono essere condivisi tra i vari triangoli adiacenti, ma c'è un problema. E cioè che possiamo memorizzare una singola normale per ogni vertice. Ma nel nostro cubo ogni vertice appartiene a facce che hanno 3 orientamenti diversi.

Quindi i vertici non possono essere condivisi tra tutti triangoli adiacenti. Solo quelli complanari tra loro potranno condividere tra loro i vertici. Come vediamo nell'esempio qui sotto le facce c e d, possono condividere i vertici 3 e 4.

Mentre le facce b e c, pur essendo adiacenti, non possono condividere i vertici 3 e 4, quindi il triangolo b dovrà usare altri vertici (6 e 7 coincidenti a 3 e 2) le cui normali saranno diverse da quelle dei vertici 3 e 2.

Possiamo concludere che per ogni vertice geometrico del cubo ci sono 3 vertici, ognuno con la propria normale.

3 * 8 = 24 vertici

Group

Inoltre dentro alla nostra geometria troviamo anche una proprietà group, che ci permette di raggruppare i triangoli che compongono la mesh in gruppi. Nel cubo per esempio possiamo notare che i triangoli sono organizzanti in 6 gruppi, uno per ogni faccia, che contengono i triangoli di cui ogni faccia è composta.

Dividere la mesh in gruppi ci permette di applicare più materiali diversi alla mesh, uno per ogni gruppo. Nel cubo per esempio potremmo applicare un materiale diverso ad ogni faccia.

Conclusioni

A questo punto le geometrie non dovrebbero avere più segreti per te. Ti invito ad andare sulla documentazione e giocare un po con tutte le geometrie presenti in three.js. Sia dal configuratore che nei tuoi esercizi.

Nel prossimo articolo parleremo di Texture, capiremo cosa sono e come caricarle su three.js per utilizzarle nei nostri materiali.

--

--

Gianluca Lomarco

I am a creative developer working with webGL and fullstack Main teacher at Boolean Academy. Now I’am starting a new experience as content creator in YouTube