\\ Home : Articoli : Stampa
Ombre Volumetriche
Di RobyDx (del 25/03/2007 @ 15:12:22, in DirectX9, linkato 2968 volte)

La stencil buffer permette, come ho detto, la possibilita di creare maschere per delimitare con precisione delle zone dello schermo in cui renderizzare. Uno degli infiniti usi è la gestione delle ombre volumetriche. Un ombra si dice volumetrica se un oggetto la proietta non solo a terra ma su tutti gli oggetti che si trovano in ombra. Una cosa normale in natura ma molto difficile in computer grafica specie se in real time. Giocate a qualche gioco in terza persona e osservate l'ombra a terra: questa nella maggior parte dei casi si proietterà perfettamente al suolo e su un muro ma non su nemici che potrebbero trovarsi nel cono d'ombra. In questo caso l'ombra non è reale ma solo approssimata. Una vera ombra invece dovrebbe coprire tutti gli oggetti. Lo stencil permette proprio questo.

Ombra Volumetrica 

Come potete notare l'ombra dell'aereo si proietta sia a terra che sul cubo. Eventuali oggetti tra il cubo e l'aereo verrebbero ombreggiati.
Come fare?
L'idea elaborata da molti è semplice: creare una mesh che assuma la forma del cono d'ombra coperto (sostanzialmente l'oggetto proiettato al suolo dal punto iniziale) e rendere visibile solo le zone toccate. Osservate questo disegno

Nel primo vi è la parte terminale di un cilindro (proiezione d'ombra di una sfera) e nella successiva la visione della parte interna di questo cilindro. La differenza è l'ombra. Basta quindi creare una mesh ombra, proiettarla al suolo con visione normale (cullMode in senso antiorario) ed aumentare lo stencil, proiettarla invertita (cullMode in senso orario) e decrementare. La zona con valore differente da zero è la zona d'ombra: un bel rettangolo nero con stencil appropriato e l'ombra comparirà solo nei punti giusti. Infatti il cerchio compare dove viene interrotto il fascio d'ombra e di conseguenza ogni oggetto che la interromperà modificherà lo stencil.

Codice

Innanzitutto occorre creare la mesh proiettata (la cosa meno semplice). Ci sono vari algoritmi sulla rete, uno dei più semplici ma mediamente efficace: quello proposto negli esempi microsoft. Ora ve lo spiegherò (è una classe quindi semplice da usare).

Class shadowMesh
Private vertices(32000) As Vector3
Private numVertices As Integer = 0
Public Structure ShadowVertex
Public p As Vector4
Public color As Integer
Public Const Format As VertexFormats = VertexFormats.Transformed Or VertexFormats.Diffuse
End Structure 'ShadowVertex
Public Sub BuildFromMesh(ByVal mesh As Mesh, ByVal light As Vector3)
' Note: the MeshVertex format depends on the FVF of the mesh
Dim tempVertices As Vertex() = Nothing
Dim indices As Short() = Nothing
Dim edges As Short() = Nothing
Dim numFaces As Integer = mesh.NumberFaces
Dim numVerts As Integer = mesh.NumberVertices
Dim numEdges As Integer = 0
' Allocate a temporary edge list
edges = New Short(numFaces * 6) {}
' Lock the geometry buffers
tempVertices = CType(mesh.LockVertexBuffer(GetType(Vertex), 0, numVerts), Vertex())
indices = CType(mesh.LockIndexBuffer(GetType(Short), 0, numFaces * 3), Short())
' For each face
Dim i As Integer
For i = 0 To numFaces - 1
Dim face0 As Short = indices((3 * i + 0))
Dim face1 As Short = indices((3 * i + 1))
Dim face2 As Short = indices((3 * i + 2))
Dim v0 As Vector3 = tempVertices(face0).p
Dim v1 As Vector3 = tempVertices(face1).p
Dim v2 As Vector3 = tempVertices(face2).p
' Transform vertices or transform light?
Dim vCross1 As Vector3 = Vector3.Subtract(v2, v1)
Dim vCross2 As Vector3 = Vector3.Subtract(v1, v0)
Dim vNormal As Vector3 = Vector3.Cross(vCross1, vCross2)
If Vector3.Dot(vNormal, light) >= 0.0F Then
AddEdge(edges, numEdges, face0, face1)
AddEdge(edges, numEdges, face1, face2)
AddEdge(edges, numEdges, face2, face0)
End If
Next i
For i = 0 To numEdges - 1
Dim v1 As Vector3 = tempVertices(edges((2 * i + 0))).p
Dim v2 As Vector3 = tempVertices(edges((2 * i + 1))).p
Dim v3 As Vector3 = Vector3.Subtract(v1, Vector3.Multiply(light, 10))
Dim v4 As Vector3 = Vector3.Subtract(v2, Vector3.Multiply(light, 10))
' Add a quad (two triangles) to the vertex list
vertices(numVertices) = v1 : numVertices += 1
vertices(numVertices) = v2 : numVertices += 1
vertices(numVertices) = v3 : numVertices += 1
vertices(numVertices) = v2 : numVertices += 1
vertices(numVertices) = v4 : numVertices += 1
vertices(numVertices) = v3 : numVertices += 1
Next i
' Unlock the geometry buffers
mesh.UnlockVertexBuffer()
mesh.UnlockIndexBuffer()
End Sub 'BuildFromMesh
'-----------------------------------------------------------------------------
' Name: AddEdge()
' Desc: Adds an edge to a list of silohuette edges of a shadow volume.
'-----------------------------------------------------------------------------
Public Sub AddEdge(ByVal edges() As Short, ByRef numEdges As Integer, ByVal v0 As Short, ByVal v1 As Short)
' Remove interior edges (which appear in the list twice)
Dim i As Integer
For i = 0 To numEdges - 1
If edges((2 * i + 0)) = v0 And edges((2 * i + 1)) = v1 Or (edges((2 * i + 0)) = v1 And edges((2 * i + 1)) = v0) Then
If numEdges > 1 Then
edges((2 * i + 0)) = edges((2 * (numEdges - 1) + 0))
edges((2 * i + 1)) = edges((2 * (numEdges - 1) + 1))
End If
numEdges -= 1
Return
End If
Next i
edges((2 * numEdges + 0)) = v0
edges((2 * numEdges + 1)) = v1
numEdges += 1
End Sub 'AddEdge
Sub render()
device.VertexFormat = VertexFormats.Position
device.DrawUserPrimitives(PrimitiveType.TriangleList, numVertices / 3, vertices)
End Sub
End Class

Questo codice genera tramite la funzione buildFromMesh un'array di vertici che sono la proiezione della mesh secondo la posizione della luce. L'istruzione render la renderizza. Disegnate la scena. Una volta renderizzato tutto si inizia a disegnare le ombre.
Il rendering dell'ombra va fatto due volte:

device.RenderState.ZBufferWriteEnable = False
device.RenderState.StencilEnable = True
' Dont bother with interpolating color
device.RenderState.ShadeMode = ShadeMode.Flat
device.RenderState.StencilFunction = Compare.Always
device.RenderState.StencilZBufferFail = StencilOperation.Keep
device.RenderState.StencilFail = StencilOperation.Keep
device.RenderState.ReferenceStencil = 1
device.RenderState.StencilPass = StencilOperation.Increment
device.RenderState.AlphaBlendEnable = True
device.RenderState.SourceBlend = Blend.Zero
device.RenderState.DestinationBlend = Blend.One

Con questo settaggio l'ombra non viene disegnata (alphablending a zero) ed aumenta il valore dello stencil di 1. Ora disegnate le ombre.

device.RenderState.CullMode = Cull.Clockwise
' Decrement stencil buffer value
device.RenderState.StencilPass = StencilOperation.Decrement

Ora ridisegnate le ombre.
Rimettete tutto a posto

' Restore render states
device.RenderState.ShadeMode = ShadeMode.Gouraud
device.RenderState.CullMode = Cull.CounterClockwise
device.RenderState.ZBufferWriteEnable = True
device.RenderState.StencilEnable = False
device.RenderState.AlphaBlendEnable = False

Ora dovete disegnare un rettangolo nero per l'ombra (possibilmente semitrasparente).

device.RenderState.ZBufferEnable = False
device.RenderState.StencilEnable = True
device.RenderState.AlphaBlendEnable = True
device.RenderState.SourceBlend = Blend.SourceAlpha
device.RenderState.DestinationBlend = Blend.InvSourceAlpha
device.TextureState(0).ColorArgument1 = TextureArgument.TextureColor
device.TextureState(0).ColorArgument2 = TextureArgument.Diffuse
device.TextureState(0).ColorOperation = TextureOperation.Modulate
device.TextureState(0).AlphaArgument1 = TextureArgument.TextureColor
device.TextureState(0).AlphaArgument2 = TextureArgument.Diffuse
device.TextureState(0).AlphaOperation = TextureOperation.Modulate
device.RenderState.ReferenceStencil = 1
device.RenderState.StencilFunction = Compare.LessEqual
device.RenderState.StencilPass = StencilOperation.Keep
device.SetTexture(1, Nothing)
device.VertexFormat = CustomVertex.TransformedColored.Format
device.DrawUserPrimitives(PrimitiveType.TriangleStrip, 2, v)
' Restore render states
device.RenderState.ZBufferEnable = True
device.RenderState.StencilEnable = False
device.RenderState.AlphaBlendEnable = False

Dove v è un array di vertici trasformati che rappresentano un rettangolo nero.
La scena è ora ombrata!!!!!!!!

Dettagli!!!!!

Il vettore luce dell'ombra va calcolato in questo modo

v4 = Vector3.Transform(vLight, Matrix.Invert(matriceO))

V4 è un vector4. MatriceO è la matrice world dell'oggetto. Il vettore luce è ora orientato secondo l'oggetto.
L'oggetto shadowMesh andrebbe creato ogni frame se l'oggetto in movimento e qui nasce il problema. Infatti se osservate un oggetto che ruota davanti ad una luce l'ombra non sarà la rotazione. Di conseguenza se la posizione dell'oggetto muta rispetto alla luce occorre ricreare l'ombra. La cosa risulta molto pesante e di difficile realizzazione per una scena molto complessa (pochi giochi oggi la usano). Di conseguenza va usata con un minimo di astuzia. L'ombra infatti è diversa solo se l'oggetto ruota mentre rimane invariata per traslazioni e scala. Potrete quindi preparare a priori diversi set di ombre sacrificando un pò di memoria e proiettarle a secondo delle posizioni anzichè generarle tutte continuamente. Per uno stage la cosa è anche migliore: uno stage è immobile quindi dovrete generarle solo una volta. Difficilmente la gente noterà le ombre dei personaggi mentre noterà quelle di un ponte e la qualità sarà senz'altro migliore.
Vi lascio all'esempio che vi permette di gestire l'ombra.

Esempio VB.Net

Esempio C#