VS85 Team's profileXNALearnersBlogListsGuestbook Tools Help

Blog


    8/2/2008

    Illuminazione con Shader e Introduzione a Microsoft.XNA.Effect

    Come usare HLSL per scrivere i primi shaders in XNA. [il post e' un WIP: una ulteriore versione con moltissimo codice verra' pubblicata in futuro]

    Dato un progetto XNA nuovo di zecca, dichiariamo gli oggetti fondamentali che ci permettono di renderizzare geometria:

    Effect effect;
    VertexBuffer vertexBuffer;
    IndexBuffer indexBuffer;
    VertexDeclaration vertexDeclaration;

    L'Effect serve a caricare i nostri shader HLSL, VertexBuffer e IndexBuffer contengono la geometria che disegneremo, ed infine la VertexDeclaration contiene una descrizione del formato dei vertici contenuti nel VertexBuffer associato.

    Definiamo il vertice che ci interessa adoperare, seguendo il template dei vertici gia' pronti in XNA (come ad esempio VertexPositionNormalTexture):

    struct VertexPositionNormal
    {
        public Vector3 Position;
        public Vector3 Normal;
    
        public VertexPositionNormal(Vector3 Position, Vector3 Normal)
        {
            this.Position = Position;
            this.Normal = Normal;
        }
    
        static public int SizeInBytes { get { return 4 * 3 * 2; } }
        static public VertexElement[] VertexElements
        {
            get
            {
                return new VertexElement[]
                {
                    new VertexElement(0, 0, VertexElementFormat.Vector3,
                        VertexElementMethod.Default, VertexElementUsage.Position, 0),
                    new VertexElement(0, 4 * 3, VertexElementFormat.Vector3,
                        VertexElementMethod.Default, VertexElementUsage.Normal, 0)
                };
            }
        }
    }
    

    Vediamo che il nostro vertice contiene due Vector3, Position e Normal. La proprieta' SizeInBytes dice quanto grande e' un vertice, ossia due Vector3, composti ciascuno da tre float grandi ognuno quattro bytes: 4 * 3 * 2. La proprieta' VertexElement serve per descrivere byte per byte come e' costruito il nostro vertice, ossia ogni campo del vertice quanto spazio occupa, che tipo di interpolazione usa, e soprattutto che semantica (o Usage) ha tra una serie di possibilita'. In questo modo indichiamo che il nostro vertice e' composto da due campi (il numero di elementi nell'array VertexElements, il primo dei quali e':

    new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0),

    ossia un oggetto che comincia dal byte 0, e' un Vector3, interpola secondo l'algoritmo di default (lineare) e contiene una posizione nello spazio. Il secondo campo e' invece:

    new VertexElement(0, 4 * 3, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0)

    che comincia dal byte 4 * 3 (la dimensione di un Vector3!), e' sempre un Vector3 e contiene una normale.

    Assegnamo alla vertex declaration il formato dei vertici che useremo:

    vertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionNormal.VertexElements);

    e costruiamo i nostri vertex e index buffer in modo da contenere un cilindro (il che si ottiene in modo molto elegante tramite delle query Linq):

    Func<int, float, IEnumerable<VertexPositionNormal>> createCircle =
        (numVertices, y) =>
            Enumerable
                .Range(0, numVertices)
                .Select(i => new VertexPositionNormal(
                    new Vector3(
                        (float)Math.Cos(2.0f * Math.PI * i / (numVertices - 1.0f)),
                        y,
                        (float)Math.Sin(2.0f * Math.PI * i / (numVertices - 1.0f))),
                    new Vector3(
                        (float)Math.Cos(2.0f * Math.PI * i / (numVertices - 1.0f)),
                        0.0f,
                        (float)Math.Sin(2.0f * Math.PI * i / (numVertices - 1.0f)))));
    var vertices = new List<VertexPositionNormal>();
    for (int i = 0; i < numCircles; i++)
    {
        vertices.AddRange(createCircle(numVerticesPerCircle, (i - 2.5f) / 5.0f));
    }
    
    var indices = new List<Int32>();
    for (int i = 0; i < numCircles - 1; i++)
    {
        var baseIndex = i * numVerticesPerCircle;
        var baseIndex1 = (i + 1) * numVerticesPerCircle;
        var quads = Enumerable
            .Range(0, numVerticesPerCircle - 1)
            .Select(j =>
                new Int32[]
                {
                    j + baseIndex,
                    j + 1 + baseIndex,
                    j + baseIndex1,
                    j + 1 + baseIndex,
                    j + 1 + baseIndex1,
                    j + baseIndex1
                })
            .Aggregate((acc, quad) => acc.Concat(quad).ToArray());
        indices.AddRange(quads);
    }
    
    indexBuffer = new IndexBuffer(GraphicsDevice,
        4 * indices.Count, BufferUsage.WriteOnly, IndexElementSize.ThirtyTwoBits);
    indexBuffer.SetData(indices.ToArray());
    
    vertexBuffer = new VertexBuffer(GraphicsDevice,
        vertices.Count * vertexDeclaration.GetVertexStrideSize(0),
        BufferUsage.WriteOnly);
    vertexBuffer.SetData(vertices.ToArray());
    

    Lo shader che impieghiamo e' un file che risiede nel progetto Content (il nome del file e' "SimpleShader.fx") i cui parametri globali sono le matrici World, View, Projection e i dati seulla luce singola della scena:

    float4x4 World;
    float4x4 View;
    float4x4 Projection;
    
    float3 LightPosition;
    float4 LightColor;
    

    Il tipo di vertice che entra nello shader e' lo stesso del vertex buffer:

    struct LambertVertexShaderInput
    {
        float4 Position : POSITION0;
        float4 Normal : NORMAL;
    };

    mentre quello che esce contiene anche una serie di dati per calcolare l'illuminazione (normale, direzione della luce, direzione dell'osservatore):

    struct LambertVertexShaderOutput
    {
        float4 Position : POSITION0;
        float3 N : TEXCOORD0;
        float3 L : TEXCOORD1;
        float3 V : TEXCOORD2;
    };

    Il vertex shader vero e proprio e' una funzione che calcola un LambertVertexShaderOutput a partire dal LambertVertexShaderInput passato:

    LambertVertexShaderOutput LambertVertexShaderFunction(LambertVertexShaderInput input)
    {
        LambertVertexShaderOutput output;
    
        float4 worldPosition = mul(input.Position, World);
        float4 viewPosition = mul(worldPosition, View);
        output.Position = mul(viewPosition, Projection);
        float3 N = normalize(mul(input.Normal, World));
        float3 L = normalize(LightPosition - worldPosition);
    
        float3 eyePosition = mul(-View._m30_m31_m32, transpose(View));
        float3 V = normalize(eyePosition - worldPosition);
    
        output.N = N;
        output.L = L;
        output.V = V;
    
        return output;
    }

    Infine il pixel shader in questione calcola, a partire dall'output del vertex shader, il colore dei punti occupati dal triangolo (tramite la funzione di Phong):

    float4 LambertPixelShaderFunction(LambertVertexShaderOutput input) : COLOR0
    {
        float3 N = normalize(input.N);
        float3 L = normalize(input.L);
        float3 V = normalize(input.V);
        float3 Lr = reflect(-L, N);
        
        float4 diffuse = LightColor * saturate(dot(N, L));
        float4 reflection = 0.9f * pow(saturate(dot(Lr, V)), 8.0f);
        
        return diffuse + reflection;
    }

    Per caricare l'effetto HLSL appena visto, dobbiamo appoggiarci alla content pipeline. Appena caricato l'effetto in questione impostiamo subito i valori iniziali dei parametri globali dell'effetto:

    effect = Content.Load<Effect>("SimpleShader");
    effect.Parameters["World"].SetValue(Matrix.Identity);
    effect.Parameters["View"].SetValue(
        Matrix.CreateLookAt(Vector3.Backward * 2.0f, Vector3.Zero, Vector3.Up));
    effect.Parameters["Projection"].SetValue(
        Matrix.CreatePerspectiveFieldOfView(1.5f, 1.333f, 0.01f, 10000.0f));
    

    Per renderizzare la nostra geometria tramite l'effetto caricato e' sufficiente impostare i parametri mancanti (colore e posizione della luce) e invocare GraphicsDevice.Draw*Primitives con la geometria che ci interessa:

     

    var time = (float)gameTime.TotalGameTime.TotalSeconds;
    var LightPosition = new Vector3(1.0f, 0.0f, 1.0f);
    var LightRotation = 
        Matrix.CreateRotationY(-time * 0.1f - 0.5f);
    LightPosition = Vector3.Transform(LightPosition, LightRotation);
    
    effect.Begin();
    
    effect.Parameters["LightPosition"].SetValue(LightPosition);
    effect.Parameters["LightColor"].SetValue(Color.Salmon.ToVector4());
    effect.Parameters["World"].SetValue(Matrix.CreateRotationY(0.0f * time / 2.0f)); 
    effect.CurrentTechnique.Passes["Lambert"].Begin();
    
    DrawVertexAndIndexBuffersAsTriangleList(vertexBuffer, indexBuffer,
        vertexDeclaration);
    
    effect.CurrentTechnique.Passes["Lambert"].End();
    effect.End();

    Il risultato ottenuto e' di notevole...effetto :)

    image

    Trovate il codice sorgente qui; inoltre potete scaricare qui il video o guardarlo direttamente embedded ad alta risoluzione: