:::TITOLO: TRUE BELIEVER MOBILE by Luca Ziliani, Davide Puopolo 773198 :::INTRODUZIONE: l'applicazione da noi sviluppata come progetto d'esame per il corso di Geometria Computazionale è un videogioco per dispositivi Smartphone e Tablet con sistema operativo Android. Il motore di rendering è stato sviluppato ex novo appoggiandosi alle librerie OpenGLES 2.0 Caratteristiche principali: -Motore di Rendering: .SceneManager .WidgetManager .Rendering oggetti 2D con Texture .Traslazioni oggetti 2D .EntityManager .Rendering entità3D con Texture .Rendering Mesh 3D con texsture e struttura scheletrica .AnimationManager .gestione delle animazioni scheletriche per keyframe .Color Picking: .CameraManager: .TextureManager: .ShaderManager: -Exporter\Importer per Blender: .Esportazione da Blender di Mesh, Texture, Scheletro, animazioni nel formato proprietario TBM .Importazione di Mesh, Texture, Scheletro, animazioni in formato TBM -MazeMagaer: .Generatore pseudocasuale di labirinti .importazione labiritni da file esportati dall editor .Gestione dell albero dei cammini per navigare nel labirito tramite Dijkstra -AImanager: .implementazione di Cerberus: AI engine basato sul Q-Learning -Editor di Livelli: .Generazione da parte degli utenti di livelli .Esportazione nel formato TBM -Multiplayer: .gestione Match tra due giocatori .gestione profilo giocatori -Motore Audio: .Riproduzione di effetti Sonori .Riproduzione di Musiche da file in streaming :::RICHIAMI DI TEORIA 1 Vertici Ogni punto dello spazio è definito da una terna di punti (px, py, pz) e viene considerato come destinazione di un vettore Vp centrato nell'origine degli assi del suo sistema di riferimento. Un poligono è semplicemente una lista ordinata di punti, e quindi di vettori, essendo ogni punto un vertice del poligono. Tutte le trasformazioni per il movimento dei poligoni vengono calcolate sui vettori dei suoi vertici. 1.1 Coordinate Omogenee In computer grafica vengono utilizzate le coordinate omogenee che permettono di rappresentare tutte le trasformazioni affini tramite operazioni matriciali. Esse sono utilizzate dentro al sistema grafico invece delle coordinate cartesiane. Un punto e' descritto da una terna di coordinate cartesiane P = (X,Y,Z). Lo stesso punto e' descritto da infinite quadruple di coordinate omogenee P = (x,y,z,w), tutte quelle per cui: w != 0 x/w = X y/w = Y z/w = Z Rappresentazione canonica: w=1, dunque P = (X,Y,Z,1). Le quadruple di coordinate omogenee con w=0, cioè della forma (x,y,z,0) identificano punti all'infinito nella direzione del vettore (x,y,z). Le coordinate omogenee consentono di rappresentare punti all'infinito (con w=0) come per esempio sorgenti luminose. Inoltre permettono di esprimere tutte le trasformazioni di coordinate in forma matriciale. Dato un punto P (vettore di 4 elementi) e una matrice di trasformazione M (matrice 4x4), il risultato di applicare la trasformazione a P e' dato da P' = M P (moltiplicazione matrice vettore). 2 Trasformazioni di Vertici Le trasformazioni dei vertici, quali rotazioni, traslazioni, scala e le differenti proiezioni (prospettiva e ortografica), possono essere rappresentate applicando un'appropriata matrice 4x4 alle coordinate del vertice. 2.1 Matrice di traslazione dove x,y,z è il valore della traslazione lungo il rispettivo asse. 2.2 Matrice di Scala dove x,y,z sono i fattori di scala lungo il rispettivo asse. 2.3 Matrice di Rotazione Attorno all'asse X Attorno all'asse Y Attorno all'asse Z dove a indica i gradi di rotazione. Il sistema grafico OpenGLES 2.0 fornisce istruzioni per definire: Matrici di traslazioni dato il vettore di traslazione (_tx,_ty,_tz) Matrix.translateM(_TranslateMatrix, 0, _tx, _ty, _tz); Matrici di scala con punto fermo l'origine dati i fattori di scala (sca_X, sca_Y, sca_Z) Matrix.scaleM(_ScaleMatrix, 0, sca_X, sca_Y, sca_Z); Matrici di rotazione attorno ad una retta passante per l'origine dato l'angolo di rotazione _angle ed un vettore (_rx,_ry,_rz) parallelo all'asse di rotazione Matrix.rotateM(mRotateMatrix, 0, _angle, _rx, _ry, _rz); L'ordine da sinistra a destra in cui scrivo le matrici nella moltiplicazione e' opposto all'ordine in cui applico le trasformazioni. Se P e' un punto, ed M1, M2 sono due matrici di trasformazione, la moltiplicazione M1 M2 P applica a P prima M2 e poi M1. 3 Gestione della scena 3.1 SceneManager L'oggetto SceneManager si occupa di mantenere una struttura dati che contiene i nodi della scena. Ogni nodo della scena contiene le matrici public float[] mTranslateMatrix; public float[] mRotateMatrix; public float[] mScaleMatrix; Inoltre contiene una lista di nodi della scena figli e un oggetto a cui verranno applicate le matrici di trasformazioni del nodo. Le trasformazioni di un nodo sono ottenute ereditando le trasformazioni dei nodi padri in aggiunta alle proprie trasformazioni. L'oggetto del nodo della scena può essere di tipo Entità o Camera. 3.1.1 Oggetto Entità L'oggetto entità contiene le informazioni relative alla geometria, ovvero una lista di vertici che ne determinano la forma geometrica. Per ogni vertice, a seconda del tipo di Entità a cui si riferiscono, avremo al più informazioni riguardanti: coordinate X,Y,Z, Vettore della Normale, Coordinate Texture UV, quattro riferimenti e quattro pesi delle Matrici di trasformazioni dello scheletro. 3.1.2 Oggetto Camera L'oggetto camera contiene due Matrici di proiezione. 3.1.2.1 Matrice di proiezione ortografica: caso particolare di proiezione parallela dove il fascio di rette e' perpendicolare al piano di proiezione. Il volume di vista è dato da un parallelepipedo. Tale parallelepipedo e' descritto dalle sue coordinate xyz minime e massime. Matrix.orthoM(orthoMatrix,0, x_min, x_max,y_min,y_max, z_min, z_max); Questa matrice viene passata allo Shader delle entità che compongono la parte bidimensionale della scena, come ad esempio i menu di gioco e i widgets. 3.1.2.2 Matrice di proiezione prospettica: Proietta con un fascio di rette centrate in un punto, non le mantiene le proporzioni (amplifica gli oggetti vicini, riduce quelli lontani). Il volume di vista è dato da un tronco di piramide. Tale volume di vista è definito tramite : fovy (Field Of View) = angolo di apertura della piramide in gradi rispetto all'asse Y. aspect = deformazione larghezza/altezza della base del tronco di piramide (in generale la si fa coincidere con quella della viewport). znear, zfar = distanza delle due basi del tronco di piramide dallo occhio (valori positivi) . setPerspective(perpMatrix, fovy, aspect, zNear, zFar); Questa matrice viene passata allo Shader delle entità che compongono la parte tridimensionale della scena come le entità di tipo Mesh3D 4 Ciclo di Rendering in OpenGL ES 2.0 4.1 OpenGL ES 2.0 OpenGL ES 2.0 eliminano la maggior parte della pipeline di rendering a funzioni fisse in favore di una programmabile: quasi tutte le funzioni di rendering della pipeline di transform & lighting, come il settaggio dei parametri di luci e materiali, vengono sostituite da vertex shaders e fragment shaders programmati separatamente. La CPU invia le istruzioni e le coordinate 3D della scena alla GPU. Nel vertex shader programmabile viene trasformata la geometria e vengono applicati alcuni effetti di illuminazione. La geometria viene riprodotta in triangoli, che vengono ulteriormente trasformati in fragment. Un fragment può essere pensato come l'insieme delle informazioni necessarie per colorare un pixel e per determinare se il fragment diventerà un pixel e quindi disegnato nel frame buffer. Per ogni fragment vengono applicati ulteriori effetti tramite il pixel shader. Viene effettuato il test di visibilità (z-test): se un pixel è visibile, viene scritto nel framebuffer per l'output su schermo. 4.1.1 Vertex Shader Il vertex shader è una sequenza di istruzioni, usate nella computer grafica, che permette di modificare gli attributi dei vertici della geometria assegnando il calcolo alla GPU presente sulla scheda video. I vertex shader possono modificare le coordinate dei vertici, i parametri di illuminazione (tra cui il colore), la parte di texture visualizzata, eccetera. Il vertex Shader viene eseguito per ogni vertice all'interno del frustum di visione. 4.1.2 Fragment Shader Il fragment shader o pixel shader è una sequenza di istruzioni, un breve programma, che viene eseguito da una GPU per ciascun pixel da elaborare. L'uso dei pixel shader consente di applicare effetti come bump mapping, ombre, esplosioni, effetti di diffrazione, rifrazione e la simulazione dell'effetto fresnel, permettendo una migliore simulazione degli effetti dell'illuminazione e un aspetto più realistico di superfici dalle proprietà ottiche particolari. 4.2 Implemetazione Ad ogni ciclo del Mainloop dell'applicazione viene eseguita la funzione di disegno per le componenti 2D e 3D della scena tramite la chiamata di: mSceneManager.draw(); mGuiManager.draw(); La classe SceneManager e GUIManager contengono rispettivamente gli alberi degli oggetti 3D e 2D che compongono la scena. Partendo dalla radice dell'albero, viene richiamata per ogni nodo la funzione drawEntity. Questa funzione prende in input le matrici di trasformazione World, View e Proj. La ViewMatrix rappresenta la le rototraslazioni necessarie per portarsi in coordinate camera, la ProjMatrix contiene le trasformazioni sul tipo di proiezione da applicare. SceneManager passa sempre una matrice di proiezione perspettica e GUIManager passa sempre una matrice di proiezione ortogonale. La WorldMatrix contiene le trasformazioni necessarie per posizionare l'oggeto in coordinate mondo. La WorldMatrix viene settata inizialmente alla matrice identità. Ogni SceneNode, dopo aver chiamato la procedura per disegnare la propria entità, passa ai nodi figli la sua WorldMatrix che è formata dalla WorldMatrix del padre moltiplicata per le sue trasformazioni locali. Questo procedimento permette di ereditare le trasformazioni dei nodi padri in modo gerarchico. La procedura di disegno consiste nel caricare nella GPU le informazioni riguardanti la geometria, le texture, gli shader e le matrici da utilizzare. Successivamente viene chiamata la funzione Opengl drawArrays che comporta l'inizio della fase di rendering. 5 Animazioni Scheletriche 5.1 Introduzione L'animazione tramite scheletro è una tecnica per la Computer Animation. Il modello 3D animato con questa tecnica dev'essere composto essenzialmente da due parti: una Mesh Poligonale e uno Scheletro. Una Mesh Poligonale è una collezione di vertici, spigoli e facce che definiscono la forma di un oggetto poliedrico . Nell'ambito delle animazioni in Real-Time la Mesh Poligonale viene prodotta da un tool di modellazione quali Maya o Blender e poi importata nel formato del motore di Rendering utilizzato. Essa è la parte del modello 3D che viene effettivamente disegnata (Renderizzata ) dall'applicazione. Lo Scheletro è un insieme gerarchico di ossa, utilizzato per animare la Mesh Poligonale a cui è associato. Un osso dello scheletro è composto da una trasformazione tridimensionale (che include la sua posizione, scala e orientamento ) . L'osso è associato ad una porzione di mesh (insieme di vertici ), questo procedimento di associazione viene chiamato Skinning. La trasformazione che effettua un osso figlio sulla sua porzione di mesh, è data dall'applicazione della trasformazione del padre, più la sua trasformazione. Una porzione di vertici può essere associata contemporaneamente a più ossa, ogni ossa contiene l'informazione di quanta influenza deve avere la sua trasformazione sulla porzione di vertici. Mediante questa tecnica e ad esempio possibile muovere l'intera gamba del personaggio applicando una rotazione all'osso femore. Questa trasformazione verrà propagata all'osso stinco e all'osso piede, ognuna di queste ossa eseguirà la trasformazione della porzione di mesh associata, creando il movimento della gamba, e i relativi effetti grafici di deformazione della mesh (contrazioni dei muscoli, pieghe della pelle, ecc.). Questa tecnica di animazione è quella che viene utilizzata per la produzione di animazioni nell'industria cinematografica e dei videogiochi. Tra le tecniche di animazione utilizzate per produrre animazioni tramite scheletro distinguiamo il Key-Frame Animation, Motion Capture Animation e Animazioni Procedurali. 5.2 Animazioni con key-frame La tecnica del Key-Frame è la più semplice utilizzabile per produrre animazioni. Il termine inglese Key-Frame (traducibile come fotogramma chiave) è un tipo di fotogramma che può rappresentare la posizione iniziale, finale o intermedia di un animazione, normalmente i momenti più rilevanti di una sequenza. Nell'animazione classica i fotogrammi chiave vengono disegnati dagli artisti principali, e la transizione tra un fotogramma chiave ed un altro viene disegnata dai suoi assistenti. Per la computer grafica le cose sono simili. L'animatore modifica l'orientamento delle ossa e lascia al software di animazione il compito di eseguire l'interpolazione tra i due fotogrammi (posizioni) chiave. 5.3 Implementazione 5.3.1 Importazione Mesh Per modellare un entità 3D abbiamo utilizzato il software di modellazione Blender. Una volta completato il modello abbiamo realizzato un exporter che esportasse la Mesh nel nostro formato TBM. Questo procedimento genera un file al cui interno sono memorizzate per ogni vertice: le coordinate X-Y-Z , le coordinate della Normale, le coordinate UV della Texture, fino a quattro identificativi di ossa che influenzano la posizione del vertice durante l'animazione, e i relativi pesi. Queste informazioni vengono importare nell'applicazione e successivamente utilizzate nella fase di disegno. 5.3.2 Importazione Scheletro Insieme alle informazioni relative alla mesh, durante la fase di esportazione, vengono esportare anche le informazioni relative allo scheletro. Dello scheletro vengono memorizzate la gerarchia delle ossa e per ogni osso la relativa matrice di trasformazione per portarlo in posizione iniziale. 5.3.3 Importazione Animazione Il File dell'animazione esportato da Blender contiene per ogni animazione una lista di KeyFrame che la compongono, con le relative informazioni temporali. Ogni KeyFrame contiene la Matrice di trasformazione locale di ogni osso. Per ottenere l'animazione completa occorre moltiplicare le matrici di trasformazione di ogni osso in modo ricorsivo partendo dall'osso Radice. In questo modo ogni osso figlio eredita le trasformazioni dai parenti creando una matrice di trasformazione globale. Questa matrice viene usata in fase di disegno dai vertici, per modificarne la posizione e generare l'animazione finale completa. 5.3.4 Elaborazione dei dati delle animazioni Come abbiamo detto nel punto 5.3.2 lo scheletro non è altro che un albero gerarchico di ossa con relativa matrice di trasformazione di posa iniziale (detta “Pose Matrix”). La Pose Matrix contiene generalmente una traslazione e una rotazione (e se previsto anche una scala). La traslazione viene applicata al relativo osso usando come sistema di riferimento il sistema di riferimento dell'osso padre (l'osso radice avrà come sistema di riferimento iniziale le coordinate mondo). Al termine della traslazione il sistema di riferimento non cambia. Si applica poi una rotazione, in questo modo si porta il sistema di riferimento in coordinate locali osso (dal sistema di riferimento del padre si passa al sistema di riferimento dell'osso figlio). In queste tre immagini viene mostrato un esempio banale di animazione scheletrica. L'immagine di sinistra corrisponde all'istante t0, quella centrale all'istante t1 e quella di destra all'istante t2. Lo scheletro è composto da due ossa 1 e 2. L'osso radice è il numero 1 e l'osso numero 2 è figlio dell'osso 1. Nell'immagine di sinistra vediamo le due ossa in posizione iniziale (notare gli assi dei due sistemi di riferimento in cima alle ossa), con sistema di riferimento uguale. Nell'immagine centrale abbiamo applicato una rotazione di 90° lungo l'asse X all'osso numero 2, e il suo sistema di riferimento è cambiato rispetto a quella dell'osso numero 1 (in precedenza i tre assi coincidevano, adesso l'unico asse comune ad entrambi è l'asse X essendo l'asse su cui è stata fatta la rotazione). Successivamente nella figura di destra abbiamo applicato una rotazione di -90° all'osso 1 sempre lungo l'asse X. Come si può notare la rotazione applicata all'osso 1 è stata ereditata dall'osso 2, tenendo come centro di rotazione il punto di origine dell'osso 1. Questa rotazione ereditata dall'osso 1 la si può vedere come una traslazione e poi una rotazione direttamente applicata all'osso 2 (la traslazione traslerebbe l'osso 2 dalla sua origine locale in su lungo il proprio asse -z visibile nella figura centrale). In ogni KeyFrame sono descritte delle trasformazioni per ogni osso. Queste sono le trasformazioni locali delle ossa. Applicare una trasformazione locale ad un osso vuol dire applicarla a tutti i vertici associati all'osso (a seconda del peso dell'osso sul vertice, se previsto), ma la posizione dei vertici è descritta secondo il sistema di riferimento del modello 3D, non secondo il sistema di riferimento dell'osso. Bisogna quindi portare i vertici associati all'osso nelle coordinate dell'osso stesso e poi applicare le trasformazioni del KeyFrame. Per fare questo occorre memorizzare per ogni osso una matrice di trasformazione che tenga traccia delle trasformazioni gerarchiche delle ossa parenti (per l'osso radice questa matrice corrisponde alla matrice identità) e invertirla così da ottenere una matrice inversa che porti l'osso in coordinate locali. BonePoseMatrix = ParentTransformMatrix * LocalTransformMatrix; InverseTransformMatrix = (BonePoseMatrix) -1 Ottenuta questa matrice possiamo applicare le trasformazioni del KeyFrame e successivamente in coordinate globali moltiplicando per BonePoseMatrix. globalFrameTransformMatrix = BonePoseMatrix * localFrameTransformMatrix * InverseTransformMatrix Inoltre bisogna ereditare dalle ossa parenti le trasformazioni che hanno fatto nel frame e quindi si moltiplicano alla globalFrameTransformMatrix le parentGlobalFrameTransformMatrix. FrameTransformMatrix = (rootFrameTransformMatrix * . . . . * parentGlobalFrameTranformMatrix) * globalFrameTranformMatrix A questo punto abbiamo ottenuto frameTranformMatrix, che sarà applicata a tutti i vertici associati all'osso e verrà ereditata da tutte le ossa figlie. Questo procedimento dovrà essere fatto anche per le normali dei vertici. Quando un vertice viene ruotato in un'animazione anche la normale associata ruoterà in base alle trasformazioni subite dal vertice. Le normali ricavate da questa trasformazione verranno poi utilizzate nella fase shading in cui vengono creati gli effetti di luce. 6 Level Editor Per costruire i livelli di gioco abbiamo implementato un software stand-alone in grado di generare livelli su file importabili da TrueBeliever Mobile tramite un proprio importer. Il level editor è un tool che permette di generare labirinti e popolarli con unità nemiche. Per ogni unità controllata dalla CPU è possibile scegliere la modalità di intelligenza artificiale: Modalità Hold – l'unità stà ferma in attesa dell'ingaggio. Modalità Patrol – l'unità si muove secondo un percoso scelto dal creatore del livello. Modalità Cerberus -l'unità nemica è controllata dall'algoritmo di intelligenza artificiale Cerberus da noi implementato per il corso di Sistemi Intelligenti. Il tool permette di posizionare le basi per entrambi i player, trappole e power-up, oltre che posizionare i muri che compongono il labirinto. :::MANUALE UTENTE 1 Introduzione True Believer Mobile è un gioco di strategia in tempo reale sviluppato per dispositivi Android. In questa breve Demo Giocabile si avrà la possibilità di vedere il motore di rendering da noi sviluppato in azione. 2 Regole Lo scopo del gioco è quello di conquistare la base nemica portandoci sopra una propria unità prima che il nemico conquisti la propria. Le basi dei giocatori sono rappresentate dalla seguente icona. Nella mappa di gioco sono presenti delle trappole rappresentate dalla seguente icona. Le trappole uccidono istantaneamente qualsiasi unità gli passi sopra. In questa Demo Giocabile l'utente ha a disposizione due unità: Beast e Hunter. L'unità Beast attacca in corpo a corpo. Essa vince uno scontro corpo a corpo contro un Hunter e rimane in stallo se coinvolta in uno scontro corpo a corpo con un altra Beast. Per portare la Beast in combattimento è necessario muoverla vicino ad un unità nemica. L'unità hunter attacca a distanza. L'arma dell Hunter ha una gittata di 3 caselle. Quando un nemico entra a distanza di tiro con un Hunter comparirà su di esso un mirino Giallo. Toccando il mirino l'Hunter comincerà a sparare sul nemico. 2.1 Come schierare in campo un unità: Per ogni livello il numero di unità che si possono schierare dalla panchina è variabile. Il DeployButton [situato nella parte alta destra del display] indica il numero di unità che si possono schierare. Come viene mostrato nella figura sopra, le icone sotto il bottone di Deploy indicano il numero di unità che si possono schierare. Quando una unità viene schierata il l'icona diventerà da grigia a rossa. Quando un unità muore l'icona diventerà da rossa a nera. Per schierare un unità è necessario premere sul bottone di Deploy [1]. A questo punto viene visualizzata la panchina, con le unità disponibili per superare il livello. Scegliere l'unità desiderata premendo sulla sua icona[2].Compariranno delle celle evidenziate sulla mappa, queste celle rappresentano le postazioni in cui è possibile schierare l'unità precedentemente selezionata. Per schierare l'unità premere su una cella evidenziata[3]. Per poter schierare un ulteriore unità è necessario aspettare che la barra di Deploy si ricarichi, al termine del caricamento lo barra comincerà a lampeggiare e sarà possibile schierare un'altra unità. 2.2 Come muovere un unità sul campo Dopo aver schierato un unità è possibile muoverla all'interno del labirinto. Come prima cosa è necessario selezionare l'unità. Per selezionare un unità è necessario premere su di essa[1]. Una volta selezionata l'unità comparirà un indicatore azzurro sulla sua cella corrente. Dopo aver selezionato un unità premere sulla cella del labirinto in cui la si vuole far muovere[2]. 2.3 Sparare con un unità tipo Hunter. L'unità Hunter ha la possibilità di sparare contro un unità nemica a 3 celle di distanza. Per effettuare un azione di sparo, selezionare l'unità hunter e muoverla a tre celle di distanza dal nemico. Una volta giunti a tre celle di distanza comparirà un icona triangolare gialla sul bersaglio. Per sparare al nemico è necessario premere sul mirino[1]. Una volta premuto sul mirino esso diventerà rosso e l'unità huner comincerà a sparare fino ad ucciderlo.