Sei sulla pagina 1di 36

Libreria Grafica in OpenGL

Dario Oliveri

Relatore: Enrico Puppo


Co-relatore: Nikolas De Giorgis

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

Motivazione & scelta dei Tool/Servizi

● OpenGL: E’ uno standard aperto e portabile su (quasi) ogni piattaforma.

● C++ ( 11 ): E’ l’unico linguaggio che mi ha permesso di impostare una toolchain di


deploy e test in modo cross-platform senza dover passare per configurazioni e
installazioni complicate e senza utilizzare tool proprietari.

● CMake: Se utilizzato correttamente permette di semplificare il codice rimuovendo


molti costrutti di compilazione condizionale (macro, ifdef, etc.) rendendo più
semplice scrivere codice cross-platform. Cmake permette di scrivere script di
compilazione indipendenti dal sistema operativo e compilatore.

● GCC: Permette di avere un compilatore semplicemente scaricando da qualche parte


nel computer un archivio ZIP (niente installazione, niente modifiche alle variabili di
ambiente, si può mettere su chiavetta e spostare tutto l’ambiente di lavoro con un
copia - incolla)

● 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.

● GIT: sistema di versionamento che mette in primo piano lo sviluppo ramificato


(branching) la complessità è nascosta dai client grafici. (Github for Windows,
sourceTree, GIT Tortoise)

● Travis.ci: E’ un servizio web di integrazione continua, ovvero ad ogni commit


vengono eseguiti degli script shell personalizzati (forniti dall’utente) che permettono
di compilare il codice (eventualmente farne anche il deploy) su vari sistemi operativi
(Linux/ OSX) e di eseguire i test automatici utilizzando macchine virtuali come
ambienti di sviluppo.
● Appveyor: Come Travis è un servizio online di integrazione del codice continua, ma
utilizza macchine virtuali su cui è installato windows permettendo di effettuare test
automatici anche per windows.

● 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:

1. Rendering: visualizzazione di oggetti 3D su uno schermo bidimensionale


2. Engine 3D: o motore 3D, uno strato software che si interpone tra le applicazioni
grafiche real-time (ad esempio i videogiochi) e le API di rendering sottostanti (ad
esempio OpenGL)
3. GPU: Graphics processing unit, termine usato genericamente come abbreviazione per
“Scheda Grafica” (in realtà indicava originariamente dei processori con funzionalità
specializzate).
4. Draw call: Richiesta esplicita alla GPU di visualizzare qualcosa a schermo
(solitamente è preceduta da un certo numero di chiamate OpenGL per “preparare la
visualizzazione”)
5. Chiamata GPU/Chiamata OpenGL: OpenGL è un’API funzionale, pertanto ogni
utilizzo dell’API richiede la chiamata di una funzione, il termine ha rilevanza poichè
tali chiamate hanno un costo e si cerca in generale di ridurne il numero.
6. Frame: si riferisce ad un fotogramma composto da varie operazioni di rendering. La
durata di un frame (ovvero il tempo in cui rimane visualizzato prima che venga
visualizzato il frame sucessivo) viene solitamente misurata in millisecondi e fornisce
una stima della performance di un’applicazione grafica.
Cos’è OpenGL?

La Open Graphics Library, è un’interfaccia di programmazione per effettuare operazioni di


rendering. Si tratta di un modello Client/Server: il client chiede di disegnare degli oggetti 3D
a schermo e il server (solitamente una scheda grafica montata sulla stessa macchina) soddisfa
tali richieste.

In figura: Visione semplificata di OpenGL

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.

In Figura: stadio di primitive Assembly.

Una volta descritto approssimativamente un insieme di triangoli che rappresenta un oggetto, è


possibile applicarvi sopra un’immagine in modo da aggiungere dettagli extra.
In figura: esempio di fragments rasterizing.

In prima approssimazione possiamo equiparare OpenGL ad una funzione che prende in


ingresso dei vertici, immagini, un sistema di coordinate e restituisce come output
un’immagine che possiamo visualizzare o utilizzare successivamente.

In realtà OpenGL e le schede grafiche offrono funzionalità più complesse:

● In OpenGL si controlla la scheda grafica scrivendo programmi (shader) in un


linguaggio simile al C (GLSL) che permette di effettuare operazioni quasi arbitrarie.
● E’ possibile creare nuovi vertici a partire da quelli vecchi (Geometry shader)
● E’ possibile scomporre un triangolo in N triangoli più piccoli usando le coordinate
baricentriche ( Tessellation shader)
● E’ possibile effettuare il rendering all’interno di immagini (Render To Texture)
● E’ possibile passare input numerici arbitrari ai vari shader ( Uniform Buffer)
Scelta della versione API di OpenGL
La continua evoluzione del mondo hardware delle schede grafiche si riflette anche nelle
specifiche di OpenGL, che subiscono periodicamente degli aggiornamenti o dei completi
stravolgimenti: al momento di scrivere questa tesi si è giunti alla versione di OpenGL 4.5.
Le principali versioni di OpenGL sono state:

- OpenGL 2.1 (introdotto GPU programming)


- OpenGL 3.0 / 3.2 / 3.3 (deprecato il fixed pipeline)
- OpenGL 4.0 / 4.5

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”).

Un’applicazione grafica ha due scelte:

● Utilizzare direttamente OpenGL: in questo caso è possibile un controllo di basso


livello, a patto di essere disposti a scrivere una quantità maggiore di codice; inoltre
OpenGL è strutturato come una macchina a stati globale, quindi un cambiamento in
un punto del codice può causare un errore da un’altra parte del codice.

● Utilizzare un’astrazione di OpenGL (Motore 3D): in questo caso bisogna adattarsi


ad una nuova interfaccia per poterne utilizzare solo una piccola parte; in molti casi un
controllo a basso livello non è più possibile, e bisogna effettuare lavoro extra per
aggirare alcuni limiti imposti dal motore 3D. Inoltre la maggior parte dei motori 3D
fornisce molte funzionalità extra che esulano dal compito delle applicazioni grafiche
“pure” (networking, gestione file system, parser XML/Json etc.).

Quello che ho voluto sviluppare è quindi un’astrazione di OpenGL che vuole


semplificare l’utilizzo di OpenGL, mantenendo tuttavia un numero minimo di
dipendenze, in modo da permettere a chi vuole utilizzarlo la maggior semplicità
possibile.

Chi potrebbe essere interessato all’utilizzo di questo software?

● Persone che vogliono integrare funzionalità di rendering di basso/medio livello


mantenendo le dipendenze al minimo (ad esempio da utilizzare con sistemi di
windowing già esistenti come Qt, WxWidgets, SDL, SFML etc.)
● Chiunque voglia scrivere codice che possa cooperare con altre applicazioni grafiche
(ad esempio codice dedicato alla visualizzazione / generazione di oggetti particolari
come Alberi, Terreni, Nuvole) che utilizzano a loro volta questo modulo software e
non vogliono creare adattatori tra 2 interfacce diverse.
● Gli utenti che desiderano un livello di controllo maggiore rispetto a quello permesso
da motori 3D pensati per videogiochi o altri utilizzi specifici, senza dover affrontare la
complessità dell’utilizzo diretto di OpenGL
Scelte architetturali
Impostazione generale:
Il software che ho sviluppato cerca di seguire il principio di information hiding, permettendo
agli utenti finali di interagire con esso attraverso una serie di classi astratte (equivalenti alle
interfacce Java o C#), e si appoggia interamente ad un framework di Dependency Injection
(Infector++, anch’esso sviluppato da me).
Da notare che questo non comporta l’uso di polimorfismo, ma rende comunque il client
indipendente dalla costruzione di oggetti potenzialmente molto complessi.

Esempio di utilizzo: leggere da file un’immagine usando la Dependency Injection

Ecco i principali punti su cui mi sono concentrato durante lo sviluppo:

● 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 e configurazione di un oggetto di tipo ShaderOptions, in questo oggetto


vengono configurati i dati in comune ai vari stati del pipeline programmabile, ovvero i dati
riguardo al tipo e nome delle componenti dei vertici (tra le componenti più utilizzate
troviamo i vettori “posizione”, “colore”, coordinate “uv”, vettore “normale”).

• 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.

I frammenti di codice shader devono essere scritti dall'utente, soltanto le variabili in


input/output sono generate automaticamente.

Esempio di utilizzo dello shader manager. Da notare l'attributo “diffuse”


TextureManager:
E' la classe che si occupa della creazione delle texture. Gli oggetti di tipo Texture sono
creati a partire da immagini (che vengono caricate da file usando l'ImageManager).

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.

Nell'esempio di utilizzo dello ShaderManager, viene creato un oggetto ShaderOptions che ha


un attributo “diffuse”, la cui texture unit è impostata a 1. Grazie al TextureSlot siamo in
grado di abbinare una data Texture alla posizione logica “1”, in modo che chi scrive il
programma shader ritrovi effettivamente l'immagine desiderata nella variabile “diffuse”.

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).

Lo scopo di un RenderPass è quello di organizzare logicamente le operazioni di rendering.


Di fatto nelle tecniche di rendering moderne ogni fase logica viene chiamata “RenderPass”,
quindi mi sono attenuto alla stessa terminologia.

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).

Inoltre un'interfaccia aggiuntiva “Renderer” (visibile all'utente) viene iniettata nel


RenderThread, ed è utilizzata come punto di esecuzione dei comandi OpenGL.

Sono presenti inoltre un certo numero di classi di utilità che non richiedono la presenza di
Manager (RenderSlot, RenderPass, MiniArray, MaxiArray).
Rendering Asincrono

Il modello di threading utilizza 2 thread. Un thread principale (quello da cui l’utente


interagisce usando l’API pubblica) e un thread dedicato al rendering.

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.

Con questo approccio abbiamo due problemi:

Problema 1: Il codice client può essere bloccato da operazioni di rendering

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

Qualora il client effettuasse operazioni costose, come lettura/scrittura di file o computazioni


lunghe, il codice di rendering verrebbe bloccato causando sensibili ritardi nella
visualizzazione delle immagini.
La soluzione classica prevederebbe l'utilizzo di alcuni Worker Thread (per i task più onerosi)
ma ciò comporterebbe un codice client più complesso e difficile da mantenere (servirebbero
una Thread Pool, sincronizzazioni esplicite e così via).

La soluzione che ho scelto


L'approccio che ho scelto è quindi quello di distribuire quanto più possibile il lavoro su
Thread distinti senza complicare l'interfaccia finale per l'utente. Il seguente diagramma
illustra la sequenza di operazioni su thread distinti, con evidenziati i cambiamenti rispetto
all'approccio sincrono.

Nell'approccio asincrono il controllo è restituito immediatamente al codice client anche


quando vengono avviati task onerosi in background. I task, a loro volta, vengono eseguiti
senza essere interrotti da eventuali ritardi del codice client.
Vediamo ora come viene gestita la comunicazione dietro le quinte, ovvero i dettagli
implementativi dell'approccio asincrono.

Creazione di Oggetti OpenGL:

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.

La richiesta del client viene soddisfatta restituendo immediatamente un oggetto Texture


(implementato attualmente dalla classe GL3Texture), mentre in parallelo viene avviato il
caricamento dei dati nella GPU.

Da notare il campo “nativeHandle” che utilizzeremo in seguito

Come viene effettivamente creata la texture?


Il fulcro di questa operazione è il TextureManager:

“loadBitmapTexture” è il punto in cui viene generato un “callback” asincrono

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:

1. L'esecuzione dei comandi di “caricamento” (come ad esempio glGenTextures o


glTexImage2D) deve essere effettuata una sola volta e in anticipo rispetto ad altre
operazioni.
2. L'esecuzione di comandi di “rendering” (DrawCalls, Update degli Uniform Buffer)
deve essere effettuata in modo continuo fino al prossimo aggiornamento.
3. L'esecuzione dei comandi di “cancellazione” deve essere eseguita solamente quando
il “nativeHandle” è diventato visibile al mainThread, per il semplice fatto che si
deve generare il callback “glDeleteTexture(nativeHandle)”.

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.

In una prima implementazione utilizzavo uno SpinLock; in seguito ho deciso di utilizzare un


Mutex, semplicemente perchè la nuova libreria standard del C++11 fornisce un costrutto
lock_guard che mi permette di semplificare il codice, ma funziona solamente in
congiunzione con un mutex.
In realtà le sincronizzazioni necessarie sono così poche che il mutex non causa rallentamenti
sensibili.
Cancellazione di Oggetti OpenGL:

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.

Possiamo anche richiedere il controllo del Reference Count in modo manuale:

Esempio di richiesta esplicita di cancellazione ( analogo ad una chiamata System.GC() in


un linguaggio managed).

Una volta che il manager trova un oggetto che è possibile cancellare, effettua 2 operazioni:

1. Genera il comando che cancellerà la Texture

Generazione del Callback che cancella una Texture.

2. Riutilizza la memoria dedicata all'oggetto Texture, mantenendo un array di oggetti


“Texture” inutilizzati, che viene ridimensionato saltuariamente.
Quando un utente chiede una nuova texture, viene invece restituito un oggetto da tale
array.
Inoltre, quando possibile, la memoria utilizzata viene “compattata” imitando il
comportamento di un Garbage Collector (per migliorare la cache locality), e vengono
restiutiti oggetti inutilizzati contigui a oggetti utilizzati.
Esempio di utilizzo:

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:

Distribuire il lavoro su 2 thread permette di aumentare la quantità di lavoro parallelizzabile e


offre un vantaggio aggiuntivo, infatti il costo computazionale (cicli CPU totali) con
l'approccio standard è il seguente:

Ciclitotali = (Ciclirendering + Cicliclient) * Looptotali

Mentre con l'approccio asincrono: Ciclitotali = Ciclirendering*Looprendering + Cicliclient*Loopclient

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)

Un fatto secondo me importante è poter rendere la compilazione il più semplice e automatica


possibile; questo permette a più persone di poter verificare il funzionamento del codice senza
dover risolvere problemi specifici per particolari piattaforme. Nella soluzione che ho
sviluppato è presente un unico file script di CMake che gestisce tutta la complessità “cross-
platform”, mentre l’utente deve solamente preoccuparsi di “avviarlo”. Ecco 3 semplici script
shell che fanno tutto il lavoro (ovviamente devono già essere installati Cmake e un
compilatore C++11)

WINDOWS

Visual Studio 2015 + CMake (batch script):


set PATH=C:\Program Files (x86)\MSBuild\14.0\Bin;%PATH%
mkdir build
cd build
cmake -G "Visual Studio 14 2015" ..
cmake --build . --target ALL_BUILD --config Release

GCC 4.8 + CMake (batch script):


set PATH=D:\GCC\mingw32\bin;%PATH%
mkdir build
cd build
cmake -G "CodeBlocks - MinGW Makefiles" ..
mingw32-make all
ctest

LINUX/OSX

GCC + CMake (bash script):


#!/usr/bin/env bash
mkdir build
cd build
cmake ..
make
make test
Minimizzazione dei cambi di stato OpenGL ( approccio Greedy)

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:

glBindTexture( GL_TEXTURE_2D, textureId); // 1a attivazione


glBindTexture( GL_TEXTURE_2D, textureId); // 2a attivazione

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:

glBindTexture( GL_TEXTURE_2D, textureId);


glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

glBindTexture( GL_TEXTURE_2D, textureId2);


glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

glBindTexture( GL_TEXTURE_2D, textureId);


glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

glBindTexture( GL_TEXTURE_2D, textureId2);


glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

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:

glBindTexture( GL_TEXTURE_2D, textureId);


glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

glBindTexture( GL_TEXTURE_2D, textureId2);


glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
Stato dell’arte (approccio non greedy):
Ogni volta che viene richiesto il rendering di un oggetto 3D è necessario effettuare sempre le
seguenti fasi:

1. Bind di un Framebuffer (molto costoso)


2. Bind di un GPU Program (costoso)
3. Setup dei buffer per I vertici (mediamente costoso)
4. Setup delle texture unit (mediamente costoso)
5. Update uniform buffer (costo proporzionato alla quantità di dati)
6. Setup pipeline (WriteMask, Stencil, DepthTest)
7. Drawcall (il costo varia in base alle opzioni precedenti)

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);

foreach g:Geometry in s.Geometries


SetupTextures(g.Textures);
SetupVertexArray(g.Vertices);
SetupIndexArray(g.Indices);
SetupPipeline(g.Options);
Draw(g);
end
end
end

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:

foreach g:Geometry in geometries


if(g.Framebuffer != previousFrameBuffer)
BindFrameBuffer(g.Framebuffer);

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”.

Il listato completo dell'algoritmo Greedy è disponibile nel seguente file:

Bade3D\badeSource\gl3\GL3RenderQueue.cpp
Alcune strategie di minimizzazione stati

Clear Buffers ( glClear )

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:

← Questa è Suzanne, la mascotte di Blender


(software per modellazione 3D).

Quando decido di importare Suzanne nel mio


videogioco fatto con Unity3D (software
proprietario per la creazione di videogiochi)
questa viene “ruotata” perché Unity3D di
default assume come “Su” l’asse Y. Mentre
Blender usa come default l’asse Z per la
direzione “Su”.

← A sinistra Suzanne importata in modo


non corretto e quindi “ruotata”.

Software sviluppati da persone diverse


utilizzano convenzioni diverse. Questo causa
solitamente delle difficoltà di comunicazione
tra i diversi software.

Per questo motivo ho cercato un possibile


approccio per risolvere una volta per tutte
il problema.

Ovviamente il software non puo' accorgersi


che un oggetto è stato importato in modo non corretto, come risulta invece evidente all'utente.
Per poter correggere “numericamente” la disposizione dei dettagli, è necessario che sia chi ha
creato l'oggetto 3d sia chi vuole visualizzarlo dichiarino quale assi debbano essere utilizzati
come direzioni logiche “Su”, “Destra”, “Davanti”.
L'algoritmo che ho progettato si applica in fase di “caricamento da disco rigido” dei
dati.

1. Scelta di una convenzione per la disposizione dei dettagli.


2. Apertura del file di un modello3D ed acquisizione della relativa convenzione
utilizzata.
3. Se la convenzione utilizzata dal modello 3D è diversa da quella scelta, applicazione
dell'algoritmo per convertire i dati al suo interno.

Il fatto che una convenzione sia preferibile ad un'altra è puramente soggettivo.

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:

1. Scambio di due assi


2. Cambio del “verso” di un asse

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).

La necessità di convertire anche i quaternioni

I moderni software di modellazione e di visualizzazione non visualizzano singoli oggetti


statici, bensì permettono di utilizzare interi Scene Graph oppure modelli con parti mobili (ad
esempio un umanoide che cammina). Per poter realizzare modelli animati o parti di
SceneGraph è necessario poter trattare anche le rotazioni, in questo modo non limitiamo
l'utilizzo dell'algoritmo ai soli oggetti 3D statici.

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 {+,-}.

Intuitivamente se dentro ad un programma di modellazione 3D creiamo un umanoide, la


convenzione ci è data dalla disposizione dei dettagli all'interno di tale modello:

– 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”.

Un esempio possibile di convenzione:

(Su) → (Y, + )
(Destra) → (X, +)
(Davanti) → (Z, -)

La convenzione logica è soggettiva, e dipende dalla disposizione dei dettagli all'interno di un


modello 3d. Pertanto soltanto la persona che ha creato il modello 3D è in grado di dire quale
convenzione è stata utilizzata.
Supponiamo che uno dei vertici della mano destra del nostro umanoide abbia coordinate

x = 2, y = 3, z = 0.5

Qualora dovessimo visualizzare l'umanoide in un software di rendering 3D che utilizza una


diversa convenzione (anch'essa decisa in modo arbitrario e soggettivo) dovremmo agire sui
dati dell'umanoide andandoli a sostituire o invertendoli di segno in base alla nuova
convenzione.

Ad esempio se il software di rendering utilizzasse come convenzione


(Su) → ( Z, + )
(Destra) → ( Y, - )
(Davanti) → ( X, + )
Il vertice della mano destra dovrebbe avere come nuove coordinate

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).

L'algoritmo per convertire i vettori:

Date due convenzioni: { Originale, Destinazione}

- Per ogni vettore p del modello che vogliamo importare


- Creo un vettore p' che conterrà i dati convertiti di p
- Per ogni etichetta s del vettore
- k ← valoreNumerico( s, p)
- d ← Direzione logica a cui s è associata in Originale
- e ← Etichetta ( d , Destinazione )
- Se Verso( d, Destinazione) è uguale a Verso( d, Originale)
p'.e ← k
altrimenti
p'.e ← - k

L'algoritmo per convertire i quaternioni:

Ai fini di questo lavoro considero i quaternioni come vettori a cui sia stato aggiunto un
ulteriore elemento etichettato “w”.

I quaternioni rappresentano una rotazione intorno ad un asse, pertanto ad ogni operazione


elementare effettuata su di essi corrisponde anche un cambio di segno della componente reale
di un quaternione ovvero l'angolo di rotazione (se specchio una rotazione devo anche
cambiare il senso di rotazione da orario ad anti-orario e viceversa, infatti se guardo un
orologio allo specchio, la lancetta risulta ruotare in senso antiorario).
Se invece di effettuare operazioni elementari, effettuiamo un'assegnazione diretta tra etichette
sappiamo che:

– Se l'etichetta finale è diversa abbiamo effettuato uno scambio di assi


– Se il verso finale è diverso abbiamo effettuato una riflessione

Date due convenzioni: { Originale, Destinazione}

- Per ogni quaternione q del modello che vogliamo importare


- Creo un quaternione q' che conterrà i dati convertiti di q
-n←0
- Per ogni etichetta s del quaternione eccetto w
- k ← valoreNumerico( s, q)
- d ← Direzione logica a cui s è associata in Originale
- e ← Etichetta ( d , Destinazione )
- q'.e ← k
- Se Verso( d, Destinazione) è diverso da Verso( d, Originale)
n←n +1
q'.e ← - q'.e

- Se Etichetta( d, Destinazione) è diverso da Etichetta( d, Originale)


n←n +1
q'.e ← - q'.e

- Se n è dispari
q'.w ← - q.w
altrimenti
q'.w ← q.w

Contratto:

L'algoritmo per convertire i vettori si applica a vettori di dimensione >= 2


L'algoritmo per convertire i quaternioni si applica a quaternioni di dimensione >=3
Per applicazioni grafiche di dimensioni ne utilizziamo 3 quindi è possibile scrivere algoritmi
molto più efficienti sia per il caso generale che per sottoinsiemi di conversioni.

E' necessario saper convertire altri dati oltre a vettori e quaternioni?

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

LookAtMatrix( Vector3 position, Vector3 target, Vector3 up);


Siccome l'input di questa funzione sono 3 vettori, è sufficiente convertire i 3 vettori in input
per ottenere una matrice convertita correttamente:

non è quindi necessario creare un algoritmo per convertire anche la matrice.

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à?

Stato attuale della libreria

Repository “ufficiale”:

https://github.com/Darelbi/Bade3D

● Problema rendering asincrono e consistenza stati OpenGL: risolto


(vedesi QueueExecutor e AsyncProxy)
● Utilizzabile: no
( non tutte le entità e i manager sono completi)
● Deploy e Continuous Integration per Desktop: pronta
● Architettura & Design: pronti
● Linux, OSX, Windows: supportati
● Testing remoto di OpenGL: richiede aggiunta MESA ( www.mesa3d.org )
● Emscripten: web workers e toolchain non ancora supportati (in alternativa si può
implementare usando 1 solo thread)
● Android: native activity non ancora supportata, nessun binding presente per Java
● Windows Phone: solo Direct X (investigazione richiesta)
● Oggetti FrameBuffer (render to texture): non ancora implementato
● Animazioni interpolate lato render Thread: non ancora implementato

Framework di supporto:

https://github.com/Darelbi/Infectorpp2

● Completo
Riferimenti (ordinati alfabeticamente per nome)

A. Thomas (2013) - EntityX, a fast, type-safe C++ Entity Component System:


https://github.com/alecthomas/entityx

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.

J. B. Kuipers (1999) - Quaternions and rotation sequences, 5 - Quaternion Algebra

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

OpenGL Wiki (2015) - Ubiquitous Extensions: https://www.opengl.org/wiki

T. Forsyth (2006) - Premultiplied Alpha, TomF’s Tech Blog

W. R. Hamilton (1844-1850) - On Quaternions, or a new system of imaginaries in Algebra


Note aggiuntive:

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:

● T. Forsyth (2006) per progettare il sistema di rendering in sovrapposizione trasparente, in


pratica invece che fornire oggetti trasparenti e oggetti in colorazione additiva che richiedono 2
stati diversi per la macchina OpenGL, mi limito ad usare la tecnica consigliata che permette di
ottenere lo stesso risultato ma usando un solo stato OpenGL (inoltre l’alfa pre-moltiplicato
permette di comporre immagini trasparenti tramite i FrameBuffer objects, lacuna che ho
trovato in alcuni motori 3D open source: Irrlicht, Ogre).
● N. Thibieroz (2012) per alcune parti riguardanti l’ottimizzazione degli stati OpengL (in
particolare, se possibile cancellare contemporaneamente Z buffer e Stencil buffer è più veloce
se questi sono memorizzati insieme)
● L. Hrabcak, A. Masserann (2012) per l’aggiornamento dei buffer di OpenGL in modo
asincrono, tra le tecniche suggerite ho scelto quella più “stabile” e semplice da implementare
(sebbene non sia tra le più performanti, in futuro si potrà comunque cambiare).
● A. Thomas (2013) l’architettura della libreria è ispirata al pattern “entity component system”.
L’unica ragione per cui non ho usato la libreria EntityX direttamente è che non mi permetteva
di risolvere in modo semplice la comunicazione asincrona. Sto ancora cercando di capire se
posso utilizzarla per poter ridurre la quantità di codice totale e semplificare
l’implementazione.
● E. Persson (2007) mostra che certe funzioni di Depth testing e stenciling sono più performanti,
io ho scelto arbitrariamente di supportare solo tali operazioni e di esporle nell’interfaccia
dell’engine (L’idea è che fino a quando non trovo uno use-case che richiede necessariamente
un interfaccia più ricca, mi limiterò a offrire tali funzionalità).

Potrebbero piacerti anche