Meine Werkzeuge
Namensräume
Varianten

Terrain 101/Das erste Dreieck

Aus indiedev
Wechseln zu: Navigation, Suche
Hinweis Dieser Artikel wird gerade geschrieben bzw. bearbeitet und erfüllt noch nicht die Qualitätsstandards von indiedev.
Bitte bearbeite diesen Artikel nur, wenn du kleine Fehler korrigieren möchtest oder der Autor bist.
Terrain 101
Das erste Dreieck
TutorialTerrain101.png
Autor Roland "Glatzemann" Rosenkranz
Programmier­sprache C#
Kategorie XNA
Diskussion Thread im Forum
Lizenz indiedev article license
Terrain 101 auf MitOhneHaare.de


Dies ist der dritte Artikel meiner Artikelreihe "Terrain 101", deren Ziel darin besteht, sowohl dem Einsteiger, als auch dem Fortgeschrittenen beizubringen, wie man eine dreidimensionale Landschaft entwickeln kann. 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 wird, daß man sie auch in einem Spiel verwenden kann.

In diesem Teil fangen wir nun endlich richtig an, nachdem ich ein paar Worte zu den Grundlagen verloren habe und erklärt habe, wie man ein neues Projekt anlegt. Wir werden uns in diesem Artikel nun das erste mal an eine 3D-Grafik wagen und ich werde das Thema ausführlich in kleinen Schritten erläutern.

Inhaltsverzeichnis

Der Effekt

Bevor wir richtig loslegen, müssen wir eine kleine Vorbereitung treffen: Wir müssen einen Effekt erzeugen und diesen in unser Content-Projekt einbinden. Dies klingt jetzt vielleicht etwas kompliziert, ist es aber nicht wirklich. Ich werde aber vorerst nicht genauer darauf eingehen, was dieser Effekt im Detail macht und warum er so erstellt wurde, wie er ist. An dieser Stelle nur soviel: Ohne einen Effekt ist es in XNA 4.0 nicht möglich 3D-Grafiken zu rendern. Dies liegt daran, daß seit DirectX 10 die sogenannte Fixed-Function-Pipeline abgeschafft wurde. Diese war im Grunde genommen ein fest verdrahteter Shader, der noch aus Zeiten stammt, in denen 3D-beschleunigte Grafikkarten gar keine programmierbaren Shader hatten. XNA 4.0 basiert zwar immer noch auf DirectX 9, aber Microsoft hat schon mal vorgesorgt und bereits vieles für den Umstieg auf DirectX 10 oder 11 vorbereitet. Die Ära von DirectX 9 neigt sich langsam den Ende entgegen. Fehlen wird uns im Grunde genommen nichts, denn XNA liefert den sogenannten BasicEffect mit, der die Fixed-Function Pipeline ganz gut emuliert bzw. sogar noch ein paar Dinge zusätzlich bietet.

Lange Rede, kurzer Sinn: Wir benötigen einen Effekt.

Um einen neuen Effekt anzulegen, klicken wir mit der rechten Maustaste auf unser Content-Projekt und fügen ein neues Element hinzu. Im folgenden Fenster wählen wir ganz einfach "Effect File" aus und geben diesem den Namen Triangle.fx. Visual Studio legt nun einen Standard-Effekt an, der minimalen Code enthält. Diesen benötigen wir aber für diesen ersten Schritt nicht: Einfach alles löschen (Strg-A, Entf.) und danach folgenden Code einfügen.


struct VertexShaderInput
{
    float4 Position : POSITION0;
	float4 Color    : COLOR0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
	float4 Color    : COLOR0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

	output.Position = input.Position;
	output.Color = input.Color;

    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return input.Color;
}

technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Wie schon beschrieben müssen wir den erstmal nicht verstehen. Wie Shader funktionieren und was wir darin machen müssen werde ich zu einem späteren Zeitpunkt erklären. Wer es garnicht erwarten kann und schon mal etwas über Shader erfahren will, den möchte ich an dieser Stelle auf die Shader-Kategorie meines Blogs verweisen. Dort wird einiges über Shader geschrieben und die Kategorie wird ständig ausgebaut. Diese Artikel zu lesen und zu verstehen ist aber, wie schon geschrieben, zum jetzigen Zeitpunkt noch nicht notwendig.

Den Effekt müssen wir natürlich auch laden, was der Content-Manager übernimmt. Wir müssen dafür lediglich eine Member-Variable einfügen. Dies geschieht am Anfang der Klasse, die nun wie folgt aussehen sollte:


using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

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

Dies ist selbstverständlich nicht die komplette Klasse, nur der Anfang. Zur besseren Übersicht werde ich am Ende dieses Artikels nochmal die gesamte Klasse bereitstellen. Hier habe ich einfach unmittelbar nach der neuen Zeile aufgehört. Die Kommentare, die im Standard-Template für die Game-Klasse enthalten sind habe ich komplett entfernt, da uns diese für dieses Tutorial nicht weiter interessieren.

In der LoadContent-Methode laden wir nun den Effekt mit folgendem Befehl:


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

Damit weisen wir den ContentManager Content schlicht und einfach an, daß ein Asset vom Type Effect mit dem Namen Triangle geladen und der Verweis darauf in der Variablen tringleEffect gespeichert werden soll. Die harte Arbeit dabei übernimmt der Content-Manager für uns, der sich auch um das entladen und aufräumen kümmert.

Als letztes müssen wir der Grafikkarte noch sagen, daß dieser Effekt verwendet werden soll. Dies erfolgt in der Draw-Methode unmittelbar nach dem Löschen des Bildschirms mit dem Clear-Befehl. Der Befehl dazu ist relativ einfach:


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

Wir haben nun alles vorbereitet und der Grafikkarte mitgeteilt, daß wir mit unserem Effekt rendern möchten. Kommen wir nun zum interessanten Teil dieses Tutorials: Das Rendern.

Das Rendern von Vertices

Eine moderne Grafikkarte kann 3D-Grafik rendern. Diese besteht aus Dreiecken und davon kann so eine Grafikkarte ziemlich viele darstellen. Die Grafikkarte kann noch einiges mehr, aber das Wichtigste sind erstmal die Dreiecke, aus denen jeder Gegenstand besteht, der dargestellt werden soll. Genau so ein Dreieck, erstmal ein Einziges, werden wir in diesem Teil der Terrain 101 Reihe erzeugen, denn darauf wird alles weitere aufbauen.

terrain101_untransformedaxis_clockwise.jpg

Ein Dreieck besteht aus Vertices (Mehrzahl, Einzahl: Vertex). Ein Vertex ist in diesem Zusammenhang einfach eine 3D-Koordinate im Raum, die jedoch auch noch um weitere Informationen angereichert werden kann. Ein Dreieck besteht dabei aus drei Vertices, für jeden Eckpunkt einer. Dabei ist die Reihenfolge der Vertices sehr wichtig und diese ist nicht optional. Der Standard ist dabei, diese im Uhrzeigersinn anzuordnen, also: Der obere Vertex an der Spitze, dann rechts unten und schliesslich links unten. Frei wählbar ist jedoch der Vertex, mit dem man beginnt.

Wieso ist denn nun die Reihenfolge so wichtig? Ganz einfach: Wenn diese immer gleich ist, dann kann ich eine wichtige Information daraus gewinnen und zwar, von welcher Richtung das Dreieck betrachtet wird. Schaue ich von vorne auf das Dreieck, so sind die Vertices im Uhrzeigersinn angeordnet. Schaue ich mir dieses Dreieck jedoch von hinten an, so ist die Reihenfolge genau entgegengesetzt. Dies ist eine sehr, sehr wichtige Information für die Grafikkarte, die für eine wichtige Optimierung verwendet wird. Über die Vertex-Reihenfolge realisiert die Grafikkarte das sogenannte Backface-Culling, bei dem alle Dreiecke entfernt werden, die man von hinten sieht, da diese normalerweise unsichtbar sind. Wird eine Kugel aus Dreiecken erzeugt, so kann dadurch fast die Hälfter aller Dreiecke ausgelassen werden, ohne das sichtbare Ergebnis zu beeinflussen.

Man kann diese Reihenfolge übrigens beeinflussen. Dies geschieht mit dem sogenannten CullMode des RasterizerStates. In bestimmten Fällen (bei Schattenwurf oder zum rendern von Lichtkegeln z.B.) kann es notwendig sein, daß die Richtung umgekehrt werden muss. In diesem Tutorial und auch sonst wird aber meistens mit der Default-Einstellung gearbeitet: CullCounterClockwise. Daran sollten wir uns auch halten und dies verinnerlichen, denn dann werden wir keine Probleme bekommen. Nicht einfach zu findende Fehler und unschöne Effekte entstehen nämlich immer dann, wenn man beide Richtungen kombiniert und Teile der Geometrie dadurch falsch oder garnicht dargestellt werden.

Wie bereits angedeutet kann ein Vertex verschiedene Informationen enthalten. Um festzulegen welche dies sind und um dies der Grafikkarte mitzuteilen benötigt man ein sogenanntes VertexFormat. Zum Glück bietet XNA ein paar vorgefertigte, die wir schnell und einfach verwenden können und das werden wir im folgenden auch tun. In diesem Bereich wurde einiges in XNA 4.0 optimiert. Mittlerweile ist es relativ einfach und transparent mit Vertex-Formaten zu arbeiten. Bis XNA 3.1 war dies mitunter ein etwas kompliziertes Unterfangen. Für den Anfang reicht uns eine Position und eine Farbe, weshalb wir das Vertex-Format VertexPositionColor verwenden werden, die genau diese beiden Informationen bietet. Um unsere Daten zu halten, definieren wir einfach eine Member-Variable mit einem Array dieses Typs. Dies erfolgt direkt nach der Member-Variable für unseren Effect:


VertexPositionColor[] vertices;

Dieses Array befüllen wir mit Daten und wie ich im letzten Teil schon beschrieben habe, gehört sowas in die LoadContent-Methode. Dort definieren wir nun einfach die drei Eckpunkte unseres Dreiecks:


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),
           };

Um die einzelnen Vertices im fertigen Programm besser auseinander halten zu können habe ich ihnen unterschiedliche Farben gegeben. Der Erste ist rot, der Zweite grün und der Dritte ist blau. Dies ist übrigens eine gute Möglichkeit zu Debuggen. Wenn ich mir unsicher bin wo ein bestimmter Vertex im fertigen Ergebnis landet, so färbe ich diesen einfach ein und kann mich so besser orientieren. Bei einem Dreieck ist dies noch nicht so wichtig, wenn aber später mal hunderte von Vertices gleichzeitig dargestellt werden, dann kann dies die Übersichtlichkeit deutlich erhöhen.

terrain101_untransformedaxis.jpg

Der andere Parameter ist ganz einfach unsere Position und diese muss in einem bestimmten Koordinatensystem angegeben werden. Da dieses momentan noch untransformiert ist läuft dieses auf allen Achsen von -1 bis +1. Zur Transformation werden wir später noch kommen. Die Grafikkarte arbeitet aber intern mit diesen Koordinaten. Dies ist auch sehr einfach zu erklären: Die enauigkeit von Fließkommazahlen ist in exakt diesem Wertebereich am höchsten.

Um nun unser gerade erzeugtes Dreieck auch sichtbar zu machen, müssen wir natürlich die Grafikkarte anweisen, dies zu tun. Dafür gibt es eine Reihe von Draw-Befehlen und in diesem Fall erfolgt dies unmittelbar nachdem wir der Grafikkarte mitgeteilt haben, welchen Effekt wir verwenden wollen und zwar mit folgender Zeile:


GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleList, vertices, 0, 1);

Eine ausführliche Beschreibung dieses Befehls befindet sich in der MSDN-Dokumentation, aber trotzdem hier der Vollständigkeit halber ein paar Erklärungen, die hoffentlich etwas einfacher verständlich sind. Zuerst mal diese komische Schreibweise mit hinter dem Methodennamen: Dies zeigt dem fortgeschrittenen C#-Entwickler, daß hier eine generische Methode verwendet wird. Dabei kann man einen Typparameter mitgeben, in diesem Fall unser Vertex-Format VertexPositionColor.

Der erste Parameter ist der PrimitiveType mit dem wir in diesem Fall festlegen, daß wir eine Dreiecksliste, also eine TriangleList darstellen wollen. Dies ist die am häufigsten verwendete Möglichkeit. Sie definiert einfach, daß aus jeweils drei aufeinanderfolgenden Vertices ein Dreieck gebildet werden soll. Wir können nämlich, falls dies noch nicht klar geworden sein sollte, mit DrawUserPrimitives mehr als ein Dreieck gleichzeitig rendern, wenn wir auch mehrere in unser Vertex-Array packen.

Der zweite Parameter ist die Referenz auf unser zuvor definiertes Vertex-Array, daß die einzelnen Vertices enthält.

Der dritte Parameter ist der sogenannte vertexOffset. Dieser ermöglicht es uns nicht nur beim ersten Element in unserem Vertex-Array mit dem Zeichnen zu beginnen, sondern auch bei einem späteren. Nehmen wir mal an, daß wir in unserem Vertex-Array 10 Dreiecke gespeichert haben, von denen wir aber aus irgendeinem Grund nur die letzten 9 darstellen wollen. Anstatt nun dieses Array neu aufbauen zu müssen, können wir einfach mit diesem Parameter festlegen, daß erst ab dem vierten Vertex mit dem zeichnen begonnen werden soll, wir übergeben also eine 3, da dieser Parameter nullbasiert ist (das erste Element ist an Index 0). Dies wird aber nicht so häufig benötigt, kann aber das ein oder andere mal in speziellen Fällen sehr, sehr hilfreich sein.

Der letzte Parameter gibt an, wieviele Primitive, also Grundtypen wir darstellen wollen. Wie dieser Parameter exakt zu interpretieren ist, hängt vom PrimitiveType ab. In unserem Fall stellen wir ein Dreieck dar. Jedes Dreieck besteht aus drei Vertices und daher müssen wir in diesem Fall die Anzahl der Vertices durch drei teilen. Daraus ergibt sich die Anzahl der Dreiecke in unserem Vertex-Array. In unserem Fall ein einziges. Bei den anderen Grundtypen, wie z.B. einer LineList müssen hier andere Werte angegeben werden, daß soll uns aber für den Anfang nicht interessieren.

Der vertexCount Parameter ist jedenfalls eine Möglichkeit am Ende des Arrays einzelne Dreiecke auszulassen. Um beim vorherigen Beispiel mit den 10 Dreiecken zu bleiben: Wenn wir hier eine 9 übergeben, dann werden nur die ersten neun Dreiecke gezeichnet. Übergeben wir eine 8 und als vertexOffset eine 3, so werden acht Dreiecke ab dem zweiten Dreieck im Vertex-Array gerendert. Lücken in der Mitte des Arrays sind damit zwar nicht möglich, aber wir haben schon etwas Einfluß und können damit später interessante Effekte erzielen.

Wenn wir nun unser Programm mit F5 (Kompilieren + Ausführen) starten sehen wir unser erstes 3D-Dreieck in voller Pracht.

terrain101_firsttriangle.jpg

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.

  • Stelle drei Dreiecke mit einem DrawUserPrimitives-Aufruf dar
  • Stelle die drei Dreiecke in unterschiedlichen Farben dar
  • Stelle nur die letzten beiden Dreiecke im Vertex-Array dar, ohne das Array zu verändern
  • Stelle nur die ersten beiden Dreiecke im Vertex-Array dar, ohne das Array zu verändern
  • Steller nur das zweite Dreieck im Vertex-Array dar, ohne das Array zu verändern

Zusammenfassung und Ausblick

In diesem Artikel der Reihe Terrain 101 habe ich euch gezeigt, wie euer erstes Dreieck im 3D-Raum gezeichnet wird. Bisher ist es noch nichts besonderes und noch nicht sonderlich eindrucksvoll, vermittelt jedoch die Basics, auf denen die gesamte Welt der 3D-Programmierung basiert. Mit einfachen Erklärungen und einem sehr einfachen Beispiel wurde verdeutlicht, daß der Einstieg in die 3D-Programmierung nicht schwer sein muß.

Im nächsten Teil dieser Artikelreihe werde ich dieses Beispiel weiter ausbauen und ein erklären, was Transformationen und Projektionen sind. Damit werden wir unser Wissen im Bereich 3D-Programmierung weiter ausbauen und einen weiteren, wichtigen Schritt in Richtung 3D-Terrain gehen.

Der gesamte Sourcecode dieses Artikels

Um diesen Artikel endgültig abzuschliessen möchte ich noch kurz den gesamten Quellcode, den wir in diesem Artikel erstellt haben auflisten. Durch die zusammenhängende Darstellung ist es einfacher sich einen Überblick zu verschaffen.


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;
        VertexPositionColor[] vertices;

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

        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            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");

            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),
                                                 };
        }

        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.CurrentTechnique.Passes[0].Apply();
            GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleList, vertices, 0, 1);

            base.Draw(gameTime);
        }
    }
}

Navigation
Tutorials und Artikel
Community Project
Werkzeuge