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:

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.

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:

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

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.

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):

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.