Thursday, November 12, 2015

xna - How can I attach a model to the bone of another model?


I am trying to attach one animated model to one of the bones of another animated model in an XNA game.


I've found a few questions/forum posts/articles online which explain how to attach a weapon model to the bone of another model (which is analogous to what I'm trying to achieve), but they don't seem to work for me.


So as an example: I want to attach Model A to a specific bone in Model B.


Question 1. As I understand it, I need to calculate the transforms which are applied to the bone on Model B and apply these same transforms to every bone in Model A. Is this right?


Question 2. This is my code for calculating the Transforms on a specific bone.


private Matrix GetTransformPaths(ModelBone bone)

{
Matrix result = Matrix.Identity;
while (bone != null)
{
result = result * bone.Transform;
bone = bone.Parent;
}
return result;
}


The maths of Matrices is almost entirely lost on me, but my understanding is that the above will work its way up the bone structure to the root bone and my end result will be the transform of the original bone relative to the model.


Is this right?


Question 3. Assuming that this is correct I then expect that I should either apply this to each bone in Model A, or in my Draw() method:


private void DrawModel(SceneModel model, GameTime gametime)
{
foreach (var component in model.Components)
{
Matrix[] transforms = new Matrix[component.Model.Bones.Count];
component.Model.CopyAbsoluteBoneTransformsTo(transforms);


Matrix parenttransform = Matrix.Identity;
if (!string.IsNullOrEmpty(component.ParentBone))
parenttransform = GetTransformPaths(model.GetBone(component.ParentBone));


component.Player.Update(gametime.ElapsedGameTime, true, Matrix.Identity);

Matrix[] bones = component.Player.GetSkinTransforms();

foreach (SkinnedEffect effect in mesh.Effects)

{
effect.SetBoneTransforms(bones);

effect.EnableDefaultLighting();
effect.World = transforms[mesh.ParentBone.Index]
* Matrix.CreateRotationY(MathHelper.ToRadians(model.Angle))
* Matrix.CreateTranslation(model.Position)
* parenttransform;
effect.View = getView();
effect.Projection = getProjection();

effect.Alpha = model.Opacity;
}
}
mesh.Draw();
}

I feel as though I have tried every conceivable way of incorporating the parenttransform value into the draw method. The above is my most recent attempt. Is what I'm trying to do correct? And if so, is there a reason it doesn't work?


The above Draw method seems to transpose the models x/z position - but even at these wrong positions, they do not account for the animation of Model B at all.


Note: As will be evident from the code my "model" is comprised of a list of "components". It is these "components" that correspond to a single "Microsoft.Xna.Framework.Graphics.Model"



Answer




Okay this is how I got it to work. It is by no means elegant. It is based off of the SkinnedModelSample. My game has a collection of AnimatedModels - which in turn have a dictionary of other Animated Models. The string key for this dictionary is the name of the bone which it inherits it transforms from (or is "attached" to). A limitation of this is that only one model can be attached to a specific bone. (This could be easily changed, but there is no need as it meets my requirements).


public class AnimatedModel
{
public Model Model { get; set; }
public SkinningData SkinningData { get; set; }
public AnimationPlayer Player { get; set; }
public AnimationClip Clip { get; set; }
public Dictionary Components { get; set; }

public AnimatedModel() : base() { }

public AnimatedModel(Model model)
{
Model = model;
Components = new Dictionary();

SkinningData = model.Tag as SkinningData;
if (SkinningData != null)
{
Player = new AnimationPlayer(SkinningData);
Clip = SkinningData.AnimationClips["ArmatureAction"];

Player.StartClip(Clip);
}
}

public void AddModel(string parentBone, Model model)
{
Components.Add(parentBone, new AnimatedModel(model));
}
}


My Update() and Draw() methods:


protected override void Update(GameTime gameTime)
{
foreach (var model in CurrentScene.GetModels().Where(x => x.IsVisible))
UpdateModelAnimation(model.Model, gameTime, Matrix.Identity);
base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)
{

graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

GraphicsDevice.DepthStencilState = DepthStencilState.Default;
foreach (var model in CurrentScene.GetModels().Where(x => x.IsVisible))
DrawModel(model, model.Model);

base.Draw(gameTime);
}

And Update and Draw methods for individual models:



private void UpdateModelAnimation(AnimatedModel model, GameTime gameTime, Matrix rootTransform)
{
model.Player.Update(gameTime.ElapsedGameTime, true, rootTransform);
var bones = model.Player.GetWorldTransforms();

foreach (var item in model.Components)
{
ModelBone parentbone = model.Model.Bones.Where(x => x.Name == item.Key).Single();
UpdateModelAnimation(item.Value, gameTime, bones[model.SkinningData.SkeletonHierarchy[parentbone.Index - 2]]);
}

}

private void DrawModel(SceneModel sceneModel, AnimatedModel model)
{
Matrix[] transforms = new Matrix[model.Model.Bones.Count];
model.Model.CopyAbsoluteBoneTransformsTo(transforms);
Matrix[] bones = model.Player.GetSkinTransforms();

foreach (ModelMesh mesh in model.Model.Meshes)
{

foreach (SkinnedEffect effect in mesh.Effects)
{
effect.SetBoneTransforms(bones);

effect.EnableDefaultLighting();
effect.World = Matrix.CreateRotationY(MathHelper.ToRadians(sceneModel.Angle))
* Matrix.CreateTranslation(sceneModel.Position);
effect.View = getView();
effect.Projection = getProjection();
effect.Alpha = sceneModel.Opacity;

}
mesh.Draw();
}
foreach (var item in model.Components)
DrawModel(sceneModel, item.Value);
}

The UpdateModelAnimations and DrawModel methods are recursive and work their way down a hierarchy of attached models. In this way it is possible to have models attached to models attached to models - indefinitely.


Of particular note is this line:


UpdateModelAnimation(item.Value, gameTime, bones[model.SkinningData.SkeletonHierarchy[parentbone.Index - 2]]);


The "parentbone.Index - 2" is used because my models (for some unresolved reason) have two additional bones which are not part of the SkinningData. So depending on how your models are created you may need to change this line.


In any case this is how I resolved the issue of attaching one animated model to the bone of another.


I haven't touched the project for a while, so I don't know if and when I'll be able to update with a more refined version of the code, but hopefully this will help others who are struggling with the same problem.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...