Inledning
I denna artikel ska vi förklara enklare pixel shaders. Shaders finns i två varianter: pixel och vertex. Dessa används för att skapa extra visuella effekter i renderingen på en modern GPU (Graphics Processing Unit).
Shaders har främst använts vid 3D-rendering, något som vi på csharpskolan ännu inte skrivit mycket om, men de är också användbara i 2D. Säg att du vill skapa en "flashback"-scen i ett spel där någon minns en tidigare händelse. Ett enkelt visuellt knep kan då vara att rita allt i gråskala utom huvudkaraktären. Problemet i 2D är då att du måste göra om all grafik till gråskala!
Inte om du använder shaders!
Ett mer avancerat exempel på en pixel shaders är t.ex. att emulera renderingen för att efterlikna en gammal GameBoy. Originalgrafiken i GameBoy är i gråskala men skärmen ger en speciell grön karakteristik med bieefekter av den svart-vita LCD-panelen. Exempel på avancerad GameBoy shader.
Längst ned i artikeln finns exempelkod att ladda hem och testa. Det krävs dock minst MonoGame 3.5 installlerat för att köra exemplet.
Pixel eller Vertex shader?
Först och främst måste vi veta lite hur grafikkortet jobbar. En GPU jobbar med polygoner. En polygon är 3 eller fler punkter, i realiteten 3 punkter. Punkterna i polygonen kallas vertices (en vertex flera vertices). En polygon med 3 punkter kan också kallas triangel.
Moderna grafikkort (GPU) erbjuder möjligheten att köra egna program som manipulerar renderingen. Dessa program är just shaders.
Vertex shader
Detta är ett program som tar vertices från polygonen och transformerar dem från spelvärlden (3D) till en avbildning på skärmen (2D). Den tillåter också en hel del annan manipulering med texturer och färger/toning på polygonen. Vi kommer inte att fördjupa oss i dessa just nu.
Pixel shader
Detta program kommer in lite senare i renderingsprocessen. När pixlarna ska ritas ut på skärmen så kan man samtidigt manipulera dem. Här kan bl.a. data från vertex shadern användas. I MonoGame kan vi haka på en pixel shader när vi använder SpriteBatch för 2D-uppritning.
protected override void Draw(GameTime gameTime)
{
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied,
SamplerState.PointClamp, DepthStencilState.Default, RasterizerState.CullNone, pixelShader);
spriteBatch.Draw(texture, position, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
Alla shaders i MonoGame laddas som resursen Effect. Sedan MonoGame 3.5 finns det också färdiga templates för att skriva både pixel och vertex shaders.
private Effect effect;
...
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
effect = Content.Load<Effect>("defaultEffect"));
}
Hur skriver man en pixel shader?
Standard shader
Öppna "Content"-filen i MonoGame med PipeLine-verktyget genom att dubbelklicka. Högerklicka och välj "Add"->"New Item". I dialogen väljer du sedan "Sprite Effect" för pixel shader. Du får då en .fx-fil inlagd i din Content. Fx-filen kan du sedan öppna i valfri text-editor och göra ändringar. Glöm bara inte att bygga din Content när du är klar.
Som hjälp får du en "default" shader via mallen. Det denna shader gör är just att se till att pixeln från texturen renderas utan ändring. Dvs. du märker ingen skillnad när du använder denna.
#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
Texture2D SpriteTexture;
sampler2D SpriteTextureSampler = sampler_state
{
Texture = <SpriteTexture>;
};
struct VertexShaderOutput
{
float4 Position : SV_POSITION;
float4 Color : COLOR0;
float2 TextureCoordinates : TEXCOORD0;
};
float4 MainPS(VertexShaderOutput input) : COLOR
{
return tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color;
}
technique SpriteDrawing
{
pass P0
{
PixelShader = compile PS_SHADERMODEL MainPS();
}
};
Språket som används kallas HLSL, eller High Level Shader Language, och togs fram av Microsoft inför DirectX 9. Liknande språk som GSLS (OpenGL Shader Language) används för OpenGL. Vi ska inte fördjupa oss i HLSL språket. De enklare exempel vi kommer att visa kräver inte så djupa kunskaper i HLSL för att förstå.
Gråskala shader
Denna shader beräknar intesiteten för en pixel och ersätter röd, grön och blå färg med värdet av intesiteten. På så vis blir varje pixel i gråskala. Samtidigt reducerar man antalet möjliga färger från 24 bit till 8 bit, dvs. 256 kombinationer.
#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
Texture2D SpriteTexture;
sampler2D SpriteTextureSampler = sampler_state
{
Texture = <SpriteTexture>;
};
struct VertexShaderOutput
{
float4 Position : SV_POSITION;
float4 Color : COLOR0;
float2 TextureCoordinates : TEXCOORD0;
};
float4 MainPS(VertexShaderOutput input) : COLOR
{
float4 color = tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color;
float value = 0.299*color.r + 0.587*color.g + 0.114*color.b;
color.r = value;
color.g = value;
color.b = value;
return color;
}
technique SpriteDrawing
{
pass P0
{
PixelShader = compile PS_SHADERMODEL MainPS();
}
};
GameBoy shader
Detta är ett försök att göra en shader som liknar GameBoy. Färgerna reduceras till 16 skalor i grönt samtidigt som en offset läggs på så att fägerna går från mörkt grönt till ljust. Genom att avrunda intensiteten till ett värde mellan 0 och 15 så reduceras färgerna.
#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
Texture2D SpriteTexture;
sampler2D SpriteTextureSampler = sampler_state
{
Texture = <SpriteTexture>;
};
struct VertexShaderOutput
{
float4 Position : SV_POSITION;
float4 Color : COLOR0;
float2 TextureCoordinates : TEXCOORD0;
};
float4 MainPS(VertexShaderOutput input) : COLOR
{
float4 color = tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color;
float4 f4 = color * float4(0.299f, 0.587f, 0.114f, 1.0f);
int f = (f4.r + f4.g + f4.b - 0.01f)*16;
color.r = 0;
color.g = f / 20.0f + 0.1f;
color.b = 0;
return color;
}
technique SpriteDrawing
{
pass P0
{
PixelShader = compile PS_SHADERMODEL MainPS();
}
};
Avslutning
Pixel shaders kan vara mycket användbara. Det krävs dock en hel del kunskap i språket HLSL samt hur en GPU fungerar. Vidare läsning rekommenderas!
Här nedan finns en kort videogenomgång samt ett exempel som använder alla 3 shaders.