Ora conosciamo tutti i segreti delle tecniche fin'ora usate, abbiamo un paesaggio texturizzato di alta qualità, ma mancano ancora
molte cose prima di finire. La prima di queste è il cielo.
Come simulare il cielo: Cube Map
La tecniche usate per simulare il cielo in un'applicazione 3D sono principalmente. La prima prevede ci caricare una gigantesca smisfera
sulla quale si applica un'immagine del cielo; la seconda, invece, usa un enorme cubo, e ad ogni faccia associa un'immagine di una direzione
differente. Nonostante possa apparire migliore la prima, noi useremo la seconda, principalmente per questi motivi:
La prima tecnica non copre tutto la spazio 3D, ma si posizione generalmente sopra il centro del terreno. Per questo motivo, quando
introdurremo la tecnica dell'acqua, potrebbero esserci delle riflessioni errate, poiché riflettono una parte di spazio che non è
la volta celeste
Usando un cubo, possiamo anche limitare l'estensione dell'acqua e ottimizzare le riflessioni
Bastano otto vertici per definire un cubo, ma ce ne voglio molti di più per una (semi)sfera
Caricare modelli 3D con visual basic, senza un gestore integrato come quello di C#, è abbastanza complesso
Ora potreste anche avere delle domande, ad esempio: se usiamo un cubo, non c'è pericolo che le facce non combacino? No, poiché
useremo delle texture fatte apposta (Cube Map) per essere usate in situazioni come queste. E ancora: ma gli spigoli verrebbero comunque
evidenziati dalla luce! No, poiché solo le superfici provviste di normali rientrano nel calcolo della luce: se noi lasciamo le
facce del cubo senza normale, verranno disegnate esattamente come accadeva nella tecnica Colored.
Costruire il cubo
Per definire un cubo servono, come accennato prima, otto vertici, che definiscono 10 triangoli (non usiamo la parete inferiore, perchè
non si vede ed è inutile) e perciò 30 indici. Potrebbe sembrare un lavoretto semplice semplice, ma bisogna tenere conto di un
fattore non da poco: ogni faccia ha una texture diversa, quindi bisognerà usare parametri diversi nello shader e perciò
sarà necessario renderizzarne una alla volta. Questo implica che dovremo definire ogni faccia separatemente dalle altre. Per semplificare
il compito, creiamo una nuova struttura:
'Rappresenta una parete del cuboPrivateStructure SkyCubeFace
'Vertici della parete (4)Public Vertices() As VertexPositionTexture
'Indici da usare per disegnare i triangoli (6).
'Ricordate che un quadrato è formato da due
'triangoli e per ognuno di questi occorrono 3 indici.Public Indices() As Int32
'Immagine da applicare a questa paretePublic Texture As Texture2D
EndStructure
Dichiariamo, inoltre, altre tre variabili (se siete dei perfezionisti, potete ridurle a due):
Private SDeclaration As VertexDeclaration
'...
'Un array di 5 texture, una per ogni paretePrivate SkyFaces(4) As Texture2D
'Un array di 5 SkyCubeFacePrivate SkyCube(4) As SkyCubeFace
Ora, prima di continuare, fermiamoci ad analizzare questo schema:
Cube map
Per ogni vertice ho indicato un numero: questo numero verrà usato nel codice successivo, e potrete meglio visualizzare il ragionamento
che seguo io. Ammettiamo che il centro della faccia inferiore del cubo sia il punto (0,0,0) dello spazio 3D. Ammettiamo anche che ogni
lato sia lungo K. Sotto questa ipotesi, avremo che ogni vertice di destra (0, 1, 4 e 5) avrà una coordinata X pari a -K/2, mentre ogni
vertice di sinistra (2, 3, 6 e 7) avrà una coordinata X pari a +K/2. Allo stesso modo, i vertici 1, 3, 5 e 7 avranno una Z pari a
-K/2, mentre i vertici 0, 2, 4 e 6 avranno una Z pari a +K/2. Quelli alla base, li considereremo con Y = 0, mentre quelli più in alto
avranno un'altezza arbitraria che possiamo impostare a 200. Inoltre, bisogna considerare un'altra importante premessa: le coordinate texture
devono essere esatte per ottenere il risultato voluto. Quindi, dovremo assegnare una coordinata texture (0,0) all'angolo superiore sinistro
e (1,1) all'angolo inferiore destro.
Ora possiamo passare al codice vero e proprio:
PublicClass Game
'...#Region "Cielo"
'Questa funzione generic serve solo da aiuto. Dato un array di
'qualsiasi tipo, la funzione restituisce gli elementi
'con gli indici desiderati. Non confondete il significato
'di indice di un elemento in un array con quello di indice
'usato per disegnare una primitiva.PrivateFunction GetElements(Of T)(ByVal Sources() As T, _
ByVal ParamArray Indices() As Int32) As T()
Dim Result(Indices.Length - 1) As T
For I As Int16 = 0 To Indices.Length - 1
Result(I) = Sources(Indices(I))
NextReturn Result
EndFunction
'Carica la volta celestePrivateSub LoadSkyDome()
'Array che conterrà gli otto vertici del cubo. È
'vero che ogni faccia dovrà avere i suoi quattro, ma
'se noi li dichiariamo prima tutti e poi li copiamo
'a quattro a quattro in una parete alla volta, il tutto
'diventa molto più semplice, e anche molto più
'ordinato. Notare che utilizziamo un tipo VertexPositionTexture.
'Questa struttura contiene dati solo sulla posizione
'e sulle coordinate texture e non sulla normale.Dim SkyVertices(7) As VertexPositionTexture
'HalfSize rappresenta il K/2 di cui parlavamo primaDim HalfSize As Int16 = 200
'Guardate la figura del cubo inserita qua sopra
'per sapere quale vertice sto definendo
SkyVertices(0).Position = New Vector3(-HalfSize, 200, HalfSize)
SkyVertices(1).Position = New Vector3(-HalfSize, 200, -HalfSize)
SkyVertices(2).Position = New Vector3(HalfSize, 200, HalfSize)
SkyVertices(3).Position = New Vector3(HalfSize, 200, -HalfSize)
SkyVertices(4).Position = New Vector3(-HalfSize, 0, HalfSize)
SkyVertices(5).Position = New Vector3(-HalfSize, 0, -HalfSize)
SkyVertices(6).Position = New Vector3(HalfSize, 0, HalfSize)
SkyVertices(7).Position = New Vector3(HalfSize, 0, -HalfSize)
'La prima faccia (quella superiore) è formata dai vertici
'0, 1, 2 e 3. Con la funzione GetElements li preleviamo direttamente
'dall'array testé dichiarato, senza fare troppa fatica
SkyCube(0).Vertices = GetElements(SkyVertices, 0, 1, 2, 3)
'Faccia sinistra
SkyCube(1).Vertices = GetElements(SkyVertices, 5, 1, 4, 0)
'Faccia anteriore
SkyCube(2).Vertices = GetElements(SkyVertices, 4, 0, 6, 2)
'Faccia destra
SkyCube(3).Vertices = GetElements(SkyVertices, 6, 2, 7, 3)
'Faccia posteriore
SkyCube(4).Vertices = GetElements(SkyVertices, 7, 3, 5, 1)
'Dichiaro la prima faccia separata per un semplice motivo.
'Tutte le altre parti della cube map hanno un'immagine che
'rappresenta per metà la terra e per metà il
'cielo (è il meglio che sono riuscito a trovare).
'Per questo motivo, le loro coordinate texture andranno da
'0 a 1 in larghezza, ma da 0 a solo 0.5 in altezza. Invece,
'la faccia superiore ospita solo cielo e non c'è
'bisogno di togliere niente.With SkyCube(0)
.Indices = New Int32() {0, 1, 2, 2, 1, 3}
.Texture = SkyFaces(0)
.Vertices(0).TextureCoordinate = New Vector2(0, 1)
.Vertices(1).TextureCoordinate = New Vector2(0, 0)
.Vertices(2).TextureCoordinate = New Vector2(1, 1)
.Vertices(3).TextureCoordinate = New Vector2(1, 0)
EndWith
'Inizializza le altre facceFor I AsByte = 1 To 4
With SkyCube(I)
.Indices = New Int32() {0, 1, 2, 2, 1, 3}
.Texture = SkyFaces(I)
.Vertices(0).TextureCoordinate = New Vector2(0, 0.5)
.Vertices(1).TextureCoordinate = New Vector2(0, 0)
.Vertices(2).TextureCoordinate = New Vector2(1, 0.5)
.Vertices(3).TextureCoordinate = New Vector2(1, 0)
EndWithNext
'Poiché stiamo utilizzando un diverso tipo di vertici
'rispetto al programma principale, occorre una diversa
'definizione:
SDeclaration = New VertexDeclaration(Me.GraphicsDevice, _
VertexPositionTexture.VertexElements)
EndSub
'Disegna il cieloPrivateSub DrawSky()
'È la prima volta che usiamo due tecniche
'contemporaneamente!For I AsByte = 0 To 4
'Notare che qui usiamo una semplice tecnica Textured...
Shader.CurrentTechnique = Shader.Techniques("Textured")
Shader.Parameters("View").SetValue(ViewMatrix)
Shader.Parameters("Projection").SetValue(ProjectionMatrix)
Shader.Parameters("World").SetValue(Matrix.Identity)
'... senza calcolo della luce
Shader.Parameters("LightEnabled").SetValue(False)
Shader.Parameters("ATexture").SetValue(SkyFaces(I))
Me.GraphicsDevice.RenderState.CullMode = CullMode.None
Shader.Begin()
ForEach Pass As EffectPass In Shader.CurrentTechnique.Passes
Pass.Begin()
WithMe.GraphicsDevice
'Cambia la dichiarazione
.VertexDeclaration = SDeclaration
.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, _
SkyCube(I).Vertices, 0, SkyCube(I).Vertices.Length, _
SkyCube(I).Indices, 0, SkyCube(I).Indices.Length / 3)
EndWith
Pass.End()
Next
Shader.End()
NextEndSub#End Region
'...ProtectedOverridesSub Draw(ByVal gameTime As GameTime)
Me.Graphics.GraphicsDevice.Clear(Color.CornflowerBlue)
'Disegna il cielo prima del resto
DrawSky()
'Ora che abbiamo utilizzato il metodo
'DrawUserIndexedPrimivites, dobbiamo per forza aggiornare
'i buffer se vogliamo che funzioni tutto come primaMe.CopyToBuffer()
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 AsNew 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)
Shader.Parameters("Texture0").SetValue(Sand)
Shader.Parameters("Texture1").SetValue(Grass)
Shader.Parameters("Texture2").SetValue(Rock)
Shader.Parameters("Texture3").SetValue(Snow)
Shader.Begin()
ForEach Pass As EffectPass In Shader.CurrentTechnique.Passes
Pass.Begin()
WithMe.GraphicsDevice
.VertexDeclaration = VDeclaration
Me.GraphicsDevice.DrawIndexedPrimitives( _
PrimitiveType.TriangleList, 0, 0, _
Vertices.Length, 0, Indices.Length / 3)
EndWith
Pass.End()
Next
Shader.End()
MyBase.Draw(gameTime)
EndSubEndClass
L'unica cosa che manca ora, sono le immagini. Potete scaricarle da qui (il sito originale da dove le ho prese è
questo). Sono numerate da uno a cinque, e per questo è
facile caricarle: