La potenza della grafica nel Web – il caso di successo GELLIFY | Antreem
Vai al blog

La potenza della grafica nel Web – il caso di successo GELLIFY

di lettura
Massimo Artizzu
thumbnail

Il Web sta evolvendo verso una maggiore tridimensionalità, offrendo agli sviluppatori la possibilità di creare esperienze immersive e interattive per gli utenti attraverso l’utilizzo di tecnologie come WebGL, Three.js e CSS 3D.

GELLIFY, ha deciso di inserire nel suo progetto di revisione strategica del posizionamento, un elemento di tridimensionalità con l’obiettivo di rappresentare il potenziale trasformativo del gruppo all’interno del nuovo sito web.

In questo articolo approfondiremo la progettazione e l’implementazione del blob viola con uno sguardo verso il futuro su queste tecnologie.

Il web e gli scenari tridimensionali

Il World Wide Web è nato come sistema ipertestuale, vale a dire come un agglomerato di documenti di testi ed immagini interconnessi tra loro da collegamenti. Presto acquisì la funzionalità di avere form di invio dati al server, e un linguaggio di scripting (JavaScript) per l’interazione sul terminale, ma è sempre rimasto – ed è tutt’ora, a dire il vero – un sistema di esposizione documentale.

Già da qualche anno si vedono in rete veri e propri videogiochi che è possibile eseguire sui propri browser. E non si parla di giochi con interfaccia testuale o con grafica semplice, magari con variazioni saltuarie e dipendenti dalle azioni dei giocatori (si pensi agli scacchi o a giochi di carte), ma di veri e propri scenari tridimensionali con azione frenetica! E tutto questo non usando plugin di terze parti (come il buon vecchio Flash), ma è tutto gestito nativamente dal browser.

WebGL: sfruttare la GPU sul Web

I browser web nel tempo si sono dotati di supporto a tantissime tecnologie che hanno reso lo sviluppo di questi giochi possibile. Nello specifico:

  • API per la riproduzione diretta dell’audio;
  • API per la gestione dell’input da gamepad e altre periferiche;
  • WebAssembly per l’esecuzione di task intensivi su client;
  • accesso e persistenza di grandi volumi di dati su client;
  • e, soprattutto, accesso più diretto alle primitive grafiche del sistema.

Riguardo all’ultimo punto, le API WebGL sono la trasposizione sul web delle note specifiche OpenGL, grandemente usate per lo sviluppo di videogiochi e applicazioni. Il loro supporto è ormai consolidato da tempo su tutti i principali browser, ed è pronto all’utilizzo per il Web.

Caso d’uso: il “blob” GELLIFY

Queste tecnologie ci sono state utili per lo sviluppo del “blob” – un oggetto tridimensionale di forma mutevole e indefinita – che rappresenti il potenziale trasformativo del gruppo GELLIFY.

Idealmente, avremmo potuto usare un filmato – come quello presente in questo articolo – ma per avere un effetto migliore si è voluto che il “blob” fosse dinamico e reagisse al cambio di parametri (colore, increspatura, distorsione, ecc.) e all’interazione con l’utente. Cose che il web “classico”, fatto di <div> e <span>, non è adatto a fare.

Anche se è possibile creare oggetti tridimensionali con semplice HTML e CSS, anche con risultati molto soddisfacenti, è necessario molto lavoro manuale, soprattutto per la gestione dell’illuminazione. Inoltre, risulta diversi ordini di grandezza più lento di quanto riescono a fare le schede grafiche.

Il problema che si pone a questo punto è che passare dallo sviluppo di un DOM al mondo degli shader tipico dello sviluppo OpenGL non è immediato. Fortunatamente, la libreria Three.js è diventata uno standard de facto per la rappresentazione di oggetti e scene 3D sul web usando WebGL, offrendo un’API che nasconde interamente l’uso degli shader dietro le quinte e consentendo di concentrarci su dettagli di più alto livello piuttosto che quelli implementativi.

Three.js e le “scene” 3D

Ciò che sta alla base dell’uso di Three.js è il concetto di “scena”. Una scena è l’insieme di oggetti tridimensionali, la loro posizione e rotazione rispetto ad un sistema di riferimento ortogonale, e delle sorgenti di luce che consentano effettivamente di vedere qualcosa all’interno della scena stessa (senza di esse è come se osservassimo l’interno di una stanza completamente buia).

Un oggetto tridimensionale è definito dalla superficie che lo racchiude, la sua posizione e rotazione spaziale, e dalle caratteristiche del materiale della superifie, quali il colore, l’opacità, la reflettività e così via. Una superficie a sua volta è definita da un’insieme di triangoli che ne vanno ad approssimare il contorno (similmente a come un insieme di segmenti può approssimare una circonferenza), e quindi dai vertici dei triangoli stessi.

Una sfera in wireframe
Una sfera tridimensionale approssimata dalla triangolazione della sua superficie, evidenziata dall’effetto wireframe

È facile intuire come una superficie risulti meglio approssimata e più “liscia” quanto più fitti sono i suoi vertici e triangoli. Il problema è che ogni triangolo deve essere calcolato e disegnato singolarmente, e fa poca differenza la dimensione degli stessi. Di conseguenza, diventa fondamentale trovare il corretto bilanciamento tra la definizione degli oggetti 3D e la capacità di renderizzarli velocemente.

Il “blob”

L’approccio iniziale per il nostro “blob” è stato quello di partire da una semplice sfera:

Una sfera con 5 fonti di luce e immagine ambientale, renderizzata con Three.js
Una sfera con 5 fonti di luce e immagine ambientale, renderizzata con Three.js

Il punto successivo è stato quello di andare a modificare i vertici che definiscono la triangolazione della sfera in modo che venisse introdotta un certo livello di increspatura. Questo è stato effettuato usando una funzione matematica nota come rumore di Perlin abbinata ad una funzione d’onda in dipendenza dall’asse temporale.

Si è raggiunto subito un risultato accettabile, ma si è presto raggiunto un limite di performance dovuto alla quantità di poligoni (triangoli) usati per la scena 3D:

Per ottenere un’animazione fluida, si dovrebbe puntare a disegnare intorno a 60 frame al secondo, usando quindi non più di 16.7 millisecondi per calcolare e disegnare ogni poligono del “blob”. Ci penserà il browser con il metodo requestAnimationFrame a “dare i tempi” per ogni frame, in dipendenza eventualmente dalla frequenza di aggiornamento dello schermo in uso (i display degli smartphone di fascia alta hanno comunemente una frequenza di 120 Hz, mentre i monitor da gaming possono raggiungere 200 Hz e oltre).

La questione si poteva fare ancora più problematica se si pensa che questo componente può essere visualizzato anche su dispositivi mobile e/o con scarse potenzialità grafiche. Le soluzioni possibili a questo punto potevano essere:

  • una semplice, che prevedeva di ridurre il numero di poligoni utilizzati;
  • una più evoluta, che consisteva di adattare dinamicamente i poligoni usati in base alle performance ottenute sul device dell’utente;
  • una più impegnativa, corrispondente all’uso di metodi più performanti per il rendering dei poligoni.

La prima soluzione ha reso evidente come poter “accontentare” la maggior parte dei dispositivi avrebbe comportato un abbassamento della definizione tale da imbruttire il risultato finale, e quindi è stata scartata. La seconda, invece, ha avuto migliori risultati con però lo scotto di avere un fastidioso istante iniziale di “rallentamento” dovuto all’uso di una definizione elevata e una reazione alla misurazione delle performance applicata dopo un tempo minimo di un secondo.

Dopo un’attenta analisi, si è notato che i problemi di performance non erano tanto legati al calcolo dell’illuminazione e al disegno dei singoli poligoni (effettuati dalla GPU), ma alle funzioni JavaScript create per calcolare le increspature sulla sfera e la correzione delle normali, che consistevano in migliaia di calcoli vettoriali per ogni frame dell’animazione. Antreem ha quindi puntato decisamente sulla terza soluzione.

A caccia di performance: i custom shader

Come accennato, Three.js nasconde dietro le sue API la programmazione diretta degli shader della GPU in modo da offrire un approccio semplice agli sviluppatori. Tuttavia, per i più intraprendenti, è comunque possibile accedere a queste API di “basso livello”. Prima, però, è necessario avere una comprensione migliore di cosa avviene quando una scheda grafica renderizza una scena tridimensionale.

Il processo che porta al disegno finale su schermo si basa una pipeline grafica, suddivisa in diversi stadi, in ognuno di quali viene eseguita una specifica fase dell’elaborazione (calcolo posizioni, esclusione poligoni non visibili, tracciamento fonti di luce e così via).

Fasi della pipeline grafica OpenGL
Fasi della pipeline grafica OpenGL

La specifica WebGL, quale transposizione delle API OpenGL per il Web, consente di operare direttamente su due di questi stadi con la programmazione dei rispettivi shader; nello specifico:

  • i vertex shader, che si occupano della definizione della posizione spaziale dei vertici dei poligoni e delle loro normali;
  • i fragment shader, che sono dedicati alla definizione del colore di ogni pixel renderizzato.

Per i nostri scopi, abbiamo avuto di necessità di operare solo sui vertex shader, che effettivamente corrispondevano alla fase che fino a quel momento era il “collo di bottiglia” per le performance. Al contrario di JavaScript, le schede grafiche sono grandemente performanti nei calcoli vettoriali e fortemente orientate alla parallelizzazione degli stessi, rendendole candidate ideali per questo tipo di calcoli.

A questo punto, il task principale è diventato il porting dei metodi JavaScript usati per il rumore superficiale nel corrispettivo linguaggio di programmazione degli shader, o GLSL – che è un linguaggio con molte più similarità a C che a JavaScript, e per questo piuttosto alieno allo sviluppo front-end più comune.

Fortunatamente, non è stato necessario scrivere tutto da principio, ma si è attinto a quanto messo a disposizione da Three.js nei suoi sorgenti.

uniform float time;
uniform float distort;
uniform float speed;
uniform float radius;
uniform float bumpPoleAmount;
uniform float numberOfWaves;
uniform bool fixNormals;
// ...

float computeMultiplier(vec3 point) {
  float amount = sin(smoothstep(-1., 1., point.y) * PI_VAL);
  float bumpPoleAmount = mix(amount, 1.0, bumpPoleAmount);
  float wavePoleAmount = mix(amount, 1.0, surfacePoleAmount);

  // computing bump noise + wave noise ...

  return bump * bumpPoleAmount + waves * wavePoleAmount;
}

// Adapted from the original MeshPhysicalMaterial vertex shader from three@0.142.0
void main() {
  vec3 displacedNormal = normalize(normal);
  vec3 displacedPosition = position + displacedNormal * radius * computeMultiplier(position / radius);

  if(fixNormals) {
    // ...
  }
  // ...
}

Il risultato finale ha centrato appieno le aspettative, raggiungendo agevolmente 160 fps in un monitor da gaming e non presentando problemi di sorta anche in dispositivi mobili di fascia media (con 60 fps raggiunti su un iPhone 7) senza rinunciare ad un’elevata definizione dell’oggetto.

Uno sguardo al futuro

Per quanto diverse tecnologie siano già mature, la strada per una piena adozione delle tecnologie 3D e in generale grafica ad alta performance sul Web non è ancora breve. Anche lo standard WebGL risentirà del fatto che Khronos Group, l’ente che si è occupato degli sviluppi delle API OpenGL, ha interrotto gli sviluppi di quest’ultima nel 2017 per dedicarsi al successore Vulkan, con minore impatto sulla CPU e supporto nativo al ray tracing. WebGPU sarà un’evoluzione di WebGL basata su Vulkan/Metal/DirectX.

In ogni caso, la strada in questione esiste e viene calcata con decisione da sempre più implementazioni sul Web, quindi aspettiamoci di vedere sempre più “magie” realizzate con le GPU dei nostri dispositivi.

Massimo Artizzu
Scritto da
Massimo Artizzu
Formato in Matematica, ma adottato professionalmente nella programmazione, un pallino avuto sin da piccolo. Amante di enigmi e origami, giocatore di rugby per diletto.