Friday, February 17, 2012

Game Elements 4: Intro to Shaders

In this tutorial I will be talking about pixel shaders, with examples in C#/XNA4 and HLSL. Shaders are a powerful tool that you can use to make your games really stand out. Many game developers using XNA that I have talked do not know much about shaders and thus do not use them. However, writing simple shaders is actually really easy and can add eye catching effects to your games without a major preformance hit.

A pixel shader is basically a function that you want to run on each pixel of an image. If you try doing this in code you will find it really slow. To improve performance shaders are run on the graphics hardware which is specially designed for quick repetitive operations.

Writing simple shaders is not that difficult but it can get a bit tricky when you try doing more complicated operations, but in this tutorial I will stay pretty simple. Shaders are written in a shader language called HLSL (high level shader language). HLSL looks similar to C# but has some differences, most important are that HLSL has a much more limited selection of functions, mostly just ones for math. Also, because HLSL is trying to be fast it has a lot more in common with C, for example all arrays in fixed size. Since we are only writing simple functions in HLSL these should not be too restricting, but they are good to know and a good function reference can be found in the official documentation.

Example Shaders

The first shader that we are going to write will replace the colors with shades of gray. To start we need to create an effect file. XNA treats shaders like any other type of content, so to create an effect file right click on a folder in your content project in visual studio and click "Add->New Item" then select "Effect File". A new effect file will be generated with place holder functions, most of which is unnecessary for our simple shader. Replace everything in your effect file with the following:

sampler TextureSampler : register(s0);

float4 GrayScalePS(float2 texCoord : TEXCOORD0) : COLOR0 {
    float4 tex = tex2D(TextureSampler, texCoord);

 float intensity = tex.r*0.3 + tex.g*0.59 + tex.b*0.11;
 tex.rgb = float3(intensity, intensity, intensity);
    
    return tex;
}

technique Vignette {
    pass Pass1 {
        PixelShader = compile ps_2_0 GrayScalePS();
    }
}

The important part is in the GrayScalePS function, which takes as texture coordinate as an input. We next use a texture sampler to get the color at that texture coordinate. Then take the weighted sum of the three color components of the texture to get the intensity. The weights determine the luminosity of the image and by adjusting these values you can change how each color component contributes to the brightness. These values though are the most commonly used since they provide a good contrast between different colors. For those that have a better understanding on math you might notice that what we are doing here is actually a dot product, so we can simplify these two lines into one and get the same result:

tex.rgb = dot(tex.rgb, float3(0.3, 0.59, 0.11));

Now to apply this shader to your image go to your game code to the area where you are loading your content. Put in this line to load your effect file, the same way you would load a texture:

Effect MyEffect = Game.Content.Load("Effects/MyEffect");

Next, go to where you are drawing your image. In the sprite batch begin method there is an argument for imputing a shader.

spritebatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullCounterClockwise, MyEffect, Matrix.Identity);

Now everything that is drawn between the sprite batch begin and end calls will have this shader applied to it. If you run the game now you will see that everything is gray and colorless.


Congratulations, you have written your first shader. Its not a very complicated one but with slight modifications you could reuse it to tint the screen a certain color, invert colors, or make a sepia tone effect.

No comments:

Post a Comment