B5. Le Height Maps
Anatomia di una Height Map
La Height Map (mappa di altezza) serve per definire l'altezza di ogni singolo vertice che compone un paesaggio: è, infatti, molto
comodo avere tutte le altezze salvate in un file, dal quale poi, possiamo caricare tutto il terreno mediante due semplici metodi. In genere,
le height map non sono altro che immagini, in cui ogni pixel rappresenta l'altezza del vertice corrispondente. Di conseguenza, le dimensioni
dell'immagine determinano anche le dimensioni del terreno 3D: un'immagine di 128x128 pixel produrrà un terreno lungo 128 unità
e largo 128 unità (ovviamente è possibile ingrandire o ridurre le dimensioni e del terreno e dell'immagine a proprio
piacimento). Il singolo pixel viene rappresentato da una combinazione di 4 componenti: Alpha (trasparenza), Red (Rosso), Green (Verde) e
Blue (Blu). Perciò basta considerare una o più di queste come se fossero un'altezza. Di solito, le height map sono totalmente in
bianco e nero, perciò i valori RGB sono sempre uguali. Nel nostro caso, il bianco
definirà una montagna o un altopiano, il grigio un terreno a metà e il nero i fondali mairini.
In questo capitolo preleveremo
la componente Rossa di ogni pixel della heightmap per definire la coordinata Y del vertice corrispondente.

Useremo questa Height Map, prelevata da questo bellissimo sito
Leggere l'HeightMap e impostare i vertici
Dopo aver salvato l'immagine sul vostro hard disk, copiatela nella cartella bin/Debug del vostro progetto. Ecco il codice:
Allora, passiamo a spiegare i tre passaggi più complessi del codice:Imports Microsoft.Xna.FrameworkImports Microsoft.Xna.Framework.InputImports Microsoft.Xna.Framework.GraphicsPublic Class GameInherits Microsoft.Xna.Framework.Game 'Cominciamo a dividere il sorgente in sezioni, per 'renderlo più ordinato. Alla fine di questo 'tutorial mi ringrazierete, perchè ci sarà 'un bel po' di codice. 'Usiamo lo statement #Region, che non ho spiegato nella 'guida sul Vb.Net. È molto semplice: '#Region "Nome regione" ''codice '#End Region 'Non ha alcuna funzione dinamica: serve solo per rendere 'il codice più ordinato #Region "Variabili globali"Private AppPathAs String = _My .Application.Info.DirectoryPathPrivate GraphicsAs GraphicsDeviceManagerPrivate ShaderAs EffectPrivate Vertices(5)As VertexPositionColorPrivate Indices(11)As Int32Private VDeclarationAs VertexDeclarationPrivate ViewMatrixAs MatrixPrivate ProjectionMatrixAs MatrixPrivate WorldMatrixAs MatrixPrivate DX, DZAs Single Private AngleAs Single 'La variabile HeightData è un array a 'due dimensioni, che a due coordinate x e z 'fa corrispondere una coordinata y. Ad esempio 'HeightData(1, 2) restituirà l'altezza y 'del vertice di coordinate x = 1, z = -2 '(ricordate sempre che z è invertito) Private HeightData(,)As Single 'Definiamo qui come costanti l'altezza minima 'e massima raggiungibili da un vertice. Dato 'che la componente R di un colore può 'andare da 0 a 255, certo non vorremmo che un 'vertice abbia altezza 255, altrimenti il 'terreno sarebbe enormemente deformato verso 'l'alto! Private Const MinHeightAs Single = 0Private Const MaxHeightAs Single = 30 'Queste variabili assumeranno il valore di 'larghezza e lunghezza del terreno Private TerrainWidth, TerrainLengthAs Int32#End Region #Region "Funzioni utili"Private Function GetEffect(ByVal FileNameAs String )As EffectDim CompEffectAs CompiledEffect = _ Effect.CompileEffectFromFile(FileName, _ Nothing, Nothing, _ CompilerOptions.None, _ TargetPlatform.Windows)Return New Effect(Me .GraphicsDevice, _ CompEffect.GetEffectCode, _ CompilerOptions.None,Nothing )End Function 'Ridefiniamo la cara funzione GetTexture, già usata 'nel tutorial sul 2D Private Function GetTexture(ByVal FileNameAs String )Return Texture2D.FromFile(Me .GraphicsDevice, FileName)End Function #End Region #Region "Caricamento Heightmap" 'Carica l'array HeightData Private Sub LoadHeightMap(ByVal HeightMapAs Texture2D) 'Imposta le dimensioni del terreno TerrainWidth = HeightMap.Width TerrainLength = HeightMap.Height '2D! 'Crea un array tabulare per contenere i colori 'di ogni pixel dell'heightmap Dim Colors(TerrainWidth * TerrainLength - 1)As Color 'Riempie l'array prelevando i colori dall'immagine HeightMap.GetData(Colors) 'Ridimensiona HeightData. 'Ricordate che 'ReDim ha un effetto particolaresugli array a due dimensioni. Infatti, 'bisogna ridimensionare ogni singola dimensione 'dell'array. Per rinfrescarvi la memoria, 'guardate il capitolo sugli array nella guida For IAs Int32 = 0To TerrainWidth - 1ReDim HeightData(I, TerrainLength - 1)Next For XAs Int32 = 0To TerrainWidth - 1For ZAs Int32 = 0To TerrainLength - 1 'Vedi Nota 1 per la spiegazione di questo calcolo HeightData(X, Z) = MinHeight + _ (Colors(X + Z * TerrainWidth).R / 255) * _ (MaxHeight - MinHeight)Next Next VDeclaration =New VertexDeclaration(Me .GraphicsDevice, _ VertexPositionColor.VertexElements)End Sub 'Ora che abbiamo la dimensione del terreno e l'altezza di 'ogni vertice, popoliamo l'array Vertices Private Sub SetVertices() 'Consideriamo Vertices come un array tabulare ReDim Vertices(TerrainWidth * TerrainLength - 1)For XAs Int32 = 0To TerrainWidth - 1For ZAs Int32 = 0To TerrainLength - 1With Vertices(X + Z * TerrainWidth) 'Impostiamo la posizione .Position =New Vector3(X, HeightData(X, Z), -Z) 'E il colore .Color = Color.WhiteEnd With Next Next End Sub 'Questo metodo è più complesso di prima, 'quindi fate attenzione Private Sub SetIndices() 'Vedi Nota 2 per la spiegazione di questo codice ReDim Indices((TerrainWidth - 1) * (TerrainLength - 1) * 6 - 1) 'Noi stiamo leggendo una tabella, ma Indices 'è un array a una dimensione, quindi ci 'serve un contatore per impostarne i valori Dim CounterAs Int32 = 0For XAs Int32 = 0To TerrainWidth - 2For ZAs Int32 = 0To TerrainLength - 2 'Questi valori rappresentano delle coordinate 'nell'array tabulare Vertices. Infatti, 'ogni quattro pixel nell'immagine, si hanno 'quattro vertici, perciò due triangoli 'e quindi 6 indici. È un po' confusionario, 'lo so, ma fateci l'abitudine. 'Vertice inferiore sinistro Dim LowerLeftAs Int16 = X + Z * TerrainWidth 'Vertice inferiore destro Dim LowerRightAs Int16 = (X + 1) + Z * TerrainWidth 'Vertice superiore sinistro Dim TopLeftAs Int16 = X + (Z + 1) * TerrainWidth 'Vertice superiore destro Dim TopRightAs Int16 = (X + 1) + (Z + 1) * TerrainWidth 'Ricordate che le coordinate X e Z sono prese da 'un'immagine a due dimensioni. Perciò 'leggendo l'immagine dall'alto verso il basso, la 'coordinata y dei pixel aumenta 'Primo triangolo, formato, in ordine, dai vertici: '- Superiore sinistro '- Inferiore destro '- Inferiore sinistro Indices(Counter) = TopLeft Indices(Counter + 1) = LowerRight Indices(Counter + 2) = LowerLeft Counter += 3 'Secondo triangolo, formato, in ordine, dai vertici: '- Superiore sinistro '- Superiore destro '- Inferiore destro Indices(Counter) = TopLeft Indices(Counter + 1) = TopRight Indices(Counter + 2) = LowerRight Counter += 3Next Next End Sub #End Region #Region "Gestione risorse"Sub New ()Me .Graphics =New GraphicsDeviceManager(Me )Me .Content.RootDirectory = "content"End Sub Protected Overrides Sub Initialize()MyBase .Initialize()End Sub Protected Overrides Sub LoadContent() Shader = GetEffect(AppPath & "\SimpleShader.fx") 'Carica la HeightMap LoadHeightMap(GetTexture(AppPath & "\HeightMap.bmp")) 'Imposta i vertici SetVertices() 'Imposta gli indici SetIndices() 'Spostiamo la telecamera molto in fuori, poiché 'il terreno sarà piuttosto grande ViewMatrix = Matrix.CreateLookAt( _New Vector3(70, 100, -70), _New Vector3(0, 0, 0), _New Vector3(0, 1, 0)) ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView( _ MathHelper.PiOver4, _Me .GraphicsDevice.Viewport.AspectRatio, _ 1, 500)MyBase .LoadContent()End Sub Protected Overrides Sub UnloadContent()MyBase .UnloadContent()End Sub #End Region #Region "Aggiornamento mondo 3D"Protected Overrides Sub Update(ByVal GameTimeAs GameTime) 'Facciamo sì che il terreno ruoti, per poterlo 'ammirare da ogni angolazione Angle += MathHelper.Pi / 600MyBase .Update(GameTime)End Sub Protected Overrides Sub Draw(ByVal gameTimeAs GameTime)Me .Graphics.GraphicsDevice.Clear(Color.CornflowerBlue) Shader.CurrentTechnique = Shader.Techniques("Colored") Shader.Parameters("View").SetValue(ViewMatrix) Shader.Parameters("Projection").SetValue(ProjectionMatrix) 'Trasliamo il mondo in modo che tutto il terreno sia 'al centro del nostro sguardo, puoi ruotiamolo Shader.Parameters("World").SetValue( _ Matrix.CreateTranslation( _New Vector3(-TerrainWidth / 2, 0, TerrainLength / 2)) * _ Matrix.CreateRotationY(Angle))Me .GraphicsDevice.RenderState.FillMode = FillMode.WireFrame Shader.Begin()For Each PassAs EffectPassIn Shader.CurrentTechnique.Passes Pass.Begin()With Me .GraphicsDevice .VertexDeclaration = VDeclaration .DrawUserIndexedPrimitives(PrimitiveType.TriangleList, _ Vertices, 0, Vertices.Length, _ Indices, 0, Indices.Length / 3)End With Pass.End()Next Shader.End()MyBase .Draw(gameTime)End Sub #End Region End Class
- Nota 1
HeightData(X, Z) = MinHeight + _ (Colors(X + Z * TerrainWidth).R / 255) * _ (MaxHeight - MinHeight)Come abbiamo detto prima, non vogliamo che i nostri vertici possano avere un'altezza che raggiunga le 255 unità, perchè, altrimenti, il terreno risulterebbe enormemente deformato. I valori che abbiamo stabilito prevedono un'altezza minima di 0 (ossia i vertici più in basso avranno coordinata y = 0) e una massima di 30 (ossia i vertici più in alto avranno una coordinata y = 30). Allora, consideriamo la componente R come se rappresentasse una percentuale: 255 significa 100% di altezza (ossia 30 unità), mentre 0 significa 0% (ossia 0 unità). Calcoliamo la percentuale con una proporzione:R : 255 = x : 100
x è la nostra percentuale, la mettiamo da parte un attimo. Dato che l'altezza minima del vertice è MinHeight, aggiungiamo subito questo valore alla sua altezza:
x = 100 * R / 255Y = MinHeight
Allora, se vogliamo ottenere l'altezza massima dobbiamo aggiungere alla minima la loro differenza:Ymax = MinHeight + (MaxHeight - MinHeight)
Per ottenere le altezze intermedie, non dobbiamo aggiungere tutta la differenza (MaxHeight - MinHeight), ma solo una sua parte, definita dalla percentuale x, allora arriviamo a questo rislutato:Y = MinHeight + (x / 100) * (MaxHeight - MinHeight)
Svolgiamo il termine x:Y = MinHeight + ((R * 100 / 255) / 100) * (MaxHeight - MinHeight)
Dove R * 100 / 100 = R. Sostituendo le variabili opportune arriviamo a:HeightData(X, Z) = MinHeight + _ (Colors(X + Z * TerrainWidth).R / 255) * _ (MaxHeight - MinHeight) - Nota 2
Intanto iniziamo a spiegare questa linea:
Ormai sappiamo che il "-1" finale è onnipresente nella definizione di un array, quindi lo do per scontato. Quello che bisogna spiegare sono i -1 nelle parentesi e il fattore 6. Guardiamo questo disegno, dove i pallini neri sono vertici:ReDim Indices((TerrainWidth - 1) * (TerrainLength - 1) * 6 - 1)Sorvoliamo sulla precisione...
Ci sono 5 vertici in orizzontale e 3 in verticale. Tuttavia ogni riga contiene 4 (e non 5) triangoli, e ogni colonna contiene 2 (e non 3) triangoli. La conclusion logica è che in una tabella larga X vertici e alta Y vertici, ci saranno X-1 triangoli per ogni riga e Y-1 triangoli per ogni colonna. Ecco la motivazione di quei -1.
Per quanto riguarda il 6, il discorso è molto più semplice. Dato che ogni vertice fa parte di due triangoli, saranno necessari 6 indici per disegnarli tutti e due (potete controllare nell'immagine sopra).
Ora, immaginate che ogni vertice dell'immagine sopra sia un pixel della nostra Height Map. È evidente che per ogni quattro pixel, ci sono due triangoli, il primo formato dai pixel superiore sinistro, inferiore destro e inferiore sinistro; il secondo definito dai pixel inferiore destro, superiore sinistro, superiore destro. E così si spiega anche il pezzo di codice dentro il for. - Nota 3:
Se non sapete cos'è un array tabulare, guardate la sezione appunti
Il paesaggio in wireframe
The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.



