Wellen in DirectX
von Klaus Neumann
Vorwort
Dieses Tutorial beschäftigt sich mit der Erstellung einer möglichst realistisch wirkenden Wasseroberfläche und baut auf der DirectX Tutorialserie von Jack Hoaxley auf. Da ich in VB nur sehr wenige wirklich gute Beispielcodes fand, habe ich mich entschlossen dieses Thema zu behandeln. Unser Ziel ist es diese Wasseroberfläche so flexibel, realistisch und schnell wie möglich zu machen. Hierfür verwende ich einige Rechentechniken aus der Trigonometrie, welche aber auch für jüngere Leser leicht zu verstehen sein werden. Da ich selbst noch vieles in DirectX lernen kann, würde ich mich freuen, wenn ich reichlich Emails bekommen würde um diese Szene noch realistischer wirken zu lassen. Beispielsweise könnte man diese Szenen noch mit zusätzlichem Lightning und Fogging ausstatten. Auch ist zu sagen, dass selbst Spiele wie Counterstrike nicht mit allzu realistischen Wasseroberflächen dienen können.
Einführung
Um eine Wasseroberfläche in DirectX zu erstellen, braucht man zuerst mal einen Ansatz, wie die Wellen, welche erstellt werden, auszusehen haben. Es gibt im wesentlichen 3 Arten, welche ich am sinnvollsten ansehe. Die erste Art, ist die Wellenbewegung in Form eines sich von einem Zentrum wegbewegenden Wellenzirkels. Hierzu 2 Skizzen:
Die Pfeile auf der rechten Seite stellen die Orientierung der Wellen dar. Die Kreisen die Wellen selbst. Die zweite Art ist eine Oberfläche, welche aus verschiedenen sich auf und ab bewegenden Aufstiegspunkten besteht. Diese Sorte lasse ich allerdings nur sehr wenig Aufmerksamkeit zukommen, da sie nicht sehr realistisch wirkt. Diese Art wurde in ersten 3D- Anwendungen verwendet.
Die hier als letzte angesprochne Wasseroberfläche ist linear, das heißt sie bewegt sich von rechts nach links, bzw. anders herum. In diesem Beispiel ist ebenfalls eine Mathematische Rechenart von Nöten. Diese Wellenbewegung wollen wir nun vertiefen. Hierzu eine Skizze:
Bevor wir aber starten, möchte ich auf die Geschwindigkeit, der oben angesprochenen Wellenarten, zu sprechen kommen. Die erste und zweite Art benötigt n² mal so viele Vertices um die Wasseroberfläche zu erstellen wie die, welche wir im Anschluss besprechen. Die erste Art benötigt von der Bearbeitung dann noch eine Pufferstärke, welche die Lineare Methode um längen übersteigt. Alles in allem, bin ich zu dem Schluss gekommen, dass die Lineare Methode für uns die beste Mischung aus Realismus, Geschwindigkeit und Flexibilität darstellt.
Vertexaufbau
Zu erst müssen wir uns um den Vertexaufbau kümmern. Hier werden Frage geklärt was für Vertices wir benötigen, welche Vor- und Nachteile sie besitzen und wie wir sie in ein "vernünftiges" Programm bringen. Um die Flexibilität zu gewährleisten erstellen wir eine Schnittstelle von diesem Typ:
Public Sub CreateWaves( _ Move As Single, _ Wellenhöhe As Single, _ Fliesgeschwindigkeit As Single, _ Länge As Single, _ Breite As Single) ' ... End Sub
"Move" steht später für die Bewegung der Wellen, "Wellenhöhe" für die Streckung unserer Amplitude, "Fliesgeschwindigkeit" hat mit der anschließenden Texturierung des Primitive zu tun, "Länge" und "Breite" sind Variablen für die Ausmaße der Wasseroberfläche.
Es gibt verschiedene Möglichkeiten und Vertexarten um die Wasseroberfläche zu gestalten. Ich denke, dass es einleuchtet, wenn wir zur Erstellung Dreieck-Strips (D3DPT_TRIANGLESTRIP) verwenden. Ich habe das gleiche Beispiel einmal mit Dreieck-Listen (D3DPT_TRIANGLELIST) programmiert. Allerdings sind hier unschöne Lücken aufgetreten. Wahrscheinlich waren es Rundungsfehler. Mit den Dreieck- Strips tritt so etwas natürlich nicht auf. Außerdem ist die Arbeit mit der Textur anschließend wesentlich leichter. In diesem Bild kann man schon einige Ansätze an Wellen erkennen. Wir benötigen in der fertigen Wasseroberfläche 60 Aufzugspunkte. Natürlich ist es anschließend dem Leser vorbehalten dies Zahl zu ändern. Mehr machen die Oberfläche genauer aber auch langsamer.
Nun zu der Architektur der Oberfläche, die y- Koordinate der Vertices besprechen wir direkt im Anschluss, denn noch sind sie nicht wichtig für uns.
Um dieses Netz zu kreieren brauchen wir eine Schleife, die jeweils den geraden und ungeraden Vertex bearbeitet.
For X = -15 To 15 '... Next X
Die Schleife bearbeitet insgesamt (15 - (-15)) * 2 = 60 Punkte
Hier die Vertices. Zunächst beschreibe ich die Parameter, welche für uns von Interesse sind.
Dim Waves(61) As LITVERTEX Waves((X + 15) * 2 + 0) = CreateLitVertex(X * Länge, Sin(X + move) * Wellenhöhe _ , Breite, C110, 0, ((X + 15) / 30) + move / Fliesgeschwindigkeit, 1) Waves((X + 15) * 2 + 1) = CreateLitVertex(X * Länge, Sin(X + move) * Wellenhöhe _ , -Breite, C010, 0, ((X + 15) / 30) + move / Fliesgeschwindigkeit, 0)
Kommen wir zuerst zum Arrayfeld: Waves((X + 15) * 2 + 0). Da unser Array korrekt aufsteigen soll, benötigen wir diese, recht einfach zu verstehende mathematische Formel. Die Schleife beginnt Anfangs mit: Waves(0) dann Waves(1) usw.
Der erste Parameter stellt die X- Koordinate dar. "Länge" ist dafür verantwortlich die Wasseroberfläche zu strecken. Der dritte Parameter stellt lediglich die Breite der Wasseroberfläche dar. Ich habe die Breite 30 gewählt. Nummer 4 ist die Farbe des einzelnen Vertex, hier eine Konstante, welche ist aus den vorhergegangenen Tutorials von Jack Hoaxley übernommen habe. Parameter Nummer 5 hat für uns ebenfalls keine Bedeutung. Die letzten beiden Parameter sind für die Textur verantwortlich.
Dim Texture As Direct3DTexture8 Set Texture = D3DX.CreateTextureFromFileEx( _ D3DDevice, _ App.Path & "\wasser.jpg", _ 128, _ 128, _ D3DX_DEFAULT, _ 0, _ DispMode.Format, _ D3DPOOL_MANAGED, _ D3DX_FILTER_LINEAR, _ D3DX_FILTER_LINEAR, _ 0, _ ByVal 0, _ ByVal 0)
Da wir die Höhe der einzelnen Vertices verändern, müssen wir der Grafikkarte noch sagen, wie sie die Textur auf unseren Primitive zu setzen hat. "Fliesgeschwindigkeit" und "move" sind dafür da um das Wasser in Bewegung zu bringen. Spielt ruhig ein wenig mit den Werten herum um zu erkennen wie der Computer die Werte verarbeitet. Learning by doing ist wieder einmal das Stichwort.
Die Wellen
Kommen wir nun zu den Wellen. Wie man vielleicht auf den einzelnen Bildern erkennen kann. Sieht man das ich für die Wellenbewegung die Sinuskurve verwendet habe. In der Regel bespricht man die Sinuskurve in der 9. - 10. Klasse. Ich möchte hier aber ein paar kleine mathematische Finessen ansprechen:
Mein Parameter sieht so aus: Sin(X + move) * Wellenhöhe.
Ich transformiere die eigentliche Sinusfunktion mit Techniken der Funktionsmutation. Klingt kompliziert, ist es aber nicht. Move ist für die Graphenverschiebung nach Rechts verantwortlich. Die Variable "Wellenhöhe" streckt die Funktion. Sollte das z.B. Wetter im Spiel schlechter sein, so kann man die Höhe der Wellen vergrößern. Gegen Ende bringen wir noch alles in einen Vertexbuffer um die spätere Visualisierung zu bewerkstelligen.
Dim VBuffer(10) As Direct3DVertexBuffer8 Set VBuffer(0) = D3DDevice.CreateVertexBuffer( _ Len(Waves(0)) * 120, _ 0, _ Lit_FVF, _ D3DPOOL_DEFAULT) D3DVertexBuffer8SetData VBuffer(0), 0, Len(Waves(0)) * 120, 0, Waves(0)
In der späteren Renderfunktion sieht der Pogrammcode ca. so aus:
D3DDevice.SetTexture 0, Texture D3DDevice.SetStreamSource 0, VBuffer(0), Len(Waves(0)) D3DDevice.DrawPrimitive D3DPT_TRIANGLESTRIP, 0, 60
Ich denke, dass dies aber kein großes Problem darstellt. Im folgenden Teil, bauen wir noch ein paar Alphablending Effekte ein, um das Wasser transparent erscheinen zu lassen. Hier bauen wir in der Initialisierung einige Optionen ein:
D3DDevice.SetRenderState D3DRS_SRCBLEND, 3 '2 (hell) oder 3 (etwas dunkler)
D3DDevice.SetRenderState D3DRS_DESTBLEND, D3DBLEND_ONE
D3DDevice.SetRenderState D3DRENDERSTATE_SHADEMODE, D3DSHADE_FLAT
Man muss nur noch vor die Visualisierung diese Zeile einbauen:
D3DDevice.SetRenderState D3DRS_ALPHABLENDENABLE, 1
Mit 0 schaltet man das Ganze wieder ab.
Die Kiste
Um die Szene realistischer wirken zu lassen bauen wir noch eine Kiste ein, welche sich in den Wellen bewegt. Auch hier wollen wir eine möglichst flexible Funktion erstellen um alles leichter bearbeiten zu lassen.
Public Sub CreateCubes( _ move As Single, _ größe As Single, _ X As Single, _ Y As Single, _ Z As Single)
Der Parameter "move" ist für die Bewegung in den Wellen. "größe" erklärt sich von selbst und X,Y,Z gibt die Position im dreidimensionalen Raum an. Hier die Funktion:
Public Sub CreateCubes( _ vert As Long, _ move As Single, _ größe As Single, _ X As Single, _ Y As Single, _ Z As Single) On Error Goto BailOut: 'Front cube(0) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z + größe, C110, 0, 0, 0) cube(1) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 1, 1) cube(2) = CreateLitVertex(X - größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 0, 1) cube(3) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 1, 1) cube(4) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z + größe, C110, 0, 0, 0) cube(5) = CreateLitVertex(X + größe, Sin(Y + move) - größe, _ Z + größe, C110, 0, 1, 0) 'Back cube(6) = CreateLitVertex(X - größe, Sin(Y + move) + größe, _ Z - größe, C110, 0, 0, 1) cube(7) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z - größe, C110, 0, 1, 0) cube(8) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 0, 0) cube(9) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z - größe, C110, 0, 0, 1) cube(10) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 1, 0) cube(11) = CreateLitVertex(X + größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 0, 0) 'Right cube(12) = CreateLitVertex(X - größe, Sin(Y + move) + größe, _ Z - größe, C110, 0, 0, 1) cube(13) = CreateLitVertex(X - größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 1, 0) cube(14) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 0, 0) cube(15) = CreateLitVertex(X - größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 0, 1) cube(16) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 1, 0) cube(17) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z + größe, C110, 0, 0, 0) 'Left cube(18) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z - größe, C110, 0, 0, 1) cube(19) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 1, 0) cube(20) = CreateLitVertex(X + größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 0, 0) cube(21) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 0, 1) cube(22) = CreateLitVertex(X + größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 1, 0) cube(23) = CreateLitVertex(X + größe, Sin(Y + move) - größe, _ Z + größe, C110, 0, 0, 0) 'Top cube(24) = CreateLitVertex(X - größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 0, 1) cube(25) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 1, 0) cube(26) = CreateLitVertex(X - größe, Sin(Y + move) + größe, _ Z - größe, C110, 0, 0, 0) cube(27) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z + größe, C110, 0, 0, 1) cube(28) = CreateLitVertex(X - größe, Sin(Y + move) + größe, _ Z - größe, C110, 0, 1, 0) cube(29) = CreateLitVertex(X + größe, Sin(Y + move) + größe, _ Z - größe, C110, 0, 0, 0) 'Top cube(30) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z + größe, C110, 0, 0, 1) cube(31) = CreateLitVertex(X + größe, Sin(Y + move) - größe, _ Z + größe, C110, 0, 1, 0) cube(32) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 0, 0) cube(33) = CreateLitVertex(X + größe, Sin(Y + move) - größe, _ Z + größe, C110, 0, 0, 1) cube(34) = CreateLitVertex(X - größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 1, 0) cube(35) = CreateLitVertex(X + größe, Sin(Y + move) - größe, _ Z - größe, C110, 0, 0, 0) Set VBuffer_Kiste(vert) = D3DDevice.CreateVertexBuffer( _ Len(cube(0)) * 36, _ 0, _ Lit_FVF, _ D3DPOOL_DEFAULT) D3DVertexBuffer8SetData VBuffer_Kiste(vert), _ 0, _ Len(cube(0)) * 36, _ 0, _ cube(0) BailOut: Exit Sub End Sub
Ich empfehle die Funktion so zu übernehmen. Und wieder einmal mit den Werten zu spielen. Wichtig oder etwas komplizierter ist die Y- Achse: Sin(Y + move) - größe. Hier bewerkstelligen wir die Auf- und Abbewegung in den Wellen. Doch beachten Sie, dass Kiste und Wasseroberfläche unabhängig voneinander sind. Außerdem sollte sie die Blendung der Kiste abschalten, den das wirkt alles andere als realistisch. Ich habe noch ein paar Kleinigkeiten mit der Rotationsmatrix verändert um die Kiste ein bisschen schwanken zu lassen. Hier der Inhalt der Renderfunktion:
If RotatePoint = 1 Then RotateAngle = RotateAngle + 0.1 If RotatePoint = -1 Then RotateAngle = RotateAngle - 0.1 If RotateAngle >= 10 Then RotatePoint = -1 If RotateAngle <= -10 Then RotatePoint = 1 D3DXMatrixIdentity matWorld D3DXMatrixIdentity matTemp D3DXMatrixRotationX matTemp, RotateAngle * (pi / 180) D3DXMatrixMultiply matWorld, matWorld, matTemp D3DXMatrixIdentity matTemp D3DXMatrixRotationZ matTemp, RotateAngle * (pi / 180) D3DXMatrixMultiply matWorld, matWorld, matTemp D3DDevice.SetTransform D3DTS_WORLD, matWorld D3DDevice.SetRenderState D3DRS_ALPHABLENDENABLE, 0 D3DDevice.SetTexture 0, Texture2 D3DDevice.SetStreamSource 0, VBuffer_Kiste(0), Len(cube(0)) D3DDevice.DrawPrimitive D3DPT_TRIANGLELIST, 0, 12 D3DXMatrixIdentity matWorld D3DDevice.SetTransform D3DTS_WORLD, matWorld D3DDevice.SetRenderState D3DRS_ALPHABLENDENABLE, 1 D3DDevice.SetTexture 0, Texture D3DDevice.SetStreamSource 0, VBuffer(0), Len(Waves(0)) D3DDevice.DrawPrimitive D3DPT_TRIANGLESTRIP, 0, 60
RotateAngle stellt die Schwenkbewegung dar.
Nachwort
In diesem Tutorial habe ich versucht eine möglichst realistische Wasseroberfläche zu erstellen und sie mit einer im Wasser schwimmenden Kiste zu vollenden. Ich würde mich freuen, wenn Leute sich melden würden um diese Szene zu erweitern. Auch hoffe ich, dass dies ein gutes Beispiel darstellt wie man die erworbenen Kenntnisse effektiv einsetzt und Spaß an der DirectX Programmierung in Visual Basic gewinnt. Für Anregungen oder Kritik bin ich natürlich auch offen.
Beispielprojekt zum Tutorial [56000 Bytes]
Ihre Meinung
Falls Sie Fragen zu diesem Tutorial haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.