Academic Club Website

Categorie Articoli

Archivio Articoli

  • XNA@DSI Contest

    Nel secondo semestre dell'a.a. 2007-2008 il Dipartimento di Informatica dell'Universita' Ca'Foscari di Venezia ha ospitato una iniziativa decisamente nuova e particolare: una serie di seminari (strutturatii come un vero e proprio corso) dedicati alla tecnologia XNA, tenuti da uno studente della specialistica del corso di Informatica e Microsoft Student Partner (MSP).

    I seminari, in totale 7, e della durata di 2 ore ciascuno consistevano in una parte teorica (accompagnata da slides) e una parte applicata (in cui scrivendo codice in tempo reale venivano applicati i concetti della parte teorica). Al termine del corso gli studenti sono stati messi in grado di scrivere videogiochi di media complessita'.

    Gli argomenti trattati sono:

    • Geometria in XNA
    • Algoritmi di illuminazione
    • Texturing
    • La classe Microsoft.XNA.Framework.Model
    • Esempio di semplice gioco

    Terminate le lezioni, grazie alla sponsorizzazione di Microsoft, e' stato organizzato un concorso per stimolare gli studenti a mettere in pratica cio' che avevano imparato. I giochi inviati sono stati di qualita' stupefacente, ed il vincitore (scegliere il quale e' stata durissimo J) si e' portato a casa una XBox360 nuova fiammante. Il secondo e il terzo hanno realizzato lo stesso punteggio e sono stati premiati entrambi come secondi classificati, ricevendo ciascuno due giochi della serie Games For Windows. L'anno prossimo verra' ripetuto il corso rivolgendosi nuovamente agli studenti della laurea triennale in Informatica, sempre presso il Dipartimento di Informatica dell'Universita' Ca' Foscari di Venezia.

  • Testo e primitive 2D in XNA

    Disegnare oggetti 2D sullo schermo

    by Giuseppe Maggiore

    Introduzione

    XNA supporta in modo molto ordinato il disegno di oggetti bidimensionali su schermo. Tipicamente queste operazioni di disegno verranno eseguite durante la chiamata a Game.Draw, tramite un oggetto helper, lo sprite batch:

    SpriteBatch spriteBatch;

    Lo sprite batch, come suggerisce il nome, e' un sistema di raggruppamento ottimizzato di chiamate di disegno di sprites. Gli sprites sono piccoli rettangoli che vogliamo prendere da una texture e mostrare su schermo. Tale sistema richiede che le chiamate di disegno avvengano in mezzo tra apertura (Begin()) e chiusura (End()), in modo che lo sprite batch le possa ottimizzare o ordinare come preferisce:

    spriteBatch.Begin();

    /*...*/

    spriteBatch.End();

    La funzione Begin prende opzionalmente tre parametri:

    • SpriteBlendMode che specifica se vogliamo usare alpha blending, alpha blending additivo o nessun blending
    • SpriteSortMode che indica in che ordine lo sprite batch deve disegnare: per z crescente, per z decrescente, immediato, deferito alla chiamata di End()
    • SaveStateMode per far si che la chiamata a End() ripristini tutte le flags della scheda video ai valori che avevano prima della chiamata a Begin(): utile per rendere l'uso dello SpriteBatch del tutto trasparente agli altri moduli di disegno

    SpriteBatch.Draw

    Quando arriva il momento di disegnare ecco che entra in gioco la prima delle due primitive di disegno: SpriteBatch.Draw. Tale funzione prende come parametri di base una texture, un rettangolo in pixel su schermo in cui la texture va messa, ed un colore per cui moltiplicare quello della texture. Quindi per inserire un'immmagine come sfondo dovremo scrivere:

    spriteBatch.Draw(Content.Load<Texture2D>("background"),

    new Rectangle(0, 0, 800, 600), Color.White);

    Questa chiamata a draw sta indicando di disegnare la texture "background" con il suo colore originale (moltiplicare per Color.White lascia inalterato il colore) nell'area di schermo che inizia al pixel (0,0) ed ha dimensioni (800,600) pixel:

    Ipotizziamo ora di voler disegnare un oggetto di qualche tipo che si muova ruotando sullo schermo. Diciamo anche che tale oggetto ha posizione "Position" ed e' ruotato di "Rotation" radianti rispetto al suo centro. Allora dovremo invocare nuovamente draw, ma prima sara' necessario introdurre un nuovo di draw parametro che definisce il centro di rotazione. Di default infatti uno sprite viene fatto ruotare intorno al suo angolo in alto a sinistra, dove effettivamente l'oggetto viene posizionato:

    Questo pero' non e' affatto cio' che desideriamo: vorremmo che lo sprite ruotasse attorno al suo centro, non attorno ad un suo angolo. Esiste una versione della funzione Draw che accetta anche il parametro origin. Tale parametro serve per indicare quale e' l'origine della texture, rispetto a cui Position e' definita. Nel disegno qui accanto vediamo che l'origine e' stata messa al centro della texture (25,25) posto che la texture fosse grande (50,50). Qualsiasi rotazione adesso sara' relativa al centro e non agli angoli, proprio come desideriamo. La chiamata risultante per ottenere questo effetto sara':

    spriteBatch.Draw(Content.Load<Texture2D>("rotating_particle"),

    new Rectangle((int)Position.X, (int)Position.Y, (int)Size.X, (int)Size.Y),

    null, Color.White, Rotation, Size / 2, SpriteEffects.None, 0.0f);

    in cui possiamo notare come la texture sia posizionata in Position, abbia dimensione Size, e sia centrata sul suo punto medio Size/2, affinche' le rotazioni usino quello come riferimento e non l'angolo (0,0).

    SpriteBatch.DrawString

    Per disegnare testo la situazione non e' molto piu' complicata. Prima pero' ci serve un file di contenuto di tipo spritebatch, per descrivere il tipo di font da usare, i suoi parametri (dimensione, bold, etc.) e il set di caratteri da caricare (di default si usa Basic Latin, che va dallo spazio ASCII sino alla tilde ~). Creiamo il file di contenuto "Arial.SpriteFont":

    In questo semplice file xml sono specificati il tipo del font di sistema da usare per il disegno (arial), la sua dimensione in punti (14), spaziatura, uso di kerning, stile (bold, regular, italic) e i range di caratteri da usare (qui vediamo che e' stato richiesto di caricare i caratteri dal 32 al 126, ossia il set Basic Latin menzionato prima).

    Ipotizziamo di voler disegnare la doverosa stringa "Hello World!" su schermo. La chiamata opportuna risulta essere:

    spriteBatch.DrawString(Content.Load<SpriteFont>("arial"), "Hello World!",

    new Vector2(10, 70), Color.LightBlue);

    Vediamo che la funzione DrawString richiede lo SpriteFont da usare per costruire la scritta, il testo da scrivere, la posizione su schermo (dell'angolo in alto a sinistra della scritta) ed infine il colore del testo. Effettivamente questa invocazione produce la scritta desiderata in azzurro a partire dal pixel (10,70):

    Se pero' volessimo far ruotare la scritta intorno al suo centro? Avremmo bisogno di specificare l'Origin della nostra DrawString a meta' della dimensione della stringa disegnata, ma per fare cio' e' necessario sapere quanto grande risultera' la stringa su schermo. SpriteFont fortunatamente e' in grado di fornirci questa misura:

    SpriteFont Arial = Content.Load<SpriteFont>("arial");

    Vector2 stringSize = Arial.MeasureString("Hello World!");

    Tramite cui possiamo disegnare la nostra stringa di testo roteante intorno al proprio centro:

    float Rotation = (float)gameTime.TotalGameTime.TotalSeconds;

    SpriteFont arialBig = Content.Load<SpriteFont>("arialBig");

    Vector2 stringSize = arialBig.MeasureString("Hello World!");

    spriteBatch.DrawString(arialBig, "Hello World!", new Vector2(400, 300),

        Color.Yellow, Rotation, stringSize / 2, 1.0f, SpriteEffects.None, 0.0f);

    Font disegnati a mano

    Tutto cio' che abbiamo visto finora e' relativo a fonts true type che siano installati nel sistema. Immaginiamo pero' di avere un font disegnato a mano da un artista, come quello riportato in figura:

    Sara' possibile usare questo font non come texture (ossia importandolo direttamente), ma elaborarlo con un content processor appropriato e caricarlo in quanto sprite font. Il primo passo e' quello di importare il file tra i contenuti del nostro gioco:

    A questo punto sappiamo che il file dovra' essere importato con un caricatore di texture, ma vogliamo che sia processato con un processor in grado di leggere i singoli caratteri e metterceli a disposizione. Tale processor e' chiamato Sprite Font Texture - XNA Framework, e puo' essere attivato dalle proprieta' del file "customfont.png":

    Da tenere a mente che il font, per funzionare correttamente, dovra' rispettare due punti fondamentali:

    • Il canale alfa dovra' rispecchiare quali parti del testo sono caratteri e quali sono lo sfondo dei caratteri
    • In mezzo tra i vari caratteri dovra' esserci un colore magenta con alfa impostato ad opaco

    Ed e' fatta, a questo punto e' possibile caricare ed usare il nostro font come vedete qui sotto:

    spriteBatch.DrawString(Content.Load<SpriteFont>("customfont"), "test-text",

    new Vector2(10, 10), Color.LightBlue);

    Caratteri Unicode speciali

    Ipotizziamo adesso di tornare al caso iniziale, ossia di caricare un font true type installato nel sistema. Vogliamo pero' poter inserire dei simboli speciali, nel caso specifico vogliamo vedere come stampare tramite sprite font i caratteri elencati di seguito: "€™∞". Se proviamo direttamente a stampare una stringa del genere pero' otterremo un'errore runtime che causera' il crash del nostro gioco! Ricordiamoci che alla creazione dello sprite font abbiamo visto che alla fine del file generato automaticamente ci sono una serie di "character regions", che denotano gli insiemi di caratteri che intendiamo usare. Di default il solo insieme caricato e' il cosiddetto Basic Latin, ma possiamo estenderlo in due modi. Il primo e' naturalmente quello di trovare una lista di caratteri unicode ed aggiungere a mano i codici dei caratteri desiderati nella definizione dello sprite font. La seconda strada e' apparentemente piu' complessa, ma in realta' ci da' molto piu' controllo. Per prima cosa creiamo una custom pipeline nella soluzione che contiene il nostro gioco:

    La sola classe che deve essere contenuta nel nostro processor sara' una che aggiunge i caratteri desiderati al set di caratteri da caricare, e quindi lancera' il processor originale con il set di caratteri desiderato:

    namespace FontProcessor

    {

    [ContentProcessor(DisplayName = "ExtendedCharacterSetFontProcessor")]

    public class ContentProcessor1 : FontDescriptionProcessor

    {

    public override SpriteFontContent Process(FontDescription input,

    ContentProcessorContext context)

    {

    input.Characters.Add('€');

    input.Characters.Add('™');

    input.Characters.Add('∞');

    // should load this list of characters from a file!

    return base.Process(input, context);

    }

    }

    }

    A questo punto aggiungiamo questo nuovo progetto tra le references del progetto content:

    Ed infine definiamo come font processor dello sprite font da estendere il processor appena creato:

    Il font arial adesso sara' identico a prima, ma in aggiunta supportera' i caratteri che il nuovo processor aggiunge, rendendo una chiamata come

    spriteBatch.DrawString(Content.Load<SpriteFont>("arial"), "€™∞",

    new Vector2(10, 70), Color.LightBlue);

    perfettamente valida!

    Conclusione

    Trovate il sorgente online qui, mentre il webcast in cui viene scritto e spiegato nel dettaglio tutto il sorgente puo' essere scaricato da questo link, o guardato direttamente nella sua versione low-res embedded:

  • Primi passi con XNA - Parte 3

    Primi passi con XNA - Parte 3

    by Petrucco Marco

     

    Spostamento dell’avatar

    Conclusa la panoramica relativa alla visualizzazione della scena tridimensionale (e delle principali tecniche utilizzate per ottenerla), osserviamo come è stato implementato lo spostamento dell’avatar nel mondo 3D.

    Nella seguente immagine è possibile vedere i principali punti di tale processo, tutti presenti all’interno della procedura Update():

    Immagine 23 – Spostamento avatar

    Immagine 23 – Spostamento avatar

     

    Poiché dobbiamo far interagire l’utente con il nostro ambiente tridimensionale, per prima cosa è indispensabile leggere l’input che l’operatore ci fornisce.

    Grazie a XNA possiamo indifferentemente utilizzare la tastiera, il mouse o il joypad della console Xbox 360. Al momento è supportato solamente l’input tramite tastiera, perché era il metodo più veloce da implementare tra le tre possibilità; in questo modo l’interazione con l’ambiente è simile a quella offerta da titoli pioneristici nel settore dei videogiochi con visuale in prima persona, come il famoso “Doom”.

    Ovviamente in una ipotetica versione definitiva rivolta al grande pubblico, si aggiungerebbe anche il supporto al Freelook tramite mouse e, volendo, anche il supporto alle levette analogiche del joypad. Dato che la periferica di input non è di cruciale importanza in questa fase del progetto, per ora ci limitiamo ad analizzare come viene gestito l’input da tastiera.

    Quest’ultimo viene processato nella procedura ProcessKeyboard : essenzialmente ad ogni frame si leggono i tasti premuti (grazie a “KeyboardState keys = Keyboard.GetState()”) e si modificano alcune variabili relative ai tre assi (x, y,z).

    Per il movimento sull’asse orizzontale si usano i tasti W, A, S, D (classici comandi degli attuali First Person Shooter), mentre per il movimento dello sguardo utilizziamo i tasti freccia, gestendo l’UP, il DOWN ed il ROLL della camera.

    Spostamento orizzontale:

                if (keys.IsKeyDown(Keys.W))

                {

                    // avanti

                    position += addvector * speed;

                }

                if (keys.IsKeyDown(Keys.S))

                {

                    // indietro

                    position -= addvector * speed;

                }   

    if (keys.IsKeyDown(Keys.A))

                {

        // gira a sinistra

                    zRot = 1.5f * speed;

                }

                if (keys.IsKeyDown(Keys.D))

                {

        // gira a destra

                    zRot = -1.5f * speed;

                }

    Rotazione sugli assi Y (up/down) e X (roll):

                    if (keys.IsKeyDown(Keys.Right))

                    {

                        yRot = 1.5f * speed;

                    }

                    if (keys.IsKeyDown(Keys.Left))

                    {

                        yRot = -1.5f * speed;

                    }

                    if (keys.IsKeyDown(Keys.Down))

                    {

                        xRot = 1.5f * speed;

                    }

                    if (keys.IsKeyDown(Keys.Up))

                    {

                        xRot = -1.5f * speed;

                    }

    A questo punto si crea un quaternione contenente le informazioni relative ai tre assi:

    Quaternion additionalRot = Quaternion.CreateFromAxisAngle(new Vector3(0, 1, 0), yRot) * Quaternion.CreateFromAxisAngle(new Vector3(1, 0, 0), xRot) * Quaternion.CreateFromAxisAngle(new Vector3(0, 0, 1), zRot);

     

    Aggiornando infine anche la variabile globale che tiene traccia delle rotazioni dell’avatar:

    MyAvatarRotation *= additionalRot;

    Limitandoci solo a questo, il nostro avatar non farebbe altro che ruotare su se stesso: dobbiamo perciò anche aggiornare la variabile MyAvatarPosition oltre che la MyAvatarRotation. Il tutto viene fatto grazie a UpdatePosition(), in cui prima si calcola la direzione del movimento (prendendo il vettore (0,1,0) e trasformandolo secondo la rotazione dell’avatar), poi la si normalizza (in modo che la lunghezza diventi pari a 1), poi si moltiplica il tutto per la velocità ed infine si aggiorna la posizione.

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

    addvector = Vector3.Transform(addvector, Matrix.CreateFromQuaternion(rotationquat));

                addvector.Normalize();

                position += addvector * speed;

     

    Questo ci permette di ottenere il pieno controllo sul movimento dell’avatar e quindi di navigare liberamente all’interno dell’ambiente tridimensionale, come riportato in figura 24.

    Navigazione all’interno della libreria

    Immagine 24 – Navigazione all’interno della libreria

    Rilevamento collisioni

    Prima di spiegare come è stato risolto il problema del rilevamento delle collisioni, è opportuno spiegare al lettore gli strumenti che XNA offre allo sviluppatore per la gestione di simili problematiche.

    Come precedentemente annunciato, il framework dispone di due classi per il rilevamento delle collisioni; queste sono: BoundingBox e  BoundingSphere. Entrambe non presentano elementi visibili e dispongono di un metodo Intersects() che controlla se due oggetti collidono tra di loro.

    La struttura base di un controllo sulle collisioni, è riassumibile nella seguente scrittura:

    if (Oggetto1BoundingBox.Intersects(Oggetto2BoundingBox))

    {

    // COLLISIONE

    }

     

    Vi sono ancora delle limitazioni nell’uso di entrambe le classi:

    -         è compito del programmatore aggiornare la posizione di questi oggetti quando i modelli corrispondenti si muovono all’interno della scena;

    -         se scaliamo il modello, anche la corrispondente box deve essere scalata manualmente;

    -         soltanto le BoundingSphere sono generate in automatico quando importiamo un modello dall’esterno... se vogliamo utilizzare le BoundingBox, dobbiamo specificarle noi;

    -         si deve gestire anche il caso di rotazione dei modelli, modificando opportunamente le caratteristiche della box.

     

    Axis Aligned Bounding Box

    Ricordiamo che XNA utilizza delle Axis Aligned Bounding Box, ossia dei “contenitori” allineati agli assi.

    Forma e posizione sono semplicemente definite da un valore minimo ed un massimo per ogni dimensione: avremo quindi sia i minimi per X, Y e Z che i loro valori massimi. Il minimo ed il massimo rappresentano i due angoli “diagonalmente opposti” della box.

    Si può comprendere meglio tale caratteristica grazie al seguente esempio:


    Immagine 25 – Bounding Box

    Per comprendere come creare una bounding box, osserviamo questo esempio, nel quale se ne costruiscono due per due oggetti A e B:

    Vector3 MinV1 = new Vector3(OggettoA_X, OggettoA_Y,0);

    Vector3 MinV2 = new Vector3(OggettoB_X, OggettoB_Y,0);

    Vector3 MaxV1 = new Vector3(OggettoA_X+32, OggettoA_Y+48,0);

    Vector3 MaxV2 = new Vector3(OggettoB_X+8, OggettoB_Y+16,0);

    Ora che conosciamo i vertici, possiamo definire le BoundingBox:

    bb1.Min = MinV1; bb2.Min = MinV2;

    bb1.Max = MaxV1; bb2.Max = MaxV2;

    ed eseguire il controllo di collisione tra le due:

    if(bb1.Intercept(bb2)

    {

    // Collisione

    }

    Poichè dobbiamo specificare direttamente la posizione dei vertici della BoundingBox e poichè non è al momento possibile leggere tali informazioni dalle mesh dei modelli importati dall’esterno, risulta chiaro che tale tecnica è praticabile solo sugli oggetti creati da noi, per i quali siamo già a conoscenza di tali informazioni.

    Un ulteriore problema relativo alle Axis Aligned Bounding Box si ha nel momento in cui si ruota il modello a cui fanno riferimento: ovviamente possiamo trasformare i corrispondenti vettori (e quindi i vertici) delle box seguendo le rotazioni, ma così facendo non ruoteremo anche le box, che invece si adatteranno ai nuovi vertici.

    Questo problema è facilmente comprensibile osservando le seguenti immagini:

                   

            

    Immagine 26 – BoundingBox “ruotata”                            Immagine 27 - BoundingBox interpretata da XNA

    Nelle due immagini i punti di minimo e di massimo sono gli stessi, ma il risultato desiderato (figura 31) e quello ottenuto (figura 32) sono differenti.

    La spiegazione di questo comportamento è che queste Bounding Box sono allineate agli assi (Axis Aligned).

     

    BoundingSphere

    Per ovviare al problema della rotazione della Bounding Box, possiamo utilizzare le BoundingSphere, dato che per queste la rotazione è ininfluente.

    Inoltre, usando tale oggetto, evitiamo anche il lavoro di dover creare manualmente i confini (Bounding) per le singole mesh, poichè tale operazione la esegue XNA in automatico quando carica il modello stesso.

    Ovviamente, per il nostro avatar, abbiamo optato per questa tecnica, anche in considerazione del fatto che il modello stesso è una sfera!

    Importando in XNA il modello usato per gli esempi precedenti (figure 30-32), si otterebbe la suddivisione presentata in figura 33 (assumendo che il modello sia costituito dalle sole tre mesh di fusoliera, ali e coda).

     


    Immagine 28 – BoundingSphere generate in automatico da XNA

     

    Nel caso si desideri una suddivisione più accurata del modello, non resta che eseguirne una settando le caratteristiche delle sfere a mano, partendo come nel caso delle BoundingBox, da dati conosciuti.

    Non presenteremo il processo (seppure semplice) in tale sede, ma ci limiteremo a fornire due immagini del risultato finale (ovviamente il numer di BoundingSphere può variare a seconda delle scelte del programmatore).

             

               

    Immagine 29 – BoundingSphere multiple (sopra)           Immagine 30 - BoundingSphere multiple (lato) 

      

    Rilevamento Collisioni: Implementazione

    Nel nostro caso ci siamo limitati ad utilizzare una BoundingSphere per contenere l’avatar dell’utente ed alcuni controlli matematici.

    La gestione delle collisioni è affidata alla funzione CheckCollision(), che accetta in ingresso la posizione dell’avatar, definita dal centro della sfera, ed il raggio della stessa. Fornirà come risultato uno “0” (nessuna collisione) oppure un “1” (collisione avvenuta), grazie a due semplici controlli:

    // Controllo se sono più basso del pavimento

    if (position.Z - radius < 0) return 1;

     

    // Controllo la mia posizione rispetto agli scaffali della libreria

    if ((position.X - radius > 0) && (position.X + radius < WIDTH) && (position.Y - radius > 0) && (position.Y + radius < HEIGHT))

                {

    // Se sono “dentro” uno scaffale, la collisione è avvenuta

                    if (position.Z - radius < AltezzaScaffali[int_Floorplan[(int)position.X, (int)position.Y]]) return 1;

                }

    I commenti dovrebbero fornire una spiegazione già esaustiva di come è strutturato l’algoritmo, vediamone comunque i passi principali:

    -      per prima cosa verifico di non  trovarmi sotto il pavimento

    -      in seguito controllo di trovarmi entro i confini della biblioteca

    -      in caso affermativo, verifico di non trovarmi nell’area occupata da uno scaffale

    Questo controllo, richiamato all’interno di Update(), viene eseguito 60 volte al secondo (una per ogni frame disegnato) ed in caso di avvenuta collisione, blocca l’avanzata dell’avatar, sostituendo la sua posizione con quella che aveva nel frame precedente.

    if (CheckCollision(MyAvatarPosition, MyAvatarModel.Meshes[0].BoundingSphere.Radius * 0.0005f) > 0)

                    {

                      // Nel caso in cui si rilevi una collisione,

    // fermo l'avanzata dell'avatar ripristinando

    // l'ultima sua posizione corretta.

                      MyAvatarPosition = MyOldAvatarPosition;

                    }

    In questo modo l’utente ha l’impressione di essere bloccato (ed in effetti lo è) e può cambiare strada.

     

    Conclusioni

    Si può affermare che al momento attuale la suite XNA permetta all’utente alle prime armi di confrontarsi in modo relativamente semplice con le principali problematiche di un ambiente tridimensionale e, più in generale, con quelle relative allo sviluppo di un videogioco,  facendo concentrare la persona sul prodotto che desidera sviluppare e non su questioni troppo tecniche che vengono invece gestite dal framework.

    Si consiglia a tutti coloro che sono interessati a questa piattaforma di visitare il sito “Creators Club” (http://creators.xna.com) ed in particolare le seguenti sezioni:

    GettingStarted

    Tutorials

    Samples

    StarterKits

     

    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.

     

    Clicca qui per scaricare il codice del progetto

     

  • 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) *