Academic Club Website

Categorie Articoli

Primi passi con XNA - Parte 1

Primi passi con XNA - Parte 1

by Petrucco Marco

 

Un primo semplice ambiente tridimensionale

Per questa prima analisi delle potenzialità di XNA riprendiamo un progetto che il sottoscritto ha presentato, insieme ad alcuni suoi colleghi, alla competizione Microsoft “Imagine Cup 2007 – Software Design” (www.imaginecup.com).

La caratteristica principale di tale applicazione consisteva nella rappresentazione 3D di una wiki on-line, sotto forma di una biblioteca tridimensionale generata in tempo reale tramite XNA, partendo dalla struttura ad indice tipica delle wiki.

Quello che otterremo alla fine della nostra analisi sarà un ambiente texturizzato, dotato di SkyBox e completamente esplorabile grazie al nostro avatar, con in più una semplice logica di rilevamento delle collisioni.

Il risultato che si otterrà una volta eseguito il programma sarà simile a questo:

Biblioteca realizzata tramite XNA

Immagine 01 – Biblioteca realizzata tramite XNA

 

Ovviamente non parleremo nel dettaglio di come funziona il procedimento di creazione, partendo dal documento html sino a giungere ai metadati necessari per la definizione della scena tridimensionale, ma concentreremo la nostra attenzione sulle tecniche utilizzate per generare tale ambiente 3D e sulle problematiche riscontrate per realizzare il tutto.

L’idea alla base del progetto è quella di fornire un nuovo paradigma di interazione per gli utilizzatori delle wiki online: si desidera offrire la possibilità di navigare la wiki in un ambiente tridimensionale, slegandosi dalla staticità delle pagine ipertestuali e nel contempo in un modo più vicino alle abitudini delle persone con scarsa alfabetizzazione informatica.

Come già detto, la wiki è rappresentata come una grande biblioteca, con tante sezioni quante sono quelle presenti nel sito; l’utente ha la capacità di spostarsi  liberamente da una sezione all’altra mediante delle porte che recano il nome della sottocategoria alla quale conducono.

Durante l’esplorazione dell’ambiente 3D l’utente ha la possibilità di incontrare gli avatar di gente che come lui ha optato per questa modalità alternativa di ricerca; qui sta la seconda differenza fondamentale del nostro progetto rispetto ad una semplice navigazione tramite ipertesto, ossia il fatto che posso incontrare le persone che in quel momento stanno navigando nella wiki, e magari si stanno documentando sugli stessi argomenti che sono di mio interesse.

Se raggiungo una sezione che mi interessa, avvicinandomi agli scaffali posso leggere i titoli dei libri presenti, titoli che corrispondono ai topic fisicamente presenti nella wiki disponibile online.

A questo punto posso leggere uno di questi, semplicemente cliccandoci sopra.

 

Logica di funzionamento

Nella seguente immagine è rappresentata la logica di base di tale processo.

Costruzione della scena con XNA

Immagine 02– Costruzione della scena con XNA

 

Nella sua semplicità la precedente immagine espone i tre punti chiave del progetto: la realizzazione di un set di metadati per contenere le informazioni della biblioteca, la generazione vera e propria dell’ambiente e lo spostamento dell’avatar dell’utente.

Data la natura introduttiva di questo saggio, non parleremo dei dettagli più raffinati del progetto, come ad esempio l’applicazione (ai modelli dei libri) delle texture generate in tempo reale partendo dal contenuto dell’indice della wiki, ma ci limiteremo a spiegare come renderizzare un ambiente 3D con XNA partendo dagli oggetti base, ossia gli spigoli ed i vertici dei modelli tridimensionali, nei quali mapperemo poi delle particolari immagini.

 

La piantina 2D

Come piantina iniziale utilizzeremo una matrice di interi, costituita da tanti “0” e “1”: in presenza di un “uno” avremo uno scaffale ed in presenza di uno “zero” avremo una porzione di pavimento.

La piantina utilizzata per la creazione della biblioteca in figura 01 è ad esempio questa:

Piantina di un’ala della biblioteca

 

La generazione dell’ambiente 3D

La generazione della scena tridimensionale è ovviamente una delle parti più complesse tra quelle realizzate ed è strutturata nel seguente modo:

Generazione ambiente 3D

Immagine 04 – Generazione ambiente 3D

 

Analizziamo ora le operazioni che stanno alla base del processo di generazione della scena.

 

Inizializzazione della periferica di visualizzazione

Le operazioni di setup iniziali sono abbastanza semplici: in primo luogo specifico quale dispositivo video utilizzare, dichiaro le dimensioni del buffer, se lavoriamo a pieno schermo ed il titolo della finestra.

        private void SetUpXNADevice()

        {

            // Specifichiamo con quale dispositivo lavorare.

            device = graphics.GraphicsDevice;

// Definiamo le dimensioni del buffer            graphics.PreferredBackBufferWidth = 800;

            graphics.PreferredBackBufferHeight = 480;

            // Proprietà

            graphics.IsFullScreen = false;

            graphics.ApplyChanges();

            // Titolo

            Window.Title = "Academic Club – Primi passi con XNA";

        }

Il dispositivo graphics viene generalmente settato subito dopo la partenza del programma, nel metodo principale Game1().

       public Game1()

        {

            graphics = new GraphicsDeviceManager(this);

            content = new ContentManager(Services);

        }

 

Caricamento degli effetti

Tutte le operazioni che possiamo eseguire sugli oggetti della scena sono definite in particolari file con estensione “.fx”. In ognuno di essi possono essere definite più tecniche di elaborazione, che grazie ai Pixel e Vertex Shaders permettono di mappare ogni singolo pixel del nostro ambiente tridimensionale  in un pixel della rappresentazione bidimensionale disegnata a video.

Nel nostro caso carichiamo il file “Effects.fx” e lo associamo alla variabile effect.

// Carichiamo il file con gli effetti:

// Si carica il codice HLSL dal file effects.fx

// e lo si compila in assembler...

CompiledEffect compiledEffect = Effect.CompileEffectFromFile("@/../../../../Effects.fx", null, null, CompilerOptions.None, TargetPlatform.Windows);

// ... e si carica l'effetto compilato nella variabile

effect = new Effect(graphics.GraphicsDevice, compiledEffect.GetEffectCode(), CompilerOptions.None, null);

 

Se apriamo il file Effects.fx possiamo notare che contiene molte tecniche al suo interno; tra queste facciamo notare la “Textured” che utilizzeremo per l’applicazione delle trame a tutti gli elementi della biblioteca.

 

//------- Technique: Textured --------

 

VertexToPixel TexturedVS( float4 inPos : POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0)

{    

      VertexToPixel Output = (VertexToPixel)0;

      float4x4 preViewProjection = mul (xView, xProjection);

      float4x4 preWorldViewProjection = mul (xWorld, preViewProjection);

   

      Output.Position = mul(inPos, preWorldViewProjection);     

      Output.TextureCoords = inTexCoords;

     

      float3 Normal = normalize(mul(normalize(inNormal), xWorld));    

      Output.LightingFactor = 1;

      if (xEnableLighting)

            Output.LightingFactor = dot(Normal, -xLightDirection);

   

      return Output;   

}

 

PixelToFrame TexturedPS(VertexToPixel PSIn)

{

      PixelToFrame Output = (PixelToFrame)0;        

     

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

 

      return Output;

}

 

technique Textured

{

      pass Pass0

    {  

        VertexShader = compile vs_1_1 TexturedVS();

        PixelShader  = compile ps_1_1 TexturedPS();

    }

}

 

Notiamo che la tecnica “Textured” è costituita da un singolo passo e dispone sia di un VertexShader che di un PixelShader.

Il metodo TexturedVS accetta come parametri la posizione del pixel nella scena tridimensionale, la relativa normale e le coordinate della texture.

Vengono inviate al Pixel Shader  la posizione del pixel nella scena bidimensionale e le coordinate della trama; oltre a questi valori, viene restituito anche il fattore di illuminazione se tale opzione è stata abilitata.

Il Pixel Shader a questo punto si limita a mappare il giusto colore nel pixel, basandosi sui dati appena inviati dal Vertex Shader, applicandovi eventualmente l’illuminazione.

Nella seguente immagine possiamo osservare il flusso dei dati da una generica applicazione in XNA verso Vertex e Pixel Shader della scheda grafica.

 

Flusso di Vertex e Pixel streams da XNA al monitor

Immagine 05 – Flusso di Vertex e Pixel streams da XNA al monitor


Impostazione della visuale

In questa sezione specifichiamo le matrici che definiscono la scena osservata dall’utente.

Con viewMatrix  definiamo una matrice che memorizza la posizione e l'orientamento della visuale con cui noi osserveremo la scena. I parametri specificati sono nell’ordine:

-         - la posizione della camera

-         - l'oggetto al quale si sta puntando

-         - il vettore che sarà considerato come "UP"   

 viewMatrix = Matrix.CreateLookAt(new Vector3(20, 5, 13), new Vector3(8, 7, 0), new Vector3(0, 0, 1));

 

 La seconda matrice ad essere definita è la projectionMatrix; questa memorizza "come" la telecamera osserva la scena. In questo caso indichiamo:

-         - l'angolo di visione;

-          - le proporzioni dell'immagine (aspect ratio);

-         - la distanza visiva: min – max, ossia l’intervallo oltre il quale non disegnare nulla.

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

 

Definite le proprietà della camera, dobbiamo passare le relativi matrici alle tecniche di visualizzazione che vogliamo utilizzare.

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

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

effect.Parameters["xWorld"].SetValue(Matrix.Identity);

Per concludere, indichiamo i parametri relativi all’illuminazione della scena:

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

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

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

 

Inizializzazione dei vertici

La procedura di creazione degli scaffali partendo dalle informazioni contenute nella matrice int_Floorplan è piuttosto semplice: si controlla ogni elemento della matrice e se il suo contenuto è differente da “0” (zero) si procede con la creazione delle facce dello scaffale: quattro facce per un totale di 8 triangoli definiti (ricordiamo che la struttura base è infatti un triangolo e che ne servono 2 per definire un rettangolo).

Ritroviamo quindi un doppio ciclo con la seguente struttura:

for (int x = 0; x < WIDTH; x++)

            {

                for (int y = 0; y < HEIGHT; y++)

                {

                    int ScaffaleCorrente = int_Floorplan[x, y];

// Definizione scaffale

Notiamo che è possibile assegnare delle altezze differenti ai vari scaffali: questa operazione viene eseguita subito dopo la creazione della piantina bidimensionale di “0” e “1” ed il risultato viene salvato nel vettore AltezzaScaffali[]. Dato che al momento non c’è la necessità di disporre di scaffali di grandezza differente, abbiamo scelto di impostare la loro altezza ad un valore fisso.

La procedura di creazione di una singola faccia, come accennato prima, prevede la definizione di due triangoli: inseriamo tutte le informazioni necessarie nella lista verticeslist, che sarà poi utilizzata dalla procedura Draw() per disegnare l’oggetto nella scena.

Nel seguente codice è possibile osservare le istruzioni necessarie alla definizione di un singolo triangolo:

verticeslist.Add(new VertexPositionNormalTexture(new Vector3(x + 1, y, 0f), new Vector3(0, 1, 0), new Vector2(ScaffaleCorrente * 2 / imagesintexture, 1)));

verticeslist.Add(new VertexPositionNormalTexture(new Vector3(x + 1, y, AltezzaScaffali[ScaffaleCorrente]), new Vector3(0, 1, 0), new Vector2(ScaffaleCorrente * 2 / imagesintexture, 0)));

verticeslist.Add(new VertexPositionNormalTexture(new Vector3(x, y, 0f), new Vector3(0, 1, 0), new Vector2((ScaffaleCorrente * 2 - 1) / imagesintexture, 1)));

Si noti che ogni vertice è del tipo VertexPositionNormalTexture  e che per la sua definizione indichiamo sia le coordinate spaziali (tre valori di tipo Vector3(x, y, z),), sia le informazioni sulle normali (new Vector3(0, 1, 0)), sia le coordinate della texture da utilizzare: questi ultimi sono due valori tipo (u,v), con (0,0) che indica il punto in alto a sinistra dell’immagine e (1,1) che indica quello in basso a destra.

Notiamo inoltre che tutti i vertici devono essere definiti in senso orario: in questo modo XNA non li elimina tramite culling!

 

Caricamento Media

L’intera procedura di caricamento dei dati esterni avviene all’interno del metodo LoadGraphicsContent(): in esso ritroviamo la chiamata LoadEffect() per il caricamento degli effetti (i file “.fx”) e LoadModels() per il caricamento dei modelli (i file “.x”).

In LoadModels() utilizziamo il metodo FillModelFromFile() per ogni variabile che rappresenta un modello nel nostro progetto; il suo compito è quello di importare la struttura del modello, ossia l’insieme delle sue mesh.

MyAvatarModel = FillModelFromFile("Assets/Models/MyAvatar");

Questo è il relativo codice:

        private Model FillModelFromFile(string asset)

        {

            // Carico il modello passato in input

            Model mod = content.Load<Model>(asset);

 

            // Colleghiamo ad ogni parte del modello l'effetto desiderato

            foreach (ModelMesh modmesh in mod.Meshes)

                foreach (ModelMeshPart modmeshpart in modmesh.MeshParts)

                    modmeshpart.Effect = effect.Clone(device);

            return mod;

        }

Con il comando Model mod = content.Load<Model>(asset) carico in una variabile di tipo Model i dati dell’oggetto passato in input, mentre grazie al doppio ciclo for collego ad ogni mesh del modello, il generico effetto ad esso collegato (presente nel file .x che definisce il modello).

Alla fine dell’elaborazione, si restituisce il modello con tutti le sue proprietà settate:

return mod;

Se oltre a queste informazioni desideriamo associare ad ogni singola mesh anche delle particolari textures, allora utilizzeremo un ulteriore ciclo:

          foreach (BasicEffect currenteffect in mesh.Effects)

                {

                    MyModelTextures[i++] = currenteffect.Texture;

                }

 

Come precedentemente specificato, utilizziamo questo metodo solo per gestire quei modelli .x che hanno tali informazioni incluse nel file.

Poiché il modello della nostra biblioteca viene generato in tempo reale definendo vertici e spigoli della struttura (e non appoggiandosi ad oggetti esterni), per il momento ci limiteremo a caricare una risorsa contenente tutte le immagini generiche da applicare sui solidi finali.

 

Eseguiamo tale operazione con il seguente comando, con il quale associamo una variabile ad un’immagine bidimensionale:

scenerytexture = content.Load<Texture2D>("Assets/Textures/TexturesScaffaliMod2048");

 Nel caso specifico indichiamo di utilizzare la seguente trama (di dimensioni 2048x384):

  Texture della biblioteca

Immagine 06 – Texture della biblioteca

 

Questa immagine verrà poi suddivisa in diverse porzioni e si preleverà quella opportuna al momento di applicare le texture agli oggetti.

La texture in esame è composta da 11 immagini distinte: la prima è la texture del pavimento ed in seguito è possibile notare la coppia di texture che definisce ogni singolo scaffale: la facciata contenente i libri e la corrispondente struttura in legno.

Quindi nella precedente immagine è specificata l’apparenza di 5 tipi differenti di scaffali.

Allo stesso modo vengono caricate le immagini di tutti gli altri elementi della scena, con procedure quali LoadDoorsTextures() (per le porte) e LoadBooksTextures()(per i titoli dei libri).

 

Con il caricamento dei materiali concludiamo questa prima parte riguardante la creazione di contenuti tridimensionali con XNA.

Nella prossima parte studieremo le tecniche utilizzate per la costruzione della scena, che includono il caricamento della Skybox, la creazione delle strutture tridimensionali e le tecniche di texturizzazione.

 

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, 07.45 by Marco Petrucco
Filed under:

Comments

No Comments
Syndication
Archive