Tutorial Vertexbufferobject

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Ave!

Und willkommen zu meinem zweiten Tutorial. Diesmal beschäftigen wir uns mit den sogenannten Vertex Buffer Objects. Die Extension GL_ARB_vertex_buffer_object wurde vom ARB am 12.Februar 2003 fertiggestellt. Später im Jahr 2003 wurde sie in den Core von OpenGL 1.5 aufgenommen. In diesem Tutorial werden diese Core-Funktionen verwendet. Das Verhalten der Extension sollte aber identisch sein.

VBOs (Vertex Buffer Objects) sind quasi eine Erweiterung der Vertexarrays, die jedoch den großen Vorteil besitzen, dass ihre Vertexdaten serverseitig, also im schnellen VRAM der Grafikkarte, statt wie bei den VAs im Hauptspeicher, abgelegt werden. Das ist bei einer Displayliste zwar (fast) genauso der Fall, allerdings können die Daten nicht so einfach dynamisch geändert werden, was bei einem VBO aber bei Bedarf sehr einfach ist.

Kurz gesagt vereint das VBO also die Flexibilität eines Vertexarrays mit der Geschwindigkeit einer Displayliste und eignet sich daher besonders für das Rendern aufwändiger Geometrie, egal ob statisch oder dynamisch.

Eine weitere tolle Neuerung der VBO-API ist die Tatsache, dass man sich von OpenGL einen Pointer übergeben lassen kann, mit dessen Hilfe man Vertexdaten direkt in den VRAM schreiben kann. Dadurch spart man sich natürlich den Umweg über den Hauptspeicher und somit auch den erhöhten Speicherbedarf des Programmes beim Laden von Modellen o. Ä.

VBOs bieten übrigens auch die Möglichkeit, sie je nach Anwendungsfall vom Grafikkartentreiber "optimieren" zu lassen. So legt man Vertexdaten, die im Nachhinein nicht mehr verändert werden sollenm am besten mit dem Schlüsselwort STATIC_DRAW ab, während dynamische Vertexdaten über DYNAMIC_DRAW abgelegt werden sollten. Doch dazu gibts später nähere Informationen.

Hardwareunterstützung

Inzwischen sollten alle gängigen Grafikkarten diese Extension problemlos unterstützen, ältere Modelle tun dies aber evtl. nur emuliert über den Treiber. Will man sein Programm auch für sehr alte Hardware (Karten älter als ATIs Radeon 9x00 bzw. NVidias GeForce 4-Serie) lauffähig machen, sollte man noch einen alternativen Renderpfad auf Basis von Displaylisten einbauen.

Erstellen des Vertexbufferobjects

Wie der Name vermuten lässt, handelt es sich beim VBO um ein Objekt, welches in Sachen Syntax an die anderen in OpenGL verfügbaren Objekte wie z. B. Texturenobjekte angelehnt ist. Das Erstellen eines VBOs geht daher genauso einfach von der Hand:

glGenBuffers(1, @VBO);

Nun sollten wir in VBO eine gültige ID zurückgeliefert bekommen haben, welche ein Vertex buffer object referenziert.

Nachdem wir nun ein VBO erstellt haben, sollten wir dieses natürlich auch "aktivieren" (also binden), damit wir mit ihm arbeiten können:

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glEnableClientState(GL_VERTEX_ARRAY);

Wie erwähnt binden wir in der ersten Zeile unser frisch erstelltes VBO, während wir in der zweiten Zeile Vertexarrays auf der Clientseite aktivieren. Dies ist deshalb nötig, da wir das VBO später genauso zeichnen werden wie dies bei einem normalen VA der Fall wäre.

Übergabe der Vertexdaten (Variante 1: Ohne Umweg über den Hauptspeicher)

Wie in der Einleitung bereits erwähnt, bietet uns die VBO-API die Möglichkeit, unsere Vertexdaten ohne Umweg über den Hauptspeicher direkt in den Grafikkartenspeicher zu schreiben. Da dies eines der besten Features dieser neuen Extension ist, wollen wir uns auch zuerst mit dieser Übergabemethode beschäftigen.

Allerdings erstmal eine kleine Warnung vorweg: Pointer sind ein recht komplexes Thema, und ihre Nutzung erfordert einige Kenntnisse. Wer weder weiß, wie man damit umgeht, noch wie man sie einsetzt, sollte sich da zuerstmal schlau machen. Denn mit Pointer kann man ganz böse Sachen machen, wenn man sie nicht richtig nutzen kann!

Um unsere Vertexdaten als Pointer zu nutzen,müssen wir erstmal eine Vertexstruktur erstellen und auch einen passenden Pointertyp. Der Pointertyp ist zwar nicht zwingend (ein ^ täte es auch), aber macht die Sache übersichtlicher:

PVertex = ^TVertex;
TVertex = packed record
  S, T, X, Y, Z: TGLFloat;
end;

Sicher fragen sich einige von euch jetzt, warum unser TVertex gerade so aussieht (also zuerst S,T und dann der Rest). Das liegt daran, dass wir unser VBO später über die Funktion glInterleavedArrays rendern werden, welche wissen will, wie unsere Vertexdaten im Speicher abgelegt sind. Unsere Vertexstruktur entspricht dabei dem Format GL_T2F_V3F. T2F entspricht 2 Floats für die Texturenkoordinaten (S&T), gefolgt von V3F für die drei Floatwerte die unsere Vertexkoordinaten repräsentieren. Wenn ihr ein anderes Vertexformat verwendet, müsst ihr euch auch ein passendes Vertexformat aus der Spezifikation heraussuchen. Ich verwende oben übrigens einen packed Record (auch wenn dies bei unserem Vertexformat nicht unbedingt ein Muss ist). Dies tue ich deshalb, da Delphi Werte bei einem normalen Record für einen beschleunigten Zugriff an einem (Double)Word-Raster ausrichtet und dies deshalb bei bestimmten Vertexformaten Probleme bereiten könnte.

Hoffe mal das euch obiges klar ist,denn jetzt gehts ans Eingemachte. Wir kommen zum "schwersten" (ist ja ein relativer Begriff) Teil des Tutorials, nämlich der Übergabe der Vertexdaten an das VBO. Dazu müssen wir OpenGL zuerstmal mitteilen, wie groß unser VBO eigentlich sein soll,damit wir auch genug Platz im VRAM bekommen:

glBufferData(GL_ARRAY_BUFFER, VertexAnzahl * SizeOf(TVertex), nil, GL_STATIC_DRAW);

Schauen wir uns also mal die Parameter von glBufferData an. Zuerst müssen wir OpenGL mitteilen, was für einen Puffer wir erstellen wollen (in zukünftigen Versionen werden wohl noch andere folgen, z. B. Überbuffer). In unserem Falle also einen Arraypuffer. Parameter zwei übergibt dann den Speicherplatz, den wir reservieren wollen und ist recht selbsterklärend. Der dritte Parameter würde normalerweise einen Pointer auf unsere Vertexdaten enthalten. Da wir diese aber nicht im Hauptspeicher abgelegt haben, sondern über einen Pointer dort noch ablegen wollen, übergeben wir hier nil. OpenGL nimmt dies zur Kenntnis und spuckt dementsprechend auch keinen Fehler aus.

Den letzten Parameter hab ich auch schon kurz in der Einführung angesprochen. Er teilt OpenGL mit, auf welchen Anwendungsfall unser VBO optimiert werden soll. Dabei gibt es folgende Anwendungsfälle,die ihr euch dann entsprechend eurer VBO-Verwendung aussuchen könnt:

STREAM_DRAW Die Vertexdaten werden einmal übergeben und dann eher selten als Quelle für OpenGL-Befehle genutzt.
STREAM_READ Die Vertexdaten werden einmal übergeben und dann selten von der Anwendung angefordert.
STREAM_COPY Die Vertexdaten werden einmal durch Kopieren von der GL übergeben und dann eher selten als Quelle für OpenGL-Befehle genutzt.
STATIC_DRAW Die Vertexdaten werden einmal übergeben und dann recht oft von der Anwendung zum Rendern genutzt.
STATIC_READ Die Vertexdaten werden einmal übergeben und dann recht oft von der Anwendung angefordert.
STATIC_COPY Die Vertexdaten werden einmal durch Kopieren von der GL übergeben und dann recht oft von der Anwendung zum Rendern genutzt.
DYNAMIC_DRAW Die Vertexdaten werden wiederholt angegeben (verändert) und recht oft von der Anwendung zum Rendern genutzt.
DYNAMIC_READ Die Vertexdaten werden wiederholt durch das Auslesen von Daten durch OpenGL angegeben (verändert) und oft von der Anwendung angefordert.
DYNAMIC_COPY Die Vertexdaten werden wiederholt durch das Auslesen von Daten durch OpenGL angegeben (verändert) und recht oft zum Rendern genutzt.

Beachtet werden sollte, dass es sich bei obigen Anwendungsmuster nur um Hinweise (Hints) handelt, die es auch in anderen Bereichen von OpenGL gibt. Diese Hinweise sind allerdings nicht verbindlich und werden von Treiber zu Treiber unterschiedlich behandelt.

Nachdem OpenGL nun weiss was für ein VBO wir genau haben wollen, brauchen wir letztendlich nur noch einen Pointer, der "auf" das VBO zeigt:

VBOPointer := glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

Die Funktion glMapBuffer liefert uns einen Pointer zurück, der den Speicherplatz des VBOs in den Adressraum des Clients "ummappt", also in den Hauptspeicher. Dadurch wird es dann möglich, über eine Adresse im Hauptspeicher des Rechners direkt in den Grafikkartenspeicher zu schreiben. Der zweite Parameter gibt übrigens an, das wir in das VBO schreiben wollen. Der einzige weitere zulässige Parameter ist hier GL_READ_ONLY, welcher zum Auslesen des VBOs dient.

Endlich haben wir einen Pointer auf unser inzwischen auch im VRAM der Grafikkarte richtig dimensioniertes Vertexbufferobject, und sind nun in der Lage dort unsere Vertexdaten abzulegen. Was jetzt folgt ist ein wenig Pseudo-Code der diesen Vorgang darstellt. Pseudo-Code deshalb, weil die Vertexdaten die ins VBO kommen ja in jeder Anwendung unterschiedlich sind, und man so auch selbst ein wenig denken muss:

VertexDatenLaenge := 0;
for i := 0 to High(MeineVertexDaten) do
begin
  VertexPointer    := VBOPointer;

  VertexPointer^.X := GeneriertesVertex.X;
  VertexPointer^.Y := GeneriertesVertex.Y;
  VertexPointer^.Z := GeneriertesVertex.Z;
  VertexPointer^.S := GeneriertesVertex.S;
  VertexPointer^.T := GeneriertesVertex.T;

  inc(Integer(VBOPointer), SizeOf(TVertex));

  inc(VertexDatenLaenge);
end;

Da sich dieses Tutorial (und v. a. diese Methode der Vertexdatenübergabe) an die fortgeschritteneren wendet, rede ich nicht lange um den heissen Brei herum. Wir durchlaufen also eine Schleife, in der wir unsere Vertexdaten dynamisch erstellen. Entweder in jedem Durchlauf, oder wir laden die Daten vor der Schleife z. B. aus einem 3D-Modell und entfernen sie danach aus dem Hauptspeicher. Erste Methode spart sowohl beim Programmstart, also auch während des Programmlaufes Arbeitsspeicher, während letztere dies nur während des Programmablaufs macht.

Wir setzen die Adresse des Pointers der auf das momentan zu verändernde Vertex zeigt also auf die aktuelle Zeigerposition im VBO und schreiben dann unser Vertex direkt in den Grafikkartenspeicher. Ist dies getan, müssen wir die Adresse unseres VBOPointers natürlich um die Größe des gerade geschriebenen Vertexes erhöhen. Der Typecast auf ein Integer geschieht deshalb, weil man Pointer in Delphi nicht so einfach erhöhen oder verringern kann, Integers hingegen schon.

Ich gehe mal davon aus,das meine Leserschaft mit obigem Codeschnippsel keinerlei Probleme hatte und bringe diese Methode der Übergabe von Vertexdaten an das VBO dann auch mit folgender Quellcodezeile zu Ende:

glUnMapBuffer(GL_ARRAY_BUFFER);

Wie unschwer zu erraten geben wir hier unser VBO für den weiteren Zugriff wieder frei. Dies ist nötig um dies später rendern zu können.

Übergabe der Vertexdaten (Variante 2: Über den Hauptspeicher)

Wem obige Variante nicht zugesagt hat, weil sie ihm zu komplex war, oder weil er seine Vertexdaten aus diversen Gründen im Hauptspeicher braucht, dem wird in diesem Abschnitt geholfen.

Das Übergeben der Vertexdaten an das VBO über den Hauptspeicher gestaltet sich nämlich sehr einfach:

glBufferData(GL_ARRAY_BUFFER, SizeOf(VertexDaten), @VertexDaten, GL_STATIC_DRAW);

Sieht einfach aus,hört sich einfach an und ist auch einfach! Statt wie im letzten Kapitel für die Vertexdaten einen Zeiger auf nil zu übergeben, übergeben wir OpenGL jetzt einen direkten Zeiger auf die Vertexdaten die im Hauptspeicher abgelegt sind. Die Daten werden dann zum VBO hochgeladen, und können anschliessen auch wieder aus dem Hauptspeicher entfernt werden.

Rendern des VBOs

Kommen wir nun also schon zum Ende unseres VBO-Tutorials. Dank der Einfachheit dieser Extension gabs halt einfach nicht mehr zu erklären. Das Zeichnen gestaltet sich nämlich OpenGL-typisch mehr als einfach:

glInterleavedArrays(GL_T2F_V3F, SizeOf(TVertex), nil);
glDrawArrays(GL_QUADS, 0, VertexDatenLaenge);

Schnell gezeichnet und schnell erklärt. Der erste Parameter gibt an, in welchem Format die Vertexdaten unseres VBOs abgelegt wurden (wir erinnern uns: GL_T2F_V3F), während der zweite Parameter den Speicherplatz, der zwischen zwei Vertices liegt, angibt. Würden wir ein einfaches Vertexarray zeichnen, so wäre der letzte Parameter ein Pointer auf die Vertexdaten im Hauptspeicher. Da wir allerdings vorher unser VBO gebunden haben, teilt hier ein nil mit, dass die vorher ins VBO geschriebenen Vertexdaten gerendert werden sollen.

Mittels glDrawArrays sagen wir OpenGL dann noch, das es unsere VBO-Daten als Quads rendern soll.

Freigabe

Wie mit allen Objekten in der OpenGL ists auch keine schlechte Idee, unser VBO freizugeben,wenn es nicht mehr benötigt wird. Und obwohl beim Löschen des Renderkontextes solche Objekte vom Grafikkartentreiber freigegeben werden sollten, wäre es keine schlechte Idee des selbst zu erledigen,für den Fall das der Treiber da schlampig arbeitet:

glDeleteBuffers(1, @VBO);

Weitere Vertexformate

Wem allein Texturkoordinaten und Vertices nicht reichen, sondern auch Farbangaben oder Vertexnormalen braucht, muss sich eines der folgenden Formate aussuchen. Die Konstantennamen sind logisch aufgebaut: GL_AngabeAnzahlTyp_AngabeAnzahlTyp... Als Angabe gibt es Vertex, Texturkoordinaten, Normalen und Color-Werte, also Farben. Anzahl ist die Anzahl der Komponenten pro Angabe (V3 wären zum Bespiel die X, Y und Z-Koordinaten eines dreidimensionalen Vertex). Typ ist entweder Float oder Unsigned Byte. Die Vertexdaten müssen in der richtigen Reihenfolge mit den richtigen Typen an das VBO übergeben werden.

  GL_V2F
  GL_V3F
  GL_C4UB_V2F
  GL_C4UB_V3F
  GL_C3F_V3F           
  GL_N3F_V3F           
  GL_C4F_N3F_V3F       
  GL_T2F_V3F
  GL_T4F_V4F
  GL_T2F_C4UB_V3F
  GL_T2F_C3F_V3F
  GL_T2F_N3F_V3F      // Wohl eines der sinnvollsten Vertexformate. 2 Floats für Texturkoordinaten, 3 Floats für den Normalenvektor und 3 Floats für den eigentlichen Vertex.
  GL_T2F_C4F_N3F_V3F
  GL_T4F_C4F_N3F_V4F

Es ist sinnvoll, sich für jedes der Vertexformate, die man verwendet, einen Typen anzulegen, um später einfacher mit den VBOs arbeiten zu können.

Falls man mit Shadern arbeitet kann man sich die Wahl des Vertexformates eigentlich sparen, denn hier kann man z.B. für jeden Attributtyp den man braucht (Vertex, Normale, Farbe, Normale, etc.) ein eigenes VBO anlegen und dieses dann entsprechend seinen Bedürfnissen im Shader interpretieren, oder einfach nur ein riesiges VBO nutzen und dort alle Daten nach Belieben ablegen und im Shader selbst interpretieren.

Nachwort

Das wars auch schon. Wie zu erkennen ist das Vertexbufferobjekt mal wieder eine gut durchdachte, recht nützliche und zudem auch weit nutzbare Erweiterung, die OpenGL technisch wieder einen Schritt weitergebracht hat.

Wer mehr wissen will, der sollte sich unbedingt mal die passende Spezifikation ansehen.

Hoffe das Tutorial hat euch soviel Spaß gemacht wie mir das Verfassen des selbigen und ich hoffe auf reges Feedback!

Euer

Sascha Willems (webmaster_at_delphigl.de)

Links

Nvidia: Using Vertex Buffer Objects

Spezifikation


Vorhergehendes Tutorial:
Tutorial_Cubemap
Nächstes Tutorial:
Tutorial_Vertexprogramme

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.