flopoloco's picture

2D Vertex Skinning

This example is an extension to this tutorial, supporting vertex skinning on the CPU.
http://www.opentk.com/node/3597

It took lot of time to figure out how it works, but I am happy that it did. :) I have started learning more about character animation and vertex skinning, so until I figure out how things work and gain knowledge, I will have to implement everything -almost- from scratch. It will take some more weeks, but I have plans to make a fully featured character animation example.

using System;
using OpenTK;
using OpenTK.Graphics.OpenGL;
 
namespace OpenTKCPUVertexSkinning
{
	class Bone
	{
		public string Name;
		public Vector3 LocalPosition;
		public Vector3 Position;
		public float Angle;
		public Bone Parent;
		public Matrix4 Matrix;
	}
 
	class MainClass
	{
		public static void Main(string[] args)
		{
			#region Set the data of the skeleton
			// Bones - coordinates set in local space
			var bones = new Bone[4];
			bones[0] = new Bone { Name = "root", LocalPosition = new Vector3(0, 0, 0) };
			bones[1] = new Bone { Name = "body", LocalPosition = new Vector3(0, 2, 0) };
			bones[2] = new Bone { Name = "neck", LocalPosition = new Vector3(0, 1, 0) };
			bones[3] = new Bone { Name = "head", LocalPosition = new Vector3(0, 1, 0) };
			bones[1].Parent = bones[0]; // "body" has "root" parent
			bones[2].Parent = bones[1]; // "neck" has "body" parent
			bones[3].Parent = bones[2]; // "head" has "neck" parent
 
			// Vertices - coordinates are relative to the associated bone
			var vertices = new Vector3[8];
			vertices[0] = new Vector3(-0.2f, -2f, 0f); // body - bottom left
			vertices[1] = new Vector3(0.2f, -2f, 0f); // body - bottom right
			vertices[2] = new Vector3(-0.2f, -1f, 0f); // body - top left
			vertices[3] = new Vector3(0.2f, -1f, 0f); // body - top right
			vertices[4] = new Vector3(-0.2f, 0f, 0f); // neck - left
			vertices[5] = new Vector3(0.2f, 0f, 0f); // neck - right
			vertices[6] = new Vector3(-0.2f, 0f, 0f); // head - left
			vertices[7] = new Vector3(0.2f, 0f, 0f); // head - right
 
			// Array for storing the transformed vertices
			var verticesTransformed = new Vector3[vertices.Length];
 
			// Vertex and bone association (boneIndices[vertexIndex] = boneIndex)
			var boneIndices = new int[vertices.Length];
			boneIndices[0] = 1;
			boneIndices[1] = 1; // body
			boneIndices[2] = 2;
			boneIndices[3] = 2; // neck
			boneIndices[4] = 2;
			boneIndices[5] = 2; // neck
			boneIndices[6] = 3;
			boneIndices[7] = 3; // head
 
			// Create vertex indices for line loop drawing
			var drawIndices = new int[]
			{
				0,			// start
				1, 3, 2, 	// right, up, left
				3, 5, 4, 	// right, up, left
				5, 7, 6, 	// right, up, left
				6, 4, 2,	// down, down, down
				0			// start
			};
			#endregion
 
			// OpenTK application
			var win = new GameWindow(800, 600);
			Matrix4 modelview, projection;
			var mvp = Matrix4.Identity;
 
			// Temp variables for moving the skeleton
			var skeletonSpeed = 0f;
			var skeletonAngle = 0f;
			var skeletonPosition = new Vector3();
			var skeletonPosTarget = new Vector3();
 
			win.Load += (object sender, EventArgs e) =>
			{
				modelview = Matrix4.LookAt(
					new Vector3(0, 2, 12), new Vector3(0, 2, 0), Vector3.UnitY);
				projection = Matrix4.CreatePerspectiveFieldOfView(
					(float)Math.PI / 4, (float)win.Width/win.Height, 0.1f, 100f);
				mvp = modelview * projection;
			};
 
			#region Build tuple array (Key, BoneIndex, ValueSign) bone angle input logic
			var inputBones = new Tuple<OpenTK.Input.Key, int, float>[]
			{
				Tuple.Create(OpenTK.Input.Key.Z, 1, 1f), 	// body
				Tuple.Create(OpenTK.Input.Key.C, 1, -1f),
				Tuple.Create(OpenTK.Input.Key.A, 2, 1f), 	// neck
				Tuple.Create(OpenTK.Input.Key.D, 2, -1f),
				Tuple.Create(OpenTK.Input.Key.Q, 3, 1f),	// head
				Tuple.Create(OpenTK.Input.Key.E, 3, -1f)
			};
			#endregion
 
			#region Build tuple array (Key, Rot, Move) for skeleton movement input logic
			var inputSkeleton = new Tuple<OpenTK.Input.Key, float, float>[]
			{
				Tuple.Create(OpenTK.Input.Key.Left, 1f, 0f),
				Tuple.Create(OpenTK.Input.Key.Right, -1f, 0f),
				Tuple.Create(OpenTK.Input.Key.Up, 0f, 1f),
				Tuple.Create(OpenTK.Input.Key.Down, 0f, -1f)
			};
			#endregion
 
			win.UpdateFrame += (object sender, FrameEventArgs e) =>
			{
				#region Check input for setting bone angles
				foreach (var i in inputBones)
				{
					if (win.Keyboard[i.Item1])
						bones[i.Item2].Angle += i.Item3 * (float)e.Time;
				}
				#endregion
 
				#region Check input for setting the movement of the skeleton
				skeletonSpeed = 0f;
				foreach (var i in inputSkeleton)
				{
					if (win.Keyboard[i.Item1])
					{
						if (i.Item2 != 0) skeletonAngle += i.Item2 * (float)e.Time;
						if (i.Item3 != 0) skeletonSpeed += i.Item3 * (float)e.Time;
					}
				}
				#endregion
 
				#region Calculate transformation of the skeleton
				var quat = Quaternion.FromAxisAngle(Vector3.UnitZ, skeletonAngle);
				skeletonPosition += Vector3.Transform(Vector3.UnitY * skeletonSpeed, quat);
				skeletonPosTarget = skeletonPosition + Vector3.Transform((Vector3.UnitY * 1f), quat);
				#endregion
 
				#region Calculate bone transformations
				for (var i = 0; i < bones.Length; i++)
				{
					var bone = bones[i];
					float angle = 0f;
 
					// Hack to affect the position of the root bone
					if (i == 0)
					{
						bone.LocalPosition = skeletonPosition;
						bone.Angle = skeletonAngle;
					}
 
					// Hack to affect the last bone based on the root bone
					if (i == 3)
						angle += skeletonAngle;
 
					// Translate bone
					var matrix = Matrix4.CreateTranslation(bone.LocalPosition);
					if (bone.Parent != null)
					{
						angle += bone.Angle + bone.Parent.Angle;
						if (bone.Parent.Parent != null)
							angle += bone.Parent.Parent.Angle;
 
						matrix *= Matrix4.CreateRotationZ(angle);
						matrix *= Matrix4.CreateTranslation(bone.Parent.Position);
					}
 
					bone.Matrix = matrix;
					bone.Position = matrix.Row3.Xyz;
				}
				#endregion
			};
 
			win.RenderFrame += (object sender, FrameEventArgs e) => {
				GL.Clear(ClearBufferMask.ColorBufferBit);
				GL.LoadMatrix(ref mvp);
 
				// Transform the vertices
				for (var i = 0; i < vertices.Length; i++)
				{
					var matrix2 = bones[boneIndices[i]].Matrix;
					var vertex = vertices[i];
					verticesTransformed[i] = Vector3.Transform(vertex, matrix2);
				}
 
				// Draw bones
				GL.Color3(1f, 1f, 1f);
				GL.Begin(PrimitiveType.Lines);
				foreach (var i in bones)
				{
					if (i.Parent != null)
					{
						GL.Vertex3(i.Parent.Position);
						GL.Vertex3(i.Position);
					}
				}
				GL.End();
 
				// Draw joints
				GL.Color3(1f, 0f, 0f);
				GL.PointSize(10f);
				GL.Begin(PrimitiveType.Points);
				foreach (var i in bones)
					GL.Vertex3(i.Position);
				GL.Vertex3(bones[1].Position);
				GL.End();
				GL.PointSize(1f);
 
				// Draw edges of vertices
				GL.Color3(0f, 1f, 0f);
				GL.Begin(PrimitiveType.LineLoop);
				for (var i = 0; i < drawIndices.Length; i++)
					GL.Vertex3(verticesTransformed[drawIndices[i]]);
				GL.End();
 
				GL.Color3(0f, 0f, 1f);
				GL.Begin(PrimitiveType.Lines);
				GL.Vertex3(skeletonPosition);
				GL.Vertex3(skeletonPosTarget);
				GL.End();
 
				win.SwapBuffers();
			};
 
			win.Run();
		}
	}
}

Next steps for the tutorial will be:
a. Calculate vertex skinning on the GPU
b. Support for 4 weights per vertex which is very standard in game engines.
c. Convert example to 3D (e.g. human skeletal shape)
d. Support for animations.

P.S. If any of you happens to be experienced in such projects and can hack something, please respond so you can save me a dozen of time. :)

Inline Images