Academic Club Website

Categorie Articoli

Primi passi con XNA - Parte 2

Primi passi con XNA - Parte 2

by Petrucco Marco


Costruzione della scena

Continuiamo la nostra panoramica su XNA con questa seconda parte, nella quale parleremo della costruzione della scena.

Dopo aver definito tutti gli oggetti presenti sulla scena (avendoli importati con modelli esterni o avendoli creati definendo i vertici) e le loro proprietà (texture ed effetti da appliccare), è ora possibile disegnare il tutto su schermo.

Tale operazione viene svolta dalla procedura Draw().

Il primo gruppo di comandi che incontriamo ci permette di configurare le opzioni di visualizzazione ed altri parametri ad essa relativi: in primo luogo specifichiamo se vogliamo una visualizzazione a wireframe o meno, con il seguente comando:

device.RenderState.FillMode = FillMode.WireFrame;

Sebbene questo sia abbastanza inutile per l’utente generico, a scopo didattico può essere molto interessante studiare la composizione della scena.

Eseguendo tale codice, possiamo scoprire che già una scena relativamente semplice come quella presentata in figura 07 ha una composizione relativamente complessa, come si può notare in figura 08.

Scena texturizzata

Immagine 07 – Scena texturizzata

Scena in wireframe

Immagine 08 – Scena in wireframe

Ovviamente durante la fruizione normale del software, tale opzione non sarà utilizzata!

Vi sono inoltre altri due importanti comandi da analizzare, comandi che dovranno essere eseguiti per primi ogni volta che si richiama la procedura; questi sono:

device.RenderState.CullMode = CullMode.None;

device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Blue, 1.0f, 0);

Il primo  serve ad abilitare o meno il "Culling" e lo utilizziamo per dire a XNA di disegnare tutti i triangoli, anche quelli che non sono rivolti dalla parte della telecamera.

Ricordiamo infatti che XNA normalmente disegna solo i triangoli definiti in senso orario rispetto al punto di vista dell’utente! Questo potrebbe crearci dei problemi con alcuni oggetti (come ad esempio gli avatar degli altri utenti): sebbene ci siano più modi per superare tali problemi, data la natura semplice di questa introduzione, ci limiteremo a disabilitare il Culling.

Il secondo comando serve invece a pulire il buffer della scheda video ed è quindi importante che venga richiamato prima di iniziare le operazione di disegno del frame successivo.

Ricordiamo che XNA disegna prima nel buffer video, invece di disegnare direttamente nella finestra, ed alla fine dell'esecuzione del metodo "Draw" il contenuto del buffer viene copiato nella finestra vera e propria.

La SkyBox  

Conclusa l’impostazione delle opzioni di visualizzazione, è ora possibile disegnare i vari elementi della scena. La prima procedura ad essere richiamata è la ViewSkyBox(), che come si può intuire dal nome si occupa di disegnare la skybox.

private void ViewSkyBox(Matrix worldMatrix)

{

int i = 0;

    foreach (ModelMesh modmesh in skybox.Meshes)

    {

       foreach (Effect currenteffect in modmesh.Effects)

       {   

                    currenteffect.CurrentTechnique = currenteffect.Techniques["Textured"];

                    worldMatrix = Matrix.CreateRotationX((float)Math.PI / 2) * Matrix.CreateScale(4, 4, 4) *                                                 Matrix.CreateTranslation(MyAvatarPosition);

                    currenteffect.Parameters["xWorld"].SetValue(worldMatrix);

                    currenteffect.Parameters["xView"].SetValue(viewMatrix);

                    currenteffect.Parameters["xProjection"].SetValue(projectionMatrix);

                    currenteffect.Parameters["xTexture"].SetValue(skyboxtextures[i++]);

        }

    modmesh.Draw();

    }

}

Questa procedura applica le texture definite in skyboxtextures[] alle singole mesh modmesh del modello skybox utilizzando l’effetto currenteffect con la tecnica Textured. Inoltre si occupa di scalare il modello (grazie al metodo CreateScale), aumentandone le dimensioni di 4 volte.

Da notare che la skybox si muove seguendo la posizione del nostro avatar, in modo tale che questo sia sempre rinchiuso all’interno dell’ambiente artificiale. Date le dimensioni della skybox, l’utente ovviamente non si accorge di questo ed ha la sensazione che il tutto rimanga fisso nella sua posizione.

La skybox come di consueto si presenta come un grande cubo che racchiude tutta la scena: noi ci dobbiamo solo preoccupare di trovare le giuste tessiture da applicare alle pareti interne e procedere alla loro mappatura.

Ovviamente per evitare degli spiacevoli artefatti (ad esempio delle evidenti discontinuità grafiche in presenza degli spigoli del cubo), è necessario disporre di immagini create ad hoc per essere utilizzate in questo contesto.

Per tale motivo ci siamo procurati tramite internet delle trame liberamente utilizzabili, che possono essere apprezzate dal lettore nelle seguenti immagini.

Skybox

Immagini 09, 10, 11 – Skybox: back, bottom, front + Immagini 12, 13, 14 – Skybox: left, right, top


Puntando lo sguardo nello spigolo di intersezione di tre facce, possiamo osservare che le texture collimano perfettamente e l’utente non si accorge della transizione da un poligono all’altro.

Angolo

Immagine 15 – Angolo texturizzato della skybox

Angolo

Immagine 16 – Angolo in wireframe della skybox in figura 15

Oltre alla chiamata ViewSkyBox(), ritroviamo le seguenti procedure, dall’ovvio significato:

View3DLibrary()   : per la visualizzazione della libreria 3d;

ViewDoorTexture() : per l’inserimento delle porte nelle stanze;

ViewMyAvatar()    : posizionamento dell’avatar dell’utente;

DrawAvatarUtenti(): inserimento degli avatar degli altri utenti.

 

La texturizzazione della libreria

Il metodo di texturizzazione della biblioteca è leggermente differente da quello utilizzato con la skybox: ricordiamo infatti che utilizziamo una singola immagine come contenitore di tutte le trame da applicare alla struttura: sarà proprio questo file ad essere passato come parametro all’effetto con l’istruzione effect.Parameters["xTexture"].SetValue(scenerytexture).

Osserviamo la procedura nella sua interezza:

 

private void View3DLibrary(Matrix worldMatrix)

 {

 effect.Parameters["xTexture"].SetValue(scenerytexture);

 effect.Begin();

 foreach (EffectPass pass in effect.CurrentTechnique.Passes)

  {

   pass.Begin();

   device.VertexDeclaration =

   new VertexDeclaration(device, VertexPositionNormalTexture.VertexElements);

   device.DrawUserPrimitives(PrimitiveType.TriangleList, verticesarray, 0,       verticesarray.Length / 3);

   pass.End();

  }

 effect.End();

 }

Come nel caso della SkyBox, anche in questo caso utilizziamo la tecnica “Textured”, con l’accortezza di indicare che stiamo usando VertexPositionNormalTexture al posto di semplici VertexPositionTexture: ossia nei dati inviati alla tecnica, ci saranno anche le normali per la gestione dell’illuminazione. 

new VertexDeclaration(device, VertexPositionNormalTexture.VertexElements);

 

Fatto questo, possiamo passare a disegnare i relativi triangoli, definendo il tipo di primitiva che passeremo alla procedura (TriangleList), il vettore con i dati dei vertici (verticesarray), l’eventuale offset (in questo caso nullo) ed il totale delle primitive (verticesarray.Length / 3).

 

La seconda procedura, ViewDoorTexture(), serve a disporre le porte all’interno dell’ala attualmente visitata: tali porte recano il nome delle altre alee alle quali si può accedere (e corrisponderebbero alle sottocategorie della wiki che si possono raggiungere partendo dal punto in cui ci troviamo).

Per renderizzare ogni porta utilizziamo una funziona generica, ViewObjectTexture(), che si limita a creare un oggetto bidimensionale nella scena e ad applicargli una determinata texture.

ViewObjectTexture(worldMatrix, textureDoorA, VerticesDoorA);

Il codice che ne governa il funzionamento è lo stesso delle procedure viste sino ad ora, ed è alla base della quasi totalità degli elementi posizionati nella scena e creati mediante definizione dei vertici.

private void ViewObjectTexture(Matrix worldMatrix, Texture2D textureObject, VertexPositionTexture[] VerticesObject)

        {

            effect.CurrentTechnique = effect.Techniques["Textured"];

            effect.Parameters["xWorld"].SetValue(worldMatrix);

            effect.Parameters["xTexture"].SetValue(textureObject);

            effect.Begin();

            foreach (EffectPass pass in effect.CurrentTechnique.Passes)

            {

                pass.Begin();

                device.VertexDeclaration = new VertexDeclaration(device, VertexPositionTexture.VertexElements);

                device.DrawUserPrimitives(PrimitiveType.TriangleList, VerticesObject, 0, 2);

                pass.End();

            }

            effect.End();

        }

 

La necessità di un puntatore

La terza procedura che abbiamo incontrato nella lista di quelle richiamate dal metodo Draw()è ViewMyAvatar().

Questa, seppur simile alle altre, si differenzia per la particolare tecnica utilizzata per colorare il modello tridimensionale: si utilizza infatti la "Colored" al posto della "Textured". Inoltre, vengono utilizzate alcune interessanti opzioni offerte da XNA, come i quaternioni.

        private void ViewMyAvatar(Matrix worldMatrix)

        {

            foreach (ModelMesh modmesh in MyAvatarModel.Meshes)

            {

                foreach (Effect currenteffect in modmesh.Effects)

                {

                    currenteffect.CurrentTechnique = currenteffect.Techniques["Colored"];

                    worldMatrix = Matrix.CreateScale(0.00205f, 0.00205f, 0.00205f) * Matrix.CreateRotationX((float)Math.PI / 2) * Matrix.CreateRotationZ((float)Math.PI) * Matrix.CreateFromQuaternion(MyAvatarRotation) * Matrix.CreateTranslation(MyAvatarPosition);

                    currenteffect.Parameters["xWorld"].SetValue(worldMatrix);

                    currenteffect.Parameters["xView"].SetValue(viewMatrix);

                    currenteffect.Parameters["xProjection"].SetValue(projectionMatrix);

                }

                modmesh.Draw();

            }

        }

Se andiamo a studiare la relativa tecnica presente nel file effects.fx possiamo notare che il relativo pixel shader è così definito:

PixelToFrame ColoredPS(VertexToPixel PSIn)

{

      PixelToFrame Output = (PixelToFrame)0;        

      Output.Color = PSIn.Color*clamp(PSIn.LightingFactor + xAmbient,0,1);

      return Output;

}

Essenzialmente creiamo la struttura di output e vi poniamo il colore ricevuto dal vertex shader, opportunamente modificato per tener conto dell’illuminazione alla quale l’oggetto è sottoposto.

Il nostro avatar sarà quindi rappresentato da un modello colorato con un colore a tinta unita, al quale sarà applicato un semplice algoritmo di illuminazione.

Poiché si è pensato che la navigazione della biblioteca fosse più naturale se attuata in prima persona, NON facciamo percepire all’utente la presenza di un vero e proprio avatar, ma importiamo una sfera (che NOI programmatori useremo come avatar dell’utente) e la utilizzeremo come puntatore (o “mirino”, che dir si voglia), per far capire all’operatore dove sta puntando in un dato istante.

Nella seguente immagine è possibile osservare l’avatar (ingrandito di 10 volte) e la sua illuminazione.

Ingradimento dell’avatar con dettaglio di illuminazione

Immagine 17 – Ingradimento dell’avatar con dettaglio di illuminazione

Di seguito possiamo invece osservare la sua struttura in wireframe:

Wireframe dell’avatar

Immagine 18 – Wireframe dell’avatar

 

Prima di addentrarci nell’analisi della restante parte di codice (ed in particolar modo dei quaternioni), è opportuno aprire una parentesi su due aspetti che abbiamo incontrato alcune volte sino ad ora, ma sui quali non abbiamo ancora discusso, ossia:

-         la struttura dei modelli tridimensionali .x che stiamo utilizzando;

-         la gestione dell’illuminazione nella nostra scena.

 

I modelli X

I modelli con i quali lavoriamo sono delle strutture dati che (ovviamente) contengono tutte le informazioni per disegnare l’oggetto che rappresentano. Contengono la posizione dei vertici, le informazioni sulle normali, sul colore e, se necessario, sulle texture associate alle singole mesh.

Le informazioni geometriche vengono memorizzate nei vertexbuffers e negli indexbuffers.

Ovviamente i modelli più complessi sono costituiti da più parti; risulta perciò necessario un metodo per accedere a questi dati ulteriori.

Il file del modello salva tutti i vertici in un grande vertexbuffer ed ogni singola parte del modello mantiene memorizzati gli indici dei vertici che la definiscono.

Il modello salva tutti gli indexbuffers (per tutte le parti del modello) uno dopo l’altro, in un grande indexbuffer.

Il tutto è ben schematizzato nella seguente immagine:

Struttura dei modelli “.x”

Immagine 19 – Struttura dei modelli “.x”

Quindi ogni parte del modello contiene le informazioni che indicano in quale parte dell’indexbuffer andare a recuperare i dati sensibili.

Inoltre, ogni parte contiene un effetto e (se necessaria) la relativa texture da utilizzare su quel particolare oggetto; in questo modo ogni singola componente può avere una resa grafica completamente differente dalle altre parti del modello a cui appartiene.

 
L’illuminazione

L’illuminazione complessiva della scena è affidata ad una semplice luce direzionale, definita alla fine della procedura LoadEffect().

effect.Parameters["xEnableLighting"].SetValue(true);

effect.Parameters["xLightDirection"].SetValue(new Vector3(0.6f, -1, -0.9f));

Inoltre, tutti i vertici degli oggetti presenti nella scena dispongono di una normale: i vertici della nostra biblioteca tridimensionale hanno tali informazioni perchè le abbiamo inserite noi in fase di creazione degli stessi, mentre tutti i modelli importati dall’esterno presentano tali dati direttamente al loro interno.

Oltre alla luce direzionale, è stata abilitata anche quella ambientale, ossia la luce riflessa dagli altri oggetti.

effect.Parameters["xAmbient"].SetValue(0.5f);

 

I quaternioni

Per collegare l’avatar dell’utente con i movimenti di quest’ultimo è stato utilizzato un quaternione, in modo tale che la camera segua sempre il movimento del modello, sistemandosi dietro di lui, senza che le rotazioni o le traslazioni dello stesso ne compromettano la posizione.

Per far questo sono state introdotte due variabili che contengono la posizione e la rotazione dell’avatar all’interno del mondo tridimensionale: MyAvatarPosition e MyAvatarRotation.

Vector3 MyAvatarPosition = new Vector3(8, 4, 2);

Quaternion MyAvatarRotation = Quaternion.Identity;

Per posizionare correttamente l’avatar, è sufficiente modificare la matrice worldMatrix tenendo in considerazione queste due variabili.

worldMatrix = ... * Matrix.CreateFromQuaternion(MyAvatarRotation) * Matrix.CreateTranslation(MyAvatarPosition);

Per posizionare la camera dietro l’avatar richiamiamo la procedura UpdateCamera(), un metodo che crea nuove matrici viewMatrix e projectionMatrix che dipendono dalla posizone e dalla rotazione corrente dell’avatar.

In essa viene dichiarato un nuovo vettore (CameraPosition) che definisce dove vogliamo che si trovi la nostra camera, relativamente alla posizione dell'avatar; poichè  desideriamo che si posizioni un po' dietro e leggermente sopra l'avatar, è sufficiente modificare soltanto le coordinate Y e Z.

Vector3 CamPos = new Vector3(0, -0.6f, 0.1f);

 
Il vettore deve quindi essere ruotato allo stesso modo dell'avatar, nonchè traslato in corrispondenza della posizione della mesh.

CamPos=Vector3.Transform(CamPos,Matrix.CreateFromQuaternion(MyAvatarRotation));

CamPos=Vector3.Transform(CamPos,Matrix.CreateTranslation(MyAvatarPosition));

Poichè quando si definisce la viewMatrix si ha bisogno, oltre che della posizione e dell’obiettivo della camera, anche del vettore che indica l’UP, è necessario calcolare anche quest’ultimo: si definisce un vettore che punta verso l’alto e lo si ruota utilizzando la matrice di rotazione dell’avatar.

Vector3 CamUp = new Vector3(0, 0, 1);

CamUp=Vector3.Transform(CamUp,Matrix.CreateFromQuaternion(MyAvatarRotation));

Ora è possibile calcolare anche le matrici (viewMatrix e projectionMatrix) della camera:

viewMatrix = Matrix.CreateLookAt(CamPos, MyAvatarPosition, CamUp);

projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, (float)this.Window.ClientBounds.Width / (float)this.Window.ClientBounds.Height, 0.2f, 500.0f);

In questo modo abbiamo posizionato la telecamera dietro l’avatar dell’utente, settandola in modo tale che segua sempre i suoi movimenti.

 

La rappresentazione degli utenti

L’ultima procedura che rimane da analizzare è quella che disegna gli altri utenti nella libreria.

Il codice utilizzato in essa è lo stesso che abbiamo analizzato in precedenza (infatti si ri-utilizza la tecnica “Textured”), con la differenza che in questo caso usiamo anche il canale alfa per la gestione delle trasparenze.

Tale differenza si concretizza a livello pratico con l’introduzione di quattro istruzioni, tre delle quali dichiarate prima del rendering dell’oggetto.

device.RenderState.AlphaBlendEnable = true;

device.RenderState.SourceBlend = Blend.One;

device.RenderState.DestinationBlend = Blend.One;

La prima serve ovviamente per abilitare la gestione del canale dedicato alle trasparenze, mentre le rimanenti due definiscono il fattore di trasparenza del colore: SourceBlend rappresenta il valore per il quale moltiplicare il colore del pixel d’origine prima di sommarlo al pixel di destinazione, per produrre il colore dato dalla loro unione. DestinationBlend è la stessa cosa, ma relativa al pixel di destinazione.

Successivamente a queste tre, troviamo il ciclo foreach per la texturizzazione dei modelli, seguito dal comando device.RenderState.AlphaBlendEnable = false;

Terminata la rappresentazione dell’oggetto, si disabilita quindi il rendering con trasparenze.

Per consentire la massima flessibilità di personalizzazione degli avatar, abbiamo deciso di utilizzare delle immagini definibili dall’utente: per la demo sono state realizzate due figure stilizzate su sfondo nero (colore che verrà filtrato dall’algoritmo di gestione delle trasparenze), ma qualsiasi persona può collegare il proprio avatar con un’immagine a sua scelta, semplicemente rispettando i vincoli di formato (quest’ultimo deve essere ovviamente supportato da XNA – noi consigliamo jpg o png) e sfondo (che deve essere nero).

Nel nostro caso abbiamo utilizzato le seguenti due immagini:

Texture degli avatar usate per la demo          

Immagini 20, 21 – Texture degli avatar usate per la demo

La loro visualizzazione nella scena finale è la seguente:

Visualizzazione avatar con gestione della trasparenza

Immagine 22 – Visualizzazione avatar con gestione della trasparenza


Con questo chiudiamo questa seconda analisi su XNA, per concludere con il prossimo capitolo, nel quale si parlerà dello spostamento degli oggetti nel nostro mondo tridimensionale, ponendo particolare attenzione al rilevamento delle collisioni.

 

Disclaimer

I contenuti presentati in tale documento sono offerti dall’autore  in forma gratuita, al meglio delle sue capacità.

Nel documento sono state inserite delle immagini gratuitamente reperibili su internet.

L’autore ed Academic Club declinano ogni responsabilità, diretta e indiretta, nei confronti degli utenti e in generale di qualsiasi terzo, per eventuali imprecisioni, errori, omissioni, danni (diretti,indiretti, conseguenti, punibili e sanzionabili) derivanti dai suddetti contenuti.

 

Published mar 13 2008, 08.34 by Marco Petrucco
Filed under:

Comments

No Comments
Syndication
Archive