• Save
Tesi
Upcoming SlideShare
Loading in...5
×
 

Like this? Share it with your network

Share

Tesi

on

  • 2,403 views

Tesi Politecnico di Torino. Italian.

Tesi Politecnico di Torino. Italian.

Statistics

Views

Total Views
2,403
Views on SlideShare
2,322
Embed Views
81

Actions

Likes
0
Downloads
0
Comments
0

5 Embeds 81

http://yetanothergameprogrammingblog.blogspot.com 48
http://www.linkedin.com 16
https://www.linkedin.com 13
http://www.lmodules.com 3
http://www.slideshare.net 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Tesi Document Transcript

  • 1. POLITECNICO DI TORINO Facoltà di Ingegneria Corso di Laurea in Ingegneria Informatica TESI DI LAUREA Motore tridimensionale per ambienti virtuali distribuiti Relatori Prof. Paolo Montuschi Prof. Raffaele Meo Ing. Andrea Sanna Candidato Francesco E. Carucci Ottobre 2000
  • 2. Ai miei nonni
  • 3. Premessa Premessa La creazione di mondi tridimensionali ha assunto una grande importanza negli ul- timi anni; in particolare un ampio spettro di applicazioni consiste nella costruzione di scenari virtuali multiutente dove gli avatar possano interagire e/o collaborare (video- giochi, realtà virtuale, visualizzazione e progettazione collaborativa, applicazioni mul- timediali). I ricercatori e gli sviluppatori mirano a rendere mondi sempre più comples- si e dettagliati, che possano essere “visitati” da un gran numero di utenti. Nuovi e più sofisticati strumenti sono messi a disposizione dagli attori virtuali per interagire sia con i mondi virtuali sia con gli altri avatar. Il mercato delle applicazioni grafiche sta, inoltre, subendo una notevole espansione grazie alla definitiva affermazione del setto- re videoludico: il videogioco è diventato un prodotto di massa e la sua enorme diffu- sione sta spingendo alla progettazione e commercializzazione di hardware dedicato alla grafica tridimensionale sempre più potente e ricco di funzionalità. Il software in grado di sfruttare l’hardware odierno è ancora sviluppato con criteri non ingegneristici e poco flessibili, che porta, a volte, all’uscita sul mercato di prodotti poco portabili e mantenibili. Il videogioco moderno, tuttavia, ha raggiunto un livello di qualità e di cu- ra dei particolari nei mondi tridimensionali tale da richiedere lunghi tempi di sviluppo e costi di produzione elevati; ne sono validi esempi giochi famosi ormai entrati nell’immaginario collettivo quali Quake III prodotto dalla ID software e Tribes pro- dotto dalla Dynamix. Il progetto PEUCK (Pixel ‘Em Up Construction Kit) si colloca in questo scenario e mira a fornire strumenti semplici da usare, flessibili, portabili per lo sviluppo di mondi tridimensionali realistici e multiutente. L’obiettivo è creare uno strato software alta- mente ottimizzato fra l’hardware e l’utente, tale da permettere al progettista di con- centrare i propri sforzi sul lato “artistico” del progetto, relegando le problematiche tecniche e realizzative, alla base della realizzazione di un’applicazione in tempo reale, allo strato software intermedio. Il PEUCK può essere considerato alla stregua di un Sistema Operativo che virtualizza e gestisce le risorse di sistema (oggetti nella scena, effetti sonori, luci, connessioni di rete) fornendo un insieme di “chiamate di sistema” che possono essere usate dall’utente per realizzare la propria applicazione. Il PEUCK fornisce le primitive per la gestione di ambienti sia esterni sia interni or- ganizzati in diversi mondi visibili attraverso delle telecamere Nell’ottica di realizzare una piattaforma modulare e semplice da estendere, il PEUCK è stato interamente progettato e realizzato usando estensivamente tecniche di progettazione e programmazione ad oggetti (OOD, OOP); l’utente “vede” ogni entità del Sistema Operativo (oggetti, suoni, luci, connessioni) come una classe estendibile a piacere. L’efficienza della piattaforma software è garantita adottando tecniche di pro- grammazione a basso livello nelle parti di codice più critiche dal punto di vista presta- zionale (“inner loops”). I
  • 4. Premessa Il progetto nasce nel settembre del 1998 e si è sviluppato secondo un modello di programmazione prototipale: è stato dapprima programmato un prototipo dell’ambiente, che permettesse di valutare le problematiche da affrontare e fornisse un’idea delle specifiche di progetto. Il prototipo è stato successivamente usato come base per la programmazione effettiva del motore e tenuto presente durante lo sviluppo della gerarchia di classi. Il progetto è stato, quindi, portato avanti secondo un modello evolutivo, riformulando le specifiche in base all’analisi dell’evoluzione dell’hardware e delle esigenze del mercato videoludico, primo riferimento e applicazione di questa tesi. Questo progetto è stato usato per la produzione di due applicazioni che girano su normali PC dotati di una scheda grafica che acceleri le normali funzioni tridimensio- nali, dimostrando la sua utilizzabilità in prodotti commerciali. La prima applicazione consiste nella simulazione di un’aula virtuale in cui un professore è in grado di tenere una lezione a diversi studenti collegati da postazioni remote in un ambiente tridimen- sionale; un server gestisce la propagazione dell’ambiente ai diversi client e lo scambio di messaggi dimostrando l’usabilità di questo lavoro anche in applicazioni che preve- dano ambienti virtuali distribuiti. L’applicazione è stata testata con un massimo di 6 studenti virtuali collegati in una rete locale. La seconda applicazione consiste nella navigazione di una città composta da un totale di 35 quartieri e 700 palazzi, per una scena composta da circa 140k poligoni. Il motore tridimensionale permette la naviga- zione in tempo reale all’interno della città dimostrando una buona adattabilità ad am- bienti densi di oggetti e con un alto numero di poligoni, ambienti che saranno nell’immediato futuro sempre più frequenti nelle applicazioni videoludiche. Il presente lavoro è diviso in cinque sezioni: Introduzione, Architettura, Esempi d’uso e risultati, Note tecniche e Conclusioni. La prima sezione, Introduzione, introduce le problematiche relative alla produzione di grafica in tempo reale descrivendo i concetti di “interattività” e “realismo” nell’ambito delle applicazioni grafiche. Sono qui forniti al lettore gli strumenti per comprendere le basi per giudicare la bontà di un motore in grado di generare immagi- ni “abbastanza realistiche” in tempo reale, di ambienti dei quali è fornita una descri- zione geometrica. La seconda sezione che occupa quasi interamente questo scritto, descrive l’architettura del PEUCK: dopo una descrizione generale dell’architettura, sono pre- sentati uno dopo l’altro gli strati dai quali il progetto è composto a partire dallo strato piu’ prossimo all’ambiente operativo ospite (Operative Layer), proseguendo con lo strato di astrazione della piattaforma (PAL), per concludere, infine, con il cuore del progetto che risiede nello strato indipendente dalla piattaforma dove sono realizzati i servizi offerti direttamente all’utente organizzati in una gerarchia di classi. Una breve descrizione dello strato applicativo, conclude la seconda sezione. II
  • 5. Premessa La terza sezione descrive due esempi d’uso del PEUCK con alcuni stralci di codice che permettano di valutare l’interfaccia a disposizione dell’utente e immagini che mo- strano i risultati visivi. I risultati forniscono un’analisi delle prestazioni velocistiche del motore di rendering tridimensionale. La quarta sezione descrive l’hardware e l’ambiente di sviluppo usati durante la scrittura del codice sorgente e il testing delle applicazioni. Nella quinta sezione, sono stilate le conclusioni e descritti gli obiettivi raggiunti da questo lavoro con particolare enfasi nel descrivere i possibili usi in ambiti professio- nali quali la produzione di titoli per l’intrattenimento. Sono, inoltre, descritti gli svi- luppi del progetto con l’elenco dei servizi che il motore non è ancora in grado di for- nire, ma che sono fondamentali per il suo uso professionale e che saranno realizzati nei mesi futuri dal team di sviluppo responsabile del progetto. III
  • 6. Ringraziamenti Ringrazio i miei genitori per il sostegno fornitomi fin qui. Ringrazio l’ing. Sanna per la sua disponibilità e per la sua pazienza. Ringrazio Sebastiano Mandalà per l’aiuto nella progettazione e nella realizzazione del codice.
  • 7. Indice Indice 1. INTRODUZIONE...................................................................................1 2. ARCHITETTURA..................................................................................4 Descrizione dell’architettura......................................................................4 Piattaforma operativa (Operative layer) ..................................................6 Platform Abstraction Layer (PAL) ...........................................................7 Kernel.......................................................................................................7 Driver .......................................................................................................9 Driver di Rendering............................................................................10 PEUCK.......................................................................................................14 Il Peuck Object Mode (POM) ...............................................................14 Persistenza degli oggetti ........................................................................16 PEUCK3D..............................................................................................18 Trasformazioni, viewport e telecamera ..............................................18 Trasformazioni..............................................................................18 Viewport e proiezioni ...................................................................19 Telecamera....................................................................................23 Realizzazione ................................................................................25 Modello d’illuminazione ....................................................................32 Interpretazione matematica...........................................................32 Realizzazione ................................................................................34 Texture mapping.................................................................................39 Texture mapping.................................................................................39 Introduzione..................................................................................39 Realizzazione ................................................................................41 Bump mapping ...................................................................................46 Introduzione..................................................................................46 Interpretazione matematica...........................................................47 Realizzazione ................................................................................52 Mesh e oggetti ....................................................................................55 Mesh, Parti e VertexBuffer...........................................................56 Oggetti ..........................................................................................63 Strato applicazioni ............................... Errore. Il segnalibro non è definito. I
  • 8. Indice 3. ESEMPI D’USO ..................................................................................70 Classroom ..................................................................................................70 SFZ .............................................................................................................75 4. CONCLUSIONI E POSSIBILI SVILUPPI ...........................................92 Conclusioni ................................................................................................92 Possibili Sviluppi .......................................................................................93 5. BIBLIOGRAFIA ..................................................................................95 II
  • 9. Introduzione 1. Introduzione La produzione di un’immagine generata dal calcolatore consiste in un processo formato da diversi passi: • Il designer dell’applicazione specifica gli oggetti che compongono la scena, le loro forme, le posizioni, gli orientamenti e il colore (oppure la texture, concet- to che sarà definito in seguito) delle superifici. • Sceglie, quindi, la posizione dell’osservatore e la direzione di vista. Il motore tridimensionale trasforma, quindi, i punti dell’immagine per creare una proie- zione prospettica della scena • A questo punto del processo, è eseguito uno o più algoritmi in cascata che de- terminano quali parti della scena sono visibili all’osservatore. Questo è ciò che si chiama in letteratura “hidden surface removing”, ovvero rimozione delle su- perfici nascoste. • Gli oggetti della scena sono resi realistici attraverso la simulazione del proces- so fisico di illuminazione usando modelli più o meno realistici. Il designer de- finisce le sorgenti luminose, la loro posizione, l’intensità e il colore. La luce emanata da una superficie è la combinazione della luce che arriva direttamente dalle sorgenti luminose e riflessa dalla superficie stessa, luce dell’ambiente che simula il contributo uniforme di tutte le sorgenti luminose, la luce generata dalla superficie stessa. • L’immagine è, quindi, disegnata dal motore di rendering che calcola per ogni punto dell’immgine il colore visto dal punto di vista dell’osservatore. Il compito più importante di un motore di rendering tridimensionale è la simula- zione del processo di illuminazione. Due algoritmi per l’illuminazione delle scene so- no al momento i più popolari, ray tracing e radiosity. Questi due metodi usano ap- procci opposti al problema. Il ray tracing è stato introdotto nel 1979 da Turner Whitted dei Bell Laboratories. L’idea principale consiste nel tracciare un raggio che attraversi la scena dal punto di vista dell’osservatore verso ogni pixel dello schermo. Se il raggio colpisce una super- ficie, l’algoritmo genera una serie di raggi riflessi e rifratti che, a turno, sono tracciati per conoscere la loro eventuale intersezione con altre superfici. Il colore finale e l’intensità di ogni pixel sono calcolati sommando i contributi forniti da ogni raggio generato. Il ray tracing produce immagini realistiche simulando accuratamente super- 1
  • 10. Introduzione fici riflettenti e metalliche ed è, per sua natura, dipendente dalla posizione dell’osservatore; questo significa che l’intero calcolo deve essere ripetuto quando la posizione dell’osservatore si modifica. Il radiosity, metodo sviluppato nel 1984, e’, al contrario, indipendente dalla punto di vista: data una scena statica, il calcolo deve essere effettuato una sola volta. Una volta che l’illuminazione globale della scena è stata determinata, è semplice creare una serie di immagini movendo il punto di vista dell’osservatore. Radiosità usa il principio di conservazione dell’energia per determinare l’intensità della luce per ogni superficie: per ogni superficie della scena è scritta un’equazione che calcola il contri- buto della luce come la somma di dei contributi di tutte le restanti superfice. Sebbene questo metodo possa sembrare ideale per la simulazione dell’illuminazione in un am- biente virtuale, il suo principale difetto risiede nell’enorme numero di calcoli necessa- rio nel ricalcolo qualora la scena non fosse statica, numero di calcoli ancora non alla portata degli odierni calcolatori. Entrambi i metodi sono quindi inadatti al giorno d’oggi per la resa grafica di am- bienti virtuali tridimensionali. Sebbene in futuro si possa ipotizzare che la potenza di calcolo delle macchine commercializzate a livello “consumer” sarà sufficiente per l’esecuzione in tempi ragionevoli di questi due algoritmi, altri modelli di illuminazio- ne, descritti nel seguito del lavoro, sono oggi usati per il rendering in tempo reale. Il rendering in tempo reale, infatti, consiste nel generare rapidamente immagini sul monitor di un computer: un’immagine appare sullo schermo, l’utente interagisce con essa e reagisce, i suoi comandi influiscono su ciò che è visualizzato successivamente. Il ciclo di reazione e resa dell’immagine avviene ad un ritmo abbastanza rapido tale che l’utente non possa notare le singole immagini, ma sia immerso in un processo di- namico che appare a lui come un’animazione. La velocità alla quale le immagini sono visualizzate sullo schermo è misurata in “frame per secondo” (fps) oppure Hertz (Hz). Alla velocità di un frame per secondo, c’è poca sensazione di interattività con il mondo visualizzato e l’utente è consapevole dell’arrivo di ogni singola immagine. A circa 6 frame per secondo, l’utente inizia a percepire una certa sensazione di interattività con l’ambiente. Un’applicazione che riesca a visualizzare 15 frame per secondo può già iniziare ad essere considerata “un’applicazione grafica real time”; l’utente riesce a focalizzare la propria attenzione sull’azione e sulla reazione. Esiste un limite oltre il quale le differenze di interattività si fanno talmente scarse da non essere più notabili: il punto di fusione delle immagini, è mediamente per l’uomo attorno ai 30 frame per secondo. Sebbene questo limite teo- rico sia sufficiente a fornire l’illusione di un’animazione continua, oltre il 60/70 frame per secondo, l’utente percepisce un’animazione talmente fluida che la sua interazione con il mondo diventa naturale e realistica. L’interattività non è il solo criterio con il quale si giudica un’applicazione grafica in tempo reale: se così fosse, qualunque applicazione che rispondesse rapidamente ai 2
  • 11. Introduzione comandi dell’utente e disegnasse qualcosa sullo schermo potrebbe essere giudicata positivamente. Un’applicazione grafica in tempo reale, per essere usabile e immersi- va, deve produrre immagini dotate di un certo “realismo”, che implica ricorrere alla resa di immagini tridimensionali il più possibile fedeli alla realtà. E’ necessario in porre l’attenzione sul compromesso che si cerca di ottenere in questo campo: un’immagine fotorealistica nell’accezione classica non e’, con la tecnologia attuale, ottenibile in tempo reale: ciò che si cerca di ottenere è una buona approssimazione di fotorealismo, che, con l’ausilio dell’animazione, possa ragionevolmente ingannare l’utente. L’interattività e un certo senso di tridimensionalità dello spazio sono condizioni sufficienti per un’applicazione grafica in tempo reale, ma un terzo elemento sta rapi- damente diventando parte importante in questa definizione: l’accelerazione hardware della grafica tridimensionale. Sebbene hardware dedicato per la grafica tridimensiona- le è disponibile nelle workstation professionali da parecchi anni, è recente l’ingresso di acceleratori a livello “consumer”. Con il rapido avanzamento del mercato; adattato- ri in grado di accelerare la resa di primitive tridimensionali stanno diventando acces- sori comuni nei PC di fascia bassa. Sebbene un acceleratore tridimensionale non sia assolutamente necessario per la generazione di grafica in tempo reale, esso velocizza le operazione e permette una resa visiva migliore a un livello tale da rendere il suo supporto necessario per una qualsiasi applicazione grafica in tempo reale odierna. Il motore tridimensionale qui presentato si propone come una API in grado di for- nire primitive ad alto livello per la programmazione di applicazioni grafiche in tempo reale e pone massima attenzione sulla generazione di immagini il più possibile reali- stiche ad un livello “interattivo”, facendo largo uso delle possibilità offerte dai mo- derni acceleratori grafici tridimensionali. Durante lo sviluppo è stata, quindi, posta particolare attenzione nel delegare quando possibile all’acceleratore grafico la mag- gior parte delle operazione così da ottenere due scopi: un maggiore frame rate e una minore occupazione della cpu, così da lasciarla il più possibile libera di eseguire altri task quali possono essere ad esempio la simulazione di leggi fisiche oppure l’esecuzione di algoritmi di intelligenza artificiale che forniscano maggior realismo all’ambiente virtuale. 3
  • 12. Architettura 2. Architettura Figura 1: L'architettura del PEUCK Descrizione dell’architettura L’architettura di sistema del PEUCK è mostrata in Fig. 1. Ogni modulo apparte- nente ad un determinato livello, può usare i servizi messi a disposizione dai moduli appartenenti al medesimo livello oppure ai livelli inferiori, in un’architettura comune a quella usata tipicamente nella progettazione dei sistemi operativi, che permette un’organizzazione dei servizi coerente e semplice da mantenere. Il module Peuck3D, che si occupa della resa di un ambiente tridimensionale, usa i servizi forniti dal modulo Peuck2D (per visualizzare un’interfaccia utente bidimen- sionale ad esempio) al suo stesso livello e l’API messa a disposizione dal driver di Rendering situato ad un livello inferiore (PAL). L’uso di oggetti appartenenti ad un modulo per la visita di mondi VRML sarebbe illegittimo. Ad ogni modulo, quindi, non è permesso l’uso di servizi forniti da un livello superiore. L’organizzazione a strati permette, inoltre, una netta separazione del S.O. dai det- tagli della piattaforma sulla quale è eseguito, incapsulati nel PAL. Il Kernel si farà ca- rico di fornire agli strati superiori i servizi di base comuni ad ogni piattaforma quali l’allocazione della memoria, la gestione dei moduli, il multithreading e la sincronizza- 4
  • 13. Architettura zione, l’accesso alle periferiche di memorizzazione di massa. La struttura a driver permette una semplice estendibilità dell’architettura fornendo API per la gestione del- le schede grafiche accelerate, delle periferiche di input quali tastiera, mouse, joystick, dei diversi protocolli di rete (TCP/IP, NetBEUI). Lo strato intermedio che realizza le classi base del PEUCK risulta, quindi, portabile e risente in minima parte del cambio di piattaforma. E’ possibile ipotizzare a questo livello la realizzazione del PEUCK an- che su piattaforme non “standard” quali Win32/Macintosh, ma su hardware dedicato come può essere una consolle per videogiochi. 5
  • 14. Architettura Piattaforma operativa (Operative layer) Il primo livello in cui si articola l’architettura del PEUCK è lo strato operativo di- pendente dalla piattaforma. Allo stato attuale del progetto, l’unica piattaforma hardware supportata è la famiglia di processori Intel e compatibili (PentiumII, Pen- tiumIII, AMD e Cyrix), con sistema operativo Win32 (Windows 9x, Windows NT/2000) oppure BeOS. Il primo strato riguarda l’hardware specifico e/o il sistema operativo sottostante sul quale il progetto si appoggia: questo non è, quindi, oggetto del nostro lavoro, ma si in- tende fornito “così com’è) dal produttore della piattaforma. Le risorse messe a disposizione dal primo strato sono strettamente dipendenti dalla piattaforma in oggetto e possono variare al variare della piattaforma stessa: è possibi- le, ad esempio, avere hardware avanzato per la gestione di grafica tridimensionale come doversi appoggiare ad una realizzazione completamente software del disegno delle primitive. Ancora ci si può trovare di fronte ad una piattaforma che non realizzi nativamente un’architettura multithreading: sarà compito dello strato superiore di a- strazione dell’hardware fornire una sua realizzazione adeguata. Le risorse sono, infatti, “virtualizzate” dagli strati superiori e fornite in un contesto coerente e invariante alla piattaforma; in quest’ottica, l’utente non è direttamente a contatto con l’hardware e la migrazione della propria applicazione da una piattaforma all’altra può essere eseguita con maggiore semplicità, riducendo i tempi di sviluppo di un progetto multimediale multipiattaforma. 6
  • 15. Architettura Platform Abstraction Layer (PAL) Anche il PAL (Platform Abstraction Layer) dipende strettamente dalla piattaforma, in quanto al suo interno è situato il Kernel che fornisce i servizi base del PEUCK: al- locazione della memoria, gestione dei task, primitive di sincronizzazione, file system, caricamento e gestione di moduli dinamici. Al contrario della piattaforma operativa, il PAL è oggetto del nostro lavoro, in quanto il suo compito è quello di virtualizzare l’ambiente di lavoro e di fornire una piattaforma virtuale alla quale gli strati superiori possano appoggiarsi per fornire i propri servizi. Oltre al Kernel, il PAL contiene moduli a caricamento dinamico (driver) per l’accesso alle periferiche comuni in applicazioni di grafica in tempo reale: parliamo del driver di rendering per l’accesso alle schede grafiche acceleratici (in caso di loro assenza, fornirà una loro realizzazione software), il driver di input per l’accesso a pe- riferiche quali tastiera, mouse, joystick, il driver sonoro per l’accesso ad eventuali schede di riproduzione sonora, driver di rete per l’accesso a schede di rete attraverso vari protocolli. Il driver di rendering, ad esempio, fornisce agli strati superiori un set di chiamate per la manipolazione di primitive bidimensionali e tridimensionali (vertici, triangoli), per la loro trasformazione e proiezione, per la gestione di texture e proprietà dei mate- riali. I driver dornite correntemente, supportano sia la libreria di “rastering” OpenGL di Silicon Graphic (in versione generica o con particolare supporto per l’etensioni proprietarie NVIDIA) sia la librearia Direct3D fornita assieme al pacchetto DirectX di Microsoft. Kernel Il Kernel si occupa di astrarre le risorse tipiche di un sistema operativo e di fornire agli strati superiori i servizi ottimizzati per il genere d’applicazione richiesto: grafica in tempo reale. Il Kernel è strattamente dipendente dalla piattaforma e, per ragioni d’efficienza d’esecuzione, è scritto in C puro. Si divide in quattro sezioni principali: inizializzazione e logging, gestione della memoria, multithreading e sincronizzazione, gestione dei moduli. int _KERNEL_API kernelInit (struct EnvStruct *env, char *log_file) int _KERNEL_API kernelShutdown () void _KERNEL_API kernelRegisterLogger (LOG_PROC loggerProc) void _KERNEL_API kernelLog (char *msg) 7
  • 16. Architettura Al momento del bootstrap del PEUCK (a cura dell’hardware oppure di un modulo software lanciato dal Sistema Operativo ospite), il Kernel è inizializzato mediante una chiamata alla funzione kernelInit; è possibile registrare una propria funzione che ver- rà richiamata ogni qual volta sarà richiesto al kernel il “logging” di una stringa me- diante una chiamata a kernelLog. void* _KERNEL_API kernelMemAlloc (dword size) void* _KERNEL_API kernelMemRealloc (void *blk, dword size) void _KERNEL_API kernelMemFree (void *blk) La gestione della memoria del Kernel si divide in base al tipo di compilazione: in modalità Debug, l’allocatore di memoria controlla e tiene traccia di ogni singola ope- razione di allocazione e disallocazione alla ricerca di possibili blocchi di memoria la- sciati non deallocati; al momento della deallocazione di un blocco di memoria, il “mxemory tracker” controlla eventuali scritture al di fuori della zona di memoria as- segnata per fornire maggior sicurezza all’utente. int _KERNEL_API kernelStartThread (char name[], THREAD_PROC thread, int priority, void *data) int _KERNEL_API kernelStopThread (int thread) int _KERNEL_API kernelTerminateThread (int thread) int _KERNEL_API kernelSuspendThread (int thread) int _KERNEL_API kernelResumeThread (int thread) int _KERNEL_API kernelChangeThreadPriority (int thread, int priority) int _KERNEL_API kernelSleep (int ms) int _KERNEL_API kernelCreateSemaphore (int count) int _KERNEL_API kernelDestroySemaphore (int semaphore) int _KERNEL_API kernelAcquireSemaphore (int semaphore) int _KERNEL_API kernelReleaseSemaphore (int semaphore, int count) int _KERNEL_API kernelCreateTimer () int _KERNEL_API kernelDestroyTimer (int timer) int _KERNEL_API kernelStartTimer (int timer, int ms) int _KERNEL_API kernelStopTimer (int timer) int _KERNEL_API kernelSetTimerResolution (int micros) int _KERNEL_API kernelWaitForThread (int thread) int _KERNEL_API kernelWaitForTimer (int timer) Il Kernel fornisce un meccanismo di programmazione concorrente attraverso l’astrazione del concetto di thread, fornendo agli strati superiori le primitive per la creazione, distruzione, esecuzione e gestione di porzioni di codice concorrente. Il Kernel fornisce il semaforo come unica primitiva di sincronizzazione nativa: sarà compito degli strati superiori definire meccanismi di sincronizzazione più complessi. 8
  • 17. Architettura Le applicazioni grafiche/multimediali necessitano sovente di timer sui quali sin- cronizzare le operazioni: il Kernel fornisce le funzioni per la gestione di un massimo di quattro timer contemporanei. int _KERNEL_API kernelLoadModule (char module[], struct ModuleInfoStruct *info) int _KERNEL_API kernelUnloadModule (int module) int _KERNEL_API kernelGetModuleProc (int module, char name[], void **proc) Sono forniti, inoltre, i meccanismi per la gestione di moduli caricabili dinamica- mente; i driver di gestione delle periferiche sono, ad esempio, un particolare di tipo di modulo dinamico. Driver Il driver generico è un modulo a caricamento dinamico per la gestione di periferi- che quali la scheda grafica acceleratrice, le periferiche di input, la scheda di produzio- ne sonora, la scheda di rete. Un driver è tipicamente realizzato come un elenco di funzioni; fornisce, quindi, un API non ad oggetti per l’astrazione del periferico gestito. E’ suddiviso in due parti di- stinte: che si occupano della sua inizializzione, distruzione e dall’API di gestione ve- ra e propria del periferico. _PEXPORT int _INTERFACE moduleGetInfo (struct ModuleInfoStruct *info) _PEXPORT int _INTERFACE moduleInit (struct KernelAPIStruct *kernel) _PEXPORT int _INTERFACE moduleShutdown () _PEXPORT int _INTERFACE buildAPI (struct APIStruct *api) L’interfaccia esportata è comune e si occupa di fornire informazioni sul modulo (e quindi sul driver del quale è un tipo particolare), inizializzarlo, rimuoverlo dalla me- moria e costruire l’API specifica. Questo è un esempio delle informazioni fornite da un driver di rendering: struct opengl { quot;OpenGL driverquot;, quot;OpenGL driver under Win32quot;, quot;Francesco E. Carucciquot;, 0x0090, 1 } L’interfaccia specifica di ogni driver è richiesta mediante una chiamata a buildAPI e varia con il variare del tipo di driver. 9
  • 18. Architettura Driver di Rendering Il driver di rendering espone agli strati superiori un’API generica per la trasforma- zione, l’illumanizione e il disegno di primitive tridimensionali. Crea, inoltre, il conte- sto di visualizzazione adatto alla risoluzione e alla profondità di colore richieste dall’utente. La libreria di rendering astratta, si basa sul modello della macchina a stati: un’insieme di variabili impostabili dall’esterno, stabilisce lo stato di rendering corren- te sul quale si baseranno le operazioni di disegno delle primitive; alcuni esempi di va- riabili di stato sono il materiale corrente, che contiene le informazioni usate durante la fase di illuminazione, il buffer di vertici corrente, la texture di dettaglio corrente, il colore della luce ambiente, il colore di sfondo. Altri stati contengono le informazioni che riguardano particolari modalità di disegno quali il “disegno a fil di ferro”. L’interfaccia del driver di rendering generico presenta per prime le funzioni per la sua inizializzione. int _DRIVER_API renInit (struct PeuckContext const *context) int _DRIVER_API renStart (struct HostWinStruct *window, int w, int h, int bpp, bool full- screen) int _DRIVER_API renStop (int arena) int _DRIVER_API renSetCurrent (int arena) int _DRIVER_API renShutdown () L’applicazione inizializza il driver di rendering fornendo a renInit il contesto cor- rente all’interno del quale il PEUCK è in esecuzione. La struttura dati PeuckContext contiene informazioni specifiche dell’ambiente operativo ed è inizializzata durante il bootstrap del PEUCK; in ambiente Win32, ad esempio, la struttura contiene un’istanza dell’applicazione che ospita il Sistema Operativo: struct PeuckContext { HINSTANCE hInstance; // application process instance }; L’apertura di un contesto di rendering (chiamato arena di rendering) avviene me- diante una chiamata a renStart, fornendo una struttura riservata di nome HostWin- Struct, il cui contenuto dipende dal particolare tipo di ambiente operativo e può esse- re l’identificativo di una finestra sistema, come di un particolare monitor per applica- zioni a tutto schermo (specificate inoltre dall’appisito parametro fullscreen). La di- mensione dell’arena di rendering e la profondità di colore desiderata, completano le informazioni necessarie all’inizializzazione dell’arena di rendering, la cui chiusura è richiesta invocando renStop. L’arena di rendering oggetto del disegno è selezionata mediante una chiamata a renSetCurrent. La fine delle operazioni e la distruzione del- le strutture interne al driver di rendering è segnalato da renShutDown. 10
  • 19. Architettura int _DRIVER_API renSetCurrentViewport (struct ViewportStruct *viewport) int _DRIVER_API renSetCurrentCamera (float camera[]) int _DRIVER_API renClear () int _DRIVER_API renBeginScene () int _DRIVER_API renEndScene () int _DRIVER_API renSwap () Dopo aver informato il driver dell’inizio del disegno delle primitive che compon- gono una scena con renBeginScene, l’applicazione riempie lo schermo con un colore predefinito mediante renClear. I passi per disegnare una scena iniziano con la selezione di una viewport corrente (renSetCurrentViewport) per la visualizzazione. Una viewport è definita da una porzione bidimensionale sullo schermo caratterizzata dalla posizione, dalla dimensio- ne, da un livello di trasparenza che permetta di visualizzare viewport sovrapposte e da una matrice di proiezione, che indichi il tipo di visualizzazione richiesto (prospettico, ortogonale). La funzione renSetCurrentCamera seleziona la telecamera mediante la quale le successive primitive devono essere disegnate. La scena si chiude con una chiamata a renEndScene. Al fine di mantenere un’immagine visualizzata stabile ed evitare le distorsioni do- vute all’effetto della rintraccia verticale del monitor, il driver di rendering disegna la scena su un porzione di memoria video non correntemente visualizzata. Al termine del disegno di un “frame di animazione”, il driver è forzato a visualizzare la zona conte- nente la nuova scena chiamando la funzione renSwap. L’uso della tecnica del “dou- ble buffering” non può essere disabilitato. int _DRIVER_API renUseMaterial (struct MaterialStruct *material) int _DRIVER_API renSetAmbientLight (float color[]) int _DRIVER_API renUseLight (int pos, struct Light3DStruct *light) int _DRIVER_API renBlockVertexBuffer (struct VertexBufferStruct *buffer) int _DRIVER_API renUnblockVertexBuffer (struct VertexBufferStruct *buffer) int _DRIVER_API renUseVertexBuffer (struct VertexBufferStruct *buffer) int _DRIVER_API renTransformVertexBuffer (float matrix[], int stage) Il materiale corrente (renUseMaterial) descrive le proprietà di diffusione, specula- rità e emissione delle primitive secondo l’equazione di Blinn per l’illuminazione che sarà descritta in seguito. Un massimo di 8 luci e di una luce ambiente possono essere rese operative correntemente mediante renUseLight e renSetAmbientLight. Il disegno delle primitive si basa sulla selezione di un insieme di vertici corrente: le informazioni riguardanti un vertice comprendono le sue coordinate nello spazio dell’oggetto in cui è definito, la base ortonormale che ne definisce uno spazio, l’insieme di coordinate nello spazio delle texture ed eventualmente un insieme di pesi 11
  • 20. Architettura da usare in caso di trattamento delle deformazioni durante animazioni scheletriche. Gli insiemi di vertici (Vertex Buffer) si dividono in due categorie: vertici dinamici e statici. Le caratteristiche di un insieme di vertici dinamici possono essere modificate durante l’esecuzione per ottenere effetti quali morphing e ritassellazione di superfici curve. Un insieme di vertici statico, al contrario, non può essere modificato durante l’esecuzione e viene indicato come tale mediante la chiamata alla funzione renBlo- ckVertexBuffer, che suggerisce al driver la possibilità di effettuare ottimizzazioni particolari in presenza di un buffer statico. La versione corrente del driver di rende- ring, ad esempio, copia i dati relativi ad un insieme di vertici statici all’interno della memoria video della scheda grafica, in modo da minimizzare il traffico dalla memoria di sistema e velocizzare enormemente il disegno delle primitive. int _DRIVER_API renDownloadTexture (struct TextureStruct *texture, int compress) int _DRIVER_API renUnloadTexture (struct TextureStruct *texture) int _DRIVER_API renUseTexture (struct TextureStruct *texture, float *matrix, int stage) Per aumentare il realismo delle scene, una tecnica comune consiste nell’associare alle primitive un’immagine (detta texture) che descriva i dettagli fini della superficie. Una texture è, solitamente, una bitmap mappata dallo spazio delle texture allo spazio dell’oggetto (da qui il termine texture map); ogni vertice contiene le coordinate nello spazio delle texture alle quali l’applicazione può associare una matrice di trasforma- zione. Il driver di rendering richiede che ogni texture sia trasferita all’hardware prima del suo uso (renDownloadTexture); la texture può eventualmente essere compressa all’interno della memoria video (se l’operazione è supportata dall’hardware) con lieve perdita di dettaglio visivo, al fine di ottimizzare l’occupazione di memoria e la banda passante verso la scheda grafica. Le texture tendono, infatti, ad occupare una notevole quantità di memoria soprattutto se altamente dettagliate. La texture corrente è specifi- cata dal comando renUseTexture. int _DRIVER_API renDrawTriangles (word v[], int count) int _DRIVER_API renDrawTriangleStripe(word v[], int count) Il driver di rendering supporta il disegno di primitive composte da un’insieme di triangoli (renDrawTriangles) oppure da una striscia (renDrawTriangleStripe). I triangoli sono specificati come indici ai vertici presenti nel VertexBuffer corrente. Non è supportato il disegno di soli punti, linee, strisce di linee oppure ventagli di triangoli (GR_TRIANGLE_FAN). 12
  • 21. Architettura GR_POINTS GR_LINES GR_TRIANGLES GR_LINE_STRIP GR_TRIANGLE_STRIP GR_TRIANGLE_FAN Figura 2. Primitive grafiche 13
  • 22. Architettura PEUCK Il Peuck Object Mode (POM) Il Peuck Object Model rappresenta il cuore della gerarchia di classi dell’intero Si- stema Operativo; dichiara, infatti, la classe base della gerarchia dalla quale eredita o- gni classe dell’ambiente. Realizza, inoltre, un sistema di riconoscimento dei tipi a tempo di esecuzione (RTTI), mediante una gerarchia di metaclassi; ad ogni classe del- la gerarchia è possibile associare una metaclasse che contiene le informazioni che la riguardano (nome, dimensione, codice identificativo, puntatore alla metaclasse asso- ciata alla classe padre). PClass PClass (char name[], int size, int code, void *parent, void *func) BaseClass* createObjectPtr () const BaseClass& createObject () const bool isDerivedFrom (PClass const& aClass) const char m_name [21] int m_size int m_code PClass* m_parent PClass* m_next BaseClass* (*m_creator )() Ogni oggetto che vive all’interno dell’ambiente, ha associato un puntatore (unico per tutti gli oggetti istanza di una classe) alla propria metaclasse di appartenenza. A partire da una metaclasse, l’applicazione può creare un nuovo oggetto (vuoto) istanza della particolare classe oppure conoscere la lista delle metaclassi associate alle classe parenti. La gerarchia di classi esportata dal POM e’ mostrata in figura. La classe base dell’intera gerarchia del Sistema Operativo è BaseClass. Il suo compito è fornire gli strumenti base di un qualsiasi oggetto dell’ambiente: è possibile conoscere la validità dell’oggetto mediante una chiamata al metodo isValid oppure conoscerne il tipo con un istruzione del tipo: a.isA(_pclass(A)); La macro _pclass restituisce la metaclasse associata alla classe richiesta. L’ultimo compito della classe base è fornire i metodi standard per l’allocazione e la disallocazione di un oggetto (new and delete), che useranno le funzione del kernel per la gestione della memoria di sistema. 14
  • 23. Architettura BaseClass BaseClass () BaseClass* getThis () bool isValid () const PClass& getParentClass () const const char* getClassName () const int getClassCode () const bool isA (PClass const& aClass) const void* _PCDECL operator new (size_t size) void _PCDECL operator delete (void *ptr) void* _PCDECL operator new[] (size_t) void _PCDECL operator delete[] (void*) Le macro _DECLARE_CLASS e _IMPLEMENT_CLASS aggiungono una classe al sistema di riconoscimento del tipo a tempo d’esecuzione. Il meccanismo di persistenza degli oggetti del Sistema Operativo si basa sulla clas- se Persistent (ereditata da BaseClass), che esporta i metodi riguardanti l’immagazzinamento e il recupero dell’oggetto (load and store). L’ultima classe nella gerarchia del Peuck Object Module è String che fornisce una classe generale per la gestione delle stringhe (concatenazione, copia, ricerca) attraver- so il conteggio delle referenze. Due stringhe uguali condividono la medesima zona di memoria: il conflitto è risolto ricorrendo ad una variabile che conta il numero di strin- ghe che si riferiscono alla medesima copia. La class String si appoggia all’unica clas- se esterna alla gerarchia all’interno dell’ambiente: RefCount. Il POM fornisce, inoltre, i puntatori intelligenti (smart pointer): dichiarando un puntatore intelligente ad una classe ereditata da RefCount, esso, pur comportandosi come un comune puntatore, si occupa autonomamente del conteggio delle referenze agli oggetti e della loro deallocazione in caso di cessato utilizzo. 15
  • 24. Architettura Persistenza degli oggetti Il PEUCK propone un sistema di persistenza degli oggetti alternativo e proprieta- rio, basandosi sui servizi offerti dal Peuck Object Model, in particolare sul sistema di riconoscimento del tipo a tempo d’esecuzione. La classe cardine alla base della persi- stenza è PeuckStream, che realizza un generico flusso d’oggetti bidirezionale: la classe FileStream ad esempio realizza in concreto il flusso d’oggetti verso un file memorizzato sui supporti di massa dell’ambiente ospite. PStream void init () void shutdown () void registerClass (POM::PClass *aClass) void unregisterClass (POM::PClass *aClass) const POM::PClass* findClass (int code) const POM::PClass* findClass (char name[]) Al momento del bootstrap del PEUCK, il sistema di persistenza degli oggetti è ini- zializzato (init) e ogni classe della gerarchia è registrata automaticamente attraverso un codice univoco universale. L’applicazione può registrare nuove classi aggiungendo un codice univoco alla macro _IMPLEMENT_CLASS. Il metodo findClass ricerca una metaclasse all’interno della gerarchia di classi a partire dal codice univoco oppure da una stringa contenente il nome con la quale la classe è stata registrata nel sistema. void store (const POM::Persistent *obj) void store (const POM::Persistent& obj) POM::Persistent* load (void *arg = 0) L’applicazione memorizza un oggetto all’interno del flusso mediante il metodo store e lo recupera mediante il metodo load. Il metodo store riconosce al momento della sua esecuzione il tipo dell’oggetto da memorizzare, scrive nel flusso il codice univoco e invoca il metodo ereditato da Per- sistent per la scrittura del contenuto dell’oggetto all’interno del flusso. Le operazioni compiute dal metodo load, durante il caricamento sono leggermente più complicate; dal flusso di dati è estratto il codice che identifica univocamente una classe all’interno della gerarchia. Il codice è usato dal metodo statico findClass per cercare all’interno della gerarchia di classi la metaclasse corrispondente; la metaclasse 16
  • 25. Architettura è in grado di creare istanze “vuote” della classe alla quale corrisponde: all’oggetto “vuoto” è chiesto di “riempirsi” con i restanti dati estratti dal flusso. Ogni classe i cui oggetti debbano essere persistenti è responsabile della scrittura e dell’estrazione dei suoi dati dal flusso scrivendo il codice dei metodi store e load ere- ditati da Persistent e si appoggia, per svolgere il suo compito, ai metodi di scrittura e lettura generici di PeuckStream. void write (byte *data, int len) void write (void *data, int len) void write (int x) void write (byte b) void write (char c) void write (long l) void write (bool b) void write (float f) void read (byte *data, int len) void read (void *data, int len) int readInt () byte readByte () char readChar () long readLong () bool readBool () float readFloat () La classe PeuckStream fornisce i metodi per la scrittura e la lettura dei tipi di dati standard, sui quali appoggiarsi per la gestione di strutture dati complesse. 17
  • 26. Architettura PEUCK3D Trasformazioni, viewport e telecamera Trasformazioni Le trasformazioni assumono notevole importanza nella grafica realizzata al compu- ter e sono parte integrante del processo di generazione di un’immagine. Se all’interno di una scena sono, ad esempio, presenti due oggetti di forma identica ma in posizione diversa, solo uno dei due necessita di essere costruito, il secondo è ottenuto sempli- cemente copiando il primo e applicando una trasformazione per portarlo nella giusta posizione. Operazioni quali la traslazione, la rotazione, lo scolamento sono dette tra- sformazioni geometriche. Una trasformazione geometrica è una funzione f che lavora su punti. La notazione P = f(P) implica che l’applicazione di f al punto P porta al punto trasformato P*. Dal * momento che le trasformazioni trattate in computer grafica sono chiamate geometri- che, devono avere un’interpretazione geometrica: non tutte le trasformazioni sono quindi utili. Le funzioni utili geometricamente devono soddisfare le due seguenti pro- prietà. Una generica funzione f mappa il suo dominio D in C. Se ogni punto di C ha un punto corrispondente in D, allora la funzione mappa l’intero dominio di in C. La funzione è detta suriettiva Una funzione arbitraria può mappare due punti distinti x e y sullo stesso punto. Una funzione iniettiva soddisfa la proprietà x != y -> f(x) != f(y). E’ sensato richiedere che una trasformazione geometrica sia iniettiva, in quan- to è possibile costruire la trasformazione inversa di un dato punto P* facen- te parte dall’immagine trasformata di un solo punto. Definizione. Una trasformazione geometrica è una funzione contemporaneamente suriettiva e iniettiva, il cui dominio e condominio sono punti. La combinazione di trasformazioni è un’operazione fondamentale e si riduce alla composizione di funzioni. Se due funzioni f e g rappresentano due trasformazioni, la loro composizione g o f rappresenta il prodotto (combinazione, concatenazione) delle due trasformazioni. E’ possibile scrivere la trasformazione composta nella seguente forma: P* = g(f(P)). 18
  • 27. Architettura Un esempio importante di concatenazione di trasformazioni geometriche sono le trasformazioni lineari che mappano il punto P = (x, y, z) nel punto P* = (x*, y*, z*), dove: x* = a11x + a12y + a13z + a14 y* = a21x + a22y + a23z + a24 [1] z* = a31x + a32y + a33z + a34 Questo tipo di trasformazione è anche detto affine. I termini ai4 rappresentano le quantità che sono sommate alle coordinate trasforma- te e rappresentanto semplicemente la traslazione di P* lungo gli assi coordinati. Il si- stema precedente può essere semplificato in: x* = a11x + a12y + a13z y* = a21x + a22y + a23z [2] z* = a31x + a32y + a33z Se la matrice dei coefficienti 3x3 del sistema di equazioni è non singolare o, equi- valentemente, se il determinante della matrice dei coefficienti è diverso da zero, allora il sistema è invertibile e può essere espresso nella forma x = b11x*+ b12y*+ b13z* y = b21x*+ b22y*+ b23z* [3] z = b31x*+ b32y*+ b33z* dove i coefficienti bij sono espressi in termini dei coefficienti aij. Questa proprietà tornerà utile nella gestione della telecamera. Maggiori dettagli sulle trasformazioni possono essere trovati in [2]. Viewport e proiezioni Data una scena della quale mediamente solo una parte può essere visualizzata, è possibile pensare allo schermo come ad una finestra sull’immagine. Questa visione introduce il problema della trasformazione delle coordinate. Si chiami la scena mondo (world). Un punto in coordinate mondo (x, y, z) per essere visualizzato all’interno di una finestra, che chiameremo viewport d’ora in poi, deve essere proiettato e trasfor- mato in coordinate viewport. Una viewport può, inoltre, coprire solo una porzione del- lo schermo; il punto deve quindi essere ulteriormente scalato e traslato dalle coordina- te viewport alle coordinate schermo. 19
  • 28. Architettura Una proiezione è, in generale, una trasformazione da uno spazio1 con dimensione n a uno spazio con dimensione n-1. Le proiezioni sono classificate in lineari e non linea- ri. I due principali tipi di proiezioni lineari solo la parallela e prospettica. Il PEUCK supporta nativamente matrici solo proiettive. La proiezione prospettica è importante in quanto è il modo in cui l’uomo osserva gli oggetti nella vita reale. Questo tipo di proiezione è ottenuta posizionando l’osservatore nel centro di proiezione. Sappiamo dall’esperienza quotidiana che la proiezione prospettica ha le seguenti tre proprietà: • Maggiore è la distanza dell’oggetto dall’osservatore, più esso appare picco- lo • Tutte le linee dell’oggetto parallele alla linea di vista dell’osservatore ap- paiono convergere a ciò che è chiamato punto di fuga. • La quantità di prospettiva vista da un osservatore dipende dalla distanza fra l’osservatore e l’oggetto; un osservatore vicino vede più prospettiva, ovvero una differenza di dimensioni maggiore fra la porzione dell’oggetto più vici- na e piu’ lontana. Assumiamo che l’osservatore sia posizionato lungo l’asse z negativo a distanza k dall’origine e guarda nella direzione dell’asse z positivo. Assumiamo, inoltre, che il piano di proiezione (lo schermo sul quale l’immagine bidimensionale sarà disegnata) sia il piano xy. Sia P = (x, y, z) un punto tridimensionale e P*=(x*, y*) il punto tra- sformato sul piano di proiezione. La regola della proiezione prospettica afferma che per ottenere P*, è necessario tirare una linea dal punto P al centro di projezione (l’osservatore); il punto in cui questa linea interseca il piano di proiezione è P*. x* x = z+k k [4] * y y = z+k k perciò 1 Sebbene le denominazioni “spazio del mondo”, “spazio dell’oggetto” e “spazio della telecamera” non siano formalmente corrette, saranno usate all’interno di questo lavoro per questione di chiarezza perché comunemente accettate. Sia chiaro che esiste un solo spazio tridimensionale e diversi sistemi di rifelrimento (mondo, oggetto, telecamera). 20
  • 29. Architettura x x* = [5] (z + k) +1 y y* = (z + k) +1 Possiamo ora provare le tre proprietà della proiezione prospettica precedentemente citate: a) Quando l’oggetto è lontano, l’equazione produce piccoli valori per x* e y*. lim x* = 0, lim y* = 0. z->infinito z->infinito Percio’ l’oggetto appare più piccolo all’osservatore. b) Sia data una linea parallela all’asse z (la linea di vista). Tutti i punti su questa linea hanno le stesse coordinate x e y e differiscono solo per la z. Quando que- sta linea è estesa all’infinito, la sua proiezione sullo schermo si avvicina al pun- to (0, 0) senza che le coordinate x e y possano influire. Questo mostra che tutte le linee parallele all’asse z hanno una proiezione che converge all’origine. L’origine è quindi il punto di fuga di queste linee. c) Scegliendo due punti P1=(x1, y1, z1) e P2=(x1, y1, z2) sull’oggetto (con le stesse coordinate x e y) e considerando le loro proiezioni P1*=(x1*, y1*,) e P2*=(x1*, y1*,), il rapporto x1*/x2* è così calcolato: x1 z / k + 1 z2 / k + 1 d 2 * x =1 = = 1 [6] x2 z1 / k + 1 d 1 * x 2 z2 / k + 1 Quando l’oggetto e l’osservatore si allontanano l’uno dall’altro, sia d1 sia d2 crescono, portando il rapporto x1*/x2* vicino a 1. I due punti proiettati si avvici- nano, quindi, sullo schermo, il che sta a significare che l’oggetto è proiettato sullo schermo con minore prospettiva. 21
  • 30. Architettura La matrice di trasformazione (non affine) che genera la trasformazione prospettica è: 1 0 0 0   0 1 0 0 0 r 0 1   0 1 0 0   dove r = 1 / k. Si assuma, ora, che la finestra attraverso la quale il mondo proiettato è visualizzato, occupi il rettangolo (Wxl, Wxr, Wyb, Wyt) mentre la viewport occupi sullo schermo il rettangolo (Vxl, Vxr, Vyb,Vtr). Per trasformare il punto proiettato (Xw, Yw) nel punto di coordinate schermo (Xs, Ys) si procede attraverso i seguenti tre passi: 1. Si calcola la distanza fra il punto e la l’angolo in basso a sinistra della fine- stra: Xw – Wxl e Yw – Wyb. 2. Si scala le distanze della dimensione relativa dell viewport V −V A= xr x1 ( X w − Wx1 ) Wxr −Wx1 [7] V −V B= yt yb (Yw − Wyb ) Wyt −W yb 3. Si aggiungono le coordinate dell’angolo in basso a sinistra della viewport Xs = A + Wxl [8] Ys = B + Vyb Da questo si ricava la matrice che trasforma i punti dalle coordinate della vie- wport alle coordinate dello schermo:  a 0 0   ( X s , Ys ,1)= ( X w , Yw ,1) 0 c 0  [9]  m n 1   22
  • 31. Architettura Dove a, c, m, n sono coefficienti calcolati una sola volta al momento di inizia- lizzare la viewport. Telecamera E’ stato fino ad ora assunto che i punti fossero trasformati in un sistema di coordi- nate statico. Una telecamera, al contrario, introduce una trasformarmazione del siste- ma di coordinate piuttosto che dei singoli punti. Si consideri, ad esempio, una sempli- ce traslazione e un punto P trasformato in un punto P* traslandolo di m e n unità ri- spettivamente lungo gli assi x e y. La trasformazione può essere invertita in uno dei seguenti due modi: 1. Si supponga che la trasformazione originale sia P* = PT, dove  1 0 0   T=  0 1 0    m n 0 è semplice ricavare la matrice di trasformazione che porta il punto P* indietro al punto P 1 0 0   S=  0 1 0  − m − n 0   è altrettanto semplice dimostrare che S è la matrice inversa di T. 2. La trasformazione può essere invertita traslando il sistema di coordinate nella direzione inversa (di –m e –n unità) usando una (ancora sconosciuta) matrice di trasformazione M. Dal momento che entrambi i metodi producono il medesimo risultato, si può con- cludere che M = S = T-1. La trasformazione degli assi coordinati è perciò effettutata dalla matrice che è l’inversa della matrice che trasforma i punti. Questò è vero per o- gni trasformazione affine, non solo per le traslazioni. 23
  • 32. Architettura E’ quindi possibile introdurre, inoltre, un orientamento della telecamera che defini- sce il cambio di sistema di riferimento dallo spazio del mondo allo spazio della tele- camera successivamente proiettato nello spazio della viewport e, infine, dello schermo per la visualizzazione, sicuri che l’operazione possa sempre essere invertita per torna- re dallo spazio della telecamera allo spazio del mondo. 24
  • 33. Architettura Realizzazione Una scena è divisa in uno o più mondi, contenenti gli oggetti da visualizzare e vi- sualizzati attraverso una o più viewport. Una viewport consiste in una finestra sullo schermo che visualizza uno e un solo mondo attraverso una delle telecamere associa- te. La viewport si occupa di creare e gestire la matrice di proiezione e i parametri ad essa associati. Figura 3. Diagramma di Viewport Viewport Viewport () Viewport (float x, float y, float w, float h, int mode = 0) virtual ~Viewport () void setPosition (float x, float y) virtual void setDimension (float w, float h) virtual void setAspectRatio (float ratio) void setAlpha (float alpha) void setOverlapped (int type) float x () const float y () const float width () const float height () const int priority () const float aspectRatio () const float invAspectRatio () const float screenXUnit () const float screenYUnit () const MATH::Point3D project (MATH::Point3D& point) const const ViewportStruct* data () const virtual void makeCurrent () = 0 virtual void render () = 0 25
  • 34. Architettura La classe Viewport astrae il concetto di finestra generica attraverso la quale osser- vare una scena che può essere bidimensionale o tridimensionale. Al momento della costruzione della viewport, si specificano la sua posizione e la sua dimensione sullo schermo in percentuale. Una viewport posizionata, ad esempio, in (0.1, 0.2) su uno schermo di dimensione 800 x 600 pixel, si troverà a 80 pixel dal bordo sinistro e 120 pixel dal bordo in alto. I metodi setPosition() e setDimension() modificano la posizione e la dimensione della finestra durante l’esecuzione. SetAspectRatio() stabilisce il rapporto desiderato fra x e y; l’aspect ratio è automaticamente calcolato dalla viewport come rapporto fra la sua larghezza e la sua altezza: per mantenere un aspetto dell’immagine uniforme anche su viewport di rapporto differente, l’utente può specificare un proprio valore differente da quello calcolato in automatico. Si immagini di avere, ad esempio, una viewport di larghezza 100 e altezza 200, il cui aspect ratio calcolato automaticamente è 0.5, su uno schermo 800x600 (aspect ratio 0.75); l’utente può forzare l’aspect ratio della viewport a 0.75 per mantenere una visualizzazione non deformata (cerchi non visualizzati come ellissi ad esempio). Il metodo setOverlapped() informa la viewport che essa può sovrapporsi ad altre viewport; può, inoltre, essere associata con setAlpha() una trasparenza globale per ef- fetti quali una barra di stato semi trasparente per la visualizzazione di informazioni particolari o una mappa sovrimposta alla scena. I metodi x(), y(), width(), height(), priority(), aspectRatio(), invAspectRatio(), screenXUnit(), screenYUnit() permettono l’accesso ai campi interni della viewport. I metodi screenXUnit() e screenYUnit(), in particolare, restituiscono il fattore di con- versione dall’unità di misura della viewport all’unità di misura dello schermo. Il me- todo project() applica la matrice di proiezione ad un punto tridimensionale effettuan- do la proiezione dallo spazio della telecamera allo spazio della viewport. L’ultimo metodo di accesso data() restituisce un puntatore costante alla struttura che contiene i dati della viewport (ViewportStruct): i dati contenuti all’interno della struttura pos- sono essere letti, ma non modificati dall’esterno della classe Viewport. ViewportStruct float x float y float w float h float hither float yon float alpha 26
  • 35. Architettura float projection [4][4] int type Dove x, y, w e h sono i valori della posizione e della dimensionae della viewport validi per una finestra generica, i campi hither e yon rappresentano la distanza dei piani di clipping perpendicolare al piano di proiezione che definiscono i valori in z minimo e massimo degli oggetti visualizzabili; questi valori hanno, quindi, senso solo per viewport associate a scene tridimensionali. La matrice di proiezione dallo spazioe della telecamera allo spazio della viewport è contenuta nel campo projection. Gli ultimi due metodi astratti della classe Viewport definiscono l’interfaccia d’uso: makeCurrent() informa il driver che la viewport è la finestra corrente in cui disegna- re gli oggetti, il metodo render() disegna il contenuto della viewport. Viewport3D Viewport3D () Viewport3D (float x, float y, float w, float h, int mode = 0) virtual ~Viewport3D () void setZLimits (float minz, float maxz) virtual void makeCurrent () virtual void render () void useCamera (Camera& camera) Camera& camera () const void addClipPlane (PEUCK::MATH::Plane3D& plane) void disableClipPlane (int nplane) virtual void setWorld (World3D& world) World3D const& world () const float viewingDistance () const float invViewingDistance () const La classe Viewport3D estende il concetto di viewport per supportare la visione di mondi tridimensionali: ogni viewport tridimensionale può avere associato un solo mondo tridimensionale attraverso la chiamata al metodo setWorld(); il metodo use- Camera() specifica la telecamera attraverso la quale il mondo deve essere visualizza- to. Il metodo setZLimitz() specifica la distanza dei piani di clipping più vicino e più lontano che, assieme ai restanti 4 piani calcolati dai valori della dimensione e dal fov (field of view) specificato dalla telecamera corrente, definiscono il tronco di piramide 27
  • 36. Architettura che delimita la porzione di spazio visibile. E’ possibile specificare fino ad altri sei piani di clipping supplementari invocando il metodo addClipPlane(); l’uso di questo metodo è, tuttavia, sconsigliato in quanto introduce notevoli rallentamenti nella fase di rendering delle primitive portanto ad un sensibile decadimento delle prestazioni. Gli ultimi due metodi restituiscono l’informazione riguardante la distanza del pun- to di vista dal piano di proiezione e il suo inverso (viewingDistance() e invViewin- gDistance()). Figura 4. Gerarchia della telecamera La classe astratta Camera realizza il concetto di telecamera che, per essere usata, deve necessariamente essere inclusa all’interno di un mondo tridimensionale da visua- lizzare (classe World3D). L'utente segnala alla Viewport3D corrente, la telecamera tramite la quale il mondo assegnato alla viewport è visualizzato. Il fov (field of view) della telecamera definisce, assieme ai campi della classe Viewport3D, il viewing fru- stum tridimensionale, ovvero la porzione di spazio da visualizzare. Il fov rappresenta geometricamente l'angolo d’apertura della camera ed è specificato in gradi. Camera Camera () Virtual ~Camera () virtual void evaluateGlobalMatrix () = 0 virtual void setFoV (float angle) virtual void setFoV (int angle) Float tfov () Float fov () float focus () virtual Vector3D const& position () const virtual Matrix const& matrix () const 28
  • 37. Architettura Il calcolo effettivo della matrice di trasformazione è effettuato al momento dell’invocazione del metodo evaluateGlobalMatrix() che del quale le classi derivate forniranno un realizzazione concreta. Il metodo setFovAngle() specifica il valore espresso in gradi del field of view tra- mite un numero reale oppure un numero intero, valore che può essere recuperato in- vocando i metodi tfov() (tangente del fov) e fov() (fov espresso in gradi). Il metodo focus() restituisce un valore direttamente proporzionale alla distanza del punto di vista dal piano di proiezione che rappresenta l’unità di misura del mondo. Il metodo position() restituisce la posizione della telecamera espresso nello spazio del mondo. L’ultimo metodo matrix() restutisce la matrice di trasformazione dallo spazio del mondo allo spazio della telecamera. StandardCamera StandardCamera () ~StandardCamera () void translate (float x, float y = 0.0f, float z = 0.0f) void setPosition (float x, float y = 0.0f, float z = 0.0f) void setPosition (Point3D const& p) void setRotation (float x, float y = 0.0f, float z = 0.0f) void rotateX (int rx) void rotateY (int ry) void rotateZ (int rz) void rotateX (float rx) void rotateY (float ry) void rotateZ (float rz) void lookAt (Vector3D const& eye, Vector3D const& up = Vector3D(0.0f, 1.0f, 0.0f)) void lookAt (OBJECT3D::Object3D const& obj, Vector3D const& up = Vector3D(0.0f, 1.0f, 0.0f)) void unlockLookAt () virtual void evaluateGlobalMatrix () virtual Matrix const& matrix () const virtual Vector3D const& position () const Moveable const& moveable () const La StandardCamera è la realizzazione concreta della telecamera fornita dal PEUCK che usa le informazioni di movimento fornite da un’istanza della classe Mo- veable. 29
  • 38. Architettura La posizione e la rotazione iniziale della telecamera possono essere modificati in- vocando i metodi setPosition() e setRotation(); traslate() e i metodi relativi alla ro- tazione attorno agli assi, al contrario, specificano un valore dipendente dalla posizione o dagli angoli di rotazione precedenti. I metodi lookAt() e unlockLookAt() dirigono l’inquadratura della telecamera ver- so un punto specifico nello spazio del mondo che può essere specificato indipenden- temente come un vettore oppure un oggetto. L’ultimo metodo moveable() restituisce un handle costante all’oggetto responsabi- le dell’orientamento della telecamera. Figura 5. Gerarchia di class Moveable. La classe Moveable gestisce il movimento e l’orientamento delle entità del PEUCK: può essere considerata una classe che astrae il concetto di matrice affine ge- nerica di passaggio da un sistema di riferimento ad un altro. E’ usata, ad esempio, per gestire il cambio di coordinate dallo spazio dell’oggetto allo spazio del mondo (nel caso degli oggetti) o dallo spazio del mondo. L’orientamento durante il cambio di ba- se è gestito mediante l’uso del metodo degli angoli euleriano (classe EulerianOrien- table) o dei quaternioni (classe QuatOrientable). Moveable Moveable () Moveable (Vector3D& position, Matrix& matrix) Moveable (Matrix& matrix) Moveable (Vector3D& position) 30
  • 39. Architettura virtual ~Moveable () Matrix const& self () const Vector3D const& position () const Matrix const& matrix () const Vector3D const& worldPosition () const virtual void useHierarchicalMatrix () virtual void setRotation (int x, int y, int z) virtual void setRotation (float x, float y, float z) virtual void setPosition (float x, float y, float z) virtual void setPosition (Point3D const& p) virtual void translate (float x, float y, float z) virtual void rotateX (float rx) virtual void rotateY (float ry) virtual void rotateZ (float rz) virtual void rotateX (int rx) virtual void rotateY (int ry) virtual void rotateZ (int rz) virtual void lookAt (Vector3D const& eye, Vector3D const& up) virtual void unLockLookAt () virtual bool nextPosition () virtual void updatePosition () virtual void updateRotation () virtual bool buildMatrix () virtual bool buildHierarchicalMatrix () virtual void buildHierarchicalMatrix (Matrix& parent) Molti metodi (per lo spostamento, la rotazione e il calcolo delle matrici) sono ana- loghi ai metodi incontrati nell’analisi della classe StandardCamera in quanto usati “per delegazione” ogni qual volta è necessario costruire matrici di cambio base a se- guito di rototraslazioni. La classe Moveable è in grado di gestire concatenazioni di cambiamenti di basi ge- rararchiche attraverso i metodi useHierarchicalMatrix() e buildHierarchicalMa- trix() utile, ad esempio, nella realizzazione delle animazioni gerarchiche di un ogget- to. 31
  • 40. Architettura Modello d’illuminazione Interpretazione matematica Il PEUCK usa un’illuminazione semplificata rispetto a modelli realistici quali il Ray Tracing e il Radiosity, che basano il calcolo sulle proprietà fisiche dell’interazione fra luce e materiali. Gli algoritmi “foto realistici” sono, al livello at- tuale della tecnologia, troppo onerosi perché siano realizzati in applicazioni grafiche in tempo reale, perché alla loro base è un’enorme quanitità di calcoli. Il modello usato calcola l’illuminazione al livello del singolo vertice: per ogni ver- tice presente nella scena, è valutata l’equazione del modello di Phong I = Ia + Id + Is [10] L’intensità della luce “vista” da ogni vertice è data dal contributo di tre componen- ti: componente ambientale, componente diffusa, componente speculare. La componente ambientale è propria di ogni oggetto a prescindere dalle luci pre- senti nella scena; si tratta di un’approssimazione dell’intensità luminosa presente nell’ambiente senza una particolare collocazione spaziale. E’ usata per dare agli og- getti un’illuminazione minima, priva di sfumature, costante e, per questo, poco reali- stica. La componente ambientale si valuta mediante la seguente espressione: Ia = KaLa [11] Dove Ka è il coefficiente ambientale specifico del materiale associato al vertice e indica la quantità di luce ambiente non assorbita (e quindi visibile) dall’oggetto. La è l’intensità della luce ambientale costante per tutti i vertici presenti nella scena. Gli oggetti sottoposti alla sola componente ambientali ricevono un’illuminazione uniforme lungo le loro superfici. La componente diffusa simula l’illuminazione for- nita da una sorgente luminosa puntiforme; l’intensità luminosa sull’oggetto varia in dipendenza della direzione d’incidenza della sorgente luminosa. L’intensità della componente diffusa, anche detta riflessione Lambertiana, appare costane da ogni an- golo visuale perché riflette la luce con eguale intensità in tutte le direzioni. Per una data superificie, la luminosità dipende solo dall’angolo Φ fra la direzione della luce L e la direzione della normale N. 32
  • 41. Architettura La componente diffusa si valuta per ogni luce presente nella scena mediante la se- guente espressione: I d = K d Ld ( N o L) [12] Dove Kd è il coefficiente di diffusione specifico del materiale, che rappresenta la quantità di luce diffusa non assorbita dalla superficie. Ld rappresenta l’intensità della luce. N è il vettore normale alla superficie che, in generale, varia da vertice a vertice. L è il vettore direzione che porta dalla luce al vertice: il valore di questo vettore è co- stante in ogni vertice per luci poste all’infinito (luci direzionali), mentre varia per luci non all’infinito. La componente speculare può essere osservata su qualunque superficie riflettente e simula il riflesso della sorgente luminosa sull’oggetto. L’intensità della componente varia al variare della posizione dell’osservatore ed è, quindi, dipendente dalla posizio- ne del punto di vista. La forma generale dell’espressione che valuta la componente speculare è: I s = K s Ls ( R o L) ns [13] Ks è l’analogo coefficiente che si trova nella componente ambientale e diffusa. Ls è l’intensita speculare della sorgente luminosa (o colore speculare della luce): per avere effetti realistici, Ls e Ld sono tipicamente uguali per ogni luce presente nella scena. R è il vettore simmetrico della direzione V del punto di vista secondo la nor- male N alla superficie. L’esponente ns dipende dal materiale associato alla superficie (e, quindi, al vertice che ne fa parte) e varia tipicamente da 1 a 256. Piu’ ns è grande, più il riflesso della luce appare piccolo; per ns sufficientemente grande, il firlesso ri- sulta evidente solo osservando la superficie in direzione simmetrica di L rispetto alla normale e per piccoli scostamente da tale direzione. Una versione semplificata del calcolo della componente speculare che non conside- ra il vettore R è: I s = K s Ls ( H o N ) ns [14] 33
  • 42. Architettura Dove H è il vettore a metà angolo definito da: L +V H= [15] L +V V è il vettore normalizzato che porta dal vertice al punto di vista. Sebbene questa versione della componente speculare si semplificata e veloce da calcolare, quindi più adatta alla grafuca in tempo reale, risulta fornire risultati visivi più realistici ed è usata dal presente motore tridimensionale sia in versione sw sia quando il calcolo dell’illuminazione è delegato alle schede che supportano il protocollo T&L (Tran- sformation and Lightning). Le due componenti diffusa e speculare devono essere calcolate per ogni sorgente luminosa e sommate assieme per valutare tutta la componente di colore locale al ver- tice. Realizzazione Figura 6. Gerarchia di classi delle luci In figura 5 è mostrata la gerarchia di classi che realizza il sistema di illuminazione base per vertici del PEUCK. La classe padre astratta Light3D contiene al suo interno una struttura privata (Light3DStruct) che raccoglie le informazioni comuni quali colo- ri (diffuso, speculare), posizione, direzione, coefficienti di attenuazione: ogni tipo di luce è libero di usare i soli campi necessari. 34
  • 43. Architettura Light3DStruct float position [4] float direction [4] float diffuse [4] float specular [4] float spot_direction [4] float spot_cutoff float spot_exponent float attenuation0 float attenuation1 float attenuation2 int type La classe Light3D espone, inoltre, l’interfaccia comune ad ogni tipo di luce e i me- todi di gestione base di una luce in un ambiente tridimensionale. Light3D Light3D () Light3D (MATH::Color const& color) ~Light3D () void setPosition (Vector3D const& position) void setDiffuseColor (Color const& color) void setSpecularColor (Color const& color) Vector3D position () const Vector3D direction () const Vector3D position (Matrix& matrix) const Vector3D direction (Matrix& matrix) const Color diffuse () const Color specular () const void use (int index) virtual void transform (Matrix& matrix) = 0 I primi metodi permettono di accedere ai parametri base di una luce quali la posi- zione, la direzione e il colore sia in lettura sia in scrittura. E’ possibile specificare due colori differenti per la componente diffusiva della luce e per la componente speculare: sebbene questi due colori saranno nella maggior parte dei casi uguali, è data la possi- bilità di specificare valori diversi per simulare effetti particolari. Non esiste, tuttavia, 35
  • 44. Architettura la certezza che il driver di rendering supporti questa differenziazione; in tal caso le specifiche progettazione del driver di rendering affermano che il colore speculare sia ignorato. Il metodo use informa il PEUCK dell’intenzione di usare la luce all’interno della scena. Il motore tridimensionale è in grado di usare un massimo di 8 luci contempora- neamente attive all’interno della scena. Un mondo può contenere, quindi, un numero virtualmente illimitato di luci, ma solo otto di queste possono essere attive contempo- raneamente e quindi fornire illuminazione per vertici agli oggetti della scena. L’indice specificato dal metodo use informa quale degli otto disponibili sarà occupato dalla lu- ce corrente: una luce eventualmente già assegnata allo stesso indice sarà disattivata. L’ultimo metodo applica alla luce una matrice di trasformazione aggiornandone di conseguenza i vettori posizione e direzione. Può essere usato per muovere la luce at- traverso diversi sistemi di riferimento oppure per ancorare la stessa ad un oggetto par- ticolare, come ad esempio una torcia in mano ad un Avatar. DirectionalLight3D DirectionalLight3D () DirectionalLight3D (Vector3D const& direction, Color const& color, int type) virtual ~DirectionalLight3D () virtual Moveable& moveable () void rotateX (int rx) void rotateY (int ry) void rotateZ (int rz) void rotateX (float rx) void rotateY (float ry) void rotateZ (float rz) virtual void transform (Matrix& camera) Il primo tipo è direzionale e simula una luce con sorgente posta all’infinito; la sola direzione è quindi il dato caratterizzante. Con DirectionalLight3D è possibile simula- re la luce proveniente, ad esempio, dal sole in un ambiente esterno. L’utente può ruo- tare il vettore direzione della luce secondo gli assi di un valore espresso con un nume- ro reale oppure intero (versione ottimizzata). Il metodo moveable() restutisce un og- getto di tipo Moveable associato alla luce che ne descrive il tipo di orientamento. 36
  • 45. Architettura OmniLight3D OmniLight3D () OmniLight3D (Vector3D const& position, Color const& color, int type = PEUCK3D_MOV_MOVEABLE) virtual ~OmniLight3D () void translate (float x, float y, float z) Matrix const& evaluateGlobalMatrix () void transform (Matrix& cameraMatrix) Una luce di tipo OmniLight3D simula una sorgente luminosa puntiforme che irra- dia uniformemente in tutte le direzioni, senza attenuazione dovuta alla distanza del vertice da illuminare dalla sorgente luminosa. La posizione della luce è considerata prendendo il mondo come sistema di riferimento e il metodo translate() le applica una semplice traslazione. PointLight3D PointLight3D () PointLight3D (Vector3D const& position, Color const& color, int type) virtual ~PointLight3D () void setAttenuation (float constant = 1.0f, float linear = 0.0f, float quadratic = 0.0f) La classe PointLight3D fornisce una luce puntiform che irradia uniformemente in tutte le direzione, ma permette, invoando il metodo setAttenuation(), di specificare l’attenuazione dell’intensità luminosa al variare della distanza dei vertici illuminati secondo la seguente formula, 1 A= [16] c + ld + qd 2 dove A è il valore dell’attenuazione dell’intensità luminosa, c è il coefficiente co- stante, l il coefficiente lineare e q il coefficiente quadratico. 37
  • 46. Architettura SpotLight3D SpotLight3D () SpotLight3D (Vector3D const& position, Color const& color, int type) virtual ~SpotLight3D () void setDirection (Vector3D const& direction) void setCutoff (float cutoff) void setExponent (float exp) La classe SpotLight fornisce una luce puntiforme che irradia in una zona di spazio definita da un cono la cui direzione è indicata mediante il metodo setDirection(), l’ampiezza mediante il metodo setCutoff(). L’ultimo metodo setExponent() indica l’esponente da associare alla legge quadratica che regola il valore dell’attenuazione della luce al variare della posizione del vertice all’interno del cono illuminato: l’attenuazione è minima se il vertice giace al centro del cono, infinita se giace all’estremità del cono di luce. 38
  • 47. Architettura Texture mapping Introduzione Alla ricerca d’immagini realistiche, la principale critica verso i primi sintetizzatori d’immagini digitali è stata l’eccessiva “piattezza” delle superfici, che non mostravano alcun tipo di particolare, rugosità: il realismo richiede complessità oppure, al limite, un’apparenza di complessità. Il “Texture mapping” è una tecnica relativamente effi- ciente per creare l’apparenza della complessità senza aumentare la complessità geo- metrica degli oggetti. Texture mapping è il processo che mappa una funzione sopra una superficie tridi- mensionale; il dominio della funzione può essere mono, bi o tridimensionale e può es- sere rappresentato sia da un vettore di valori sia da una funzione matematica. Il moto- re tridimensionale all’interno del Sistema Operativo supporta attualmente solo texture bidimensionali. L’immagine sorgente è mappata su una superficie di un oggetto tridimensionale, che è successivamente mappato sull’immagine destinazione (lo schermo) attraverso la proiezione visiva. Lo spazio delle texture è solitamente indirizzato dalla coppia (u, v), lo spazio dell’oggetto dalla terna (x0, y0, z0) e lo spazio dello schermo dalla coppia (x, y). Nella pratica, il texture mapping è fatto usando uno dei due seguenti metodi: 1. E’ data una superficie P(x, y) da mappare con una texture. L’intera superficie deve essere esaminata e ad ogni pixel assegnato un valore a partire dalla te- xture da mappare. La superficie è tipicamente più grande o più piccola della mappa, che comporta una soluzione non banale al problema. La superficie è attraversanta variando i parametri x e y indipendentemente da 0 a 1 usando una funzione che mappi ogni coppia (x, y) in una coppia (u, v) all’interno della texture (dove 0 < u < 1 e 0 < v < 1). 2. Sono date diverse superfici e l’intera scena è illuminata usando il metodo del raytracing (v. pag XXX). Piuttosto che calcolare separatamente ogni superfi- cie, si segue un raggio di luce dal punto di vista all’osservatore attraverso la sceno, raggio che potrebbe colpire una superficie P(x, y) nel punto P = (x, y, z). Per mappare la texture, si cerca la coppia (x, y) che corrisponde al punto di intersezione e la si mappa alla coppia di indici (u, v) nella texture. Come precedentemente analizzato, dal punto di vista del rendering in tempo reale il raytracing non è ancora praticabile con la potenza delle macchine a disposizione del mercato consumer: la prima strada è quindi quella percorsa anche dal PEUCK. 39
  • 48. Architettura I possibili usi del texture mapping sono molteplici; alcuni parametri delle superfici tipicamente simulati sono: • Il colore della superficie • La riflessione speculare • La perturbazione della normale (bump mapping) • La trasparenza • Le ombre Un particolare uso del texture mapping si trova nel simulare la riflessione dell’ambiente sugli oggetti (environment mapping), dove un’immagine rappresentante l’ambiente, in cui un determinato oggetto è situato, è associata ad una sfera oppure ad un cubo che circonda la scena. Il processo che mappa una funzione dallo spazio della texture allo spazio dell’oggetto è generalmente suddiviso in due fasi: dapprima la superficie è parame- trizzata di modo da associare lo spazio della texture allo spazio dell’oggetto (genera- zione delle coordinate uv), seguito dalla normale trasformazione e proiezione delle primitive dallo spazio dell’oggetto allo spazio dello schermo. La parametrizzazione è solitamente naturale per le superfici definite mediante una funzione matematica, ad esempio superfici curve di Bezier, ma meno naturale per al- tre superfici quali triangoli, poligoni e insiemi di primitive standard (strisce di poligo- ni), che sono generalmente definie in maniera implicita. Un triangolo, ad esempio, è mappato semplicemente associando ad ogni vertice una coppia (o più coppie) di coor- dinate (u,v) che rappresentano la sua posizione all’interno dello spazio della texture, posizione che, nell’attuale generazone di schede acceleratrici, è interpolata lungo la primitiva al livello del singolo pixel. Dopo il calcolo della parametrizzazione e la proiezione delle primitive, l’immagine deve essere ricampionata sulla griglia di pixel dello schermo: questo processo è chia- mato filtraggio della texture o, più comunemente, filtering. Il più semplice metodo di filtraggio disponibile trova il pixel all’interno della texture più vicino al punto deside- rato. Questo metodo porta ad artefatti molto visibili nelle immagini che hanno la tipi- ca apparenza “blocchettosa”. Un metodo più preciso consiste nell’associare al pixel sullo schermo il valore ricavato dalla media pesata dei pixel della texture piu’ vicini al punto considerato. Sebbene fornisca ottimi risultati visivi, l’ultimo metodo di filtraggio considerato è estremamente lento anche se eseguito in hardware e non applicabile alla generazione di immagini in tempo reale. Per velocizzare il processo, si preferisce, quindi, prefiltra- re la funzione da mappare creando una sua rappresentazione piramidale; a ogni livello della piramide è associata una versione prefiltrata da usare per il calcolo del pixel sul- la texture da associare al pixel sullo schermo per un determinato valore del coefficien- 40
  • 49. Architettura te di LOD (Level Of Detail). Il coefficiente di LOD è, tipicamente, calcolato in base al numero di pixel sulla texture associata al pixel dello schermo. Realizzazione Figura 7. Gerarchia di classi delle texture La classe Texture fornisce i servizi per la gestione delle mappe da applicare du- rante il processo di texture mapping e per la gestione della memoria della scheda vi- deo associata alle texture. Come mostrato in figura 6, la classe Texture eredita dalla classe RefCount i servizi per la gestione dei conteggi di riferimento: è possibile quindi condividere una texture fra diversi oggetti per risparmiare sia memoria centrale sia memoria video per il loro immagazzinamento durante l’uso. Una texture è composta da un numero di immagini che può andare dal minimo di uno al massimo di nove, che corrispondono ai livelli di dettaglio via via più definititi della mappa usati durante la tecnica di filtraggio delle texture denominata mipmapping. E’ possibile indicare l’immagine associata ad ogni livello di dettaglio oppure richiedere il filtraggio auto- matico a partire dall’immagine che rappresenta il livello massimo. La dimensione massima della texture è di 1024x1024 pixel, dove la larghezza e l’altezza possono es- sere diversi (texture rettangolari), ma sempre potenze di due. Texture Texture () Texture (POM::String const& name) virtual ~Texture () void create (Bitmap& bitmap) virtual void create (int width, int height) virtual void destroy () virtual void download (bool compress = false) virtual void unload () virtual void reload () void use (MATH::Matrix& matrix, int stage = 0, int op = 0) void use (int stage = 0, int op = 0) POM::String const& name () const byte* getBitmap (int level) 41
  • 50. Architettura int width () const int height () const bool isLoaded () const bool isCompressed () const bool isGlobal () const bool isLocal () const int computeSize () const virtual void load (PeuckStream& s) virtual void store (PeuckStream& s) const Ad ogni texture è associtato un nome che semplifica le operazioni di immagazzi- namento e di ricerca all’interno degli insiemi di texture gestiti dagli oggetti di tipo TextureSet. La creazione vera e propria della memoria centrale associata alla texture avviene mediante l’invocazione del metodo create(), al quale è possibile fornire un oggetto di tipo Bitmap che contiene le informazioni sui colori dei pixel della mappa oppure le dimensioni dell’area di memoria da allocare che sarà successivamente riempita. Il me- todo destroy() distrugge l’area di memoria centrale associata alla texture. I dati relativi alla texture sono spediti al driver di rendering e, quindi inoltrati alla memoria video della scheda grafica acceleratrice, mediante l’invocazione del metodo download() al quale è possibile richiedere la compressione delle informazioni qualora l’hardware supporti la gestione di texture compresse. Il metodo unload(), al contrario, libera l’area di memoria video associata alla texture. In caso l’immagine associata alla texture sia modificata come, ad esempio, in caso di texture animate, è possibile invo- care il metodo reload() per richiedere l’aggiornamento della memoria video con le nuove informazioni, metodo generalmente più veloce dell’uso congiuto di un’invocazione al metodo unload() seguita dall’invocazione del metodo load(). Il me- todo use() informa il driver di rendering dell’intenzione di usare una texture per il di- segno delle successive primitive. I successivi metodi forniscono informazioni generali sulla texture, quali il suo no- me (name()), un vettore di byte in cui sono contenuti i colori dei pixel dell’immagine al livello di dettaglio richiesto (getBitmap()), le dimensione dell’immagine a più alto livello di dettaglio (width() e height()), un valore booleano che informa riguardo la spedizione della texture alla memoria video della scheda grafica (isLoaded()), la compressione o meno della texture (isCompressed()), l’appartenenza o meno della texture ad un’insieme di texture globale (isGlobal() e isLocal()), la dimensione della memoria occupata dalla texture in byte (computeSize()). Gli ultimi due metodi load() e store() caricano e salvano la texture da o in uno stream di oggetti. 42
  • 51. Architettura Bitmap Bitmap () Bitmap (byte& bitmap, int width, int height, int bpp, int mode) Bitmap (byte* bitmap, int width, int height, int bpp) Bitmap (ImageLoader& loader, int mode) virtual ~Bitmap () BitmapStruct* getData () byte* bitmap () void to32 (byte alpha) void to32 (byte *alpha) void setBitmap (byte& bitmap, int width, int height, int BPP, int mode) void setBitmap (byte * bitmap, int width, int height, int BPP) int setBitmap (ImageLoader& loader, int mode) Int width () const int height () const int bpp () const int size () const Gli oggetti della classe Bitmap gestiscono i dati relativi ad un’immagine e possono essere create specificando la dimensione e la profondità di colore desiderata oppure a partire da un oggetto istanza della classe ImageLoader che si occupa del caricamento dei file in formati diversi quali BMP, GIF e JPG. Il metodo getData() restituisce un puntatore ad una struttura dati che contiene le informazioni dell’immagine, che può essere estesa, in caso di necessita, ad una pro- fondità di colore di 32 bit indicando al metodo to32() il valore del canale alpha asso- ciato ai pixel sotto forma di un solo valore o di un vettore di valori. E’ possibile modificare il contenuto dell’immagine invocando i metodi setBitmap() a partire da un vettore di pixel oppure da un caricatore di immagini. Gli ultimi quattro metodi restituiscono rispettivamente la larghezza, l’altezza, la profondità di colore e la dimensione dell’immagine. 43
  • 52. Architettura BitmapStruct unsigned int* palette int size byte* bitmap int width int height int bpp La struttura BitmapStruct contiene le informazioni associate ad un immagine co- me la palette di colori (in caso di un formato a 8 bit), la dimensione dell’area associata all’immagine, un vettore che contiene i dati dei colori di ogni pixel dell’immagine, la larghezza, l’altezza e la profondità di colore che può essere 8 nel caso di immagini pa- lettizzate, 16 nel caso di immagini con un massimo di 65536 colori, 24 bit per imma- gini in true color, 32 bit per immagini che necessitano di un canale alpha che contenga i valori di trasparenza associati ad ogni pixel. Le immagini gestite dal PEUCK sono sempre in formato RGB. TextureSet TextureSet (int textureLineSize = 16) ~TextureSet () void add (Texture& texture, bool preload = true) void add (Texture *texture, bool preload = true) void remove (Texture& texture) void remove (Texture *texture) void remove (char *name) Texture* find (POM::String const& name) Texture* find (POM::String const *name) Texture& get (POM::String const& name) Texture& get (POM::String const *name) bool isIn (Texture& texture) bool isIn (Texture *texture) void unloadAll () long computeSize () const virtual void load (PeuckStream& s) virtual void store (PeuckStream& s) const 44
  • 53. Architettura La classe TextureSet fornisce i servizi per la gestione di insiemi di texture, per la loro ricerca e condivisione; il PEUCK fornisce sempre almeno un’insieme di texture glo- bali a tutta l’applicazione, ma l’utente può creare altri insiemi di texture per particolari usi. Il metodo add() aggiunge una texture all’insieme che può essere rimossa dalla colle- zione specificando il nome o l’oggetto stesso al metodo remove(). La ricerca di una texture all’interno dell’insieme avviene indicando un nome ai meto- di find() o get(), che si differenziano in quanto il metodo find fornisce un puntatore (eventualmente nullo in caso il nome non sia presente all’interno della collezione) all’oggetto texture trovato oppure un reference. Il metodo isIn() restituisce vero o fal- so in corrispondenza della presenta o meno di una texture all’interno della collezione. Gli ultimi quattro metodi scaricano tutte le texture dell’insieme dalla memoria video (unloadAll()), calcolano l’occupazione in memoria delle texture (computeSize()) e caricano o salvano l’insieme di texture da o in uno stream di oggetti (load() e store()). 45
  • 54. Architettura Bump mapping Introduzione Le tecniche convezionali di texture mapping danno alle superfici poligonali un’apparenza piatta. Sebbene l’illuminazione aumenti il realismo, la sensazione di piattezza resta piuttosto evidente. Il problema consiste in ciò che nel linguaggio usuale della grafica generata al computer è chiamato “texture”, che non corrisponde all’esperienza della realtà che ci circonda, in cui il termine “texture” si riferisce alla somma delle sensazioni che le irregolarità e i dettagli di una superficie forniscono. La nozione informatica di texture corrisponde maggiormente a ciò che nella vita di tutti i giorni è chiamato “decal”, ad esempio un’immagine incollata ad un muro. Nella vi- sione reale, riconosciamo la rugosità delle superfici per il modo in cui la luce interagi- sce con le irregolarità, che possono essere considerate come geometricamente facenti parte dell’oggetto preso in considerazione, sebbene la scala di queste irregolarità sia notevolmente piccola rispetto all’oggetto stesso (ad esempio le minuscole buche pre- senti in una palla da golf, la superficie di un’arancia). Per catturare il significato quotidiano del termine “texture”, la computer grafica ha introdotto un’estensione del texture mapping conosciuta come “bump mapping”. Blinn inventò tale tecnica nel 1978 [11]. Il bump mapping è una tecnica di resa delle immagini basata sul texture mapping, per simulare gli effetti dell’interazione della lu- ce con le irregolarità locali ad una superficie. Codificando le irregolarità della superfi- cie in una texture, il bump mapping simula l’apparenza di una superficie irregolare senza modellare la geometria di tali irregolarità, mantenendo limitato il numero di po- ligoni presenti nella scena. Disegnare superfici irregolari modellando la geometria sarebbe troppo oneroso da punto di vista dei calcoli e aumenterebbe a dismisura la quantità di dati da trattare, senza fornire un’immagine in qualche modo più realistica. La scala delle perturbazio- ni geometrica è, inoltre, solitamente minore della risoluzione in pixel dell’immagine, problema che porta alla comparsa di effetti di aliasing spaziale. Il bump mapping è un approccio al problema più efficiente perché raddoppia la descrizione della superficie mediante texture proponendo una mappa che descriva le irregolarità in piccola scala, usata per perturabare il calcolo dell’illuminazione a livello di pixel a partire da una descrizione in larga scala dell’oggetto mediante vertici. La mole di calcoli richiesta dal bump mapping come formulato orginariamente da Blinn è troppo onerosa per gli odierni hardware dedicati al texture mapping; il motore tridimensionale all’interno del PEUCK realizza una semplificazione della formulazio- ne orginaria adattata alla corrente generazione di GPU (Graphics Processing Unit), in particolare la serie di processori grafici NVIDIA. La tecnica è in grado di valutare la componente di luce ambientale, diffusa e speculare a livello del singolo pixel (con maggior precisione, quindi, rispetto alle tecniche classiche a livello vertice), una te- 46
  • 55. Architettura xture di “decal” e una texture di bump che perturbi le caratteristiche geometriche della superficie. Interpretazione matematica La tecnica si divide in due fasi: per ogni pixel è prima calcolata una normale alla superficie perturbata in base alle informazioni presenti nella texture di bump, succes- sivamente è svolto il calcolo dell’illuminazione usando la normale perturbata. Si assuma una superficie definita mediante una funzione vettoriale bivariata P(u, v) che genera i punti tridimensionali che la compongono. (Una funzione vettoriale biva- riata è una funzione di due variabili il cui risultato è un vettore. Una texture bidimen- sionale contenente i colori rappresentati da una terna RGB è un esempio di funzione vettoriale bivariata). La normale alla superficie per un punto P è definita come: [16] Una seconda funzione scalare bivariata F(u, v) definisce l’intensità delle variazioni sulla superficie P(u, v). La direzione della perturbazione è determinata normalizzando N(u, v).. F può essere interpetata geometricamente come un campo di altezze che per- turba la superficie P. P e F definiscono una nuova superficie perturbata P’(u, v) come mostrato in figura. Figura 8. Normali e normali perturbate 47
  • 56. Architettura P’(u, v) è definita da: [17] La normale alla superficie per un punto su P’ è definita come: [18] Espandendo le derivate parziali di P’ otteniamo: [19] [20] In questo contesto, F rappresenta la rugosità della superficie, che può essere consi- derata come una micro perturbazione trascurabile risptto alla scala globale di P. As- sumendo questo, il termine più a destra nelle precedenti equazioni può essere appros- simato a zero; sebbene F sia trascurabile, le sue derivate parziali non lo sono. Espandendo il prodotto vettoriale si arriva alla seguente espressione di N’ 48
  • 57. Architettura [21] [22] Il primo termine è N per definizione, mentre l’ultimo termine è zero perché NxN = 0. Perciò, N’ si semplifica a [23] Se le derivate parziali di P e F non sono disponibili analiticamente, possono essere approssimate mediante le differenze finite. L’equazione [23] valuta la normale alla superficie perturbata usata nel bump map- ping, ma i calcoli richiesti sono troppo numerosi per essere eseguiti sul singolo pixel in tempo reale; N’ deve, inoltre, essere normalizzato, operazione che aggrava ulte- riormente i tempi di calcolo. E’ necessario un approccio più efficiente al calcolo di N’. N’ può essere interpretato anche come una rotazione del vettore N attorno a un asse nel piano tangente alla superficie. Questo vettore è il prodotto vettoriale fra N’ e N come mostrato in figura. 49
  • 58. Architettura Figura 9. Normale perturbata mediante rotazione Dove D rappresenta lo scostamento della normale perturbata N’ dalla normale ori- ginale N e A rappresenta l’asse di rotazione della normale. Questa interpetazione di N’ porta ad secondo tipo di immagazzinamento delle in- formazioni riguardanti le perturbazioni delle normali alla superfice all’interno della mappa di bump. Angoli euleriani, matrici di rotazione 3x3 e quaternioni sono tutte rappresentazioni ragionevoli per il vettore di rotazione. Una texture di bump che rap- presenti le perturbazioni delle normali in questa maniera è chiamata mappa di pertur- bazione della normale o semplicemente mappa delle normali, in alternativa alla rap- presentazione classica mediante mappe di altezza. Il PEUCK fornisce le primitive per la gestione sia di mappe d’altezza sia di mappe delle normali e gli strumenti per la conversione da una rappresentazione all’altra. Dopo aver calcolato la normale perturbata per un determinato pixel, essa è usata per la valutazione dell’illuminazione mediante un modello a d’illuminazione a livello del pixel. Illuminazione diffusa di superfici rugose Come già illustrato nel caso del modello classico per vertice, l’illuminazione diffu- sa è modellata mediante la legge di Lambert. Nel caso del modello di illuminazione a livello pixel, la legge di Lambert è riformulata nel modo seguente: 50
  • 59. Architettura La differenza rispetto al caso già esaminato riguarda la sottoespressione max(0, L N), che tratta il caso in cui la luce e la normale siano situate ai lati opposti del piano tangente alla superficie come mostrato in figura. Figura 10. Ombra propria In questa situazione, il punto è detto essere in un’ombra (quindi non illuminato). Nel caso di normale perturbata, anch’essa deve essere considerata durante l’illuminazione. La normale non perturbata N si basa, come detto, sul modello geome- trica a larga scala della superficie quando la normale perturbata N’ si basa sul modello a piccola scala. La figura seguente mostra il caso in cui sebbene il prodotto scalare della normale perturbata con il vettore che rappresenta la luce, fornisca un valore positivo che porterebbe a pensare il punto in posizione di non ombra, il calcolo sulla normale non perturbata mostra il contrario. Per trattare questo caso, la legge di Lam- bert deve essere riscritta come: [24] 51
  • 60. Architettura dove [25] Questa versione della legge usata nel PEUCK minimizza i casi di zone d’ombra il- luminate ed è molto adatta ad una realizzazione in hardware. Illuminazione speculare di superfici rugose Nel modello di illuminazione speculare per pixel, il modello empirico usato nel PEUCK è il modello di Blinn formulato nel caso di una singola luce [26] dove H è il vettore già definito nel caso dell’illuminazione per vertici. A differenza del modello classico di Phong che definisce il vettore R, il modello usato è meno pe- sante dal punto di vista computazionale e sperimentalmente più plausibile; il vettore H è, inoltre, indipendente da N, rendendo quindi semplice la sostizione con la norma- le perturbata N’. Realizzazione La realizzazione fornita dal motore tridimensionale presente nel PEUCK richiede i seguenti passi: • le mappe di perturbazione sono rappresentate mediante mappe di normali (sono forniti i servizi per la conversione di mappe d’altezza), filtrate in seguito per ridurre l’incidenza di effetti d’aliasing e migliorare le presta- zioni dell’hardware. 52
  • 61. Architettura • Il modello di Blinn è valutato per ogni pixel della superficie interessata mediante l’operazione di prodotto scalare fra texture fornita dall’hardware; in caso l’operazione non fosse disponibile in hardware, il driver di rende- ring si occupa di simularla in software, tuttavia, con un netto calo delle prestazioni. • Il driver disegna la medesima superficie in più passate (tipicamente tre con l’hardware odierno). La prima passata disegna i dettagli forniti dalla textu- re classica. La seconda passata modula i tettagli mediante l’applicazione del contributo ambientale e diffusivo perturbato dalla mappa di bump per una singola luce. La terza passata aggiunge un contributo speculare (anch’esso perturbato dalla mappa di bump) per una singola luce. Sia la seconda sia la terza passata sfruttano l’hardware in grado di fornire due u- nità per l’applicazione delle texture se presente. Questa tecnica offre il vantaggio di essere scalabile: effetti quali ad esempio l’attenuazione con la distanza e l’applicazione di ombre portate possono essere aggiunti median- te ulteriori passate; altre luci possono essere supportate mediante altre pas- sate. L’efficienza di questa tecnica può essere, inoltre, incrementata elima- nando le passate che riguardano la texture di dettaglio oppure quando la superficie non richiede un’apparenza speculare. Un ulteriore vantaggio ri- siede nella possibilità di collassare diverse passate quando le future GPU supporteranno più unità di texture mapping oppure più sofisticate opera- zioni fra le texture. • L’illuminazione è calcolata, come visto, nello spazio ortonormale locale al singolo punto della superficie definito dal vettore normale, il vettore tan- gente e il vettore binormale. Ad ogni vertice deve quindi essere associato il vettore della luce trasformato nello spazio ortonormale, usato per la valu- tazione della legge di Lambert al livello del singolo pixel mediante il pro- dotto scalare con la mappa normale. Data la base ortonormale per ogni vertice, il vettore L e il vettore H sono portati nello spazio ortonormale mediante la seguente trasformazione [27] 53
  • 62. Architettura Dove Los è la posizione della luce nello spazio dell’oggetto e la matrice è formata dai vettori normale, tangente e binormale. La posizione della luce nello spazio orto- normale deve essere ricalcolata ogni qual volta varia la posizione della luce oppure dell’oggetto. Essendo la normalizzazione del vettore L un’operazione costosa, essa può essere omessa durante il calcolo al livello vertice e delegata all’hardware, in grado di norma- lizzare al livello del singolo pixel attraverso l’uso di una cube map. La cube map di normalizzazione e la mappa di normale sono le due texture usate nella seconda e terza passata e fornite alle due unità di testurizzazione dell’hardware. 54
  • 63. Architettura Mesh e oggetti La scena tridimensionale S è un insieme di oggetti statici o in movimento da proiettare e disegnare sullo schermo e di luci che li illuminano, quindi S = (O, L) dove O è la collezione di oggetti e L la collezione di luci. Ogni oggetto è descritto da una terna (T, M, O), dove T è la matrice di trasformazione dallo spazio dell’oggetto allo spazio del mondo che descrive la sua posizione e il suo orientamento, M è la descri- zione della forma dell’oggetto comunemente indicata con il termine inglese mesh, O è la collezione di oggetti figli che hanno come sistema di riferimento padre lo spazio dell’oggetto. All’oggetto è associato un proprio sistema di riferimento (un sistema cartesiano) comunemente chiamato spazio dell’oggetto all’interno della quale è descritta la sua geometria. Il passaggio della geometria dell’oggetto dallo spazio dell’oggetto allo spazio del mondo, ovvero il sistema di riferimento della scena, avviene mediante la matrice di trasformazione T che descrive in maniera compatta la posizione P dell’oggetto all’interno dello spazio del mondo e il suo orientamento come valori di rotazione attorno ai tre assi (Rx, Ry, Rz). La posizione P è, quindi, il baricentro dell’oggetto B trasformato mediante la matrice T nello spazio del mondo. P=B T [28] Il punto B può essere posizionato indifferentementemente nell’origine O del si- stema di riferimento dell’oggetto oppure in un punto differente in caso l’oggetto stes- so non sia perfettamente centrato nell’origine degli assi. Nel secondo caso, il baricen- tro subirà non soltanto l’effetto traslativo, ma anche l’effetto di rotazione della tra- sformazione. La Mesh M è un insieme (VB, P) formato da una collezione di vertici VB e una collezione di parti P che ne descrive la forma. La geometria della mesh si suppone sempre descritta nel sistema di riferimento dell’oggetto; una signola mesh può essere, quindi, condivisa da più oggetti aventi posizioni e orientamenti diversi nella scena, ma aventi la medesima forma: è compito della trasformazioni T posizionare e orientare la mesh M nella giusta maniera. Un’aula scolastica, ad esempio, contiene molti oggetti che differiscono fra di loro per la sola posizione e non per la forma (banchi, sedie), che possono essere efficacemente descritti da poche mesh condivise con notevole ri- sparmio della memoria necessaria a descrivere l’intera geometria della scena. La collezione di oggetti O figli, infine, è la base per la creazione di una gerarchia di oggetti. La matrice di trasformazione T assume, per gli oggetti figli, un significato a- nalogo ma leggermente diverso dal significato assunto nel caso dell’oggetto capostipi- te della gerarchia: se per esso T descrive la posizione e l’orientamento dell’oggetto all’interno dello spazio del mondo, effettuando quindi il cambiamento di base dal si- 55
  • 64. Architettura stema di riferimento dell’oggetto al sistema di riferimento del mondo, la matrice T, nel caso di un oggetto figlio, effettua il cambiamento di base dal sistema di riferimen- to dell’oggetto al sistema di riferimento dell’oggetto padre. La matrice composta di trasformazione dallo spazio dell’oggetto figlio allo spazio dell’oggetto padre è così calcolata: Tc = T * Tp [29] dove Tc è la matrice composta che rappresenta il cambiamento di base verso il si- stema di riferimento del mondo, T è la matrice di trasformazione dell’oggetto figlio e Tp la matrice di trasformazione associata all’oggetto padre. Ad ogni oggetto figlio è possibile associare un’ulteriore collezione di oggetti figli che concatenano le matrici di trasformazione mediante la seguente formula: Tc = T * Tn * Tn-1 * ... * T1 [30] Questo procedimento è anche indicato con il termine di animazione gerarchica in quanto ogni singolo oggetto della gerarchia può essere mosso rispetto all’oggetto pa- dre consentendo la realizzazione di semplici animazioni, ad esempio, di figure umane. Mesh, Parti e VertexBuffer In figura è mostrata la gerarchia di classi che fornisce gli strumenti per la gestione della forma di un oggetto. La classe base della gerarchia è la classe Mesh astratta che fornisce il concetto di forma di un oggetto descrivendola come un insieme di vertici, senza ancora fornire il concetto di collezione di parti. E’, infatti, permessa a questo li- vello una definizione diversa di forma dell’oggetto che pur basandosi sull’insieme di vertici può fornire un’interpretazione diversa delle primitive che la compongono. La classe CompositeMesh introduce le parti di una mesh come descrizione della forma dell’oggetto. Le ultime classi sono concretizzazioni successive che forniscono forme di oggetti comunemente usate nel descrivere scene tridimensionali come scatole (Box), sfere (Sphere), semisfere (HemiSphere) e, infine, SkeletalMesh, una classe che descrive la forma di un oggetto dotato di scheletro per animazioni scheletrali. 56
  • 65. Architettura Figura 11. Gerarchia delle Mesh Mesh Mesh (dword size = 0) Mesh (VertexBuffer& buffer) Virtual ~Mesh () void setName (String& name) VertexBuffer& getVertexBuffer () void setVertexBuffer (VertexBuffer& buffer) void block () void unblock () void use () virtual void computeTangentSpaces (int mode) = 0 virtual void computeNormals (int mode) = 0 virtual void setBFCMode (int mode) = 0 int getBFCMode () virtual void bfc (Vector3D const& origin) = 0 virtual void render () = 0 virtual bool isTrasparent () const = 0 virtual void rebuildTransparencies () = 0 void scale (float value) virtual void load (PeuckStream& s) virtual void store (PeuckStream& s) const Un oggetto di un tipo derivato dalla classe Mesh si costruisce indicando il numero di vertici dal quale è composta la forma dell’oggetto oppure indicando un oggetto di tipo VertexBuffer che descrive un’insieme di vertici. E’ possibile assegnare un nome alla mesh invocando il metodo il metodo setName(). 57
  • 66. Architettura I metodi getVertexBuffer() e setVertexBuffer() gestiscono il VertexBuffer asso- ciato alla mesh; l’utente riceve un riferimento all’insieme di vertici oppure informa l’oggetto di tipo Mesh riguardo all’insieme di vertici da usare, insieme che può essere condiviso con altre mesh per un uso efficiente della memoria. Il VertexBuffer può essere bloccato o sbloccato mediante i metodi block() e unblock() che invocano per delegazione gli omonimi metodi della classe VertexBuffer; anche il metodo use() in- voca per delegazione. I due metodi computeTangentSpaces() e computeNormals() calcolano gli spazi tangenti associati ad ogni vertice oppure le pseudonormali associate: i dati da essi cal- colati sono usati durante il processo di ombreggiatura delle primitive mediante uno dei due modelli precedentemente descritti (illuminazione per vertici e illuminazione per pixel). Nel caso dell’illuminazione per pixel l’utente è obbligato al calcolo dell’intero spazio tangente, mentre è sufficiente il solo calcolo delle normali, con con- seguente risparmio di memoria, nel caso che la mesh sia usata in oggetti la cui illumi- nazione è calcolata solo per vertici. Per la gestione della tecnica del backface culling l’utente specifica la modalità con la quale la mesh debba essere trattata invocando il metodo setBFCMode() con una con un valore uguale a zero nel caso non sia desiderata l’applicazione della tecnica, maggiore di zero nel caso siano da eliminare le primitive con senso orario, minore di zero per l’eleminazione delle primitive in senso antiorario. Il metodo getBFCMode() restituisce il valore corrente associato alla mesh. L’effettiva eliminazione delle primi- tive avverrà durante la pipeline di rendering con l’invocazione del metodo bfc(). Durante la pipeline di rendering, il disegno della mesh è richiesto invocando il me- todo render() che invierà al driver di rendering l’elenco delle primitive da rendere sullo schermo. Il metodo isTransparent() restituisce un valore “vero” in caso la mesh contenga al suo interno “parti” trasparenti, mentre il metodo rebuildTransparencies() è invocato nel momento in cui si modificano le proprietà di trasparenza delle parti della mesh. Il metodo scale() scala tutti i vertici associato alla mesh di un fattore indicato dal parametro reale. La mesh può essere caricata da uno strema oppure immagazzinata con i metodi load() e store() invocati dai meccanismi di persistenza degli oggetti del PEUCK. CompositeMesh CompositeMesh () CompositeMesh (VertexBuffer& buffer) CompositeMesh (dword size) CompositeMesh (MeshLoader& loader) 58
  • 67. Architettura CompositeMesh (CompositeMesh& mesh, int share) virtual ~CompositeMesh () void addPart (Part3D& part) dword getPartCount () const Part3D* getPart (int no = 0) Part3D* findPart (String& name) Virtual void computeTangentSpaces (int mode) Virtual void computeNormals (int mode) virtual void setMaterial (Material& material) virtual void setBFCMode (int mode) virtual void transform (Matrix const& camera) virtual void bfc (Vector3D const& origin) virtual void render () Bool isTrasparent () const void rebuildTransparencies () virtual void load (PeuckStream& s) virtual void store (PeuckStream& s) const La classe CompositeMesh realizza una mesh composta da una collezione di parti che definiscono la forma dell’oggetto e fornisce i metodi per l’aggiunta, il conteccio e la ricerca delle parti. Ai metodi di creazione della mesh eriditati dalla classa padre, CompositeMesh aggiunge un costruttore che accetti come parametro un oggetto i- stanza della classe MeshLoader che, analogamente alla classe che carica i formati di immagini, descrive l’interfaccia di un caricatore di mesh generico: sono fornite classi specializzate per il caricamento dei formati dei modellatori tridimensionali più cono- sciuto (3DStudio, LightWave, Maya). I metodi caratteristici di CompositeMesh riguardano la gestione della collezione di parti e permettono l’aggiunta di una parte alla collezione mediante l’invocazione di addPart(), il conteggio delle parti con getPartCount(), la ricerca di una parte cono- scendo la sua posizione all’interno della collezione (getPart()) oppure il suo nome (findPart()). VertexBuffer VertexBuffer () VertexBuffer (dword size, int texture_stages = 1) VertexBuffer (VertexBuffer& buffer) virtual ~VertexBuffer () 59
  • 68. Architettura void block () void unblock () void use () int alloc (word size, bool clean = false) void free () int size () int count () int add (float x, float y, float z, float tx = 0.0f, float ty = 0.0f) int add (float c[], int size) int add (Vertex const& v) int reserve (int count) float* cdata (int start = 0) void setNormal (int i, float nx, float ny, float nz) void setNormal (int i, Vector3D& normal) void setTangentSpace (int i, float axis[]) void setTangentSpace (int i, float normal[], float tangent[], float binor- mal[]) void setTangentSpace (int i, Vector3D& normal, Vector3D& tangent, Vector3D& binormal) void setUV (word i, float u, float v) void enableWeights (int flag = true) void enableTangentSpaces (int flag) void enableNormals () void enableTextureStage (int stage, int flag = true) void enableLights (int count = 1) void disableWeights () void disableTangentSpaces () void disableTextureStage (int stage) void disableLights () void copyWeights (float weights[], int start = 0, int end = -1) void computeLightVectors (int num, Vector3D& light) void computeHalfVectors (int num, Vector3D& light, Vector3D& view) bool isAllocated () const bool isBlocked () const bool isTangentSpaceAvailable () const bool areLightsAvailable () const virtual void load (PeuckStream& s) virtual void store (PeuckStream& s) const 60
  • 69. Architettura La classe VertexBuffer è la base su cui poggia la pipeline di rendering del PEUCK: il disegno di ogni primitiva si basa, infatti, sull’insieme di vertici corrente, gestito da instanza di questa classe, al cui interno sono contenute tutte le informazioni riguardanti i vertici quali la posizione nello spazio dell’oggetto, le pseudo normali (o lo spazio tangente) e le coordinate nello spazio della texture. La costruzione di un insieme di vertici avvenire indicando il numero massimo dei vertici contenibili oppure specificando un altro insieme di vertici dal quale copiare i dati. Nel caso non sia necessario modificare i dati dei vertici, è possibile bloccare l’insieme invocando il metodo block(), che informa il driver di procedere ad eventuali ottimizzazioni quali, ad esempio, la compilazione dei vertici oppure il loro spostamen- to nella memoria video della scheda per un accesso più rapido. Lo sblocco dei vertici avviene invocando il metodo unblock(). Il metodo use() informa il driver di rendering di usare l’insieme di vertici come insieme corrente per il disegno delle successive primitive. Allocazione e deallocazione della memoria di sistema associata all’insieme dei ver- tici avviene a seguito dell’invocazione dei metodi alloc() e free(), mentre con i metodi size() e count() è possibile ricevere informazioni riguardo al massimo numero di ver- tici immagazzinabili oppure al numero correnti di vertici. Il metodo add() aggiunge all’insieme uno o più vertici dei quali sono fornite le coordinate come array di valori reali oppure come un oggetto di tipo Vertex3D; il metodo reserve(), al contrario, ri- serva un certo numero di vertici dell’insieme per uso futuro, le cui coordinate possono essere accedute mediante l’invocazione del metodo cdata() che restituisce un vettore di numeri reali nel quale le coordinate sono memorizzate come triplette di valori. I successivi metodi specificano per ogni vertice i valori della pseudo normale (set- Normal()) o dello spazio tangente (setTangentSpace()), nel caso sia stato attivato, e le coordinate uv nello spazio della texture (setUV()). I metodi enableXXX() e disableXXX() attivano o disattivano il supporo del Ver- texBuffer per la gestione delle pseudo normali o dello spazio tangente, per i pesi asso- ciati ad ogni vertice (nel caso di animazioni scheletrali), per diversi stadi per il texture mapping a multipassata, per i dati relativi alle luci per l’uso durante l’illuminazione per pixel. Il metodo copyWeights(), infatti, copia un vettore di valori reali rappresen- tanti i pesi, all’interno dell’insieme e i metodi computeLightVectors() e computeHal- fVectors() calcolano per ogni vertici i vettori che rappresentano la luce, negli spazi tangenti, e il vettore H durante l’illuminazione per pixel. Sono forniti, inoltre, metodi per l’interrogazione dello stato del VertexBuffer per conoscere se è stato allocato (isAllocated()), bloccato (isBlocked()), se è disponibile lo spazio tangente e la memoria per contenere i vettori di posizione della luce (isTan- gentSpaceAvailable() e areLightsAvailable()). 61
  • 70. Architettura Il metodo load() carica l’insieme di vertici da uno stream di oggetti e il metodo store() lo immagazzina. Figura 12. Gerarchia di classi delle Parti La figura 11 mostra la gerarchia di classi corrente per le parti di una mesh che si appoggia alla classe astratta Part3D, che fornisce l’interfaccia generia di una parte: l’utente può estendere il comportamento di Part3D e derivare propri tipi di parte. Sa- ranno, ad esempio, gestiti in futuro parti formate da superfici curve e sistemi particel- lari oppure parti dotate di un sistema di scelta dinamico del livello di dettaglio. FastPart3D FastPart3D () FastPart3D (String& name, Material& material, int size) virtual ~FastPart3D () virtual void add (word v0, word v1, word v2) virtual void add (word v[], int c) virtual void render () virtual void setMaterial (Material& material) word triCount () const int indexCount () const word const* tri () const virtual void load (PeuckStream& s) virtual void store (PeuckStream& s) const La classe FastPart3D realizza una parte ottimizzata per il rendering veloce di una lista di triangoli accomunati da un solo materiale. Il metodo add() aggiunge alla lista uno o più triangoli specificati da una array di triplette di numeri interi che rappresen- tano gli indici, all’interno del vertex buffer della mesh, dei vertici che li formano: l’indice massimo rappresentabile è 65535 che rappresenta, inoltre, la dimensione mas- sima di un vertex buffer. Il numero massimo di triangoli memorizzabili all’interno del- la parte dipende, invece, dalla memoria di sistema disponibile. 62
  • 71. Architettura Il metodo render() invia la lista di indici dei triangoli al driver di rendering, che si occupa di inoltrarla alla scheda grafica per il disegno con il materiale indicato median- te il metodo setMaterial(). Le informazioni riguardanti il numero di triangoli e di indici contenuti nella parte sono recuperate mediante l’invocazione dei metodi triCount() e indexCount(), men- tre il metodo tri() restituisce un vettore di numeri interi non modificabile che rappre- senta i triangoli da disegnare. I metodi load() e store() caricano e salvano la parte su uno stream o da uno stream. Oggetti Figura 13. Gerarchia degli oggetti La gerarchia di classi che gestisce gli oggetti tridimensionali, mostrata in figura 12, si basa sulla classe astratta Object3D, che definisce l’interfaccia astratta di un oggetto tridimensionale dotato di un proprio sistema cartesiano di riferimento.Gli oggetti tri- dimensionali si dividono in statici e “liberi” (free), dotati di libertà di movimento nel mondo. Fino al presente livello della gerarchia, le classi sono astratte e non forniscono il concetto di forma introdotto dalle classi FreeMeshObject3D e StaticMeshO- bject3D. Object3D Object3D () virtual ~Object3D () String const& name () void setName (String& name) void setID (int id) int getID () 63
  • 72. Architettura Vector3D const& center () const virtual Matrix const& object2world () const Matrix const& world2object () const Matrix const& object2camera () const virtual void checkCollision (Object3DVector * objs) = 0 virtual void evaluateGlobalMatrix () = 0 virtual int checkFrustum (Viewport3D const& viewport) = 0 virtual void use () = 0 virtual void bfc (Vector3D const& origin) = 0 virtual void render () = 0 virtual bool isTrasparent () = 0 virtual void rebuildTransparencies () = 0 bool isEnabled () const void disable () void enable () virtual void useBoundingSphere (Point3D center = Point3D(0.0f, 0.0f, 0.0f), float radius = 0.0f) = 0 virtual void useBoundingBox (Point3D center = Point3D(0.0f, 0.0f, 0.0f), float lx = 0.0f, float ly = 0.0f, float lz = 0.0f) = 0 BoundingVolume& boundingVolume () bool hasBoundingVolume () void removeBoundingVolume () Un oggetto tridimensionale istanza di una classe derivata da Object3D è caratte- rizzato da un nome e da un ID (numero intero) gestiti mediante i metodi setName(), name(), getID(), setID(). Lo spazio dell’oggetto è gestito mediante i metodi center() che restituisce la posi- zione del centro dell’oggetto trasformato nello spazio del mondo, object2world() che resitutisce la matrice di trasformazione dallo spazio dell’oggetto allo spazio del mon- do, world2object() che restituisce la matrice di trasformazione inversa e o- bject2camera() che restituisce la matrice di trasformazione dallo spazio dell’oggetto allo spazio della telecamera. La gestione delle collisioni avviene invocando il metodo checkCollision() con una lista di oggetti da controllare come parametro: in caso di collisione, la matrice di tra- sformazione dallo spazio dell’oggetto allo spazio del mondonon viene aggiornata con le informazioni che riguardano il movimento; come risultato, l’oggetto non si muove dalla posizione attualmente occupata. Il controllo delle collisioni è effettuato anche sugli eventuali oggetti figli la cui matrice globale di trasformazione è valutato dal me- todo evaluateGlobalMatrix(). 64
  • 73. Architettura Il metodo checkFrustrum() controlla se l’oggetto è visibile all’interno del viewing frustrum indicato dalla viewport passata come parametro. I tre metodi use(), bfc() e render() sono usati dalla pipeline grafica per il disegno dell’oggetto e sono invocati per informare il driver che l’oggetto è usato per un suc- cessivo rendering, per evettuare l’eventuale rimozione delle superfici nascoste me- diante back face culling e per disegnare effettivamente l’oggetto. I metodi isTransparent() e rebuildTransparencies() invocano per delegazione i metodi della mesh, se presente, associata all’oggetto. E’ possibile a tempo di esecuzione disabilitare o abilitare un’oggetto invocando i metodi enable() e disable(); un oggetto disabilitato è ignorato dalla pipeline grafica e il metodo isEnabled() informa riguardo allo stato di abilitazione. Gli ultimi metodi gestiscono il bounding volume relativo ad un oggetto. Un boun- ding volume è definito come un volume, descritto il pià delle volte da figure geome- triche semplici quali sfere e box, che contiene totalmente l’oggetto. Il bounding volu- me, se presente, è usato durante il controllo delle collisioni e per decidere se un ogget- to è presente all’interno del viewing frustrum ed è quindi potenzialmente visibile dalla telecamera; l’attivazione del bounding volume attraverso i metodi useBoundin- gSphere() e useBoundingBox(), rispettivamente per sfere e box, velocizza enorme- mente la fase di rendering, in quanto la geometria associata agli oggetti non visibili dalla telecamera è eliminata dalla pipeline in maniera estremamente efficiente. Il me- todo boundingVolume() restituisce un istanza della classe astratta BoundingVolume che rappresenta il volume associato all’oggetto, mente il metodo hasBoundingVolu- me() restituisce un valore bollano che rappresenta la presenza o meno del volume. Il metodo removeBoundingVolume(), infine, rimuove un volume eventualmente asso- ciato. FreeObject3D FreeObject3D (int type) virtual ~FreeObject3D () void rotateX (int rx) void rotateY (int ry) void rotateZ (int rz) void rotateX (float rx) void rotateY (float ry) void rotateZ (float rz) virtual void setPosition (Point3D& p) virtual void setPosition (float x = 0.0f, float y = 0.0f, float z = 0.0f) void translate (float x = 0.0f, float y = 0.0f, float z = 0.0f) 65
  • 74. Architettura void lockLookAt (Vector3D const& eye, Vector3D const& up = Vector3D(0.0f, 1.0f, 0.0f)) void lockLookAt (OBJECT3D::Object3D const& obj, Vector3D const& up = Vector3D(0.0f, 1.0f, 0.0f)) void unlockLookAt () Vector3D const& worldPosition () const Matrix const& object2world () const Moveable const& moveable () const virtual void add (FreeObject3D& child) virtual void remove (FreeObject3D& child) virtual Object3D& getObjectChild (dword index) virtual Object3D& findObjectChild (String& name) virtual void checkCollision (Object3DVector * objs) virtual bool nextPosition () virtual void updatePosition () virtual void updateRotation () virtual void evaluateGlobalMatrix () virtual void evaluateGlobalMatrix (Matrix& up) virtual void moveInCameraSpace (Matrix const& camera) virtual int checkFrustum (Viewport3D const& viewport) virtual void bfc (Vector3D const& origin) virtual void render () virtual void use () virtual void useBoundingSphere (Point3D center = Point3D(0.0f, 0.0f, 0.0f), float radius = 0.0f) virtual void useBoundingBox (Point3D center = Point3D(0.0f, 0.0f, 0.0f), float lx = 0.0f, float ly = 0.0f, float lz = 0.0f) virtual bool isTrasparent () void rebuildTransparencies () virtual void transform (Matrix const& camera) FreeMeshObject3D FreeMeshObject3D (int type = PEUCK3D_MOV_MOVEABLE) FreeMeshObject3D (Mesh& mesh, int type = PEUCK3D_MOV_MOVEABLE) FreeMeshObject3D (dword size, int type = PEUCK3D_MOV_MOVEABLE) 66
  • 75. Architettura virtual ~FreeMeshObject3D () virtual void bfc (Vector3D const& origin) virtual void render () virtual void use () Mesh& mesh () virtual void useBoundingSphere (Point3D center = Point3D(0.0f, 0.0f, 0.0f), float radius = 0.0f) virtual void useBoundingBox (Point3D center = Point3D(0.0f, 0.0f, 0.0f), float lx = 0.0f, float ly = 0.0f, float lz = 0.0f) bool isTrasparent () void rebuildTransparencies () int checkFrustum (Viewport3D const& viewport) virtual void transform (Matrix const& camera) StaticObject3D StaticObject3D (Point3D& p) virtual void evaluateGlobalMatrix () = 0 virtual bool nextPosition () = 0 virtual void updatePosition () = 0 virtual void updateRotation () = 0 virtual void checkCollision (Object3DVector * objs) = 0 67
  • 76. Architettura Network Engine Figura 14. Architettura client server Il motore di rete, allo stato attuale della realizzazione, è ancora allo stato embriona- le e realizza una semplice architettura client/server, nella quale i messaggi possono es- sere scambiati unicamente fra un client e un server e non fra due client (figura 14). Il driver di rete si basa sul protocollo TCP/IP. Nel semplice realizzato, una connessione (figura 15) è inizializzata da un client che spedisce al server un messaggio indicando una richiesta di connession (connection re- quest); una connessione può essere accettata o rifiutata (ad esempio quando è stato raggiunto il massimo numero di clienti supportato). Se il server accetta la connessio- ne, ritorna un messaggio che contiene le informazioni che riguardano la sessione in corso quali il nome del server e il massimo numero di client accettabili. Il client accet- ta queste informazioni e inizia la connessione, durante la quale il server spedisce il mondo virtuale al client inviando i singoli oggetti che compongono la scena (mesh, luci, materiali, texture). Il server, inoltre, gestisce la sincronizzazione fra la scena locale e le scene memo- rizzate all’interno di ogni client: quando un client modifica la posizione di un oggetto, 68
  • 77. Architettura ad esempio, l’informazione è inviata a tutti i client connessi di modo che possano ag- giornare la scena e sincronizzarla con la scena presente nella memoria del server. Figura 15. Sincronizzazione degli oggetti 69
  • 78. Esempi d’uso 3. Esempi d’uso Classroom L’applicazione, scritta durante lo sviluppo di questo lavoro, realizza un’aula virtua- le dovre un professore tiene una lezione a non più di sei studenti. Il professore e gli studenti possono connettersi da remoto al server; il ruolo del professore è tenuto dal primo cliente connesso al server. Al momento, gli avatar virtuali possono usare i seguenti tool: • Una chat in modalità testo • Un puntatore laser per indicare un punto all’interno dell’ambiente • Una lavagna per la proiezione di slide La costruzione dell’aula virtuale avviene mediante una serie di chiamate di sistema messe a disposizione dal PEUCK. Il primo passo: il mondo virtuale, le luci e la telecamera (alla quale è associata una viewport) devono essere posizionati nella scena contenuta all’interno del server; que- sto può essere ottenuto mediante il seguente pseudocodice che riflette fedelmente il codice che si scriverebbe in un linguaggio tradizionale: World world = new WorldServer(new ClassroomServer()); viewport = new Viewport(0, 0, 800, 600, 0.1f, 50.0f, 1.0f); camera = new StandardCamera(); viewport.addCamera(camera); light = new PointLight(Vector(0.0f, 3.0f, -1.5f), 10.0f, Color::RED); world.add(light); world.setAmbient(Color::GREY); world.add(viewport); Il mondo è un server che è in grado di scaricare la sua scena ai clienti remoti. Clas- sroomServer è una specializzazione della classe Server che gestisce le connessioni 70
  • 79. Esempi d’uso attraverso la rete. Dopo la creazione del mondo associato alla scena, un’oggetto può essere inserito al suo interno: Mesh room = Mesh::create(100); room.setName(“Room”); float w1 = 2.5f; float w2 = 3.0f; float h = 1.5; I vertici che formano i muri sono aggiunti alla VertexBuffer associato alla Mesh della stanza. int v0 = room.getVertexBuffer().add(w1, w2, h, 0.0f, 0.0f); int v1 = room.getVertexBuffer().add(-w1, w2, h, 1.0f, 0.0f); int v2 = room.getVertexBuffer().add(w1, -w2, h, 0.0f, 1.0f); int v3 = room.getVertexBuffer().add(-w1, -w2, h, 1.0f, 1.0f); /* ... */ In seguito è creata una parte per ogni muro, aggiunti i due triangoli che lo formano e associato il materiale che descrive le proprietà di illuminazione e la texture di detta- glio da usare. ObejctPart3D wall = new ObjectPart3D(floorMaterial); wall.add(v0, v1, v2); wall.add(v0, v2, v3); La texture di dettaglio è creata e associata al materiale con il seguente codice. Texture texture = new Texture(String(“Floor”)); texture.createFromBitmap(“floor.bmp”); texture.download(); floor.setDetailMap(texture); 71
  • 80. Esempi d’uso Infine la Mesh della stanza è associata ad un’oggetto appena creato ed inserito suc- cessivamente nel mondo. Object3D object = new Object3D(room); world.add(object); Se la Mesh che rappresenta un oggetto della scena è memorizzato su disco, essa può essere caricata mediante il seguente codice. Mesh teacher = stream.load(); L’applicazione può desiderare di associare effetti particolari quali il bump mapping ad una parte della mesh con un codice simile al seguente. ObjectPart3D legs = chair.findPart(“legs”); Material material = legs.getMaterial(); material.setBumpMap(bumpmap, PEUCK3D_NORMAL_MAP); material.enableBumpmaping(); L’applicazione può costruire, in maniera semplice e intuiva, un mondo tridimen- sionale multi utente e controllare L’applicazione può costruire, in maniera semplice e intuiva, un mondo tridimen- sionale multi utente e controllarne le proprietà. La realizzazione di un’applicazione simile all’esempio dell’aula virtuale con le tecniche convenzionali, sarebbe estremamente più complicata costringendo il pro- grammatore a ricorrere a linee di codice notevolmente meno intuitive. Di seguito sono riportate delle fotografie che mostrano l’ambiente virtuale in esecuzione, dove, in par- ticolare, sono presenti due avatar: un’insegnante e uno studente. Il codice binario e le informazioni aggiornate riguardo al PEUCK possono essere trovate ai seguenti indirizzi: http://www.hipeco.polito.it/peuck http://www.peuck.it 72
  • 81. Esempi d’uso 73
  • 82. Esempi d’uso 74
  • 83. Esempi d’uso Side EffectZ Il progetto PEUCK è usato attualmente per lo sviluppo di un videogioco multiu- tente chiamato SFZ, che si basa sulla rappresentazione di un’ambientazione cittadina corredata da mezzi di locomozione. SFZ può mostrare le potenzialità del Sistema Operativo nella progettazione di ap- plicazioni commerciali fortemente orientate all’interattività quali i videogiochi inseriti in un mercato attualmente in forte sviluppo. La prima immagine mostra un’automobile composta di circa 15000 poligioni, sot- toposta ad una forte illuminazione frontale. Il calcolo dell’illuminazione è effettuato mediante il modello per vertici che fornisce buoni risultati grazie all’elevata tassella- zione dell’oggetto, sebbene sia possibile ancora notare alcuni artefatti nelle zone in cui il numero di poligoni è scarso. E’ particolarmente evidente il contributo fornito dalla componente speculare sulla portiera dell’automobile e sulle ruote. Le ruote sono, al contrario, illuminate mediante il modello per pixel, che ha per- messo l’applicazione di una mappa di rugosità ai cerchioni, che fa risaltare i dettagli geometrici di piccola scala. Le successive tre immagini (figura 17, 18, 19) mostrano una ripresa aerea notturna di uno dei quartieri che fanno parte della città in cui il videogioco è ambientato. Ogni palazzo è composto mediamente da circa 1000 poligoni; ogni immagine presenta 8/10 palazzi ed è ridisegniata mediamente 30 volte al secondo come è possibile leggere nell’angolo in alto a sinistra delle immagini. Il driver di rendering usato è generico e non ottimizzato per la particolare scheda grafica. Il rendering è a 32 bit, quindi alla massima profondità di colore. Questo calcolo fornisce un’indice indicativo dell prestazioni medie del motore tri- dimensionale presente all’interno del PEUCK, calcolabile in poligoni disegnati al se- condo: Oggetti 8 Poligoni per oggetto 1500 Frame rate 30 fps 360 Kpoly/sec La figura 20 mostra la ripresa della città di giorno con un driver ottimizzato a 800x600 16 bit. In figura è riportato il numero di poligoni e il frame rate: 75
  • 84. Esempi d’uso Poligoni nella scena 3340 Frame rate 81 fps 270 Kpoly/sec La figura 21 mostra una ripresa aerea della città a 1024x768 16 bit con driver otti- mizzato. Poligoni nella scena 11270 Frame rate 25 fps 280 Kpoly/sec La figura 25 mostra un’altra scena con il back face culling disabilitato e senza i problemi di overdraw evidenziati dalla città. La scena è resa in 1280x1024 a 32 bit. Poligoni nella scena 6750 Frame rate 81 fps 546 Kpoly/sec Con un minor overdraw, il throughput sale a circa 500k al secondo. La figura 26 mostra una ripresa aerea della scena precedente in 1280x1024 a 16 bit. Poligoni nella scena 6750 Frame rate 89 fps 600 Kpoly/sec Il passaggio dalla profondità di colore a 32 bit alla profondità a 16 bit evidenzia un aumento nel framerate di circa 8 fps. Questo calcolo semplificato è in linea con i risultati ottenuti nei test prestazionali che parlano di un “throughput” medio dai 300 mila ai 600 mila poligoni al secondo. Questo permette alle applicazioni di grafica in tempo reale di mostrare scene di circa 5/10 mila poligoni a piu’ di 30 fotogrammi al secondo, che permette una fluidità più che sufficiente per i livelli di interattività richiesti dalle applicazioni quali: 76
  • 85. Esempi d’uso • Videogiochi • Realtà virtuale • Progettazione assistita dal calcolatore • Architettura virtuale 77
  • 86. Esempi d’uso Figura 16. 1024x768 32 bit (driver generico) 78
  • 87. Esempi d’uso Figura 17. 1024x768 32 bit (driver generico) 79
  • 88. Esempi d’uso Figura 18. 1024x768 32 bit (driver generico) 80
  • 89. Esempi d’uso Figura 19. 1024x768 32 bit (driver generico) 81
  • 90. Esempi d’uso Figura 20. 800x600 16bit 82
  • 91. Esempi d’uso Figura 21. 1024x768 32 bit 83
  • 92. Esempi d’uso Figura 22. 1024x768 32 bit (nvidia) 84
  • 93. Esempi d’uso Figura 23. 1024x768 32 bit 85
  • 94. Esempi d’uso Figura 24. 1024x768 32 bit BFC off 86
  • 95. Esempi d’uso Figura 25. 1280x1024 32 bit BFC off 87
  • 96. Esempi d’uso Figura 26. 1280x1024 16 bit BFC off 88
  • 97. Esempi d’uso Figura 27. 1280x1024 32 bit BFC off 89
  • 98. Esempi d’uso Figura 28. 1280x1024 16bit BFC off 90
  • 99. Note tecniche 4. Note tecniche Per lo sviluppo e per il testing prestazionale del Sistema Operativo, abbiamo usato un PC IBM compatibile dotato della seguente configurazione: • Processore Intel Pentium III a 550 Mhz • 256 Mbyte di memoria centrale • Hard Disk IBM da 19 Gbyte • SB live 256 • Windows 2000 o BeOS 5.0 • Geforce 256 DDR 32 Mbyte I compilatori scelti per lo sviluppo sono il Visual C++ 6.0 e il Metrowerks Code- Warrior. Il progetto è stato sviluppato per piattaforma Win32 e BeOS di Be Inc. La librerie di rastering usate sono OpenGL della Silicon Grapghics in versione 1.2 (ambiente Win32 BeOS) e Microsoft DirectX in versione 7 (ambiente Win32). La schede grafiche acceleratrici sulle quali il progetto è stato testato sono la Gefor- ce 256 DDR di NVIDIA con 32 Mbyte di DRAM e la Voodoo 3500 di 3DFX con 32 Mbyte di RAM. 91
  • 100. Conclusioni e possibili sviluppi 5. Conclusioni e possibili sviluppi Conclusioni In questa tesi abbiamo sviluppato un Sistema Operativo ottimizzato per lo sviluppo di applicazioni grafiche tridimensionali ponendo una particolare attenzione per: • l’efficienza in modo da permettere alle applicazioni che ne fanno uso un alto numero di immagini sintetizzate al secondo con modelli dotati di un’alto numero di poligoni, consentendo all’utente la massima sensazione di fluidita per scene dotate di ottimo realismo • la semplictà d’uso e l’intuitività della gerarchia di classi, di modo da facili- tare il compito dello sviluppatore dell’applicazione e diminuire i tempi di sviluppo della stessa; il risultato è stato ottenuto seguendo in maniera dili- gente i dettami stilistici della programmazione orientata agli oggetti • la semplice estendibilità e manutenibilità del codice, per permettere un ve- loce aggiornamento delle tecniche e degli algoritmi data la velocità di svi- luppo delle metodologie sia software sia hardware alla base della Compu- ter Graphics, in particolare nel campo della grafica in tempo reale • la portabilità del codice, tale da ridurre al minimo i tempi di adattamento a diverse piattaforme hardware e software, così da presentare agli sviluppa- tori una piattaforma comune per lo sviluppo delle applicazione grafiche E’ possibile dunque impiegare il Sistema Operativo come piattaforma per lo svi- luppo di applicazioni grafiche che necessitano di un alto realismo delle scene e di un’alta interattività per mercati in rapido sviluppo quali ad esempio l’intrattenimento videoludico e la realtà virtuale usata per la progettazione in ambito industriale o la ri- cerca scientifica. Campi d’applicazione interessanti potrebbero essere l’arredamento virtuale in cui un’ambientazione può essere mostrata prima ancora che l’arredamento sia effettiva- mente eseguito. 92
  • 101. Conclusioni e possibili sviluppi Possibili Sviluppi I possibili sviluppi legati al PEUCK riguardano principalmente due settori: • ampliamento delle risorse messe a disposizione del programmatore • ottimizzazione e razionalizzazione del codice L’ampliamento delle risorse consiste nello sviluppare i moduli che non sono anco- ra stati toccati dalla fase realizzativi quali il modulo sonoro e buona parte del modulo di networking, che, al momento, realizza solo una semplice architettura client-server. Dal punto di vista del modulo che realizza il motore tridimensionale di renderin, molti sono gli algoritmi e le tecniche ancora da progettare e programmare per migliorare sia la resa grafica sia le prestazioni del sistema: • gestione di mesh progressiva e scelta dinamica del livello di dettaglio, così da realizzare un sistema in grado di adattarsi dinamicamente alle prestazio- ni del sistema, di modo da fornire un frame rate sempre accettabile anche degradando la qualità visiva • un modello di illuminazione che si basa su una versione semplificata del radiosità che sia possibile gestire in tempo reale per ricreare un’ombreggiatura degli oggetti più realistica del semplice modello usato • calcolo delle ombre portare dagli oggetti che conferisce profondità alla sce- na e un maggior realisimo • gestione avanzata di ambientazioni interne mediante l’uso di algoritmi di culling quali il portal rendering e il potentially visible set Il sottosistema grafico necessita, inoltre, di approfondire i seguenti punti: • supporto per le animazioni gerarchiche che costituiscono la base per ogni applicazione grafica di intrattenimento e la cui realizzazione attuale è proto- tipale • ottimizzazione del driver di rendering per aumentare il numero di primitive grafiche che escono dalla pipeline grafica nell’unità di tempo 93
  • 102. Conclusioni e possibili sviluppi • estensione e miglioramento del modulo di rete e supporto di architettura peer to peer 94
  • 103. Bibliografia 6. Bibliografia [1] Silbershatz A., P. Galvin (1997), quot;Operating System Conceptsquot;, Quinta Edizione, Addison Wesley Publishing Company [2] Foley (1990), “Computer Graphics. Principles and Practice” [3] Tomas Moller, Eric Hainess (1999), “Real-Time Rendering” [4] Alan Watt, Mark Watt (1996), “Advanced Animation and Rendering Techniques” [5] Alan Watt (2000), “3D Computer Graphics” [6] Rogers Adama (1990), “Mathematical Elements For Computer Graphics”, 2nd Edition [7] Phong Bui-Tuong (1973), “Illumination for computer-generated images”, PhD thesis, University of Utah [8] Aupperle L., Hanrahan P. (1993), “A Hierarchical Illumination Algorithm for Surface with Glossy Reflection“, Computer Graphics proceedings, Annual conference series 1993 [9] James Blinn, Martin Newell (1976), “Texture and Relflection in Computer generated images”, Comunications of the ACM [10] James Blinn (1977), “Models of Light Relflection for Computer Synthetized Pictures”, Computer Graphics (Proc. Siggraph 77) [11] James Blinn (1978), “Simulation of Wrinkled Surfaces”, Computer Graphics (Proc. Siggraph 78) [12] Mark Segal, Kurt Akeley (1998), “The OpenGL Graphics System: A specification (version 1.2.1)” [13] Andreas Schilling (1997), “Toward Real-Time Photorealistic Rendering: Challanges and Solutions”, Proc. 1997 Eurographics/Siggraph Workshop on Graphics Hardware, Los Angeles, California [14] Mark Segal, Carl Korbkin, Rolf van Widenfelt, Jim Foran, Paul Haeberli (1992), “Fast Shadows and Lighting Effects using Texture Mapping”, Computer Graphics (Proc Siggraph 92) 95
  • 104. Bibliografia [15] Mark Peercy, John Airey, Brian Cabra (1997), “Efficient Bump Mapping Hardware”, Computer Graphics (Proc. Siggraph 97) 96
  • 105. Bibliografia Indice delle figure Figura 1: L'architettura del PEUCK...............................................................................4 Figura 2. Primitive grafiche .........................................................................................13 Figura 3. Diagramma di Viewport ...............................................................................25 Figura 4. Gerarchia della telecamera ...........................................................................28 Figura 5. Gerarchia di class Moveable. .......................................................................30 Figura 6. Gerarchia di classi delle luci.........................................................................34 Figura 7. Gerarchia di classi delle texture ...................................................................41 Figura 8. Normali e normali perturbate .......................................................................47 Figura 9. Normale perturbata mediante rotazione .......................................................50 Figura 10. Ombra propria ............................................................................................51 Figura 11. Gerarchia delle Mesh..................................................................................57 Figura 12. Gerarchia di classi delle Parti .....................................................................62 Figura 13. Gerarchia degli oggetti ...............................................................................63 Figura 14. Architettura client server ............................................................................68 Figura 15. Sincronizzazione degli oggetti ...................................................................69 Figura 16. 1024x768 32 bit (driver generico)..............................................................78 Figura 17. 1024x768 32 bit (driver generico)..............................................................79 Figura 18. 1024x768 32 bit (driver generico)..............................................................80 Figura 19. 1024x768 32 bit (driver generico)..............................................................81 Figura 20. 800x600 16bit.............................................................................................82 Figura 21. 1024x768 32 bit..........................................................................................83 Figura 22. 1024x768 32 bit (nvidia) ............................................................................84 Figura 23. 1024x768 32 bit..........................................................................................85 Figura 24. 1024x768 32 bit BFC off............................................................................86 Figura 25. 1280x1024 32 bit BFC off.........................................................................87 Figura 26. 1280x1024 16 bit BFC off..........................................................................88 Figura 27. 1280x1024 32 bit BFC off..........................................................................89 Figura 28. 1280x1024 16bit BFC off...........................................................................90 97