Skeletal animation - Fish-In-A-Suit/Conquest GitHub Wiki
Introduction
- programming language: Java
- export format: collada (.dae)
- tutorial series used: https://www.youtube.com/watch?v=f3Cr8Yx3GGA&t=1s by ThinMatrix
An animated model has two main components: skin (or mesh) and skeleton, which is made up of bones/joints. Moving a bone also deforms vertices around that bone. Note that bones do not get rendered.
Animation is represented as series of poses (known as keyframes) at different times. A keyframe is a specific pose of a model at a specific time of the animation.
Joints/bones of a model are arranged in a hierarchy - each joint can have a parent joint and multiple children joints. For example, the hand joint is the child of the lower arm joint. When a joint is moved, all of its children move with it; when the lower arm joint is moved, the hand joint moves with it. The root joint of a model is a joint which only has children and no parent - think of it as a "starting" joint in the hierachy.
Code
This is an overview of the code (classes) that will be used to implement animation:
AnimatedModel
An animated model is made out of two things: the skeleton and the skin. The skeleton is made up of joints (or bones as some call them), which are in a hierarchy. The skin refers to the mesh data (vertices and their attribtues).
- Joint jointHierachy
- Mesh, which holds vertex positions, texture coordinates and normals, which are stored inside a vao. Now, hoewever, each vertex is going to need some extra information about how it should be affected by the joints (jointIDs and weights, both stored as vec3 - allow a vertex to be affected by maximum up to three joints for simplicity, where weights determine how much each of those joints affect the vertex in question).
Joint
Each Joint has a:
- list of children joints
- id, so it can be identified
- Matrix4f transform, which refers to the current position and rotation of that joint in model space. By changing the transform of the joint we change how the joints are positioned and therefore put our model into different poses.
Renderer
The renderer's job is to take in an AnimatedModel instance and render it onto the screen in the pose determined by the joint transforms. The joint transforms are going to be loaded to the shaders as an uniform array of type mat4. The information about how much each vertex is affected by those joints comes from the vao of the model as per-vertex attribtues (in_jointIndices and in_weights). The vertex shader calculates the deformed position of a vertex by applying the transforms of all the joints that affect it and taking the weighted average of the result.
Animation
- KeyFrame[] frames - a series of keyframes
JointTransform
- Vector3f position
- Quaternion rotation
KeyFrame (is just a certain pose, represented as joint transform for every join in the skeleton at certain time)
- (pose) JointTransform[] transforms; if a model has 10 joints, then the length of JointTransform[] array is going to have 10 transforms (one for each joint). Note: these transforms are all in relation to the parent joint, rather in relation to the model's origin --> between JointTransform[] transforms (in KeyFrame) and Matrix4f transform (in Joint), the transforms are going to have to be calculated into model-space coordinate system.
- float timeStamp
Animator
The role of this class is to apply an animation to AnimatedModel. It runs through the animation and finds out what the current pose of a model should be at each frame (based on the animation) and put AnimatedModel in that pose by setting the joint transforms. To do this, the Animator class requires access to currentAnimation (the animation that is currently being carried out) and the current time of the animation, ie how far through the animation it has already come ... it increases this time every frame so that the animation keeps progressing. When the time gets bigger than the length of the animation, it sets it back to zero, so that the animation plays from the beginning again. For any given time in the animation, to find out what the current pose should be, it gets the previous and next keyframes and it interpolates between the poses at those keyframes based on how far the animation currently is. To interpolate between two poses, the position and rotation of each joints need to be interpolated (this data is stored in JointTransform).
Interpolating between two positions is simple: just interpolate between x, y and z values. Interpolating between rotations, however, is much more difficult. If you try to interpolate between each of the components of two Euler rotations (rotx, roty, rotz), the results won't be correct and often an occurrence known as deadlock occurs, where the object's rotation is fixed to some weird value. Therefore, to represent the rotation of joint transforms at each keyframe, quaternions are used. These make it very easy to correctly interpolate between two rotations by using a method called SLERP. Once we've interpolated between two quaternions defining two joint rotations for a given pose, it's also pretty easy to convert the quaternion rotation into a rotation matrix, which is then applied to transformation matrices when the final joint transforms are calculated.
Loaders
We need two loaders: one to load mesh data from a Collada file, and the other to load animation data from a Collada file.
ModelLoader
The role of this class is to load mesh data from a Collada (.dae) file
AnimationLoader
Loads animation data (all keyframes etc) from a Collada file.