Realizzare 100.000 stelle

Michael Chang
Michael Chang

Ciao! Mi chiamo Michael Chang e lavoro con il Data Arts Team di Google. Di recente abbiamo completato 100.000 stelle, un esperimento di Chrome per visualizzare le stelle vicine. Il progetto è stato creato con THREE.js e CSS3D. In questo case study illustrerò il processo di scoperta, condividerò alcune tecniche di programmazione e terminerò con alcune riflessioni per migliorare il futuro.

Gli argomenti qui discussi sono piuttosto ampi e richiedono una certa conoscenza di THREE.js, anche se spero che tu possa apprezzare questo approccio post-mortem tecnico. Sentiti libero di passare a un'area di interesse utilizzando il pulsante del sommario a destra. Per prima cosa mostrerò la parte del progetto relativa al rendering, seguita dalla gestione dello Shadr e infine come utilizzare le etichette di testo CSS in combinazione con WebGL.

100.000 stelle, un esperimento di Chrome del team Data Arts
100.000 stelle utilizza THREE.js per visualizzare le stelle vicine nella Via Lattea

Alla scoperta dello spazio

Poco dopo aver terminato Small Arms Globe, stavo sperimentando una demo sulle particelle THREE.js con profondità di campo. Ho notato che potevo cambiare la "scala" interpretata della scena regolando la quantità dell'effetto applicato. Quando l'effetto di profondità di campo era davvero estremo, gli oggetti lontani diventavano davvero sfocati, in modo simile alla fotografia tilt-shift, che permette di creare l'illusione di una scena microscopica. Al contrario, la riduzione dell'effetto faceva sembrare come se stessi osservando lo spazio profondo.

Iniziai a cercare dati che avrei potuto utilizzare per inserire le posizioni delle particelle, un percorso che mi portasse al database HYG di astronexus.com, una compilazione delle tre origini dati (Hipparcos, Yale Bright Star Catalog e Gliese/Jahreiss Catalog) accompagnate da coordinate cartesiane xyz precalcolate. Iniziamo.

Rappresentazione grafica dei dati delle stelle.
Il primo passaggio consiste nel rappresentare graficamente ogni stella del catalogo come una singola particella.
Le stelle con nome.
Alcune stelle nel catalogo hanno nomi propri, indicati qui.

Ci è voluta circa un'ora per hackerare qualcosa che mise i dati stellari nello spazio 3D. Ci sono esattamente 119.617 stelle nel set di dati, quindi rappresentare ogni stella con una particella non è un problema per una GPU moderna. Esistono inoltre 87 stelle identificate singolarmente, quindi ho creato un overlay di indicatori CSS utilizzando la stessa tecnica che ho descritto in Small Arms Globe.

In quel periodo avevo appena finito la serie Mass Effect. Nel gioco il giocatore è invitato a esplorare la galassia e a scansionare vari pianeti e leggere la loro storia completamente immaginaria e al suono di Wikipedia: quali specie erano fiorite sul pianeta, la sua storia geologica e così via.

Conoscendo la quantità di dati reali che esistono sulle stelle, si potrebbe verosimilmente presentare allo stesso modo informazioni reali sulla galassia. L'obiettivo principale di questo progetto è dare vita a questi dati, consentire allo spettatore di esplorare la galassia à la Mass Effect, di conoscere le stelle e la loro distribuzione e, speriamo, di ispirare un senso di stupore e meraviglia sullo spazio. Finalmente.

Probabilmente dovrei introdurre il resto di questo case study dicendo che non sono affatto un astronomo e che si tratta di un lavoro di ricerca amatoriale supportato da alcuni consigli di esperti esterni. Questo progetto deve sicuramente essere interpretato come un'interpretazione artistica dello spazio.

La costruzione di una galassia

Il mio piano era quello di generare proceduralmente un modello della galassia in grado di contestualizzare i dati delle stelle e, speriamo, di offrire una visuale eccezionale del nostro posto nella Via Lattea.

Uno dei primi prototipi della galassia.
Un primo prototipo del sistema di particelle della Via Lattea.

Per generare la Via Lattea, ho generato 100.000 particelle e le ho posizionate in una spirale emulando il modo in cui si formano le braccia galattiche. Non mi preoccupavano troppo le specifiche della formazione dei bracci a spirale, perché sarebbe un modello rappresentativo piuttosto che matematico. Tuttavia, ho provato a ottenere il numero di bracci a spirale più o meno corretto e a ruotare nella "direzione giusta".

Nelle versioni successive del modello della Via Lattea ho sottolineato l'uso delle particelle a favore di un'immagine planare di una galassia che le accompagni, nella speranza che abbia un aspetto più fotografico. L'immagine effettiva è la galassia a spirale NGC 1232 a circa 70 milioni di anni luce da noi, manipolata in modo da assomigliare alla Via Lattea.

Capire la scala della galassia.
Ogni unità GL è un anno luce. In questo caso la sfera è larga 110.000 anni luce e comprende il sistema particellare.

Ho deciso fin da subito di rappresentare un'unità GL, fondamentalmente un pixel in 3D, per un anno luce. Si tratta di una convenzione che unificava il posizionamento per tutto ciò che veniva visualizzato, ma purtroppo in seguito mi presentava seri problemi di precisione.

Un'altra convenzione che ho deciso era quella di ruotare l'intera scena invece di spostare la videocamera, un'operazione che ho fatto in altri progetti. Un vantaggio è che tutto è posizionato su un "girevole" in modo che, trascinando il mouse verso sinistra e verso destra, l'oggetto in questione ruoti verso sinistra e verso destra, ma per aumentare lo zoom basta cambiare camera.position.z.

Anche il campo visivo della videocamera è dinamico. Mentre uno spinge verso l'esterno, il campo visivo si allarga, conquistando sempre più della galassia. Il contrario avviene quando ci si sposta verso l'interno verso una stella e il campo visivo si restringe. Ciò consente alla videocamera di vedere cose che sono infinitesimali (rispetto alla galassia) schiacciando il FOV fino a qualcosa di una lente d'ingrandimento simile a quella di un dio, senza dover gestire problemi di taglio del piano quasi.

Diversi modi di rappresentare una galassia.
(sopra) Galassia delle particelle iniziali. (sotto) Particelle accompagnate da un piano visivo.

Da qui sono riuscita a "posizionare" il Sole a un certo numero di unità di distanza dal nucleo galattico. Sono riuscito anche a visualizzare le dimensioni relative del sistema solare tracciando il raggio della scogliera di Kuiper (alla fine ho scelto di visualizzare la nuvola di Ort). All'interno di questo modello di sistema solare, potrei anche visualizzare un'orbita terrestre semplificata e a confronto il raggio effettivo del sole.

Il sistema solare.
Il Sole orbita intorno a diversi pianeti e una sfera che rappresenta la cintura di Kuiper.

Il Sole era difficile da visualizzare. Dovevo barare con tutte le tecniche grafiche in tempo reale che conoscevo. La superficie del Sole è una schiuma calda di plasma e ha bisogno di pulsare e cambiare nel tempo. Ciò è stato simulato tramite una texture bitmap di un'immagine a infrarossi della superficie solare. Lo strumento di tonalità della superficie esegue la ricerca dei colori in base alla scala di grigi di questa texture ed esegue la ricerca in una rampa di colore separata. Quando questa ricerca viene spostata nel tempo, crea questa distorsione simile a lava.

Una tecnica simile è stata utilizzata per la corona del Sole, ad eccezione del fatto che si tratta di una carta sprite piatta rivolta sempre alla fotocamera utilizzando https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js.

Rendering Sol.
La prima versione del Sole.

I bagliori solari sono stati creati tramite i versi e i frammenti applicati a un toro, che giravano intorno al bordo della superficie solare. Lo strumento di tessitura dei vertici ha una funzione di rumore che lo rende simile a una bolla.

È stato qui che iniziavo a riscontrare alcuni problemi di Z-fighting causato dalla precisione del GL. Tutte le variabili per la precisione erano predefinite in THREE.js, quindi non potevo aumentare realisticamente la precisione senza un'enorme quantità di lavoro. I problemi di precisione non erano così gravi vicino all'origine. Tuttavia, quando ho iniziato a modellare altri sistemi stellari, questo è diventato un problema.

Modello stellare.
Il codice per il rendering del Sole è stato successivamente generalizzato per il rendering di altre stelle.

Ho usato alcuni attacchi per mitigare lo z-fighting. Material.polygonoffset di TREE è una proprietà che consente la visualizzazione dei poligoni in una posizione percepita diversa (per quanto ho capito). Questo serviva per forzare il rendering dell'aereo della corona sempre in cima alla superficie del Sole. Al di sotto, è stato reso un "alone" del Sole per emettere raggi di luce affilati che si allontanavano dalla sfera.

Un altro problema legato alla precisione era che i modelli stellari iniziavano a tremolare quando la scena aumentava lo zoom. Per risolvere il problema, ho dovuto azzerare la rotazione della scena e ruotare separatamente il modello stellare e la mappa ambientale per dare l'illusione di orbitare intorno alla stella.

Creazione di Lensflare in corso...

Da un grande potere derivano grandi responsabilità.
Da un grande potere derivano grandi responsabilità.

Le visualizzazioni dello spazio sono il momento in cui mi sembra di poter farla franca con l'uso eccessivo di riflesso. THREE.LensFlare serve a questo scopo, tutto quello che dovevo fare era aggiungere alcuni esagoni anamorfici e un trattino di JJ Abrams. Lo snippet seguente mostra come crearli nella tua scena.

// This function returns a lesnflare THREE object to be .add()ed to the scene graph
function addLensFlare(x,y,z, size, overrideImage){
var flareColor = new THREE.Color( 0xffffff );

lensFlare = new THREE.LensFlare( overrideImage, 700, 0.0, THREE.AdditiveBlending, flareColor );

// we're going to be using multiple sub-lens-flare artifacts, each with a different size
lensFlare.add( textureFlare1, 4096, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );

// and run each through a function below
lensFlare.customUpdateCallback = lensFlareUpdateCallback;

lensFlare.position = new THREE.Vector3(x,y,z);
lensFlare.size = size ? size : 16000 ;
return lensFlare;
}

// this function will operate over each lensflare artifact, moving them around the screen
function lensFlareUpdateCallback( object ) {
var f, fl = this.lensFlares.length;
var flare;
var vecX = -this.positionScreen.x _ 2;
var vecY = -this.positionScreen.y _ 2;
var size = object.size ? object.size : 16000;

var camDistance = camera.position.length();

for( f = 0; f < fl; f ++ ) {
flare = this.lensFlares[ f ];

flare.x = this.positionScreen.x + vecX * flare.distance;
flare.y = this.positionScreen.y + vecY * flare.distance;

flare.scale = size / camDistance;
flare.rotation = 0;

}
}

Un modo semplice per scorrere la texture

Ispirato a Homeworld.
Un aereo cartesiano per facilitare l'orientamento spaziale nello spazio.

Per il "piano di orientamento spaziale", è stato creato un gigantesco THREE.CylinderGeometry(), che era centrato sul Sole. Per creare un'"onda di luce" che si sventola verso l'esterno, ne ho modificato l'offset di texture nel tempo in questo modo:

mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}

map è la texture appartenente al materiale, che riceve una funzione onUpdate che puoi sovrascrivere. L'impostazione del relativo offset fa sì che la texture venga "scorriata" lungo l'asse e l'invio di spam needUpdate = true forzerebbe il loop di questo comportamento.

Utilizzo delle rampe dei colori

Ogni stella ha un colore diverso in base a un "indice di colori" assegnato dagli astronomi. In generale, le stelle rosse sono più fredde, mentre le stelle blu/viola sono più calde. In questo gradiente è presente una banda di colori bianco e arancione intermedio.

Per il rendering delle stelle, volevo dare a ogni particella il proprio colore in base a questi dati. Per farlo, è stato possibile assegnare gli "attributi" al materiale dello Shadr applicati alle particelle.

var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};

Il riempimento dell'array colorIndex darà a ogni particella il suo colore univoco nello Shadr. Normalmente si passa in un colore vec3, ma in questo caso passo in un float per l'eventuale ricerca della rampa di colore.

Rampa di colore.
Una rampa di colore utilizzata per cercare il colore visibile dall'indice di colori di una stella.

La rampa dei colori era simile a questa, ma dovevo accedere ai dati dei colori bitmap da JavaScript. Per farlo, ho caricato innanzitutto l'immagine nel DOM, disegnarla in un elemento canvas e infine accedere alla bitmap canvas.

// make a blank canvas, sized to the image, in this case gradientImage is a dom image element
gradientCanvas = document.createElement('canvas');
gradientCanvas.width = gradientImage.width;
gradientCanvas.height = gradientImage.height;

// draw the image
gradientCanvas.getContext('2d').drawImage( gradientImage, 0, 0, gradientImage.width, gradientImage.height );

// a function to grab the pixel color based on a normalized percentage value
gradientCanvas.getColor = function( percentage ){
return this.getContext('2d').getImageData(percentage \* gradientImage.width,0, 1, 1).data;
}

Lo stesso metodo viene utilizzato per colorare singole stelle nella vista del modello stellare.

I miei occhi!
La stessa tecnica viene utilizzata per effettuare una ricerca del colore per la classe spettrale di una stella.

Wrangling ombreggiante

Nel corso del progetto ho scoperto che per realizzare tutti gli effetti visivi dovevo scrivere un numero sempre maggiore di shard. A questo scopo ho scritto a questo scopo un caricatore di smoother personalizzato, perché ero stufo di vedere i programmi in index.html.

// list of shaders we'll load
var shaderList = ['shaders/starsurface', 'shaders/starhalo', 'shaders/starflare', 'shaders/galacticstars', /*...etc...*/];

// a small util to pre-fetch all shaders and put them in a data structure (replacing the list above)
function loadShaders( list, callback ){
var shaders = {};

var expectedFiles = list.length \* 2;
var loadedFiles = 0;

function makeCallback( name, type ){
return function(data){
if( shaders[name] === undefined ){
shaders[name] = {};
}

    shaders[name][type] = data;

    //  check if done
    loadedFiles++;
    if( loadedFiles == expectedFiles ){
    callback( shaders );
    }

};

}

for( var i=0; i<list.length; i++ ){
var vertexShaderFile = list[i] + '.vsh';
var fragmentShaderFile = list[i] + '.fsh';

//  find the filename, use it as the identifier
var splitted = list[i].split('/');
var shaderName = splitted[splitted.length-1];
$(document).load( vertexShaderFile, makeCallback(shaderName, 'vertex') );
$(document).load( fragmentShaderFile,  makeCallback(shaderName, 'fragment') );

}
}

La funzione loadShaders() prende un elenco di nomi di file dello Shadr (prevede .fsh per il frammento e .vsh per i vertici Shadrs), tenta di caricare i propri dati e poi si limita a sostituire l'elenco con gli oggetti. Con le tue uniformi THREE.js, puoi passare iocchi in tonalità in questo modo:

var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});

Probabilmente avrei potuto utilizzare request.js, anche se sarebbe stato necessario riassemblare il codice proprio per questo scopo. Questa soluzione, sebbene molto più semplice, potrebbe essere migliorata a mio avviso, magari anche come estensione THREE.js. Se hai suggerimenti o consigli per farlo meglio, non esitare a contattarmi.

Etichette di testo CSS sopra THREE.js

Nel nostro ultimo progetto, Small Arms Globe, ho scherzato facendo apparire etichette di testo sopra una scena THREE.js. Il metodo che stavo utilizzando calcola la posizione assoluta del modello in cui voglio che appaia il testo, poi risolve la posizione dello schermo utilizzando THREE.Projector() e infine utilizza i CSS "in alto" e "a sinistra" per posizionare gli elementi CSS nella posizione desiderata.

Nelle prime fasi di questo progetto veniva utilizzata la stessa tecnica, ma non vedo l'ora di provare questo altro metodo descritto da Luis Cruz.

L'idea di base: abbinare la trasformazione della matrice CSS3D alla videocamera e alla scena di THREE, è possibile "posizionare" gli elementi CSS in 3D come se fossero in cima alla scena di TRE. Tuttavia, questo processo presenta delle limitazioni; ad esempio, non potrai far posizionare il testo sotto un oggetto THREE.js. Questa operazione è comunque molto più veloce rispetto al tentativo di eseguire il layout utilizzando gli attributi CSS "top" e "left".

Etichette di testo.
Utilizzo delle trasformazioni CSS3D per posizionare etichette di testo sopra WebGL.

Qui puoi trovare la demo (e il codice nel codice sorgente) di questo strumento. Tuttavia, ho scoperto che l'ordine delle matrici da allora è cambiato per THREE.js. La funzione che ho aggiornato:

/_ Fixes the difference between WebGL coordinates to CSS coordinates _/
function toCSSMatrix(threeMat4, b) {
var a = threeMat4, f;
if (b) {
f = [
a.elements[0], -a.elements[1], a.elements[2], a.elements[3],
a.elements[4], -a.elements[5], a.elements[6], a.elements[7],
a.elements[8], -a.elements[9], a.elements[10], a.elements[11],
a.elements[12], -a.elements[13], a.elements[14], a.elements[15]
];
} else {
f = [
a.elements[0], a.elements[1], a.elements[2], a.elements[3],
a.elements[4], a.elements[5], a.elements[6], a.elements[7],
a.elements[8], a.elements[9], a.elements[10], a.elements[11],
a.elements[12], a.elements[13], a.elements[14], a.elements[15]
];
}
for (var e in f) {
f[e] = epsilon(f[e]);
}
return "matrix3d(" + f.join(",") + ")";
}

Dato che tutto viene trasformato, il testo non è più rivolto verso la fotocamera. La soluzione consiste nell'utilizzare THREE.Gyroscope(), che obbliga un Object3D a "perdere" il suo orientamento ereditato dalla scena. Questa tecnica è chiamata "billboard" e il giroscopio è perfetto per farlo.

L'aspetto davvero interessante è che tutti i normali DOM e CSS sono rimasti lì, ad esempio il passaggio del mouse sopra un'etichetta di testo 3D e la possibilità di brillare con le ombre.

Etichette di testo.
Per fare in modo che le etichette di testo siano sempre rivolte verso la fotocamera, collegala a un TREE.Gyroscope().

Quando ho aumentato lo zoom, ho scoperto che il ridimensionamento della tipografia causava problemi di posizionamento. Forse questo è dovuto alla crenatura e alla spaziatura interna del testo? Un altro problema era che il testo diventava pixelato quando viene aumentato lo zoom, dato che il renderer DOM tratta il testo visualizzato come un riquadro con texture, un aspetto da considerare quando si utilizza questo metodo. Col senno di poi, avrei potuto semplicemente utilizzare un testo con dimensioni enormi per i caratteri, e forse questo è un aspetto da esplorare in futuro. In questo progetto ho anche utilizzato le etichette di testo per i posizionamenti CSS "in alto/a sinistra", descritte in precedenza, per elementi molto piccoli che accompagnano i pianeti del sistema solare.

Riproduzione di musica e loop

Il brano musicale suonato durante "Galactic Map" di Mass Effect è stato realizzato dai compositori di Bioware Sam Hulick e Jack Wall ed ha avuto l'emozione che volevo provare al visitatore. Volevamo un po' di musica nel nostro progetto perché ritenevamo che fosse una parte importante dell'atmosfera, e ci aiutasse a creare quel senso di stupore e stupore che cercavamo di raggiungere.

Il nostro produttore Valdean Klump ha contattato Sam, che ha lavorato a un gruppo di brani di Mass Effect che si sono esibite con grazia. Il brano intitolato "In a Strange Land".

Ho utilizzato il tag audio per la riproduzione di musica, tuttavia anche in Chrome l'attributo "loop" era inaffidabile, a volte il loop non funzionava. Alla fine, questo attacco a doppio tag audio è stato usato per controllare la fine della riproduzione e passare all'altro tag per la riproduzione. Ciò che è stato deludente è che il video non era ancora perfettamente in loop in ogni momento, ma penso che sia stato il meglio che potevo fare.

var musicA = document.getElementById('bgmusicA');
var musicB = document.getElementById('bgmusicB');
musicA.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playB = function(){
musicB.play();
}
// make it wait 15 seconds before playing again
setTimeout( playB, 15000 );
}, false);

musicB.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playA = function(){
musicA.play();
}
// otherwise the music will drive you insane
setTimeout( playA, 15000 );
}, false);

// okay so there's a bit of code redundancy, I admit it
musicA.play();

Margini di miglioramento

Dopo aver lavorato con THREE.js per un po', mi sembra di essere arrivato al punto in cui i miei dati si mescolavano troppo con il mio codice. Ad esempio, per definire in linea i materiali, le texture e le istruzioni di geometria, utilizzavo essenzialmente "modellazione 3D con codice". Questo non sembra essere un buon aspetto, ed è un'area in cui le attività future con THREE.js potrebbero migliorare notevolmente, ad esempio la definizione dei dati materiali in un file separato, preferibilmente visualizzabile e modificabile in un determinato contesto, e può essere riportato nel progetto principale.

Anche il nostro collega Ray McClure ha dedicato del tempo alla creazione di alcuni incredibili "rumore spaziali" generativi che hanno dovuto essere tagliati perché l'API Web Audio era instabile, causando ogni tanto l'arresto anomalo di Chrome. È una sfortuna... ma ci ha fatto pensare di più nel campo della sonorità per il lavoro futuro. Al momento della stesura di questo articolo, sono consapevole che l'API Web Audio ha subito delle patch, pertanto è possibile che stia funzionando ora, qualcosa da tenere d'occhio in futuro.

Gli elementi tipografici abbinati a WebGL rimangono ancora un problema e non sono sicuro al 100% di cosa stiamo facendo qui nel modo corretto. Sembra ancora un attacco. Forse le versioni future di THREE, con il suo prossimo renderer CSS, possono essere utilizzate per unire meglio i due mondi.

Crediti

Grazie ad Aaron Koblin per avermi permesso di andare in città con questo progetto. Jono Brandel per l'eccellente progettazione e implementazione dell'interfaccia utente, l'elaborazione del tipo e l'implementazione del tour. Valdean Klump per aver assegnato un nome al progetto e tutte le copie. Sabah Ahmed per aver liberato il tonnellata metrica dei diritti d'uso per le origini di dati e immagini. Clem Wright per aver contattato le persone giuste per la pubblicazione. Doug Fritz per l'eccellenza tecnica. George Brower per avermi insegnato JS e CSS. E, naturalmente, Mr. Doob per THREE.js.

Riferimenti