Content Pipeline – Part 2

In part 1 we created our Content Pipeline Extension Library, so we can now focus on the code. Let’s start off by removing the following method:

public override TOutput Process(TInput input, ContentProcessorContext context)

Also, we need to change the inherited type to “ModelProcessor” rather than “ContentProcessor”:

public class ProjectVanquishContentProcessor : ModelProcessor

Ok, the last thing before we can start coding is to add the following namespaces:

using System.Collections;
using System.ComponentModel;
using System.IO;

And declare some new variables:

string directory;
// Normal and Specular Map textures
string normalMapTexture, specularMapTexture;

// These Keys are used to search the Normal and Specular map in the opaque data of the model
// Normal Map Key
string normalMapKey = "NormalMap";
// Specular Map Key
string specularMapKey = "SpecularMap";

// Create a List of Acceptable Vertex Channel Names
static IList acceptableVertexChannelNames = new string[]
{
    VertexChannelNames.TextureCoordinate(0),
    VertexChannelNames.Normal(0),
    VertexChannelNames.Binormal(0),
    VertexChannelNames.Tangent(0),
};

We will expand on the Vertex Channel Names when we come to look at adding Skinned models. We’ll continue with the code and we look at properties next. These properties will be used in Visual Studio (as shown below):

The first property we need to create is to override “GenerateTangentFrames”:

[Browsable(false)]
public override bool GenerateTangentFrames
{
    get { return true; }
    set { }
}

Next, we’ll create the properties for the Normal and Specular Keys.

[DisplayName("Normal Map Key")]
[Description("This will be the key that will be used to search the Normal Map in the Opaque data of the model")]
[DefaultValue("NormalMap")]
public string NormalMapKey
{
    get { return normalMapKey; }
    set { normalMapKey = value; }
}

[DisplayName("Specular Map Key")]
[Description("This will be the key that will be used to search the Specular Map in the Opaque data of the model")]
[DefaultValue("SpecularMap")]
public string SpecularMapKey
{
    get { return specularMapKey; }
    set { specularMapKey = value; }
}

Let’s look over that code. We are defining 3 attributes for the property. These are pretty much self explanatory. The “DisplayName” relates to the left hand column in the “Properties” window, whilst the “Description” is what is displayed at the base of the “Properties” Window. The third property is defining a default value for the property.

Next, we’ll create the Normal and Specular Map Texture properties:

[DisplayName("Normal Map Texture")]
[Description("If set, this file will be used as the Normal Map on the model, overriding anything found in the Opaque data.")]
[DefaultValue("")]
public string NormalMapTexture
{
    get { return normalMapTexture; }
    set { normalMapTexture = value; }
}

[DisplayName("Specular Map Texture")]
[Description("If set, this file will be used as the Specular Map on the model, overriding anything found in the Opaque data.")]
[DefaultValue("")]
public string SpecularMapTexture
{
    get { return specularMapTexture; }
    set { specularMapTexture = value; }
}

Again, the same principals apply to the previously defined properties. We are defining a display name and description and this time we aren’t setting a default value. This will be done in the code itself. We need to create 3 methods, well, 1 new method and override two methods that are in the “ModelProcessor” class. We’ll start with the overrides first:

protected override void ProcessVertexChannel(GeometryContent geometry, int vertexChannelIndex, ContentProcessorContext context)
{
    string vertexChannelName = geometry.Vertices.Channels[vertexChannelIndex].Name;

    // If this vertex channel has an acceptable names, process it as normal.
    if (acceptableVertexChannelNames.Contains(vertexChannelName))
        base.ProcessVertexChannel(geometry, vertexChannelIndex, context);
    // Otherwise, remove it from the vertex channels; it's just extra data
    // we don't need.
    else
        geometry.Vertices.Channels.Remove(vertexChannelName);
}

This override method, we are checking for acceptable Vertex Channel Names compared to our list and if the Vertex Channel Name exists within our list, we process it as normal, otherwise we remove the Vertex Channel Name.

protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context)
{
    EffectMaterialContent deferredShadingMaterial = new EffectMaterialContent();
    deferredShadingMaterial.Effect = new ExternalReference<EffectContent>("Shaders/RenderGBuffer.fx");

    // Copy the textures in the original material to the new normal mapping
    // material, if they are relevant to our renderer. The
    // LookUpTextures function has added the normal map and specular map
    // textures to the Textures collection, so that will be copied as well.
    foreach (KeyValuePair<String, ExternalReference<TextureContent>> texture in material.Textures)
    {
        if ((texture.Key == "Texture") ||
            (texture.Key == "NormalMap") ||
            (texture.Key == "SpecularMap"))
            deferredShadingMaterial.Textures.Add(texture.Key, texture.Value);
    }

    return context.Convert<MaterialContent, MaterialContent>(deferredShadingMaterial, typeof(MaterialProcessor).Name);
}

public override ModelContent Process(NodeContent input, ContentProcessorContext context)
{
    if (input == null)
        throw new ArgumentNullException("input");

    directory = Path.GetDirectoryName(input.Identity.SourceFilename);
    LookUpTextures(input);
    return base.Process(input, context);
}

In the last snippet of code, you’ll notice that we have referenced a new Shader. This Shader is required in order to render our Color, Normal and Specular textures, combining them together to give us our final output. We will create this Shader in the Shaders folder of the “ProjectVanquishContent” project. Add a new Effect file called “RenderGBuffer” and delete the contents. Add the following:

float4x4 World;
float4x4 View;
float4x4 Projection;
float SpecularIntensity = 0.8f;
float SpecularPower = 0.5f;

These are our parameters for the shader. We’ll need to pass in the World, View and Projection matrices plus we can override the Specular Intensity or Power if we fancy changing them. We’ll define some Sampler States now:

// Define the Color RenderTarget
texture Texture;
sampler diffuseSampler = sampler_state
{
    Texture = (Texture);
    MAGFILTER = LINEAR;
    MINFILTER = LINEAR;
    MIPFILTER = LINEAR;
    AddressU = Wrap;
    AddressV = Wrap;
};

// Define the Normal Map Texture
texture NormalMap;
sampler normalSampler = sampler_state
{
    Texture = (NormalMap);
    MAGFILTER = LINEAR;
    MINFILTER = LINEAR;
    MIPFILTER = LINEAR;
    AddressU = Wrap;
    AddressV = Wrap;
};

// Define the Specular Map Texture
texture SpecularMap;
sampler specularSampler = sampler_state
{
    Texture = (SpecularMap);
    MAGFILTER = LINEAR;
    MINFILTER = LINEAR;
    MIPFILTER = LINEAR;
    AddressU = Wrap;
    AddressV = Wrap;
};

Here we have our 3 Sampler States for the Color, Normal and Specular textures and we configure them to use Linear and Wrap. Let’s carry on with the Shader code and add in our Vertex Shader structs and define our Vertex Shader function:

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
    float3 Binormal : BINORMAL0;
    float3 Tangent : TANGENT0;
    // We will add additional variables when we add Skinned models
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
    float2 Depth : TEXCOORD1;
    float3x3 tangentToWorld : TEXCOORD2;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(float4(input.Position.xyz,1), World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    output.TexCoord = input.TexCoord;
    output.Depth.x = output.Position.z;
    output.Depth.y = output.Position.w;

    // Calculate tangent space to world space matrix using the world space tangent,
    // binormal, and normal as basis vectors
    output.tangentToWorld[0] = mul(input.Tangent, World);
    output.tangentToWorld[1] = mul(input.Binormal, World);
    output.tangentToWorld[2] = mul(input.Normal, World);

    return output;
}

The last thing left to add to the Shader is the Pixel Shader:

struct PixelShaderOutput
{
    half4 Color : COLOR0;
    half4 Normal : COLOR1;
    half4 Depth : COLOR2;
};

PixelShaderOutput PixelShaderFunction(VertexShaderOutput input)
{
    PixelShaderOutput output;
    output.Color = tex2D(diffuseSampler, input.TexCoord);
    
    float4 specularAttributes = tex2D(specularSampler, input.TexCoord);
    // Specular Intensity
    output.Color.a = specularAttributes.r;
    
    // Read the normal from the normal map
    float3 normalFromMap = tex2D(normalSampler, input.TexCoord);
    // Transform to [-1,1]
    normalFromMap = 2.0f * normalFromMap - 1.0f;
    // Transform into world space
    normalFromMap = mul(normalFromMap, input.tangentToWorld);
    // Normalize the result
    normalFromMap = normalize(normalFromMap);
    // Output the normal, in [0,1] space
    output.Normal.rgb = 0.5f * (normalFromMap + 1.0f);

    // Specular Power
    output.Normal.a = specularAttributes.a;
    output.Depth = input.Depth.x / input.Depth.y;

    return output;
}

Excellent. Now we have our Vertex and Pixel Shader functions. We need a Technique in order to call these:

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

That’s it done. We are nearing the end. We need to create our final method back in our “ProjectVanquishContentProcessor” file. The code is lengthy, but commented very well:

void LookUpTextures(NodeContent node)
{
    MeshContent mesh = node as MeshContent;
    if (mesh != null)
    {
        // This will contatin the path to the normal map texture
        string normalMapPath;

        // If the NormalMapTexture property is set, we use that normal map for all meshes in the model.
        // This overrides anything else
        if (!String.IsNullOrEmpty(NormalMapTexture))
            normalMapPath = NormalMapTexture;
        else
            // If NormalMapTexture is not set, we look into the opaque data of the model, 
            // and search for a texture with the key equal to NormalMapKey
            normalMapPath = mesh.OpaqueData.GetValue<string>(NormalMapKey, null);

        // If the NormalMapTexture Property was not used, and the key was not found in the model, than normalMapPath would have the value null.
        if (normalMapPath == null)
        {
            // If a key with the required name is not found, we make a final attempt, 
            // and search, in the same directory as the model, for a texture named 
            // meshname_n.tga, where meshname is the name of a mesh inside the model.
            normalMapPath = Path.Combine(directory, mesh.Name + "_n.tga");
            if (!File.Exists(normalMapPath))
                // If this fails also (that texture does not exist), 
                // then we use a default texture, named null_normal.tga
                normalMapPath = "null_normal.tga";
        }
        else
            normalMapPath = Path.Combine(directory, normalMapPath);

        string specularMapPath;

        // If the SpecularMapTexture property is set, we use it
        if (!String.IsNullOrEmpty(SpecularMapTexture))
            specularMapPath = SpecularMapTexture;
        else
            // If SpecularMapTexture is not set, we look into the opaque data of the model, 
            // and search for a texture with the key equal to specularMapKey
            specularMapPath = mesh.OpaqueData.GetValue<string>(SpecularMapKey, null);

        if (specularMapPath == null)
        {
            // We search, in the same directory as the model, for a texture named 
            // meshname_s.tga
            specularMapPath = Path.Combine(directory, mesh.Name + "_s.tga");
            if (!File.Exists(specularMapPath))
                // If this fails also (that texture does not exist), 
                // then we use a default texture, named null_specular.tga
                specularMapPath = "null_specular.tga";
            }
            else
                specularMapPath = Path.Combine(directory, specularMapPath);

            // Add the keys to the material, so they can be used by the shader
            foreach (GeometryContent geometry in mesh.Geometry)
            {
                // In some .fbx files, the key might be found in the textures collection, but not
                // in the mesh, as we checked above. If this is the case, we need to get it out, and
                // add it with the "NormalMap" key
                if (geometry.Material.Textures.ContainsKey(normalMapKey))
                {
                    ExternalReference<TextureContent> texRef = geometry.Material.Textures[normalMapKey];
                    geometry.Material.Textures.Remove(normalMapKey);
                    geometry.Material.Textures.Add("NormalMap", texRef);
                }
                else
                    geometry.Material.Textures.Add("NormalMap", new ExternalReference<TextureContent>(normalMapPath));

                if (geometry.Material.Textures.ContainsKey(specularMapKey))
                {
                    ExternalReference<TextureContent> texRef = geometry.Material.Textures[specularMapKey];
                    geometry.Material.Textures.Remove(specularMapKey);
                    geometry.Material.Textures.Add("SpecularMap", texRef);
                }
                else
                    geometry.Material.Textures.Add("SpecularMap", new ExternalReference<TextureContent>(specularMapPath));
            }
        }

        // go through all children and apply LookUpTextures recursively
        foreach (NodeContent child in node.Children)
            LookUpTextures(child);
}

That’s it! Firstly, thanks for sticking with it, but we now have a Content Processor for our models. You will notice that we are referencing two files, “null_normal.tga” and “null_specular.tga”. You’ll need to extract these from Roy’s and add them to your “ProjectVanquishContent” project.

In the next part, we’ll start looking at rendering a model.

Adding the Effect files

Let’s jump right in there and start by structuring our Content project. Create two new folders as follows:

If you are unfamiliar with HLSL, Microsoft wrote a brilliant guide about programming in HLSL. And, again, Catalin Zima wrote a good article too. It is definitely worth reading these articles if you are new to HLSL and I’d recommend starting with Catalin’s article first.

Let’s add our first Shader.  Right mouse click the Shaders folder and highlight “Add” and then click “New Item”.  Select “Effect File” and name it “Clear”:

Click the “Add” button and it will create a template Effect file.  Select all of the code in the file and delete it as we won’t need any of it. The purpose of this Effect file is to clear our G-Buffer. Enter the following code:

struct VertexShaderInput
{
    float3 Position : POSITION0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    output.Position = float4(input.Position,1);
    return output;
}

struct PixelShaderOutput
{
    float4 Color : COLOR0;
    float4 Normal : COLOR1;
    float4 Depth : COLOR2;
};

PixelShaderOutput PixelShaderFunction(VertexShaderOutput input)
{
    PixelShaderOutput output;

    // Black color
    output.Color = 0.0f;
    output.Color.a = 0.0f;

    // When transforming 0.5f into [-1,1], we will get 0.0f
    output.Normal.rgb = 0.5f;

    // No specular power
    output.Normal.a = 0.0f;

    // Max depth
    output.Depth = 0.0f;
    return output;
}

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

Excellent, we now have our first shader for our G-Buffer. We won’t hook this up in code just yet as we’ll add the next shader. Again, follow the same process as above and name the new Effect file “Final”. Remove all of the code and enter the following:

texture colorMap;
texture lightMap;
float2 halfPixel;

sampler colorSampler = sampler_state
{
    Texture = (colorMap);
    AddressU = CLAMP;
    AddressV = CLAMP;
    MagFilter = LINEAR;
    MinFilter = LINEAR;
    Mipfilter = LINEAR;
};

sampler lightSampler = sampler_state
{
    Texture = (lightMap);
    AddressU = CLAMP;
    AddressV = CLAMP;
    MagFilter = LINEAR;
    MinFilter = LINEAR;
    Mipfilter = LINEAR;
};

struct VertexShaderInput
{
    float3 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    output.Position = float4(input.Position,1);
    output.TexCoord = input.TexCoord - halfPixel;
    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float3 diffuseColor = tex2D(colorSampler,input.TexCoord).rgb;
    float4 light = tex2D(lightSampler,input.TexCoord);
    float3 diffuseLight = light.rgb;
    float specularLight = light.a;
    return float4((diffuseColor * diffuseLight + specularLight),1);
}

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

This shader is used to combine the Color RenderTarget with the Light RenderTarget to create our final scene. Now, let’s add them to the “DeferredRenderer” class. Declare two new variables:

private Effect clearBufferEffect, finalEffect;

And we’ll instantiate them in the constructor:

public DeferredRenderer(GraphicsDevice device, ContentManager content)
{
    // After the RenderTargets initialize

    clearBufferEffect = content.Load<Effect>("Shaders/Clear");
    finalEffect = content.Load<Effect>("Shaders/Final");
}

Do a quick build to make sure that we don’t have any errors (you shouldn’t ;-)). Now we have instantiated them, we can finally start to use these shaders. Let’s add some more code. Find your ClearGBuffer method and add the following:

public void ClearGBuffer()
{
    clearBufferEffect.CurrentTechnique.Passes[0].Apply();
}

What that line does is apply the current technique from the Clear Effect file. We only have the one pass in our shader file, which is why we use the index 0. If we had multiple passes, we’d need to use a foreach loop in order to iterate through each pass.

Find your CombineGBuffer method and add the following code:

void CombineGBuffer()
{
    finalEffect.Parameters["colorMap"].SetValue(colorRT);
    finalEffect.Parameters["lightMap"].SetValue(lightRT);
    finalEffect.Parameters["halfPixel"].SetValue(halfPixel);
    finalEffect.CurrentTechnique.Passes[0].Apply();
}

In this code we are assigning some parameters which are in our shader file. If you look back at the Final Effect file, declared at the top are:

texture colorMap;
texture lightMap;
float2 halfPixel;

These are the parameters that we are assigning in the CombineGBuffer method. You’ll notice that we’ve not declared a variable for halfPixel, yet we are using it in the CombineGBuffer method. Let’s go back and declare this new variable. Add the following with your other variables:

private Vector2 halfPixel;

Great. We now have to instantiate this variable, so let’s go to the constructor and do this. After your backbufferWidth and backbufferHeight declarations, and before the instantiation of the RenderTargets, add in the following snippet:

halfPixel = new Vector2()
{
    X = 0.5f / (float)backbufferWidth,
    Y = 0.5f / (float)backbufferHeight
};

Building the solution won’t give you any errors, but trying to run the application will possibly bring a few to the table. Before we run the application, we need to make sure that we are using the Hi-Def profile rather than the Reach profile. We need to do this for both projects, so right click the ProjectVanquish library project and click “Properties”. If the Hi-Def profile isn’t selected, select it:

Do the same for the ProjectVanquishTest project and then rebuild the solution. Once built, run the application. Your code is now using your new shaders and you should see something like this:

This is a good place to end this part. In the next part, we’ll continue by adding a Quad Renderer.