Home > TutorialsMeshes and I/O
3.2. Animation 
In this tutorial you will learn how to import an animation made in blender to BetaCell.



Click here to go to the forum discussion of this tutorial


 

3.2.1. Prerequisites
default.jpg
Before you start this tutorial make sure that you have:
  • Installed XNA Game Studio 3.1
  • Downloaded the starter project from the downloads section
  • Successfully run the starter project
  • For a better understanding, is preferred to have finished the previous tutorials.
  • If you want to obtain the mesh from blender, a blender installation with a version equal or greater than 2.45 is required; if you just want to load the mesh, it's a game asset of the tutorial. 
 


3.2.2. Game Assets 
 

3.2.3. The using statements
Besides the using statements from the starter project, this tutorial needs the following using statements:

using BetaCell.Environment.Animation;
using BetaCell.Environment.Mesh;
using BetaCell.Environment.Light;
using BetaCell.Environment.Mesh.Content;
using BetaCell.Environment;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Dynamic.Skinned
;

 

3.2.4. Game Attributes 
The attributes that come with the starter project are explained in tutorial section 1.1.4.

For this tutorial we are going to be needing the following attributes:

BCMesh evilMesh;

BCAnimationSetBase animationSet;

BCAnimationBase idle;
BCAnimationBase run;
BCAnimationBase iddleToRun;
BCAnimationBase runToIdle;

BCAnimationController controller;

BCAddSequenceListener animationSequence;

double timeToChange = 5;
double lastTime = 0;
bool isIdle = true;


 

3.2.5. Importing Animations from Blender 

BetaCell can be extended to use any skinned animation format, but out of the box, it supports the bca (beta cell animation) format. This is a new format and it's not supposed to replace or even compete with the already popular formats out there, it's just that the current formats have some undesired complications like:
  • The free ones have no visible owner, this allowed different parties to create extensions over the time to a point where writing an importer for them is very difficult.
  • The proprietary ones have a closed definition so writing an importer for them requires reverse engineering from the mesh files which is difficult.
  • Some formats store information that's not commonly used in a BetaCell program, this makes them complicated and writing an importer for them is also very difficult. 
The purpose of the BCA file format is to be an intermediary between popular 3D content creation tools and BetaCell, nothing more. In this tutorial you will load an animated character in blender, export the mesh info to a BCM file, export the animation info to a BCA file and finally load both the BCM and the BCA in BetaCell. If you want to skip the blender part of the tutorial, you can just download the BCM, BCA and texture files from the game assets section, and skip to 3.2.6.

default.jpg
First download the evilAnimatedTexturedB.blend file and double click it; you should see the different animations in the middle right part of the blender window; they are shown in the figure. As you can see, you can choose the animation of the character in the drop down shown in the figure; try selecting each of the different four animations, one by one, to see what happens.

There are four animations, Idle is the character just standing, Run is the character running, the other two are just transitions from one animation to the other, that's because passing from idle to run abruptly looks unreal and breaks the animation effect.

The BetaCell exporter exports both the animation and the mesh, creating the bca and the bcm files; for the exporter to do that, YOU MUST SELECT BOTH THE MESH AND THE SKELETON ONLY.

default.jpg
So first unselect everything with the A key, then right click the skeleton, it should turn pink, now, press and hold the shift key and right click the mesh; at this point both the skeleton and the mesh should be pink, IF ANY OF THEM IS NOT PINK OR IF ANY OTHER OBJECT BESIDES THE SKELETON AND THE MESH IS SELECTED (PINK) , THE DESIRED RESULT WONT BE ACHIEVED.

Now, once you have the mesh and the skeleton selected, export them [3.1.5]. You will need to add the exported meshes to the gameAssets folder as shown in 2.3.2.

 

Advertisement
 

3.2.6. The Initialize method 

The code that loads the mesh was shown in 3.1.6.

Now we are going to load the animation to the character. First we need to get the animation data from the BCA file:

animationSet = content.Load<BCAnimationSetBase>(
                "Content/gameAssets/evilAnimatedTexturedBAnim");


After that we need to get the four different animations out of it and store them in the game attributes: 

animationSet.animations.TryGetValue("Idle", out idle);
animationSet.animations.TryGetValue("Run", out run);
animationSet.animations.TryGetValue("IdleToRun", out iddleToRun);
animationSet.animations.TryGetValue("RunToIdle", out runToIdle);

Now, we need an entity that knows how to keep an eye on the animations of a mesh; this class is the BetaCell.Environment.Animation.BCAnimationController, and is constructed with the following call:

controller = new BCAnimationController(evilMesh.skeleton.boneCount);

After that, we actually set the controller to the mesh:

evilMesh.controller = controller;

Ok, we can update all the mesh's vertices in the update method with the animation information; this requires performing a series of operations per vertex in the CPU, but we know some one who can do that job in a more efficient way; the GPU, so we need to add an effect part to the mesh that handles the animation information.

First we create the animation feeder:

BCSkinnedMeshFeeder skinnedFeeder = new BCSkinnedMeshFeeder();

Then we create the effect visitor:

BCDynamicEffectVisitor skinnedSetter = new BCDynamicEffectVisitor(
    skinnedFeeder.getFunction("Actuator.Skinned.SkinnedMesh"),
    skinnedFeeder, 0
);

Notice that the function used now is BCSkinnedMeshFeeder.SKINNED the other function is used when the skinned mesh has a tangent space which will be discussed later.

Also notice that the last parameter of the visitor's constructor is 0, this means that the skinned mesh effect should be applied BEFORE ANY COLOR AND LIGHTNING calculations; that's because the skinned mesh algorithm transforms the normals of the mesh.

Then we apply the visitor:

evilMesh.visit(skinnedSetter);

This is the last visitor applied to the mesh.

Now we need to prepare for the sequence that we are going to be running in the tutorial. In it, the character stands idle for 5 seconds, after that he starts running and runs for 5 seconds, finally he stops running and the sequence starts again.

For that we need a BCAddSequenceListener, this class is responsible of giving the animation controller the animation to play once the current animation finishes.

animationSequence = new BCAddSequenceListener();
animationSequence.setNextAnimation(null, idle);


The setNext method receives two parameters, the first one is the transition animation and the second one is the recurrent animation; the way it works is:
  1. The controller is playing an animation
  2. The animation finishes
  3. The controler asks the BCAddSequenceListener for the next animation to play
    1. If the transition animation isn't null, the BCAddSequenceListener returns the transition animation and sets it (the transition animation) to null
    2. If the transition animation is null it returns the recurrent animation
Notice that this is a loop, after the recurrent animation is returned, the controller starts playing it, when it's over, the BCAddSequenceListener will return the recurrent animation again.

The result is that the transition is played once and the recurrent keeps playing in a loop.

Now, we need to tell the controller that at first, he must run the idle animation on the mesh and that once it finishes, it must ask the BCAddSequenceListener for the next animation.

controller.addRecurrent(idle, animationSequence, 0);


 

3.2.7. The Update method
Here we do the sequence planned in 3.2.6. first we obtain the time in seconds that the application has been running: (don't be confused, the seconds are counted in fractions, so it's not like you will get 1, 2, 3, ... , n seconds, if you ask for the time 1/2 a second after the start of the application, the method will return 0.5 seconds)

double time = gameTime.TotalGameTime.TotalSeconds;

If the current time minus the last time at which the animation was changed is greater than 5 seconds (timeToChange variable):

if (time - lastTime > timeToChange)

Then it's time to change the animation, first we update the last time at which the animation changed to the current time, since we are changing the animation right now.

lastTime = time;

Now, if the current animation is idle (if the character is standing)

if (isIdle)

Then change the animation to run:

if (animationSequence.setNextAnimation(idleToRun, run))
{
    isIdle = false;
}


Notice that the time at which this method is called is not the same time at which the idle animation ends. It's very important that you understand how animation works. The setNextAnimation method doesn't change the animation in the animation controller, it sets the animation that the animationSequence will return to the controller once it finishes the current animation (the idle animation in this case) ; in particular what we are telling the animation controller with the setNextAnimation method is: "once the idle animation finishes, play the idleToRun animation once and then loop the run animation".

Also notice that the setNextAnimation method returns a boolean, that's because once you call it successfully (once you call it and returns true), it sets it's internal state to ignore all subsequent setNextAnimation calls until the recurrent animation is succesfully playing on the controller. Thats the reason we change the isIdle attribute only if it succeds.

Finally we update the animation controller:

controller.update(time);

This call lets the controller update the current animation, end it or change to the next animation using the animationSequence.

 

3.2.8. The Draw method
Nothing new, just draw the mesh:

evilMesh.Draw(gameTime);

 

3.2.9. Conclusion
This is a very important tutorial, you can now create the content of your application using blender and load it using BetaCell. This tutorial is very shallow in the blender subject because that information is already in the web.

Click here to go to the forum discussion of this tutorial

 

Advertisement
 

3.2.10. Complete source code
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

using BetaCell.Debug;
using BetaCell.Util.GlobalInfo.Content;
using BetaCell.Environment.Camera;
using BetaCell.Util.GlobalInfo;


using BetaCell.Environment.Animation;
using BetaCell.Environment.Mesh;
using BetaCell.Environment.Light;
using BetaCell.Environment.Mesh.Content;
using BetaCell.Environment;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Dynamic.Skinned;

namespace Starter
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        //Game attributes
        GraphicsDeviceManager graphics;
        GraphicsDevice device;
        ContentManager content;

        BCFirstPersonHumanCamera camera;
        //end game attributes

        //This tutorial's attributes
        BCMesh evilMesh;

        BCAnimationSetBase animationSet;

        BCAnimationBase idle;
        BCAnimationBase run;
        BCAnimationBase idleToRun;
        BCAnimationBase runToIdle;

        BCAnimationController controller;

        BCAddSequenceListener animationSequence;

        //----------------------------
        //Animation Control Attributes
        //----------------------------

        double timeToChange = 5;
        double lastTime = 0;
        bool isIdle = true;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
            content = new ContentManager(Services);
#if(XBOX360)
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;
#else
            graphics.PreferredBackBufferWidth = 852;
            graphics.PreferredBackBufferHeight = 480;
#endif
        }

        protected override void Initialize()
        {
            //base initialization
            device = graphics.GraphicsDevice;

            BCLogger.instance.init(device, content);
            BCInitializationManager.initialize(content);

            OnActivated(null, null);

            //-----
            //MESHES
            //-----

            evilMesh =
                BCMeshReader.readMesh("Content/gameAssets/evilAnimatedTexturedB", null, device, content);

            //-----
            //Light Effect
            //-----

            BCBasicLight light = new BCBasicLight();
            light.range = 3000;
            light.transform = Matrix.CreateTranslation(-1f, -1f, -1f);
            light.cubAtten = 0;
            light.constAtten = 1;
            light.linAtten = 0;
            light.useShadows = false;

            BCDynamicEffectVisitor lightSetter = new BCDynamicEffectVisitor(
                light.getFunction("Shader.Light.Classic.PixelDirectionLight"), light, 1
            );

            evilMesh.visit(lightSetter);

            //---------
            //ANIMATION
            //---------
            animationSet = content.Load<BCAnimationSetBase>("Content/gameAssets/evilAnimatedTexturedBAnim");

            animationSet.animations.TryGetValue("Idle", out idle);
            animationSet.animations.TryGetValue("Run", out run);
            animationSet.animations.TryGetValue("IdleToRun", out idleToRun);
            animationSet.animations.TryGetValue("RunToIdle", out runToIdle);

            controller = new BCAnimationController(evilMesh.skeleton.boneCount);

            evilMesh.controller = controller;

            BCSkinnedMeshFeeder skinnedFeeder = new BCSkinnedMeshFeeder();
            BCDynamicEffectVisitor skinnedSetter = new BCDynamicEffectVisitor(
                skinnedFeeder.getFunction("Actuator.Skinned.SkinnedMesh"),
                skinnedFeeder, 0
            );
            evilMesh.visit(skinnedSetter);

            animationSequence = new BCAddSequenceListener();
            animationSequence.setNextAnimation(null, idle);
            controller.addSingle(idle, animationSequence, 0);

            base.Initialize();
        }

        protected override void OnActivated(object sender, EventArgs args)
        {
            buildViewMatrix();
            base.OnActivated(sender, args);
        }

        void buildViewMatrix()
        {
            Vector3 pos = new Vector3(6f, 2, 6f);
            Vector3 look = new Vector3(-7, -3.0f, -7);
            look.Normalize();

            camera = new BCFirstPersonHumanCamera(look, pos,
                MathHelper.PiOver4,
                (float)this.Window.ClientBounds.Width / (float)this.Window.ClientBounds.Height,
                1f, 200);

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.VIEW_INDEX,
                camera.getViewMatrix(-1)
            );

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.PROJECTION_INDEX,
                camera.getProjectionMatrix(-1)
            );
        }

        protected override void Update(GameTime gameTime)
        {
            double time = gameTime.TotalGameTime.TotalSeconds;

            if (time - lastTime > timeToChange)
            {
                lastTime = time;
                if (isIdle)
                {
                    if (animationSequence.setNextAnimation(idleToRun, run))
                    {
                        isIdle = false;
                    }
                }
                else
                {
                    if (animationSequence.setNextAnimation(runToIdle, idle))
                    {
                        isIdle = true;
                    }
                }
            }

            controller.update(time);
        }

        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.White, 1.0f, 0);

            evilMesh.Draw(gameTime);

            BCLogger.instance.printFPS(gameTime);
            BCLogger.instance.flush();
            base.Draw(gameTime);
        }
    }
}