VBO ohne glInterleavedArrays
(Voraussetzung: mindestens OpenGL 1.5)
"VBO" ist die Abkürzung von "Vertex Buffer Object" und ist eine Möglichkeit der Darstellung von Objekten mit OpenGL. In den OpenGL-Versionen ab Nummer 3 ist es die einzige Möglichkeit.
Es gibt aber mehrere Möglichkeiten, einen VBO zu verwenden. Die Variante, die ich unten erkläre, ist also nicht die einzige.
VBO mit frei definierter Datenstruktur, ohne glInterleavedArrays
Der untenstehende Pascal-Code zeichnet das Opengl-Dreieck mit einem VBO. Ich hatte beim Erzeugen dieses Code-Stücks eine steile Lernkurve und jede Menge Frust, denn beim Ändern einzelner Teile sind dann natürlich auch korrespondierende Teile anderswo zu ändern, aber da kann man leicht etwas übersehen, speziell wenn es gegen vier Uhr morgens geht. :)
1. Zunächst die globalen Deklarationen
Typen
TVector4D = Packed Record X,Y,Z,W: Single; End;
TVBOVertex = Packed Record
Position,
Color,
Empty: TVector4D;
End;
TVBOBuffer = Array Of TVBOVertex;
TIndexBuffer = Array Of Word;
Konstanten
POS_OFFSET = 0;
COL_OFFSET = SizeOf(TVector4D);
STRIDE = SizeOf(TVBOVertex);
Variablen
VBOBuffer: TVBOBuffer; // VBO-Daten
IndexBuffer: TIndexBuffer; // VBO-Indices
VertexBufSize, // VBO-Daten: Puffergröße
IndexBufSize, // VBO-Indices: Puffergröße
IndexCount, // Anzahl der Indices
IDVbo,IDIndex: Cardinal; // Opengl-"Handles" für VBO- und Indexpuffer
Bei dieser Variante des VBO kann man sein eigenes Vertex definieren und ist nicht auf vorgegebene Datenstrukturen beschränkt wie bei den glInterleavedArrays. Der "Empty"-Vektor im TVBOVertex soll darauf hinweisen, dass man vorgegebene Vertexattribute mit benutzerdefinierten mischen kann. Das wird hier aber nicht behandelt.
Ihr könnt die Typen definieren, wie Ihr wollt, aber es ergeben sich daraus Konsequenzen. Die Datendeklaration hat unmittelbaren Einfluss auf den untenstehenden Initialisierungs- und Rendercode. Wenn also der Datentyp geändert wird, muss man den ganzen untenstehenden Code auf notwendige Anpassungen untersuchen. Wie sich gezeigt hat, habe ich dabei immer wieder das eine oder andere vergessen. :(
2. Dann das Hochladen der Daten aus dem Hauptspeicher in den Grafikkartenspeicher
IndexCount:= 3;
VertexBufSize:= IndexCount*SizeOf(TVBOVertex);
IndexBufSize:= IndexCount*SizeOf(Word);
Vertexpuffer erzeugen,initialisieren und mit Daten bestücken
SetLength(VBOBuffer,IndexCount);
FillChar(VBOBuffer[0],Length(VBOBuffer)*SizeOf(TVBOVertex),#0);
With VBOBuffer[0] Do
Begin
With Position Do Begin X:=-1.0; Y:=-1.0; Z:=+0.0; W:=+1.0; End;
With Color Do Begin X:=+1.0; Y:=+0.0; Z:=+0.0; W:=+1.0; End;
End;
With VBOBuffer[1] Do
Begin
With Position Do Begin X:=+1.0; Y:=-1.0; Z:=+0.0; W:=+1.0; End;
With Color Do Begin X:=+0.0; Y:=+1.0; Z:=+0.0; W:=+1.0; End;
End;
With VBOBuffer[2] Do
Begin
With Position Do Begin X:=+0.0; Y:=+1.0; Z:=+0.0; W:=+1.0; End;
With Color Do Begin X:=+0.0; Y:=+0.0; Z:=+1.0; W:=+1.0; End;
End;
Vertexdaten hochladen
glGenBuffers(1,@IDVbo);
glBindBuffer(GL_ARRAY_BUFFER, IDVbo);
glBufferData(GL_ARRAY_BUFFER,VertexBufSize,@VBOBuffer[0],GL_STATIC_DRAW);
Dasselbe mit dem Indexpuffer
SetLength(IndexBuffer,IndexCount);
IndexBuffer[0]:= 0;
IndexBuffer[1]:= 1;
IndexBuffer[2]:= 2;
Indexdaten hochladen
glGenBuffers(1,@IDIndex);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IDIndex);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,IndexBufSize,@IndexBuffer[0],GL_STATIC_DRAW);
Ich habe hier für den VBO einen Record genommen, weil bei den Demos im Netz normalerweise immer Arrays benutzt werden. Der Recordtyp ist zusätzlich viel selbsterklärender als ein Array und man kann ihn auch noch durch treffende Namensgebung verbessern.
Abgesehen von der Entscheidung, welchen Datentyp man nehmen soll, ist das der einfachere Teil des Ganzen. Und damit es nicht ganz trivial wird, habe ich noch einen winzigen Indexbuffer definiert, damit ich zum Zeichnen glDrawElements verwenden kann.
In den Beispielen, die man im Web finden kann, gibt es außerdem noch die Variante, wo glBufferdata nur zur Definition der Puffer-Parameter benutzt wird, aber für das Hochladen der Daten auf die Grafikkarte wird glBufferSubdata verwendet. Damit kann man entweder den ganzen Puffer oder auch nur Teile davon neu beschicken.
3. Der Rendercode
Vertexpuffer binden
glBindBuffer(GL_ARRAY_BUFFER, IDVbo);
Nötige Zeiger setzen
glVertexPointer(4, GL_FLOAT, STRIDE, POS_OFFSET);
glColorPointer(4, GL_FLOAT, STRIDE, COL_OFFSET);
Indexpuffer binden
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IDIndex);
Zeichnen
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glDrawElements(GL_TRIANGLES,IndexCount,GL_UNSIGNED_SHORT,NIL);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
Über das Binden der Puffer gibt es eigentlich nicht viel zu sagen. Die OpenGL-Pufferobjekte sind alle ziemlich ähnlich.
Aber bei den Zeigern kann man eine Menge falsch machen. Etliche Leute im Netz haben z.B. nachgeforscht, wie man den Parameter "Stride" zu interpretieren hat. Ich habe Stride ganz oben im Defnitionsteil als Konstante definiert: STRIDE = SizeOf(TVBOVertex); damit ist klar, dass hier die gesamte Blockgröße des Vertex gemeint ist. Die "Offset"-Parameter habe ich als Konstanten definiert weil sie von der Struktur des Vertex abhängen und daher vermutlich nicht dauernd geändert werden. "Offset" ist hier genau das richtige Wort. Zwischen dem Beginn des Vertex und der Color liegt eine Position mit dem Typ "TVector4D", daher ist der Offset der Color SizeOf(TVector4D).
Ich habe hier zwar kein nächstes Vertex-Element, aber wenn ich eines hätte, dann wäre dessen Offset COL_OFFSET+SizeOf(TVector4D) oder auch in Pseudoschreibweise: Offset(Color)+Size(Color).
Nach dem Binden des Indexpuffers muss man den VBO Client-seitig aktivieren, und zwar für jeden Attribut-Typ extra. Wenn Ihr also eine Texturkoordinate ergänzen wollt - dazu muss man natürlich die Vertex-Definition ändern -, dann vergeßt bloß nicht das "glEnableClientState(GL_TEXTURE_COORD_ARRAY)" nachzutragen.
So, jetzt kann man endlich zeichnen. Die Typangabe im glDrawElements beschreibt den Index und der letzte Parameter muss "NIL" sein, denn in unserem Fall ist dieser Parameter überflüssig (er wird in einem anderen Zusammenhang zum Nachladen der Daten benutzt).
4. Nacher reinen Tisch machen
Finalize(IndexBuffer);
Finalize(VBOBuffer);
glDeleteBuffers(1,@IDVbo);
glDeleteBuffers(1,@IDIndex);
Und, naja, wenn man etwas auf eine solche Art zeichnet, sollte es natürlich nicht grade ein Dreieck sein. Das wäre nämlich so, als ob man um die Ecke in den Supermarkt gehen wollte und dazu einen Jumbojet benutzt. :) Allerdings lässt uns OpenGL keine Wahl, denn ab OpenGL 3 gibt es keine anderen Möglichkeiten mehr. Also müssen wir uns mit dem Jumbojet anfreunden.
Insgesamt fand ich am Ende, dass ich Euch vielleicht ein wenig Zeit und Frust ersparen kann, wenn ich das Ganze ins WIKI stelle.
Ich hoffe, es hilft. Viele Grüße, Traude