Thursday, August 11, 2011

More XNA Cel Shading - Cel Shaded Animated Models using a SkinnedModelProcessor

This short post builds upon my previous article A Cel-Shading Example in XNA 4.0, applying what we did there to a skinned model with animation.

Before you begin
First you will need to complete and understand the Skinned Model tutorial on the MSDN AppHub site http://create.msdn.com/en-US/education/catalog/sample/skinned_model. After that this example is pretty straight forward as long as you understand what digitalerr0r did in his post XNA Shader Programming Tutorial 7, Toon shading and what we in my previous post A Cel-Shading Example in XNA 4.0.

Update CelShader.fx
First we will update out CelShader.fx and call it SkinnedCelShader.fx.  Using the Microsoft stock effects as an example add the following message to get the skinning data from the model.
/* Get the skinning data from each of the bones
 */
void Skin(inout VertexShaderInput vin, uniform int boneCount)
{
    float4x3 skinning = 0;

    [unroll]
    for (int i = 0; i < boneCount; i++)
    {
        skinning += Bones[vin.Indices[i]] * vin.Weights[i];
    }

    vin.Position.xyz = mul(vin.Position, skinning);
    vin.N = mul(vin.N, (float3x3)skinning);
}
And then call Skin from your vertex shader before making any of the other position calculations
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    Skin(input, 4);

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    output.Tex = input.Tex;
    output.L = normalize(LightDirection);
    output.N = normalize(mul(InverseWorld, input.N));

    return output;
}
The game class
The modifications to the game class are pretty simple.  Start with the game class you implemented in the SkinnedEffect tutorial.

Add some class members to hold our new effect data and some parameters we will need to set their properties:
/* CelShader effects and data
 */
Effect celShader; // Toon shader effect
Texture2D celMap; // Texture map for cell shading
Vector4 lightDirection; // Light source for toon shader

Effect outlineShader; // Outline shader effect
float defaultThickness = 1.0f; // default outline thickness
float defaultThreshold = 0.9f; // default edge detection threshold
float outlineThickness = 1.0f; // current outline thickness
float outlineThreshold = 0.9f; // current edge detection threshold
float tStep = 0.01f; // Ammount to step the line thickness by
float hStep = 0.001f; // Ammount to step the threshold by

// Render target for post render outlining
RenderTarget2D celTarget;

Set our light direction in the initialize method:
/* Set our light direction for the cel-shader
 */
lightDirection = new Vector4(2f, 45f, -110f, 1.0f);
And in LoadContent load our custom effects:
// load and initialize our cel shader effect
celShader = Content.Load<Effect>("SkinnedCelShader");
celMap = Content.Load<Texture2D>("celMap");

celShader.Parameters["Projection"].SetValue(proj);
celShader.Parameters["View"].SetValue(view);
celShader.Parameters["LightDirection"].SetValue(lightDirection);
celShader.Parameters["CelMap"].SetValue(celMap);

/* Load and initialize the outline shader effect
 */
outlineShader = Content.Load<Effect>("OutlineShader");
outlineShader.Parameters["Thickness"].SetValue(outlineThickness);
outlineShader.Parameters["Threshold"].SetValue(outlineThreshold);
outlineShader.Parameters["ScreenSize"].SetValue(
new Vector2(GraphicsDevice.Viewport.Bounds.Width, GraphicsDevice.Viewport.Bounds.Height));

/* Set up a render target to draw our cel shaded model to
 * for post render outlining
 */
celTarget = new RenderTarget2D(GraphicsDevice, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height,
    false, SurfaceFormat.Color, DepthFormat.Depth24);

Finally update Draw to draw our shaded model (you will notice this is pretty much identical to the way we drew the cel shaded model in my previous post).  The only trick here is that we need to get the model's texture.  We do not have that directly because its reference is baked into the fbx.  Because our model is loaded with its default effect as SkinnedEffect though we can get it from there with the following line of code:
Texture2D texture = ((SkinnedEffect)meshPart.Effect).Texture;
The full draw method:
protected override void Draw(GameTime gameTime)
{
    /* Set our render target
     */
    GraphicsDevice.SetRenderTarget(celTarget);

    /* Make sure we have a depth stencil for proper
     * depth culling
     */
    GraphicsDevice.DepthStencilState = DepthStencilState.Default;

    /* clear the graphics device with a color with a clear
     * alpha channel.
     */
    Color alpha = Color.White;
    alpha.A = 0;
    GraphicsDevice.Clear(alpha);

    /* Get our model bones from the animation player
     * so that we can pass this to the cel shader effect
     */
    Matrix[] bones = animationPlayer.GetSkinTransforms();

    // for each model in the mesh
    foreach (ModelMesh mesh in model.Meshes)
    {
        // for each mesh part
        foreach (ModelMeshPart meshPart in mesh.MeshParts)
        {
            // Set the vertex buffer and indices in the graphics device
            GraphicsDevice.SetVertexBuffer(meshPart.VertexBuffer,
            meshPart.VertexOffset);
            GraphicsDevice.Indices = meshPart.IndexBuffer;

            /* The texture of a skinned model is available in SkinnedEffect.Texture.
             * We are loading our model with the SkinnedModelProcessor and using
             * SkinnedEffect as the model's default effect. Use that to get the
             * texture of the current mesh part.
             */
            Texture2D texture = ((SkinnedEffect)meshPart.Effect).Texture;

            /* Set up a simple world, no translation, scaling or rotation
             * for this example
             */
            Matrix world = Matrix.Identity;

            /* Set the color map, world, inverse world and bones
             * properties of the cel shader effect
             */
            celShader.Parameters["ColorMap"].SetValue(texture);
            celShader.Parameters["World"].SetValue(world);
            celShader.Parameters["InverseWorld"].SetValue(Matrix.Invert(world));
            celShader.Parameters["Bones"].SetValue(bones);

            // for each effect pass in the cell shader
            foreach (EffectPass pass in celShader.CurrentTechnique.Passes)
            {
                // apply the effect
                pass.Apply();

                // and draw the current mesh part
                GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
                    meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
            }
        }
    }

    /* We are done with the render target so set it back to null.
     * This will get us back to rendering to the default render target
     */
    GraphicsDevice.SetRenderTarget(null);

    /* Clear the device to get ready for more drawing
     */
    GraphicsDevice.Clear(Color.Wheat);

    /* Draw the game model again without cell shading so we can do a side
     * by side comparison
     */
    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (ModelMeshPart meshPart in mesh.MeshParts)
        {
            foreach (SkinnedEffect effect in mesh.Effects)
            {
                effect.SetBoneTransforms(bones);
                effect.View = view;
                effect.Projection = proj;
                effect.EnableDefaultLighting();
                effect.SpecularColor = Vector3.Zero;
            }
            mesh.Draw();
        }
    }

    /* Draw the cel shaded model with outlining
     */
    spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied,
        null, null, null, outlineShader);
    spriteBatch.Draw(celTarget, new Vector2(-175, 0), Color.White);
    spriteBatch.End();

    /* Draw our debug message
     */
    spriteBatch.Begin();
    spriteBatch.DrawString(debugFont, debugMsg, debugLoc, consoleFgColor);
    spriteBatch.End();

    base.Draw(gameTime);
}

Things to watch out for:
After adding your animated model to the content project make sure you change its content processor property to SkinnedModelProcessor and then under set the default effect to SkinnedEffect.  If you miss either of these steps you will get errors.

Here is what the result will look like (cel shaded model on the left compared to a naturally lit model on the right).

You do not have the latest version of Flash installed. Please visit this link to download it: http://www.adobe.com/products/flashplayer/




The full code for this project can be downloaded here.

Good luck!

5 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Nice Tutorial!
    However i have some questions that i hope you could answear :)
    1.How about rendering a model that has no texture? Using color instead.
    2. In your previous post about regulare celshading you used a model of some spiderish thing, i've tried creating my own model and applying a basic texture to it, but what kind of texture should i use to make the m_texture look good? I've tried Aplha 0 texture and a simple white texture, but all turns up as black and i only see some red colors :/
    3. When trying to do a cel shaded animation on my model i get an error when trying to parse the texture to "SkinnedEffect", sais it can't parse since the meshPart.Effect is "BasicEffect"

    Hope you can somewhat help me atleast :)
    Keep up the good work

    ReplyDelete
  3. I will not be able to help too much until after work today but this should help get you started.

    1) Changing the effect to use vertex color instead of a texture isn't too big of a deal. It will mostly involve removing the texture coordinate and reference from the effect and replacing it with vertex color data. I will send you those changes later tonight (PST, GMT -8).

    2) It sounds like you may be having a problem with your UV unwrapping. Make sure you can apply a texture correctly with a basic effect. Try working through my UV Unwrapping tutorial if needed. http://adventureincode.blogspot.com/2011/09/very-quick-blender-uv-unwrapping.html

    3) It sounds like you do not have the correct default effect set for the model. With your solution open in Visual Studio make sure the properties view is open and then select your model in the solution explorer. In the properties view make sure the ContentProcessor is set to SkinnedModelProcessor (or what ever custom processor you are using). Then expand the content processor section and make sure that the default effect is set to SkinnedEffect.

    Hopefully that will help some until I am out of work and can post further.

    John

    ReplyDelete
  4. Thanks for the help!
    I have looked somewhat into changing the shader to vertex color data, however since i'm really new to 3D programing, and escecially making models in blender i came up short on that part. It said i didn't have any color0 in my meshParts, so how exacly do you create a model that has a color?
    As for the animation i missed to change the defualy effect! that fixed it (thou my animation looks really weird, but i think i need to apply some rotations for my bones) And supprisingly the UV mapping looks good on the animated model, but not when trying to use it in combination with m_texture.

    Looking forward to more posts on XNA and 3D programing :)

    ReplyDelete
  5. And just noticed that my static model with m_Texture did look good, just that it was scaled down pretty much and i didn't zoom in and look at it up close. So from a distance it mostly looked black. Now i just need some help on Vertex Color, and how to make a model that uses color instead of textures :)

    ReplyDelete