From 18c8c5a8b885a3d300834ecd1580f0f290445e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Juan=20Rico=20Mendoza?= Date: Sun, 28 May 2023 23:09:48 -0300 Subject: [PATCH 1/4] Add Skinny animation sample. * Add skinny animation sample. * Add CustomPipelineManager to process animations content. --- README.md | 1 + .../Animations/DataTypes/AnimationClip.cs | 24 + .../Animations/DataTypes/Bone.cs | 20 + .../Animations/DataTypes/Keyframe.cs | 39 ++ .../Animations/DataTypes/ModelExtra.cs | 19 + .../Animations/Models/AnimatedModel.cs | 180 +++++++ .../Animations/Models/AnimationPlayer.cs | 115 ++++ .../Animations/Models/Bone.cs | 131 +++++ .../Animations/Models/BoneInfo.cs | 147 +++++ .../PipelineExtension/AnimationProcessor.cs | 496 +++++++++++++++++ .../CustomPipelineManager.cs | 110 ++++ .../3D/tgcito-classic/skeleton/Idle-2.fbx | 3 + .../3D/tgcito-classic/skeleton/Idle.fbx | 3 + .../tgcito-classic/skeleton/Standard-Run.fbx | 3 + .../tgcito-classic/skeleton/Standard-Walk.fbx | 3 + .../3D/tgcito-classic/skeleton/T-Pose.fbx | 3 + .../3D/tgcito-classic/skeleton/uvw.jpg | 3 + .../Samples/Animations/AnimationType.cs | 9 + .../Samples/Animations/SkinnedAnimation.cs | 157 ++++++ .../Samples/Audio/SoundStatic.cs | 2 +- .../Samples/Shaders/SkyBox/SimpleSkyBox.cs | 4 +- .../Samples/TGCSampleCategory.cs | 1 + .../Samples/Tutorials/Tutorial1.cs | 2 +- .../Samples/Tutorials/Tutorial2.cs | 2 +- .../Samples/Tutorials/Tutorial3.cs | 2 +- .../TGC.MonoGame.Samples.csproj | 9 +- .../GUI/Modifiers/ModifierController.cs | 503 +++++++++--------- TGC.MonoGame.Samples/app-settings.json | 9 + docs/install/install-windows.md | 5 + 29 files changed, 1749 insertions(+), 256 deletions(-) create mode 100644 TGC.MonoGame.Samples/Animations/DataTypes/AnimationClip.cs create mode 100644 TGC.MonoGame.Samples/Animations/DataTypes/Bone.cs create mode 100644 TGC.MonoGame.Samples/Animations/DataTypes/Keyframe.cs create mode 100644 TGC.MonoGame.Samples/Animations/DataTypes/ModelExtra.cs create mode 100644 TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs create mode 100644 TGC.MonoGame.Samples/Animations/Models/AnimationPlayer.cs create mode 100644 TGC.MonoGame.Samples/Animations/Models/Bone.cs create mode 100644 TGC.MonoGame.Samples/Animations/Models/BoneInfo.cs create mode 100644 TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs create mode 100644 TGC.MonoGame.Samples/Animations/PipelineExtension/CustomPipelineManager.cs create mode 100644 TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Idle-2.fbx create mode 100644 TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Idle.fbx create mode 100644 TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Standard-Run.fbx create mode 100644 TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Standard-Walk.fbx create mode 100644 TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/T-Pose.fbx create mode 100644 TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/uvw.jpg create mode 100644 TGC.MonoGame.Samples/Samples/Animations/AnimationType.cs create mode 100644 TGC.MonoGame.Samples/Samples/Animations/SkinnedAnimation.cs create mode 100644 TGC.MonoGame.Samples/app-settings.json diff --git a/README.md b/README.md index 9ea7b57..1180a19 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/TGC.MonoGame.Samples/Animations/DataTypes/AnimationClip.cs b/TGC.MonoGame.Samples/Animations/DataTypes/AnimationClip.cs new file mode 100644 index 0000000..c3bcb96 --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/DataTypes/AnimationClip.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace TGC.MonoGame.Samples.Animations.DataTypes; + +/// +/// An animation clip is a set of keyframes with associated bones. +/// +public class AnimationClip +{ + /// + /// The bones for this animation clip with their keyframes. + /// + public List Bones { get; set; } = new(); + + /// + /// Duration of the animation clip. + /// + public double Duration { get; set; } + + /// + /// Name of the animation clip. + /// + public string Name { get; set; } +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/DataTypes/Bone.cs b/TGC.MonoGame.Samples/Animations/DataTypes/Bone.cs new file mode 100644 index 0000000..2a53f50 --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/DataTypes/Bone.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace TGC.MonoGame.Samples.Animations.DataTypes; + +/// +/// Keyframes are grouped per bone for an animation clip. +/// +public class Bone +{ + /// + /// The keyframes for this bone. + /// + public List Keyframes { get; set; } = new(); + + /// + /// The bone name for these keyframes. + /// Each bone has a name so we can associate it with a runtime model. + /// + public string Name { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/DataTypes/Keyframe.cs b/TGC.MonoGame.Samples/Animations/DataTypes/Keyframe.cs new file mode 100644 index 0000000..85d0d39 --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/DataTypes/Keyframe.cs @@ -0,0 +1,39 @@ +using Microsoft.Xna.Framework; + +namespace TGC.MonoGame.Samples.Animations.DataTypes; + +/// +/// An Keyframe is a rotation and Translation for a moment in time. +/// It would be easy to extend this to include scaling as well. +/// +public class Keyframe +{ + /// + /// The rotation for the bone. + /// + public Quaternion Rotation { get; set; } + + /// + /// The keyframe time. + /// + public double Time { get; set; } + + /// + /// The Translation for the bone. + /// + 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; + } + } +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/DataTypes/ModelExtra.cs b/TGC.MonoGame.Samples/Animations/DataTypes/ModelExtra.cs new file mode 100644 index 0000000..5f1ea6f --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/DataTypes/ModelExtra.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace TGC.MonoGame.Samples.Animations.DataTypes; + +/// +/// Class that contains additional information attached to the model and shared with the runtime. +/// +public class ModelExtra +{ + /// + /// Animation clips associated with this model. + /// + public List Clips { get; set; } = new(); + + /// + /// The bone indices for the skeleton associated with any skinned model. + /// + public List Skeleton { get; set; } = new(); +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs b/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs new file mode 100644 index 0000000..89db1d9 --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using TGC.MonoGame.Samples.Animations.DataTypes; +using TGC.MonoGame.Samples.Cameras; + +namespace TGC.MonoGame.Samples.Animations.Models; + +/// +/// An enclosure for an XNA Model that we will use that includes support for Bones, animation, and some manipulations. +/// +public class AnimatedModel +{ + /// + /// Creates the Model from an XNA Model. + /// + /// The name of the asset for this Model. + public AnimatedModel(string assetName) + { + AssetName = assetName; + } + + /// + /// The Model asset name. + /// + private string AssetName { get; } + + /// + /// The underlying Bones for the Model. + /// + private List Bones { get; } = new(); + + /// + /// The actual underlying XNA Model. + /// + private Model Model { get; set; } + + /// + /// Extra data associated with the XNA Model. + /// + private ModelExtra ModelExtra { get; set; } + + /// + /// An associated animation clip Player. + /// + private AnimationPlayer Player { get; set; } + + /// + /// The Model animation clips. + /// + public List Clips => ModelExtra.Clips; + + /// + /// Play an animation clip. + /// + /// The clip to play. + /// The Player that will play this clip. + public AnimationPlayer PlayClip(AnimationClip clip) + { + // Create a clip Player and assign it to this Model. + Player = new AnimationPlayer(clip, this); + return Player; + } + + /// + /// Update animation for the Model. + /// + public void Update(GameTime gameTime) + { + Player?.Update(gameTime); + } + + /// + /// Draw the Model. + /// + /// A camera to determine the view. + /// A world matrix to place the Model. + public void Draw(Camera camera, Matrix world) + { + if (Model == null) + { + return; + } + + // 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) + { + switch (effect) + { + case BasicEffect basicEffect: + basicEffect.World = boneTransforms[modelMesh.ParentBone.Index] * world; + basicEffect.View = camera.View; + basicEffect.Projection = camera.Projection; + basicEffect.EnableDefaultLighting(); + basicEffect.PreferPerPixelLighting = true; + break; + + case SkinnedEffect skinnedEffect: + skinnedEffect.World = boneTransforms[modelMesh.ParentBone.Index] * world; + skinnedEffect.View = camera.View; + skinnedEffect.Projection = camera.Projection; + skinnedEffect.EnableDefaultLighting(); + skinnedEffect.PreferPerPixelLighting = true; + skinnedEffect.SetBoneTransforms(skeleton); + break; + } + } + + modelMesh.Draw(); + } + } + + /// + /// Load the Model asset from content. + /// + public void LoadContent(ContentManager content) + { + Model = content.Load(AssetName); + ModelExtra = Model.Tag as ModelExtra; + Debug.Assert(ModelExtra != null); + + ObtainBones(); + } + + /// + /// 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. + /// + 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); + } + } + + /// + /// Find a bone in this Model by name. + /// + public Bone FindBone(string name) + { + foreach (var bone in Bones) + { + if (bone.Name == name) + { + return bone; + } + } + + return null; + } +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/Models/AnimationPlayer.cs b/TGC.MonoGame.Samples/Animations/Models/AnimationPlayer.cs new file mode 100644 index 0000000..dd083f9 --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/Models/AnimationPlayer.cs @@ -0,0 +1,115 @@ +using Microsoft.Xna.Framework; +using TGC.MonoGame.Samples.Animations.DataTypes; + +namespace TGC.MonoGame.Samples.Animations.Models; + +/// +/// Animation Clip player. +/// It maps an animation Clip onto a Model. +/// +public class AnimationPlayer +{ + /// + /// Current position in time in the clip. + /// + private float _position; + + /// + /// Constructor for the animation player. + /// It makes the association between a Clip and a Model and sets up for playing. + /// + public AnimationPlayer(AnimationClip clip, AnimatedModel model) + { + Clip = clip; + Model = model; + + // Create the bone information classes. + BonesCount = clip.Bones.Count; + BonesInfo = new BoneInfo[BonesCount]; + + for (var b = 0; b < BonesInfo.Length; b++) + { + // Create it. + BonesInfo[b] = new BoneInfo(clip.Bones[b]); + + // Assign it to a Model bone. + BonesInfo[b].SetModel(model); + } + + Rewind(); + } + + /// + /// The number of bones. + /// + private int BonesCount { get; } + + /// + /// We maintain a BoneInfo class for each bone. + /// This class does most of the work in playing the animation. + /// + private BoneInfo[] BonesInfo { get; } + + /// + /// The Clip we are playing. + /// + private AnimationClip Clip { get; } + + /// + /// A Model this animation is assigned to. + /// It will play on that Model. + /// + private AnimatedModel Model { get; } + + /// + /// The Looping option. + /// Set to true if you want the animation to loop back at the end. + /// + public bool Looping { get; set; } + + /// + /// Current Position in time in the Clip. + /// + private float Position + { + get => _position; + set + { + if (value > Duration) + { + value = Duration; + } + + _position = value; + foreach (var bone in BonesInfo) + { + bone.SetPosition(_position); + } + } + } + + /// + /// The Clip duration. + /// + public float Duration => (float)Clip.Duration; + + /// + /// Reset back to time zero. + /// + public void Rewind() + { + Position = 0; + } + + /// + /// Update the Clip Position. + /// + public void Update(GameTime gameTime) + { + Position += (float)gameTime.ElapsedGameTime.TotalSeconds; + if (Looping && Position >= Duration) + { + Position = 0; + } + } +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/Models/Bone.cs b/TGC.MonoGame.Samples/Animations/Models/Bone.cs new file mode 100644 index 0000000..ea6ac1b --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/Models/Bone.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace TGC.MonoGame.Samples.Animations.Models; + +/// +/// Bones in this model are represented by this class, which allows a bone to have more detail associate with it. +/// This class allows you to manipulate the local coordinate system for objects by changing the scaling, Translation, +/// and Rotation. +/// These are independent of the bind transformation originally supplied for the model. +/// So, the actual transformation for a bone is the product of the: +/// * Scaling. +/// * Bind scaling (scaling removed from the bind transform). +/// * Rotation. +/// * Translation. +/// * Bind Transformation. +/// * Parent Absolute Transformation. +/// +public class Bone +{ + /// + /// Constructor for a bone object. + /// + /// The name of the bone. + /// The initial bind transform for the bone. + /// A Parent for this bone. + public Bone(string name, Matrix bindTransform, Bone parent) + { + Name = name; + Parent = parent; + parent?.Children.Add(this); + + // I am not supporting scaling in animation in this example, so I extract the bind scaling from the bind transform and save it. + BindScale = new Vector3(bindTransform.Right.Length(), bindTransform.Up.Length(), + bindTransform.Backward.Length()); + + bindTransform.Right /= BindScale.X; + bindTransform.Up /= BindScale.Y; + bindTransform.Backward /= BindScale.Y; + BindTransform = bindTransform; + + // Set the skinning bind transform. + // That is the inverse of the absolute transform in the bind pose. + ComputeAbsoluteTransform(); + SkinTransform = Matrix.Invert(AbsoluteTransform); + } + + /// + /// The bind scaling component extracted from the bind transform. + /// + private Vector3 BindScale { get; } + + /// + /// The bind transform is the transform for this bone as loaded from the original model. It's the base pose. + /// I do remove any scaling, though. + /// + private Matrix BindTransform { get; } + + /// + /// The Children of this bone. + /// + private List Children { get; } = new(); + + /// + /// The Parent bone or null for the root bone. + /// + private Bone Parent { get; } + + /// + /// The bone absolute transform. + /// + public Matrix AbsoluteTransform { get; set; } = Matrix.Identity; + + /// + /// The bone name. + /// + public string Name { get; set; } + + /// + /// Any Rotation applied to the bone. + /// + private Quaternion Rotation { get; set; } = Quaternion.Identity; + + /// + /// Any scaling applied to the bone. + /// + private Vector3 Scale { get; } = Vector3.One; + + /// + /// Any Translation applied to the bone. + /// + private Vector3 Translation { get; set; } = Vector3.Zero; + + /// + /// Inverse of absolute bind transform for skinning. + /// + public Matrix SkinTransform { get; set; } + + /// + /// Compute the absolute transformation for this bone. + /// + public void ComputeAbsoluteTransform() + { + var transform = Matrix.CreateScale(Scale * BindScale) * Matrix.CreateFromQuaternion(Rotation) * + Matrix.CreateTranslation(Translation) * BindTransform; + + if (Parent != null) + // This bone has a Parent bone. + { + AbsoluteTransform = transform * Parent.AbsoluteTransform; + } + else + // The root bone. + { + AbsoluteTransform = transform; + } + } + + /// + /// This sets the Rotation and Translation such that the Rotation times the Translation times the bind after set equals + /// this matrix. This is used to set animation values. + /// + /// A matrix include Translation and Rotation + public void SetCompleteTransform(Matrix m) + { + var setTo = m * Matrix.Invert(BindTransform); + + Translation = setTo.Translation; + Rotation = Quaternion.CreateFromRotationMatrix(setTo); + } +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/Models/BoneInfo.cs b/TGC.MonoGame.Samples/Animations/Models/BoneInfo.cs new file mode 100644 index 0000000..c5cdae6 --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/Models/BoneInfo.cs @@ -0,0 +1,147 @@ +using Microsoft.Xna.Framework; +using TGC.MonoGame.Samples.Animations.DataTypes; + +namespace TGC.MonoGame.Samples.Animations.Models; + +/// +/// Information about a bone we are animating. +/// This class connects a bone in the clip to a bone in the model. +/// +public class BoneInfo +{ + /// + /// Constructor. + /// + public BoneInfo(DataTypes.Bone bone) + { + ClipBone = bone; + SetKeyframes(); + SetPosition(0); + } + + /// + /// Bone in a model that this keyframe bone is assigned to. + /// + private Bone AssignedBone { get; set; } + + /// + /// The current keyframe. Our position is a time such that the we are greater than or equal to this keyframe's time and + /// less than the next keyframes time. + /// + private int CurrentKeyframe { get; set; } + + /// + /// We are at a location between Keyframe1 and Keyframe2 such that Keyframe1's time is less than or equal to the + /// current position. + /// + public Keyframe Keyframe1 { get; set; } + + /// + /// Second keyframe value. + /// + public Keyframe Keyframe2 { get; set; } + + /// + /// Current animation Rotation. + /// + private Quaternion Rotation { get; set; } + + /// + /// Current animation Translation. + /// + public Vector3 Translation { get; set; } + + /// + /// We are not Valid until the Rotation and Translation are set. + /// If there are no keyframes, we will never be Valid. + /// + public bool Valid { get; set; } + + /// + /// The bone in the actual animation clip. + /// + public DataTypes.Bone ClipBone { get; } + + /// + /// Set the bone based on the supplied position value. + /// + public void SetPosition(float position) + { + var keyframes = ClipBone.Keyframes; + if (keyframes.Count == 0) + { + return; + } + + // If our current position is less that the first keyframe we move the position backward until we get to the right keyframe. + while (position < Keyframe1.Time && CurrentKeyframe > 0) + { + // We need to move backwards in time. + CurrentKeyframe--; + SetKeyframes(); + } + + // If our current position is greater than the second keyframe we move the position forward until we get to the right keyframe. + while (position >= Keyframe2.Time && CurrentKeyframe < ClipBone.Keyframes.Count - 2) + { + // We need to move forwards in time. + CurrentKeyframe++; + SetKeyframes(); + } + + if (Keyframe1 == Keyframe2) + { + // Keyframes are equal. + Rotation = Keyframe1.Rotation; + Translation = Keyframe1.Translation; + } + else + { + // Interpolate between keyframes. + var t = (float)((position - Keyframe1.Time) / (Keyframe2.Time - Keyframe1.Time)); + Rotation = Quaternion.Slerp(Keyframe1.Rotation, Keyframe2.Rotation, t); + Translation = Vector3.Lerp(Keyframe1.Translation, Keyframe2.Translation, t); + } + + Valid = true; + if (AssignedBone == null) + { + return; + } + + // Send to the model. + // Make it a matrix first. + var m = Matrix.CreateFromQuaternion(Rotation); + m.Translation = Translation; + AssignedBone.SetCompleteTransform(m); + } + + /// + /// Set the keyframes to a Valid value relative to the current keyframe. + /// + private void SetKeyframes() + { + if (ClipBone.Keyframes.Count > 0) + { + Keyframe1 = ClipBone.Keyframes[CurrentKeyframe]; + Keyframe2 = CurrentKeyframe == ClipBone.Keyframes.Count - 1 + ? Keyframe1 + : ClipBone.Keyframes[CurrentKeyframe + 1]; + } + else + { + // If there are no keyframes, set both to null. + Keyframe1 = null; + Keyframe2 = null; + } + } + + /// + /// Assign this bone to the correct bone in the model. + /// + public void SetModel(AnimatedModel model) + { + // Find this bone. + AssignedBone = model.FindBone(ClipBone.Name); + } +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs b/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs new file mode 100644 index 0000000..47f5ad5 --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs @@ -0,0 +1,496 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Graphics; +using Microsoft.Xna.Framework.Content.Pipeline.Processors; +using TGC.MonoGame.Samples.Animations.DataTypes; + +namespace TGC.MonoGame.Samples.Animations.PipelineExtension; + +/// +/// This class extends the standard ModelProcessor to include code that extracts a skeleton, pulls any animations, and +/// does any necessary prep work to support animation and skinning. +/// +[ContentProcessor(DisplayName = "Animation Processor")] +public class AnimationProcessor : ModelProcessor +{ + private const float TinyLength = 1e-8f; + private const float TinyCosAngle = 0.9999999f; + + /// + /// Bones lookup table, converts bone names to indices. + /// + private readonly Dictionary _bones = new(); + + /// + /// A dictionary so we can keep track of the clips by name. + /// + private readonly Dictionary _clips = new(); + + /// + /// Extra content to associated with the model. This is where we put the stuff that is unique to this project. + /// + private readonly ModelExtra _modelExtra = new(); + + /// + /// A lookup dictionary that remembers when we changes a material to skinned material. + /// + private readonly Dictionary _toSkinnedMaterial = new(); + + /// + /// This will keep track of all of the bone transforms for a base pose. + /// + private Matrix[] _boneTransforms; + + /// + /// The model we are reading. + /// + private ModelContent _model; + + /// + /// The function to process a model from original content into model content for export. + /// + public override ModelContent Process(NodeContent input, ContentProcessorContext context) + { + // Skeleton Support. + // Process the skeleton for skinned character animation. + var skeleton = ProcessSkeleton(input); + + // Skinned Support. + SwapSkinnedMaterial(input); + + // Base Model process. + _model = base.Process(input, context); + + // Animation Support. + ProcessAnimations(_model, input, context); + + // Add the extra content to the model. + _model.Tag = _modelExtra; + + return _model; + } + + /// + /// Process the skeleton in support of skeletal animation. + /// + private BoneContent ProcessSkeleton(NodeContent input) + { + // Find the skeleton. + var skeleton = MeshHelper.FindSkeleton(input); + + if (skeleton == null) + { + return null; + } + + // We don't want to have to worry about different parts of the model being in different local coordinate systems, so let's just bake everything. + FlattenTransforms(input, skeleton); + + // 3D Studio Max includes helper bones that end with "Nub" These are not part of the skinning system and can be discarded. TrimSkeleton removes them from the geometry. + TrimSkeleton(skeleton); + + // Convert the hierarchy of nodes and bones into a list. + var nodes = FlattenHierarchy(input); + var bones = MeshHelper.FlattenSkeleton(skeleton); + + // Create a dictionary to convert a node to an index into the array of nodes. + var nodeToIndex = new Dictionary(); + for (var i = 0; i < nodes.Count; i++) + { + nodeToIndex[nodes[i]] = i; + } + + // Now create the array that maps the bones to the nodes. + foreach (var bone in bones) + { + _modelExtra.Skeleton.Add(nodeToIndex[bone]); + } + + return skeleton; + } + + /// + /// Convert a tree of nodes into a list of nodes in topological order. + /// + /// The root of the hierarchy. + private List FlattenHierarchy(NodeContent item) + { + var nodes = new List(); + nodes.Add(item); + + foreach (var child in item.Children) + { + FlattenHierarchy(nodes, child); + } + + return nodes; + } + + private void FlattenHierarchy(ICollection nodes, NodeContent item) + { + nodes.Add(item); + + foreach (var child in item.Children) + { + FlattenHierarchy(nodes, child); + } + } + + /// + /// Bakes unwanted transforms into the model geometry, so everything ends up in the same coordinate system. + /// + private void FlattenTransforms(NodeContent node, BoneContent skeleton) + { + foreach (var child in node.Children) + { + // Don't process the skeleton, because that is special. + if (child == skeleton) + { + continue; + } + + // This is important: Don't bake in the transforms except for geometry that is part of a skinned mesh. + if (IsSkinned(child)) + { + FlattenAllTransforms(child); + } + } + } + + /// + /// Recursively flatten all transforms from this node down. + /// + private void FlattenAllTransforms(NodeContent node) + { + // Bake the local transform into the actual geometry. + MeshHelper.TransformScene(node, node.Transform); + + // Having baked it, we can now set the local coordinate system back to identity. + node.Transform = Matrix.Identity; + + foreach (var child in node.Children) + { + FlattenAllTransforms(child); + } + } + + /// + /// 3D Studio Max includes an extra help bone at the end of each IK chain that doesn't effect the skinning system and + /// is redundant as far as any game is concerned. This function looks for children who's name ends with "Nub" and + /// removes them from the hierarchy. + /// + /// Root of the skeleton tree. + private void TrimSkeleton(NodeContent skeleton) + { + var toDelete = new List(); + + foreach (var child in skeleton.Children) + { + if (child.Name.EndsWith("Nub") || child.Name.EndsWith("Footsteps")) + { + toDelete.Add(child); + } + else + { + TrimSkeleton(child); + } + } + + foreach (var child in toDelete) + { + skeleton.Children.Remove(child); + } + } + + /// + /// Determine if a node is a skinned node, meaning it has bone weights associated with it. + /// + private bool IsSkinned(NodeContent node) + { + // It has to be a MeshContent node. + if (node is MeshContent mesh) + // In the geometry we have to find a vertex channel that has a bone weight collection. + { + foreach (var geometry in mesh.Geometry) + { + foreach (var vertexChannel in geometry.Vertices.Channels) + { + if (vertexChannel is VertexChannel) + { + return true; + } + } + } + } + + return false; + } + + /// + /// If a node is skinned, we need to use the skinned model effect rather than basic effect. This function runs through + /// the geometry and finds the meshes that have bone weights associated and swaps in the skinned effect. + /// + private void SwapSkinnedMaterial(NodeContent node) + { + // It has to be a MeshContent node. + if (node is MeshContent mesh) + // In the geometry we have to find a vertex channel that has a bone weight collection. + { + foreach (var geometry in mesh.Geometry) + { + var swap = false; + foreach (var vertexChannel in geometry.Vertices.Channels) + { + if (vertexChannel is VertexChannel) + { + swap = true; + break; + } + } + + if (swap) + { + if (_toSkinnedMaterial.ContainsKey(geometry.Material)) + { + // We have already swapped it. + geometry.Material = _toSkinnedMaterial[geometry.Material]; + } + else + { + var skinnedMaterial = new SkinnedMaterialContent(); + + // Copy over the data. + if (geometry.Material is BasicMaterialContent basicMaterial) + { + skinnedMaterial.Alpha = basicMaterial.Alpha; + skinnedMaterial.DiffuseColor = basicMaterial.DiffuseColor; + skinnedMaterial.EmissiveColor = basicMaterial.EmissiveColor; + skinnedMaterial.SpecularColor = basicMaterial.SpecularColor; + skinnedMaterial.SpecularPower = basicMaterial.SpecularPower; + skinnedMaterial.Texture = basicMaterial.Texture; + } + + skinnedMaterial.WeightsPerVertex = 4; + + _toSkinnedMaterial[geometry.Material] = skinnedMaterial; + geometry.Material = skinnedMaterial; + } + } + } + } + + foreach (var child in node.Children) + { + SwapSkinnedMaterial(child); + } + } + + /// + /// Entry point for animation processing. + /// + private void ProcessAnimations(ModelContent model, NodeContent input, ContentProcessorContext context) + { + // First build a lookup table so we can determine the index into the list of bones from a bone name. + for (var i = 0; i < model.Bones.Count; i++) + { + _bones[model.Bones[i].Name] = i; + } + + // For saving the bone transforms. + _boneTransforms = new Matrix[model.Bones.Count]; + + // Collect up all of the animation data. + ProcessAnimationsRecursive(input); + + // Ensure there is always a clip, even if none is included in the FBX. + // That way we can create poses using FBX files as one-frame animation clips. + if (_modelExtra.Clips.Count == 0) + { + var clip = new AnimationClip(); + _modelExtra.Clips.Add(clip); + + var clipName = "Take 001"; + + // Retain by name. + _clips[clipName] = clip; + + clip.Name = clipName; + foreach (var bone in model.Bones) + { + var clipBone = new Bone(); + clipBone.Name = bone.Name; + + clip.Bones.Add(clipBone); + } + } + + // Ensure all animations have a first key frame for every bone. + foreach (var clip in _modelExtra.Clips) + { + for (var b = 0; b < _bones.Count; b++) + { + var keyframes = clip.Bones[b].Keyframes; + if (keyframes.Count == 0 || keyframes[0].Time > 0) + { + var keyframe = new Keyframe(); + keyframe.Time = 0; + keyframe.Transform = _boneTransforms[b]; + keyframes.Insert(0, keyframe); + } + } + } + } + + /// + /// Recursive function that processes the entire scene graph, collecting up all of the animation data. + /// + private void ProcessAnimationsRecursive(NodeContent input) + { + // Look up the bone for this input channel. + if (_bones.TryGetValue(input.Name, out var inputBoneIndex)) + { + // Save the transform. + _boneTransforms[inputBoneIndex] = input.Transform; + } + + foreach (var animation in input.Animations) + { + // Do we have this animation before? + var clipName = animation.Key; + + if (!_clips.TryGetValue(clipName, out var clip)) + { + // Never seen before clip. + clip = new AnimationClip(); + _modelExtra.Clips.Add(clip); + + // Retain by name. + _clips[clipName] = clip; + + clip.Name = clipName; + foreach (var bone in _model.Bones) + { + var clipBone = new Bone(); + clipBone.Name = bone.Name; + + clip.Bones.Add(clipBone); + } + } + + // Ensure the duration is always set. + if (animation.Value.Duration.TotalSeconds > clip.Duration) + { + clip.Duration = animation.Value.Duration.TotalSeconds; + } + + // For each channel, determine the bone and then process all of the keyframes for that bone. + + foreach (var channel in animation.Value.Channels) + { + // What is the bone index? + if (!_bones.TryGetValue(channel.Key, out var boneIndex)) + { + continue; // Ignore if not a named bone. + } + + // An animation is useless if it is for a bone not assigned to any meshes at all. + if (UselessAnimationTest(boneIndex)) + { + continue; + } + + // I'm collecting up in a linked list so we can process the data and remove redundant keyframes. + var keyframes = new LinkedList(); + foreach (var keyframe in channel.Value) + { + // Keyframe transformation. + var transform = keyframe.Transform; + + var newKeyframe = new Keyframe(); + newKeyframe.Time = keyframe.Time.TotalSeconds; + newKeyframe.Transform = transform; + + keyframes.AddLast(newKeyframe); + } + + LinearKeyframeReduction(keyframes); + foreach (var keyframe in keyframes) + { + clip.Bones[boneIndex].Keyframes.Add(keyframe); + } + } + } + + foreach (var child in input.Children) + { + ProcessAnimationsRecursive(child); + } + } + + /// + /// This function filters out keyframes that can be approximated well with linear interpolation. + /// + private void LinearKeyframeReduction(LinkedList keyframes) + { + if (keyframes.Count < 3) + { + return; + } + + for (var node = keyframes.First.Next;;) + { + var next = node.Next; + if (next == null) + { + break; + } + + // Determine nodes before and after the current node. + var a = node.Previous.Value; + var b = node.Value; + var c = next.Value; + + var t = (float)((node.Value.Time - node.Previous.Value.Time) / + (next.Value.Time - node.Previous.Value.Time)); + + var translation = Vector3.Lerp(a.Translation, c.Translation, t); + var rotation = Quaternion.Slerp(a.Rotation, c.Rotation, t); + + if ((translation - b.Translation).LengthSquared() < TinyLength && + Quaternion.Dot(rotation, b.Rotation) > TinyCosAngle) + { + keyframes.Remove(node); + } + + node = next; + } + } + + /// + /// Discard any animation not assigned to a mesh or the skeleton. + /// + private bool UselessAnimationTest(int boneId) + { + // If any mesh is assigned to this bone, it is not useless. + foreach (var mesh in _model.Meshes) + { + if (mesh.ParentBone.Index == boneId) + { + return false; + } + } + + // If this bone is in the skeleton, it is not useless. + foreach (var b in _modelExtra.Skeleton) + { + if (boneId == b) + { + return false; + } + } + + // Otherwise, it is useless. + return true; + } +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/PipelineExtension/CustomPipelineManager.cs b/TGC.MonoGame.Samples/Animations/PipelineExtension/CustomPipelineManager.cs new file mode 100644 index 0000000..4e68e3e --- /dev/null +++ b/TGC.MonoGame.Samples/Animations/PipelineExtension/CustomPipelineManager.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; +using MonoGame.Framework.Content.Pipeline.Builder; + +namespace TGC.MonoGame.Samples.Animations.PipelineExtension; + +/// +/// TODO This class needs a refactor. +/// This class allows you to build content in runtime without the need of adding the resources in the mgcb. +/// Currently, it only builds fbx with animations. +/// +public class CustomPipelineManager : PipelineManager +{ + public CustomPipelineManager(string projectDir, string outputDir, string intermediateDir, + IConfigurationRoot configuration) : base(projectDir, outputDir, intermediateDir) + { + Configuration = configuration; + } + + private IConfigurationRoot Configuration { get; } + + /// + /// Provides methods for writing compiled binary format. + /// + public ContentCompiler Compiler { get; private set; } + + public static CustomPipelineManager CreateCustomPipelineManager(IConfigurationRoot configuration) + { + var binFolder = configuration["BinFolder"]; + var objFolder = configuration["ObjFolder"]; + var contentFolder = configuration["ContentFolder"]; + + // This code is from MonoGame.Content.Builder.BuildContent.Build(out int successCount, out int errorCount). + var projectDirectory = PathHelper.Normalize(Directory.GetCurrentDirectory()); + var projectContentDirectory = + PathHelper.Normalize(Path.GetFullPath(Path.Combine(projectDirectory, "../../../" + contentFolder))); + var outputPath = PathHelper.Normalize(Path.Combine(projectDirectory, contentFolder)); + var projectDirectoryParts = projectDirectory.Split(new[] { binFolder }, StringSplitOptions.None); + var intermediatePath = PathHelper.Normalize(Path.GetFullPath(Path.Combine(projectContentDirectory, + "../" + objFolder + projectDirectoryParts.Last()))); + + return new CustomPipelineManager(projectContentDirectory, outputPath, intermediatePath, configuration); + } + + public void BuildAnimationContent(string modelFilename) + { + var importContext = new PipelineImporterContext(this); + var importer = new FbxImporter(); + var nodeContent = + importer.Import(ProjectDirectory + modelFilename + Configuration["FbxExtension"], importContext); + var animationProcessor = new AnimationProcessor(); + + var parameters = new OpaqueDataDictionary + { + { "ColorKeyColor", "0,0,0,0" }, + { "ColorKeyEnabled", "True" }, + { "DefaultEffect", "BasicEffect" }, + { "GenerateMipmaps", "True" }, + { "GenerateTangentFrames", "False" }, + { "PremultiplyTextureAlpha", "True" }, + { "PremultiplyVertexColors", "True" }, + { "ResizeTexturesToPowerOfTwo", "False" }, + { "RotationX", "0" }, + { "RotationY", "0" }, + { "RotationZ", "0" }, + { "Scale", "1" }, + { "SwapWindingOrder", "False" }, + { "TextureFormat", "Compressed" } + }; + + // Record what we're building and how. + var pipelineEvent = new PipelineBuildEvent + { + SourceFile = modelFilename, + DestFile = OutputDirectory + modelFilename + Configuration["ContentExtension"], + Importer = Configuration["FbxImporterName"], + Processor = Configuration["ProcessorName"], + Parameters = ValidateProcessorParameters(Configuration["ProcessorName"], parameters) + }; + + var processContext = new PipelineProcessorContext(this, pipelineEvent); + var modelContent = animationProcessor.Process(nodeContent, processContext); + + // Write the content to disk. + WriteXnb(modelContent, pipelineEvent); + } + + private void WriteXnb(object content, PipelineBuildEvent pipelineEvent) + { + // Make sure the output directory exists. + var outputFileDir = Path.GetDirectoryName(pipelineEvent.DestFile); + + Directory.CreateDirectory(outputFileDir); + + Compiler ??= new ContentCompiler(); + + // Write the XNB. + using (var stream = new FileStream(pipelineEvent.DestFile, FileMode.Create, FileAccess.Write, FileShare.None)) + { + Compiler.Compile(stream, content, Platform, Profile, CompressContent, OutputDirectory, outputFileDir); + } + + // Store the last write time of the output XNB here so we can verify it hasn't been tampered with. + pipelineEvent.DestTime = File.GetLastWriteTime(pipelineEvent.DestFile); + } +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Idle-2.fbx b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Idle-2.fbx new file mode 100644 index 0000000..af495ea --- /dev/null +++ b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Idle-2.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af6baed4a483247dbfe0c2e9f74ee5440fecfe9d3ce4b3939db54fba6974dea2 +size 851488 diff --git a/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Idle.fbx b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Idle.fbx new file mode 100644 index 0000000..b6f78c6 --- /dev/null +++ b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Idle.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eeb59c89dcce7449c3f267c0e2430ee99d0a13341930524b18181c72607e2df0 +size 664832 diff --git a/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Standard-Run.fbx b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Standard-Run.fbx new file mode 100644 index 0000000..499ac4b --- /dev/null +++ b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Standard-Run.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf202c523868ae5e90a4a059ca0c1a50b9b0ce6727b30072d3079371e598f71f +size 556368 diff --git a/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Standard-Walk.fbx b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Standard-Walk.fbx new file mode 100644 index 0000000..51b79ba --- /dev/null +++ b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/Standard-Walk.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5603540e9d506d86e6d47e64f1e44571358839709a2585eeb25022561276dd36 +size 598960 diff --git a/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/T-Pose.fbx b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/T-Pose.fbx new file mode 100644 index 0000000..abc701a --- /dev/null +++ b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/T-Pose.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4aea7122d2068895d944605f84faf1926ff9b7513fb8054c313cefea991d9bb3 +size 889436 diff --git a/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/uvw.jpg b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/uvw.jpg new file mode 100644 index 0000000..66c438f --- /dev/null +++ b/TGC.MonoGame.Samples/Content/3D/tgcito-classic/skeleton/uvw.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e972342402ee6bb84b5338d148b84495ce709bdc6d010964bc82d5e5807dfed +size 148940 diff --git a/TGC.MonoGame.Samples/Samples/Animations/AnimationType.cs b/TGC.MonoGame.Samples/Samples/Animations/AnimationType.cs new file mode 100644 index 0000000..2db6a3d --- /dev/null +++ b/TGC.MonoGame.Samples/Samples/Animations/AnimationType.cs @@ -0,0 +1,9 @@ +namespace TGC.MonoGame.Samples.Samples.Animations; + +public enum AnimationType +{ + Idle = 0, + Idle2 = 1, + StandardRun = 2, + StandardWalk = 3 +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Samples/Animations/SkinnedAnimation.cs b/TGC.MonoGame.Samples/Samples/Animations/SkinnedAnimation.cs new file mode 100644 index 0000000..f4898b4 --- /dev/null +++ b/TGC.MonoGame.Samples/Samples/Animations/SkinnedAnimation.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Xna.Framework; +using TGC.MonoGame.Samples.Animations.Models; +using TGC.MonoGame.Samples.Animations.PipelineExtension; +using TGC.MonoGame.Samples.Cameras; +using TGC.MonoGame.Samples.Viewer; + +namespace TGC.MonoGame.Samples.Samples.Animations; + +/// +/// Skinned Animation: +/// Example of skeletal animation. +/// To build the animations we use CustomPipelineManager class. +/// Animations can be easily generated in Mixamo - https://www.mixamo.com. +/// Author: Rene Juan Rico Mendoza. +/// +public class SkinnedAnimation : TGCSample +{ + public SkinnedAnimation(TGCViewer game) : base(game) + { + Category = TGCSampleCategory.Animations; + Name = "Skinned Skeletal Animation"; + Description = "A Better Skinned Sample."; + } + + /// + /// The Camera we use. + /// + private Camera Camera { get; set; } + + /// + /// Application configuration file. + /// + private IConfigurationRoot Configuration { get; set; } + + private List ModelAnimationFileNames { get; set; } + + private List ModelFileNames { get; set; } + + /// + /// This Model is loaded solely for the Animation animation. + /// + private AnimatedModel Animation { get; set; } + + /// + /// The animated Model we are displaying. + /// + private AnimatedModel Model { get; set; } + + /// + /// Allows the game to perform any initialization it needs to before starting to run. + /// This is where it can query for any required services and load any non-graphic related content. + /// Calling base.Initialize will enumerate through any components and initialize them as well. + /// + public override void Initialize() + { + // Configuration file. + var configurationFileName = "app-settings.json"; + Configuration = new ConfigurationBuilder().AddJsonFile(configurationFileName, true, true).Build(); + + Camera = new SimpleCamera(GraphicsDevice.Viewport.AspectRatio, new Vector3(0, 100, 500), 30, 0.5f); + + base.Initialize(); + } + + /// + /// LoadContent will be called once per game and is the place to load all of your content. + /// + protected override void LoadContent() + { + // File names of models and animations. + var skeletonFolder = ContentFolder3D + "tgcito-classic/skeleton/"; + ModelFileNames = new List { skeletonFolder + "T-Pose" }; + ModelAnimationFileNames = new List + { + skeletonFolder + "Idle", + skeletonFolder + "Idle-2", + skeletonFolder + "Standard-Run", + skeletonFolder + "Standard-Walk" + }; + + // Build content. + var manager = CustomPipelineManager.CreateCustomPipelineManager(Configuration); + + foreach (var model in ModelFileNames) + { + manager.BuildAnimationContent(model); + } + + foreach (var animation in ModelAnimationFileNames) + { + manager.BuildAnimationContent(animation); + } + + // Load the Model we will display. + Model = new AnimatedModel(ModelFileNames[0]); + Model.LoadContent(Game.Content); + + ModifierController.AddOptions("Animation", AnimationType.Idle, OnAnimationChange); + } + + /// + /// Allows the game to run logic such as updating the world, checking for collisions, gathering input, and playing + /// audio. + /// + /// Provides a snapshot of timing values. + public override void Update(GameTime gameTime) + { + Model.Update(gameTime); + Camera.Update(gameTime); + + base.Update(gameTime); + } + + /// + /// This is called when the game should draw itself. + /// + /// Provides a snapshot of timing values. + public override void Draw(GameTime gameTime) + { + Game.Background = Color.CornflowerBlue; + + Model.Draw(Camera, Matrix.Identity); + + base.Draw(gameTime); + } + + /// + /// Processes a change in the animation selected. + /// + /// The animation to play. + private void OnAnimationChange(AnimationType animation) + { + // Load the Model that has an animation clip it in. + Animation = new AnimatedModel(ModelAnimationFileNames[Convert.ToInt32(animation)]); + Animation.LoadContent(Game.Content); + + // Obtain the clip we want to play. + // I'm using an absolute index, because XNA 4.0 won't allow you to have more than one animation associated with a Model, anyway. + // It would be easy to add code to look up the clip by name and to index it by name in the Model. + var clip = Animation.Clips[0]; + + // And play the clip. + var player = Model.PlayClip(clip); + player.Looping = true; + } + + /// + /// UnloadContent will be called once per game and is the place to unload all content. + /// + protected override void UnloadContent() + { + // Unload any non ContentManager content here. + } +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Samples/Audio/SoundStatic.cs b/TGC.MonoGame.Samples/Samples/Audio/SoundStatic.cs index 94840ad..bff8581 100644 --- a/TGC.MonoGame.Samples/Samples/Audio/SoundStatic.cs +++ b/TGC.MonoGame.Samples/Samples/Audio/SoundStatic.cs @@ -19,7 +19,7 @@ public class SoundStatic : TGCSample public SoundStatic(TGCViewer game) : base(game) { Category = TGCSampleCategory.Audio; - Name = "Sound effect"; + Name = "Sound Effect"; Description = "Shows how to play a sound file. Audio from https://www.fesliyanstudios.com"; } diff --git a/TGC.MonoGame.Samples/Samples/Shaders/SkyBox/SimpleSkyBox.cs b/TGC.MonoGame.Samples/Samples/Shaders/SkyBox/SimpleSkyBox.cs index 7acee8f..05039f4 100644 --- a/TGC.MonoGame.Samples/Samples/Shaders/SkyBox/SimpleSkyBox.cs +++ b/TGC.MonoGame.Samples/Samples/Shaders/SkyBox/SimpleSkyBox.cs @@ -8,11 +8,11 @@ namespace TGC.MonoGame.Samples.Samples.Shaders.SkyBox /// /// Simple SkyBox: /// Units Involved: - /// # Unidad 4 - Texturas e Iluminación - SkyBox. + /// # Unit 4 - Textures and Lighting - SkyBox. /// # Unit 8 - Video Adapters - Shaders. /// Shows how to use a cube with a texture on each of its faces, which allows to achieve the effect of an enveloping /// sky in the scene. - /// Author: René Juan Rico Mendoza + /// Author: Rene Juan Rico Mendoza /// public class SimpleSkyBox : TGCSample { diff --git a/TGC.MonoGame.Samples/Samples/TGCSampleCategory.cs b/TGC.MonoGame.Samples/Samples/TGCSampleCategory.cs index 7f45ad6..3fada24 100644 --- a/TGC.MonoGame.Samples/Samples/TGCSampleCategory.cs +++ b/TGC.MonoGame.Samples/Samples/TGCSampleCategory.cs @@ -5,6 +5,7 @@ /// public static class TGCSampleCategory { + public const string Animations = "Animations"; public const string Audio = "Audio"; public const string Collisions = "Collisions"; public const string CompleteSolutions = "Complete Solutions"; diff --git a/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial1.cs b/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial1.cs index 485ebc1..037c2e6 100644 --- a/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial1.cs +++ b/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial1.cs @@ -8,7 +8,7 @@ namespace TGC.MonoGame.Samples.Samples.Tutorials /// Tutorial 1: /// Shows how to create a triangle and display it on the screen. /// The classic hello world in graphics. - /// Author: René Juan Rico Mendoza. + /// Author: Rene Juan Rico Mendoza. /// public class Tutorial1 : TGCSample { diff --git a/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial2.cs b/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial2.cs index 02cb830..1ad56ce 100644 --- a/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial2.cs +++ b/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial2.cs @@ -9,7 +9,7 @@ namespace TGC.MonoGame.Samples.Samples.Tutorials /// /// Tutorial 2: /// Shows how to create a colored 3D box and display it on the screen. - /// Author: René Juan Rico Mendoza. + /// Author: Rene Juan Rico Mendoza. /// public class Tutorial2 : TGCSample { diff --git a/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial3.cs b/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial3.cs index 01661d6..9949bd9 100644 --- a/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial3.cs +++ b/TGC.MonoGame.Samples/Samples/Tutorials/Tutorial3.cs @@ -10,7 +10,7 @@ namespace TGC.MonoGame.Samples.Samples.Tutorials /// /// Tutorial 3: /// This sample shows how to draw 3D geometric primitives such as cubes, spheres and cylinders. - /// Author: René Juan Rico Mendoza. + /// Author: Rene Juan Rico Mendoza. /// public class Tutorial3 : TGCSample { diff --git a/TGC.MonoGame.Samples/TGC.MonoGame.Samples.csproj b/TGC.MonoGame.Samples/TGC.MonoGame.Samples.csproj index d307c41..cb4aabd 100644 --- a/TGC.MonoGame.Samples/TGC.MonoGame.Samples.csproj +++ b/TGC.MonoGame.Samples/TGC.MonoGame.Samples.csproj @@ -21,10 +21,17 @@ - + + + + + + Always + + diff --git a/TGC.MonoGame.Samples/Viewer/GUI/Modifiers/ModifierController.cs b/TGC.MonoGame.Samples/Viewer/GUI/Modifiers/ModifierController.cs index ac02b70..fe81cc2 100644 --- a/TGC.MonoGame.Samples/Viewer/GUI/Modifiers/ModifierController.cs +++ b/TGC.MonoGame.Samples/Viewer/GUI/Modifiers/ModifierController.cs @@ -1,289 +1,294 @@ -using ImGuiNET; +using System; +using System.Collections.Generic; +using ImGuiNET; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using System; -using System.Collections.Generic; -using System.Text; using TGC.MonoGame.Samples.Viewer.GUI.ImGuiNET; -namespace TGC.MonoGame.Samples.Viewer.GUI.Modifiers +namespace TGC.MonoGame.Samples.Viewer.GUI.Modifiers; + +public class ModifierController { - public class ModifierController + /// + /// Constructs a ModifierController, in charge for managing and drawing Modifiers + /// + public ModifierController() { - private List Modifiers { get; set; } - - private List TextureModifiers { get; set; } - - - /// - /// Constructs a ModifierController, in charge for managing and drawing Modifiers - /// - public ModifierController() - { - Modifiers = new List(); - TextureModifiers = new List(); - } + Modifiers = new List(); + TextureModifiers = new List(); + } - /// - /// Adds a Button Modifier. - /// - /// The text to display in the button - /// The action to execute when the button is pressed - /// If the button is enabled - public void AddButton(string text, Action onPress, bool enabled = true) - { - Modifiers.Add(new ButtonModifier(text, onPress, enabled)); - } + private List Modifiers { get; } - /// - /// Adds a Color Modifier with a given name, action on change and a default Color. - /// - /// The name of the modifier that will show on the GUI - /// An action to be called when the Color changes - /// The Color that the Color Modifier starts with - public void AddColor(string name, Action onChange, Color defaultColor) - { - Modifiers.Add(new ColorModifier(name, onChange, defaultColor)); - } + private List TextureModifiers { get; } - /// - /// Adds a Color Modifier with a given name, an , and a default Color. - /// - /// The name of the modifier that will show on the GUI - /// An that will recieve the Color as value - /// The Color that the Color Modifier starts with - public void AddColor(string name, EffectParameter effectParameter, Color defaultColor) - { - Modifiers.Add(new ColorModifier(name, effectParameter, defaultColor)); - } + /// + /// Adds a Button Modifier. + /// + /// The text to display in the button + /// The action to execute when the button is pressed + /// If the button is enabled + public void AddButton(string text, Action onPress, bool enabled = true) + { + Modifiers.Add(new ButtonModifier(text, onPress, enabled)); + } - /// - /// Adds a Float Modifier with a given name, an action on change and a default value. - /// - /// The name of the modifier that will show on the GUI - /// The action to be called when the value changes - /// The default value for the modifier - public void AddFloat(string name, Action onChange, float defaultValue) - { - Modifiers.Add(new FloatModifier(name, onChange, defaultValue)); - } + /// + /// Adds a Color Modifier with a given name, action on change and a default Color. + /// + /// The name of the modifier that will show on the GUI + /// An action to be called when the Color changes + /// The Color that the Color Modifier starts with + public void AddColor(string name, Action onChange, Color defaultColor) + { + Modifiers.Add(new ColorModifier(name, onChange, defaultColor)); + } - /// - /// Adds a Float Modifier with a given name, an action on change, a default value, and a minimum and maximum values for the float. - /// - /// The name of the modifier that will show on the GUI - /// The action to be called when the value changes - /// The default value for the modifier - /// The minimum value for the modifier - /// The maximum value for the modifier - public void AddFloat(string name, Action onChange, float defaultValue, float min, float max) - { - Modifiers.Add(new FloatModifier(name, onChange, defaultValue, min, max)); - } + /// + /// Adds a Color Modifier with a given name, an , and a default Color. + /// + /// The name of the modifier that will show on the GUI + /// An that will receive the Color as value + /// The Color that the Color Modifier starts with + public void AddColor(string name, EffectParameter effectParameter, Color defaultColor) + { + Modifiers.Add(new ColorModifier(name, effectParameter, defaultColor)); + } - /// - /// Adds a Float Modifier with a given name, an and a default value. - /// - /// The name of the modifier that will show on the GUI - /// An that will recieve the Float as value - /// The default value for the modifier - public void AddFloat(string name, EffectParameter effectParameter, float defaultValue) - { - Modifiers.Add(new FloatModifier(name, effectParameter, defaultValue)); - } + /// + /// Adds a Float Modifier with a given name, an action on change and a default value. + /// + /// The name of the modifier that will show on the GUI + /// The action to be called when the value changes + /// The default value for the modifier + public void AddFloat(string name, Action onChange, float defaultValue) + { + Modifiers.Add(new FloatModifier(name, onChange, defaultValue)); + } - /// - /// Adds a Float Modifier with a given name, an , a default value, and a minimum and maximum values for the float. - /// - /// The name of the modifier that will show on the GUI - /// An that will recieve the Float as value - /// The default value for the modifier - /// The minimum value for the modifier - /// The maximum value for the modifier - public void AddFloat(string name, EffectParameter effectParameter, float defaultValue, float min, float max) - { - Modifiers.Add(new FloatModifier(name, effectParameter, defaultValue, min, max)); - } + /// + /// Adds a Float Modifier with a given name, an action on change, a default value, and a minimum and maximum values for + /// the float. + /// + /// The name of the modifier that will show on the GUI + /// The action to be called when the value changes + /// The default value for the modifier + /// The minimum value for the modifier + /// The maximum value for the modifier + public void AddFloat(string name, Action onChange, float defaultValue, float min, float max) + { + Modifiers.Add(new FloatModifier(name, onChange, defaultValue, min, max)); + } + /// + /// Adds a Float Modifier with a given name, an and a default value. + /// + /// The name of the modifier that will show on the GUI + /// An that will receive the Float as value + /// The default value for the modifier + public void AddFloat(string name, EffectParameter effectParameter, float defaultValue) + { + Modifiers.Add(new FloatModifier(name, effectParameter, defaultValue)); + } + /// + /// Adds a Float Modifier with a given name, an , a default value, and a minimum and + /// maximum values for the float. + /// + /// The name of the modifier that will show on the GUI + /// An that will receive the Float as value + /// The default value for the modifier + /// The minimum value for the modifier + /// The maximum value for the modifier + public void AddFloat(string name, EffectParameter effectParameter, float defaultValue, float min, float max) + { + Modifiers.Add(new FloatModifier(name, effectParameter, defaultValue, min, max)); + } - /// - /// Adds an Options Modifiers with a name and option change listener. - /// This way to construct an Options Modifier uses the enum names as option names. - /// The default option is the first one on the enum type. - /// - /// The name of the modifier that will show on the GUI - /// An action to be called when the option value changes - public void AddOptions(string name, Action onChange) where EnumType : Enum - { - Modifiers.Add(new OptionsModifier(name, onChange)); - } + /// + /// Adds an Options Modifiers with a name and option change listener. + /// This way to construct an Options Modifier uses the enum names as option names. + /// The default option is the first one on the enum type. + /// + /// The name of the modifier that will show on the GUI + /// An action to be called when the option value changes + public void AddOptions(string name, Action onChange) where TEnumType : Enum + { + Modifiers.Add(new OptionsModifier(name, onChange)); + } - /// - /// Adds an Options Modifiers with a name, default option value and option change listener. - /// This way to construct an Options Modifier uses the enum names as option names. - /// - /// The name of the modifier that will show on the GUI - /// The default option value - /// An action to be called when the option value changes - public void AddOptions(string name, EnumType defaultValue, Action onChange) where EnumType : Enum - { - Modifiers.Add(new OptionsModifier(name, defaultValue, onChange)); - } + /// + /// Adds an Options Modifiers with a name, default option value and option change listener. + /// This way to construct an Options Modifier uses the enum names as option names. + /// + /// The name of the modifier that will show on the GUI + /// The default option value + /// An action to be called when the option value changes + public void AddOptions(string name, TEnumType defaultValue, Action onChange) + where TEnumType : Enum + { + Modifiers.Add(new OptionsModifier(name, defaultValue, onChange)); + } - /// - /// Adds an Options Modifiers with a name, option names, default option value and option change listener. - /// - /// The name of the modifier that will show on the GUI - /// The sorted option names array - /// The default option value - /// An action to be called when the option value changes - public void AddOptions(string name, string[] optionNames, EnumType defaultValue, Action onChange) where EnumType : Enum - { - Modifiers.Add(new OptionsModifier(name, optionNames, defaultValue, onChange)); - } + /// + /// Adds an Options Modifiers with a name, option names, default option value and option change listener. + /// + /// The name of the modifier that will show on the GUI + /// The sorted option names array + /// The default option value + /// An action to be called when the option value changes + public void AddOptions(string name, string[] optionNames, TEnumType defaultValue, + Action onChange) where TEnumType : Enum + { + Modifiers.Add(new OptionsModifier(name, optionNames, defaultValue, onChange)); + } + /// + /// Adds a Texture Modifier using a name and a texture to bind. + /// + /// The name of the texture that will show in the GUI + /// The texture to be bound to this object + public void AddTexture(string name, Texture2D texture) + { + var textureModifier = new TextureModifier(name, texture); + Modifiers.Add(textureModifier); + TextureModifiers.Add(textureModifier); + } - /// - /// Adds a Texture Modifier using a name and a texture to bind. - /// - /// The name of the texture that will show in the GUI - /// The texture to be bound to this object - public void AddTexture(string name, Texture2D texture) - { - var textureModifier = new TextureModifier(name, texture); - Modifiers.Add(textureModifier); - TextureModifiers.Add(textureModifier); - } + /// + /// Adds a Toggle Modifier with a given name, action and default value + /// + /// The name of the Toggle Modifier + /// An action to be called when the value of the modifier changes + /// The default value that this modifier will have + public void AddToggle(string name, Action onChange, bool defaultValue) + { + Modifiers.Add(new ToggleModifier(name, onChange, defaultValue)); + } - /// - /// Adds a Toggle Modifier with a given name, action and default value - /// - /// The name of the Toggle Modifier - /// An action to be called when the value of the modifier changes - /// The default value that this modifier will have - public void AddToggle(string name, Action onChange, bool defaultValue) - { - Modifiers.Add(new ToggleModifier(name, onChange, defaultValue)); - } + /// + /// Adds a Vector2 Modifier with a given name and action. + /// + /// The name that will show in the GUI + /// The action that will be called when the Vector2 changes + public void AddVector(string name, Action onChange) + { + Modifiers.Add(new Vector2Modifier(name, onChange)); + } - /// - /// Adds a Vector2 Modifier with a given name and action. - /// - /// The name that will show in the GUI - /// The action that will be called when the Vector2 changes - public void AddVector(string name, Action onChange) - { - Modifiers.Add(new Vector2Modifier(name, onChange)); - } + /// + /// Adds a Vector2 Modifier with a given name and action. + /// + /// The name that will show in the GUI + /// An that will receive the Vector3 as value + /// The default value that this modifier will have + public void AddVector(string name, EffectParameter effectParameter, Vector3 defaultValue) + { + Modifiers.Add(new Vector3Modifier(name, effectParameter, defaultValue)); + } - /// - /// Adds a Vector2 Modifier with a given name and action. - /// - /// The name that will show in the GUI - /// An that will recieve the Vector3 as value - /// The default value that this modifier will have - public void AddVector(string name, EffectParameter effectParameter, Vector3 defaultValue) - { - Modifiers.Add(new Vector3Modifier(name, effectParameter, defaultValue)); - } + /// + /// Creates a Vector3 Modifier with a given name, action and default value. + /// + /// The name that will show in the GUI + /// The action that will be called when the Vector3 changes + /// The Vector3 default value + public void AddVector(string name, Action onChange, Vector2 defaultValue) + { + Modifiers.Add(new Vector2Modifier(name, onChange, defaultValue)); + } + /// + /// Adds a Vector3 Modifier with a given name and action. + /// + /// The name that will show in the GUI + /// The action that will be called when the Vector3 changes + public void AddVector(string name, Action onChange) + { + Modifiers.Add(new Vector3Modifier(name, onChange)); + } - /// - /// Creates a Vector3 Modifier with a given name, action and default value. - /// - /// The name that will show in the GUI - /// The action that will be called when the Vector3 changes - /// The Vector3 default value - public void AddVector(string name, Action onChange, Vector2 defaultValue) - { - Modifiers.Add(new Vector2Modifier(name, onChange, defaultValue)); - } + /// + /// Creates a Vector3 Modifier with a given name, action and default value. + /// + /// The name that will show in the GUI + /// The action that will be called when the Vector3 changes + /// The Vector3 default value + public void AddVector(string name, Action onChange, Vector3 defaultValue) + { + Modifiers.Add(new Vector3Modifier(name, onChange, defaultValue)); + } - /// - /// Adds a Vector3 Modifier with a given name and action. - /// - /// The name that will show in the GUI - /// The action that will be called when the Vector3 changes - public void AddVector(string name, Action onChange) - { - Modifiers.Add(new Vector3Modifier(name, onChange)); - } + /// + /// Adds a Vector4 Modifier with a given name and action. + /// + /// The name that will show in the GUI + /// The action that will be called when the Vector4 changes + public void AddVector(string name, Action onChange) + { + Modifiers.Add(new Vector4Modifier(name, onChange)); + } - /// - /// Creates a Vector3 Modifier with a given name, action and default value. - /// - /// The name that will show in the GUI - /// The action that will be called when the Vector3 changes - /// The Vector3 default value - public void AddVector(string name, Action onChange, Vector3 defaultValue) - { - Modifiers.Add(new Vector3Modifier(name, onChange, defaultValue)); - } + /// + /// Creates a Vector4 Modifier with a given name, action and default value. + /// + /// The name that will show in the GUI + /// The action that will be called when the Vector4 changes + /// The Vector4 default value + public void AddVector(string name, Action onChange, Vector4 defaultValue) + { + Modifiers.Add(new Vector4Modifier(name, onChange, defaultValue)); + } - /// - /// Adds a Vector4 Modifier with a given name and action. - /// - /// The name that will show in the GUI - /// The action that will be called when the Vector4 changes - public void AddVector(string name, Action onChange) - { - Modifiers.Add(new Vector4Modifier(name, onChange)); - } + /// + /// Clears the previously built modifiers. + /// + public void Clear() + { + Modifiers.Clear(); + TextureModifiers.Clear(); + } - /// - /// Creates a Vector4 Modifier with a given name, action and default value. - /// - /// The name that will show in the GUI - /// The action that will be called when the Vector4 changes - /// The Vector4 default value - public void AddVector(string name, Action onChange, Vector4 defaultValue) + /// + /// Binds the Modifiers that need to be bound to the ImGuiRenderer. + /// + /// The ImGuiRenderer to bind the modifiers to + public void Bind(ImGuiRenderer renderer) + { + var count = TextureModifiers.Count; + for (var index = 0; index < count; index++) { - Modifiers.Add(new Vector4Modifier(name, onChange, defaultValue)); + TextureModifiers[index].Bind(renderer); } + } - /// - /// Clears the previously built modifiers. - /// - public void Clear() + /// + /// Unbinds the Modifiers that were previously bound to the ImGuiRenderer. + /// + /// The ImGuiRenderer to unbind the modifiers + public void Unbind(ImGuiRenderer renderer) + { + var count = TextureModifiers.Count; + for (var index = 0; index < count; index++) { - Modifiers.Clear(); - TextureModifiers.Clear(); + TextureModifiers[index].Unbind(renderer); } + } - /// - /// Binds the Modifiers that need to be bound to the ImGuiRenderer. - /// - /// The ImGuiRenderer to bind the modifiers to - public void Bind(ImGuiRenderer renderer) - { - var count = TextureModifiers.Count; - for (var index = 0; index < count; index++) - TextureModifiers[index].Bind(renderer); - } + /// + /// Draws the modifiers + /// + public void Draw() + { + ImGui.Spacing(); - /// - /// Unbinds the Modifiers that were previously bound to the ImGuiRenderer. - /// - /// The ImGuiRenderer to unbind the modifiers - public void Unbind(ImGuiRenderer renderer) + var count = Modifiers.Count; + if (count > 0 && ImGui.CollapsingHeader("Modifiers", ImGuiTreeNodeFlags.DefaultOpen)) { - var count = TextureModifiers.Count; for (var index = 0; index < count; index++) - TextureModifiers[index].Unbind(renderer); - } - - /// - /// Draws the modifiers - /// - public void Draw() - { - ImGui.Spacing(); - - var count = Modifiers.Count; - if (count > 0 && ImGui.CollapsingHeader("Modifiers", ImGuiTreeNodeFlags.DefaultOpen)) - for (var index = 0; index < count; index++) - Modifiers[index].Draw(); + { + Modifiers[index].Draw(); + } } } -} +} \ No newline at end of file diff --git a/TGC.MonoGame.Samples/app-settings.json b/TGC.MonoGame.Samples/app-settings.json new file mode 100644 index 0000000..b8781bf --- /dev/null +++ b/TGC.MonoGame.Samples/app-settings.json @@ -0,0 +1,9 @@ +{ + "BinFolder": "bin/", + "ObjFolder": "obj/", + "ContentExtension": ".xnb", + "ContentFolder": "Content/", + "FbxExtension": ".fbx", + "FbxImporterName": "FbxImporter", + "ProcessorName": "Animation Processor" +} diff --git a/docs/install/install-windows.md b/docs/install/install-windows.md index e39a43e..4c8790b 100644 --- a/docs/install/install-windows.md +++ b/docs/install/install-windows.md @@ -61,6 +61,11 @@ winget install JetBrains.Rider winget install Microsoft.VisualStudio.2022.Community ``` +#### Extensions + +* [HLSL Tools](https://marketplace.visualstudio.com/items?itemName=TimGJones.HLSLToolsforVisualStudio) +* [MonoGame](https://marketplace.visualstudio.com/items?itemName=MonoGame.MonoGame-Templates-VSExtension) + ## Set up tgc-monogame-samples ```bash From 6e68d971a72b4eb3f8f88e87c6d7750b593165da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Juan=20Rico=20Mendoza?= Date: Wed, 31 May 2023 00:53:38 -0300 Subject: [PATCH 2/4] Fix some quality issues --- .../Animations/Models/AnimatedModel.cs | 6 +++- .../PipelineExtension/AnimationProcessor.cs | 24 ++++++++-------- .../Samples/TGCSampleCategory.cs | 28 +++++++++---------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs b/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs index 89db1d9..4f06204 100644 --- a/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs +++ b/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; @@ -126,6 +127,9 @@ public void Draw(Camera camera, Matrix world) skinnedEffect.PreferPerPixelLighting = true; skinnedEffect.SetBoneTransforms(skeleton); break; + + default: + throw new InvalidOperationException("Unexpected Effect type = " + effect.GetType().FullName); } } diff --git a/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs b/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs index 47f5ad5..ae040e7 100644 --- a/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs +++ b/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content.Pipeline; using Microsoft.Xna.Framework.Content.Pipeline.Graphics; @@ -54,7 +55,7 @@ public override ModelContent Process(NodeContent input, ContentProcessorContext { // Skeleton Support. // Process the skeleton for skinned character animation. - var skeleton = ProcessSkeleton(input); + ProcessSkeleton(input); // Skinned Support. SwapSkinnedMaterial(input); @@ -63,7 +64,7 @@ public override ModelContent Process(NodeContent input, ContentProcessorContext _model = base.Process(input, context); // Animation Support. - ProcessAnimations(_model, input, context); + ProcessAnimations(_model, input); // Add the extra content to the model. _model.Tag = _modelExtra; @@ -74,14 +75,14 @@ public override ModelContent Process(NodeContent input, ContentProcessorContext /// /// Process the skeleton in support of skeletal animation. /// - private BoneContent ProcessSkeleton(NodeContent input) + private void ProcessSkeleton(NodeContent input) { // Find the skeleton. var skeleton = MeshHelper.FindSkeleton(input); if (skeleton == null) { - return null; + return; } // We don't want to have to worry about different parts of the model being in different local coordinate systems, so let's just bake everything. @@ -106,8 +107,6 @@ private BoneContent ProcessSkeleton(NodeContent input) { _modelExtra.Skeleton.Add(nodeToIndex[bone]); } - - return skeleton; } /// @@ -210,7 +209,7 @@ private bool IsSkinned(NodeContent node) { // It has to be a MeshContent node. if (node is MeshContent mesh) - // In the geometry we have to find a vertex channel that has a bone weight collection. + // In the geometry we have to find a vertex channel that has a bone weight collection. { foreach (var geometry in mesh.Geometry) { @@ -235,7 +234,7 @@ private void SwapSkinnedMaterial(NodeContent node) { // It has to be a MeshContent node. if (node is MeshContent mesh) - // In the geometry we have to find a vertex channel that has a bone weight collection. + // In the geometry we have to find a vertex channel that has a bone weight collection. { foreach (var geometry in mesh.Geometry) { @@ -289,7 +288,7 @@ private void SwapSkinnedMaterial(NodeContent node) /// /// Entry point for animation processing. /// - private void ProcessAnimations(ModelContent model, NodeContent input, ContentProcessorContext context) + private void ProcessAnimations(ModelContent model, NodeContent input) { // First build a lookup table so we can determine the index into the list of bones from a bone name. for (var i = 0; i < model.Bones.Count; i++) @@ -438,7 +437,9 @@ private void LinearKeyframeReduction(LinkedList keyframes) return; } - for (var node = keyframes.First.Next;;) + var node = keyframes.First.Next; + + while (node != null) { var next = node.Next; if (next == null) @@ -451,8 +452,7 @@ private void LinearKeyframeReduction(LinkedList keyframes) var b = node.Value; var c = next.Value; - var t = (float)((node.Value.Time - node.Previous.Value.Time) / - (next.Value.Time - node.Previous.Value.Time)); + var t = (float)((node.Value.Time - a.Time) / (next.Value.Time - a.Time)); var translation = Vector3.Lerp(a.Translation, c.Translation, t); var rotation = Quaternion.Slerp(a.Rotation, c.Rotation, t); diff --git a/TGC.MonoGame.Samples/Samples/TGCSampleCategory.cs b/TGC.MonoGame.Samples/Samples/TGCSampleCategory.cs index 3fada24..027d2c0 100644 --- a/TGC.MonoGame.Samples/Samples/TGCSampleCategory.cs +++ b/TGC.MonoGame.Samples/Samples/TGCSampleCategory.cs @@ -5,19 +5,19 @@ /// public static class TGCSampleCategory { - public const string Animations = "Animations"; - public const string Audio = "Audio"; - public const string Collisions = "Collisions"; - public const string CompleteSolutions = "Complete Solutions"; - public const string Heightmaps = "Heightmaps"; - public const string Models = "Models"; - public const string Optimizations = "Optimizations"; - public const string PBR = "PBR"; - public const string Physics = "Physics"; - public const string PostProcessing = "Post Processing"; - public const string RenderPipeline = "Render Pipeline"; - public const string Shaders = "Shaders"; - public const string Transformations = "Transformations"; - public const string Tutorials = "Tutorials"; + public static readonly string Animations = "Animations"; + public static readonly string Audio = "Audio"; + public static readonly string Collisions = "Collisions"; + public static readonly string CompleteSolutions = "Complete Solutions"; + public static readonly string Heightmaps = "Heightmaps"; + public static readonly string Models = "Models"; + public static readonly string Optimizations = "Optimizations"; + public static readonly string PBR = "PBR"; + public static readonly string Physics = "Physics"; + public static readonly string PostProcessing = "Post Processing"; + public static readonly string RenderPipeline = "Render Pipeline"; + public static readonly string Shaders = "Shaders"; + public static readonly string Transformations = "Transformations"; + public static readonly string Tutorials = "Tutorials"; } } \ No newline at end of file From 4eca56805082ea4b8ed2b842a52810a59b801a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Juan=20Rico=20Mendoza?= Date: Thu, 5 Oct 2023 01:23:51 -0300 Subject: [PATCH 3/4] Fix some review comments --- .../DataTypes/{Bone.cs => AnimationBone.cs} | 2 +- .../Animations/DataTypes/AnimationClip.cs | 2 +- .../Animations/Models/AnimatedModel.cs | 109 +++++++----------- .../Animations/Models/AnimationPlayer.cs | 50 +++----- .../Animations/Models/Bone.cs | 96 +++++++-------- .../Animations/Models/BoneInfo.cs | 84 +++++++------- .../PipelineExtension/AnimationProcessor.cs | 9 +- .../CustomPipelineManager.cs | 19 +-- TGC.MonoGame.Samples/Geometries/Arrow.cs | 8 +- .../Samples/Animations/SkinnedAnimation.cs | 2 +- .../Heightmaps/SimpleTerrain/SimpleTerrain.cs | 8 +- 11 files changed, 177 insertions(+), 212 deletions(-) rename TGC.MonoGame.Samples/Animations/DataTypes/{Bone.cs => AnimationBone.cs} (95%) diff --git a/TGC.MonoGame.Samples/Animations/DataTypes/Bone.cs b/TGC.MonoGame.Samples/Animations/DataTypes/AnimationBone.cs similarity index 95% rename from TGC.MonoGame.Samples/Animations/DataTypes/Bone.cs rename to TGC.MonoGame.Samples/Animations/DataTypes/AnimationBone.cs index 2a53f50..42c24f7 100644 --- a/TGC.MonoGame.Samples/Animations/DataTypes/Bone.cs +++ b/TGC.MonoGame.Samples/Animations/DataTypes/AnimationBone.cs @@ -5,7 +5,7 @@ namespace TGC.MonoGame.Samples.Animations.DataTypes; /// /// Keyframes are grouped per bone for an animation clip. /// -public class Bone +public class AnimationBone { /// /// The keyframes for this bone. diff --git a/TGC.MonoGame.Samples/Animations/DataTypes/AnimationClip.cs b/TGC.MonoGame.Samples/Animations/DataTypes/AnimationClip.cs index c3bcb96..d825575 100644 --- a/TGC.MonoGame.Samples/Animations/DataTypes/AnimationClip.cs +++ b/TGC.MonoGame.Samples/Animations/DataTypes/AnimationClip.cs @@ -10,7 +10,7 @@ public class AnimationClip /// /// The bones for this animation clip with their keyframes. /// - public List Bones { get; set; } = new(); + public List Bones { get; set; } = new(); /// /// Duration of the animation clip. diff --git a/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs b/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs index 4f06204..d60bf3b 100644 --- a/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs +++ b/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using TGC.MonoGame.Samples.Animations.DataTypes; -using TGC.MonoGame.Samples.Cameras; namespace TGC.MonoGame.Samples.Animations.Models; @@ -14,44 +11,44 @@ namespace TGC.MonoGame.Samples.Animations.Models; /// public class AnimatedModel { - /// - /// Creates the Model from an XNA Model. - /// - /// The name of the asset for this Model. - public AnimatedModel(string assetName) - { - AssetName = assetName; - } - /// /// The Model asset name. /// - private string AssetName { get; } + private readonly string _assetName; /// /// The underlying Bones for the Model. /// - private List Bones { get; } = new(); + private readonly List _bones = new(); /// /// The actual underlying XNA Model. /// - private Model Model { get; set; } + private Model _model; /// /// Extra data associated with the XNA Model. /// - private ModelExtra ModelExtra { get; set; } + private ModelExtra _modelExtra; /// /// An associated animation clip Player. /// - private AnimationPlayer Player { get; set; } + private AnimationPlayer _player; + + /// + /// Creates the Model from an XNA Model. + /// + /// The name of the asset for this Model. + public AnimatedModel(string assetName) + { + _assetName = assetName; + } /// /// The Model animation clips. /// - public List Clips => ModelExtra.Clips; + public List Clips => _modelExtra.Clips; /// /// Play an animation clip. @@ -61,8 +58,8 @@ public AnimatedModel(string assetName) public AnimationPlayer PlayClip(AnimationClip clip) { // Create a clip Player and assign it to this Model. - Player = new AnimationPlayer(clip, this); - return Player; + _player = new AnimationPlayer(clip, this); + return _player; } /// @@ -70,67 +67,48 @@ public AnimationPlayer PlayClip(AnimationClip clip) /// public void Update(GameTime gameTime) { - Player?.Update(gameTime); + _player.Update(gameTime); } /// /// Draw the Model. /// - /// A camera to determine the view. /// A world matrix to place the Model. - public void Draw(Camera camera, Matrix world) + /// The view matrix, normally from the camera. + /// The projection matrix, normally from the application. + public void Draw(Matrix world, Matrix view, Matrix projection) { - if (Model == null) - { - return; - } - // Compute all of the bone absolute transforms. - var boneTransforms = new Matrix[Bones.Count]; + var boneTransforms = new Matrix[_bones.Count]; - for (var i = 0; i < Bones.Count; i++) + for (var i = 0; i < _bones.Count; i++) { - var bone = Bones[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 skeleton = new Matrix[_modelExtra.Skeleton.Count]; + for (var s = 0; s < _modelExtra.Skeleton.Count; s++) { - var bone = Bones[ModelExtra.Skeleton[s]]; + var bone = _bones[_modelExtra.Skeleton[s]]; skeleton[s] = bone.SkinTransform * bone.AbsoluteTransform; } // Draw the Model. - foreach (var modelMesh in Model.Meshes) + foreach (var modelMesh in _model.Meshes) { foreach (var effect in modelMesh.Effects) { - switch (effect) - { - case BasicEffect basicEffect: - basicEffect.World = boneTransforms[modelMesh.ParentBone.Index] * world; - basicEffect.View = camera.View; - basicEffect.Projection = camera.Projection; - basicEffect.EnableDefaultLighting(); - basicEffect.PreferPerPixelLighting = true; - break; - - case SkinnedEffect skinnedEffect: - skinnedEffect.World = boneTransforms[modelMesh.ParentBone.Index] * world; - skinnedEffect.View = camera.View; - skinnedEffect.Projection = camera.Projection; - skinnedEffect.EnableDefaultLighting(); - skinnedEffect.PreferPerPixelLighting = true; - skinnedEffect.SetBoneTransforms(skeleton); - break; - - default: - throw new InvalidOperationException("Unexpected Effect type = " + effect.GetType().FullName); - } + 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(); @@ -142,9 +120,8 @@ public void Draw(Camera camera, Matrix world) /// public void LoadContent(ContentManager content) { - Model = content.Load(AssetName); - ModelExtra = Model.Tag as ModelExtra; - Debug.Assert(ModelExtra != null); + _model = content.Load(_assetName); + _modelExtra = _model.Tag as ModelExtra; ObtainBones(); } @@ -155,14 +132,14 @@ public void LoadContent(ContentManager content) /// private void ObtainBones() { - Bones.Clear(); - foreach (var bone in Model.Bones) + _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); + 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); + _bones.Add(newBone); } } @@ -171,7 +148,7 @@ private void ObtainBones() /// public Bone FindBone(string name) { - foreach (var bone in Bones) + foreach (var bone in _bones) { if (bone.Name == name) { diff --git a/TGC.MonoGame.Samples/Animations/Models/AnimationPlayer.cs b/TGC.MonoGame.Samples/Animations/Models/AnimationPlayer.cs index dd083f9..2d679a6 100644 --- a/TGC.MonoGame.Samples/Animations/Models/AnimationPlayer.cs +++ b/TGC.MonoGame.Samples/Animations/Models/AnimationPlayer.cs @@ -9,6 +9,17 @@ namespace TGC.MonoGame.Samples.Animations.Models; /// public class AnimationPlayer { + /// + /// We maintain a BoneInfo class for each bone. + /// This class does most of the work in playing the animation. + /// + private readonly BoneInfo[] _boneInfo; + + /// + /// The Clip we are playing. + /// + private readonly AnimationClip _clip; + /// /// Current position in time in the clip. /// @@ -20,47 +31,24 @@ public class AnimationPlayer /// public AnimationPlayer(AnimationClip clip, AnimatedModel model) { - Clip = clip; - Model = model; + _clip = clip; // Create the bone information classes. - BonesCount = clip.Bones.Count; - BonesInfo = new BoneInfo[BonesCount]; + var boneCount = clip.Bones.Count; + _boneInfo = new BoneInfo[boneCount]; - for (var b = 0; b < BonesInfo.Length; b++) + for (var b = 0; b < _boneInfo.Length; b++) { // Create it. - BonesInfo[b] = new BoneInfo(clip.Bones[b]); + _boneInfo[b] = new BoneInfo(clip.Bones[b]); // Assign it to a Model bone. - BonesInfo[b].SetModel(model); + _boneInfo[b].SetModel(model); } Rewind(); } - /// - /// The number of bones. - /// - private int BonesCount { get; } - - /// - /// We maintain a BoneInfo class for each bone. - /// This class does most of the work in playing the animation. - /// - private BoneInfo[] BonesInfo { get; } - - /// - /// The Clip we are playing. - /// - private AnimationClip Clip { get; } - - /// - /// A Model this animation is assigned to. - /// It will play on that Model. - /// - private AnimatedModel Model { get; } - /// /// The Looping option. /// Set to true if you want the animation to loop back at the end. @@ -81,7 +69,7 @@ private float Position } _position = value; - foreach (var bone in BonesInfo) + foreach (var bone in _boneInfo) { bone.SetPosition(_position); } @@ -91,7 +79,7 @@ private float Position /// /// The Clip duration. /// - public float Duration => (float)Clip.Duration; + public float Duration => (float)_clip.Duration; /// /// Reset back to time zero. diff --git a/TGC.MonoGame.Samples/Animations/Models/Bone.cs b/TGC.MonoGame.Samples/Animations/Models/Bone.cs index ea6ac1b..d9d419c 100644 --- a/TGC.MonoGame.Samples/Animations/Models/Bone.cs +++ b/TGC.MonoGame.Samples/Animations/Models/Bone.cs @@ -18,78 +18,78 @@ namespace TGC.MonoGame.Samples.Animations.Models; /// public class Bone { - /// - /// Constructor for a bone object. - /// - /// The name of the bone. - /// The initial bind transform for the bone. - /// A Parent for this bone. - public Bone(string name, Matrix bindTransform, Bone parent) - { - Name = name; - Parent = parent; - parent?.Children.Add(this); - - // I am not supporting scaling in animation in this example, so I extract the bind scaling from the bind transform and save it. - BindScale = new Vector3(bindTransform.Right.Length(), bindTransform.Up.Length(), - bindTransform.Backward.Length()); - - bindTransform.Right /= BindScale.X; - bindTransform.Up /= BindScale.Y; - bindTransform.Backward /= BindScale.Y; - BindTransform = bindTransform; - - // Set the skinning bind transform. - // That is the inverse of the absolute transform in the bind pose. - ComputeAbsoluteTransform(); - SkinTransform = Matrix.Invert(AbsoluteTransform); - } - /// /// The bind scaling component extracted from the bind transform. /// - private Vector3 BindScale { get; } + private readonly Vector3 _bindScale; /// /// The bind transform is the transform for this bone as loaded from the original model. It's the base pose. /// I do remove any scaling, though. /// - private Matrix BindTransform { get; } + private readonly Matrix _bindTransform; /// /// The Children of this bone. /// - private List Children { get; } = new(); + private readonly List _children = new(); /// /// The Parent bone or null for the root bone. /// - private Bone Parent { get; } + private readonly Bone _parent; /// - /// The bone absolute transform. + /// Any scaling applied to the bone. /// - public Matrix AbsoluteTransform { get; set; } = Matrix.Identity; + private readonly Vector3 _scale = Vector3.One; /// - /// The bone name. + /// Any Rotation applied to the bone. /// - public string Name { get; set; } + private Quaternion _rotation = Quaternion.Identity; /// - /// Any Rotation applied to the bone. + /// Any Translation applied to the bone. /// - private Quaternion Rotation { get; set; } = Quaternion.Identity; + private Vector3 _translation = Vector3.Zero; /// - /// Any scaling applied to the bone. + /// Constructor for a bone object. /// - private Vector3 Scale { get; } = Vector3.One; + /// The name of the bone. + /// The initial bind transform for the bone. + /// A Parent for this bone. + public Bone(string name, Matrix bindTransform, Bone parent) + { + Name = name; + _parent = parent; + parent?._children.Add(this); + + // I am not supporting scaling in animation in this example, so I extract the bind scaling from the bind transform and save it. + _bindScale = new Vector3(bindTransform.Right.Length(), bindTransform.Up.Length(), + bindTransform.Backward.Length()); + + bindTransform.Right /= _bindScale.X; + bindTransform.Up /= _bindScale.Y; + bindTransform.Backward /= _bindScale.Y; + _bindTransform = bindTransform; + + // Set the skinning bind transform. + // That is the inverse of the absolute transform in the bind pose. + ComputeAbsoluteTransform(); + SkinTransform = Matrix.Invert(AbsoluteTransform); + } /// - /// Any Translation applied to the bone. + /// The bone absolute transform. /// - private Vector3 Translation { get; set; } = Vector3.Zero; + public Matrix AbsoluteTransform { get; set; } = Matrix.Identity; + + /// + /// The bone name. + /// + public string Name { get; set; } /// /// Inverse of absolute bind transform for skinning. @@ -101,13 +101,13 @@ public Bone(string name, Matrix bindTransform, Bone parent) /// public void ComputeAbsoluteTransform() { - var transform = Matrix.CreateScale(Scale * BindScale) * Matrix.CreateFromQuaternion(Rotation) * - Matrix.CreateTranslation(Translation) * BindTransform; + var transform = Matrix.CreateScale(_scale * _bindScale) * Matrix.CreateFromQuaternion(_rotation) * + Matrix.CreateTranslation(_translation) * _bindTransform; - if (Parent != null) + if (_parent != null) // This bone has a Parent bone. { - AbsoluteTransform = transform * Parent.AbsoluteTransform; + AbsoluteTransform = transform * _parent.AbsoluteTransform; } else // The root bone. @@ -123,9 +123,9 @@ public void ComputeAbsoluteTransform() /// A matrix include Translation and Rotation public void SetCompleteTransform(Matrix m) { - var setTo = m * Matrix.Invert(BindTransform); + var setTo = m * Matrix.Invert(_bindTransform); - Translation = setTo.Translation; - Rotation = Quaternion.CreateFromRotationMatrix(setTo); + _translation = setTo.Translation; + _rotation = Quaternion.CreateFromRotationMatrix(setTo); } } \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/Models/BoneInfo.cs b/TGC.MonoGame.Samples/Animations/Models/BoneInfo.cs index c5cdae6..77574b8 100644 --- a/TGC.MonoGame.Samples/Animations/Models/BoneInfo.cs +++ b/TGC.MonoGame.Samples/Animations/Models/BoneInfo.cs @@ -9,42 +9,42 @@ namespace TGC.MonoGame.Samples.Animations.Models; /// public class BoneInfo { - /// - /// Constructor. - /// - public BoneInfo(DataTypes.Bone bone) - { - ClipBone = bone; - SetKeyframes(); - SetPosition(0); - } - /// /// Bone in a model that this keyframe bone is assigned to. /// - private Bone AssignedBone { get; set; } + private Bone _assignedBone; /// /// The current keyframe. Our position is a time such that the we are greater than or equal to this keyframe's time and /// less than the next keyframes time. /// - private int CurrentKeyframe { get; set; } + private int _currentKeyframe; /// - /// We are at a location between Keyframe1 and Keyframe2 such that Keyframe1's time is less than or equal to the - /// current position. + /// Current animation Rotation. /// - public Keyframe Keyframe1 { get; set; } + private Quaternion _rotation; /// - /// Second keyframe value. + /// Constructor. /// - public Keyframe Keyframe2 { get; set; } + public BoneInfo(AnimationBone animationBone) + { + ClipAnimationBone = animationBone; + SetKeyframes(); + SetPosition(0); + } /// - /// Current animation Rotation. + /// We are at a location between KeyframeFrom and KeyframeTo such that KeyframeFrom time is less than or equal to the + /// current position. + /// + public Keyframe KeyframeFrom { get; set; } + + /// + /// Second keyframe value. /// - private Quaternion Rotation { get; set; } + public Keyframe KeyframeTo { get; set; } /// /// Current animation Translation. @@ -60,60 +60,60 @@ public BoneInfo(DataTypes.Bone bone) /// /// The bone in the actual animation clip. /// - public DataTypes.Bone ClipBone { get; } + public AnimationBone ClipAnimationBone { get; } /// /// Set the bone based on the supplied position value. /// public void SetPosition(float position) { - var keyframes = ClipBone.Keyframes; + var keyframes = ClipAnimationBone.Keyframes; if (keyframes.Count == 0) { return; } // If our current position is less that the first keyframe we move the position backward until we get to the right keyframe. - while (position < Keyframe1.Time && CurrentKeyframe > 0) + while (position < KeyframeFrom.Time && _currentKeyframe > 0) { // We need to move backwards in time. - CurrentKeyframe--; + _currentKeyframe--; SetKeyframes(); } // If our current position is greater than the second keyframe we move the position forward until we get to the right keyframe. - while (position >= Keyframe2.Time && CurrentKeyframe < ClipBone.Keyframes.Count - 2) + while (position >= KeyframeTo.Time && _currentKeyframe < ClipAnimationBone.Keyframes.Count - 2) { // We need to move forwards in time. - CurrentKeyframe++; + _currentKeyframe++; SetKeyframes(); } - if (Keyframe1 == Keyframe2) + if (KeyframeFrom == KeyframeTo) { // Keyframes are equal. - Rotation = Keyframe1.Rotation; - Translation = Keyframe1.Translation; + _rotation = KeyframeFrom.Rotation; + Translation = KeyframeFrom.Translation; } else { // Interpolate between keyframes. - var t = (float)((position - Keyframe1.Time) / (Keyframe2.Time - Keyframe1.Time)); - Rotation = Quaternion.Slerp(Keyframe1.Rotation, Keyframe2.Rotation, t); - Translation = Vector3.Lerp(Keyframe1.Translation, Keyframe2.Translation, t); + var t = (float)((position - KeyframeFrom.Time) / (KeyframeTo.Time - KeyframeFrom.Time)); + _rotation = Quaternion.Slerp(KeyframeFrom.Rotation, KeyframeTo.Rotation, t); + Translation = Vector3.Lerp(KeyframeFrom.Translation, KeyframeTo.Translation, t); } Valid = true; - if (AssignedBone == null) + if (_assignedBone == null) { return; } // Send to the model. // Make it a matrix first. - var m = Matrix.CreateFromQuaternion(Rotation); + var m = Matrix.CreateFromQuaternion(_rotation); m.Translation = Translation; - AssignedBone.SetCompleteTransform(m); + _assignedBone.SetCompleteTransform(m); } /// @@ -121,18 +121,18 @@ public void SetPosition(float position) /// private void SetKeyframes() { - if (ClipBone.Keyframes.Count > 0) + if (ClipAnimationBone.Keyframes.Count > 0) { - Keyframe1 = ClipBone.Keyframes[CurrentKeyframe]; - Keyframe2 = CurrentKeyframe == ClipBone.Keyframes.Count - 1 - ? Keyframe1 - : ClipBone.Keyframes[CurrentKeyframe + 1]; + KeyframeFrom = ClipAnimationBone.Keyframes[_currentKeyframe]; + KeyframeTo = _currentKeyframe == ClipAnimationBone.Keyframes.Count - 1 + ? KeyframeFrom + : ClipAnimationBone.Keyframes[_currentKeyframe + 1]; } else { // If there are no keyframes, set both to null. - Keyframe1 = null; - Keyframe2 = null; + KeyframeFrom = null; + KeyframeTo = null; } } @@ -142,6 +142,6 @@ private void SetKeyframes() public void SetModel(AnimatedModel model) { // Find this bone. - AssignedBone = model.FindBone(ClipBone.Name); + _assignedBone = model.FindBone(ClipAnimationBone.Name); } } \ No newline at end of file diff --git a/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs b/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs index ae040e7..881ec9c 100644 --- a/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs +++ b/TGC.MonoGame.Samples/Animations/PipelineExtension/AnimationProcessor.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content.Pipeline; using Microsoft.Xna.Framework.Content.Pipeline.Graphics; @@ -209,7 +208,7 @@ private bool IsSkinned(NodeContent node) { // It has to be a MeshContent node. if (node is MeshContent mesh) - // In the geometry we have to find a vertex channel that has a bone weight collection. + // In the geometry we have to find a vertex channel that has a bone weight collection. { foreach (var geometry in mesh.Geometry) { @@ -234,7 +233,7 @@ private void SwapSkinnedMaterial(NodeContent node) { // It has to be a MeshContent node. if (node is MeshContent mesh) - // In the geometry we have to find a vertex channel that has a bone weight collection. + // In the geometry we have to find a vertex channel that has a bone weight collection. { foreach (var geometry in mesh.Geometry) { @@ -317,7 +316,7 @@ private void ProcessAnimations(ModelContent model, NodeContent input) clip.Name = clipName; foreach (var bone in model.Bones) { - var clipBone = new Bone(); + var clipBone = new AnimationBone(); clipBone.Name = bone.Name; clip.Bones.Add(clipBone); @@ -370,7 +369,7 @@ private void ProcessAnimationsRecursive(NodeContent input) clip.Name = clipName; foreach (var bone in _model.Bones) { - var clipBone = new Bone(); + var clipBone = new AnimationBone(); clipBone.Name = bone.Name; clip.Bones.Add(clipBone); diff --git a/TGC.MonoGame.Samples/Animations/PipelineExtension/CustomPipelineManager.cs b/TGC.MonoGame.Samples/Animations/PipelineExtension/CustomPipelineManager.cs index 4e68e3e..40383c3 100644 --- a/TGC.MonoGame.Samples/Animations/PipelineExtension/CustomPipelineManager.cs +++ b/TGC.MonoGame.Samples/Animations/PipelineExtension/CustomPipelineManager.cs @@ -15,14 +15,14 @@ namespace TGC.MonoGame.Samples.Animations.PipelineExtension; /// public class CustomPipelineManager : PipelineManager { + private readonly IConfigurationRoot _configuration; + public CustomPipelineManager(string projectDir, string outputDir, string intermediateDir, IConfigurationRoot configuration) : base(projectDir, outputDir, intermediateDir) { - Configuration = configuration; + _configuration = configuration; } - private IConfigurationRoot Configuration { get; } - /// /// Provides methods for writing compiled binary format. /// @@ -35,7 +35,8 @@ public static CustomPipelineManager CreateCustomPipelineManager(IConfigurationRo var contentFolder = configuration["ContentFolder"]; // This code is from MonoGame.Content.Builder.BuildContent.Build(out int successCount, out int errorCount). - var projectDirectory = PathHelper.Normalize(Directory.GetCurrentDirectory()); + // TODO the folder logic can be load in the game a save in the configuration, to avoid this folder logic. + var projectDirectory = PathHelper.Normalize(AppDomain.CurrentDomain.BaseDirectory); var projectContentDirectory = PathHelper.Normalize(Path.GetFullPath(Path.Combine(projectDirectory, "../../../" + contentFolder))); var outputPath = PathHelper.Normalize(Path.Combine(projectDirectory, contentFolder)); @@ -51,7 +52,7 @@ public void BuildAnimationContent(string modelFilename) var importContext = new PipelineImporterContext(this); var importer = new FbxImporter(); var nodeContent = - importer.Import(ProjectDirectory + modelFilename + Configuration["FbxExtension"], importContext); + importer.Import(ProjectDirectory + modelFilename + _configuration["FbxExtension"], importContext); var animationProcessor = new AnimationProcessor(); var parameters = new OpaqueDataDictionary @@ -76,10 +77,10 @@ public void BuildAnimationContent(string modelFilename) var pipelineEvent = new PipelineBuildEvent { SourceFile = modelFilename, - DestFile = OutputDirectory + modelFilename + Configuration["ContentExtension"], - Importer = Configuration["FbxImporterName"], - Processor = Configuration["ProcessorName"], - Parameters = ValidateProcessorParameters(Configuration["ProcessorName"], parameters) + DestFile = OutputDirectory + modelFilename + _configuration["ContentExtension"], + Importer = _configuration["FbxImporterName"], + Processor = _configuration["ProcessorName"], + Parameters = ValidateProcessorParameters(_configuration["ProcessorName"], parameters) }; var processContext = new PipelineProcessorContext(this, pipelineEvent); diff --git a/TGC.MonoGame.Samples/Geometries/Arrow.cs b/TGC.MonoGame.Samples/Geometries/Arrow.cs index 5b4eedd..0e2760e 100644 --- a/TGC.MonoGame.Samples/Geometries/Arrow.cs +++ b/TGC.MonoGame.Samples/Geometries/Arrow.cs @@ -152,11 +152,11 @@ public void UpdateValues() } - public void Draw(Matrix World, Matrix View, Matrix Projection) + public void Draw(Matrix world, Matrix view, Matrix projection) { - Effect.World = World; - Effect.View = View; - Effect.Projection = Projection; + Effect.World = world; + Effect.View = view; + Effect.Projection = projection; var graphicsDevice = Effect.GraphicsDevice; graphicsDevice.SetVertexBuffer(VertexBuffer); diff --git a/TGC.MonoGame.Samples/Samples/Animations/SkinnedAnimation.cs b/TGC.MonoGame.Samples/Samples/Animations/SkinnedAnimation.cs index f4898b4..3acd9d4 100644 --- a/TGC.MonoGame.Samples/Samples/Animations/SkinnedAnimation.cs +++ b/TGC.MonoGame.Samples/Samples/Animations/SkinnedAnimation.cs @@ -122,7 +122,7 @@ public override void Draw(GameTime gameTime) { Game.Background = Color.CornflowerBlue; - Model.Draw(Camera, Matrix.Identity); + Model.Draw(Matrix.Identity, Camera.View, Camera.Projection); base.Draw(gameTime); } diff --git a/TGC.MonoGame.Samples/Samples/Heightmaps/SimpleTerrain/SimpleTerrain.cs b/TGC.MonoGame.Samples/Samples/Heightmaps/SimpleTerrain/SimpleTerrain.cs index f791e9d..c2cb4cc 100644 --- a/TGC.MonoGame.Samples/Samples/Heightmaps/SimpleTerrain/SimpleTerrain.cs +++ b/TGC.MonoGame.Samples/Samples/Heightmaps/SimpleTerrain/SimpleTerrain.cs @@ -45,16 +45,16 @@ public SimpleTerrain(GraphicsDevice graphicsDevice, Texture2D heightMap, Texture /// /// Renderiza el terreno /// - public void Draw(Matrix World, Matrix View, Matrix Projection) + public void Draw(Matrix world, Matrix view, Matrix projection) { var graphicsDevice = Effect.GraphicsDevice; Effect.Parameters["texColorMap"].SetValue(colorMapTexture); Effect.Parameters["texDiffuseMap"].SetValue(terrainTexture); Effect.Parameters["texDiffuseMap2"].SetValue(terrainTexture2); - Effect.Parameters["World"].SetValue(World); - Effect.Parameters["View"].SetValue(View); - Effect.Parameters["Projection"].SetValue(Projection); + Effect.Parameters["World"].SetValue(world); + Effect.Parameters["View"].SetValue(view); + Effect.Parameters["Projection"].SetValue(projection); graphicsDevice.SetVertexBuffer(vbTerrain); From 4d7dc769bfb977abcb4d905db91f1bd9f6b9a66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Juan=20Rico=20Mendoza?= Date: Fri, 17 Nov 2023 23:58:32 -0300 Subject: [PATCH 4/4] Fix more review comments --- .../Animations/Models/AnimatedModel.cs | 10 +++++----- TGC.MonoGame.Samples/Animations/Models/Bone.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs b/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs index d60bf3b..bcbe7e9 100644 --- a/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs +++ b/TGC.MonoGame.Samples/Animations/Models/AnimatedModel.cs @@ -36,6 +36,11 @@ public class AnimatedModel /// private AnimationPlayer _player; + /// + /// The Model animation clips. + /// + public List Clips => _modelExtra.Clips; + /// /// Creates the Model from an XNA Model. /// @@ -45,11 +50,6 @@ public AnimatedModel(string assetName) _assetName = assetName; } - /// - /// The Model animation clips. - /// - public List Clips => _modelExtra.Clips; - /// /// Play an animation clip. /// diff --git a/TGC.MonoGame.Samples/Animations/Models/Bone.cs b/TGC.MonoGame.Samples/Animations/Models/Bone.cs index d9d419c..3963364 100644 --- a/TGC.MonoGame.Samples/Animations/Models/Bone.cs +++ b/TGC.MonoGame.Samples/Animations/Models/Bone.cs @@ -66,7 +66,7 @@ public Bone(string name, Matrix bindTransform, Bone parent) _parent = parent; parent?._children.Add(this); - // I am not supporting scaling in animation in this example, so I extract the bind scaling from the bind transform and save it. + // In this example, scaling in animation is not supported. The bind scaling is separated from the bind transform and saved. _bindScale = new Vector3(bindTransform.Right.Length(), bindTransform.Up.Length(), bindTransform.Backward.Length());