Esplora E-book
Categorie
Esplora Audiolibri
Categorie
Esplora Riviste
Categorie
Esplora Documenti
Categorie
Dario Oliveri
Nei motori grafici moderni sono impiegate tecniche di altissimo livello per garantire la
massima resa visiva: questo approccio top-down genera in molti casi codice difficile da
mantenere che rende complicato aggiungere nuove funzionalità o impegnativa la creazione di
applicazioni apparentemente semplici. L’idea alla base di questo lavoro è proporre un livello
di astrazione che si pone a metà strada tra i moderni motori 3D e la programmazione diretta
tramite OpenGL con lo scopo di rendere più semplice la creazione di applicazioni 3D
rimuovendo il boilerplate code tipico delle applicazioni grafiche senza tuttavia perdere il
controllo di cosa succede dietro le quinte.
Per quanto riguarda lo sviluppo ho scelto di usare tecniche di programmazione moderne quali
il Test Driven Development, la Dependency Injection (tramite l’utilizzo di un framework
apposito) e alcune tecniche di sviluppo Agile.
Indice:
1. Prefazione ……………………………………………………………….. pag. 1
2. Indice ……………………………………………………………………. pag. 2
3. Tool/Servizi utilizzati ………………………………………………….... pag. 3
4. Glossario ………………………………………………………………… pag. 5
5. Cos’è OpenGL ………………………………………………………….. pag. 6
6. Scelta della versione API di OpenGL …………………………...……… pag. 8
7. Motivazione per la realizzazione di questa tesi e del software collegato.. pag. 9
8. Scelte architetturali ……………………………………………………… pag. 10
9. Overview dei moduli …………………………………………………… pag. 10
10. Dependency Injection . …………………………………………………. pag. 14
11. Rendering Asincrono ……………………………………………………. pag. 16
12. Creazione oggetti in modo Asincrono ………......………………………. pag. 18
13. Cancellazione oggetti in modo Asincrono ….......………………………. pag. 20
14. Panoramica sull'approccio Asincrono ….......………........………………. pag. 22
15. Compile / Test / Deploy con un solo click (o quasi) ……………………. pag. 23
16. Minimizzazione dei cambi di stato OpenGL (approccio Greedy).........…. pag. 24
17. Esempio di minimizzazione stati OpenGL . ….....................................…. pag. 27
18. Il problema della convezione sulle coordinate …...……………………… pag. 28
19. Definizione di convenzione …………………………….……………….. pag. 30
20. Conversione di vettori e quaternioni ………………….……………….. pag. 31
21. Class Diagram …………………………………………………………… pag. 16
22. Generalizzare il sistema di coordinate …………………………………… pag. 17
23. Stato attuale …………………………………………………………….... pag. 34
24. Riferimenti ………………………………………………………………. pag. 35
25. Note aggiuntive ………………………………………………………….. pag. 36
Tool/Servizi utilizzati
● OpenGL
● C++ (11/14)
● CMake
● GCC Toolchain
● Github
● GIT
● Travis.ci
● OpenGL load gen
● Catch framework
● Github: fornisce un client grafico per GIT ed inoltre permette un approccio “Social”
al coding, permettendo agli utenti di interagire più facilmente e ottenere feedback.
● OpenGL load gen: Script Lua che crea un file utilizzabile da C/C++ per accedere alle
funzioni di un versione specifica di OpenGL. A differenza di GLEW i file generati
sono molto più piccoli (si scende da diversi MB a pochi KB).
● Catch framework: Framework per lo unitTest. I test case possono essere nidificati
evitando la verbosità dei frameworks come JUnit. L’approccio scelto da questo
framework permette di evitare di avere parti di “Setup” oppure “Teardown”.
Glossario
Ecco una lista di termini di uso comune in programmazione grafica, il cui significato
potrebbe essere sconosciuto al di fuori di tale campo che pertanto riporto in questo lavoro:
OpenGL è quindi prima di tutto uno standard che cerca di mettere d’accordo varie industrie.
E’ possibile darne un’implementazione software (escludendo di fatto alcuni casi di proprietà
intellettuale su alcune funzionalità, si tratta di uno standard aperto e implementabile
liberamente), ma è pensato principalmente per essere implementato dalle GPU.
Per renderizzare oggetti a schermo, OpenGL utilizza una loro rappresentazione discreta,
ovvero una lista di posizioni e una lista di triangoli costruiti usando come vertici queste
posizioni.
E per il Web/Mobile:
- OpenGL ES
- OpenGL ES 2 (GPU programming su mobile)
- OpenGL ES 3
Per quanto riguarda questa tesi ho scelto di investigare OpenGL 3.3, un suo sottoinsieme
di funzionalità compatibili con OpenGL 3.0 (e quindi anche con la maggior parte delle
schede grafiche 2.1 con driver aggiornati) e OpenGL ES 2, in modo da semplificare il
porting verso il maggior numero possibile di piattaforme senza dover scendere a
compromessi in termini di performance o di mantenibilità (o di feature).
Da notare che in realtà questa scelta include inevitabilmente al suo interno dei piccoli
compromessi:
● OpenGL ES 2 non supporta i 2D texture array; quindi questa funzionalità non è stata
inclusa nel motore 3D che sto sviluppando (potrà essere aggiunta in futuro per il
desktop).
● OpenGL ES 2 non utilizza i Sampler Object; questo determina un utilizzo maggiore di
memoria video per la parte riguardante OpenGL ES 2, qualora una stessa texture
dovesse essere usata ripetutamente con opzioni diverse.
● Vengono utilizzate alcune estensioni ubique, che ufficialmente non fanno parte dello
standard OpenGL, ma sono ampiamente adottate da quasi tutti i produttori di schede
grafiche ( Vedi OpenGL wiki: Ubiquitous Extensions).
● OpenGL ES 2 non supporta il filtraggio anisotropico delle texture, quindi per la
versione mobile il fallback sarà il filtraggio “trilineare” (bilineare su immagini 2D e
lineare tra 2 diversi livelli di mipmap).
Motivazione per la realizzazione di questo lavoro
Rifattorizzare il codice di applicazioni grafiche può essere un compito arduo (Ahlgren, 2004);
la causa è classica: da un lato abbiamo un’applicazione top-down che evolve nel tempo e che
richiede la possibilità di eseguire tante piccole operazioni per gestire meglio le richieste (alta
granularità); dall’altro lato troviamo OpenGL, che per ragioni implementative predilige una
bassa granularità delle operazioni grafiche (questo limite è difficile da stimare, ma in generale
vale il principio empirico “meno operazioni = più veloce”).
● Rendering Asincrono
● Compile / Test / Deploy con un solo click (o quasi)
● Minimizzazione dei cambi di stato OpenGL
● Cooperazione con altre applicazioni
● Keep it simple (almeno per gli utenti finali).
● Non appoggiarsi a strumenti proprietari.
Ho individuato un ulteriore punto (selezionato fra alcuni altri), che a mio parere puo’
essere molto importante, anche se esula dalle attuali responsabilità del motore 3D, e che
puo’ essere spunto di ulteriori approfondimenti:
● L'import di mesh statiche /mesh animate/ scene graph deve essere indipendente
dalla scelta locale di una convenzione sull'utilizzo delle coordinate
Overview dei moduli
La libreria che ho sviluppato è suddivisa in moduli, ognuno con precise responsabilità.
ShaderManager:
E' la classe che si occupa di creare oggetti di tipo Shader. La creazione di un oggetto di tipo
Shader passa attraverso due fasi.
• Creazione dell'oggetto Shader attraverso una delle funzioni apposite (a seconda della
funzione chiamata è possibile aggiungere vari stadi opzionali come il Geometry shader
oppure il Tessellator shader). Sono richiesti almeno 2 stadi ovvero il Vertex Shader e il
Fragment Shader, gli altri stadi sono opzionali.
Per poter abbinare una texture all'interno di uno shader è necessario fare riferimento alla sua
posizione logica (un numero che scegliamo noi, che in gergo è chiamato “texture unit”, ed ha
un corrispettivo hardware). L'abbinamento viene fatto creando un TextureSlot, il cui scopo è
quello di associare una Texture, una strategia di campionamento Sampler, e la relativa
posizione logica.
Render Slot
Il RenderSlot è il punto logico in cui abbiniamo uno Shader a delle immagini (TextureSlot),
in modo da poterle utilizzare per il renderening. Esso rappresenta una singola operazione
eseguita dalla scheda grafica, ovvero una Draw Call. Come si può vedere dall'immagine
seguente, è necessario assegnare i TextureSlot e uno Shader al RenderSlot (usando il move
assignment “std::move”), poiché generalmente sono oggetti che vengono creati per un
singolo utilizzo, e vogliamo liberarci il prima possibile della loro “proprietà”. L'unico
oggetto del quale vogliamo mantenere la proprietà è il RenderSlot.
RenderPass
Il RenderPass è l'ultimo tassello per poter completare un'operazione di rendering; esso è
costituito da una collezione di RenderSlot che hanno in comune alcune opzioni globali
(DepthMask, StencilTest, PolygonMode, RenderTarget).
Possiamo vedere che, a parte le opzioni (membri del RenderPass), l'unico vero scopo di un
RenderPass è poter mantenere al suo interno una collezione di RenderSlot, che
condivideranno quindi le stesse opzioni.
RenderQueue
La lista di RenderPass rappresenta le operazioni logiche che l'utente vuole eseguire.
Ho definito “compileStates” il metodo che esegue la trasformazione di una lista di
RenderPass in una sequenza di comandi OpenGL.
La RenderQueue è la classe che fa da contenitore temporaneo ai comandi OpenGL così
generati.
L'algoritmo usato dal metodo “compileStates” legge la lista di RenderPass (e dei
RenderSlot in essi contenuti) e genera una sequenza di comandi OpenGL ottimizzata, che
sarà valida per un certo periodo di tempo (fino a quando l'utente non fornirà una nuova lista
di RenderPass).
Entità Managed
Gli oggetti creati dai manager sono tutti smart pointer di tipo ManagedResource< T>, dove T
è un tipo pure virtual che eredita da ManagedEntity. (file badeSource/BadeForwards.hpp)
La peculiarità di questi puntatori è che, una volta distrutti, invece di cancellare l'oggetto a cui
puntano vanno invece a decrementarne il ReferenceCount. Sarà poi il manager di tale oggetto
a decidere se cancellarlo o meno, in base al ReferenceCount. L'utilizzo di questi smart pointer
è necessario poiché gli oggetti devono essere mantenuti in vita non solo quando sono
utilizzati dall'utente, ma anche quando vengono utilizzati dal codice asincrono per operazioni
di rendering.
Dependency Injection
Per poter collegare i vari componenti della libreria tra di loro ho deciso di usare un
framework di Dependency Injection. Si tratta di una Factory di oggetti che permette di
mantenere i moduli della libreria disaccoppiati fra di loro, in modo che se un modulo deve
comunicare con un altro, questo viene inettato (per costruttore) automaticamente in maniera
lazy.
Il vantaggio di utilizzare un framework simile è che è possibile evitare di usare anti-pattern
quali il Service Locator o il Singleton, mentre viene reso più facile il testing delle varie
componenti dell'applicazione (in quanto sono separate tra di loro).
Come framework di Dependency Injection ho deciso di utilzzarne uno scritto da me in
passato. Mi sono ispirato a framework noti come Ninject e Svelto (scritti in C#) e ho
riadattato le funzionalità in modo che funzionassero in C++.
Il risultato è una libreria leggera che presenta le caratteristiche principali per poter effettuare
binding di tipi ad interfacce e costruzione di Object Graph contestualizzate. Siccome il C++
non ha la possibilità di effettuare “reflection” a runtime, ho dovuto utilizzare la meta-
programmazione con i template. Ecco come viene assemblata la libreria grafica:
Binding delle componenti del Main Thread; si può notare come ogni componente abbia in
realtà un numero esiguo di dipendenze: tutto il codice “colla” che tiene insieme la libreria
grafica conta una ventina di righe.
Siccome la libreria utilizza 2 thread, anche la dependency injection viene effettuata in 2 fasi
distinte: è equivalente ad avere 2 moduli Ninject collegati da un solo punto di comunicazione
(molto conveniente per tenere separati l'Object Graph del MainThread e l'Object Graph del
RenderThread).
Gli oggetti utilizzati dai due thread vivono in “context” (zone di memoria) separate. L'unico
oggetto accessibile ad entrambe è il Queue Executor che infatti presenta al suo interno
primitive di sincronizzazione.
Come è possibile notare, le “interfacce” (classi pure virtual) che nascondono la complessità
della libreria all'utente finale sono solo 4 (RenderQueue, ImageManager, TextureManager,
ShaderManager).
Le istanze di tali classi sono ottenibili direttamente tramite l'oggetto “mainContext”, ma è
sconsigliabile, in quanto per creare un'istanza dell'Object Graph è sufficiente crearne la
radice, le cui dipendenze (risolte in modo lazy), porteranno all'iniezione di tutte le istanze
necessarie per il funzionamento.
Le altre interfacce servono solamente a nascondere il tipo concreto degli oggetti creati dai
manager (Texture, Image, Shader, ShaderOptions, Sampler).
Sono presenti inoltre un certo numero di classi di utilità che non richiedono la presenza di
Manager (RenderSlot, RenderPass, MiniArray, MaxiArray).
Rendering Asincrono
Vediamo prima di tutto come sarebbe la sequenza di chiamate OpenGL nel caso in cui
utilizzassimo un approccio sincrono:
Caricamento di una texture: il codice client è bloccato fintanto che ci sono chiamate
OpenGL da eseguire.
Il modello OpenGL è pensato per essere asincrono, ovvero le chiamate OpenGL dietro le
quinte generano dei comandi che vengono letti dalla GPU in modo asincrono.
Tuttavia questo è vero solo in teoria, perchè in pratica alcuni vincoli imposti dall'API
OpenGL richiedono comunque un certo overhead di validazione dei dati, che in pratica
blocca ugualmente il codice client.
Tali inconvenienti possono essere alleviati in versioni di OpenGL più recenti, che però
richiedono GPU recenti con driver aggiornati.
Problema 2: Il rendering può essere bloccato da operazioni del client
Il client può interagire con la mia libreria utilizzando solamente le interfacce pubbliche.
Ad esempio, per la creazione di una Texture, è necessario utilizzare il TextureManager.
Esso infatti predispone una zona di memoria ( ManagedEntity) che verrà utilizzata per la
comunicazione con la scheda grafica e al tempo stesso genera un Callback che verrà aggiunto
in una CommandQueue.
Ci sono vari compiti di comunicazione che vanno assolti dai manager per garantire la
consistenza durante il rendering:
Per garantire queste 3 condizioni è sufficiente aspettare che siano stati eseguiti almeno una
volta tutti i comandi precedentemente inviati.
Infatti se è stato chiamato compileStates, che effettua uno scambio dei buffer di comandi,
sappiamo che abbiamo anche già effettuato una sincronizzazione (memory barrier), e quindi i
valori scritti dal Render Thread sono visibili al Main Thread.
La cancellazione di oggetti è più semplice dal punto di vista implementativo. Siccome tutti
gli oggetti della mia libreria sono Reference Counted, i Manager possono decidere di
cancellare tali oggetti semplicemente controllando il Reference Count.
Una volta che il manager trova un oggetto che è possibile cancellare, effettua 2 operazioni:
In questo esempio vediamo una funzione che riceve in input un oggetto RenderSlot
completo di dati quali una Mesh e uno Shader. A tale oggetto viene aggiunta una Texture e il
relativo Sampler (necessari per effettuare il rendering). L'utilizzo dei manager comporta la
creazione di comandi asincroni che vengono messi da parte in un buffer. Chiamando la
funzione GPUResourceUpdate viene avviata l'esecuzione in background dei comandi
precedentemente messi da parte. Questo permette di eseguire in parallelo altri task mentre i
driver della scheda video saranno liberi di preparare il rendering effettuando le validazioni
necessarie.
L'invio dei comandi di rendering viene invece effettuato tramite la funzione compileStates,
che prende come input una lista di RenderPass e la trasforma in una lista di comandi da
eseguire. La compilazione degli stati OpenGL viene eseguita dal MainThread, mentre i
comandi generati da tale compilazione sono immediatamente accodati per l'esecuzione nel
Render Thread. In realtà compileStates effettua, quando opportuno, anche la chiamata a
GPUResourceUpdate; tuttavia avere due funzioni permette di disaccoppiare la loro
funzionalità, se necessario.
Panoramica sull'approccio asincrono.
Come possiamo vedere dalla seguente immagine, l'approccio asincrono presenta un vantaggio
in termini prestazionali:
Può quindi capitare che l'overhead totale sia minore rispetto all'overhead con un singolo
thread (ad esempio quando uno dei due thread esegue un minor numero di loop rispetto
all'altro).
Compile / Test / Deploy con un solo click (o quasi)
WINDOWS
LINUX/OSX
Uno dei possibili principali bottleneck nelle applicazioni grafiche è l’uso eccessivo di
chiamate di funzioni OpenGL. Nella totalità dei motori 3D, viene utilizzata una qualche
strategia per ridurre il numero di chiamate OpenGL, ovvero il numero di cambi di stato.
Che cos’è un cambio di stato OpenGL? OpenGL è strutturato come una macchina a stati;
ogni operazione effettua un cambio di stato che può essere più o meno costoso.
Ad esempio, per poter manipolare un oggetto bisogna effettuare un “Bind” che attiva
quell’oggetto:
Se l’oggetto è già stato “attivato” è ridondante attivarlo 2 volte (ovviamente in questo caso
semplice è ragionevole assumere che i Driver non eseguano lavoro aggiuntivo).
Tuttavia, possono verificarsi casi come il seguente, dove dal punto di vista del driver la
seconda attivazione non è piu’ ridondante:
Quindi, per non effettuare una eccessiva quantità di “Bind” superflui, che causerebbero un
certo dispendio computazionale da parte dei driver, l’ideale sarebbe poter effettuare le
operazioni nel seguente ordine, che eviterebbe ogni ridondanza:
L'approccio standard per ridurre il costo di queste fasi è cercare di effettuare un ordinamento
parziale delle Drawcall, in modo che si minimizzi prima il cambio di stati del Framebuffer, e
poi i bind dei programmi GPU.
Approccio standard:
Sort(framebufferList);
foreach f:Framebuffer in frameBufferList
if(f != previousFramebuffer)
BindFramebuffer( f );
Sort(f.shaders);
foreach s:Shader in f.shaders
if( s != previousShader)
BindProgram( s);
L'approccio standard è efficace nella maggior parte dei casi e riduce notevolmente l'overhead
dei driver. Tuttavia ci sono situazioni in cui non è applicabile, e che richiedono o algoritmi di
ordinamento più complessi (basti pensare alle recenti tecniche di rendering deferred o di
virtual texturing) oppure approcci differenti (molte volte la soluzione più semplice è
utilizzare direttamente codice OpenGL, oppure fare affidamento a tool proprietari).
Si tratta pertanto di un approccio di carattere empirico, che pur essendo stato efficacemente
utilizzato in altri motori 3D (OpenSceneGraph ad esempio), mi è sembrato inadeguato ai fini
del mio lavoro.
Approccio Greedy
Nel mio lavoro ho cercato di applicare invece una soluzione più semplice: l’ordine di
rendering non viene modificato a priori, ma sono invece eliminati TUTTI i cambi di stato
che possono ritenersi superflui dato un ordine prefissato. Questo approccio è anche più
consistente con la filosofia del mio framework: fornire maggiore controllo ed evitare
“stravolgimenti” dietro le quinte.
.
Quindi il codice utente è libero di effettuare ordinamenti laddove lo ritenga necessario,
mentre il codice della mia libreria si occupa di effettuare uno sfoltimento di tutte le
chiamate OpenGL superflue. Quindi ordinando i RenderSlot in base allo Shader utilizzato
la libreria eliminerà le chiamate BindProgram superflue.
Approccio Greedy:
if(g.Shader != previousShader)
BindProgram( g.Shader)
if(g.DepthTest != previousDepthTest)
DepthFunc( g.DepthTest)
...
end
Per ogni parametro OpenGL esposto all'utente finale l'algoritmo greedy implementa una
strategia di sfoltimento diversa.
Ho cercato di applicare l'approccio Greedy in modo sistematico, anziché in modo parziale
(come effettuato da molte altre applicazioni esistenti).
Il problema principale è che, applicando lo sfoltimento degli stati ad ogni frame, si rischia di
avere un overhead maggiore delle stesse istruzioni OpenGL sfoltite.
Nel mio caso invece questo non è un problema, poiché lo sfoltimento degli stati viene
eseguito solo quando viene chiamato “compileStates”.
Bade3D\badeSource\gl3\GL3RenderQueue.cpp
Alcune strategie di minimizzazione stati
La cancellazione dei buffer (Color, Depth, Stencil) viene effettuata solo se necessaria:
• Se abbiamo effettuato delle operazioni di rendering quando uno dei buffer era attivo,
sappiamo che il buffer è in uno stato “dirty” e può quindi essere cancellato se
richiesto.
• Se invece il buffer è in uno stato “clean”, non viene cancellato e viene risparmiata una
chiamata a glClear.
• I Buffer Depth & Stencil sono memorizzati insieme (tipicamente le schede grafiche
utilizzano, per motivi di efficienza, una struttura dati chiamata Hierarchical Zbuffer).
Per questo motivo, se possibile, ogni richiesta di cancellazione di uno di questi due
buffer è trasformata in modo da cancellare entrambi (N. Thibieroz, 2012 )
• Per poter cancellare uno di questi buffer è necessario che sia attiva la rispettiva
WriteMask. Essa viene quindi attivata solo se la cancellazione è effettivamente
eseguita. Di default le Write Mask sono tutte attive, ma per certe operazioni è utile
disattivarle.
• Le impostazioni delle WriteMask sono anch'esse cambiate solo se necessario (se lo
stato di una WriteMask non cambia, non è necessario riattivarlo/disattivarlo).
Use case:
Supponiamo che l'utente voglia visualizzare dei contenuti opachi in 3D. Supponiamo inoltre
che voglia visualizzare anche un'interfaccia grafica in sovrimpressione. Sarà necessario
dividere le operazioni di rendering in 2 RenderPass. Il primo RenderPass dovrà cancellare sia
il Depth Buffer che il Color Buffer, dopodiché dovremmo cancellare solo il Depth Buffer per
evitare che l'interfaccia grafica abbia intersezioni con i contenuti 3D:
Una volta compilati gli stati, le chiamate OpenGL effettuate sono le seguenti:
Che corrispondono alla sequenza ottimale di operazioni ( si tratta di un caso semplice, ma nei
casi complessi viene sempre mantenuto il numero minimo di operazioni).
Algoritmo per convertire convenzioni locali sul sistema di coordinate
Il problema pratico:
Motivazione: Esistono almeno 5 diverse convenzioni in utilizzo per quanto riguarda i sistemi
di coordinate all’interno di videogiochi e motori 3D (proprietari, oppure open source). Le
convenzioni possibili sono 48 ( 6 scelte per il primo asse, 4 per il secondo, 2 per il terzo),
teoricamente quindi potrebbe essere necessario avere 2209 (ovvero 47 2 ) routine di
conversione da un sistema di coordinate ad un altro.
Analizzata la problematica, mi sono reso conto che in realtà queste conversioni sono
generalizzabili in sole due operazioni, e quindi ho realizzato il mio software basandomi
su di esse:
Sebbene in teoria esista una discreta varietà di oggetti da convertire (oltre ai vettori con 3
coordinate troviamo matrici 4x4, quaternioni, bounding box, ecc.), la mia opinione è che sia
necessario convertire soltanto 2 tipi di oggetti da un sistema di coordinate ad un altro:
1. Vettori a 3 coordinate
2. Quaternioni.
La ragione è che in realtà gli oggetti più complessi (bounding box, frustum view, matrici 4x4)
sono sempre creati a partire da oggetti più semplici (in particolare solo i due oggetti citati).
Delle 5 convenzioni che ho trovato essere in utilizzo, almeno 3 sono largamente diffuse.
Ogre3D / Blender: Z = Su, X = Sinistra, Destrorso
Unity3D: Y = Su, X = Destra, Sinistrorso
Irrlicht: Y= Su, X = Destra, Destrorso
Definizione di convenzione:
Una convenzione descrive come dobbiamo interpretare i dati 3D, ovvero come ci aspettiamo
che vengano visualizzati sullo schermo degli oggetti 3D in modo che l'utente che vede
visualizzato l'oggetto possa vederlo “importato correttamente”.
Una convenzione prevede di associare ad ognuna delle direzioni logiche {Su, Destra,
Davanti} una delle etichette {X, Y, Z } ed un verso per ciascuna delle etichette {+,-}.
– la direzione verso cui è rivolto il viso, ci indica l'asse locale e il verso per la direzione
logica “Davanti”.
– La direzione verso cui punta la spalla destra ci indica l'asse locale e il verso per la
direzione logica “Destra” (destra e sinistra le riconosciamo da dettagli asimmetrici, ad
esempio la presenza di un anello ad una mano).
– La direzione in cui punta la sommità della testa ci indica l'asse e il verso per la
direzione “Su”.
(Su) → (Y, + )
(Destra) → (X, +)
(Davanti) → (Z, -)
x = 2, y = 3, z = 0.5
x = -0.5, y = -2, z = 3
L'algoritmo che ho sviluppato esegue la conversione dei dati del modello 3D in fase di
“caricamento”, in modo che rispettino la nuova convenzione.
Da notare che mi riferisco volutamente ai dati come etichettati poiché questo algoritmo è
totalmente agnostico al fatto che il modello 3D abbia uno spazio locale o sia realmente 3D. E'
vero che è come se “avessimo applicato” una trasformazione allo spazio locale, ma possiamo
dimenticarci in questo modo della complessità di avere uno spazio a N-Dimensioni poiché ci
limitiamo a lavorare su dei dati e su delle etichette (Di fatto possiamo estendere facilmente
l'algoritmo a quante dimensioni vogliamo).
Ai fini di questo lavoro considero i quaternioni come vettori a cui sia stato aggiunto un
ulteriore elemento etichettato “w”.
- Se n è dispari
q'.w ← - q.w
altrimenti
q'.w ← q.w
Contratto:
A mio avviso non è necessario, infatti oggetti più complessi sono costruiti a partire da oggetti
più semplici. Ad esempio per costruire una matrice “guarda al punto”, dobbiamo chiamare
una funzione simile a questa
Ecco un esempio di modello importato in modo non corretto da Blender a Unity3D (in questo
caso ho utilizzato un implementazione in C#):
Lo stesso modello dopo aver convertito i dati al suo interno ( vettori posizione, vettori
normale, vettori scala, quaternioni) appare correttamente visualizzato:
Per quanto riguarda la mia libreria non è presente tutt'ora un mesh importer (cosa che farò in
futuro sotto forma di un modulo separato poiché è una responsabilità aggiuntiva) tutta via è
presente già il codice per poter dichiarare la convenzione da utilizzare e una routine per
effettuare conversione di vettori e quaternioni a 3 dimensioni.
Lavori futuri
Non esiste un formato di file non proprietario in grado di trattare correttamente diverse
convenzioni (di fatto basterebbe dichiarare una convenzione all'inizio del file ed effettuare la
conversione dei dati sensibili in fase di caricamento, cosa che non viene fatta). Ho già
abbozzato una specifica per un formato di file libero e ho pianificato la scrittura di un SDK
per importare mesh animate in C++/C# ed esportare mesh da Unity3D e Blender.
Per quanto riguarda il motore 3D ci sono alcuni punti non completati che necessitano di
approfondimento, in particolare gli Uniform Buffer necessiterebbero di essere aggiornati ad
ogni frame, qual'è la soluzione più efficiente? Un interpolazione lato RenderThread oppure
un maggior numero di sincronizzazioni con il MainThread? Oppure dare la possibilità di fare
entrambe le cose per permettere un tradeoff Efficienza vs flessibilità?
Repository “ufficiale”:
https://github.com/Darelbi/Bade3D
Framework di supporto:
https://github.com/Darelbi/Infectorpp2
● Completo
Riferimenti (ordinati alfabeticamente per nome)
E. Persson(2007) - Depth in Depth, Draw the skybox last and the Gun first: developer.amd.com
Gamma, Helm, Johnson, Vlissides (2002) - Design Patterns: Elementi per il riuso di software a oggetti,
Prima edizione italiana
H. Ahlgren (2004) - Graph Visualization with OpenGL, 5.1.1 Refactoring and Integration.
K. Akeley, M. Segal (2010) - The OpenGL Graphics System, a specification (Version 3.3 core
profile ,11 March 2010): www.opengl.org
L. Hrabcak, A. Masserann (2012) - Opengl Insights, sample chapter 28: Asynchronous Buffer
Transfers
M. Callow, G. Colling, J. Ström (2013) - The KTX File Format Specification (approved final
specification) : www.opengl.org
N. Thibieroz (2012) - ATIClever Shader Tricks, Understanding Z Fast Clears and Compression:
developer.amd.com
Certi riferimenti non sono menzionati all’interno della tesi perchè la loro consultazione è stata usata per
la stesura di codice, in particolare ho usato i seguenti riferimenti: