SunBurn & HLSL Shaders

SunBurn’s inbuilt lighting, shadows, HDR, bloom & materials are all fantastic and for most  implementations will do everything you need for a beautifully rendered 3D scene; while successfully hiding the complexities that they are providing in the platform. Having said that, there are times when you want to do something that isn’t already covered in one of the existing implementations of Lighting, Shadows or Materials and when you find this boundary then you’ll need to look at HLSL.

HLSL (High Level Shader Language) is the Direct X language used for talking to the GPU. It’s worth noting that Open GL have their own different implementation, I’ve not looked in to it personally so I’m not sure how translatable those shaders are.

Although HLSL is another language, it’s not going to look to dissimilar to the syntax of C# – it’s different but close enough to still feel familiar.

Synapse Gaming have provided a great example implementation of a base shader, in their Custom Effects Sample Project. It demonstrates how to write a shader that uses a blend map and multiple textures to come up with a composite material for the object. We will use this shader as a base for the next shader we are going to write, so go grab a copy of the sample project if you don’t already have it.

Because HLSL is a different language (or because it was to much work) there is no Intelisense support in Visual Studio for HLSL. I’ve become so accustomed to Syntax Highlighting over the years that I can’t stand to look at black and white text in an editor. As such, before we get started, I recommend you either find a plugin for Visual Studio that will give you HLSL Syntax highlighting or you do what I do and use Shazzam – http://shazzam-tool.com/

Shazzam is a HLSL pixel shader editor, released for free under the MS-PL licence. It was written for WPF applications but the syntax and implementation is practically the same. It great for trying out new pixel shading ideas; it won’t help you with Vertex transforms though.

Shader Says Hello

Lets get a simple and somewhat useful shader going to introduce the concepts you’ll need for shader development. For this we will need to take a momentary dive in to the bowls of Shaders.

Every XNA game uses shaders, whether the programmer knows it or not. In the instances where some one will argue this point, they aren’t aware that the XNA team built a set of really simple shaders to make our collective lives easier. These inbuilt shaders reduce the learning curve and allow you to reach out when you need to.

Shaders work by altering the data that’s going to be rendered. First there is a Vertex shader; it’s job is to worry about where vertices in your model are and where they should be rendered. If your not adjusting the geometry of your model when it’s being rendered, this section will pretty much remain the same for every shader you implement.

After the Vertex shader pass has been completed, a set of co-ordinates are provided to the pixel shader as input for it’s pass. The pixel shader is very powerful and allows you to achieve a range of creative outcomes because it gives you so much power over the GPU.

The Pixel Shader is called for every pixel on your screen, where the model is going to be rendered. In it’s simplest form it allows you to determine the colour and transparency of the pixel that’s going to be drawn to the screen.

To define a pixel shader function in the HLSL .fx file, we need to define a new Pixel Shader function like so:

float4 SimgleTexturePS(ShaderLink input) : COLOR
{
      float4 diffuse = float4(1, 0, 0,1);
      return diffuse;
}

This pixel shader doesn’t do much just yet, other than turn the material of that model, using this shader, red (& non-transparent).

HLSL uses a float4 to represent every pixel in the format of Red, Green, Blue, Alpha. With values of 0 to 1. 1 being that colour on full, 0 being the absence of that colour. The same is true for the Alpha, 1 being that the pixel isn’t transparent and 0 being that the pixel is completely transparent.

As the pixel shader is called for each pixel, the return value from the pixel shader will be a single Pixel’s float4 value, which the GPU then uses to colour the pixel.

These 4 lines by them self won’t work, with out a supporting structure.

How did you get my number?

Although normally positioned at the end of a shader file, the technique instructs the shader in the order of operations and passes which will be made. The following technique would be used to support our shader:

technique SingleTexture_Technique
{
   pass P0
   {
        VertexShader = compile vs_3_0 SingleTextureVS();
        PixelShader  = compile ps_3_0 SimgleTexturePS();
   }
}

The entry point for the Shader is the technique defined.

As you can see, the second function called is the PixelShader, the first is the VertexShader. What you don’t see here is an out put being taken from the result of the VertexShader and being passed in to the PixelShader but that’s what’s happening via the struct called ShaderLink (see the parameters for SingleTexturePS above).

The scope of variable accessibility is the same in HLSL as in C# – variables declared outside of a function will be accessible for the entire class. ShadeLink is an example of one of these structs, declared outside of a function, with accessibility anywhere in the shader.

struct ShaderLink
{
      float4 position    : POSITION;
      float2 diffuseCoord  : TEXCOORD0;
      float4 viewPosition : TEXCOORD1;
      float3 worldNormal  : TEXCOORD2;
};

If your not going to alter the geometry of the object that’s being rendered, we don’t need to change any of this. It does need to get setup so that it can be passed in to the pixel shader. I’ve not personally tried to reduce all of this down to find the very bear minimum of what SunBurn will support but personally, I like the shadows and lighting calculations and as such, I would recommend that you use the support structure and get the benefit from the lighting calculations that are already provided. (I get the irony that this inital tutorial for the pixel shader is just colouring the material a single colour regardless of the lighting calculations – but we will come to this bit latter. Follow along some more.)

The ShaderLink needs to have some actual values passed in to it at some point, so that it’s usable by the pixel shader.

It’s the way I’m shaped

As we have seen the Pixel Shader and the code for the Technique, the only component we haven’t reviewed is the Vertex Shader; let’s take a look.

ShaderLink SingleTextureVS(InputData input)
{
    ShaderLink output;
    // calculate data for the pixel shader.
    output.position = mul(input.position, _World);
    output.position = mul(output.position, _View);
    output.position = mul(output.position, _Projection);
    output.worldNormal = mul(input.normal, (float3x3)_World);
    output.viewPosition = output.position;
    output.diffuseCoord = input.diffuseCoord;
    return output;
}

Honestly, for a pixel shader calculation, it’s unlikely you will need to change any of this. As such we are going to just leave the code here for completeness and breeze over it a bit.

It is worth point out the definition of ouput, based on the ShaderLink struct. As you can see it is what is returned from this function, which is funnily enough, the same return type.

The other item we need to explore is the variable, input, which has a type of InputData;

struct InputData
{
    float4 position    : POSITION;
    float4 normal    : NORMAL;
    float2 diffuseCoord  : TEXCOORD0;
};

This is standard data which is being passed in to the shader, it’s required to have useful shaders 🙂

There is more data that’s automatically passed in:

// main scene transforms – bound as common semantics to
// automatically receive data from SunBurn.
float4x4 _World : WORLD;
float4x4 _View : VIEW;
float4x4 _Projection : PROJECTION;

The following items are for lighting are also passed in to the Shader:

// variables that control lighting – bound to SAS lighting
// addresses (note: this shader only supports ambient and
// directional lighting).

float3 AmbientLightColor
<
    string SasBindAddress = “Sas.AmbientLight[0].Color”;
>;

float3 DirectionalLightColor
<
    string SasBindAddress = “Sas.DirectionalLight[0].Color”;
>;

float3 DirectionalLightDirection
<
    string SasBindAddress = “Sas.DirectionalLight[0].Direction”;
>;

Since we are still relying on the SunBurn implementation for lighting (and not changing any of the actual lighting calculations) we are just going to leave these as-is.

For those who like to cut and paste, the following is the complete code so far.

ColourModelRed.fx – Shader for SunBurn:

All of the following code, other than the single line setting this to red, is courtesy of Synapse Gaming; so I’ve included their copyright notice for completeness. I seriously don’t know why there is a question mark in it..

//———————————————–
// Synapse Gaming – SunBurn Lighting System
// Copyright ? Synapse Gaming 2008
//———————————————–


// main scene transforms – bound as common semantics to
// automatically receive data from SunBurn.
float4x4 _World : WORLD;
float4x4 _View : VIEW;
float4x4 _Projection : PROJECTION;

// variables that control lighting – bound to SAS lighting
// addresses (note: this shader only supports ambient and
// directional lighting).

float3 AmbientLightColor
<
    string SasBindAddress = “Sas.AmbientLight[0].Color”;
>;

float3 DirectionalLightColor
<
    string SasBindAddress = “Sas.DirectionalLight[0].Color”;
>;

float3 DirectionalLightDirection
<
    string SasBindAddress = “Sas.DirectionalLight[0].Direction”;
>;

struct InputData
{
    float4 position : POSITION;
    float4 normal : NORMAL;
    float2 diffuseCoord : TEXCOORD0;
};

struct ShaderLink
{
    float4 position : POSITION;
    float2 diffuseCoord : TEXCOORD0;
    float4 viewPosition : TEXCOORD1;
    float3 worldNormal : TEXCOORD2;
};

ShaderLink SingleTextureVS(InputData input)
{
    ShaderLink output;
    // calculate data for the pixel shader.
    output.position = mul(input.position, _World);
    output.position = mul(output.position, _View);
    output.position = mul(output.position, _Projection);
    output.worldNormal = mul(input.normal, (float3x3)_World);
    output.viewPosition = output.position;
    output.diffuseCoord = input.diffuseCoord;
    return output;
}

float4 SimgleTexturePS(ShaderLink input) : COLOR
{
    float4 diffuse = float4(1, 0, 0,1);
    return diffuse;
}

technique SingleTexture_Technique
{
    pass P0
    {
        VertexShader = compile vs_3_0 SingleTextureVS();
        PixelShader = compile ps_3_0 SimgleTexturePS();
    }
}

 

User, Please!

To use the shader above, copy the code, go to Visual Studio and in your content project create a new FX file:

image

Paste in the code to this new shader file and then run your program.

Lets assume you have a non textured cube like this to start with:

nontexturedcube

Hit F11, then in the editor right click on the Material, select Convert Effect To & then chose the new ColourModelRed.fx file:

ConvertToShader

You should then see your shader in action, in a picture similar to this:

redcustomeffect

Turn On the Lights

Now that we have built a Shader, that is self illuminating and doesn’t take in to account any lighting from SunBurn; let’s add in the lighting calculations that will shadows to our objects and use the lighting from ambient and directional sources (Just for clarification – directional sources don’t include spot lights or point lights).

As we have setup all of the framework for the lighting variables to be passed in; we only need to update the Pixel Shader component with the following:

// apply basic ambient and directional lighting
// based on the light colours, directional light
// direction, and surface normal.

float3 surfacenormal = normalize(input.worldNormal);
float3 lighting = saturate(dot(surfacenormal, -DirectionalLightDirection)) * DirectionalLightColor + AmbientLightColor;
diffuse.xyz *= lighting.xyz;

This code, takes in to account the lighting data that’s been passed, the calculations from the vertex function for its direction and orientation of each pixel and then adjusts the colour of the pixel. I, personally, don’t understand all the maths that sits behind this lighting calculation (and honestly, I don’t any time soon intend to go and learn about it either) – this is taken care of for me by SunBurn and I’m happy with this segregation of duties.

In fact, this is so awesome and takes care of it so well, just past this code directly above the return component, such that you calculate the colour of the pixel to what you want (a float 4 value called diffuse) and then this calculation makes the required calculations for it to then be shadowed and lighted correctly.

The full function now looks like this:

float4 SimgleTexturePS(ShaderLink input) : COLOR
{
    float4 diffuse = float4(1, 0, 0,1);
    // apply basic ambient and directional lighting
    // based on the light colours, directional light
    // direction, and surface normal.

    float3 surfacenormal = normalize(input.worldNormal);
    float3 lighting = saturate(dot(surfacenormal, –
    DirectionalLightDirection)) * DirectionalLightColor +
    AmbientLightColor;
    diffuse.xyz *= lighting.xyz;

    return diffuse;
}

So, now we have a red coloured pixel, which has lighting and shadows applied. We probably wont win any awards with this, so lets use a texture to map on to the object.

When you run the program with this updated shader, it might look something like this:

cubewithlighting

UV is an in Joke for Lighting Nerds

Quick recap, we have the standard structure for a SunBurn shader, we are able to colour a pixel a single colour (which means that where ever that texture is applied, it becomes that single colour – self illuminating) we are also able to then apply shadows to the location of that pixel, from ambient and directional lighting.

Most games though, use textures – because hardly anything is ever a single colour.

To use a texture, we need to get a reference to the texture we want to use passed in to the shader. This is achieved through creating a reference that we can update through the editor. Start by defining the variable to store the texture & then create the link.

// Reference that will be visible in the SunBurn editor
texture2D Diffuse1Texture;

// Load the Texture from the editor in to Diffuse1Sampler
sampler Diffuse1Sampler = sampler_state
{
    Texture = <Diffuse1Texture>;
};

This allows us sample from the texture to work out what pixel should be used from the texture for every given location that’s being rendered to the screen.

Let’s take a quick break to think about this pipeline and how much is going on under the hood.

Model Created > Vertex co-ordinates unwrapped to UV’s on texture > Texture created > Model and Texture loaded through content pipeline > Model placed in world location > View port used to constrain what’s going to be rendered > Scene passed to render > For each material on a model > Models location and position passed to shader with details of lighting > Texture sampler used to look up pixel to be rendered, based on the UV coordinates passed in (repeat for every pixel on the screen being rendered for a model)

Even this is an oversimplified course of events.

To get the correct pixel from the texture, for every given location, we need to use the UV co-ordinates for the given location. Lucky for us, we don’t really need to know all this & thanks to the framework that we setup earlier we can easily just reference it, like so:

diffuse = tex2D(Diffuse1Sampler, input.diffuseCoord);

Just add that in to the SimgleTexturePS function, after you define the diffuse variable. Now each pixel will be sampled from the texture that you set in the editor. As the lighting is applied to the pixel after this in the code we have previously setup, that’s all you need to do to sample a texture in to the shader.

Run the program with these updated changes and you will see something that feels a bit disappointing:

UnTexturedBlack

Hit F11, open the editor and navigate to the custom effects tab and you should see a new option called Diffuse1 Texture, like so:

beforeImage

Then drag one of the textures from the texture browser over and you should see something that brings it all together:

TexturedCube

Awesome.

Blending Textures

So now we have a Shader, that is effectively a subset of support, from what SunBurn give you out of the box. Lets have a look at how to blend two textures together.

Add in a new Texture Sampler at the top of the Shader:

// Reference that will be visible in the SunBurn editor
texture2D Diffuse2Texture;

// Load the Texture from the editor in to Diffuse1Sampler
sampler Diffuse2Sampler = sampler_state
{
    Texture = <Diffuse2Texture>;
};

In the texture shader function, after the first texture sample, add in the following line:

diffuse = (diffuse * 0.5) + (tex2D(Diffuse2Sampler, input.diffuseCoord) * 0.5);

This line does a couple of things, first we multiple the variable diffuse (which is a float4) by 0.5. This actually multiples every float in the float4 variable by 0.5, so if we originally had a pixel of red (1,0,0,1) it would now become (0.5,0,0,0.5). Notice that the Alpha value is also reduced.

Next we sample the second texture and then also half the float4 values. The result is then added to the result of the first multiplication (which was the first texture). In this way we have take a 2 pixels, from 2 separate textures, divided them by half and added them together. This is blending in it’s simplest form.

Go in to the editor and add another texture to see the result, or just trust me an look at this:

BlendTexture

Go Forth & Shade

This chapter has introduced the concepts of Shaders and setup a framework for the pixel shader that you can extend, more to come..