VBO ohne glInterleavedArrays

Aus DGL Wiki
Wechseln zu: Navigation, Suche

(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.

Es gibt 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

type
  TVector4f = packed record 
    X, Y, Z, W: Single; 
  end; 
  
  TVBOVertex = packed record  
    Position,
    Color,
    Empty: TVector4f;
  end;
  
  TVBOBuffer   = array of TVBOVertex;
  TIndexBuffer = array of Word;

Konstanten

const
  POS_OFFSET  =  Pointer(0);                  // Position-Offset als Zeiger
  COL_OFFSET  =  Pointer(SizeOf(TVector4D));  // Color-Offset als Zeiger
  STRIDE      =  SizeOf(TVBOVertex);

Variablen

var
  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 Definitionsteil 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). Ganz wichtig: die Offsets müssen an die gl*Pointer-Funktionen als Zeiger übergeben werden, siehe oben Definition der Offsets.

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. Nachher reinen Tisch machen

  SetLength(IndexBuffer, 0);
  SetLength(VBOBuffer, 0);
  glDeleteBuffers(1, @IDVbo);
  glDeleteBuffers(1, @IDIndex);

Am Ende werden die Daten freigegeben, sowohl im RAM (mit SetLength(Buffer,0)) als auch im GRAM (mit glDeleteBuffers).


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. :)

Insgesamt fand ich am Ende, dass ich Euch vielleicht ein wenig Zeit und Frust ersparen kann, wenn ich das Ganze ins DGL-Wiki stelle.

Ich hoffe, es hilft.
Viele Grüße,
Traude


Fragen und Verbesserungsvorschläge wie immer ins Feedback-Forum bei DelphiGL.com.