In diesem Kapitel erkläre ich, wie in MonoGame ein Dreieck gerendert werden kann. Das erste Dreieck kommt in der 3D-Spiele-Entwicklung fast dem HelloWorld-Programm gleich. Neben Vertices werden auch einige Aspekte von Shadern abgedeckt, zu denen es eine separate Serie geben wird.

Vertices anlegen

Öffnet ein MonoGame-Projekt. Als erstes werden wir in unserer Game1-Klasse (Hauptklasse) ein globales Array anlegen, das unsere Vertices speichert. Global, weil wir das Array aus mehreren Methoden ansprechen wollen. Wie im vorherigen Kapitel erwähnt, kann ein Vertex verschiedene Informationen speichern. Aus diesem Grund bietet uns MonoGame eine Reihe von Vertex-Strukturen an. Zum Beispiel könnten wir ein Array aus VertexPosition erzeugen. Dann würde unser Dreieck allerdings weiß werden. Deshalb wählen wir VertexPositionColor aus, um die Vertices farblich voneinander zu unterscheiden.


VertexPositionColor[] vertices;

Die Initialisierung könnt ihr prinzipiell in jeder Methode vornehmen. Achtet allerdings darauf, dass die Methode einmalig aufgerufen wird, da ihr in der Update- oder Draw-Methode das Array jedes mal neu anlegen würdet. Ich lege eine neue Methode an, die ich dann in der LoadContent-Methode aufrufe.


private void SetVertices()
{
vertices = new VertexPositionColor[3];

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

Da das Dreieck genau drei Vertices besitzt, ist die Länge des Arrays drei. Den einzelnen Array-Elementen weisen wir dann drei neue VertexPositionColor-Objekte zu. Die Koordinaten sind die lokalen Koordinaten, mit denen auch die Grafikkarte ihre Berechnungen intern durchführt. Die Bildschirmmitte ist der Nullpunkt. Der rechte Bildschirmrand hat den X-Wert 1,0. Alle Werte liegen zwischen 0 und 1, da bei Fließkommaberechnungen die Genauigkeit in diesem Bereich am höchsten ist. Später werden wir diese Koordinaten allerdings in Weltkoordinaten mithilfe von Matrizen transformieren.

Außerdem ist anzumerken, dass die Punkte des Dreiecks im Uhrzeigersinn angegeben werden. Die Reihenfolge ist sehr wichtig, da man mit ihr beurteilen kann, ob man ein Dreeick von vorne oder hinten betrachtet. In Spielen werden aus Performance-Gründen nur die Dreiecke gezeichnet, die man von vorne sieht. Zwar kann man in MonoGame das Verhalten anpassen, es empfiehlt sich aber sich direkt daran zu halten.

Um das Dreieck zu zeichnen, benutzen wir die DrawUserPrimitives-Methode des GraphicsDevice-Objekts.


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

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

base.Draw(gameTime);
}

DrawUserPrimitives ist eine typsierte Methode. Als ersten Parameter geben wir den PrimitvType an. Hier wählen wir die TriangleList. Dadurch weiß unsere Grafikkarte, dass sie unsere Vertices zu Dreiecken zusammenfassen soll. Im zweiten Parameter übergeben wir unser Vertex-Array. Die 0 ist ein Offset, also ab welchem Vertex die Grafikkarte beginnen soll zu zeichnen. Alle Vertices die vor dem angegeben Index vorkommen, werden also ignoriert. Im letzten Parameter geben wir an, wie viele Primitives gezeichnet werden. Da wir nur ein Dreieck zeichnen wollen, geben wir hier die 1 an.

Wenn wir den Code ausführen, bekommen wir den Fehler, dass ein Vertex-Shader gesetzt werden muss. Einfach gesagt sind Shader Berechnungen die auf der Grafikkarte stattfinden. Wir senden unsere Vertices momentan mit jedem Draw-Call an unsere Grafikkarte, doch nehmen keine Berechnungen mit ihnen vor, um Bildpunkte zu berechnen. Also die Farbe der Pixel auf unserem Bildschirm.

Vertex-Shader

Shader lagert man in FX-Dateien aus. Die Programmiersprache ist für DirectX-Spiele HLSL (High Level Shader Language). Bei OpenGL wird das sehr ähnliche GLSL verwendet.

In unserem MonoGame Content-Pipeline-Tool legen wir ein neues Element für unsere Shader-Datei an. Ihr könnt zwischen SpriteEffect und Effect auswählen. Mit SpriteEffects könnt ihr Sprites in 2D Spielen mit Effekten versehen. Für 3D-Spiele wählt man Effect. Was ihr wählt, ist an sich egal, da wir eh alles löschen werden. Die beiden Dateien unterscheiden sich lediglich in der Startvorlage.

Der Vertex-Shader ist eine Funktion, die für jeden Vertex aufgerufen wird. In diesem Kapitel möchte ich allerdings den Shader komplett vorgeben ohne ihm näher zu erläutern. In späteren Kapiteln gehe ich dann mehr darauf ein, wenn wir gewisse Effekte programmieren.


#if OPENGL
#define SV_POSITION POSITION
#define VS_SHADERMODEL vs_3_0
#define PS_SHADERMODEL ps_3_0
#else
#define VS_SHADERMODEL vs_4_0_level_9_1
#define PS_SHADERMODEL ps_4_0_level_9_1
#endif

struct VertexShaderInput
{
float4 Position : POSITION0;
float4 Color : COlOR0;
};
struct VertexShaderOutput
{
float4 Position : SV_POSITION;
float4 Color : COlOR0;
};

VertexShaderOutput MainVS(VertexShaderInput input)
{
VertexShaderOutput output;
output.Position = input.Position;
output.Color = input.Color;
return output;
}

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

technique Technique1
{
pass P0
{
VertexShader = compile VS_SHADERMODEL MainVS();
PixelShader = compile PS_SHADERMODEL MainPS();
}
};

Speichert die Datei so ab. In unserer Hauptklasse können wir den Effekt nun laden, indem wir erstmal eine Variable oben anlegen.


Effect effect;

In der LoadContent-Methode laden wir den Effekt.


protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
effect = Content.Load("Effect");
SetVertices();
}

Nun können wir in der Draw-Methode unseren Shader setzen, bevor wir die definierten Vertices auf den Bildschirm zaubern.


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

effect.CurrentTechnique.Passes[0].Apply();
GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 1);

base.Draw(gameTime);
}

Abschluss

Sollte alles richtig gemacht worden sein, seht ihr folgendes Dreieck beim Debuggen.

firstdreieck

Im nächsten Kapitel wird das Dreieck in Welt-Koordinaten transformiert.