Terrain 101: Vertex- und Index-Buffer

Da wir mittlerweile Profis im Transformieren von Vertices sind, können wir uns nun an ein neues Ufer begeben und uns in diesem Teil meiner Artikelreihe „Terrain 101“ den Buffern widmen. Ziel ist es natürlich nach wie vor, am Ende der Artikelreihe eine dreidimensionale Landschaft entwickeln zu können. Dabei möchte ich mich nicht nur auf eine winzig kleine Landschaft beschränken, wie dies in vielen anderen Tutorials gemacht wird, sondern ich möchte riesige, detaillierte Landschaften mit euch gemeinsam erschaffen, die soweit ausgebaut werden, daß man sie auch in einem Spiel verwenden kann.

Ich werde jeden Artikel – um die Navigation zu erleichtern – mit einer Art Inhaltsverzeichnis beginnen. Dieses wird alle Artikel der Reihe enthalten und so einfach ermöglichen, daß man in diesen Artikel navigiert und bestimmte Themen einfach überspringen kann. Das Inhaltsverzeichnis in den älteren Artikeln werde ich dann entsprechend aktualisieren.

Das Inhaltsverzeichnis

Terrain 101: Eine Einführung in dreidimensionale Landschaften in Echtzeit
Terrain 101: Die Entwicklungsumgebung und das XNA 4.0 Projekt
Terrain 101: Das erste Dreieck
Terrain 101: Transformationen
Terrain 101: Vertex- und Index-Buffer
Terrain 101: Land in Sicht
Terrain 101: Technische Rafinessen
Terrain 101: Neue Sichtweisen
Terrain 101: Kamera ab und Action

Hardware-Buffer

Diese Überschrift hatte ich in einem anderen Artikel, exakt 6 Monaten bevor ich diese Zeile schreibe, bereits einmal verwendet. Im Mysterium RenderTarget wurde damals schon darüber geschrieben, was ein Hardwarebuffer ist. Im RenderTarget-Artikel ging es aber hauptsächlich um Pixel-Buffer und in diesem Artikel wird sich dies ändern. Hier werde ich jetzt über Index- und Vertex-Buffer schreiben.

Ein Hardware-Buffer ist ein einfaches Konstrukt. Es ist schlicht und einfach ein Speicherbereich, in dem bestimmte Daten gespeichert werden können. Dies können die unterschiedlichsten Daten sein, aber in der Regel sind dies Indexdaten, Vertexdaten und Pixeldaten. Man spricht dann von einem Index-Buffer, einem Vertex-Buffer, sowie von Texturen und/oder Render-Targets. Diese Buffer können – wenn man natives DirectX verwendet – entweder im Arbeitsspeicher des Rechners liegen, oder im dedizierten Speicher der Grafikkarte. In XNA gehen wir immer davon aus, daß diese Daten im Speicher der Grafikkarte liegen. Wir gehen nur davon aus, da wir nicht die volle Kontrolle darüber haben. Der Grafikkarten-Treiber, DirectX und auch XNA können diese Entscheidung beeinflussen. Hat die Grafikkarte z.B. keinen eigenen Speicher oder einfach nur zuwenig, dann landet der jeweilige Buffer nicht zwangsweise dort, wo wir es erwarten, es wird aber auch nicht zwangsweise ein Fehler geworfen.

Warum nun dieser „Klimmzug“? Dies ist eine berechtigte Frage, die eine zum Glück nicht allzu komplizierte Antwort erfordert. Die Grafikkarte kann auf ihren eigenen Speicher besser zugreifen. Besser bedeutet hierbei in der Regel eine höhere Bandbreite. Warum dies technisch gesehen so ist, daß geht hier ein wenig zuweit und würde den Rahmen mehrfach sprengen. Der Hauptunterschied ist einfach, daß die Daten nicht über den PCI-Express Bus transportiert werden müssen. Das kann man sich gut merken. Der Transfer über den PCI-X Bus ist immer langsamer als der (lokale) Zugriff der GPU auf den eigenen Speicher.

Das ist aber nicht die einzige Besonderheit, die es mit diesen Buffern gibt. Wie man schon an den unterschiedlichen Arten erkennen kann, scheinen diese Buffer recht stark typisiert zu sein. Dies liegt daran, daß eine GPU recht „dumm“ ist. Einen großen Teil der Geschwindigkeit bezieht sie daraus, daß sie nicht flexibel ist und nicht mit allen Arten von Daten so umgehen kann, wie dies eine CPU könnte. Viele Dinge müssen einfach auf die GPU zugeschnitten werden um ihr die Arbeit zu erleichtern. Dies ist auch bei den Hardwarebuffern so. Es gibt ganz spezielle Regeln, auf die ich in den nächsten beiden Abschnitten eingehen möchte.

Vertex-Buffer

Der Vertex-Buffer enthält – wie der Name unschwer erkennen lässt – Vertex-Daten. Dies sind Informationen über die einzelnen Stützpunkte unserer 3D-Objekte. Bisher hatten wir ein sogenanntes Vertex-Array. Wir haben also schlicht und einfach ein Array angelegt und darin unsere Vertex-Daten gespeichert. Dies sah im letzten Teil noch ungefähr so aus:

            vertices = new VertexPositionColor[] { new VertexPositionColor(new Vector3( 0.0f,  0.5f, 0.0f), Color.Red),
                                                   new VertexPositionColor(new Vector3( 0.5f, -0.5f, 0.0f), Color.Green),
                                                   new VertexPositionColor(new Vector3(-0.5f, -0.5f, 0.0f), Color.Blue),
                                                 };

Dies wollen wir nun „verbessern“ und etwas flexibler gestalten. Dazu löschen wir erst mal unsere Member-Variable vertices, die am Anfang der Klasse dem Speichern der Vertices diente. Diese Variable benötigen wir nicht mehr, da wir die Daten ja nicht mehr in der Klasse, sondern in einem Vertex-Buffer speichern wollen.

Danach erzeugen wir eine neue Methode, die unseren Vertex-Buffer „aufsetzt“.

private void SetupVertexBuffer()
{
            VertexPositionColor[] vertices = new VertexPositionColor[] { new VertexPositionColor(new Vector3( 0.0f,  0.5f, 0.0f), Color.Red),
                                                                         new VertexPositionColor(new Vector3( 0.5f, -0.5f, 0.0f), Color.Green),
                                                                         new VertexPositionColor(new Vector3(-0.5f, -0.5f, 0.0f), Color.Blue),
                                                                       };
}

Danach erzeugen wir eine Member-Variable die die Referenz auf unseren Vertex-Buffer enthält am Anfang der Klasse. Keine Angst, ich werde am Ende dieses Artikels – so wie in jedem Teil – nochmal den gesamten Quellcode dieses Teils auflisten. Wer also die Übersicht verliert, kann einfach runterscrollen und dort nachschauen.

VertexBuffer vertexBuffer;

In unserer SetupVertexBuffer-Methode erzeugen wir natürlich nun auch einen VertexBuffer. Dies erfolgt ganz einfach über den Konstruktor.

this.vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), 3, BufferUsage.WriteOnly);

Als ersten Parameter müssen wir eine Referenz auf das GraphicsDevice übergeben. Dies ist notwendig, da der Vertex-Buffer ja in den Grafikkartenspeicher hochgeladen werden soll. Ohne GraphicsDevice kann es daher keinen VertexBuffer geben. Der nächste Parameter gibt den Typ der Daten an, den wir in diesem Vertex-Buffer haben möchten. In diesem Fall ist das unsere bekannte VertexPositionColor-Vertex-Deklaration. Hier kann man auch andere Datentypen verwenden. Dazu aber in den Folgeartikeln noch mehr. Wir werden später noch unsere eigene Vertex-Deklaration und damit unseren eigenen Vertex-Typ erzeugen. Der dritte Parameter gibt schlicht und einfach an, wieviele Vertices im Vertex-Buffer landen sollen. Die Größe in Bytes berechnet XNA dabei für uns, so daß wir uns mit solchen Details nicht rumschlagen müssen, so wie es in nativem DirectX notwendig wäre.

Der vierte Parameter ist eine kleine Besonderheit. Dieser gibt an, wie wir den Vertex-Buffer verwenden wollen. In diesem Fall haben wir BufferUsage.WriteOnly gesetzt. Dies bedeutet, daß wir lediglich Daten in den Buffer reinschreiben wollen. Dies ist gleichzeitig ein Zugriffsmodifizierer, was bedeuten soll, daß uns XNA daran hindert, daß wir Daten aus dem VertexBuffer auslesen. Es ist aber in erster Linie ein Hinweis zur Optimierung. Die Grafikkarte kann bei einem Buffer, in den nur geschrieben wird viel besser optimieren. Es müssen z.B. keine Lese-Caches aufgebaut werden, evtl. kann ein schneller beschreibbarer Speicherbereich verwendet werden, etc.

Abschliessend müssen wir natürlich den Vertex-Buffer in der SetupVertexBuffer Methode noch befüllen. Dies erfolgt mit der SetData Methode.

this.vertexBuffer.SetData<VertexPositionColor>(vertices);

Die Daten werden nun in den Grafikspeicher der Grafikkarte geladen.

Selbstverständlich müssen wir nun noch den Draw-Aufruf zum Rendern unseres Dreiecks ändern. Erstens weil jetzt einen Compiler-Fehler bekommen (die Variable vertices wurde ja gelöscht) und zweitens weil wir der Grafikkarte natürlich mitteilen müssen, daß sie aus einem Vertex-Buffer rendern soll. Dazu sind in der Draw-Methode zwei Aufrufe notwendig. Als erstes müssen wir unseren Vertex-Buffer als aktiven Vertex-Buffer setzen und als zweiten Aufruf rendern wir diesen. In der Draw-Methode ersetzen wir also den bisherigen DrawUserPrimitives Aufruf durch folgenden Code:

GraphicsDevice.SetVertexBuffer(this.vertexBuffer);
GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);

Die Parameter sind sehr ähnlich dem bisherigen Aufruf, weshalb ich diese hier nicht nochmal erklären möchte. Zur Not bitte einfach im letzten Artikel nachschauen.

Wenn wir unser Programm nun starten, dann bekommen wir exakt das gleiche Bild, wie wir es schon im letzten Teil hatten.

Es gibt nun aber einen entscheidenden, wenn auch unsichtbaren Effekt. Bisher war es so, daß der Inhalt unseres Vertex-Arrays bei jedem einzelnen Draw-Aufruf vom Arbeitsspeicher zur Grafikkarte geschickt werden musste, damit diese gerendert werden konnten. Dies belastet den Bus zur Grafikkarte, also meist den PCI-X Bus bzw. früher den AGP-Bus. Das kostet Zeit und ist damit langsamer als die Vertex-Buffer-Variante. Es gibt hier jedoch eine Ausnahme. Auf Plattformen wie der XBox macht es praktisch keinen Unterschied. Die XBox hat keinen dedizierten Grafikspeicher, sondern CPU und GPU sind gleichzeitig an den gleichen Speicher angeschlossen. Solange beide nicht auf den gleichen Speicherbereich agieren, kommen sich diese nicht ins Gehege. Tatsächlich ist es auf der XBox so, daß der Vertex-Buffer einfach nur eine Kopie der Daten des Vertex-Arrays ist. Trotzdem sollte man auch auf dieser Plattform testen, was tatsächlich schneller ist. Zwar ist der Unterschied auf der XBox deutlich kleiner, aber in gewissen Fällen trotzdem noch vorhanden.

Ein weiterer, interessanter Aspekt ist der Aufruf von SetVertexBuffer. Der ein oder andere fragt sich vielleicht, wozu dieser Befehl notwendig ist, könnte man ihn doch explizit in DrawPrimitives verpacken. Der Grund ist sehr simpel: In einem Vertex-Buffer können beliebige Vertex-Daten vorhanden sein, auch von mehreren Meshes gleichzeitig. Es kann nun notwendig sein, daß diese mit mehreren DrawPrimitives Aufrufen gerendert werden müssen. Warum dies so sein kann, sollte uns an dieser Stelle erstmal nicht interessieren, es ist halt einfach so. In den Folgeartikeln werde ich darauf noch mal genauer eingehen (müssen). In diesem Fall spart man sich das Setzen eines neuen Vertex-Buffers. Dies gilt nämlich, genau wie das Setzen einer Textur oder eines Effekts als RenderStateChange und ist solange gültig, bis der Grafikkarte etwas anderes mitgeteilt wird. Daher macht es Sinn seine Objekte unter anderem nach VertexBuffer zu sortieren, damit dieser so selten wie möglich gewechselt werden muss.

Das sollte als Einführung in Vertex-Buffer erstmal reichen. Sicherlich sind damit noch nicht alle Aspekte vollkommen beleuchtet worden und auch noch nicht alle Besonderheiten beschrieben, aber der Leser sollte nun wissen, wie Vertex-Buffer grundsätzlich funktionieren und wozu diese benötigt werden. Die späteren Artikel werden dieses Wissen voraussetzen und darauf weiter aufbauen, so daß am Ende dieser Artikelreihe nahezu alle Aspekte von Vertex-Buffern angesprochen wurden.

Kommen wir nun zu den Index-Buffern.

Index-Buffer

Die Index-Buffer sind etwas einfacher zu erklären, als Vertex-Buffer, aber in der Handhabung sehr ähnlich. Erstmal gibt es zwei Arten von Index-Buffern, solche mit 16 Bit Indices und welche mit 32 Bit Indices. Die Unterschiede liegen dabei schlicht und einfach darin, daß mit einem 16 Bit Index-Buffer 65.536 Elemente verwendet werden können (ushort) und mit einem 32 Bit Buffer 4.294.967.295 Elemente (uint).

Warum verwendet man dann nicht einfach immer 32 Bit-Buffer? Dies liegt zum einen daran, daß nicht jede Grafikkarte 32 Bit-Buffer unterstützt und zum anderen daran, daß ein 32 Bit-Buffer natürlich doppelt soviel Speicher benötigt wie ein 16 Bit-Buffer.

Übrigens: Das Reach-Profil von XNA ist auf 16 Bit-Index-Buffer beschränkt. Wer mehr benötigt, der muss auf das HiDef-Profil umschalten, was aber nur mit neueren Grafikkarten und nicht auf dem Windows Phone funktioniert.

Die alles entscheidende Frage ist aber eigentlich folgende: Welchen Sinn hat nun dieser Index-Buffer? Stellen wir uns mal einen Würfel vor. Diesen können wir durch 8 Vertices definieren.

Soweit so gut, aber in den vergangenen Artikel hatte ich ja bereits erklärt, daß die Grafikkarte nur mit Dreiecken arbeitet. Dies bedeutet also, daß wir 12 Dreiecke erzeugen müssen, um diesen Würfel darzustellen. Und da ein Dreieck durch jeweils drei Vertices definiert wird, benötigen wir ganze 36 Vertices zur Darstellung dieses Würfels. Hierzu auch eine kleine Illustration.

Dies verschwendet natürlich eine Menge Speicherplatz, je nachdem wie groß die jeweilige Vertex-Deklaration für einen einzelnen Vertex ist. Um dieses Problem nun etwas abzuschwächen und Vertices wiederzuverwenden, ist der Index-Buffer geschaffen worden. Der Index-Buffer ist schlicht und einfach ein Index. Dies bedeutet, daß in diesem Buffer einfach vermerkt wird, an welcher Stelle der Vertex steht, der als nächstes verwendet werden soll. Wir würden also in den vorherigen Illustrationen nicht mehr 36 Vertices definieren müssen, sondern lediglich die 8 aus der ersten Abbildung. Um nun die Dreiecke zu formen, wird einfach im Index-Buffer angegeben, welchen Vertex wir verwenden wollen.

Um dies zu verdeutlichen, werden wir in unserem Beispielprogramm nun ebenfalls einen Index-Buffer verwenden. Dies macht dort nicht viel Sinn, da wir lediglich ein einziges Dreieck haben und auch nur drei Vertices, aber es verdeutlicht zumindest die Anwendung, die sich nicht unterscheidet, egal ob 1000 Indices mit geteilten Vertices vorhanden sind, oder lediglich drei.

Zunächst erzeugen wir wieder ein Member-Variable für den IndexBuffer.

IndexBuffer indexBuffer;

Nachdem wir den Vertex-Buffer aufgesetzt haben, setzen wir auch den Index-Buffer durch Aufruf der neuen Methode SetupIndexBuffer auf. Diese Methode sieht wie folgt aus.

private void SetupIndexBuffer()
{
  uint[] indices = new uint[] { 0, 1, 2 };

  this.indexBuffer = new IndexBuffer(GraphicsDevice, IndexElementSize.ThirtyTwoBits, 3, BufferUsage.WriteOnly);

  this.indexBuffer.SetData<uint>(indices);
}

Im Grunde genommen ist dies fast das gleiche Vorgehen, wie bei den Vertex-Buffern. Wir füllen ein Index-Array mit unseren Indices. In diesem Fall verwenden wir dazu den uint-Datentyp. Die drei Ziffern im Array geben schlicht und einfach an, daß wir als erstes den Vertex an Index 0 verwenden möchten, gefolgt von den Vertices an den Positionen 1 und 2.

Danach erzeugen wir den Index-Buffer durch Aufruf des Konstruktors. Auch hier müssen wir wieder das GraphicsDevice angeben, da der Index-Buffer ja im Grafikkartenspeicher angelegt wird. Danach geben wir mit dem Parameter IndexElementSize an, ob der Index-Buffer 16 oder 32 Bit Element-Größe haben soll, gefolgt von der Anzahl der Elemente, die wir darin speichern wollen. Die Größe in Byte berechnet XNA wieder für uns, diese wäre nur bei nativem DirectX von uns zu berechnen.

Der letzte Parameter, die BufferUsage, verhält sich exakt so, wie sie sich auch beim Vertex-Buffer verhält. Dies hatte ich bereits weiter oben beschrieben.

Um nun den Index-Buffer beim Rendern zu verwenden, müssen wir dies wieder XNA mitteilen. Dazu setzen wir zunächst den Index-Buffer, den wir verwenden wollen, gefolgt von einem aktualisierten Draw-Aufruf. Dies sieht wie folgt aus.

GraphicsDevice.Indices = indexBuffer;

GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 3, 0, 1);

Bei einem Programmstart werden wir nun wieder keine optische Änderung feststellen. „Unter der Haube“ hat sich jedoch einiges getan, denn nun werden alle Dreiecke unter Verwendung eines Index-Buffers gerendert.

Auf die Argumente von DrawIndexedPrimitives möchte ich an dieser Stelle noch nicht im Detail eingehen, da dies doch stark weiterführende Themen sind. Dazu mehr in der MSDN. Zu einem späteren Zeitpunkt werde ich dies jedoch wieder aufgreifen, denn wir werden dies noch benötigen. Wichtig ist momentan nur die 3 an drittletzter Stelle (Anzahl der verwendeten Indices) und das letzte Argument die 1 (Anzahl der zu rendernden Primitives).

XBox-Besonderheiten

Auf der XBox gibt es – das möchte ich an dieser Stelle nicht verschweigen – aufgrund der speziellen Architektur eine Besonderheit. SetData von Index- und Vertex-Buffer darf hier niemals in der Draw-Methode aufgerufen werden. Der Grund liegt im sogenannten Predicated Tiling. Dies ist notwendig, da die XBox ein 10 MB großes, extrem schnelles ED-RAM hat. Dies ist eine Art Zwischenspeicher für Grafikoperationen, daß sich direkt im Chip der GPU befindet. Wird nun etwas gerendert, daß größer ist als diese 10MB, dann wird automatisch das Predicated Tiling aktiviert. Der Bildschirminhalt wird in mehrere Rechtecke aufgeteilt und es werden Teile der Szene gerendert. Dies führt im Grunde genommen dazu, daß die Befehle der Draw-Methode mehrfach aufgerufen werden, aber nur ein einziges mal die (interne) Present-Methode. Die Daten werden aber erst bei Aufruf von Present an die Grafikkarte geschickt. Selbstverständlich ist dabei nicht sichergestellt, daß zu jedem Zeitpunkt die korrekten Daten im Vertex- und/oder Index-Buffer enthalten sind. Daher kann es in diesem Fall zu Grafikfehlern oder sogar Abstürzen kommen.

Aufräumen

Sowohl der Vertex-Buffer, als auch der Index-Buffer implementieren die Dispose-Schnittstelle. Dies macht auch Sinn, da beide einen unmanaged Hardware-Buffer aus dem DirectX-Umfeld im Hintergrund verwenden. Jedes .NET-Objekt, daß eine unmanaged Resource verwendet, sollte Dispose implementieren, damit diese Resourcen explizit freigegeben werden können. Dies ist also in unserem Fall auch keine Aussnahme.

Was bedeutet dies nun für unsere Arbeit mit Index- und Vertex-Buffern? Sie müssen schlicht und einfach freigegeben werden, wenn sie nicht mehr verwendet werden und genau dies machen wir in der UnloadContent-Methode, denn diese wird aufgerufen, wenn der Content der Game-Klasse nicht mehr benötigt wird.

        protected override void UnloadContent()
        {
            if (indexBuffer != null)
            {
                indexBuffer.Dispose();
                indexBuffer = null;
            }

            if (vertexBuffer != null)
            {
                vertexBuffer.Dispose();
                vertexBuffer = null;
            }

            base.UnloadContent();
        }

Ein Wort noch zur Erzeugung und Freigabe von Grafikkarten-Resourcen: Diese Operationen sind teuer bis sehr teuer. Es macht im Grunde genommen niemals Sinn, daß in regelmäßigen Abständen neue Vertex- oder Index-Buffer erzeugt werden. Man sollte die benötigten Buffer vor Abarbeitung der Game-Loop (also z.B. in LoadContent) erzeugen und dann wiederverwenden. Das befüllen von Buffern ist ebenfalls eine teure Operation, aber nicht immer vermeidbar. Auch sind diese Kosten stark von der Datenmenge abhängig und diese Operation wird auch deutlich häufiger benötigt, eigentlich sogar regelmäßig.

Abschluss und Ausblick

Ich habe in diesem Artikel die letzten, notwendigen Grundlagen erklärt: Index- und Vertex-Buffer. Unseren bisherigen Code habe ich so abgeändert, daß dieser zunächst einen Vertex-Buffer und in der letzten Ausbaustufe auch einen Index-Buffer verwendet und so die notwendigen Daten für das Rendern niemals die Grafikkarte verlassen müssen. Damit haben wir die höchstmögliche Geschwindigkeit für einzelne Meshes erreicht. Ursprünglich hatte ich für diesen Artikel bereits geplant, daß wir die ersten Teile des Terrains darstellen. Ich habe mich jedoch kurzfristig beim schreiben dieses Beitrages dagegen entschieden, da wir jetzt bereits einen stattlichen Umfang erreicht haben und dies diesen Grundlagenartikel dennoch aufgebläht hätte. So versteht man sicherlich besser den Sinn und Zweck der Hardware-Buffer. Im nächsten Teil wird dann aber tatsächlich eine kleine Landschaft sichtbar werden.

Ich hoffe auch diesmal wieder, daß dieser über 3000 Worte lange Artikel gut zu lesen und verständlich geschrieben war. Anregungen und Fragen könnt ihr natürlich gerne mit der Kommentarfunktion los werden.

Aufgaben für den Leser

In diesem Bereich möchte ich den Leser dazu ermutigen, sich ein wenig mit dem hier vermittelten Wissen zu beschäftigen und dieses auszubauen. Diese Aufgaben sind optional, aber sehr gut dazu geeignet das Erlernte zu festigen. Probleme diesbezüglich können in den Kommentaren natürlich gerne diskutiert werden.

  • Vergößere den Vertex-Buffer und erweitere ihn um weitere Dreiecke
  • Vergrößere den Index-Buffer und erweitere ihn um weitere Indices
  • Drehe das Backface-Culling mit Hilfe des Index-Buffer um
  • Erzeuge einen Würfel, so wie er im Artikel beschrieben wurde mit Hilfe von 8 Vertices und den passenden Indices

Der gesamte Sourcecode dieses Artikels

Wie auch schon im letzten Teil möchte ich durch eine zusammenhängende Darstellung des Quellcodes den Überblick erhöhen. Daher liste ich hier nochmal den gesamten Quellcode aus diesem Artikel auf.

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Terrain_101
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Effect triangleEffect;

        VertexBuffer vertexBuffer;
        IndexBuffer indexBuffer;

        Matrix projectionMatrix;
        Matrix viewMatrix;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            this.Window.Title = "http://www.MitOhneHaare.de - Terrain 101 - Hardware Buffer (Teil 5)";
        }

        protected override void Initialize()
        {
            projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1.0f, 1000.0f);
            viewMatrix = Matrix.CreateLookAt(new Vector3(2.0f, 2.0f, 2.0f), Vector3.Zero, Vector3.Up);

            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            triangleEffect = Content.Load<Effect>("Triangle");

            SetupVertexBuffer();

            SetupIndexBuffer();
        }

        protected override void UnloadContent()
        {
            if (indexBuffer != null)
            {
                indexBuffer.Dispose();
                indexBuffer = null;
            }

            if (vertexBuffer != null)
            {
                vertexBuffer.Dispose();
                vertexBuffer = null;
            }

            base.UnloadContent();
        }

        private void SetupVertexBuffer()
        {
            VertexPositionColor[] vertices = new VertexPositionColor[] { new VertexPositionColor(new Vector3( 0.0f,  0.5f, 0.0f), Color.Red),
                                                                          new VertexPositionColor(new Vector3( 0.5f, -0.5f, 0.0f), Color.Green),
                                                                          new VertexPositionColor(new Vector3(-0.5f, -0.5f, 0.0f), Color.Blue),
                                                                        };

            this.vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), 3, BufferUsage.WriteOnly);

            this.vertexBuffer.SetData<VertexPositionColor>(vertices);
        }

        private void SetupIndexBuffer()
        {
            uint[] indices = new uint[] { 0, 1, 2 };

            this.indexBuffer = new IndexBuffer(GraphicsDevice, IndexElementSize.ThirtyTwoBits, 3, BufferUsage.WriteOnly);

            this.indexBuffer.SetData<uint>(indices);
        }

        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            triangleEffect.Parameters["World"].SetValue(Matrix.Identity);
            triangleEffect.Parameters["View"].SetValue(viewMatrix);
            triangleEffect.Parameters["Projection"].SetValue(projectionMatrix);

            triangleEffect.CurrentTechnique.Passes[0].Apply();

            GraphicsDevice.SetVertexBuffer(this.vertexBuffer);
            GraphicsDevice.Indices = indexBuffer;

            GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 3, 0, 1);

            base.Draw(gameTime);
        }
    }
}
Advertisements

Veröffentlicht am 25.07.2011 in 3D Terrain, Tutorial, XNA, XNA 4.0 und mit , , , , , , , , , , getaggt. Setze ein Lesezeichen auf den Permalink. 21 Kommentare.

  1. Hellriegel

    Wie immer super! Bitte laß uns nicht wieder so lange auf die nächste Folge warten :p Ich finde es wirklich toll wie du verständlich und einleuchtend die Hintergründe erklärst. Top, weiter so!

    • Vielen, vielen Dank… Ich versuche mich zu beeilen, aber leider habe ich so wenig Zeit… Der Artikel hat ca. 5 Stunden Arbeit gekostet und die nächsten werden noch etwas komplizierter… Ich habe mir aber vorgenommen max. 14 Tage zwischen den Artikeln zu haben. Ich kann aber nichts garantieren…

      • Hellriegel

        Ich weiß, ich weiß^^ Es ist ja auch schon unverschämt zu meckern und zu drängeln wenn man schon so gut aufgearbeitete und verständliche Artikel bekommt und das völlig kostren frei und auch noch in Deutsch. Ist ja keine Selbstverständlichkeit. Ich bin Softwareentwickler, allerdings fast auschließlich fürs web (ASP.NET) und ich finde es super spannend und interessant durch das Auseinandersetzen mit XNA eine völlig andere Sichtweise zu bekommen. Mir hat das teilweise auch bei meiner Arbeit schon weitergeholfen, vielleicht auch mal unkonventionell zu denken. Und ich kenne leider niemanden der sonst so ausführlich und gleichzeitig verständlich über dieses Thema in Deutsch schreibt. Also gib Gas^^ (sonst komm ich vorbei und nerv dich, wohnst ja glaub ich ganz in der Nähe :p) Nein aber im Ernst, schade fänd ich es nur wenn es nicht zu Ende geführt wird, aber bei Dir scheint die Gefahr nicht zu bestehen.

  2. Als aller Erstes einen riesen Lob an Glatzemann, der in seiner Freizeit es fertig bringt, so gut verständliche Artikel zu schreiben! Ich hab mich mit Hilfe von zwei verschiedenen Büchern an die 3D Programmierung rangemacht, und keins davon motiviert so, wie Glatzemann’s Artikel es tun :)))

    Die Erklärungen sind mehr als gut und die Übungen eine klasse Idee. Man tendiert sogar dazu, mehr rumzuspielen, weil man es einfach gut versteht.

    Ich habe aber eine Frage zu diesem Artikel.
    Irgendwie bringe ich es nicht fertig, die beiden letzten Aufgaben zu machen. Beim Culling fehlt mir komplett der Ansatz. Könntest du, oder jemand anderes mir einfach den Ansatz verraten (ohne großartig Code oder so) und bei dem Würfel habe ich folgenden Ansatz:

    Ich habe „vertices“ um einen weiteren Vertex angepasst. Diese so positioniert, dass ich quasi den obersten Teil vom Würfel habe.
    Dann hab ich selbstverständlich da eine „4“ reingemacht:
    vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), 4, BufferUsage.WriteOnly);

    „indicies“ sieht so aus:
    ushort[] indices = new ushort[] { 0, 1, 2, 3, 0 };
    indexBuffer = new IndexBuffer(GraphicsDevice, IndexElementSize.SixteenBits, 5, BufferUsage.WriteOnly);

    und das letzte hab ich so gemacht:
    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 4, 0, 2);

    weil ich ja dachte ich hab jetzt 4 verticies und ich brauche 2 Dreiecke.

    Trotzdem zeigt er mir immer noch nur 1 Dreieck an. Was mache ich falsch?

    • oh, hab das mit Ansatz für den Würfel durch nochmal nachdenken gelöst 😀 war blöd von mir habe zu wenig indicies benutzt.

      Ich glaube der richtige weg für den obersten Teil wäre:
      ushort[] indices = new ushort[] { 0, 1, 3, 1, 2, 3};
      indexBuffer = new IndexBuffer(GraphicsDevice, IndexElementSize.SixteenBits, 6, BufferUsage.WriteOnly);

      Ist das soweit richtig? Oder gehts noch besser?

      • Perfekt und genau richtig.

        Es gibt noch einen anderen Ansatz, der aber heutzutage etwas weniger Verwendung finde und zwar die sogenannten Triangle-Stripes. Dazu hier aber jetzt keine Erklärung, denn das wäre dann etwas umfangreicher.

    • Vielen, vielen Dank… Das geht ja runter wie Öl 🙂

      Zum Culling-Thema: Lies mal weiter in der Reihe. In der übernächsten Folge „Technische Rafinessen“ gehe ich dieses Thema nochmal etwas ausführlicher an und erkläre genau, wie das mit dem Backface-Culling funktioniert. Wenn du dann noch Fragen haben solltest, melde dich einfach nochmal.

      • also den Würfel habe ich jetzt hinbekommen. Mache momentan am „Land in Sicht“ weiter.

        Das mit dem Culling Umgang hab ich noch nicht ganz gerafft(klar kappiert, wie es geht etc.) Aber, damit mein Würfel komplett normal sichtbar ist, kann ich nur mit CullMode.None bewirken.
        Wie wird das anders geregelt? Muss man irgendwie ab einem besitmmten Winkel etwas an den Indicies ändern? Oder muss man ab einem bestimmten Blickwinkel den CullMode switchen?

      • Tausch einfach mal die Reihenfolge der Indizes der einzelnen Dreiecke. Je nachdem welche Reihenfolge die haben wird geculled oder nicht. In „Technische Rafinessen“ erkläre ich das aber noch mal im Detail, da gibt es einen eigenen Abschnitt über genau dieses Thema.

      • Achsoo. Ja auf die Idee kam ich auch, als mein Würfel so krüpellig aussah. Aber ok, ich dachte, dass es jetzt halt geschickter geht^^

        Ich freue mich schon auf die neue Folge. :))

        Du solltest ein Buch schreiben xD

  3. Hi, super ein Turorial haste da 😀

    Hab allerdings noch eine Frage zum Indexbuffer die mir beim probieren gekommen ist.
    Bei meinem Programm ist es so, dass ich zur Laufzeit neue Indices hinzufüge.

    Jetzt kann ich allerding dessen Größe(indexcount beim Konstruktor aufruf) zur Laufzeit nicht mehr ändern.
    Wenn ich das Objekt neu erstelle, meckert der Compiler dass „Der Rückgabewert nicht geändert werden kann, da er keine Variable ist“. Lege ich einen temporären Indexbuffer an um ihn den alten Indexbuffer zuzuweisen, wird dieser einfach ignoriert.(die Größe ändert sich nicht). Bei beiden Varianten kann ich aber trotzdem mit setData die Daten ändern(welche auch übernommen werden).
    Gelöst habe ich es im mom, indem ich einfach die Größe des Indexbuffers auf die maximalgröße(width * height) setze, was aber unschön für den Speicher ist(vor allem wenn ich von den 96 indices nur 4 speichern will)

    Gibt es da ne bessere Variante?

    • Hallo, freut mich, dass dir das Tutorial gefällt.

      Eine Bitte wegen deiner Frage. Das Tutorial ist mittlerweile größtenteils auf meine neue Plattform „http://www.indiedev.de“ umgezogen. Könntest du dort im Forum bitte die Frage nochmal stellen, denn das hat mehrere Vorteile:

      1. Es ist viel bequemer und komfortabler (ok, beim erstenmal ist es etwas komplizierter wg. Anmeldung und so)
      2. Andere Benutzer können dort ebenfalls Tipps geben, so das schneller und qualitativ hochwertigere Antworten kommen
      3. Suchfunktion 🙂

      Wäre echt nett. Ist vollkommen kostenlos und unverbidndlich…

  1. Pingback: Terrain 101: Transformationen « "Mit ohne Haare"

  2. Pingback: Terrain 101: Das erste Dreieck « "Mit ohne Haare"

  3. Pingback: Terrain 101: Die Entwicklungsumgebung und das XNA 4.0 Projekt « "Mit ohne Haare"

  4. Pingback: Klötzchengrafik « "Mit ohne Haare"

  5. Pingback: Terrain 101: Land in Sicht « "Mit ohne Haare"

  6. Pingback: Terrain 101: Eine Einführung in dreidimensionale Landschaften in Echtzeit « "Mit ohne Haare"

  7. Pingback: Terrain 101: Technische Rafinessen « "Mit ohne Haare"

  8. Pingback: Terrain 101: Neue Sichtweisen « "Mit ohne Haare"

  9. Pingback: Terrain 101: Kamera ab und Action « "Mit ohne Haare"

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: