martedì 1 aprile 2008

Post non adatto ai deboli di cuore

Questo post è il più lungo, complesso ed addentro a cassata che abbia mai scritto fino ad ora, per cui ne è consigliabile una lettura solo a chi ha molta voglia, o vuole interessarsi a come funziona cassata.
Si tratta di un riepilogo di tutto quello che è stato fatto fino ad ora, unito a moltissime cose nuove, ed in effetti è il lavoro che ho svolto negli ultimi tempi, nei quali codice ne ho scritto pochissimo, ed anticipa un libro che ho intenzione di scrivere per tutti gli sviluppatori vari.
Attenzione che non si tratta di nulla di immutabile, ogni cosa che citerò in questo post potrà essere cambiata in futuro, e presumibilmente qualcosa lo sarà. È comunque un post molto indicativo sul funzionamento di cassata e sul suo futuro e le sue possibilità, ne raccomando quindi la lettura a tutti coloro che sono sufficientemente motivati. Eviterò il più possibile termini ed espressioni che potrebbero risultare complessi per chi non è molto addentro all'argomento, ma non credo che riuscirò ad evitarli completamente, per cui cercate di seguire meglio che potete, eventualmente con wikipedia alla mano (meglio quello inglese). :)

Per prima cosa cominciamo col vedere quali sono i componenti in cassata.
Il primo componente è il core, è un demone, quindi un'applicazione che funziona in background. Gestisce tutti i lavori, ognuno dei quali è una scena. Potrebbe essere carino, più avanti, poter stabilire delle priorità sulle scene, o cose simili, in modo da poter renderizzare delle scene di prova senza appesantire le altre, ma non è una priorità.

Poi ci sono i plugin. Questi sono di due tipi. Il primo fornisce delle funzioni utilizzabili dagli shader. Il secondo filtra tutti i file, eccetto uno (che citerò dopo), presi da cassata, per interpretare qualunque file in una maniera che sia comprensibile a cassata od agli shader.
Questo secondo tipo di plugin è interessante, perché tramite questo è possibile aggiungere tipi di immagini non supportate (ad esempio una png sarà letta tramite un plugin, per dire), oppure interpretare shader scritti in qualunque linguaggio si voglia (purché un plugin sia in grado di tradurlo in un unico linguaggio compreso da cassata) e via dicendo.

Altri componenti, già abbondantemente citato, sono gli shader. Gli shader sono dei componenti fondamentali, che dicono come renderizzare qualcosa, ma il loro ruolo preciso sarà chiaro solo più avanti nel post.

Quindi ci sono i front end. Ogni modellatore potrà avere un suo front end (o più di uno) per poter integrare cassata direttamente all'interno del modellatore stesso. In più c'è un front end ufficiale, sganciato da altri modellatori, meringa (che tralaltro è il progetto più avanti dell'insieme di progetti cassata). Tramite i front end si usa cassata, che ricordo che rimane in background, nascosto all'utente. Ovviamente si può accedere con più front end contemporaneamente a cassata, perciò non è necessario che ognuno di questi possieda ogni caratteristica immaginabile, poiché è sempre possibile avviarne un altro se serve.

Poi ci sono i file di scena. Questi non fanno altro che passare parametri agli shader, crearne istanze e definire parametri per ogni pass (cos'è un pass sarà più chiaro dopo).
Una scena può includere altre scene, al più filtrate coi plugin precedenti. Solo la prima scena non può essere filtrata (e questa è l'unica eccezione che dicevo prima).
È importante notare che in questo modo è possibile scrivere front end che in maniera completamente trasparente passino scene di un renderer a cassata. Questa pratica è possibile ma in genere non raccomandabile, perché probabilmente converrebbe utilizzare l'altro renderer, che con ottime probabilità avrebbe prestazioni migliori di cassata e non richiederebbe del lavoro aggiuntivo per scrivere front end che traducano le scene ed eventualmente plugin o shader apposta. In alcuni casi comunque può essere conveniente.

Quindi ci sono tutti i dati. I dati vengono visti come shader da altri shader, quando filtrati dai vari plugin, per cui non si è limitati a nessun tipo. È anche possibile leggere suoni, filmati, immagini vettoriali, testo o qualunque altra cosa venga in mente. L'unica richiesta è avere plugin adatti a leggere il file in questione.

Da questo primo riepilogo è possibile già cogliere una sfumatura della flessibilità di cassata, che però faccio notare, come si può notare da quello che ho già datto, non fa molto. Offre solo la possibilità di estendersi. Alcuni plugin o shader saranno ufficiali, ma molte cose non sono fatte direttamente dal progetto ufficiale. Possono invece essere estese da altri utenti.
A tal proposito nascono due esigenze. La prima, più complessa, è argomento di discussione più avanti nel post, e riguarda la compatibilità tra le varie cose scritte. Sarebbe spiacevole se più utenti scrivessero cose diverse e poi non si potessero usare nello stesso progetto.

La seconda è più semplice da risolvere. C'è il rischio che certi plugin o shader non vengano più mantenuti, oppure rimangano sconosciuti.
Quello che mi piacerebbe fare è prendere quei plugin o shader che abbiano una certa licenza e che siano interessanti, e mantenerli, come viene fatto col linux kernel coi moduli, all'interno dei progetti ufficiali. L'autore potrebbe sempre continuare a mantenere il suo lavoro, ma nel momento in cui mancasse la sua partecipazione un altro utente potrebbe occuparsene, ed inoltre il suo lavoro sarebbe visibile a tutti e già presente quando s'installa cassata.

Ora passo a spiegare come funziona il core.
Un buon modo per approcciarsi al suo funzionamento è vedere come viene renderizzata una scena. Per spiegare questo però prima ho bisogno di spiegare cos'è un pass almeno brevemente, la spiegazione più completa verrà data quando descriverò le scene.
Un pass è una variante di un rendering. Potrebbe avere una qualità diversa, un oggetto in meno, qualunque cosa.

Per prima cosa un front end crea una scena. Quindi passa il file di scena e tutte le informazioni su dove trovare tutti gli altri file utili a cassata, che lo analizza e ritorna eventuali errori.
Successivamente il front end dice di renderizzare un dato pass. Cassata apre un thread e comincia a definire tutte le relazioni tra le sottoscene (che saranno più chiare in seguito) e gli shader. Compila tutti gli shader, quindi mette in esecuzione quello o quelli che servono per ricavare la scena. Quando viene fatta la richiesta di qualche dato (ad esempio l'immagine finale) da parte del front end invia il dato, dopo averlo filtrato col plugin apposito.
Ha anche il ruolo di sincronizzare i thread, ed i processi nel caso del rendering in rete.
Inoltre fornisce una libreria completa di funzionalità utili al rendering agli shader.

Una particolarità molto interessante del core è che utilizza un terzo tipo di plugin, nascosto. Questo renderizza ogni sottoscena, ma utilizzando tipi differenti a seconda del caso e della qualità richiesta, non scendendo così a compromessi ne in velocità ne in qualità (facendo così scegliere all'utente).

Passo a descrivere gli shader. Come si è visto, il core non renderizza alla fin fine nulla, direttamente. Tutto il compito è lasciato agli shader.
Gli shader sono probabilmente la parte più complessa ed articolata di cassata, ma grazie agli elementi presenti su core e plugin il compito di scrivere uno shader è parecchio semplificato.

Gli shader sono dei programmi, spesso di piccola dimensione. Una cosa che li contraddistingue è quella di rimanere in esecuzione. Altri shader, o cassata stesso, possono eseguire determinate funzioni sullo shader.
Ogni shader ha variabili con varie visibilità: a livello di funzione, thread, processo, istanza. Potrebbero introdursi altre visibilità a seconda dell'esigenza. Comunque è anche importante evitare di scrivere sulle variabili con visibilità d'istanza perché molto lente da sincronizzare in rete. Ancora non ho bene in mente come superare il problema di prestazioni in proposito, ma fortunatamente è abbastanza raro utilizzarle.
La parallelizzazione è gestita a livello di shader, ma a livello molto alto. Ad esempio potrebbero esserci delle funzioni per lanciare ed integrare raggi, ed i raggi lanciati vengono automaticamente parallelizzati.
Va detto che scrivere uno shader risulta in genere semplicissimo, perché il core ed eventualmente i plugin offrono funzionalità di livello molto alto. Ad esempio tutte le strutture di accelerazione geometrica non sono fatte tramite shader.

L'usare shader può sembrare un notevole rallentamento. Tuttavia non è così. Intanto gli shader vengono compilati da un JIT, inoltre vengono ottimizzati per la CPU sul quale gireranno (sarebbe itneressante riuscire anche a fare qualcosa su GPU, ma non ho ancora studiato bene l'argomento). Inoltre è possibile compiere ottimizzazioni per scena. Effettivamente gli shader possono essere più lenti che senza, ma potrebbero persino essere più veloci.

Gli shader sono studiati per risultare modulari, e potersi combinare in vari modi. Ad esempio si potrebbero avere shader che rappresentano una geometria, una camera, un metodo per renderizzare la scena e tanto altro.
Rimane quindi importante, come accennato prima, fare in modo che gli shader possano lavorare correttamente assieme. È importantissimo quindi, in seguito, scrivere degli standard che non è obbligatorio seguire, ma che è caldamente raccomandato per poter collaborare con gli altri shader.
Faccio notare che facendo in questo modo è molto facile ridefinire metodi di rendering e quant'altro, e difatti cassata, sebbene sia orientato all'unbias non è unbias. Non solo almeno. :)

Vanno ancora descritti gli algoritmi utilizzati nel core, cosa che verrà fatta più avanti nel post.

Ora descrivo le scene. Come ho detto prima una scena è davvero semplice. Per capire com'è fatta però bisogna spiegare qualche concetto.
Ogni scena contiene delle sottoscene. Queste sottoscene sono semplicemente delle parti che possono venire calcolate senza condividere la stessa memoria. Possono fornire risultati indipendenti, ma anche che possono servire ad altre sottoscene. In questo proposito esistono due modi di comunicare tra le sottoscene. Uno è quello di produrre tutto il risultato, e solo dopo passarlo alle sottoscene a cui serve. L'altro è quello di produrre dati mano a mano quando servono e passarli tra le varie sottoscene.
Un esempio di utilizzo di questo secondo metodo è ad esempio utilizzare due scene, una che renderizza una stanza, l'altra che renderizza un'altra stanza con un televisore che mostra la prima stanza, il tutto rimanendo unbias. Tralaltro le scene possono avere dipendenze circolari, così ad esempio, nella scena di prima, entrambe le stanze hanno un televisore che mostra l'altra stanza, od ad esempio se stessa.

Le scene inoltre definiscono i parametri passati ad uno shader. Possono venire passati anche altri shader, come parametri, il che li rende tutti legati tra di loro.
Un altro modo di passare parametri agli shader è tramite i pass. Un pass non è altro che un profilo che dice quali scene renderizzare, i filtri da utilizzare per ritornare i vari dati (quindi qua per esempio si sceglie che tipo di immagine utilizzare) ed eventuali variabili da passare ad i vari shader per personalizzarli.

Detto questo passiamo a vedere quali sono gli algoritmi utilizzati all'interno di cassata. Va detto che questa lista è davvero incompleta, e moltissimi algoritmi sicuramente verranno aggiunti, specialmente per prestazioni migliori.

Innanzitutto cassata nasce come unbias, e rimane tale quando possibile, a meno di non fornire la scelta. Ovvero quando è possibile fornire una tecnica unbias per risolvere un problema viene fornita, e poi, se presente, si può pensare di fornire anche tecniche bias.
Detto questo non può mancare la presenza di integratori montecarlo. Vista la somiglianza tra integratori montecarlo e quasi-montecarlo si potranno usare entrambi. Questi sono onnipresenti. Sebbene inoltre non sia indispensabile, e si possano usare anche rendering basati su patch, è ovviamente fondamentale fornire tecniche di ray tracing. In particolare probabilmente le tecniche preferite saranno quelle unbias bidirezionali, possibilmente utilizzando tutte le path intermedie e riutilizzando tutte le path che si possono riutilizzare. Un'idea potrebbe essere MLT, ma sono fiducioso che si possa fare altrettanto con tecniche meno determinate da un singolo percorso importante della luce.

Un'altra tecnica importante è l'uso dell'aritmetica affine. Questa tecnica è utilizzata specialmente per il displacement, ma viene utilizzata anche per molti altri compiti. Permettendo di trovare gli errori massimi compiuti nei calcoli è facile ridurli fino ad una soglia scelta dall'utente. Ad esempio per il displacement si potrebbe utilizzare inizialmente un intervallo infinito, trovando il massimo displacement possibile. Quindi lo si dividerebbe sempre di più in modo da ottenere una precisione sempre maggiore di dove andrà un dato punto. Infine, quando si sa che il punto sarebbe sicuramente all'interno di una sfera estremamente piccola, lo si prende per valido. È una tecnica estremamente precisa e molto veloce, certo però non veloce come modificare le geometrie, ma comunque risulta una tecnica che non da errori con geometrie poco fitte o problemi simili.

Un problema che mi ha assillato per molto tempo era come mettere nella zuppa delle tecniche pure qualcosa che permetta di utilizzare materiali non lineari. Questo perché apparte questo particolare non c'è scena che cassata, secondo i termini detti prima, non può renderizzare con la qualità voluta. La soluzione l'ho trovata nel rendering basato su patch. Si fanno diversi rendering uno di seguito all'altro convergendo alla soluzione. Non ho una dimostrazione formale (che tralaltro sarebbe anche complicata), ma sono arrivato alla conclusione che facendo le cose in un certo modo (che non spiego qua perché ancora si stanno affinando) sia possibile convergere sempre ad una soluzione se questa esiste, e guidare, pure, ad una specifica soluzione, qual'ora ne esistano diverse. Questo però richiede un buon modo per produrre patch delle geometrie. Questo modo non è complicato, visto che si possono riutilizzare, ad esempio, le coordinate uv, e tralaltro può essere riutilizzata questa parte di cassata per fare ogni tipo di bake su texture.

Un'altra cosa che mi piacerebbe includere in cassata, e che già in gran parte si più fare con gli strumenti di cui ho parlato nel post è l'inserimento di simulatori, fisici e non, ed in particolare tecniche di auralizzazione. Non sono cose assurde da inserire ma richiedono un bel po' di lavoro sugli shader, e quindi non sono minimamente una priorità, tuttavia sono previsti.

Ovviamente tutte le tecniche sono condite ovunque con caching, e dove possibile vorrei aggiungere anche memoizzazione.

Un'altra cosa sulla quale lavorare (che volendo si può fare anche con quello che già si ha, ma va migliorata) è poter ricominciare il rendering da un punto dove si era arrivati, o progressivamente migliorare un rendering, od ancora unire più pass senza troppi interventi da parte del front end.
Sarebbe carina anche una certa forma d'interattività da parte del front end, in modo che possa cambiare un certo numero di cose in corso.

Detto questo avrò tralasciato molte cose, ma dovrei aver dato un quadro più o meno possibile di cos'è cassata e come funziona. Se qualcuno avrà voglia di leggere tutto e commenterà sarà il benvenuto, specialmente se ha qualche idea in mente. Faccio presente comunque che cassata è tanto flessibile da poterlo davvero considerare il più flessibile tra i renderer che conosco, il suo difetto principale sarà probabilmente prestazionale, ma architetturalmente è studiato per potersi ottimizzare enormemente, così nel tempo potrà diventare anche più veloce di molti renderer che mirano alla velocità più che alla flessibilità. Questo certamente richiederà tempo.

Nessun commento: