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.

Advertisements

Content Pipeline – Part 1

I’ve decided to break this up into two posts. The first being the creation of the Content Pipeline project. So, let’s get going. Right mouse click the solution and click Add -> New Project. Select the Content Pipeline Extension Library (4.0) and give it a name. I’ve called mine “ProjectVanquishContentPipeline”:

Once the project has been created, you’ll see a class file called “ContentProcessor1”. Rename this to “ProjectVanquishContentProcessor”. You may see the following window:

If you do, click “Yes”. ┬áIf you have the new class open, your code should look something like:

[ContentProcessor(DisplayName = "ProjectVanquishContentPipeline.ContentProcessor1")]
public class ProjectVanquishContentProcessor : ContentProcessor<TInput, TOutput>
{
    public override TOutput Process(TInput input, ContentProcessorContext context)
    {
        // TODO: process the input object, and return the modified data.
        throw new NotImplementedException();
    }
}

There is one more “ContentProcessor1” to rename in the “DisplayName” property. Just remove the 1.

[ContentProcessor(DisplayName = "ProjectVanquishContentPipeline.ContentProcessor")]

The next step we need to do is add a reference to our new project in our Content project. Right mouse click “ProjectVanquishTestContent” project and click “Add Reference”. If “Projects” isn’t selected, select it and locate your Content Pipeline Extension Library project and click OK.

Build your solution and you should have no errors. In the next part, we’ll add the code. The code won’t differ much from Roy’s version, but we’ll be looking at extending it to allow for Skinned models later on in the project.