GLSL Shader in MonoGame SDL2 - tobiasschulz/MonoGame-SDL2 GitHub Wiki

Auf dieser Seite soll beschrieben werden, wie in GLSL geschriebene Shader in MonoGame-SDL2 verwendet werden können.

Einleitung

Normalerweise werden Shader bei der Spieleentwicklung mit MonoGame in HLSL geschrieben und mit dem Tool 2MGFX in GLSL konvertiert. So können Shader, die bereits in HLSL vorhanden sind, in MonoGame weitergenutzt werden. Dies ist vor allem bei der Portierung von alten XNA-Spielen praktisch.

Wenn man aber von Grund auf neue Spiele mit MonoGame entwickeln möchte, stößt man häufig auf das Problem, dass HLSL mit Shader Model 2 für bestimmte Zwecke zu eingeschränkt ist. Mit MonoGame ist es prinzipiell möglich, aktuelle (4.X) GLSL-Shader zu nutzen, allerdings gibt es bei der Upsteam-Version von MonoGame ein Problem: beim Laden von Shadern wird das Binärformat von 2MGFX erwartet, und außer 2MGFX gibt es kein Tool, das kompatible Shader-Dateien erstellt.

Daher habe ich für ein Projekt, das ich mit fünf anderen Studenten für das Karlsruher Institut für Technologie entwickelt habe, einen Branch von MonoGame-SDL2 erstellt, der zusätzlich zu dem o.g. Binärformat auch das Einlesen von normalem (ASCII-)GLSL-Code unterstützt.

Dazu wurden vor allem die beiden Klassen "Effect" und "Shader" angepasst und zwei zusätzliche Konstruktoren hinzugefügt, dazu später mehr.

Einen HLSL-Shader in lesbares GLSL konvertieren

Nehmen wir zum Beispiel folgende einfache XNA-kompatible Fx-Datei:

float4x4 World;
float4x4 View;
float4x4 Projection;
float4 color1;

struct VertexShaderInput { float4 Position : POSITION0; };
struct VertexShaderOutput { float4 Position : POSITION0; };

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
    return output;
}

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

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

Normalerweise würde man diesen Effekt als .fx-Datei abspeichern und mit 2MGFX in eine mgfx-Datei konvertieren:

2mgfx.exe shader1.fx shader1.mgfx

Diese Datei wird dann in MonoGame wie folgt geladen:

Effect shader1 = new Effect (graphicsDevice: GraphicsDevice, effectCode: File.ReadAllBytes ("path\...\shader1.mgfx"));

Wenn MonoGame-SDL2 mit unserem Patch kompiliert wurde, kann nun über die Property EffectCode der GLSL-Effektcode in reinem ASCII ausgelesen werden:

// Write human-readable effect code to file
File.WriteAllText ("path\...\shader1.glfx_gen", shader1.EffectCode);

Dann findet man in der angegebenen Datei folgenden GLSL-Code mit MonoGame-Metadaten:

#monogame ConstantBuffer(name=ps_uniforms_vec4; sizeInBytes=32; parameters=[0, 1]; offsets=[0, 16])
#monogame ConstantBuffer(name=vs_uniforms_vec4; sizeInBytes=192; parameters=[2, 3, 4]; offsets=[0, 64, 128])

#monogame BeginShader(stage=pixel; constantBuffers=[0])

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

uniform vec4 ps_uniforms_vec4[2];
vec4 ps_r0;
#define ps_c0 ps_uniforms_vec4[0]
#define ps_c1 ps_uniforms_vec4[1]
#define ps_oC0 gl_FragColor

void main()
{
	ps_r0 = ps_c0;
	ps_r0 = ps_r0 + ps_c1;
	ps_oC0 = ps_r0;
}


#monogame EndShader()

#monogame BeginShader(stage=vertex; constantBuffers=[1])
#monogame Attribute(name=vs_v0; usage=Position; index=0)

#ifdef GL_ES
precision highp float;
precision mediump int;
#endif

uniform vec4 vs_uniforms_vec4[12];
vec4 vs_r0;
vec4 vs_r1;
#define vs_c0 vs_uniforms_vec4[0]
#define vs_c1 vs_uniforms_vec4[1]
#define vs_c2 vs_uniforms_vec4[2]
#define vs_c3 vs_uniforms_vec4[3]
#define vs_c4 vs_uniforms_vec4[4]
#define vs_c5 vs_uniforms_vec4[5]
#define vs_c6 vs_uniforms_vec4[6]
#define vs_c7 vs_uniforms_vec4[7]
#define vs_c8 vs_uniforms_vec4[8]
#define vs_c9 vs_uniforms_vec4[9]
#define vs_c10 vs_uniforms_vec4[10]
#define vs_c11 vs_uniforms_vec4[11]
attribute vec4 vs_v0;
#define vs_oPos gl_Position

void main()
{
	vs_r0.x = dot(vs_v0, vs_c0);
	vs_r0.y = dot(vs_v0, vs_c1);
	vs_r0.z = dot(vs_v0, vs_c2);
	vs_r0.w = dot(vs_v0, vs_c3);
	vs_r1.x = dot(vs_r0, vs_c4);
	vs_r1.y = dot(vs_r0, vs_c5);
	vs_r1.z = dot(vs_r0, vs_c6);
	vs_r1.w = dot(vs_r0, vs_c7);
	vs_oPos.x = dot(vs_r1, vs_c8);
	vs_oPos.y = dot(vs_r1, vs_c9);
	vs_oPos.z = dot(vs_r1, vs_c10);
	vs_oPos.w = dot(vs_r1, vs_c11);
}


#monogame EndShader()

#monogame EffectParameter(name=color1; class=Vector; type=Single; semantic=; rows=1; columns=4; elements=[]; structMembers=[])
#monogame EffectParameter(name=color2; class=Vector; type=Single; semantic=; rows=1; columns=4; elements=[]; structMembers=[])
#monogame EffectParameter(name=World; class=Matrix; type=Single; semantic=; rows=4; columns=4; elements=[]; structMembers=[])
#monogame EffectParameter(name=View; class=Matrix; type=Single; semantic=; rows=4; columns=4; elements=[]; structMembers=[])
#monogame EffectParameter(name=Projection; class=Matrix; type=Single; semantic=; rows=4; columns=4; elements=[]; structMembers=[])
#monogame EffectPass(name=Pass1; vertexShader=1; pixelShader=0)
#monogame EffectTechnique(name=Technique1)

Den generierten GLSL-Code vereinfachen

Dieser Code wurde von 2MGFX mit Hilfe der Library MojoShader generiert und sieht entsprechend schrecklich aus. Es handelt sich um eine 1:1-Übersetzung der .mgfx-Binärdatei in lesbare ASCII-Deklarationen.

An allen Stellen in der .mgfx-Datei, an denen Binärdaten standen, stehen in der ASCII-Datei MonoGame-spezifische Preprocessor-Directives. Diese beginnen immer mit #monogame, und enthalten danach meistens den Klassennamen, von dem beim Parsen der Datei ein Objekt erstellt werden soll, sowie die jeweiligen Parameter des Konstruktors.

Um den Code etwas zu vereinfachen, verschieben wir zunächst alle EffectParameter- und ConstantBuffer-Direktiven an den Anfang und ordnen sie. Außerdem benutzen wir im Gegensatz zum generierten Code den "*"-Operator zur Matrixmultiplikation.

#monogame EffectParameter(name=color1; class=Vector; type=Single; rows=1; columns=4)
#monogame EffectParameter(name=color2; class=Vector; type=Single; rows=1; columns=4)
#monogame ConstantBuffer(name=bufferColors; sizeInBytes=32; parameters=[0, 1]; offsets=[0, 16])

#monogame EffectParameter(name=World; class=Matrix; type=Single; rows=4; columns=4)
#monogame ConstantBuffer(name=World; sizeInBytes=64; parameters=[2]; offsets=[0])

#monogame EffectParameter(name=View; class=Matrix; type=Single; rows=4; columns=4)
#monogame ConstantBuffer(name=View; sizeInBytes=64; parameters=[3]; offsets=[0])

#monogame EffectParameter(name=Projection; class=Matrix; type=Single; rows=4; columns=4)
#monogame ConstantBuffer(name=Projection; sizeInBytes=64; parameters=[4]; offsets=[0])

#monogame BeginShader(stage=pixel; constantBuffers=[0])
#version 130

uniform vec4 bufferColors[2];

void main()
{
	gl_FragColor = bufferColors[0]+bufferColors[1];
}


#monogame EndShader()

#monogame BeginShader(stage=vertex; constantBuffers=[1, 2, 3])
#monogame Attribute(name=inputPosition; usage=Position; index=0; format=0)
#version 130

uniform vec4 World[4];
uniform vec4 View[4];
uniform vec4 Projection[4];
in vec4 inputPosition;

void main()
{
    mat4 world = mat4(World[0], World[1], World[2], World[3]);
    mat4 view = mat4(View[0], View[1], View[2], View[3]);
    mat4 proj = mat4(Projection[0], Projection[1], Projection[2], Projection[3]);
    gl_Position = inputPosition * world * view * proj;
}


#monogame EndShader()

#monogame EffectPass(name=Pass1; vertexShader=1; pixelShader=0)
#monogame EffectTechnique(name=Technique1)

Bei GLSL-Code, der mit MonoGame zusammen genutzt werden soll, sind einige Besonderheiten zu beachten. Eine der wichtigsten Einschränkungen ist, dass es für die über uniform, in und out übergebenen Parameter nur einen zulässigen Datentyp gibt: einen 4-dimensionalen Vektor (vec4).

MojoShader tendiert dazu, für den Pixel- und den Vertex-Shader jeweils einen einzigen großen sogenannten ConstantBuffer zu generieren, der alle uniform-Parameter als Vektoren mit 4 Dimensionen enthält. Dies ist in dem vorherigen Beispiel an der Variable vs_uniforms_vec4 gut erkennbar. Diese ist 12 Elemente groß und enthält die World-, die View- und die Projection-Matrix. Diese sind jeweils 16 float's groß, können also durch 4 vec4's repräsentiert werden.

Der erste Schritt beim Vereinfachen des generierten Codes ist es also, den großen ConstantBuffer in drei Teile aufzuspalten. Dazu werden oben zwei zusätzliche ConstantBuffer-Deklarationen hinzugefügt, bei denen die folgenden Parameter angepasst werden müssen:

  • sizeInBytes: bei einer 4x4-Matrix, repräsentiert durch ein Array von vier vec4's, sind das 64 bytes.
  • parameters: der Index des dazugehörigen EffectParameters. Die Zählung beginnt bei 0 und findet in der Reihenfolge der Zeilen in der Datei statt.
  • offsets: die Offsets der jeweiligen einzelnen Parameter innerhalb des ConstantBuffers in bytes. Bei ConstantBuffern für einzelne Parameter ist das immer 0. Das parameters- und das offsets-Array muss immer gleich lang sein.

Die ConstantBuffer, die jeweils vom Pixel- und vom Vertex-Shader benutzt werden, müssen alle im constantBuffers-Array der jeweiligen BeginShader-Direktive angegeben werden.

Echte Matrizen

Der Patch für MonoGame-SDL2 unterstützt auch die Deklaration von echten Matrizen. Der dafür nötige Code ist minimal, es wurde nur offenbar nie gebraucht, da MojoShader nur vec4's verwendet hat.

Für alle Uniform-Variablen, die keine Arrays sind und entweder den Typ mat4 oder vec4 haben, werden automatisch intern die richtigen EffectParameter- und ConstantBuffer-Direktiven gewählt. Auch Texture-Sampler vom Typ sampler2D werden automatisch richtig zugeordnet.

Damit bleibt außer den relativ offensichtlichen BeginShader/EndShader- und EffectPass/EffectTechnique-Direktiven nur noch eine Direktive übrig, die spezifiziert werden muss: Attribute. Uniform-Variablen gelten immer global, also für Vertex- und Pixelshader gleichermaßen. Im Gegensatz dazu werden die Attribute immer innerhalb eines BeginShader/EndShader-Blocks angegeben und gelten nur für den jeweiligen einzelnen Shader.

Beispiel:


#monogame BeginShader (stage=pixel)
#monogame Attribute (name=fragNormal; usage=Normal; index=0)
#monogame Attribute (name=fragTexCoord; usage=TextureCoordinate; index=0)
#version 130

uniform sampler2D ModelTexture;
in vec4 fragNormal;
in vec4 fragTexCoord;
out vec4 fragColor;

void main ()
{
    vec4 color = texture2D (ModelTexture, fragTexCoord.xy);
    color.w = 1.0;
    fragColor = color;
}

#monogame EndShader ()

#monogame BeginShader (stage=vertex)
#monogame Attribute (name=inputPosition; usage=Position; index=0)
#monogame Attribute (name=inputNormal; usage=Normal; index=0)
#monogame Attribute (name=inputTexCoord; usage=TextureCoordinate; index=0)
#version 130

uniform mat4 World;
uniform mat4 View;
uniform mat4 Projection;
uniform mat4 WorldInverseTranspose;

in vec4 inputPosition;
in vec4 inputNormal;
in vec4 inputTexCoord;
out vec4 fragNormal;
out vec4 fragTexCoord;

void main ()
{
    gl_Position = inputPosition * World * View * Projection;
    fragNormal.xyz = normalize (inputNormal * WorldInverseTranspose).xyz;
    fragTexCoord.xy = inputTexCoord.xy;
}

#monogame EndShader ()

#monogame EffectPass (name=Pass1; vertexShader=1; pixelShader=0)
#monogame EffectTechnique (name=Textured)

Im Vertex-Shader wird über die eingebaute Variable gl_Position eine Position ausgegeben, über fragNormal und fragTexCoord jeweils eine Normale und eine Texturkoordinate. Diese werden im Pixel-Shader wieder als Eingabeparameter erwartet:

#monogame Attribute (name=fragNormal; usage=Normal; index=0)
#monogame Attribute (name=fragTexCoord; usage=TextureCoordinate; index=0)

Falls es mehr als eine Variable mit der selben usage-Angabe gibt, muss der Wert unter index für jede weitere Variable inkrementiert werden.

Dieser Shader unterstützt im Gegensatz zu den Beispielen weiter oben die Anwendung einer Textur unter dem Uniform-Namen ModelTexture. Die Parameter können im C#-Code wie folgt festgelegt werden (die gleiche API wie in XNA):

Matrix worldMatrix = ...;
Matrix viewMatrix = ...;
Matrix projectionMatrix = ...;
Texture2D texture = ...;
effect.Parameters ["World"].SetValue (worldMatrix);
effect.Parameters ["View"].SetValue (viewMatrix);
effect.Parameters ["Projection"].SetValue (projectionMatrix);
effect.Parameters ["WorldInverseTranspose"].SetValue (Matrix.Transpose (Matrix.Invert (worldMatrix)));
effect.Parameters ["ModelTexture"].SetValue (texture);

Das automatische Erkennen der EffectParameter und ConstantBuffer setzt mindestens OpenGL 3.0 voraus. Als GLSL-Version muss also mindestens #version 130 angegeben sein.