SelfDXD von Martin Pyka
SelfDXD --- DirectXGraphic --- Vertex- und Indexbuffer
Der Indexbuffer
Was ist der Indexbuffer?
IDirect3DDevice8.CreateIndexBuffer()
Indexbuffer mit Daten füllen
Indizierte Vertices darstellen

Was ist der Indexbuffer?

Wenn Sie schonmal versucht haben ein 3D-Modell mit nur einem Vertexbuffer zu erstellen, werden Sie sicherlich schnell festgestellt haben, dass das gar nicht so einfach ist. Intuitiv versucht man ja zunächst, alle Dreiecke über eines der D3DPRIMITIVTYPE-Flags zu erstellen. Dabei erscheint D3DPT_TRIANGLESTRIP am günstigsten, weil man mit einer minimalen Anzahl an Vertices eine maximale Anzahl an Dreiecken rendern kann. Doch schon bei den einfachsten Aufgaben wird es hier schwierig. Ein Beispiel dafür sehen Sie hier:

Angenommen, Sie möchten eine Hügellandschaft erstellen. Dann würden Sie wahrscheinlich mit einem Raster dieser Art arbeiten. Die Höhe liesse sich dann bequem für jeden Vertex einstellen. Aus jedem Viereck würden wir dann zwei Dreiecke machen. Doch in welcher Reihenfolge schreibt man die Vertices in den Vertexbuffer, um eine Fläche erscheinen zu lassen? Als Trianglstrip werden wir das nicht hinbekommen. Es gibt keine Möglichkeit, jeden Vertex nur einmal in den Vertexbuffer zu schreiben und damit die ganze Fläche zu rendern. Bleibt also nur die Möglichkeit, die Vertices als TriangleList zu rendern, doch das hat den entscheidenden Nachteil, dass wir mehrmals die gleichen Vertices dem Vertexbuffer übergeben müssen. Betrachten wir exemplarisch mal ein Viereck aus dem Raster.

Wenn wir dieses Viereck als TriangleList rendern wollen, dann müssen wir eine Vertexreihenfolge wie 1/4/2/1/3/4 im Buffer verwenden. Die Vertices 1 und 4 werden dabei also zweimal übergeben, obwohl sich ihre Koordinaten ja nicht verändern. Das heisst also, die Vertices 1 und 4 werden zweimal von Grund auf neu gerendert, obwohl ein einmaliges Rendern völlig ausreichen würde. Desweiteren wird die Liste der Vertices im Vertexbuffer länger. Bei 4 Vertices hätten wir 6 Elemente im Buffer. Bei unserem Raster wird der Unterschied schon extremer. Obwohl wir nur 25 Vertices haben, müssten wir 96 Vertices dem Vertexbuffer übergeben (16 Vierecke * 6 Vertices). Das ist ein Zuwachs von über 380%. Sie können sich vorstellen, dass man bei grösseren Konstruktionen schnell an die Grenzen der Rechenleistung kommt, um noch akzeptable FPS zu erhalten.

Bei solchen Problemen kann hier ein Indexbuffer Abhilfe schaffen. Dabei übergeben wir dem Vertexbuffer lediglich alle 25 Vertices, die wir für die Fläche brauchen und dem Indexbuffer die Reihenfolge, in der diese gerendert werden sollen. Dadurch enthält der Vertexbuffer nur die notwendige Anzahl an zu berechnenden Vertices. Eine doppelte Berechnung ist hier ausgeschlossen. Der Indexbuffer speichert also nur Integer-Werte. Von der Konstruktion her ähnelt er aber trotzdem sehr dem Vertexbuffer, wie Sie in den folgenden Abschnitten feststellen werden.

Auch dieses Kapitel beinhaltet wieder eine Demo (Achtung: nur in der Offline-Version enthalten). Diesmal wird eine Rasterfläche, wie Sie oben vorgestellt wurde, gezeigt. Zwei Parabeln geben der Fläche dabei eine Biegung.


IDirect3DDevice8.CreateIndexBuffer()

Der Indexbuffer funktioniert so ähnlich, wie der Vertexbuffer. Auch hier wird wieder ein Speicherbereich für die Indexzahlen reserviert. Diese Zahlen werdem dem Buffer als Array des Typs WORD übergeben. Bevor wir den Indexbuffer erstellen, müssen wir zunächst das Array mit den Indizes anlegen. Und bevor wir dieses Array anlegen können, müssen wir natürlich zu erst die Fläche haben, die wir später anzeigen wollen. Deshalb hier zunächst der Code für das Raster:


function InitGeometry: HRESULT;
var
  vertices: Array[0..99] of CUSTOMVERTEX;
  pvertices: PByte;
  indizes: Array[0..((81*6) - 1)] of WORD;
  pindizes: PByte;
  z: Word;
  x,y: byte;
begin
  result:= E_FAIL;

// Erstellen des 10x10 grossen Feldes
  for y:= 0 to 9 do
    for x:= 0 to 9 do                       // die y-Koordinate gibt die Höhe an und 
                                            // ist eine Mischung aus zwei Parabel-Kurven
      vertices[y*10 + x]:= GetCustomVertex( x-5, -(x-5)*(x-5)*0.1 + (y-4)*(y-4)*0.07, y-5, D3DColor_RGBA(x*10, 255 - y*10, (x+y)*10, 0));

  d3ddev8.CreateVertexBuffer( 100 * sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX, D3DPool_Default, vbuffer);

  vbuffer.Lock(0, sizeof(vertices), pvertices, 0);
    move( vertices, pvertices^, sizeof(vertices) );
  vbuffer.unlock;

// wird fortgesetzt....

Wir erstellen also ein 10x10 grosses Raster. Damit der Nullpunkt im Zentrum des Rasters liegt, ist die x-Koordinate immer x-5 und die z-Koordinaten y-5. Die y-Koordinate beinhaltet einen Term, der aus zwei Parabeln besteht, nämlich
-(x-5)2*0.1 und (y-4)2*0.07.
Dadurch erhalten wir diese Biegungen in der Fläche. Wenn Sie statt dessen einfach nur 0 übergeben, werden Sie eine glatte Ebene später sehen.

Als nächstes wird das Array mit den Indizes angelegt. Dabei spielt es natürlich eine Rolle, in welcher Reihenfolge wir die oberen Vertices erstellt haben. Nach dem oberen Algorithmus haben wir die Vertices in folgender Reihenfolge erstellt:

Demzufolge lautet die Vertexreihenfolge für das erste Viereck 0/11/1/0/10/11. Damit die Eingabe der Vertexindizes ein wenig einfacher wird, kann man dafür folgenden Algorithmus benutzen:


// Fortsetzung.....

  x:= 0;
  y:= 0;
  z:= 0;

  for y:= 0 to 8 do
    for x:= 0 to 8 do
    begin
      indizes[z]:= y*10 + x;
      inc(z);
      indizes[z]:= y*10 + x + 11;
      inc(z);
      indizes[z]:= y*10 + x + 1;
      inc(z);
      indizes[z]:= y*10 + x;
      inc(z);
      indizes[z]:= (y+1)*10 + x;
      inc(z);
      indizes[z]:= y*10 + x + 11;
      inc(z);
    end;

// wird fortgesetzt....

Für jedes der 9x9 Vierecke in dem Raster werden die 3 Ecken der 2 Dreiecke angegeben. Daher muss dass Array auch 81*6 Felder gross sein. Nun können wir uns endlich um die eigentliche Initialisierung kümmern, die mit folgender Funktion ausgeführt wird:


function CreateIndexBuffer(const Length : Cardinal;
                                 Usage : LongWord;
                           const Format : TD3DFormat;
                                 Pool : TD3DPool;
                           out   ppIndexBuffer : IDirect3DIndexBuffer8) : HResult;

Length: Grösse des Indexbuffers in Bytes. Hierfür können Sie am besten Sizeof( Indexarray ) verwenden.

Usage: 0 oder eine Kombination der D3DUSAGEFLAGS-Flags, mit denen Sie angeben, für welche Zwecke Sie den Buffer verwenden. Dadurch kann DirectXGraphics den Buffer im Speicher optimal positionieren, womit die Performance ein wenig steigen kann.

Format: Gibt an, wieviele Bits für jeden Index verwendet werden soll. Sie haben die Wahl zwischen D3DFMT_INDEX16 und D3DFMT_INDEX32 aus den D3DFORMAT-Flags. Bei 16-Bit können Sie 65536 (216) und bei 32-Bit 4294967296 (232) Indizes verwenden.

Pool: Eine Konstante aus der D3DPOOL-Aufzählung, die angibt, in welcher Speicherklasse der Indexbuffer angelegt werden soll.

ppIndexBuffer: eine Variable, die auf den Indexbuffer zeigt.

In unserem Beispiel würde die Funktion so aussehen:


d3ddev8.CreateIndexBuffer( sizeof(indizes),
                           0,
                           D3DFMT_INDEX16,
                           D3DPOOL_DEFAULT,
                           ibuffer);

Indexbuffer mit Daten füllen

Wie beim Vertexbuffer funktioniert das Übergeben der Daten auch wieder in Verbindung mit dem Locken des Speichers.


function Lock(const OffsetToLock : LongWord;
                    SizeToLock : LongWord;
                var ppbData : PByte;
              const Flags : LongWord) : HResult;

OffsetToLock: Die Stelle in Bytes, an der der Indexbuffer beschrieben bzw. ausgelesen wird.

SizeToLock: Die Grösse des zu sperrenden Bereiches in Byte.

ppbData: Ein Zeiger auf den Speicherbereich, in dem die Indexdaten stehen.

Flags: 0 oder eine Kombination der D3DLOCKFLAGS mit denen man angibt, für welche Zwecke man den Indexbuffer lockt.

Die Übergabe der Daten dürfte Ihnen noch aus dem Kapitel Der Vertexbuffer bekannt sein, deshalb hier der restliche Beispielcode:


  ibuffer.Lock( 0, sizeof(indizes), pindizes, 0);
    move( indizes, pindizes^, sizeof(indizes));
  ibuffer.UnLock;

Indizierte Vertices darstellen

Das Rendern von Vertices, die über Indizes aufgerufen werden ist im Prinzip genauso einfach, wie der Rendervorgang, den Sie bisher kennen. Lediglich zwei Dinge sind anders. Denn neben dem Vertexbuffer, den wir dem Device bekannt machen müssen, müssen wir natürlich auch den Indexbuffer dem Device übergeben. Das geschieht mit folgender Funktion.


function SetIndices(const pIndexData : IDirect3DIndexBuffer8;
                    const BaseVertexIndex : Cardinal) : HResult;

pIndexData: Der Zeiger auf den Indexbuffer.

BaseVertexIndex: Dieser Wert wird mit jedem Indize addiert, womit also Vertices, die ganz am Anfang im Vertexbuffer liegen, ausgeschlossen werden können.

Bisher haben Sie zum Rendern die .DrawPrimitiv-Funktion verwendet. Wenn Sie indizierte Vertices darstellen wollen, müssen Sie eine andere Funktion benutzen. Diese lautet wie folgt:


function DrawIndexedPrimitive(const _Type : TD3DPrimitiveType;
                              const minIndex : Cardinal;
                                    NumVertices : Cardinal;
                                    startIndex : Cardinal;
                                    primCount : Cardinal) : HResult;

_Type: Gibt an, welche Art von Primitiven Sie darstellen wollen. Verwenden Sie hierzu eine der D3DPRIMITIVETYPE-Konstanten.

minIndex: Kleinster Vertexindex, der für diesen Rendervorgang benutzt werden soll. Im Prinzip das gleiche, wie BaseVertexIndex.

NumVertices: Anzahl der Vertices, die gerendert werden sollen, beginnend mit BaseVertexIndex + minIndex.

startIndex: Gibt die Stelle im Indexbuffer an, ab der die Indizes gelesen werden sollen.

PrimCount: Anzahl der Primitiven, die gerendert werden sollen. Also bei D3DPT_TRIANGLELIST Vertexanzahl / 3, bei D3DPT_LINELIST Vertexanzahl / 2, etc.

In unserem Beispielcode sieht das dann folgendermassen aus:


  d3ddev8.setstreamsource( 0, vbuffer, sizeof(CUSTOMVERTEX));
  d3ddev8.SetVertexShader(D3DFVF_CUSTOMVERTEX);
  d3ddev8.SetIndices(ibuffer, 0);
  d3ddev8.DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 100, 0, 486 div 3);

Es werden also alle Vertices und alle Indizes in den Rendervorgang mit einbezogen. Da wir D3DPT_TRIANGLELIST verwenden, müssen wir die Anzahl der Indizes durch 3 teilen.

Damit haben Sie ein weiteres wichtiges Element in der Erzeugung und Darstellung dreidimensionaler Objekte kennengelernt. Das Beispielprogramm finden Sie unter demos/indexbuffer/. Es enthält alle hier gezeigten Codes. Auch hier kann ich Ihnen wieder nur raten, sich die Beispiele genauer anzuschauen und durch verändern der Zahlen oder Formeln herauszufinden, welche Möglichkeiten sich bieten. Übungshalber würde es sich hier auch anbieten, Matrizen auf die Objekte anzuwenden.