| VS85 Team's profileXNALearnersBlogListsGuestbook | Help |
XNALearners |
||||
|
2/17/2009 XNA@DSI - Ingegneria del SoftwareQuest’anno al corso di Ingegneria del Software del dipartimento di Informatica dell’Universitá Ca’ Foscari di Venezia é stato svolto un interessante esperimento: affiancare al classico lavoro teorico di produzione di documenti tecnici sulla creazione di un progetto informatico anche la produzione del progetto stesso. In particolare, é stato chiesto agli studenti di produrre un videogioco (piú precisamente, un sistema che permettesse di giocare a vari giochi di carte): per lo sviluppo di tale videogioco é stata usata la tecnologia XNA. Nonostante la maggior parte degli studenti non avesse alcuna esperienza pregressa con XNA (e, a volte, nemmeno con C#, il linguaggio che si accompagna ad XNA), i ragazzi sono riusciti a padroneggiare la tecnologia in breve tempo (la durata del corso), producendo dei progetti finali veramente degni di nota. Abbiamo quindi chiesto ad un gruppo (“I Cordiali”) di presentarci il loro lavoro e di darci il loro feedback sull’esperienza, abbastanza “diversa” da quella di un classico corso universitario:
N.B.: Il corso di Ingegneria del Software é stato tenuto dal docente Agostino Cortesi, con l’assistenza di Giuseppe Maggiore e Giulia Costantini. Il gruppo dei “Cordiali” é formato da Francesco Boscariol, Mauro Barbon. Luca Cosmo, Chiara Poma e Nicola Loiolino (da sx a dx come appaiono nel video). 1/22/2009 SpriteBatch per effetti speciali - parte 4By Davide Luzzu – Webcast by Giuseppe Maggiore
Il (resto del) codice C#
// Generiamo un array di float che sono le altezze (y) dei punti di controllo. var tmp = Enumerable.Range(0, controlPoints.Width) // i ∈[0,controlPoints.Width-1] .Select(i => i / (float)controlPoints.Width) // x ∈[0,1] .Select(x => sin(-4.0f * t + 32.0f * pi * sigmoid(x + 0.5f) * x) * 0.1f + 0.5f) // y é l'altezza di un punto di controllo. La convertiamo in formato integrale. .Select(y => (UInt16)(y * (UInt16.MaxValue - 1))) .ToArray(); // Salviamo l'array di altezze nella texture. controlPoints.SetData<UInt16>(tmp);
Per dare la forma al serpente definiamo i punti di controllo sulle x e sulle y. Per fare ciò ci serviremo delle funzioni sin e sigmoid, che abbiamo definito in precedenza. Definiamo un range da 0 a 256, e ne facciamo il rapporto tra 0 e 1 in modo che x ∈ [0,1]. Dato che x è nel range [0,1] possiamo definire la forma del serpente usando il seno. Tale forma varia in base al tempo ( …4.0f * t…) ed ha, ad ogni istante di tempo, una forma curva definita da “…32.0f * pi * sigmoid(x + 0.5f) * x…”; variando tali parametri determineremo un’alterazione a piacimento della forma o del tempo di esecuzione del serpente. Una delle peculiarità derivanti da questa porzione di codice è il fatto che, ciò che viene disegnato, è ad una dimensione algoritmicamente, ma a due dimensioni visivamente; la x difatti è un seno, e ciò è sufficiente per disegnare il serpente. Ma la x è l’unica variabile da cui dipende effettivamente il controllo dello spazio. Quindi è monodimensionale. A questo punto la y è libera, e la si utilizza per inserire i dati di illuminazione, quelli che avevamo previsto inserendo SurfaceFormat.Luminance16 nella definizione della texture. Convertiamo la y in intero a 16 bit Unsigned, e trasformiamo la struttura in un array. Quindi la variabile “tmp” è un array di interi a 16 bit Unsigned. Non resta che inserire i dati dei punti di controllo nella Texture2D.
// Carichiamo la texture di sfondo. var background = Content.Load<Texture2D>("background"); /// Disegnamo la texture di sfondo. Questa operazione non dipende in alcun modo da come poi disegneremo il serpente.
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState); spriteBatch.Draw(background, new Rectangle(0, 0, 800, 600), Color.White); spriteBatch.End();
Per rendere l’applicazione più gradevole prevediamo uno sfondo, di nostro gradimento, che disegnamo con lo SpriteBatch. Ci preme ribadire che tale operazione non dipende in alcun modo da come disegneremo il serpente.
/// Disegnamo il serpente con il nostro shader in alpha blending sulla scena precedente. L'operazione di disegno genera un serpente che passa per i punti di controllo della texture "controlPoints".
new Vector3[] { new Vector3(0, 1, 1} .Select((color, index) => new {color, index}) .ToList().ForEach(snake => { fx.Parameters["SnakeColor"].SetValue(snake.color);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
fx.Begin(); pass(0, p => p.Begin());
spriteBatch.Draw(controlPoints, new Rectangle(0, 100 + 100 * snake.index, 800, 400), Color.White);
pass(0, p => p.End()); fx.End();
spriteBatch.End(); });
base.Draw(gameTime); }
Finalmente disegnamo il serpente! Definiamo innanzittutto il colore con un vettore, un azzurro tenue. Inseriamo il colore in ogni posizione di un array di Vector3 e lo modifichiamo con la tecnica dello Shader. Il primo passo è impostare il parametro “SnakeColor” dello Shader con l’azzurro appena definito. Iniziamo lo SpriteBatch con il metodo “Begin()” e avviamo il rendering dello shader con
fx.Begin();
che provoca, inoltre, la sostituzione automatica dello Shader di default nello SpriteBatch. A questo punto non resta che aprire il passo zero di rendering con
pass(0, p => p.Begin());
e disegnamo sullo schermo la Texture2D. Chiudiamo il passo e indichiamo che lo shader ha terminato la sua tecnica con
fx.End();
ricordandoci di chiudere anche le funzioni dello SpriteBatch con la funzione “End()”.
SpriteBatch per effetti speciali - parte 3By Davide Luzzu – Webcast by Giuseppe Maggiore Il codice C#
Effect fx = null; Texture2D controlPoints; const int numControlPoints = 256; protected override void LoadContent() { /// Costruiamo il renderer 2D. spriteBatch = new SpriteBatch(GraphicsDevice); /// Costruiamo la texture di control points, in cui carichiamo interi /// unsigned a 16 bit. La texture é una matrice numControlPoints × 1, /// cioè 256 colonne e una riga. controlPoints = new Texture2D(GraphicsDevice, numControlPoints, 1, 1, TextureUsage.None, SurfaceFormat.Luminance16);
// Carichiamo lo shader HLSL. fx = Content.Load<Effect>("snake shader"); }
La prima operazione da compiere è ottenere la configurazione grafica ( GraphicsDevice ) e applicarla ad uno SpriteBatch. Il GraphicsDevice ci permette di ottenere informazioni riguardo alla risoluzione grafica con cui si sta eseguendo l’applicazione, e, di conseguenza, impostare il Pixel Shader. Lo SpriteBatch è il render 2D che ci consentirà di disegnare il nostro serpente. Il secondo passaggio fondamentale è creare una Texture2D che contiene dei punti di controllo (256), che daranno la forma al nostro “serpente”. Questi punti di controllo possono essere concettualmente associati ai key frames di una animazione; cioè definiamo quali sono le posizioni per cui la nostra animazione dovrà assolutamente passare, il resto viene fatto per interpolazione. Sarebbe, infatti, impensabile dover inserire tutti e 25 i frames ( riferito ad un video in formato PAL) di ogni secondo della animazione, poiché, per ottenere un minuto di animazione, servirebbe agire su 1500 singoli frames! Allo stesso modo quello che facciamo noi è definire dei “frames” (detti anche nodi o punti di controllo ) e nessun dato nella Texture 2D, in modo da avere la possibilità di inserirli a nostro piacimento. Per poter manipolare una elevata quantità di informazioni di illuminazione sulla texture, indichiamo che il formato della superficie è Luminance16; è un formato che comprende 16 bit di dati solo per illuminazione, cioè potremo manipolare 216 combinazioni di colore.! L’ultima operazione da effettuare è il caricamento dello Shader. Si fa riferimento alla classe Content che contiene il metodo ad accesso statico Load. Tale metodo riceve in input una stringa che designa il percorso di accesso al file (*.fx ) che contiene il codice dello Shader. Ricordiamo che il nome dello Shader va indicato senza l’estensione. Il metodo Load è parametrizzato con un tipo Effect; ciò indica che la variabile di ritorno conterrà dati di tipo Effect.
protected override void Draw(GameTime gameTime) { //Resettiamo lo sfondo dell'applicazione ad azzurro. graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
// Alcuni piccoli helper trigonometrici e non. Func<float, float> sin = x => (float)Math.Sin((double)x); Func<float, float> sigmoid = x => (float)MathHelper.SmoothStep(0.5f, 0.1f, x);
var pi = (float)Math.PI;
// Da quanti secondi l'applicazione é in esecuzione? var t = (float)gameTime.TotalGameTime.TotalSeconds;
// Helper di attivazione/disattivazione di un passo dello shader HLSL. Action<int, Action<EffectPass>> pass = (i, a) => a(fx.CurrentTechnique.Passes[i]);
Eseguiamo l’override del metodo Draw di default, dove scriviamo tutto il codice per effettuare il disegno, senza demandare nulla ad altri metodi. Definiamo innanzitutto due funzioni per aiutarci nei calcoli, una (sin) che calcolerà il seno per definire la forma del serpente, e l’altra (sigmoid) per interpolare i dati. Di particolare interesse, in questa porzione di codice, è la definizione dei passi dello Shader HLSL.
Action<int, Action<EffectPass>> pass=(i, a) => a(fx.CurrentTechnique.Passes[i]);
Quello che intendiamo fare è definire in modo elegante l’attivazione e la disattivazione del passo dello Shader. Il codice ML-style ci viene incontro in questo senso, poiché creiamo una labda expression. Diremo che l’azione pass deve compiere una certa azione ad un determinato indice. L’azione che pass eseguirà la chiameremo (a), e (a) effettua un passo della tecnica dello Shader; dato che la tecnica può contenere più di un passo, dovremo indicare quale è l’indice del passo che (a) deve effettuare, e lo indichiamo con (i). Come abbiamo visto, la nostra tecnica contiene un singolo passo, quindi non dovremo aggiornare l’indice ( che sarà sempre zero ). In altre parole piuttosto che scrivere
fx.Begin(); fx.CurrentTechnique.Passes[0].Begin();
//…code…
fx.CurrentTechnique.Passes[0].End(); fx.End();
avremo una espressione decisamente più elegante
fx.Begin(); pass(0, p => p.Begin());
//…code…
pass(0, p => p.End()); fx.End();
L’azione che (a) esegue è di tipo EffectPass, cioè un tipo di effetto derivante dalla tecnica dello shader in uso.
SpriteBatch per effetti speciali - parte 2ImplementazioneBy Davide Luzzu – Webcast by Giuseppe MaggioreLo Shader
sampler ControlPoints : register(s0) = sampler_state { MipFilter = LINEAR; MagFilter = LINEAR; MinFilter = LINEAR;
AddressU = CLAMP; AddressV = CLAMP; };
Il “serpente” che andremo a definire è composto da svariati punti di controllo che contengono, non solo una posizione, ma anche un valore che ne determina il colore. Abbiamo però necessità di definire come interpretare il colore nelle posizioni tra un punto di controllo ed un altro; tale operazione viene eseguita con una semplice interpolazione lineare, lasciando invariati i colori di partenza. Per specificare l’immutabilità dei colori di partenza scriveremo:
AddressU = CLAMP; // punto di controllo di partenza AddressV = CLAMP; // punto di controllo di arrivo
dove CLAMP indica appunto che tali colori non dovranno variare. Queste specifiche le inseriamo in un sampler, cioè indichiamo che la modalità di accesso ai pixel del nostro PixelShader è attraverso un campionamento. Dato che esistono più sampler di base, indichiamo con
… : register(s0)…
che quello che ci interessa è all’indice 0 (zero), più precisamente nel registro “s0”, detto in codice assembly (dove abbiamo a che fare direttamente con i registri).
float4 PixelShaderFunction(float2 input : TEXCOORD0) : COLOR0 { float cp; cp = tex2D(ControlPoints, float2(input.x, 0.0f)); float alpha = pow(saturate(1.0f - abs(input.y - cp)), 32.0f); return float4(SnakeColor.xyz,alpha); }
Implementiamo la funzione del PixelShader, passando in ingresso i dati sulle coordinate di texture, ottenuti dal VertexShader. Come possiamo notare, il parametro di ingresso è una coordinata bidimensionale di texture, e la x del parametro rappresenta la posizione sull’asse delle ascisse del punto di controllo che dovremo andare a trovare. La posizione sull’asse delle ordinate la dobbiamo invece ricavare sfruttando i dati che abbiamo:
cp = tex2D(ControlPoints, float2(input.x, 0.0f));
Non ci resta che calcolare un valore alpha, che rappresenta la trasparenza del colore che dà la “fluorescenza” all’immagine di base. Questo colore trasparente avrà una saturazione maggiore quanto più si è vicino all’immagine del serpente, ed al contrario avrà una intensità minore tanto più è lontano. Questa “aura” che impostiamo attorno al serpente è prodotta con:
float alpha = pow(saturate(1.0f - abs(input.y - cp)), 32.0f);
Infine restituiamo un colore a quattro canali, per cui, i primi tre rappresentano il colore originale, e il quarto il canale alpha.
technique Technique1 { pass Pass1 { PixelShader = compile ps_2_0 PixelShaderFunction(); } }
Uno Shader, abbiamo già detto, che è formato da una technique e da uno o più pass; l’ultimo passaggio, appunto, è destinato a definire tale technique e il pass che ci interessa. Il PixelShader è compilato con la versione 2_0, e ciò che deve essere tradotto in assembly è la funzione PixelShaderFunction. SpriteBatch per effetti speciali - parte 1By Davide Luzzu – Webcast by Giuseppe Maggiore
AbstractUno dei problemi più pressanti, nella creazione di mondi virtuali 3D, è rendere realistica la percezione del mondo. I dettagli stanno alla base di tutto. Più sono curati i dettagli più l’utente si troverà a proprio agio nell’ambiente 3D. Per meglio comprendere vediamo un esempio. Ammettiamo che si debba rappresentare un oggetto radioattivo. Come faremo capire all’utente che esso è radioattivo? Lo coloriamo di verde? Non basterebbe. Aggiungiamo una luce all’oggetto per indicare che esso reagisce in modo “strano” al contatto con l’aria circostante? Può andare, ma si può fare di più. Dovremo, in questo caso, creare una vera e propria fluorescenza dell’oggetto radioattivo. Esistono svariati modi per creare una fluorescenza, ma, dal momento che si tratta di un dettaglio, non potrà avere un ruolo predominante nella scena 3D, cioè non può occupare troppe risorse. Si presenta dunque la necessità di possedere uno strumento che esegua del codice a basso livello, (cioè che ci permetta un grande risparmio di memoria e risorse) chiamato genericamente Shader. Potremmo dire che uno Shader è quella parte di codice che valuta come gli oggetti visuali devono rispondere alle luci; in altre parole con uno Shader determiniamo la distribuzione dei colori sugli oggetti visuali, in modo tale da ottenere effetti di tridimensionalità, fluorescenza, luminescenza ecc… Per meglio comprendere l’importanza degli Shader proviamo ad immaginare un mondo virtuale in cui venga rappresentata una tavola imbandita con ogni sorta di cibi, insomma un ambiente 3D abbastanza complesso. Se in tale ambiente non avessimo nessuna luce, niente ci vieterebbe di pensare non solo che non c’è tridimensionalità, ma che addirittura si tratta di un mondo virtuale vuoto; questo perché in assenza di luce lo percepiamo completamente nero. Al contrario se avessimo una luce intensissima lo percepiremo completamente bianco. La soluzione dunque risiede nel corretto bilanciamento delle luci e delle ombre. Quello che presentiamo in questo articolo è un Pixel Shader HLSL ( High Level Shading Language ), e ci servirà appunto per il controllo di alcuni effetti di luce, applicati ad una texture. Uno Shader in HLSL è formato da una technique (tecnica), che a sua volta è formato da vari pass (passi). Nel pass vengono specificate quali funzioni devono essere utilizzate, e a quale tipo di shader si intende fare riferimento (Pixel/Vertex Shader).
Struttura di un nuovo progettoOgni progetto XNA 3.0 possiede una sotto-struttura logica (cioè dove mettere i dati), chiamata Content, nella quale dovremo copiare il file (*.fx) che contiene il codice dello Shader. La copia può avvenire attraverso una semplice operazione di dragging. E’ importante inserire i dati nella struttura apposita Content, poiché essa contiene delle References statiche alle impostazioni di libreria, ed esse ci consentiranno, senza alcuno sforzo, il caricamento dei dati (audio, effetti, textures…), utili al nostro progetto.
Figura 1 References statiche contenute nella struttura logica Content.
L’approccio che seguiremo per la definizione del progetto è bottom-up, cioè definiremo prima lo shader, poi come si inserisce nel contesto dell’applicazione, ed infine come si controlla e si visualizza con un esempio. L’idea è quella di creare un serpente dal colore fluorescente, la cui forma è definita solo in alcuni punti, detti punti di controllo.
Tali punti di controllo verranno definiti nel codice. Lo Shader dovrà contenere una funzione generica, che imposti una texture per alcuni punti di controllo definiti altrove. L’intera forma del serpente è derivata dall’interpolazione delle posizioni dei punti di controllo. Il nostro primo obbiettivo è dunque creare uno Shader e copiarlo nella struttura logica Content. Info aggiuntivePer quanto riguarda luci e ombre, è necessario precisare che esistono due tipi di ombre:
Per quanto riguarda gli Shader, abbiamo fatto riferimento a due diversi tipi:
Uno Shader HLSL permette di scrivere complessi calcoli grafici che possono essere eseguiti con grande rapidità dalla GPU, poiché sono istruzioni di basso livello. Queste istruzioni vengono impartite direttamente alla scheda video, pertanto è necessario assicurarsi che essa supporti tali istruzioni. Uno Shader HLSL, fortunatamente, ha una sintassi C-like style, ed è abbastanza semplice da comprendere; le operazioni che supporta sono le comuni operazioni aritmetiche, più gran parte delle comuni funzioni trigonometriche. Inoltre, per le espressioni di controllo, vale la stessa sintassi del C; per i booleani si usano gli stessi operatori: &&, ||, <, >, ==, !=, <=, >=. La versione dello Shader utilizzata per questo esempio è la 2_0, che permette solitamente una maggiore ottimizzazione del codice nella traduzione in assembly. |
Benvenuto nel mio Spaces!
walter porcelliniwrote:
faccio i personali complimenti a tutto lo staff, giuseppe maggiore in particolar modo .
Ho scaricato ed installato xna3.0 con vc# express 2008 .
Be , che dire , rispetto al VS2003.net che usavo per supervisioni industriali , è tutto molto piu
"moderno" .Spero di avere il tempo per poter mettere in pratica i vostri
tutorial , i video sono eccellenti ed interessanti
grazie
walter
Oct. 13
|
|||
|
|