Tutorial Vertexbufferobject
Inhaltsverzeichnis
GL_ARB_Vertex_Buffer_Object
Ave!
Und willkommen zu meinem zweiten GL_ARB-Extension Tutorial. Diesmal beschäftigen wir uns mit der recht neuen GL_ARB_Vertex_Buffer_Object-Extension, die vom ARB am 12.Februar 2003 fertiggestellt und vor kurzem auch als Corefeature in OpenGL 1.5 promoted wurde.
VBOs (Vertex Buffer Object) sind quasi eine Erweiterung der recht weit verbreiteten 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_ARB ab, während dynamische Vertexdaten über DYNAMIC_DRAW_ARB abgelegt werden sollten. Doch dazu gibts später nähere Informationen.
Hardwareunterstützung
Da GL_ARB_Vertex_Buffer_Object eine noch recht junge Extension ist, siehts mit der Hardwareunterstützung besonders auf älteren 3D-Beschleunigern nicht so toll aus. Allerdings haben sowohl ATI als auch NVidia in ihre aktuellsten Treiber für ältere Beschleuniger Softwareemulationen dieser Extension eingebaut, hinter der wohl im Endeffekt ein simples Vertexarray seinen Dienst verrichtet. Von daher wirds auf älteren Karten keine Geschwindigkeitszuwächse ggü. Vertexarrays geben, aber man erspart sich das umständliche Schreiben mehrerer Renderpfade für verschiedene Grafikkartengenerationen. (Infos zur Unterstützung von GL_ARB_VBO gibts hier).
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:
glGenBuffersARB(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:
glBindBufferARB(GL_ARRAY_BUFFER_ARB, 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 einne 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:
glBufferDataARB(GL_ARRAY_BUFFER_ARB, VertexAnzahl*SizeOf(TVertex), nil, GL_STATIC_DRAW_ARB);
Schauen wir uns also mal die Parameter von glBufferDataARB 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_ARB | Die Vertexdaten werden einmal übergeben und dann eher selten als Quelle für OpenGL-Befehle genutzt. |
---|---|
STREAM_READ_ARB | Die Vertexdaten werden einmal übergeben und dann selten von der Anwendung angefordert. |
STREAM_COPY_ARB | Die Vertexdaten werden einmal durch Kopieren von der GL übergeben und dann eher selten als Quelle für OpenGL-Befehle genutzt. |
STATIC_DRAW_ARB | Die Vertexdaten werden einmal übergeben und dann recht oft von der Anwendung zum Rendern genutzt. |
STATIC_READ_ARB | Die Vertexdaten werden einmal übergeben und dann recht oft von der Anwendung angefordert. |
STATIC_COPY_ARB | Die Vertexdaten werden einmal durch Kopieren von der GL übergeben und dann recht oft von der Anwendung zum Rendern genutzt. |
DYNAMIC_DRAW_ARB | Die Vertexdaten werden wiederholt angegeben (verändert) und recht oft von der Anwendung zum Rendern genutzt. |
DYNAMIC_READ_ARB | Die Vertexdaten werden wiederholt durch das Auslesen von Daten durch OpenGL angegeben (verändert) und oft von der Anwendung angefordert. |
DYNAMIC_COPY_ARB | 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 := glMapBufferARB(GL_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB);
Die Funktion glMapBufferARB 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_ARB, 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:
glUnMapBufferARB(GL_ARRAY_BUFFER_ARB);
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:
glBufferDataARB(GL_ARRAY_BUFFER_ARB, SizeOf(VertexDaten), @VertexDaten, GL_STATIC_DRAW_ARB);
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, VBOSize);
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:
glDeleteBuffersARB(1, @VBO);
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
|
||
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. |