Casino online









Mercato forex






B9. Le Textures


Cosa sono le textures?
Fin'ora abbiamo sempre utilizzato dei colori uniformi per disegnare tutte le superfici sullo schermo. Ma per rendere un paesaggio realistico - o, se non proprio realistico, almeno credibile - c'è bisogno di qualcosa di più accurato, che faccia chiaramente percepire a chi guarda il tipo di superficie che si sta osservando. Stiamo parlando di textures, ossia di semplici immagini che vengono "spalmate" sopra le facce (i triangoli) dei modelli 3D, in modo da dare un'impressione di verosimiglianza. Una texture può essere usata per simulare l'erba, le rocce, i particolari del terreno, ma non solo: viene impiegata spesso anche nelle scritte, nelle insegne, in qualsiasi decoro o simbolo che stia su una superficie e, perfino, nelle facce dei personaggi. In questo tutorial, useremo delle texture per simulare erba, sabbia, roccia e neve e rimpiazzare, in questo modo, i quattro colori che avevamo utilizzato prima per distinguere le varie fasce di altezza.


Modificare il codice
Per ora useremo una sola texture, quella che rappresenta l'erba, che potete scaricare qui: non vi preoccupate del formato (*.dds); non è necessaria alcuna conversione perchè XNA riesce a leggere da solo i file di questo tipo. Comunque, se volete vedere l'immagine com'è veramente, potete scaricare XnView, un programma in grado di aprire pressoché ogni tipo di file immagine esistente.
Bene, come potrete immaginare, dovremo cambiare di nuovo il tipo dei vertici: quelli che abbiamo ora possono memorizzare soltanto un colore e non sono in grado di supportare una texture. Il nuovo tipo da usare è VertexPositionNormalTexture, ed esiste già, quindi non dobbiamo scrivere nessun'altra struttura. Se volete fare presto ed essere sicuri, usate la funzione Quick Replace (Sostituisci) di Visual Studio. Dopo aver effettuato questo cambio, bisogna rinnovare anche la tecnica usata: in questo caso, la più adatta si chiama Textured, e permette di impiegare una texture da applicare a tutti i triangoli disegnati:
Public Class Game
    '...
    
    'TEXTURE ----------------------------------------
    Private Grass As Texture2D
    
    '...
    
    Private Sub SetVertices()
        ReDim Vertices(TerrainWidth * TerrainLength - 1)

        'Come vedete, ho rimosso tutto il vecchio codice sul
        'colore. Al suo posto ci sono due nuove e semplici
        'righe, che impostano la coordinata texture di questo
        'vertice.
        'Vedi la spiegazione successiva
        For X As Int32 = 0 To TerrainWidth - 1
            For Z As Int32 = 0 To TerrainLength - 1
                With Vertices(X + Z * TerrainWidth)
                    Dim Y As Single = HeightData(X, Z)

                    .Position = New Vector3(X, Y, -Z)
                    .TextureCoordinate.X = X / 32
                    .TextureCoordinate.Y = Z / 32
                End With
            Next
        Next
    End Sub
    
    '...
    
    Protected Overrides Sub LoadContent()
        Shader = GetEffect(AppPath & "\SimpleShader.fx")
        'Carica la texture dell'erba
        Grass = GetTexture(AppPath & "\grass.dds")

        LoadHeightMap(GetTexture(AppPath & "\HeightMap.bmp"))
        SetVertices()
        SetIndices()
        SetNormals()
        CopyToBuffer()

        ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView( _
            MathHelper.PiOver4, _
            Me.GraphicsDevice.Viewport.AspectRatio, _
            1, 500)

        CameraPosition = New Vector3(0, 40, TerrainLength / 2)
        Mouse.SetPosition(Me.GraphicsDevice.Viewport.Width / 2, _
                Me.GraphicsDevice.Viewport.Height / 2)
        PrevMouseState = Mouse.GetState
        UpdateView()

        MyBase.LoadContent()
    End Sub
    
    '...
    
    Protected Overrides Sub Draw(ByVal gameTime As GameTime)
        Me.Graphics.GraphicsDevice.Clear(Color.CornflowerBlue)

        'La nuova tecnica si chiama Textured, ma i suoi parametri
        'sono quasi identici a Colored, perciò non
        'dobbiamo cancellare alcuna riga di codice
        Shader.CurrentTechnique = Shader.Techniques("Textured")

        Shader.Parameters("View").SetValue(ViewMatrix)
        Shader.Parameters("Projection").SetValue(ProjectionMatrix)
        Shader.Parameters("World").SetValue( _
            Matrix.CreateTranslation( _
            New Vector3(-TerrainWidth / 2, 0, TerrainLength / 2)))

        Dim LightDirection As New Vector3(27.73, -17.67, 6.14)
        LightDirection.Normalize()
        Shader.Parameters("LightEnabled").SetValue(True)
        Shader.Parameters("LightDirection").SetValue(LightDirection)
        Shader.Parameters("AmbientFactor").SetValue(0.3F)

        'L'unica modifica da fare è questa, ossia quella
        'di impostare la texture che si deve applicare alla 
        'superficie dei triangoli.
        Shader.Parameters("ATexture").SetValue(Grass)

        Shader.Begin()
        For Each Pass As EffectPass In Shader.CurrentTechnique.Passes
            Pass.Begin()
            With Me.GraphicsDevice
                .VertexDeclaration = VDeclaration
                Me.GraphicsDevice.DrawIndexedPrimitives( _
                    PrimitiveType.TriangleList, 0, 0, _
                    Vertices.Length, 0, Indices.Length / 3)
            End With
            Pass.End()
        Next
        Shader.End()

        MyBase.Draw(gameTime)
    End Sub
End Class 
E avremo questo risultato:


Il paesaggio d'erba

Per determinare come venga disegnata la superficie del triangolo a partire dalla texture, si usano le coordinate texture. In un'immagine, si considera come punto (0,0) l'angolo superiore sinistro e come punto (1,1) l'angolo inferiore destro. Tutti i punti all'interno hanno coordinate decimali comprese tra 0 e 1. Ad esempio, abbiamo questa texture:


Neve

E definiamo un triangolo i cui tre vertici hanno le coordinate texture mostrate in figura:


Tre vertici sulla neve

Allora, il triangolo risultante, qualunque sia la sua forma e grandezza, avrà una superficie sempre disegnata in questo modo:


La superficie finale del triangolo

Tuttavia, potrebbe sorgere un dubbio. Infatti l'operazione di assegnamento che abbiamo operato specifica che le coordinate texture devono essere un trentaduesimo delle coordinate X e Z del vertice in questione. Allora, quando un vertice nello spazio si trova su X=64, la sua coordinata texture X dovrebbe essere 2, ma noi sappiamo che il massimo è 1. Non c'è nessun problema, poiché, una volta superato l'uno, si torna indietro, quindi 1.5 è come se fosse 0.5, e 2 come se fosse 1. Tutto sommato, sarebbe stata la stessa cosa scrivere:
.TextureCoordinate.X = X Mod 32 


Multitexturing
Il terreno, ora, è molto migliore rispetto a prima e dà già un'impressione diversa di maggior realismo. Tuttavia, poiché avevamo considerato che ci fossero quattro fasce di altezza diverse, non sarebbe corretto logicamente rivestire tutto d'erba quando i picchi più alti dovrebbero essere ricoperti di neve, e le profondità marine immerse nella sabbia. Ecco, quindi, che arriviamo a considerare il Multitexturing, ossia l'applicazione contemporanea di più texture. Inoltre, dato il salto di qualità che abbiamo già fatto, sarebbe opportuno che, nella transizione tra una fascia e l'altra, non ci fosse una netta distizione tra una texture e la successiva, ma piuttosto una sfumatura - o gradiente. Non vorremmo mica che l'erba s'interrompa d'un tratto per passare istantaneamente alle rocce, vero?
Per ovviare a tutti questi inconvenienti useremo la nuova tecnica MultiTextured, in cui non solo ogni vertice ha una coordinata texture, ma ha anche un peso differente per ogni texture. Passo a spiegare. Nel caso precedente avevamo una sola texture in gioco, ma qui ce ne sono quattro e ogni vertice le possiede tutte e quattro. Per non creare confusione, ossia per non sovrapporre tutte le immagini, viene dato un differente peso a ognuna delle texture: tale peso ne determina la trasparenza. Quindi, un vertice molto in basso avrà un peso del 100% per la texture "sabbia" e 0% per tutte le altre texture che, quindi, risulteranno invisibili. Allo stesso modo un vertice ad altezza leggermente elevata avrà, ad esempio, il 50% sia per "erba" che per "roccia", e 0% per le altre texture: la sua superficie risulterà sfumata tra il verde e il marrone, dando un ottimo risultato.
Cominciamo aggiungendo una nuova struttura:
    Private Structure VertexPositionNormalMultitexture
        Public Position As Vector3
        Public Normal As Vector3
        Public TextureCoordinate As Vector4
        Public TextureWeidth As Vector4

        Public Shared SizeInBytes As Int16 = (3 + 3 + 4 + 4) * 4
        Public Shared VertexElements As VertexElement() = 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), _
            New VertexElement(0, 4 * 6, _
                VertexElementFormat.Vector4, _
                VertexElementMethod.Default, _
                VertexElementUsage.TextureCoordinate, 0), _
            New VertexElement(0, 4 * 10, _
                VertexElementFormat.Vector4, _
                VertexElementMethod.Default, _
                VertexElementUsage.TextureCoordinate, 1) _
          }
    End Structure 
Quindi sotituiamo questa struttura al vecchio tipo VertexPositionNormalTexture. Ora aggiungiamo le nuove texture (sand, rock e snow, anche queste da copiare nella cartella bin\Debug del progetto) e modifichiamo il vecchio codice:
Public Class Game
    '...
    
    Private Sand As Texture2D
    Private Rock As Texture2D
    Private Snow As Texture2D
    
    '...
    
    Private Sub SetVertices()
        ReDim Vertices(TerrainWidth * TerrainLength - 1)

        Dim Strip As Single = (MaxHeight - MinHeight) / 4
        For X As Int32 = 0 To TerrainWidth - 1
            For Z As Int32 = 0 To TerrainLength - 1
                With Vertices(X + Z * TerrainWidth)
                    Dim Y As Single = HeightData(X, Z)

                    .Position = New Vector3(X, Y, -Z)
                    .TextureCoordinate.X = X / 32
                    .TextureCoordinate.Y = Z / 32
                    
                    'Vedi la spiegazione successiva
                    .TextureWeight.X = MathHelper.Clamp(1.0F - _ 
                        Math.Abs(Y - 0) / 8.0F, 0, 1)
                    .TextureWeight.Y = MathHelper.Clamp(1.0F - _ 
                        Math.Abs(Y - 12) / 6.0F, 0, 1)
                    .TextureWeight.Z = MathHelper.Clamp(1.0F - _ 
                        Math.Abs(Y - 20) / 6.0F, 0, 1)
                    .TextureWeight.W = MathHelper.Clamp(1.0F - _ 
                        Math.Abs(Y - 30) / 6.0F, 0, 1)

                    Dim Total As Single = .TextureWeight.X
                    Total += .TextureWeight.Y
                    Total += .TextureWeight.Z
                    Total += .TextureWeight.W

                    .TextureWeight.X /= Total
                    .TextureWeight.Y /= Total
                    .TextureWeight.Z /= Total
                    .TextureWeight.W /= Total
                End With
            Next
        Next
    End Sub
    
    '...
    
    Protected Overrides Sub LoadContent()
        Shader = GetEffect(AppPath & "\SimpleShader.fx")
        Grass = GetTexture(AppPath & "\grass.dds")
        Sand = GetTexture(AppPath & "\sand.dds")
        Rock = GetTexture(AppPath & "\rock.dds")
        Snow = GetTexture(AppPath & "\snow.dds")

        LoadHeightMap(GetTexture(AppPath & "\HeightMap.bmp"))
        SetVertices()
        SetIndices()
        SetNormals()
        CopyToBuffer()

        ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView( _
            MathHelper.PiOver4, _
            Me.GraphicsDevice.Viewport.AspectRatio, _
            1, 500)

        CameraPosition = New Vector3(0, 40, TerrainLength / 2)
        Mouse.SetPosition(Me.GraphicsDevice.Viewport.Width / 2, _
                Me.GraphicsDevice.Viewport.Height / 2)
        PrevMouseState = Mouse.GetState
        UpdateView()

        MyBase.LoadContent()
    End Sub
    
    '...
    
    Protected Overrides Sub Draw(ByVal gameTime As GameTime)
        Me.Graphics.GraphicsDevice.Clear(Color.CornflowerBlue)

        Shader.CurrentTechnique = Shader.Techniques("MultiTextured")

        Shader.Parameters("View").SetValue(ViewMatrix)
        Shader.Parameters("Projection").SetValue(ProjectionMatrix)
        Shader.Parameters("World").SetValue( _
            Matrix.CreateTranslation( _
            New Vector3(-TerrainWidth / 2, 0, TerrainLength / 2)))

        Dim LightDirection As New Vector3(27.73, -17.67, 6.14)
        LightDirection.Normalize()
        Shader.Parameters("LightEnabled").SetValue(True)
        Shader.Parameters("LightDirection").SetValue(LightDirection)
        Shader.Parameters("AmbientFactor").SetValue(0.3F)

        'Ora ci sono quattro texture come parametri
        'aggiuntivi. In ordine di altezza, dalla 0 alla 3
        Shader.Parameters("Texture0").SetValue(Sand)
        Shader.Parameters("Texture1").SetValue(Grass)
        Shader.Parameters("Texture2").SetValue(Rock)
        Shader.Parameters("Texture3").SetValue(Snow)

        Shader.Begin()
        For Each Pass As EffectPass In Shader.CurrentTechnique.Passes
            Pass.Begin()
            With Me.GraphicsDevice
                .VertexDeclaration = VDeclaration
                Me.GraphicsDevice.DrawIndexedPrimitives( _
                    PrimitiveType.TriangleList, 0, 0, _
                    Vertices.Length, 0, Indices.Length / 3)
            End With
            Pass.End()
        Next
        Shader.End()

        MyBase.Draw(gameTime)
    End Sub
End Class 
Ed ecco il risultato (notate i gradienti):


Il paesaggio in multitexturing

Ora passiamo a spiegare il codice di sopra, che ho mutuato dal mio splendido sito di riferimento:
.TextureWeight.X = MathHelper.Clamp(1.0F - Math.Abs(Y - 0) / 8.0F, 0, 1)
.TextureWeight.Y = MathHelper.Clamp(1.0F - Math.Abs(Y - 12) / 6.0F, 0, 1)
.TextureWeight.Z = MathHelper.Clamp(1.0F - Math.Abs(Y - 20) / 6.0F, 0, 1)
.TextureWeight.W = MathHelper.Clamp(1.0F - Math.Abs(Y - 30) / 6.0F, 0, 1)

Dim Total As Single = .TextureWeight.X
Total += .TextureWeight.Y
Total += .TextureWeight.Z
Total += .TextureWeight.W

.TextureWeight.X /= Total
.TextureWeight.Y /= Total
.TextureWeight.Z /= Total
.TextureWeight.W /= Total 
Allora, noi vogliamo che la texture della sabbia si veda al 100% nei punti più bassi, che sfumi fino all'erba e che poi non si veda più. Ebbene, questo è esattamente il comportamento della prima funzione, ossia:
MathHelper.Clamp(1.0F - Math.Abs(Y - 0) / 8.0F, 0, 1) 
[Tra parentesi, la funzione MathHelper.Clamp(A, B, C) restituisce A se B<A<C; restituisce B se A è minore di B; restituisce C se A è maggiore di C. In pratica limita il valore di A tra un minimo (B) e un massimo (C)]
Infatti, consideriamo la funzione f(x):
f(x) = 1 - |x| / 8 
Il suo grafico sarà così


Grafico di 1 - |x| / 8

Ora, dato che f(x) viene limitata tra 0 e 1 dalla funzione Clamp, si vede benissimo che essa vale 1 per x = 0 e che declina lentamente fino a valere 0 per x=8, e quindi anche per tutti i valori successivi. La sabbia, perciò, si vedrà al 100% nei vertici di altezza 0 e sfumerà lentamente fino all'altezza 8.
La seconda funzione è simile:
f(x) = 1 - |x - 12| / 6 



Grafico di 1 - |x - 12| / 6

Clamp(f(x), 0, 1) vale 0 per x che va da 0 a 6; cresce lentamente fino a 1 per 6<x<12; quindi decresce fino a 0.
Facendo lo stesso ragionamento per tutte le funzioni, è evidente che quando cala l'una, l'altra aumenta di valore, in modo da soppiantarsi a vicenda. Anche se non avete seguito l'analisi matematica di queste funzioni, esse ci garantiscono una sfumatura omogenea da un valore al successivo per tutti i quattro valori, ossia le quattro texture.
Passiamo a spiegare la seconda parte del codice, con un esempio. All'altezza 7, il peso della texture "sabbia" è 12.5, il peso della texture "erba" è 16.7, mentre il peso delle altre due è 0. Evidentemente 12.5+16.7 non dà il 100% (ossia 1). Per trovare il valore esatto di ognuna, si deve procedere con una semplice proporzione (t1 e t2 sono i due valori trovati):
t1 : (t1 + t2) = x : 100 
Quindi si avrà che x1 = (12.5/29.2) = 42.8% e x2 = 57.2% (i triangoli a quest'altezza hanno una superficie quasi perfettamente sfumata a metà tra sabbia ed erba). Il codice sopra illustrato generalizza questo processo, calcolando il totale e dividendo ogni valore per il totale stesso.








 

The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.