flopoloco's picture

Scenegraph Transformation - Skeletal Animation

I have been trying this for a couple of days now, and finally I have succeeded. I would like to present you a new example featuring matrix transformations.

Due to the nature of the tutorial you can gain enough knowledge to extend the concept to fit as well to:
* Pivot transformations
* Hierarchical transformations
* Skeletal animations
* Scenegraph nodes

This is indeed far from perfect, but it's the least minimum code required to have a Skeletal Animation working. The example features proper hierarchic bone transformations, as well as local based movement. I guess that by a few modifications it can work for 3D as well.

You can press keys A and D to rotate the first bone,
keys Q and E to rotate the second bone
as a bonus you can press the Space Bar to move the skeleton.

Also I have provided with a worklog as documentation that I have written little by little while researching.

If you have any other information or suggestions, let me know if I can help.

Next things I will try are.
- 3D support
- Keyframe based animations
- Matrix palette skinning (Shader based)

But you will have to wait a couple of weeks, due to the fact I have limited time daily to offer on this. If any of you are faster than me and make something, I would be very interested to know because it will be a common benefit. :)

Notes / Explanation
 
What does this tutorial is about?
	Is about creating a minimal skeletal animation system, the foundation of
	the code relies based on the article found in OpenGL Wiki, but it has adapted
	to match properly the OpenTK codestyle.
	http://www.opengl.org/wiki/Skeletal_Animation
 
How are the bone positions stored in the Bone class?
	The local coordinates of the bones are stored in "LocalPosition" variable.
	The global coordinates of the bones are stored in "Position" variable.
 
Why is needed to use local positions and global positions as well?
	Local space coordinates allow calculations to produce proper,
	results for hierarchical transformations.
	World space coordinates are better when used for single objects instead,
	but also they are essential for rendering/drawing operations.
 
Why not use only one variable for storing the position?
	"LocalPosition" represents the local position of the bone, and by
	it every bone gets it's global coordinate. Otherwise if only
	one variable is used then due to data over-writing the results
	will slide off to infinity.
 
How can the position of a bone be converted from local to global space?
	bone.Position = bone.LocalPosition + bone.Parent.Position;
 
How can a bone be rendered?
	Provided that a line represents a bone, then two coordinate points must exist.
	For hierarchical structures is common to have 2 joint positions for each bone.
	GL.Vertex3(bone.Parent.Position);
	GL.Vertex3(bone.Position);
 
If a bone is needed to be rotated, how can this happen?
	Provided that we care for 2D rotations in the Z axis.
	We need to know two values, an angle and a length.
 
	The angle is the actual rotation of the bone and is
	a value between the degrees of the circle 0..360.
 
	The length is the actual length of the bone and it's
	value is set according to the distance of the two joints.
	This can be calculated using the distance formula:
	dx = bone.Parent.Position.X - bone.Position.X;
	dy = bone.Parent.Position.Y - bone.Position.Y;
	dist = Math.Sqrt(dx * dx + dy * dy);
 
	When an angle and a length are known we can calculate the transformation.
	bone.Angle = / * e.g.* / 25;
	bone.Length = / * e.g.* / 2;
	var tmpx = (float)Math.Sin(body.Angle) * body.Length;
	var tmpy = (float)Math.Cos(body.Angle) * body.Length;
 
	Then hierarchical transformation can be applied as well.
	bone.Position = new Vector2(tmpx, tmpy) + body.Parent.Position;
 
	Though this is the a way that the application can work we can rely
	better of pre existing standard math libraries, because they are 
	guaranteed to work and the knowledge is re usable, thus 
	making them better choice by far, rather instead of doing
	math on-the-fly.
 
How exactly matrix transformations work?
	This explaned in depth here:
	"A useful transformation is rotation or scaling about a position other than the origin..."
	http://trac.openscenegraph.org/projects/osg//wiki/Support/Maths/MatrixTransformations
 
	Step 3. move the object so that the pivot point is at the origin
	Matrix4.CreateTranslation(head.Parent.Position)
 
	Step 2. rotate (both local and parental angles)
	Matrix4.CreateRotationZ(head.Angle + head.Parent.Angle)
 
	Step 1. move the object so that the pivot point is at its original location
	Matrix4.CreateTranslation(head.LocalPosition)
 
	By adapting the tutorial to OpenTK we need to have the order of operations reversed.
	(Explanation needed. Does OpenTK Matrix use row major and postfix order of operations?
	How this affect the order of operations?)
 
How do you move the Skeleton around?
	This is done in a quick and hackish way because this feature was added just
	1 minute before posting this article online.
 
	The calculation that takes place is in the well known Translate*Rotate order.
	For translating we care about getting a local Y vector of the root bone,
	having as a magnitude the value of the "moveSpeed" variable. Then for
	rotating, we borrow the rotation angle of the "body" bone, since we
	have it already available and the final result makes sense.
 
	Finally, in order to store this calculation and apply the result, we shall
	add it to the bone position, this will allow the movement to happen
	and move on to the next frame.
using System;
using OpenTK;
using OpenTK.Graphics.OpenGL;
 
namespace OpenTKTransformations
{
	class Bone
	{
		public string Name;
		public Vector3 LocalPosition;
		public Vector3 Position;
		public float Angle;
		public float Length;
		public Bone Parent;
	}
 
	class MainClass
	{
		public static void Main(string[] args)
		{
			// Create bones in local space
			Bone root, body, head;
			root = new Bone { Name = "root", LocalPosition = new Vector3(0, 0, 0) };
			body = new Bone { Name = "body", LocalPosition = new Vector3(0, 2, 0) };
			head = new Bone { Name = "head", LocalPosition = new Vector3(0, 1, 0) };
			// Set hierarchy of bones
			body.Parent = root;
			head.Parent = body;
 
			var win = new GameWindow(800, 600);
			Matrix4 matModelview, matProjection;
			Matrix4 matMVP = Matrix4.Identity;
			Matrix4 matrix;
			float moveSpeed = 0f;
 
			win.Load += (object sender, EventArgs e) =>
			{
				// Set view and camera stuff
				matModelview = Matrix4.LookAt(new Vector3(0, 2, 12), new Vector3(0, 2, 0), Vector3.UnitY);
				matProjection = Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, (float)win.Width/win.Height, 0.1f, 100f);
				matMVP = matModelview * matProjection;
			};
 
			win.UpdateFrame += (object sender, FrameEventArgs e) =>
			{
				// Set angle of Body
				if (win.Keyboard[OpenTK.Input.Key.A]) body.Angle += 1f * (float)e.Time;
				if (win.Keyboard[OpenTK.Input.Key.D]) body.Angle -= 1f * (float)e.Time;
 
				// Set angle of Head
				if (win.Keyboard[OpenTK.Input.Key.Q]) head.Angle += 1f * (float)e.Time;
				if (win.Keyboard[OpenTK.Input.Key.E]) head.Angle -= 1f * (float)e.Time;
 
				// Set movespeed
				if (win.Keyboard[OpenTK.Input.Key.Space]) moveSpeed = 2f * (float)e.Time;
				moveSpeed *= 0.9f; // And decrease smoothly the value
			};
 
			win.RenderFrame += (object sender, FrameEventArgs e) => {
				GL.Clear(ClearBufferMask.ColorBufferBit);
				GL.LoadMatrix(ref matMVP);
 
				// Do transformations
				matrix = Matrix4.CreateTranslation(root.LocalPosition);
 
				// Remove these two lines below if you do not want to move the skeleton
				matrix *= Matrix4.CreateTranslation(Vector3.UnitY * moveSpeed);
				matrix *= Matrix4.CreateRotationZ(body.Angle);
				// Remove these two lines above if you do not want to move the skeleton
 
				root.Position += matrix.Row3.Xyz;
 
				matrix =
					Matrix4.CreateTranslation(body.LocalPosition) *
					Matrix4.CreateRotationZ(body.Angle) *
					Matrix4.CreateTranslation(body.Parent.Position);
				body.Position = matrix.Row3.Xyz;
 
				matrix = 
					Matrix4.CreateTranslation(head.LocalPosition) *
					Matrix4.CreateRotationZ(head.Angle + head.Parent.Angle) *
					Matrix4.CreateTranslation(head.Parent.Position);
				head.Position = matrix.Row3.Xyz;
 
 
				// Draw bones
				GL.Begin(PrimitiveType.Lines);
				GL.Vertex3(body.Parent.Position);
				GL.Vertex3(body.Position);
				GL.Vertex3(head.Parent.Position);
				GL.Vertex3(head.Position);
				GL.End();
 
				// Draw joints
				GL.Color3(1f, 0f, 0f);
				GL.PointSize(10f);
				GL.Begin(PrimitiveType.Points);
				GL.Vertex3(root.Position);
				GL.Vertex3(body.Position);
				GL.Vertex3(head.Position);
				GL.End();
				GL.PointSize(1f);
				GL.Color3(1f, 1f, 1f);
 
				win.SwapBuffers();
			};
 
			win.Run();
		}
	}
}
Images