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.

Immagine 07 –
Scena texturizzata
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.
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.
Immagine 15 –
Angolo texturizzato della skybox
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.
Immagine 17 –
Ingradimento dell’avatar con dettaglio di illuminazione
Di seguito possiamo invece osservare la sua
struttura in wireframe:
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:
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:
Immagini 20, 21 –
Texture degli avatar usate per la demo
La loro visualizzazione nella scena finale è la
seguente:
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.