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:
E avremo questo risultato:Public Class Game '... 'TEXTURE ---------------------------------------- Private GrassAs 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 XAs Int32 = 0To TerrainWidth - 1For ZAs Int32 = 0To TerrainLength - 1With Vertices(X + Z * TerrainWidth)Dim YAs Single = HeightData(X, Z) .Position =New Vector3(X, Y, -Z) .TextureCoordinate.X = X / 32 .TextureCoordinate.Y = Z / 32End 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()My Base.LoadContent()End Sub '... Protected Overrides Sub Draw(ByVal gameTimeAs 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 LightDirectionAs 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 PassAs EffectPassIn Shader.CurrentTechnique.Passes Pass.Begin()With Me .GraphicsDevice .VertexDeclaration = VDeclarationMe .GraphicsDevice.DrawIndexedPrimitives( _ PrimitiveType.TriangleList, 0, 0, _ Vertices.Length, 0, Indices.Length / 3)End With Pass.End()Next Shader.End()My Base.Draw(gameTime)End Sub End Class
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 = XMod 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:
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:Private Structure VertexPositionNormalMultitexturePublic PositionAs Vector3Public NormalAs Vector3Public TextureCoordinateAs Vector4Public TextureWeidthAs Vector4Public Shared SizeInBytesAs Int16 = (3 + 3 + 4 + 4) * 4Public Shared VertexElementsAs 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
Ed ecco il risultato (notate i gradienti):Public Class Game '... Private SandAs Texture2DPrivate RockAs Texture2DPrivate SnowAs Texture2D '... Private Sub SetVertices() ReDim Vertices(TerrainWidth * TerrainLength - 1)Dim StripAs Single = (MaxHeight - MinHeight) / 4For XAs Int32 = 0To TerrainWidth - 1For ZAs Int32 = 0To TerrainLength - 1With Vertices(X + Z * TerrainWidth)Dim YAs 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 TotalAs Single = .TextureWeight.X Total += .TextureWeight.Y Total += .TextureWeight.Z Total += .TextureWeight.W .TextureWeight.X /= Total .TextureWeight.Y /= Total .TextureWeight.Z /= Total .TextureWeight.W /= TotalEnd 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()My Base.LoadContent()End Sub '... Protected Overrides Sub Draw(ByVal gameTimeAs 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 LightDirectionAs 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 PassAs EffectPassIn Shader.CurrentTechnique.Passes Pass.Begin()With Me .GraphicsDevice .VertexDeclaration = VDeclarationMe .GraphicsDevice.DrawIndexedPrimitives( _ PrimitiveType.TriangleList, 0, 0, _ Vertices.Length, 0, Indices.Length / 3)End With Pass.End()Next Shader.End()My Base.Draw(gameTime)End Sub End Class
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)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:Dim TotalAs Single = .TextureWeight.X Total += .TextureWeight.Y Total += .TextureWeight.Z Total += .TextureWeight.W .TextureWeight.X /= Total .TextureWeight.Y /= Total .TextureWeight.Z /= Total .TextureWeight.W /= Total
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| / 8Il 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 : 100Quindi 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.



