Skeletal Animation

So I have been working on skeletal animation and skinning for a few weeks, and I hit a road block recently. I am importing the models from the MD5 format, and I have skinning and rendering meshes working.

Everything works fine until I try to animate the model and my inverse bind pose matrices distort the model.

Here is a screenshot of what is happening. The left is what the static model looks like while the right shows what happens after I load the animation and compute the inverse bind pose matrices.

Here is how I compute the bind pose matrices (I call computeBindPose() and traverse the bone hierarchy):

public void computeBindPose() {
		computeBindPose(new Matrix4f(), new Matrix4f(), new Matrix4f());
	}
	
	private void computeBindPose(Matrix4f base, Matrix4f tmp, Matrix4f tmp2) {
		if (bindPose != null)
			bindPose.computeMatrix(tmp);
		base.mul(tmp, bindPoseMatrix);
		bindPoseMatrix.invert(invBindPoseMatrix);
		children.forEach(joint -> joint.computeBindPose(bindPoseMatrix, tmp, tmp2));
	}

Here is my shader for rendering the meshes:

#version 150 core

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

in vec3 in_Position;
in vec3 in_Normal;
in vec2 in_TextureCoord;
in vec4 in_BoneIndices;
in vec4 in_BoneWeights;

uniform mat4 boneMat[64];

out vec2 pass_TextureCoord;

void main(void) {
	vec4 newVertex = vec4(0.0);
	vec4 newNormal = vec4(0.0);
	
	mat4 boneTransform = boneMat[int(in_BoneIndices.x)] * in_BoneWeights.x + boneMat[int(in_BoneIndices.y)] * in_BoneWeights.y + boneMat[int(in_BoneIndices.z)] * in_BoneWeights.z + boneMat[int(in_BoneIndices.w)] * in_BoneWeights.w;
	
	newVertex = boneTransform * vec4(in_Position, 1.0);
	newNormal = boneTransform * vec4(in_Normal, 0.0);

	gl_Position = (projectionMatrix * viewMatrix * modelMatrix) * vec4(newVertex.xyz, 1.0);
	pass_TextureCoord = in_TextureCoord;
}

How do you compute the matrices?

This is my code for computing them:

public void computeMatrix(Matrix4f matrix) {
		matrix.translationRotateScale(offset, rotation, scaling);
	}

I read the offset and rotation from the baseframe section of the MD5 file.

I’m not awake enough to go through your code, but you’re definitely doing something wrong.

Here’s my code for this:

  1. Precompute bindpose: matrices = translationRotateScale(offset, rotation, scaling).invert(). Make sure that this is transformed by the parent as well:

Vector3f translation = new Vector3f(joint.getTranslation());
Quaternionf orientation = new Quaternionf(joint.getOrientation());
Vector3f scale = new Vector3f(joint.getScale());
if(joint.getParent() != null){
	parentOrientation.transform(translation);
	translation.mul(parentScale).add(parentTranslation);
	parentOrientation.mul(orientation, orientation);
	scale.mul(parentScale);
}
matrix[i] = new Matrix4f().translationRotateScale(translation, orientation, scale).invert();

  1. When animating, interpolate between two frames, transform by the (animated) parent joint, generate a matrix and multiply it with the precomputed matrix:

//Interpolate between translations, orientations and scales. Use lerp() for Vector3fs and slerp/nlerp/nlerpIterative() for quaternions.

Vector3f translation = ...; //lerp()
Quaternionf orientation = ...; //slerp()
Vector3f scale = ...; //lerp()
if(joint.getParent() != null){
   //Note that this is the already lerped/slerped values of the parent.
   parentOrientation.transform(translation);
   translation.mul(parentScale).add(parentTranslation);
   parentOrientation.mul(orientation, orientation);
   scale.mul(parentScale);
}
matrix[i].translationRotateScaleMulAffine(translations[i], orientations[i], scales[i], bindPoseMatrices[i]);

One performance improvement. In [icode]new Matrix4f().translationRotateScale(…).invert();[/icode] do [icode].invertAffine()[/icode] instead.
invert() = 94 muls, 49 adds
invertAffine() = 57 muls, 24 adds

Oh, and since you always have access to the individual translation, orientiation and scale parts, you can get rid of the invert() call altogether by applying the inverse of the individual components in the order: scale^-1 * orientation^-1 * translation^-1
where scale^-1 is just 1/scale, orientation^-1 is just orientation.conjugate() and translation^-1 is just translation.negate().
But I guess since the invbindpose matrix is precomputed, this is not a big gain. :slight_smile:

It’s probably <1000 bones, even for AAA games. It’s not worth complicating the code for it.

It would of course be a simple Matrix4f.translationRotateScaleInvert() :slight_smile:
taking the same parameters that translationRotateScale() takes.

EDIT: Latest 1.8.1-SNAPSHOT contains this new method. JMH benchmarking shows a 2x performance boost over translationRotateScale().invertAffine()

I’ve been looking over my code and trying to get it working, but for the life of me I don’t know why it looks so distorted.

What I know:
MD5 animation files use the baseframe data and then replace values each frame (based on bit flags) to represent the orientation and position of each bone, and part of the calculation includes computing the w-component of the quaternions. This data is used (as well as the values for each joint’s parent) to compute the bone matrix, which is applied in the shader. Interpolating between frames is done by lerp() for translation and scale and slerp() for orientation.

What I’m doing
This is my code for computing the bone matrix. [icode]computeBoneMatrix()[/icode] is called for the root bone and it traverses the hierarchy. The translation, scale, and orientation are instance fields for each joint.

public void computeBoneMatrix() {
		computeBoneMatrix(null);
	}
	
	private Vector3f translation = new Vector3f(), scale = new Vector3f();
	
	private Quaternionf orientation = new Quaternionf();
	
	private void computeBoneMatrix(Joint parent) {
		translation.set(animationSequence.getTranslation());
		orientation.set(animationSequence.getOrientation());
		scale.set(animationSequence.getScale());
		if (parent != null) {
			parent.orientation.transform(translation);
			translation.mul(parent.scale).add(parent.translation);
			parent.orientation.mul(orientation, orientation);
			scale.mul(parent.scale);
		}
		bindPoseMatrix.translationRotateScale(translation, orientation, scale).invert(invBindPoseMatrix);
		children.forEach(joint -> joint.computeBoneMatrix(this));
	}

How I compute the w-component of the quaternions:

private Quaternionf convertQuat(MD5Vector3 orientation) {
		float t = 1.0f - (orientation.getX() * orientation.getX() + orientation.getY() * orientation.getY() + orientation.getZ() * orientation.getZ());
		float w = t < 0 ? 0 : (float)-Math.sqrt(t);
		return new Quaternionf(orientation.getX(), orientation.getY(), orientation.getZ(), w);
	}

How I replace data for generating the frames:

int j = 0;
			if ((flags & 1) != 0) {
				position.x = frame.getData()[joint.getStartIndex() + j];
				j++;
			}
			if ((flags & 2) != 0) {
				position.y = frame.getData()[joint.getStartIndex() + j];
				j++;
			}
			if ((flags & 4) != 0) {
				position.z = frame.getData()[joint.getStartIndex() + j];
				j++;
			}
			if ((flags & 8) != 0) {
				orientation.x = frame.getData()[joint.getStartIndex() + j];
				j++;
			}
			if ((flags & 16) != 0) {
				orientation.y = frame.getData()[joint.getStartIndex() + j];
				j++;
			}
			if ((flags & 32) != 0) {
				orientation.z = frame.getData()[joint.getStartIndex() + j];
				j++;
			}

How I interpolate between frames:

public void lerp(JointTransform end, float t, JointTransform destination) {
		offset.lerp(end.offset, t, destination.offset);
		rotation.slerp(end.rotation, t, destination.rotation);
		scaling.lerp(end.scaling, t, destination.scaling);
	}

I tried to pick out the relevant parts of code, but I can paste more if necessary. I just simply don’t understand why this code isn’t working as multiple tutorials (as well as other people’s code compiled into games) use the same code and get working skeletal animation.

Hmm. That all looks correct.

  • Are you trying to do some kind of conversion from Y-up to Z-up or something like that? It’s not easy to do that with quaternions.

  • You’ve shown the code for generating the bind pose matrix. Where’s the code for computing an animated matrix?

The intuitive idea behind skeleton animation is to create a local coordinate system for each bone, which is constructed from a translation and a rotation (and a scale, but ignore that for the intuition). Basically, we want a way to calculate the position of a vertex relative to a bone. This is done the exact same way we compute a view matrix: Create a matrix with the transform of the camera, then invert it.

Example: We have a camera at position (1, 1, 1) and construct a matrix: [icode]matrix.translation(cameraPosition);[/icode] is a matrix which simply adds the camera’s position to each vertex. If we have a vertex at (0, 0, 0) relative to the camera and want to know where it is in the world, we just add the camera’s position to it. Since the vertex is at the same point as the camera in this case (it is at (0, 0, 0) relative to the camera after all), the vertex is at (1, 1, 1) too. Easy to understand. However, we already have vertices in world space, and want to know where they are relative to the camera so we can draw them on a screen. So, we just invert the matrix we made, which in this case is the same as [icode]matrix.translation(-cameraPosition);[/icode] since it’s a simple transformation. It’s clear that if we have a world space vertex at (1, 1, 1) and apply a (-1, -1, -1) translation to it, we end up at (0, 0, 0) relative to the camera again, as we should.

We do the same thing when computing the “inverse bind pose matrix” or whatever you want to call it. We first calculate the default transform matrices of each bone of the bind pose, then invert it to create a matrix that takes us from model space to a coordinate system relative to the bone. Simply put, it allows us to calculate where a given vertex is compared to a bone. Now, why is this useful? By calculating where a vertex is relative to a bone, we can move the bone and calculate a new position of every vertex affected by it easily by simply calculating the relative position of a vertex and then taking it back to model space again using a different (animated) bone matrix. The result is what we call skeleton animation.

What you want to do is simply this:

vec4 localPosition = inverseBindPoseMatrix * modelSpacePosition;
vec4 newModelSpacePosition = animatedBoneMatrix * localPosition;

which can be rearranged like this:

vec4 newModelSpacePosition = animatedBoneMatrix * (inverseBindPoseMatrix * modelSpacePosition);
vec4 newModelSpacePosition = (animatedBoneMatrix * inverseBindPoseMatrix) * modelSpacePosition;

In other words, you can precompute a single bone matrix which takes the vertex from its current model space position directly to its new model space position by precomputing (animatedBoneMatrix*inverseBindPoseMatrix).

Now, bone animation generally uses a weighted average of 4 different bones for each vertex. That just means that we compute the new position that each bone would give us and average together the results.

vec4 newModelSpacePosition = 
        ((animatedBoneMatrix1 * inverseBindPoseMatrix) * modelSpacePosition) * weight1 + 
        ((animatedBoneMatrix2 * inverseBindPoseMatrix) * modelSpacePosition) * weight2 + 
        ((animatedBoneMatrix3 * inverseBindPoseMatrix) * modelSpacePosition) * weight3 + 
        ((animatedBoneMatrix4 * inverseBindPoseMatrix) * modelSpacePosition) * weight4;

which can be rearranged to:

vec4 newModelSpacePosition = 
        (animatedBoneMatrix1 * inverseBindPoseMatrix) * weight1 * modelSpacePosition + 
        (animatedBoneMatrix2 * inverseBindPoseMatrix) * weight2 * modelSpacePosition + 
        (animatedBoneMatrix3 * inverseBindPoseMatrix) * weight3 * modelSpacePosition + 
        (animatedBoneMatrix4 * inverseBindPoseMatrix) * weight4 * modelSpacePosition;

and then to

vec4 newModelSpacePosition = 
        (
            ((animatedBoneMatrix1 * inverseBindPoseMatrix) * weight1) +
            ((animatedBoneMatrix2 * inverseBindPoseMatrix) * weight2) +
            ((animatedBoneMatrix3 * inverseBindPoseMatrix) * weight3) + 
            ((animatedBoneMatrix4 * inverseBindPoseMatrix) * weight4)
        ) * modelSpacePosition;

which is the most efficient way of doing it and what you’re doing in your shader already. :wink:

This may all look complicated at a glance, but it’s really just multiplication and addition, just on coordinates and matrices. If you can grasp how this works, then you should be able to debug your code and fix it. There really isn’t a shortcut to getting skeleton animation to just work without understanding it, and if you can grasp it you’ll be one of the few people in the world who fully understands how this works.

Hi.

I worked few years ago on the same problem.
I post fragments of my code. Maybe this will help.

Joint.java


public class Joint implements NamedObject
{
    public final int NULL_PARENT = -1;

    private final short parentIndex;
    private final String name;
    private final Vector3f bindTranslation;
    private final Quaternion4f bindRotation;
    private final Tuple3f bindScale;

    private final Matrix4f invBindPoseMatrix;

    private short index;
    private boolean isAux;

    public final boolean hasParent()
    {
        return ( index > 0 );
    }

    public final void calcInvBindPoseMatrix( Joint parent )
    {
        Matrix4f m = Matrix4f.fromPool();

        MathUtils.compose( bindTranslation, bindRotation, bindScale, m );
        invBindPoseMatrix.invert( m );
        if( parent != null )
        {
            invBindPoseMatrix.mul( invBindPoseMatrix, parent.getInvBindPoseMatrix() );
        }

        Matrix4f.toPool( m );
    }
    public Joint( String name, short index, Joint parent, Transform bindTransform, boolean isAux )
    {
        this.parentIndex = parent == null ? NULL_PARENT : parent.getIndex();
        this.name = name;
        this.index = index;
        bindTranslation = bindTransform.getTranslation( null ).getReadOnly();
        bindRotation = bindTransform.getRotation( null ).getReadOnly();
        bindScale = bindTransform.getScale( null ).getReadOnly();

        invBindPoseMatrix = new Matrix4f();
        calcInvBindPoseMatrix( parent );
        this.isAux=isAux;
    }

    public Joint( Joint joint, Joint parent )
    {
        this.parentIndex = parent == null ? NULL_PARENT : parent.getIndex();
        name = joint.name;
        index = joint.index;

        bindTranslation = joint.bindTranslation;
        bindRotation = joint.bindRotation;
        bindScale = joint.bindScale;

        invBindPoseMatrix = joint.invBindPoseMatrix;
    }
}

SkinnedMeshUpdater.java


public class SkinnedMeshUpdater
{  
    private static Map<Skeleton, SkinnedMeshUpdater> skeletonToUpdater = new HashMap<Skeleton, SkinnedMeshUpdater>();
    private final Skeleton skeleton;
    private final Shape3D[] shapes;

    private final float[][] weights;
    private final short[][] jointIndices;

    private float[][] bindPoseCoords;
    private float[][] bindPoseNormals;

    private final int[] influencesPerVertex;

    private final Vector3f[] worldTranslations;
    private final Quaternion4f[] worldRotations;
    private final Tuple3f[] worldScales;

    private Matrix4f[] matrixPalette;

    public Skeleton getSkeleton()
    {
        return skeleton;
    }

    public Shape3D[] getTarget()
    {
        return shapes;
    }

    public float[][] getWeights()
    {
        return weights;
    }

    public short[][] getJointIndices()
    {
        return jointIndices;
    }

    public int[] getInfluencesPerVertex()
    {
        return influencesPerVertex;
    }

    public static SkinnedMeshUpdater create( Skeleton skeleton,
                                             Shape3D[] target,
                                             float[][] weights,
                                             short[][] jointIndices,
                                             int[] influencesPerVertex )
    {
        SkinnedMeshUpdater smu = skeletonToUpdater.get( skeleton );
        if( smu == null )
        {
            smu = new SkinnedMeshUpdater( skeleton, target, weights, jointIndices, influencesPerVertex );
        }

        return ( smu );
    }

    public void update( IAnimationData data )
    {
        if( data == null )
        {
            return;
        }
        updateSkeleton( data );
        for( int i = 0; i < shapes.length; i++ )
        {
            updateGeometry( i );
        }
    }

    private void updateSkeleton( IAnimationData data )
    {
        SkeletalKeyFrame pose = ( SkeletalKeyFrame ) data;
        Skeleton skeleton = pose.getSkeleton();
        for( int i = 0, jc = skeleton.getJointsCount(); i < jc; i++ )
        {
            Joint joint = skeleton.getJoint( i );
            setAbsolutes( joint, pose );
            short ji = joint.getIndex();
            if( joint.isAux() )
            {
                matrixPalette[ ji ].setIdentity();
            }
            else
            {
                mul( joint.getInvBindPoseMatrix(),
                        worldTranslations[ ji ],
                        worldRotations[ ji ],
                        worldScales[ ji ],

                        matrixPalette[ ji ]
                );
            }
        }
    }

    private void updateGeometry( int shapeIdx )
    {
        Shape3D shape = shapes[ shapeIdx ];
        Geometry geom = shape.getGeometry();
        float[] bindPoseCoords = this.bindPoseCoords[ shapeIdx ];
        float[] bindPoseNormals = this.bindPoseNormals[ shapeIdx ];
        int influencesPerVertex = this.influencesPerVertex[ shapeIdx ];
        float[] weights = this.weights[ shapeIdx ];
        short[] jointIndices = this.jointIndices[ shapeIdx ];

        float x, y, z;
        float nX = 0, nY = 0, nZ = 0;
        float vSumX, vSumY, vSumZ;
        float nSumX, nSumY, nSumZ;
        float tmpX, tmpY, tmpZ;

        for( int i = 0; i < geom.getVertexCount(); i++ )
        {
            x = bindPoseCoords[ i * 3 ];
            y = bindPoseCoords[ i * 3 + 1 ];
            z = bindPoseCoords[ i * 3 + 2 ];
            if( geom.hasNormals() )
            {
                nX = bindPoseNormals[ i * 3 ];
                nY = bindPoseNormals[ i * 3 + 1 ];
                nZ = bindPoseNormals[ i * 3 + 2 ];
            }
            vSumX = 0f;
            vSumY = 0f;
            vSumZ = 0f;

            nSumX = 0f;
            nSumY = 0f;
            nSumZ = 0f;

            float wSum = 0f;
            float weight;
            for( int j = 0; j < influencesPerVertex; j++ )
            {
                if( j != influencesPerVertex - 1 )
                {
                    weight = weights[ i * ( influencesPerVertex - 1 ) + j ];
                    wSum += weight;
                }
                else
                {
                    weight = 1 - wSum;
                }
                if( weight == 0f )
                {
                    continue;
                }
                final int jointIndex = jointIndices[ i * influencesPerVertex + j ];
                if( jointIndex == 0 )
                {
                    JAGTLog.debug( "jindex=0, influencesPerVertex=", influencesPerVertex );
                }
                Matrix4f m = matrixPalette[ jointIndex ];
                //vertices
                tmpX = m.m00() * x + m.m01() * y + m.m02() * z + m.m03();
                tmpY = m.m10() * x + m.m11() * y + m.m12() * z + m.m13();
                tmpZ = m.m20() * x + m.m21() * y + m.m22() * z + m.m23();

                vSumX += tmpX * weight;
                vSumY += tmpY * weight;
                vSumZ += tmpZ * weight;

                //normals
                if( geom.hasNormals() )
                {
                    tmpX = m.m00() * nX + m.m01() * nY + m.m02() * nZ;
                    tmpY = m.m10() * nX + m.m11() * nY + m.m12() * nZ;
                    tmpZ = m.m20() * nX + m.m21() * nY + m.m22() * nZ;

                    nSumX += tmpX * weight;
                    nSumY += tmpY * weight;
                    nSumZ += tmpZ * weight;
                }
            }

            geom.setCoordinateWithoutOpenGlHandling( i, vSumX, vSumY, vSumZ );
            if( geom.hasNormals() )
            {
                geom.setNormalWithoutOpenGlHandling( i, nSumX, nSumY, nSumZ );
            }
        }
        geom.setBoundsDirty();
        geom.getOpenGLReference_DL_GeomData().invalidateNames();
        geom.getOpenGLReference_DL().invalidateNames();

        shape.updateBounds( false );
    }

    private static void mul( Matrix4f matrix, Vector3f translation, Quaternion4f rotation, Tuple3f scale, Matrix4f out )
    {
        //out.set( matrix );
        Matrix4f tmp = Matrix4f.fromPool();

        tmp.setIdentity();
        tmp.setTranslation( translation );
        out.set( tmp );

        tmp.set( rotation );
        out.mul( tmp );

        tmp.setIdentity();
        tmp.m00( scale.getX() );
        tmp.m11( scale.getY() );
        tmp.m22( scale.getZ() );
        out.mul( tmp );

        out.mul( matrix/* , out */ );

        Matrix4f.toPool( tmp );
    }

    //doesn't work  when non-uniform scales
    private void setAbsolutes( Joint joint, SkeletalKeyFrame frame )
    {
        if( joint.hasParent() )
        {
            short parentIndex = joint.getParentIndex();
            short localIndex = joint.getIndex();
            MathUtils.mul(
                    worldTranslations[ parentIndex ],
                    worldRotations[ parentIndex ],
                    worldScales[ parentIndex ],

                    frame.getTranslations()[ localIndex ],
                    frame.getRotations()[ localIndex ],
                    frame.getScales()[ localIndex ],

                    worldTranslations[ localIndex ],
                    worldRotations[ localIndex ],
                    worldScales[ localIndex ]
            );
        }
        else
        {
            short localIndex = joint.getIndex();
            worldTranslations[ localIndex ].set( frame.getTranslations()[ localIndex ] );
            worldRotations[ localIndex ].set( frame.getRotations()[ localIndex ] );
            worldScales[ localIndex ].set( frame.getScales()[ localIndex ] );
        }
    }

    public SkinnedMeshUpdater( Skeleton skeleton, Shape3D[] target, float[][] weights, short[][] jointIndices, int[] influencesPerVertex )
    {
        this.skeleton = skeleton;
        this.shapes = target;
        this.weights = weights;
        this.jointIndices = jointIndices;
        this.influencesPerVertex = influencesPerVertex;

        worldTranslations = new Vector3f[ skeleton.getJointsCount() ];
        worldRotations = new Quaternion4f[ skeleton.getJointsCount() ];
        worldScales = new Tuple3f[ skeleton.getJointsCount() ];
        matrixPalette = new Matrix4f[ skeleton.getJointsCount() ];

        for( int i = 0; i < worldTranslations.length; i++ )
        {
            worldTranslations[ i ] = new Vector3f();
            worldRotations[ i ] = new Quaternion4f();
            worldScales[ i ] = new Tuple3f();
            matrixPalette[ i ] = new Matrix4f();
        }

        bindPoseCoords = new float[ target.length ][];
        bindPoseNormals = new float[ target.length ][];

        for( int i = 0; i < target.length; i++ )
        {
            int numVertices = target[ i ].getGeometry().getVertexCount();
            Geometry geom = target[ i ].getGeometry();
            bindPoseCoords[ i ] = new float[ numVertices * 3 ];
            geom.getCoordinates( 0, bindPoseCoords[ i ] );
            if( geom.hasNormals() )
            {
                bindPoseNormals[ i ] = new float[ numVertices * 3 ];
                geom.getNormals( 0, bindPoseNormals[ i ] );
            }
        }
    }
}

So I realized that I was only factoring in the bind pose matrix and not the animated matrix, but everything is still looking distorted. Since much of the code works fine until the model is animated, I think I can narrow down my problem to a few snippets.

They can be found here: http://pastebin.java-gaming.org/cf0c4276d4313

Also, in case this is relevant, this is how I determine the initial positions to send to the shader:

for (int idx = 0; idx < submesh.getVertices().length; idx++) {
			com.digiturtle.md5.skeleton.Vertex vertex = submesh.getVertices()[idx];
			AnimatedVertex animatedVertex = new AnimatedVertex();
			Vector3f position = new Vector3f().zero();
			Vector3f normal = new Vector3f().zero();
			Vector2f textureCoords = new Vector2f().zero();
			float[] boneWeightsArray = { 0, 0, 0, 0 };
			float[] boneIndicesArray = { 0, 0, 0, 0 };
			Vector3f tmpPos = new Vector3f(), tmpPos2 = new Vector3f();
			textureCoords.set(vertex.getTextureCoords());
			for (int j = 0; j < 4 && j < vertex.getWeightCount(); j++) {
				// position += (joint_position + (joint_orientation * weight_position)) * weight_bias
				Weight weight = submesh.getWeights().get(vertex.getStartWeight() + j);
				Joint joint = skeleton.getJoints().get(weight.getJointIndex());
				tmpPos.set(weight.getPosition()).rotate(joint.getLocalTransform().getOrientation());
				tmpPos2.set(tmpPos).add(joint.getLocalTransform().getTranslation()).mul(weight.getWeightBias());
				position.add(tmpPos2);
				boneIndicesArray[j] = (float) weight.getJointIndex();
				boneWeightsArray[j] = weight.getWeightBias();
			}
			animatedVertex.setPosition(position.x, position.y, position.z);
			animatedVertex.setNormal(normal.x, normal.y, normal.z);
			animatedVertex.setTextureCoord(textureCoords.x, textureCoords.y);
			animatedVertex.setBoneWeights(boneWeightsArray[0], boneWeightsArray[1], boneWeightsArray[2], boneWeightsArray[3]);
			animatedVertex.setBoneIndices(boneIndicesArray[0], boneIndicesArray[1], boneIndicesArray[2], boneIndicesArray[3]);
			geometry.put(animatedVertex.getData());
		}

If everything works fine till animation then the issue must be isolated to the routing that uploads the new bone matrix array to the shader (boneMat). How do you upload the data initially and how do you upload the updated data?

I’d recommend you to implement software-based animation system first.
You can use flag (useGPU) to switch between software and GPU implementations.

I also struggled with animation data (I implemented Collada loader at that time) and watched good bind pose and wrong animation that resembled something like devil’s dances.

One of the first bugs I found was incorrectly computed inverse bind pose matrix.
Also I recommend you to implement skeleton as the flat array not the tree-structure.

I’ve got the same problem. My mesh looks like some kind of spiky spaghetti.

CopyableCougar4, have you fixed the problem?

I’ve been working on it but haven’t found a solution yet. My mesh looks fine until I animate it. The instant the matrices (bindpose and animation) are no longer identity matrices, everything gets distorted (i.e. the feet are above the torso). Although it does look better than the original distortions.

In your latest pastebin, I don’t see where you are removing the bind pose by using the inverse bind pose, before animating.

Are you doing that?

My guess is that the inverse bind pose matrices aren’t being computed correctly. Either that, or the per-vertex bone indices and weights are incorrect.

if the identity position works fine then it could be a matrix multiplication order issue where you have AB rather than BA

I’m away from my development PC but will post relevant code/descriptions tomorrow.

My matrix multiplication order is boneMatrix * inverseBindPose.

I compute the bindpose matrix with

translation = bindPose.getTranslation();
		bindPoseMatrix.translationRotate(translation.x, translation.y, translation.z, bindPose.getOrientation());
		bindPoseMatrix.invert(invBindPoseMatrix);

for each bone, where translation and orientation are based on the baseframe data (with w computed like shown in a previous post).

I compute the bone matrices with

int jointIdx = index[0];
			if (parent == null) {
				frameData[jointIdx + 0] = position.x;
				frameData[jointIdx + 1] = position.y;
				frameData[jointIdx + 2] = position.z;
				frameData[jointIdx + 3] = orientationQuat.x;
				frameData[jointIdx + 4] = orientationQuat.y;
				frameData[jointIdx + 5] = orientationQuat.z;
				frameData[jointIdx + 6] = orientationQuat.w;
			} else {
				int parentJointIdx = startIndices.get(new JointID(parent.getName()));
				parentPos.x = frameData[parentJointIdx + 0];
				parentPos.y = frameData[parentJointIdx + 1];
				parentPos.z = frameData[parentJointIdx + 2];
				parentOrient.x = frameData[parentJointIdx + 3];
				parentOrient.y = frameData[parentJointIdx + 4];
				parentOrient.z = frameData[parentJointIdx + 5];
				parentOrient.w = frameData[parentJointIdx + 6];
				parentOrient.transform(position);
				frameData[jointIdx + 0] = position.x + parentPos.x;
				frameData[jointIdx + 1] = position.y + parentPos.y;
				frameData[jointIdx + 2] = position.z + parentPos.z;
				parentOrient.mul(orientationQuat);
				parentOrient.normalize();
				frameData[jointIdx + 3] = parentOrient.x;
				frameData[jointIdx + 4] = parentOrient.y;
				frameData[jointIdx + 5] = parentOrient.z;
				frameData[jointIdx + 6] = parentOrient.w;
			}
			startIndices.put(id, jointIdx);
			Vector3f pos = new Vector3f();
			pos.x = frameData[jointIdx + 0];
			pos.y = frameData[jointIdx + 1];
			pos.z = frameData[jointIdx + 2];
			Quaternionf orient = new Quaternionf(0, 0, 0, 1);
			orient.x = frameData[jointIdx + 3];
			orient.y = frameData[jointIdx + 4];
			orient.z = frameData[jointIdx + 5];
			orient.w = frameData[jointIdx + 6];

and then use Matrix4f.translationRotate(pos.x, pos.y, pos.z, orient).

The above code basically takes the frame data, replaces baseframe data based on the flags, and then adds that to the parent frame data. (I believe I pulled this code snippet from an early version of LibGDX).