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.