Skeletal Animation

Definition

Skeletal Animation, otherwise known as vertex skinning, is a process of animating a 3D mesh by controlling the mesh’s vertices through a skeletal system of joints. Thus, if a joint moves, then the respective vertices that the joint influences will also move by the same about.

Why do this?

As the title suggest, this is done mostly for animating characters in a very efficient manor. Not only is it fast, but its very effective with memory. Especially when in an application one can have multiple animations playing.

How to do it

In this post, I will walk you through how I have implementing vertex skinning. This is a project that is of interest to me because when I export animation from blender I have been unable to properly play back the animation that was created in blender.

So back to the tutorial. Below are the components needed to render a mesh which will be deformed by a skeletal system.

  1. Export a 3D meshEnsure that for each vertex of the mesh there is a count of the number of joints the vertex is attached to, how much each joint influences the vertex(weight), and the name or index of the joints influencing the vertex.
  2. Export a 3D skeleton
    This is done at the same time as exporting of the 3D mesh information, usually. The great point of note here is for each joint, export the bind offset ( relative to parent joint, if no parent, the offset will be the skeleton space coordinate of the joint), bind orientation (relative to parent), the name or id which uniquely describes the joint.
  3. Utilize exported Data
    This is really where this tutorial focuses. The most difficult part of creating the forward kinematics necessary to accurately deform your skinned mesh is, I think, the most difficult and hard to get write. So below I demonstrate how I got it to world.

Get down to brass tacks: implementing skeletal animation

public class BaseJoint {
    /** World position of this joint; obtained after building the skeleton(Armature)
    private Vector3f position = new Vector3f();
    /** Length of this joint */
    private float length = 0.0f;
    /**
     * Initialize the fundamental properties of this Joint 
     * @param name          Name of joint
     * @param bindOrient    localspace bind orientation (Identity usually)
     * @param xOffset       vector distance way from parent of this joint
     * @param length        length of this joint (optional; used mostly for rendering aide)
     */
    protected void init(String name, Quaternion bindOrient, Vector3f xOffset, float length ){
        this.name = name;
        parent = null;
        //-----------------------------------------
        // Animation Translation & Orientation
        //-----------------------------------------
        locAnimTranslation.set  (0, 0, 0);
        locAnimRot.set          (1,0,0, 0.0f);
        //-----------------------------------------
        // Bind Translation & Orientation
        //-----------------------------------------
        this.xOffset.set        (    xOffset     );
        bindLocOrientation.set  (  bindOrient    );

        this.length = length;
    }
 
    //------------------------------------------------------------------------
    // Data
    //------------------------------------------------------------------------
    /**
     * Name of this Joint
     */
    protected String name;
    /** Parent to this joint */
    protected BaseJoint parent = null;
    /** Children of this joints.  Automatically added when {@link #setParent(BaseJoint)}
     * is called.
     */
    protected ArrayList children = new ArrayList(4);

    /** Local-Space Bind Orientation of this Quaternion */
    protected Quaternion bindLocOrientation = new Quaternion();
    protected Matrix bindLocOrientationMatrix = new Matrix();
    /**
     * Derived: local invBind Orientation Matrix: calculated from {@link #bindLocOrientation}.
     * <b>This actually stores the inverse bindMatrix of the Joint</b>
     *  
     *    <b> Rotation is applied first </b>
     * M =   Offset * bindLocTransMatrix * bindLocRotation
     *  
     * */
    protected Matrix bindLocTransformMatrix = new Matrix();



    //-----
    /** 1 KEY: Vector offset from parent: if parent is null, then this is
     * the location of this joint in world space.  Defined relative to parent */
    protected Vector3f xOffset = new Vector3f(0.0f,0.0f,0.0f);
    protected Matrix xOffsetMatrix = new Matrix();

    /** 2 KEY: Local Translation of this  Joint for animation purposes */
    protected Vector3f locAnimTranslation = new Vector3f(0.0f,0.0f,0.0f);
    protected Matrix locAnimTranslationMatrix = new Matrix();

    /** 3 KEY: Local Rotation of this Joint for animation purposes: defined relative to Joint-Space */
    protected Quaternion locAnimRot = new Quaternion(1.0f, 0.0f, 0.0f, 0.0f);
    /** Local Rotation of this Joint in Matrix form. Value is calculated
     * directly from {@link #locAnimRot}. */
    protected Matrix locAnimRotMatrix = new Matrix();
    /** The Combined Transformation of this joint in local space */
    protected Matrix locAnimTransformMatrix = new Matrix();

    /** World-Space Final Transformation of this Joint*/
    protected Matrix final_Transformation;
    /** WorldSpace Final Inv transformation of this Joint*/
    protected Matrix final_Inv_Transformation;

    protected Matrix finalSkinningTransformation ;


    public BaseJoint getParent(){
        return parent;
    }

    public Quaternion getBindLocOrientation(){
        return bindLocOrientation;
    }


    public Vector3f getxOffset(){
        return xOffset;
    }

    public Quaternion getLocAnimRot(){
        return locAnimRot;
    }

    public Vector3f getLocTranslation() {
        return locAnimTranslation;
    }
    /**
     * Supplied parent will be assigned to parent this Joint, while
     * this Joint will be assigned to the parents child
     * @param newParent new parent
     */
    public void setParent(BaseJoint newParent){
        //if current parent is not null
        if(this.parent != null){
            //if same parent is being set twice, exit
            if(this.parent == newParent)
                return;
            else{
                //else remove the this joint as a child
                this.parent.removeChild(this);
            }
        }
        this.parent = newParent;
        //if valid parent, add this as a child
        if(this.parent != null){
            this.parent.addChild(this);
        }
    }
    protected void addChild(BaseJoint child){
        if(child == null) return;
        if(child == this) return;
        if(child == parent) return;
        children.add(child);
    }
    protected void removeChild(BaseJoint child){
        if(children.contains(child)){
            children.remove(child);
        }
    }




    /**
     * Calculate the final Transformation of this Joint, then apply its final transformation
     * to its children
     * @param parentWorldTransformation strictly,only: final transformation of parents-1
     * @param counter passing in zero always initially; used to avoid runtime exception
     */
    public void calculateFinalMatrix(final Matrix parentWorldTransformation, int counter){
        if(parent != null &amp;&amp; counter==0)
            throw new RuntimeException("Joint["+name+"] calculateFinalMatrix should be called from root Joint");

        Matrix fParentTransform = Matrix.IDENTITY;
        //Use the argument supplied
        if(parentWorldTransformation != null)  fParentTransform = parentWorldTransformation;

        //Apply Parent first to get final transformation of this child
        final_Transformation = Matrix.multiply(final_Transformation, getLocalTransformation(),fParentTransform);

        counter ++;
        BaseJoint child;
        for( int i=0; i &lt; children.size(); i++){
            child =  children.get(i);
            child.calculateFinalMatrix(final_Transformation,counter);
        }
    }

    /**
     * Calculate this matrix and all Children related to this.
     * @param parentInvWorldTransformation strictly,only: final transformation of parents-1
     * @param counter passing in zero always; used to avoid runtime exception
     */
    public void calculateFinalInvMatrix(final Matrix parentInvWorldTransformation, int counter){
        if(parent != null &amp;&amp; counter ==0)
            throw new RuntimeException("Joint["+name+"]  calculateFinalInvMatrix should be called from root Joint");

        Matrix fParentInvTransform = Matrix.IDENTITY;

        //Use the argument supplied
        if(parentInvWorldTransformation != null)  fParentInvTransform = parentInvWorldTransformation;

        //Apply Parent first to get final transformation of this child
        final_Inv_Transformation =Matrix.multiply(final_Inv_Transformation,fParentInvTransform,  getLocalBindMatrix() );


        counter ++;
        BaseJoint child;
        for( int i=0; i &lt; children.size(); i++){
            child = children.get(i);
            child.calculateFinalInvMatrix(final_Inv_Transformation,counter);
        }
        final_Inv_Transformation.inverse();
    }


    /**
     * Get bindLocTransformMatrix of this joint in its local space
     *

This is used to undo {@link #getLocalTransformation()}

     * @return Local Inv Transformation (inverse bind pose in local space )
     */
    public Matrix getLocalBindMatrix(){
        // Matrix.multiply(bindLocTransformMatrix,  getLocalBindTranslationMatrix()  , getLocalBindOrientationMatrix()  );

        Matrix.multiply(bindLocTransformMatrix,  getLocalBindOrientationMatrix(),getOffsetMatrix());

        return bindLocTransformMatrix;
    }
    /** Strict: transformation of this Joint within its local space. This combines rotation, offset, and translation   */
    public Matrix getLocalTransformation(){
        Matrix.multiply(locAnimTransformMatrix,  getLocalRotationMatrix(),  getOffsetMatrix()  );
        Matrix.multiply(locAnimTransformMatrix, locAnimTransformMatrix ,getLocalTranslationMatrix());


        return locAnimTransformMatrix;
    }



    /**
     * Before calling this function, ensure that
     * {@link #calculateFinalMatrix(com.soliduscode.common.lib.math.Matrix, int)} has been called using
     * the root joint.
     * @return final Transformation of this Joint
     */
    public Matrix getFinal_Transformation(){
        if(final_Transformation == null) throw new RuntimeException("Joint: Joint["+name+"] getFinal_Transformation "+
                "has not been been calculated and such its null");



        return final_Transformation;
    }

    /**
     * Before calling this function, ensure that
     * {@link #calculateFinalInvMatrix(com.soliduscode.common.lib.math.Matrix,int), int} has
     * been called using the root joint.
     * @return final Inv Bind Pose of this Mesh
     */
    public Matrix getFinal_Inv_Transformation(){
        if(final_Inv_Transformation == null) throw new RuntimeException("Joint: Joint["+name+"] getFinal_Inv_Transformation "+
                "has not been been calculated and such its null");
        return final_Inv_Transformation;
    }


    public Matrix getLocalBindOrientationMatrix(){
        //Ignore
        bindLocOrientationMatrix = bindLocOrientation.toMatrix(bindLocOrientationMatrix);
        return bindLocOrientationMatrix;
    }



    /**
     * Get Local translation matrix of this Joint
     * @return get local translation animation matrix
     */
    protected Matrix getLocalTranslationMatrix(){
        locAnimTranslationMatrix.setTranslate(locAnimTranslation);
        return locAnimTranslationMatrix;
    }

    /** Offset this Joint is to its parent.  If parent is
     * null, then this matrix will be the position of the Object in world space */
    protected Matrix getOffsetMatrix(){
        xOffsetMatrix.setTranslate(xOffset);
        return xOffsetMatrix;
    }
    /** Local joint rotation about is parent offset */
    protected Matrix getLocalRotationMatrix(){
        locAnimRotMatrix = locAnimRot.toMatrix(locAnimRotMatrix);
        return locAnimRotMatrix;
    }

    /** Create a rotation around angle and axis */
    public void setLocalOrientation(float angle, Vector3f axis){
        locAnimRot.fromAngleAxis(angle, axis);
    }
    public void setLocalOrientation(float angleX, float angleY, float angleZ){

        locAnimRot.fromAngles(angleX,angleY, angleZ);
    }
    public void setLocalOrientation(Quaternion Q){
        locAnimRot.set(Q);
    }
    public void setLocalTranslation(float x, float y, float z) {
        locAnimTranslation.set(x, y, z);
    }
    public void setLocalTranslation(Vector3f P) {
        locAnimTranslation.set(P);
    }

    public String getName() {
        return name;
    }

    public void setBindOrientation(Quaternion quaternion) {
        Console.print("Setting BindOrientation for " + name);
        bindLocOrientation.set(quaternion);
    }

    /**
     * Get the world space position of this Joint, however, ensure that the matrix was recently built
     * @return
     */
    public Vector3f getPosition() {
        //Final transformat(FT) holds the world space position of the joint
        //
        Matrix FT = getFinal_Transformation();
        Vector3f POS = FT.getColumn(3);
        position.set(POS);

        return position;
    }

    public float getLength() {
        return length;
    }
    public void setLength(float length){
        this.length = length;
    }

    /**
     * Not used in calculating the transformation of this Joint.
     * However, it describes the direction this joint.
     */
    protected Vector3f direction =new Vector3f();

    /**
     *
     * @param direction
     */
    public void setDirection(Vector3f direction){
        this.direction.set(direction);
    }
    public Vector3f getDirection(){
        return this.direction;
    }

    public void setxOffset(Vector3f xOffset) {
        this.xOffset = xOffset;
    }
}

3 Responses

  1. Derek Lesho

    Hey Solidus, so implemented skeletal animation in a very similar way to this in my game engine, but I can not find a fitting file format. All of the formats use some weird weighted anchor point thing where the weights are vectors. So what file format do you import rigged models from? Thanks!

    • Derek Lesho

      I just found out you have your own file format, that is really cool! Did you write your own exporter in python?

  2. Derek Lesho

    In your .s3d file format, is rot_wxyz in local or global

Leave a Reply