UPDATE: be sure to check out my YouTube channel where you can see live-action of content such as this article as well is other subjects.
By using a ModelProcessor custom content pipeline processor you can import 3D assets and automatically have the XNA Model class linked to the appropriate textures and HLSL FX code. No more individual code for loading effects, models and no more horrid manual mapping (or should I say re-mapping) of effects to models mesh parts.
LoadContent() now looks as clean as:
_stoneyBox = Content.Load<Model>(“models/stoneybox”);
So How is This Done?
In the case of FBX file formats, there is a wonderful amount of metadata that is sadly being ignored by our old friend BasicEffect and the standard model import pipeline. By doing some of our own magic we can access this data and automatically connect shaders, textures and models together.
Add your model to your Content folder, this should be a FBX, at this stage X does not not seem to have all the smarts in it.
Right-click your Solution and choose Add.New Project…
Then in the appearing window, select Content Pipeline Extension Library and do the usual filename stuff, adding it to your solution.
This will create a new project with a default class. Delete what is there and replace with something like the following.
[ContentProcessor(DisplayName = “Custom Effect Model Processor – MickyD”)]
public class CustomEffectModelProcessor : ModelProcessor
{
protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context)
{
EffectMaterialContent myMaterial = new EffectMaterialContent();
if (material is BasicMaterialContent)
{
Log(context, “Material is basic”);
// do appropriate basic stuff here
}
else if (material is EffectMaterialContent)
{
EffectMaterialContent effectMaterialContent = (EffectMaterialContent) material;
//
// remap effect
//
myMaterial.Effect = new ExternalReference<EffectContent>(effectMaterialContent.Effect.Filename);
// textures
foreach (KeyValuePair<string, ExternalReference<TextureContent>> pair in effectMaterialContent.Textures)
{
string textureKey = pair.Key;
ExternalReference<TextureContent> textureContent = pair.Value;
if (!string.IsNullOrEmpty( textureContent.Filename))
{
myMaterial.Textures.Add(textureKey, material.Textures[textureKey]);
Log(context, “Set texture ‘{0}’ = {1}”, textureKey, textureContent.Filename);
}
}
}
return base.ConvertMaterial(myMaterial, context);
}
Compile your solution making sure everything is fine. OK, now we have a custom pipeline in the solution but nothing is actually using it yet. We do have that FBX we added before, wouldn’t it be schmick to have it use the pipeline?
Well first we need to add a reference to the pipeline project to our main project. Once done we get a cool new drop-down option on model’s Content Processor property. In your main project, right-click Content (NOTE it’s the Content references we want NOT project references), choose Add reference and add a reference to your pipeline project on the Projects tab.
If all goes well it should look something like this:
Back in your main project (the one with your Content folder holding your assets anyway), right-click your FBX model to bring up the Properties window. If you look there should be a new choice in the Content Processor field. Fill it out as follows with particular attention to:
- Content Processor = (whatever your custom effect model processor is)
- Generate Tangent Frames = true
- Scale = … (depends on your model)
- Swap Winding Order = true EDIT: actually leave this to false
Now we can load the model in our LoadContent() as follows:
_stoneyBox = Content.Load<Model>(“models/stoneybox”);
Note though that we still need a reference to the effect so that we can pass parameters. (unless of course your shader has none)
_stoneyEffect = _stoneyBox.Meshes[0].Effects[0];
Note this is the complete reverse of the usual tutorials – which load the model AND effect and then re-apply the effect to the model!
In my draw function I do need to pass though the light position. I guess if it wasn’t moving (which it wasn’t) I could just do it during LoadContent(). Oh well.
_stoneyEffect.Parameters[“light1Pos”].SetValue(light1Pos);
Here you can see our friend the cube, but with a:
- diffuse texture map
- normal bump map
- diffuse color
- specular highlights
And here’s a shoot of a few buildings. Each building is a single FBX model with multiple FX (or materials) in XNA (glass, bricks, tar roof)
That’s it. With this technique, there is minimal code required to load your models with the correct textures and effects. What’s more it is only necessary to add your model to your project – not the textures and effects. I like to export my models out of 3DS Max directly into my Content/Models folder. I don’t tell Max to export the textures but rather have it just put the path into the FBX so that the pipeline can load my textures directly out of material library folder at compile time. This works for me, but understandably it may not suit others. That is fine.
EDIT: The draw code for drawing a XNA model with one or more custom effects is as below:
/// <summary>
/// Draws the with custom effect.
/// </summary>
/// <param name = “model”>The model.</param>
/// <param name = “world”>The world.</param>
/// <param name = “viewMatrix”>The view matrix.</param>
/// <param name = “projectionMatrix”>The projection matrix.</param>
/// <param name = “offset”>The offset.</param>
public static void DrawWithCustomEffect(Model model,
Matrix world,
Matrix viewMatrix,
Matrix projectionMatrix,
Vector3 offset)
{
var transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
var translation = Matrix.CreateTranslation(offset);
foreach (var mesh in model.Meshes)
{
foreach (var effect in mesh.Effects)
{
var w = transforms[mesh.ParentBone.Index] * world * translation;
effect.CurrentTechnique = effect.Techniques[“Complete”];
effect.Parameters[“wvp”].SetValue(w * viewMatrix * projectionMatrix);
effect.Parameters[“worldI”].SetValue(Matrix.Invert(w));
effect.Parameters[“worldIT”].SetValue(Matrix.Transpose(Matrix.Invert(w)));
effect.Parameters[“viewInv”].SetValue(Matrix.Invert(viewMatrix));
effect.Parameters[“world”].SetValue(w);
foreach (var pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
mesh.Draw();
}
}
}
}