by Thomas 'ThamasTah' van Dijk
I wrote some "Skeletal Animation" code in some spare time and I asked on Lucky's board whether anyone would be interested in a tutorial on it. Lucky wanted it! Way to go! So here it is, then.
Introduction
Sorry if I bore you, but its quite a sit to read though in one go. But it had to be: it could be quite complicated and I wanted to make sure I lost only a minimum of readers due to that, so perhaps I'm explaining a bit too much.
What is "Skeletal Animation" anyway? I hear you ask. Well, I don't
even know if it's called 'skeletal', but it animates, and you could build a
skeleton with it, so... ;)
For those of you who know Poser (formerly by Fractal Design, but recently bought out by someone), it is easy to explain: it is what Poser does; for those who don't know Poser:
I assume you know what vectors are (if not, Lucky has some tutorials on 'em:
go read them).
You take one coord(inate). That coord is the start of a line. That line is stored
as a vector: angle and length, or as in the code: cAngle and sLength. Using
the vector, we can calculate the ending point of the line (using goneometrics).
Now, we can link another line (or, bone, as I call it in the code) to the end
of the first line. We call it the child of the first line. The first line is
the parent of the second one.
If a line moves or changes angle, we re-calculate the ending point, set the
starting point of any children to the newly calculated coords, re-calculate
those children. The ending point of the children will then, of course, change
(its starting point has changed!): its children should also be re-calculated.
Just repeat until no children are left...
Another important note: the angle is relative to the angle of the parent, so
if the angle of the parent changes (the parent turns) the actual on-screen angle
of the child changes accordingly, so the angle between parent and child remains
the same.
This is most visually explained by an example involving the upper- and lower arm:
Imagine (or you could actually do this if you don't believe it!) you stand in front of a mirror (the computer screen). Bend you elbow to, say, 90 degrees (which equals 0.5pi radians). Keep this angle constant. Now, from the shoulder, rotate your upper arm.
The lower arm doesn't remain floating in the air where it used to be (duh, I hear you say). Instead, the starting point of your lower arm, has moved along with the ending point of you upper arm.
Also, if you did as I said, the angle between the upper arm and the lower arm has remained the same. Notice the angle between the lower arm and, for example, the side of the mirror (the side of the computer screen, or: the universal co-ordinate system) indeed has changed.
This is handled by my skeletal animation system. Note that the upper arm is the parent of the lower arm, and the lower arm is the child of the upper arm.
A final note before I start to describe the (my) implementation: this is my first tutorial larger than a few paragraphs and my writing tends to be rather chaotic. Sorry for that. Comments on my writing -or something else :)- are very welcome.
Implementation
Firstly, a few things you need to know:
Okay, let's start. I will just build the code alongside the explanation.
(I'll talk in 1st person plural)
The member variables
We make a class to hold the information for the bones (lines). We call it bone.
The bone will need to store: starting coords; ending coords; angle relative to its parent; length:
Public x1 as Double, y1 as Double
Public x2 as Double, y2 as Double
Public cAngle as Double
Public sLength as Double
We'll also need a value to store the 'world'angle, for drawing -that is, the angle relative not to its parent, but to the x and y axis of the screen (needed for drawing).
Public cRealAngle as Double
We'll calculate x2, y2 and cRealAnlge in bone::render
Like this we can store a single bone. If we want to add the 'skeletal' stuff,
we'll need to be able to identify our parent and children. We do that using
a pointer for our parent and a collection containing our children.
Public bParent as bone
Public bChildren as New Collection
Notice we don't say 'New bone', just 'bone': we don't want a new instance of
a bone, just a pointer to a (any) bone... We do want a New Collection.
The first bone in a series doesn't have a parent. Its starting point has to
be explicitly specified. Its bParent pointer will be NULL --what's that called
in VB? It won't let me say: If bParent = Nothing! Therefor I added a boolean
for a bone to indicate it has no parent.
Public boolAbsoluteCoords as Boolean
I also have a Boolean for indicating the angle of a bone should always be the same, relative to to the universal coord-system. I could explain this as follows:
Use the example as above. Now, add to that a book (or something else flat). Lay the book on your hand. Your hand will have to be horizontal (cRealAnlge = 0 or pi, in my code), otherwise the book will fall off.
Now turn your elbow and shoulder, but make sure the book doesn't fall off!
What you have to do, is turn you wrist --changing the angle between your lower arm (the parent) and your hand (the child).
Instead of letting the client app calculate that, it is way easier to provide it in the class (as we will see ;))
Public boolAbsoluteAngle as Boolean
Also, for drawing and client-side editing, we use a boolean to indicate whether the bone is selected.
Public boolSelected as Boolean
The Member Funtions
So, we can store the information. Now, let's actually do something with it.
I'll explain most important one: bone::render. This will calculate x2 and y2,
using x1, y1, cAngle and sLength.
n.B. Comments are italicized.
Public Sub render(Optional boolChildren as Boolean = True, Optional boolCheckParent as Boolean = False)
-- boolChildren indicates whether this bone's children's bone::render should also be called
-- boolCheckParent indicates whether we should check x2 and y2 of the parent (usually not needed, because its parent will have set its x1 and y1.
Dim var as Variant
Used in the 'for each'-loops.
If boolAbsoluteCoords Then
If it hasn't a parent...cRealAngle = cAngle
The real angle has to be angle (since it has not parent that could have another angle!)If cRealAngle > 2 * PI Then
Correct cRealAngle if its value has unneedily high or low values...cRealAngle = cRealAngle - 2 * PI
ElseIf cRealAngle < 0 Then
cRealAngle = cRealAngle + 2 * PI
End If
Else
If the bone does have a parent...If boolCheckParent Then
If the parent should be checked, copy its end coords to my start coords...x1 = bParent.x2
y1 = bParent.y2
End If
If boolAbsoluteAngle Then
If my real angle should be the same as my cAngle...cRealAngle = cAngle
Else
Otherwise...cRealAngle = bParent.cRealAngle + cAngle
My real angle is my parent's real angle, plus my angleEnd If
If cRealAngle > 2 * PI Then
Correct cRealAngle...cRealAngle = cRealAngle - 2 * PI
ElseIf cRealAngle < 0 Then
cRealAngle = cRealAngle + 2 * PI
End If
End If
This is where some math comes in:
x2 = x1 + (sLength * Cos(cRealAngle))
Using sLength and cAngle, we can calculate the x component of the vectory2 = y1 + (sLength * Sin(cRealAngle))
Similarly (but now using sine, not cosine) we calculate the y component
For Each var In bChildren
For all of my children, set their start coords to my ending coordsvar.x1 = x2
var.y1 = y2
Next
If boolChildren Then
If I should also render my children (an argument in the call)For Each var In bChildren
For each of my children...var.render
Render it!Next
End If
End Sub
Actually not so difficult, now is it?!
I added some more functions for governing the bones and stuff. It can be found
in the sample project: it isn't difficult, y'all should be able to figure it
out by yourselves. See the bottom of this page[?] to download it.
Conclusion
I'd been thinking about this for some weeks now. I thought it would be very difficult to implement, but when I finally sat down to write the code, I just did it. It just worked, which was very satisfying, as you might imagine :D "The rest is ancient history..."
"You haven't yet discussed animation!" Indeed I have not, but that shouldn't be too much of a trouble: 'just' change the values... Okay, that could actually prove to be quite difficult. Perhaps I should have called this Skeletal Modelling. I'm working on an animation thingy though; if I ever finish it --and if there's demand--, I'll write a tutorial on that too.
That animation-system should be time-based, keyframe-based, with (linear) interpolation and animation interruption:
A note: this system is quite easily adaptable to 3D: just add another angle vector (or also a third if you want to be able to turn a line alone its own axis).
I'm also working on a skinning system using Skeletal Modelling.
As a final note, I'm also contemplating Inverse Kinematics (IK). At first,
I thought this was IK, but there in nothing Inverse about this. What I'm now
thinking about is being able to 'pull' or 'apply a force to' the end of a bone,
the parents changing accordingly.
Again imagine the example. Now try to reach something above you with your hand.
By just turning your wrist, you won't reach it. You have to turn/move your lower
arm. Still can't reach it? Move/turn your upper arm!
This might prove quite a bit more difficult that Skeletal Animation, but it
is rather interesting.
Thomas 'ThamasTah' van Dijk
nov 30 2000