Darstellung der Welt

Autor: Nils Affing

Das Hexagonmodell

Um die Welt mit Hexagonalen Prismen darzustellen, ist es nötig ein passendes Modell zu definieren. Da wir auch Texturen zeichnen oder Lichtberechnungen durchführen wollen, müssen wir in diesem Modell auch Texturkoordinaten und Flächennormalen berücksichtigen.

Hier sehen wir exemplarisch, wie solches Modell aussehen könnte. Die angedeuteten Texturen oder Normalen sind repräsentativ und gelten für alle Flächen.

Erwähnenswert ist es hierbei, dass die oberen und unteren Flächen jeweils noch einen Vertex in der Mitte der Fläche beinhalten. Des weiteren hat jede Fläche separate Vertices, welches dazu führt, dass das Modell aus 38 Vertices besteht.
Diese Darstellung ermöglicht es uns später eine besonders weiche und präzise Lichtberechnung durchzuführen.

Aufbau der Welt

Um die prozedural generierte Welt mit dem oben beschriebenen Hexagonmodell zu rendern, ist es nötig die Welt in Chunks zu unterteilen.
Chunks sind Unterteilungen der Welt, die separat angesprochen, bearbeitet und gerendert werden können.
Dies ermöglicht es uns, eine theoretisch unendlich große Welten bespielen zu können.

Ein Chunk mit der Größe von 3 sähe beispielsweise wie folgt aus:


Jedes der zu sehenden Hexagone bildet eine Hexagonsäule die bei der Z-Ordinate 0 anfängt und bei der maximalen Chunkhöhe aufhört.
Verteilt auf die Höhe, beziehnungsweise der Z-Ordinate, wird dann diese Hexagonsäule wiederum weiter unterteilt.

Diese Unterteilung der Hexagonsäulen beinhalten Hexagone von gleichen Grundmaterialien wie Gras, Stein, Erde oder ähnliches. Wenn eine Höhle dargestellt wird, werden die dazugehörigen Unterteilungen einfach ausgelassen.
Beispielsweise werden zwei Hexagone, die übereinander liegen und das gleiche Grundmaterial teilen, zusammengefasst.

Diese Darstellung ermöglicht es dem Spiel die Welt möglichst effizient darzustellen. Somit werden z.B Hexagone die aus Luft bestehen komplett ignoriert. Des weiteren ist es möglich das Hexagone mit gleichen Materialien zusammengefasst werden.

Möchte man nun diese Unterteilungen rendern kann man einfach das oben beschriebene Hexagon-Modell translieren und um die Höhe der Unterteilung skalieren.
Bei einer Höhe von 20 Einheiten zeichnet man also nur ein einziges Hexagon, anstatt von 20 Hexagonen.
Aufgrund der großen Menge an Hexagonen, die pro Frame gezeichnet werden müssen, ist dies schon eine erhebliche Verbesserung.

Nachteile

Ein Nachteil bei dieser Methode ist jedoch, dass man zusammengefasste Flächen nur mäßig mit speziellem Culling bearbeiten kann.
Speziell sollten Flächen, die unabhängig von der Kameraposition niemals sichtbar sind, nicht gezeichnet werden. Gerade bei sehr großen Unterteilungen der Hexagonsäulen wird dieses Verfahren sehr ungenau.

Betrachtet man die Anzahl der so resultierenden sichtbaren Flächen, so ist der Aufwand zur Berechnung dieser Flächen zu hoch um effizient zu sein.

Eine weitere Methode wäre es, dass man nicht ein Hexagon als Modell darstellt, sondern einen kompletten Chunk. Dies wurde jedoch aus Zeitgründen nicht implementiert und könnte jedoch in eventuell zukünftigen Versionen eingefügt werden.

Zeichnen der Welt mithilfe von OpenGL

Um die oben beschriebenen Abläufe nun mit OpenGL zu rendern, müssen wir folgende Datenstrukturen generieren:

  • Vertex-Buffer
  • Index-Buffer
  • Texturkoordinaten-Buffer
  • Normalen-Buffer

Ein Buffer ist in diesem Fall nichts anderes als eine Reihe von elementaren Datenwerten (z.B Gleitkommatypen wie Float-32). Diese Buffer werden jedoch direkt im internen VRAM (Video Random Access Memory) der Grafikkarte abgelegt.

Um den VRAM möglichst effizient zu nutzen, generieren wir diese Daten für jede Spielinstanz nur ein einziges Mal. Wenn nun die Welt gerendert wird, benutzen wir die gleichen Modell-Buffer nur mit verschiedenen Transformationen.

So ein Aufruf zum zeichnen sieht beispielsweise wie folgt aus:

surface.draw(
    &self.vertex_buf,
    &self.index_buf,
    self.renderer.program(),
    &uniforms,
    &params).unwrap();

Das&selfimpliziert eine Referenz auf die generierten Buffer.

Wie wir in diesem Aufruf sehen, verwenden wir lediglich einen Vertex- und Index-Buffer zum rendern. Dies hat den Grund, da wir Vertices, Normalen und Texturkoordinaten zu einem Buffer zusammengefasst haben.
Da diese Buffer nur aus Float-32 Werten bestehen, ist dies möglich. Wir müssen jedoch OpenGL explizit sagen, in welcher Art und Weise es die Werte zuordnen soll.

Dies erreichen wir durch:

pub struct Vertex {
    pub position: [f32; 3],
    pub normal: [f32; 3],
    pub tex_coords: [f32; 2],
}

und

implement_vertex!(Vertex, position, normal, tex_coords);

Diese beiden Codestücke bewirken lediglich, dass die gegebenen Instanzfelder den oben beschriebenen Buffern zugeordnet werden können. Diese Zuordnung übernimmt Glium für uns.

Zu erwähnen ist hier noch, dass nicht alle Chunks der Welt gerendert werden. Es werden nämlich nur für den Spieler sichtbare Chunks behandelt.
Diese Chunks werden Aufgrund einer Sichtweite geladen und in eine Liste vorgemerkt. Bei jedem Frame werden dann alle Chunks dieser Liste gerendert.

Optimierungen

Auch wenn OpenGL eine sehr mächtige Schnittstelle zur Grafikkarte darstellt, kommt es durchaus zu Problemen bei der oben beschriebenen Vorgehensweise.

Das Hauptproblem hierbei ist, dass OpenGL für jeden Aufruf dersurface.draw(...)Methode einmal die komplette Grafik-Pipeline ausführt. Bei mehreren zehntausend Objekten führt dies zu erheblichen Laufzeitproblemen.

An dieser Stelle setzt das Konzept der Instantiierung an.
Dies besagt, dass man die dazugehörigen Buffer von OpenGL (oder ähnliche Schnittstellen) erst mit den passenden Werten anreichert.
Diese bestehen aus den Daten des zu instantiierenden Modells und den jeweiligen Transformationen oder Eigenschaften der Instanz.
Bei der darauf folgenden Ausführung der Pipeline werden dann alle Werte aus den Buffern nacheinander bearbeitet.

Auf unseren Aufbau der Welt angewandt, würde es sich also anbieten, jede Unterteilung der Hexagonsäulen durch Instantiierung zu rendern.
Hierbei wäre dann die Unterteilung der Hexagonsäule die Instanz und das Hexagonmodell das zu zu instantiierenden Modell.

Somit brauchen wir für jeden Chunk nur einen Durchlauf der Pipeline. Dies ist bei einer Chunkgröße von nn eine Ersparnis von mindestens n21n^2 - 1 Durchläufen der Pipeline.

results matching ""

    No results matching ""