Effect interfaces in XNA Game Studio 4.0

Originally posted to Shawn Hargreaves Blog on MSDN, Wednesday, May 5, 2010

This article is prerecorded. Shawn is away (on honeymoon). Replies to comments will be delayed.

Typical XNA model drawing code goes something like:

    Matrix[] transforms = new Matrix[model.Bones.Count];

    model.CopyAbsoluteBoneTransformsTo(transforms);

    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.World = transforms[mesh.ParentBone.Index] * world;
            effect.View = view;
            effect.Projection = projection;
        }

        mesh.Draw();
    }

But this only works as long as you limit yourself to BasicEffect. If there are any other types of effect attached to the model, the "foreach (BasicEffect effect in mesh.Effects)" part will throw, in which case you need something like:

    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (Effect effect in mesh.Effects)
        {
            if (effect is BasicEffect)
            {
                // as before
            }
            else
            {
                effect.Parameters["World"].SetValue(transforms[mesh.ParentBone.Index] * world);
                effect.Parameters["View"].SetValue(view);
                effect.Parameters["Projection"].SetValue(projection);
            }
        }

        mesh.Draw();
    }

How to set parameters for a custom effect depends entirely on the effect in question. Many people use naming conventions like the above example. Others use effect annotations to build powerful data binding mechanisms.

When we added SkinnedEffect, EnvironmentMapEffect, DualTextureEffect, and AlphaTestEffect, model drawing code got ugly. Instead of a single "if (effect is BasicEffect)" test, robust implementations had to separately handle each of the five built-in effect types. Yuck.

We fixed this by adding three new interfaces:

    public interface IEffectMatrices
    {
        Matrix World { get; set; }
        Matrix View { get; set; }
        Matrix Projection { get; set; }
    }

    public interface IEffectFog
    {
        bool FogEnabled { get; set; }
        float FogStart { get; set; }
        float FogEnd { get; set; }
        Vector3 FogColor { get; set; }
    }

    public interface IEffectLights
    {
        bool LightingEnabled { get; set; }
        Vector3 AmbientLightColor { get; set; }
        DirectionalLight DirectionalLight0 { get; }
        DirectionalLight DirectionalLight1 { get; }
        DirectionalLight DirectionalLight2 { get; }
        void EnableDefaultLighting();
    }

All five built-in effects implement IEffectMatrices and IEffectFog, while BasicEffect, SkinnedEffect, and EnvironmentMapEffect also implement IEffectLights. This allows models using any combination of these effects to be drawn with a single piece of code:

    Matrix[] transforms = new Matrix[model.Bones.Count];

    model.CopyAbsoluteBoneTransformsTo(transforms);

    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (IEffectMatrices effect in mesh.Effects)
        {
            effect.World = transforms[mesh.ParentBone.Index] * world;
            effect.View = view;
            effect.Projection = projection;
        }

        mesh.Draw();
    }

We also added a new helper method, which does the same thing as the above example, handling the CopyAbsoluteBoneTransformsTo and foreach ModelMesh shenanigans for you:

    model.Draw(world, view, projection);

This only works if all the effects used by the model implement IEffectMatrices. It will throw an exception if there are other custom effects, in which case you still need to use an old style ModelMesh.Draw loop, setting up the custom effects directly yourself.

Blog index   -   Back to my homepage