mercoledì 15 aprile 2009

3D.2) Xna tutorial 3d: Disegnare Primitive

Dopo il breve passaggio dello scorso post su alcuni concetti chiave del 3d, possiamo iniziare a scrivere qualcosa di concreto!

Lo scopo di questo post sarà farvi capire come si disegna una superfice triangolare, il piano più semplice, tramite il quale possono essere composte tutte le altre forme e solidi, e come si eseguono alcune operazioni basilari su questa figura.

Anteprima Video:


In ordine vedremo :
1) Creazione di una semplice camera fissa
2) Disegnare un triangolo colorato utilizzando vertexPositionColor
3) Effettuare operazioni di traslazione e rotazione sul triangolo creato

1) Crezione della camera :
Prima di tutto aggiungete una nuovo file di tipo GameComponent al progetto e chiamatelo Camera.cs.
In primo luogo create due variabili di classe view e projection che identicicheranno le matrici di definizione della camera (fate un salto al pos precedente se non sapete di cosa sto parlando), e in seguito scrivete il costrutture in grado di accettare 4 parametri.
Il game, la posizione della camera, il target, e il vettore Up.
Poi molto semplicemente valoriziamo le matrici attraverso due funzioni messe a disposizione da XNA: CreateLookAt e CreatePerspectiveView, come potete notare queste funzioni prendono in ingresso i parametri dei quali abbiamo parlato nel post precedente ceh identificano proprio i valori necessari per le matrici view e projection.

CAMERA----------------------------------------------------------------------
        public Matrix view { get; protected set; }

public
Matrix projection { get; protected set; }

public
Camera(Game game,Vector3 pos, Vector3 target, Vector3 up)
:
base(game)
{

//Inizializzo view
view = Matrix.CreateLookAt(pos, target, up);

//Inizializzo projection
projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.PiOver4,
(
float)Game.Window.ClientBounds.Width /
(
float)Game.Window.ClientBounds.Height,
1
, 100);
}
Ora per fare in modo che la camera entri a far parte del nostro ciclo, aggiungiamo la variabile di classe
        Camera camera;


al file Game1.cs e al funzione initialize del file GAme.cs aggiungiamo:
        //Creiamo la camera e la aggiungiamo ai componenti

camera = new Camera(this, new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up);
Components.Add(camera);

A questo punto compilando dovreste vedere la solita schermata blu vuota di xna... ma la state visualizzando attraverso una camera :D

Ora cerchiamo di dare alla camera qualcosa da visualizzare, altrimenti non c'è molto gusto.

2) Disegnare un triangolo colorato:
Per disegnare questo triangolo avremo bisogno prima di tutto di 3 punti nello spazio (vertici) e dei colori da associare a questi punti (xna pensarà automaticamente a come miscelare i colori).

La variabile che definirà i nostri vertici è un array di tipo VertexPositionColor, che è un tipo di oggetto particolare in grado di ospitare inforamzioni di posizione e di colore appunto.

Ora analizziamo il file game.cs dove avverranno tutte le logiche che ci permetteranno di disegnare il triangolo.

Copio tutto il codice e velo spiego funzione per funzione.
GAME.CS-----------------------------------------------------------
using System;

using
System.Collections.Generic;
using
System.Linq;
using
Microsoft.Xna.Framework;
using
Microsoft.Xna.Framework.Audio;
using
Microsoft.Xna.Framework.Content;
using
Microsoft.Xna.Framework.GamerServices;
using
Microsoft.Xna.Framework.Graphics;
using
Microsoft.Xna.Framework.Input;
using
Microsoft.Xna.Framework.Media;
using
Microsoft.Xna.Framework.Net;
using
Microsoft.Xna.Framework.Storage;

namespace
imparandoxna_3d2
{

public class Game1 : Microsoft.Xna.Framework.Game
{

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

Camera camera; VertexPositionColor[] vertici;

public
Game1()
{

graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}


protected
override void Initialize()
{


//Creiamo la camera e la aggiungiamo ai componenti
camera = new Camera(this, new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up); Components.Add(camera);

//Inizializiamo i vertici da disegnare

base.Initialize();
}


protected
override void LoadContent()
{

// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);

vertici = new VertexPositionColor[3];
vertici
[0] = new VertexPositionColor(new Vector3(0, 1, 0), Color.Blue);
vertici[1] = new VertexPositionColor(new Vector3(1, -1, 0), Color.Red);
vertici[2] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.White); // TODO: use this.Content to load your game content here
}

protected
override void Update(GameTime gameTime)
{

// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this
.Exit();

// TODO: Add your update logic here

base.Update(gameTime);
}


protected
override void Draw(GameTime gameTime)
{

GraphicsDevice.Clear(Color.CornflowerBlue);

// inviamo la tipologia di dato da procesare al graphic device

GraphicsDevice
.VertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionColor.VertexElements);

//Disegniamo il triangolo
BasicEffect effect = new BasicEffect(GraphicsDevice, null);
effect
.World = Matrix.Identity;
effect.View = camera.view;
effect.Projection = camera.projection;
effect.VertexColorEnabled = true;
effect.Begin();
foreach
(EffectPass pass in effect.CurrentTechnique.Passes) {
pass.Begin();
GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleStrip ,vertici,0,1);
pass.End();
}

effect
.End();

base.Draw(gameTime);
}
}
}

Abbiamo aggiunto prima di tutto la variabile di classe
VertexPositionColor[] vertici;

che inizializziamo nella loadContent, definendo semplicemente che si tratta di un array di 3 elementi e ogni elemento avrà una posizione e un colore. Questi saranno quindi i vertici del nostro triangolo in posizioni e colori differenti.

Ora la parte più laboriosa, la funzione Draw:
In primo luogo guardiamo questa riga
            // inviamo la tipologia di dato da procesare al graphic device            

GraphicsDevice
.VertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionColor.VertexElements);
Quello che stiamo facendo qui è avvisare il GraphichsDevice che stiamo per passare un determinato tipo di Vertici, quindi Xna avvierà delle logiche (che non conosco) che saranno in grado di gestire questo tipo di dato!

Prima di spiegarvi l'ultimo blocco sono obbligato ad i ntrodurvi prevente il sistema tramite il quale xna disegna il 3D.
Qualcuno di voi avrà sentito già parlare di vertex e pixel shader immagino...questi due tipi di "funzioni" sono stati introdotti da pochi anni in quella che è la logica della pipeline grafica, permettendo di scaricare la cpu da compiti e calcoli che possono essere demandati alla GPU.
Operazioni che consentono di creare tutti quei bellissimi effetti che trovate nei giochi di ultima generazione (blur, glow riflessi etc) liberando la CPU dall'onerosa quantità di calcoli altrimenti necessari e spostando il tutto lato video.

Questo tipo di operazioni vengono gestite in DirectX (e quindi in XNA) tramite un liguaggio chiamato HLSL (High Level Shader Language), e XNA utilizza sempre questo passaggio nel suo processo di rendering del 3d.
Non preoccupatevi! potrete continuare questo tutorial senza sapere assolutamene scrivere una riga di HLSL, dato che xna fornisce una classe chiamata appunto BasicEffect, che si occuperà di gestire i nostri 3d attraverso un semplice e predefinito HLSL.

Dato che farò un post dedicato interamente a HLSL, per ora non mi dilungo e chiudo dicendovi che è possibile passare dati direttamente da XNA agli effetti che creiamo e che ogni effetto è composto da una o più Techiche e ogni tecnica da uno o più passi.... quando vedremo un file HLSL capirete meglio, per ora fidatevi :P e guardate come procediamo per stampare il nostro tanto atteso triangolo.

       //Disegniamo il triangolo

BasicEffect effect = new BasicEffect(GraphicsDevice, null);
effect
.World = Matrix.Identity;
effect.View = camera.view;
effect.Projection = camera.projection;
effect.VertexColorEnabled = true;
effect.Begin();
foreach
(EffectPass pass in effect.CurrentTechnique.Passes) {
pass.Begin();
GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleStrip ,vertici,0,1);
pass.End();
}

effect
.End();
Prima di tutto definiamo l'effetto base del quale vi stavo parlando, poi passiamo a questo effetto informazioni di posizione proiezione della camera attraverso i parametri View e Projection e di posizione del triangolo stesso attraverso la matrice World.
In questo caso passiamo al parametro World il valore MatrixIdentity che identifica un parametro "nuetro" (un pò come lo zero nelle somme e l'1 nelle moltiplicazioni) questo farà si che il triangolo venga disegnato a coordinate (0,0,0).

Dopo aver abilitato la gestione dei colori, apriamo una sessione di elaborazione dell'effetto, racchiusa tra le funzioni Begin e End (un pò come succedete per quando lavoriamo con lo spriteBatch). Come vedete scorriamo ogni passo della tecnica attualmente utilizzata ( l'effetto base ha già una tecnica di deafault presettata), e per ogni passo ci curiamo di disegnare tramite la funzione :
          GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleStrip ,vertici,0,1);


I parametri passatti a questa funzioni indicano il metodo di congiunzione delle linee che uniscono i vertici disegnati (primitivetype.trianglestrip), la lista dalla quale prendere i punti da disegnare (vertici), un eventuale offset dal quale partire in questa lista (in questo caso 0) e infine il numero di figure che disegneremo (1).


Compilando otterrete questo:

e aggiungerei un bel "WOOOW!".....


3) Traslazione e rotazione del triangolo
Il terzo obiettivo che ci siamo posti è di dar vita a qualche animazione.
Facciamo effettuare una semplice rotazione al triangolo, come prima cosa ricordate che la matrice sulla quale dobbiamo lavorare è la World perchè la nostra camerà non si sposterà.

Andate nel file game1.cs aggiugnete la variabile di classe
Matrix world = Matrix.Identity;

E nella funzione Draw()
dove inizializziamo la variabile effect.world
scrivete
effect.World = world;
A questo punto siamo in grado di modificare il valore della matrice world nella funzione update
aggiungete:
world *= Matrix.CreateRotationY(MathHelper.PiOver4 / 20) ;
e compilando vedremo il nostro triangolo ruotare.....anche se a un certo punto lo vedrete sparire , proprio quando dovrebbe mostrarci la sua faccia posteriore.

Questo succede perchè di default viene attivata una funzione che nasconde le facce chiamate Backface ovvero le face che sono orientate in verso opposto alla normale del piano. Questo serve per risparmiare di mostrare e calcolare aree normalmente non visibili. Se immaginate un cubo composto da 6 facce, noi vediamo normalmente solo il lato esterno... quindi non avrebbe senso disegnare anceh il lato interno del cubo.
Per attivare questa modalità di visualizzazione aggiungiamo dopo il metodo clear nella draw:
GraphicsDevice.RenderState.CullMode = CullMode.None;
Ricompilando ora vedrete anche il lato posteriore :D

Bene per oggi direi che di concetti ne abbiamo visti un bel pò!
come al solito allego il codice sorgente del progetto.

Codice Sorgente:
imparandoxna_3d.2.zip

Alla prossima!

6 commenti:

Anonimo ha detto...

Funziona, mi disegna la linea... ma mi rallenta di una maniera allucinante il rendering.
Perchè?

yariok ha detto...

In che senso ti rallenta il rendering ?

Anonimo ha detto...

i tutorials sono ottimi.

Ho 36 anni programmo in C e vorrei inziare ad
imparare in dettaglio il C#. Sono in possesso
del software XNA 3.1 e visual C# 2008 Express.

L'unico problema è la difficoltà nel creare
anche un semplice videogame 2d ma con la mia età non + giovanissima voglio tentare dopo che ho avuto una certa familiarità nel campo 2d, praticamente verso "l'impossibile" ovvero il 3d. Chiudo l'argomento domandandovi:
è semplice inziare? Da dove posso iniziare?
Ho scaricato vari pdf in XNA. La mia email è
SILVESTRI74.INFORMATICSRESEARCH@GMAIL.COM

Anonimo ha detto...

Il tutorial non funziona più con VS2010 e il nuovo XNA

Mora ha detto...

Per farlo funzionare con XNA 4.0 (e VS2010) bisogna modificare la parte del Draw in questo modo:

foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
GrapgicsDevice.DrawUserPrimitives ... (il resto è uguale)
}



Non bisogna mettere effect.Begin() ed effect.End()


Per il fatto che rallenta, dopo un "Espolra prestazioni" ho scoperto che la parte più pesante è la creazione del BasicEffect quindi consiglierei di mettere l'inizializzazione dell'effect in Initialize o in qualche posto dove non viene chiamato continuamente. Nel Draw o ancora meglio nell'Update metterei la parte che fa ruotare il triangolo. In questo modo sono riuscito ad ottenere delle prestazioni molto soddisfacenti.

CIAO

Jasper ha detto...

Ciao! E' l'ennesimo tutorial che leggo nel tentativo di capirci qualcosa sul 3D e devo dire che finalmente ho trovato il primo ben scritto (intendo anche dire, scritto da qualcun che sembra aver capito a fondo l'argomento)! Tuttavia lascia che ti corregga una piccola sciocchezza! :D Quando scrivi "A questo punto compilando dovreste vedere la solita schermata blu vuota di xna... ma la state visualizzando attraverso una camera", in realtà ancora non stiamo visualizzando attraverso la camera!

Posta un commento