Overlay schließen
Loading...

Campus 3D 

Projektgruppe Virtueller Campus 3D

Die offizielle Projektgruppenseite ist hier zu finden.

An dieser Stelle berichte ich regelmäßig über den Fortschritt der Engine, an der ich für das Projekt Virtueller Campus 3D an der Universität Osnabrück arbeite. Zurzeit dient das Programm eher dazu, sich mit der Funktionalität von OpenGL auseinanderzusetzen und herauszufinden, was möglich ist, wo Beschränkungen liegen und all solche Sachen.

Aktuelle Versionen:

Aktuelle xml-Dateien:

Feed

Terrain mittels Clipmaps

Nach langer Zeit endlich mal wieder ein Update. Und was für ein cooles ;)
Außerhalb der Uni soll man natürlich auch etwas anständiges sehen. Man könnte natürlich ein 360° Panorama Bild vom Dach aus machen und das als Skybox oder ähnliches benutzen. Auf diese Weise würde die Bildqualität aber immens leiden und man hätte nicht das Gefühl, wirklich “in der Uni” zu sein. (Ich behaupte einfach mal, dass dieses Gefühl grundsätzlich möglich ist, während man vor dem Bildschirm sitzt)

Displacement Mapping

Eine performante Art, das Terrain um den Gebäudekomplex darzustellen ist also unabdingbar. Dazu greife ich auf ein schon sehr häufig genutztes Mittel von Computerspielen zurück: Heightmaps + Displacement Mapping.
Die Höheninformationen liegen dabei als 2D Textur vor. Mithilfe dieser Informationen wird ein planares Drahtgitter an den korrespondierenden Stellen entsprechend angehoben. Diese Technik wurde beispielsweise in Morrowind oder Oblivion angewandt.
Die Vorteile dieser Technik sind:

  • Sehr performant (bei korrekter Implementierung)
  • Sehr leichte Anpassung des Geländes an die umliegenden Strukturen (Gebäude, Bäume, Felsen, …)

Natürlich gibt es auch Nachteile:

  • Da die Idee auf Displacement Mapping beruht, kann man keine Überhänge oder Höhlen modellieren (die gibt es in Osnabrück aber ohnehin eher seltener)
  • Für eine gute Performance muss man in Kauf nehmen, dass Terrain in der Entfernung bei hoher Detailfrequenz “zackig” aussieht

Brute Force

Terrain hat die – für Echtzeitgrafik bisweilen nervige – Eigenschaft, bis an den Horizont zu reichen. Es wird also potentiell viel Geometrie gezeichnet. Der Brute Force Ansatz wäre, ein gewaltiges Drahtgitter des Bodens zu erstellen, die Heightmap einlesen und jeden Vertex entsprechend anzuheben. Es dürfte offensichtlich sein, dass dabei sehr viel Grafikspeicher verschlungen wird:
Bei einer Gridlänge von 0.25m (also 4 Vertices pro Meter) und einer Gesamtgröße von 1024m in beide Richtungen sind das 212 x 212 Vertices also 224. Nimmt man nun an, man braucht pro Vertex 3 Floats für die Position und 1 Float für die Texturierung, so kommt man auf 226 Byte = 64 MB alleine für den Vertexbuffer. Ganz zu schweigen von dem Aufwand, den es macht, eine solch riesige Geometrie zu rendern (221 Polygone). Außerdem neigt dieser Ansatz zu extremen Aliasing Effekten in der Entfernung, wenn mehrere Vertices hinter einem Pixel liegen. Und als wäre das noch nicht genug, muss man den Vertexbuffer anpassen, sobald sich die Kamera dem Rand des Terrains nähert.

Implementierung mittels Clipmaps

An diesem Punkt muss man sich andere Algorithmen überlegen. Eine Möglichkeit sind die sogenannten Clipmaps1. Es sollte reichen, nur einen kleinen Teil des Terrains um den Spieler detailliert zu rendern. Detailliert bedeutet hier, dass die Dichte des Drahtgitters maximal ist, bspw. 0.25m zwischen zwei Vertices. Je weiter das Terrain vom Spieler entfernt ist, desto gröber kann es gerendert werden.
Seit der Version 3.0 des Vertex Shaders ist es auch möglich, Texture Lookups durchzuführen. Dadurch kann der Vertex Buffer mit einem ebenen Grid gefüllt werden, welches dann im Shader antsprechend manipuliert wird. Auf diese Weise schrumpft der Speicherverbrauch immens.

Konstruktion

Schema

Zunächst überlegt man sich, wie genau die Auflösung des Grids aussehen soll. Am einfachsten ist es, sich ein Basisgrid zu definieren mit n x n Vertices. Als nächstes verdoppelt man den Abstand zwischen den einzelnen Vertices, lässt die Anzahl aber unverändert. Das wiederholt man solange, bis das neue Grid die Sichtweite der Kamera ausreizt.

Das hat den Vorteil, das nur die Geometrie für den einen Level auf der Grafikkarte gespeichert werden muss. Die anderen Levels lassen sich mittels einem Skalierungsfaktor daraus erzeugen.

Jetzt muss entschieden werden, wie man das Grid aufteilt. Das ist deswegen nötig, da auf jedem Level, außer dem feinsten, in der Mitte ein Loch gelassen werden muss.

Unterteilung

Auf dem Bild erkennt man, dass man einen Level mit nur drei sehr kleinen Formen bereits beschreiben kann. In der Mitte auf der weißen Fläche liegt der nächstfeinere Level. Diese Aufteilung setzt voraus, dass man n x n Vertices benutzt, wobei n=2k-1 ist. (Im Bild ist n=15 gewählt)

Mit dieser Technik benötigt man für das Beispiel weiter oben mit n=127 nur noch 3750 Byte, also nicht einmal 4 KB (statt 64 MB). Natürlich muss der sichtbare Teil der Heightmap jetzt im Grafikspeicher liegen. Das sind 6 Texturen der Größe 128×128, das entspricht bei unkomprimierter Speicherung 6 × 214 Byte = 96 KB.
Insgesamt also ein Grafikspeicherverbrauch von 100 KB bei gut 217 Dreiecken im Gegensatz zu 64 MB bei 224 Dreiecken.

Nette Bilder

Bild

Bild

Bild

Bild

Bild

Quellen

1 GPU Gems 2: Chapter 2. Terrain Rendering Using GPU-Based Geometry Clipmaps

Geschrieben am 16.08.2011 von Nico Marniok
ÄndernLöschen

Normal Mapping / Shadow Mapping

Ein gewaltiger Quantensprung1 in der Grafikqualität ist mir in diesen Miniferien gelungen.

Normal Mapping

Das erste, was nicht übermäßig schwer war, ist Normal Mapping. Die Normalen stehen senkrecht auf einer Fläche und geben ihre Ausrichtung an. Diese Informationen werden hauptsächlich für die Beleuchtung verwendet.
Normal Mapping verändert die Normale eine Fläche nun mittels Texturen. Dabei gibt der Farbwert in der Textur die Abweichung von der Standardnormale an. Auf diese Weise wird die Fläche mit feinen Strukturen versehen.
Die Technik sieht vor allem auf bewegten Bildern extrem gut aus.

Shadow Mapping

Deutlich kniffeliger war das Shadow Mapping. Bei der Ausleuchtung einer Szene wird für einen Pixel seine Position und Normale herangezogen. Um einen glaubhaften Schattenwurf zu simulieren benötigt man jedoch Informationen über die Umgebung. Mittels Raytracing wäre das ganze kein Problem. Die Rastergrafik muss sich jedoch mit diversen Techniken herumschlagen. Ich habe die sogenannten Shadow Maps implementiert. Dabei wird die Szene aus der Sicht einer Lichtquelle gerendert und nur die Entfernungen der einzelnen Pixel abgespeichert. Die so erhaltene Textur ist die Shadow Map. Wenn nun eine Pixel für das gerenderte Bild beleuchtet werden soll, wird die Position des Pixels in der Welt zunächst in die Shadow Map projiziert. Ist nun die Entfernung zur Lichtquelle größer, als der in der Textur gespeicherte Wert, so befindet sich irgendwelche Geometrie zwischen dem Pixel und der Lichtquelle. Also muss er nicht beleuchtet werden.

Klingt zunächst noch einigermaßen simpel. Das Problem jedoch sind die Punktlichtquellen. Diese haben keine Richtung und somit fällt das Rendern aus ihrer Sicht sehr schwer. Für diesen Umstand gibt es sogenannte Cube Maps. Diese bestehen aus 6 Texturen, die Würfelförmig angeordnet sind. Nun muss also auf jede der Texturen mit jeweils einer eigenen Richtung von der Lichtquelle aus gerendert werden. Moderne Grafikkarten bieten mit dem Geometry Shader eine Möglichkeit alle 6 Texturen mit nur einem Rendervorgang zu befüllen. Und genau das habe ich genutzt. Nach langem Kopfzerbrechen und nachdem ich mehrere Male den Determinismus von OpenGL angezweifelt habe, klappt es nun endlich und es sieht höllisch gut aus :D

Und in Bewegung sieht das noch besser aus

1 Ein Quantensprung bezeichnet in der Physik die kleinstmögliche Zustandsänderung…

Geschrieben am 04.06.2011 von Nico Marniok
ÄndernLöschen

Deferred Shading

Heute habe ich den Prototyp des sogenannten FXGraph implementiert. Er basiert auf einer Technik, die als Deferred Shading in den meisten Computerspielen heutzutage verwendet wird.

Dabei wird das Rendern eines Bildes in mehrere kleine Schritte aufgeteilt und jeder davon in eine separate Textur gespeichert. Bspw. rendert man die Normalen und Weltkoordinaten in jeweils eine Textur. Dann kann man diese beiden Texturen in einem weiteren Renderpass kombinieren, um die Beleuchtung für jeden Pixel zu ermitteln.

Mit dieser Technik sind erstaunlich viele Effekte auf sehr einfache Weise zu realisieren. Man kann zum Beispiel aus dem Tiefenpuffer und dem gerenderten Bild noch einen Tiefenunschärfe Effekt hinzufügen. Oder mit einem helligkeitsgefilterten Bild eine gebloomte Version erstellen.

Momentan habe ich normale Phong-Beleuchtung mit Punktlichtquellen in einem extra Renderpass berechnet. Das funktioniert für beliebig viele Leuchtquellen, wobei ich sie aber auf maximal 32 aktive begrenzt habe.

Phong Beleuchtung

Geschrieben am 01.06.2011 von Nico Marniok
ÄndernLöschen

Räume

Heute habe ich mich um das Einlesen von Raumdaten gekümmert. Dafür benutze ich eine XML Datei in der für jeden Raum einer Etage seine Eckpunkte stehen:

<root>
    <room id="0" floor="0" ceil="1">
        <corner x="0" y="0" ></corner>
        <corner x="0" y="2" ></corner>
        <corner x="2" y="2" ></corner>
        <corner x="2" y="0" ></corner>
        <corner x="1.25" y="0" ></corner>
        <corner x="0.75" y="0" ></corner>
        <door id="0" corner="4" h="0.8" ></door>
        <floor x="0" y="0" width="1" height="1" ></floor>
    </room>
</root>

Dabei steht jeder Raum in einem <room> Tag. Mit dem Attribut id kann man später z.B. Gegestände an die einzelnen Räume binden. Das Attribut floor sagt, auf welcher Höhe sich der Boden befindet und ceil ist die Höhe der Decke.
Außerdem kann ein Raum mehrere Türen und Böden haben. Mit <door> definiert man eine Tür, die eine id hat. Da eine Tür in zwei Räumen vorhanden ist, kann man auf diese Weise sehen, welcher Raum mit welchem verbunden ist. Das Attribut corner referenziert, welcher Eckpunkt des Raumes eine Tür sein soll. Dieser bildet dann mit seinem Nachfolger die Tür. Schließlich gibt h die Höhe der Tür an.
Den Boden muss man sich im Moment noch mit Rechtecken zusammenbasteln. Dabei geben x und y die Koordinaten der linken unteren Ecke an und width und height sind natürlich für Breite und Höhe des Rechtecks verantwortlich.

Mit dieser XML Datei würde also in etwa so ein Grundriss herauskommen:

 ________________
|                |
|                |
|                |
|                |
|                |
|                |
|                |
|                |
|______    ______|

In der aktuellen Version der Engine kann man sich das ganze auch mal in 3D anschauen. In dem Ordner /data/rooms/ liegt die XML Datei, die ich zum Testen geschrieben habe: Ein L-förmiger und ein quadratischer Raum, mit einer Tür verbunden. Und darüber schwebt die Enterprise ;)

Geschrieben am 05.05.2011 von Nico Marniok
ÄndernLöschen

Die erste Version der Engine

In der letzten Woche habe ich eine Art instanzbasierte Engine geschrieben (keine Ahnung ob das ein Fachterminus ist :P

  • Eine Szene hat verschiedene Effekte. Ein Effekt besteht dabei aus einem Shaderprogramm, das wiederum aus einem Vertex- und einem Pixelshader besteht.
  • Jeder Effekt hat eine Reihe von Meshes. Diese repräsentieren ein Objekt der Szene, welches eine feste Position im Raum einnehmen kann. Etwa ein Stuhl, ein Tisch oder ein Modell der Enterprise C.
  • Jedes Mesh hat eine Geometrie. Diese besteht aus einem Indexbuffer und mehreren Vertexbuffern (evtl. werden diese später zu einem zusammengefasst, wenn das performancefreundlich ist). Außerdem gibt es mehrere Indexbufferintervalle, denen jeweils ein Material zugeordnet ist. Auf diese Weise kann man z.B. Stühle realisieren, die eine Holzsitzfläche und ein Eisengestell haben.
  • Jedes Mesh hat zusätzlich eine (evtl. leere) Liste von Instanzen. Darin steht die MC2WC Matrix, die das Objekt im Raum platziert.

Ein Rendervorgang läuft nun folgendermaßen ab:

Für jedes Mesh m der Szene tue
  Wenn m eine gültige Geometrie g besitzt
    Aktiviere Index- und Vertexbuffer von g
    Für jede Instanz i von m tue
      Für jedes Intervall int von g tue
        Aktiviere MC2WC-Matrix von i
        Aktiviere das Material von int
        Zeichne das Intervall int

Da es im Moment nur einen Effekt gibt, habe ich mir die for Schleife über alle Effekt gespart.

Gestern habe ich einen Importer für *.obj Dateien fertiggestellt. Das klappt ganz gut. Materialien werden richtig erzeugt, Normalen und Texturkoordinaten können eingelesen werden. Falls es Erstere nicht gibt, werden sie sogar automatisch erzeugt. Für das fertige Projekt werden wir aber auf jeden Fall ein eigenes Format schreiben müssen.

Geschrieben am 22.04.2011 von Nico Marniok
ÄndernLöschen
Zurück  Seite 1 / 2  Vor
Navigation
Account