Vertex Buffer Objects (VBO)

Da sich die Frage aus dem letzten Artikel geklärt hat (ja, OpenGL ist SDL wahrlich aus vielerlei Gründen vorzuziehen) möchte ich nun mein neu erwonnenes Wissen teilen.

In diesem Artikel möchte ich mich mit Vertex Buffer Objects, kurz VBO, beschäftigen.
Viele werden den folgenden Sachverhalt kennen oder zumindest in der Theorie nachvollziehen können.
Angenommen wir haben eine TileMap. Den sichtbaren Ausschnitt der Karte (View) werden wir mit sehr hoher Wahrscheinlichkeit nicht in jedem Frame ändern, sondern er wird mehrere Frames überstehen. Je nach Kartengröße eventuell sogar die gesamte Spielzeit, da gar kein Scrolling nötig ist.
Versteht sich von selbst: ist die gesamte Karte kleiner oder gleich groß wie das Fenster -> kein Scrollen notwendig, da alles bereits zu sehen ist.
Allerdings zeichnet man üblicherweise in jedem Frame wieder und wieder die gesamte Karte. Und das braucht bei größeren Karten eine Menge Rechenlast.
Man kann zwar das ganze etwas abmildern, in dem man von vorn herein nicht sichtbare Teile der Karte nicht zeichnet, aber die sichtbaren Tiles/Teile der Karte müssen dennoch immer und immer wieder über den Bus in den Grafikspeicher.

Hier ein Beispiel einer scrollbaren Karte (für Informatiker: bool scroll = sizeof(map) > sizeof(window) ? true : false;)
Anfangs Ansicht:

Show »

und die spätere gescrollte Ansicht, die zweite Hälfte wenn man so will

Show »

Beides Views, welche eine statische Ansicht repräsentieren, solang’ der Protagonist nicht so weit läuft, als das gescrolled werden muss.
Alle Render Aufrufe in dieser statischen Zeit sind absolute Verschwendung (im Normalfall).

Bei meinem D Framework Dgame hab ich es so gelöst:
Das aktuell gezeichnete Rechteck (der aktuelle View/Blickpunkt) wird zu Vergleichszwecken gespeichert, die Karte/die Tiles auf eine zweite Surface gezeichnet (die ebenso groß ist wie der View; was leider recht Speicherlastig werden kann) und dann auf den Bildschirm bzw. ins Fenster geblittet.
Wenn im nächsten Frame nun wieder der gleiche View gezeichnet werden soll, weil sich nichts verändert hat und der Ausschnitt der sichtbarne Karte somit gleich bleibt, dann wird die bereits gezeichnete Karte auf dne Bildschirm geblittet und wir sparen und das mühselige und Rechenlastige sowie nochmalige Zusammensetzen der Karte. Dies geschieht solange, bis sich eben etwas ändert. Die Heap Allokation und aufwendige Berechnungen der zusammenpuzzelung der Karte aus einem Tileset bleibt uns somit in nicht notwendigen Momenten (was etwa 90% der Spielzeit ausmachen kann) erspart. Solange wird also nicht gerade ein Side-Scroll und Jump ‘n Run Spiel erstellen, ist dies schonmal ein Anfang, Speicher, Heap Allokation und Rechenlast zu sparen.

Mit OpenGL gibt es zwei andere, wesentlich effizientere Methoden: Display Listen sowie Vertex Buffer Objects, kurz und im weiteren VBO. Ersteres ist in neueren Versionen bereits deprecated und sollte daher nicht mehr genutzt werden. Aus diesem und aus dem Grund, das VBO’s noch etwas effizienter und Performanter sind, werde ich nur auf diese eingehen.
VBO’s sind sozusagen eigene Speicherbereiche. Man reserviert sich faktisch Speicherplatz für Koordinaten von Texturen und ihren Platz auf dem Fenster. Oder nur Koordinaten um dann Primitive zu zeichnen.
Dies spart immens Rechenzeit, da man sich die herkömmliche Übertragung der Daten in dne Speicher spart, also sie gleich im Speicher verwahrt und ohne langwierige und aufwendige Übertragung per Bus jedes mal erneut dahin transportiert.
Ich habe mir mehrere Tutorials zu VBO’s durchgelesen, in meinem Haupt-Forum gepostet, mit diversen Leuten aus dem ICQ geschrieben.
Die wohl populärste Methode, Textur und Vertex Koordinaten zu speichern und somit Bildausschnitte und ihre Position im Speicher zu behalten stellte sich als etwas komplizierter heraus und da Tutorials und Hilfe oftmals versagten musste ich mir selbst über 3 endlose Tage alles zusammensuchen und -lesen, bis es endlich klappte. Damit es andere leichter haben, schreibe ich diesen Artikel.
Zunächst einmal die Funktionen die wir benötigen, ich lasse dabei mal außer Acht, das Texturen geladen und gezeichnet werden. Dazu soll dieses Tutorial nicht dienen.
Als da wären:

Was wir nun machen müssen ist zunächst einmal einen Buffer anfordern/generieren mit

GLuint vboId;
glGenBuffers(1, &vboId);

Nachdem wir nun einen Buffer bzw. eine ID für einen Buffer angefordert haben, müssen wir unsere Daten speichern.
Wir “binden” zunächst einmal nur Vertex Koordinaten:

Show »

struct Vertex {
    float x, y, z;
 
    this(float x, float y, float z = 0.0) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}
 
 
Vertex[] buffer;
// top left
buffer ~= Vertex(100, 100);
// top right
buffer ~= Vertex(200, 100);
// bottom right
buffer ~= Vertex(200, 200);
// bottom left
buffer ~= Vertex(100, 200);
 
GLuint vboId;
// Buffer ID anfordern
glGenBuffers(1, &vboId);
 
// ...
 
if (!set) {
    writeln("set buffer data!");
    set = true;
 
    // Buffer binden/aktivieren
    glBindBuffer(GL_ARRAY_BUFFER, vboId);
    // Vertex-Koordinaten einlesen
    glBufferData(
        GL_ARRAY_BUFFER, 
        buffer.length * Vertex.sizeof, 
        buffer.ptr, 
        GL_STATIC_DRAW
    );
} else {
    // Buffer binden/aktivieren
    glBindBuffer(GL_ARRAY_BUFFER, vboId);
    glVertexPointer(2, GL_FLOAT, 0, null);
 
    // Schwarze Farbe und Texturen deaktivieren
    glDisable(GL_TEXTURE_2D);
    glColor3f(0, 0, 0);
    // Zeichnen
    glDrawArrays(GL_QUADS, 0, buffer.length);
    // Texturen wieder aktivieren
    glEnable(GL_TEXTURE_2D);
}

glDrawArrays zeichnet uns ein schwarzes Rechteck an der Position (100|100) und mit der Größe 100×100 px,
wie hier zu sehen

Show »

black rect

Das glDisable(GL_TEXTURE_2D); ist deswegen notwendig, da ansonsten die Farbänderung von glColor3f sich nicht auf Primitive sondern lediglich auf die Farbkomponenten von Texturen ausüben würden. Wir würden das Rechteck also ansonsten nicht sehen.
glBindBuffer aktiviert bzw. bindet den Buffer, wie der Code Kommentar schon aussagt, und kennzeichnet ihn somit als aktuell. glBufferData überträgt dann die Daten in der angegebenen Größe in den Grafikspeicher.
Was glVertexPointer macht überlass ich mal dem Leser ;)

Das wäre schon einmal ein Anfang. Wir haben nun ein schwarzes Rechteck, die Koordinaten dafür im Speicher und müssen sie nicht mehr über den Bus schicken. Wenn wir Position und/oder Größe ändern wollen, dann überschreiben wir mit dme gleichen Verfahren einfach die alten Buffer Daten.

Oder man guckt sich glBufferSubData. Was ich leider noch nicht groß getan habe, also eventuell folgt noch ein Update zu diesem Artikel.

» Primitive mittels eines Vertex Buffers im Speicher zu halten, ist auch eine Idee, die ich mir für Dgame 1.7 durch den Kopf gehen lassen werden.

Aber nun zum eigentlich genialen: Texturen oder Ausschnitte von Texturen gemeinsam mit ihrer Position im Speicher zu verwahren um (zumeist) statische bzw. sich langsam / wenig ändernde Objekte nicht immer und immer wieder zeichnen zu müssen. Wer sich hier noch fragt “Warum sollte man das vermeiden?” den lege ich nahe, noch einmal von vorne zu lesen, denn dann kam der Sinn der hinter VBO’s steckt wohl noch nicht ganz an. :)
Für dieses Vorhaben legen wir uns (zumindest in der einfachsten Variante) einfach einen zweiten VBO an, der dann statt der Vertex Koordinaten eben die Texturkoordinaten behinhaltet.
Das ganze Verfahren sieht äquivalent zu oberen Code Ausschnitt aus, jedoch nutzen wir statt des oben kurz angesprochenen glVertexPointer‘s glTexCoordPointer.

Wir wollen nun einen Teil dieses Tilesheets zeichnen. Und zwar an der Position (100|100) und der Größe von 100×100 px. Also exakt die Ausmaße unseres schwarzes Rechtecks.
Dazu machen wir folgendes:

Show »

struct Vertex {
    float x, y, z;
 
    this(float x, float y, float z = 0.0) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}
 
Vertex[] buffer;
// top left
buffer ~= Vertex(100, 100);
// top right
buffer ~= Vertex(200, 100);
// bottom right
buffer ~= Vertex(200, 200);
// bottom left
buffer ~= Vertex(100, 200);
 
Vertex[] texbuffer;
// top left
texbuffer ~= Vertex(0, 0);
// top right
texbuffer ~= Vertex(0.2, 0);
// bottom right
texbuffer ~= Vertex(0.2, 0.2);
// bottom left
texbuffer ~= Vertex(0, 0.2);
 
GLuint texId;
// Textur anlegen und laden
// ...
 
GLuint vBuf, tBuf;
glGenBuffers(1, &vBuf);
glGenBuffers(1, &tBuf);
 
//..
 
if (!set) {
    writeln("set buffer data!");
    set = true;
 
    glBindBuffer(GL_ARRAY_BUFFER, vBuf);
 
    glBufferData(GL_ARRAY_BUFFER, buffer.length * Vertex.sizeof, buffer.ptr, GL_STATIC_DRAW);
 
    glVertexPointer(3, GL_FLOAT, 0, null);
 
    //glBindBuffer(GL_ARRAY_BUFFER, 0);
 
    glBindBuffer(GL_ARRAY_BUFFER, tBuf);
 
    glBufferData(GL_ARRAY_BUFFER, texbuffer.length * Vertex.sizeof, texbuffer.ptr, GL_STATIC_DRAW);
 
    glTexCoordPointer(3, GL_FLOAT, 0, null);
 
    glBindBuffer(GL_ARRAY_BUFFER, 0); // unbind
} else {
    // die entsprechende Textur Binden
    glBindTexture(GL_TEXTURE_2D, texId);
 
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
 
    glDrawArrays(GL_QUADS, 0, buffer.length);  //  Ausgabe
 
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);;
}

Und schon haben wir einen Ausschnitt unserer Textur im Speicher und können ihn jederzeit ohne weitere Speicher Aufwendung rendern lassen.
Das Prinzip lässt sich sehr einfach auf TileMaps übertragen, wie man ja weiter oben anhand der Bilder sieht. Das zeichnen einer solchen Map (auch wenn man alle nicht sichtbaren Tiles nicht zeichnete) erreichte ohne VBO’s eine Framerate von 200 oder 300. Mit VBO’s bis zu 1160. Man sieht also den enormen Vorteil.
In Dgame 1.6 bekam ich beim dauerhaften Rendering eine maximale FPS von unter 60. Mit der oben genannten Behilfsmethode des selbstgemachten “Doppelbuffers” kam ich dann auf knapp 300.
Hier sieht man also auch sehr gut den Unterschied zwischen SDL 1.2 und OpenGL.

Ich hoffe der Beitrag war lehrreich und nicht zulang, bei Fragen, Kritik o.a. immer her damit.

*

Copyright © All Rights Reserved · Green Hope Theme by Sivan & schiy · Proudly powered by WordPress