Skip to content

Commit

Permalink
Add Skinny animation sample. (#79)
Browse files Browse the repository at this point in the history
* Add Skinny animation sample.
* Add CustomPipelineManager to process animations content.
  • Loading branch information
rejurime authored Nov 18, 2023
1 parent 4f30c23 commit a3398d4
Show file tree
Hide file tree
Showing 31 changed files with 1,739 additions and 277 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![.NET](https://github.com/tgc-utn/tgc-monogame-samples/actions/workflows/dotnet.yml/badge.svg)](https://github.com/tgc-utn/tgc-monogame-samples/actions/workflows/dotnet.yml)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/83dc66740f7d4b0893ad9e556a6496d6)](https://www.codacy.com/gh/tgc-utn/tgc-monogame-samples/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tgc-utn/tgc-monogame-samples&utm_campaign=Badge_Grade)
[![Join our Discord](https://img.shields.io/badge/chat%20on-discord-7289DA?logo=discord&logoColor=white)](https://discord.gg/FKZ4k39zAr)
[![GitHub license](https://img.shields.io/github/license/tgc-utn/tgc-monogame-samples.svg)](https://github.com/tgc-utn/tgc-monogame-samples/blob/master/LICENSE)

![tgc-screenshot](https://user-images.githubusercontent.com/7131403/172287114-1bc554f0-3dcd-411f-b5be-a0d994990563.png)

Expand Down
20 changes: 20 additions & 0 deletions TGC.MonoGame.Samples/Animations/DataTypes/AnimationBone.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;

namespace TGC.MonoGame.Samples.Animations.DataTypes;

/// <summary>
/// Keyframes are grouped per bone for an animation clip.
/// </summary>
public class AnimationBone
{
/// <summary>
/// The keyframes for this bone.
/// </summary>
public List<Keyframe> Keyframes { get; set; } = new();

/// <summary>
/// The bone name for these keyframes.
/// Each bone has a name so we can associate it with a runtime model.
/// </summary>
public string Name { get; set; } = string.Empty;
}
24 changes: 24 additions & 0 deletions TGC.MonoGame.Samples/Animations/DataTypes/AnimationClip.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;

namespace TGC.MonoGame.Samples.Animations.DataTypes;

/// <summary>
/// An animation clip is a set of keyframes with associated bones.
/// </summary>
public class AnimationClip
{
/// <summary>
/// The bones for this animation clip with their keyframes.
/// </summary>
public List<AnimationBone> Bones { get; set; } = new();

/// <summary>
/// Duration of the animation clip.
/// </summary>
public double Duration { get; set; }

/// <summary>
/// Name of the animation clip.
/// </summary>
public string Name { get; set; }
}
39 changes: 39 additions & 0 deletions TGC.MonoGame.Samples/Animations/DataTypes/Keyframe.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Microsoft.Xna.Framework;

namespace TGC.MonoGame.Samples.Animations.DataTypes;

/// <summary>
/// An Keyframe is a rotation and Translation for a moment in time.
/// It would be easy to extend this to include scaling as well.
/// </summary>
public class Keyframe
{
/// <summary>
/// The rotation for the bone.
/// </summary>
public Quaternion Rotation { get; set; }

/// <summary>
/// The keyframe time.
/// </summary>
public double Time { get; set; }

/// <summary>
/// The Translation for the bone.
/// </summary>
public Vector3 Translation { get; set; }

public Matrix Transform
{
get => Matrix.CreateFromQuaternion(Rotation) * Matrix.CreateTranslation(Translation);
set
{
var transform = value;
transform.Right = Vector3.Normalize(transform.Right);
transform.Up = Vector3.Normalize(transform.Up);
transform.Backward = Vector3.Normalize(transform.Backward);
Rotation = Quaternion.CreateFromRotationMatrix(transform);
Translation = transform.Translation;
}
}
}
19 changes: 19 additions & 0 deletions TGC.MonoGame.Samples/Animations/DataTypes/ModelExtra.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;

namespace TGC.MonoGame.Samples.Animations.DataTypes;

/// <summary>
/// Class that contains additional information attached to the model and shared with the runtime.
/// </summary>
public class ModelExtra
{
/// <summary>
/// Animation clips associated with this model.
/// </summary>
public List<AnimationClip> Clips { get; set; } = new();

/// <summary>
/// The bone indices for the skeleton associated with any skinned model.
/// </summary>
public List<int> Skeleton { get; set; } = new();
}
161 changes: 161 additions & 0 deletions TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using TGC.MonoGame.Samples.Animations.DataTypes;

namespace TGC.MonoGame.Samples.Animations.Models;

/// <summary>
/// An enclosure for an XNA Model that we will use that includes support for Bones, animation, and some manipulations.
/// </summary>
public class AnimatedModel
{
/// <summary>
/// The Model asset name.
/// </summary>
private readonly string _assetName;

/// <summary>
/// The underlying Bones for the Model.
/// </summary>
private readonly List<Bone> _bones = new();

/// <summary>
/// The actual underlying XNA Model.
/// </summary>
private Model _model;

/// <summary>
/// Extra data associated with the XNA Model.
/// </summary>
private ModelExtra _modelExtra;

/// <summary>
/// An associated animation clip Player.
/// </summary>
private AnimationPlayer _player;

/// <summary>
/// The Model animation clips.
/// </summary>
public List<AnimationClip> Clips => _modelExtra.Clips;

/// <summary>
/// Creates the Model from an XNA Model.
/// </summary>
/// <param name="assetName">The name of the asset for this Model.</param>
public AnimatedModel(string assetName)
{
_assetName = assetName;
}

/// <summary>
/// Play an animation clip.
/// </summary>
/// <param name="clip">The clip to play.</param>
/// <returns>The Player that will play this clip.</returns>
public AnimationPlayer PlayClip(AnimationClip clip)
{
// Create a clip Player and assign it to this Model.
_player = new AnimationPlayer(clip, this);
return _player;
}

/// <summary>
/// Update animation for the Model.
/// </summary>
public void Update(GameTime gameTime)
{
_player.Update(gameTime);
}

/// <summary>
/// Draw the Model.
/// </summary>
/// <param name="world">A world matrix to place the Model.</param>
/// <param name="view">The view matrix, normally from the camera.</param>
/// <param name="projection">The projection matrix, normally from the application.</param>
public void Draw(Matrix world, Matrix view, Matrix projection)
{
// Compute all of the bone absolute transforms.
var boneTransforms = new Matrix[_bones.Count];

for (var i = 0; i < _bones.Count; i++)
{
var bone = _bones[i];
bone.ComputeAbsoluteTransform();

boneTransforms[i] = bone.AbsoluteTransform;
}

// Determine the skin transforms from the skeleton.
var skeleton = new Matrix[_modelExtra.Skeleton.Count];
for (var s = 0; s < _modelExtra.Skeleton.Count; s++)
{
var bone = _bones[_modelExtra.Skeleton[s]];
skeleton[s] = bone.SkinTransform * bone.AbsoluteTransform;
}

// Draw the Model.
foreach (var modelMesh in _model.Meshes)
{
foreach (var effect in modelMesh.Effects)
{
var skinnedEffect = effect as SkinnedEffect;
skinnedEffect.World = boneTransforms[modelMesh.ParentBone.Index] * world;
skinnedEffect.View = view;
skinnedEffect.Projection = projection;
skinnedEffect.EnableDefaultLighting();
skinnedEffect.PreferPerPixelLighting = true;
skinnedEffect.SetBoneTransforms(skeleton);
}

modelMesh.Draw();
}
}

/// <summary>
/// Load the Model asset from content.
/// </summary>
public void LoadContent(ContentManager content)
{
_model = content.Load<Model>(_assetName);
_modelExtra = _model.Tag as ModelExtra;

ObtainBones();
}

/// <summary>
/// Get the Bones from the Model and create a bone class object for each bone. We use our bone class to do the real
/// animated bone work.
/// </summary>
private void ObtainBones()
{
_bones.Clear();
foreach (var bone in _model.Bones)
{
// Create the bone object and add to the hierarchy.
var newBone = new Bone(bone.Name, bone.Transform, bone.Parent != null ? _bones[bone.Parent.Index] : null);

// Add to the Bones for this Model.
_bones.Add(newBone);
}
}

/// <summary>
/// Find a bone in this Model by name.
/// </summary>
public Bone FindBone(string name)
{
foreach (var bone in _bones)
{
if (bone.Name == name)
{
return bone;
}
}

return null;
}
}
103 changes: 103 additions & 0 deletions TGC.MonoGame.Samples/Animations/Models/AnimationPlayer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using Microsoft.Xna.Framework;
using TGC.MonoGame.Samples.Animations.DataTypes;

namespace TGC.MonoGame.Samples.Animations.Models;

/// <summary>
/// Animation Clip player.
/// It maps an animation Clip onto a Model.
/// </summary>
public class AnimationPlayer
{
/// <summary>
/// We maintain a BoneInfo class for each bone.
/// This class does most of the work in playing the animation.
/// </summary>
private readonly BoneInfo[] _boneInfo;

/// <summary>
/// The Clip we are playing.
/// </summary>
private readonly AnimationClip _clip;

/// <summary>
/// Current position in time in the clip.
/// </summary>
private float _position;

/// <summary>
/// Constructor for the animation player.
/// It makes the association between a Clip and a Model and sets up for playing.
/// </summary>
public AnimationPlayer(AnimationClip clip, AnimatedModel model)
{
_clip = clip;

// Create the bone information classes.
var boneCount = clip.Bones.Count;
_boneInfo = new BoneInfo[boneCount];

for (var b = 0; b < _boneInfo.Length; b++)
{
// Create it.
_boneInfo[b] = new BoneInfo(clip.Bones[b]);

// Assign it to a Model bone.
_boneInfo[b].SetModel(model);
}

Rewind();
}

/// <summary>
/// The Looping option.
/// Set to true if you want the animation to loop back at the end.
/// </summary>
public bool Looping { get; set; }

/// <summary>
/// Current Position in time in the Clip.
/// </summary>
private float Position
{
get => _position;
set
{
if (value > Duration)
{
value = Duration;
}

_position = value;
foreach (var bone in _boneInfo)
{
bone.SetPosition(_position);
}
}
}

/// <summary>
/// The Clip duration.
/// </summary>
public float Duration => (float)_clip.Duration;

/// <summary>
/// Reset back to time zero.
/// </summary>
public void Rewind()
{
Position = 0;
}

/// <summary>
/// Update the Clip Position.
/// </summary>
public void Update(GameTime gameTime)
{
Position += (float)gameTime.ElapsedGameTime.TotalSeconds;
if (Looping && Position >= Duration)
{
Position = 0;
}
}
}
Loading

0 comments on commit a3398d4

Please sign in to comment.