Wednesday, May 4, 2011

Loading a model with a custom effect

XNA allows us to use complex 3D models very easily, we just use the content pipeline and load a model into the game just like a texture. It will have everything it needs, vertex buffers, index buffers, bones, effects, textures. All we will have to do is set our effect parameters (for each effect in the mesh) and draw it. But by default the Model class uses one of the 5 build-in effects and as  I love shaders, I want to make it run with one of my own.


XNA loads files using the content pipeline.We have content importers and content processors. A content importer will actually read the file from the hard drive and convert it into a intermediate object format. It's the content processor that will actually create the in game objects that we will use. They are separated this way so you can have a single content importer for .fbx models (it makes sense as the file format is the same) but different processors for different types of objects (for example we could do some pre-processing like gamma correction on textures).

Since we just want our model to load a custom effect we don't need to write a completely new content processor, the XNAcontent pipeline is very easy to extend and configure so we will go this route. If you loaded a 3D model and click properties you will notice the content importer and processor it has associated. If you expand the content processor you will see that we can select the type of effect it will use (one of the 5 build-in types):


The fact that we have that property is what's bothering us, we don't need that, we need

The first thing we have to do is create a new project, so right click the solution -> New Project and select Content Pipeline Extension and give it a name. Now select the ContentProcessor1.cs file and rename it to something like CustomModelEffect.cs. Before anything else, to be able to use it you must add a reference to it, so go in the Content project, right click References -> Add Reference and from the Project tab select the project you just created.

Now go into the file and at the top of the file add using System.ComponentModel, also you can just delete the 2 using string statements that are lower:


....
using System.ComponentModel;
 
 
namespace CustomModelEffect
....

The display name of the processor is the one that appears in the drop down in the properties pane, we will need to change that to something more relevant, also, because we what to change the way a model is loaded we will make our processor inherit ModelProcessor:

    [ContentProcessor(DisplayName = "Model (Custom) - Select Effect File")]
    public class CustomModelEffect : ModelProcessor
    {
      ....
    }

This way we can just write the code the load what we are interested in and let the parent class take care of the rest. But for this to work we will have to make our Process method call the parent's Process function on the input. Also we will have to change the Return type of the method to ModelContent and the input to NodeContent so that out method can override the parent one:


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

As stated before, the fact that the model uses one of the 5 build-in effects is of annoyance to us. We don't want it to load that property anymore, for this we will override it and set it's Browsable attribute to false (that's why we needed the ComponentModel namespace):


    [Browsable(false)]
    public override MaterialProcessorDefaultEffect DefaultEffect
    {
        get { return base.DefaultEffect; }
        set { base.DefaultEffect = value; }
    }

We will have to create our own property that will contain the name of the effect file we want to load. We will just add a string property and make it browsable:


    [Browsable(true)]
    public string Effect
    {
        get { return effectName; }
        set { effectName = value; }
    }
    private string effectName;

This way we can set a model to work with our effect file, be careful that this should be the file name (including extension).

The last thing we want to do is override the ConvertMaterial method, in this method we will set our effect. To do this we'll have to create a new EffectMaterialContent (for it to have an effect property) and convert our file name to an external reference (if we didn't specify a valid file name we will throw an error message). Because we are creating a new object we will also need to set all the textures (in case our 3D model has textures). For this we will enumerate throw all the textures in the input material object and set them to our objects. Considering all of the above our ConvertMaterial method will be:


    protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context)
    {
        EffectMaterialContent myMaterial = new EffectMaterialContent();
 
 
        if (!String.IsNullOrWhiteSpace(effectName))
        {
            myMaterial.Effect = new ExternalReference<EffectContent>(effectName);
        }
        else
        {
            throw new ArgumentNullException("Effect File Not Specified!");
        }
 
 
        foreach (KeyValuePair<StringExternalReference<TextureContent>> texture in material.Textures)
        {
            myMaterial.Textures.Add(texture.Key, texture.Value);
        }
 
        return context.Convert<MaterialContentMaterialContent>(myMaterial, typeof(MaterialProcessor).Name);
    }

Each texture is actually a KeyValuePair object composed of a String (key) and ExternalReference (object). The key will be the name of the texture parameter of the effect. The object will be the texture itself. Now we will be able to load a 3D model with any effect you would like.

A simple example of this can be downloaded here.

2 comments:

  1. I just want to thank you a bunch for posting this! I never would have figured it out on my own. Even the Microsoft samples aren't all that clear on how to render an existing model using something other than the BasicEffect (unless it's just a bunch of geometry to which you already know what texture applies). Thanks!

    ReplyDelete
  2. Incredible! Thanks so much for your time!

    ReplyDelete