[SOLVED] JME3.3 Animation Loop

Hello o/

Where can I find an example on how to loop an animation with the new animation system?

Thanks!

Play the animation. It will loop.

If you see otherwise then you may not be using the new animation system.

I am using the new system. There must be something wrong with how loaded up my animation. The mesh is deforming horrendously.

Thanks for the reply, now I have a better idea of where to look for the problem!

The joints seem to be doing all the wrong things. they animate but completely wrong. It looks like values are being overwritten as the animation plays.

You will have to give us more information to help. (Like, any information, basically.)

1 Like

I was in the middle of recording a gif.

Animation doesn’t even loop. just seemingly exponentially slows down

This is using an AssetLoader I wrote. For a format called IQM.

The first thing I would do is to check if my gltf file looks fine in an online gltf viewer.

https://gltf-viewer.donmccurdy.com/

https://sandbox.babylonjs.com/

Edit:
Ah, I thought it is a gltf file. But seems it’s not. Maybe it’s an issue with your custom loader.

Edit2:
And as pspeed said, the new animation system will loop the animation by default.

Ye, since it’s supposed to loop by default I’m led to believe that there’s something wrong with how I’ve loaded the animation into jme3

well, looking at gif it looks like its not looped and in the end of animation sizes of bones are much bigger.

Yes, it’s transforming completely incorrectly, but I’m not sure where exactly I have loaded in the incorrect values.

It’s supposed to look something like this:

Can you post the code?

Sure. Here’s the main class (reference):

/* 
 * Copyright (C) TBD - All Rights Reserved
 * Unauthorized copying of this file, via any medium is strictly prohibited
 * Proprietary and confidential
 * Written by Team TBD <EMAIL-REMOVED>
 */
package com.kudodev.multiplayergame.assets.iqm;

import com.jme3.anim.Armature;
import com.jme3.anim.Joint;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.kudodev.multiplayergame.mesh.MeshBuilder;
import com.kudodev.multiplayergame.mesh.MeshVertex;
import com.kudodev.multiplayergame.utils.Triple;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;

    /**
     *
     * @author Ike Yousuf <EMAIL-REMOVED>
     */
    public class IQMFormat {

        protected static final String IQM_MAGIC = "INTERQUAKEMODEL\0";

        protected static final int IQM_POSITION = 0; // float, 3
        protected static final int IQM_TEXCOORD = 1; // float, 2
        protected static final int IQM_NORMAL = 2; // float, 3
        protected static final int IQM_TANGENT = 3; // float, 4
        protected static final int IQM_BLENDINDEXES = 4; // ubyte, 4
        protected static final int IQM_BLENDWEIGHTS = 5; // ubyte, 4
        protected static final int IQM_COLOR = 6; // 
        protected static final int IQM_CUSTOM = 0x10;

        protected static final int IQM_BYTE = 0;
        protected static final int IQM_UBYTE = 1;
        protected static final int IQM_SHORT = 2;
        protected static final int IQM_USHORT = 3;
        protected static final int IQM_INT = 4;
        protected static final int IQM_UINT = 5;
        protected static final int IQM_HALF = 6;
        protected static final int IQM_FLOAT = 7;
        protected static final int IQM_DOUBLE = 8;

        public static IQMFile load(byte[] rawData) {
            ByteBuffer data = ByteBuffer.wrap(rawData).order(ByteOrder.LITTLE_ENDIAN);

            IQMHeader header = new IQMHeader(data);

            // build meshes
            EnumSet<VertexBuffer.Type> vAttributes = EnumSet.of(
                    VertexBuffer.Type.Position,
                    VertexBuffer.Type.Normal,
                    VertexBuffer.Type.TexCoord,
                    VertexBuffer.Type.Tangent);
            if (header.num_joints > 0) {
                vAttributes.add(VertexBuffer.Type.BoneIndex);
                vAttributes.add(VertexBuffer.Type.BoneWeight);
            }

            Map<Integer, String> text = getTextMap(data, header);

            IQMMesh[] iqmMeshes = readIQMMeshes(data, header);
            IQMVertexArray[] iqmVertexArrays = readIQMVertexArrays(data, header);
            IQMTriangle[] iqmTriangles = readIQMTriangles(data, header);
            MeshVertex[] vertexList = readMeshVertices(data, header, iqmVertexArrays, vAttributes);
            Map<String, Mesh> meshes = buildMeshes(header, iqmMeshes, vertexList, iqmTriangles, vAttributes, text);

            IQMJoint[] joints = readJoints(data, header);
            Map<String, IQMAnim> anims = readAnims(data, header, text);
            IQMPose[] poses = readPoses(data, header);
            int[] frameData = readFrameData(data, header);

            Triple<Vector3f, Quaternion, Vector3f>[] frames = processFrameData(frameData, poses, joints, header);

            IQMFile iqmFile = new IQMFile(meshes, anims, joints, frames, text);

            return iqmFile;
        }

        private static IQMVertexArray[] readIQMVertexArrays(ByteBuffer data, IQMHeader header) {
            if (header.num_vertexarrays == 0) {
                return null;
            }

            data.position(header.ofs_vertexarrays);
            IQMVertexArray[] vertexArrays = new IQMVertexArray[header.num_vertexarrays];
            for (int i = 0; i < vertexArrays.length; i++) {
                vertexArrays[i] = new IQMVertexArray(data);
            }
            return vertexArrays;
        }

        private static void checkVertexArray(IQMVertexArray va, int format, int size) {
            if (va.format != format || va.size != size) {
                throw new RuntimeException("Improper IQM file.");
            }
        }

        private static IQMTriangle[] readIQMTriangles(ByteBuffer data, IQMHeader header) {
            data.position(header.ofs_triangles);
            IQMTriangle[] triangles = new IQMTriangle[header.num_triangles];
            for (int i = 0; i < triangles.length; i++) {
                triangles[i] = new IQMTriangle(data);
            }
            return triangles;
        }

        private static IQMMesh[] readIQMMeshes(ByteBuffer data, IQMHeader header) {
            data.position(header.ofs_meshes);
            IQMMesh[] meshes = new IQMMesh[header.num_meshes];
            for (int i = 0; i < meshes.length; i++) {
                meshes[i] = new IQMMesh(data);
            }
            return meshes;
        }

        private static Map<String, Mesh> buildMeshes(IQMHeader header, IQMMesh[] iqmMeshes,
                MeshVertex[] vertexList, IQMTriangle[] iqmTriangles,
                EnumSet<VertexBuffer.Type> vAttributes, Map<Integer, String> text) {
            if (header.num_meshes == 0) {
                return null;
            }

            Map<String, Mesh> meshes = new HashMap<>();
            for (int i = 0; i < header.num_meshes; i++) {
                IQMMesh m = iqmMeshes[i];
                MeshBuilder builder = new MeshBuilder(vAttributes);
                for (int j = 0; j < m.num_vertexes; j++) {
                    builder.addVertex(vertexList[j + m.first_vertex]);
                }

                for (int j = 0; j < m.num_triangles; j++) {
                    IQMTriangle t = iqmTriangles[j + m.first_triangle];
                    builder.addTriangle(
                            t.triangle[2] - m.first_vertex,
                            t.triangle[1] - m.first_vertex,
                            t.triangle[0] - m.first_vertex);
                }

                Mesh jmeMesh = builder.build(false, false);
                meshes.put(text.get(m.name), jmeMesh);
            }

            return meshes;
        }

        private static MeshVertex[] readMeshVertices(ByteBuffer data, IQMHeader header, IQMVertexArray[] vertexArrays, EnumSet<VertexBuffer.Type> vAttributes) {
            if (vertexArrays == null) {
                return null;
            }

            MeshVertex[] vertexList = new MeshVertex[header.num_vertexes];
            for (int i = 0; i < header.num_vertexes; i++) {
                vertexList[i] = new MeshVertex(vAttributes);
            }

            for (IQMVertexArray va  : vertexArrays) {
                data.position(va.offset);
                switch (va.type) {
                    case IQM_POSITION:
                        checkVertexArray(va, IQM_FLOAT, 3);
                        for (MeshVertex v : vertexList) {
                            v.setPosition(data.getFloat(), data.getFloat(), data.getFloat());
                        }
                        break;
                    case IQM_TEXCOORD:
                        checkVertexArray(va, IQM_FLOAT, 2);
                        for (MeshVertex v : vertexList) {
                            v.setTextureCoords(data.getFloat(), data.getFloat());
                        }
                        break;
                    case IQM_NORMAL:
                        checkVertexArray(va, IQM_FLOAT, 3);
                        for (MeshVertex v : vertexList) {
                            v.setNormal(data.getFloat(), data.getFloat(), data.getFloat());
                        }
                        break;
                    case IQM_TANGENT:
                        checkVertexArray(va, IQM_FLOAT, 4);
                        for (MeshVertex v : vertexList) {
                            v.setTangent(data.getFloat(), data.getFloat(), data.getFloat());
                            data.getFloat(); //bitangent
                        }
                        break;
                    case IQM_BLENDINDEXES:
                        checkVertexArray(va, IQM_UBYTE, 4);
                        for (int i = 0; i < vertexList.length; i++) {
                            vertexList[i].setBlendIndex(
                                    data.get() & 0xFF,
                                    data.get() & 0xFF,
                                    data.get() & 0xFF,
                                    data.get() & 0xFF);
                        }
                        break;
                    case IQM_BLENDWEIGHTS:
                        checkVertexArray(va, IQM_UBYTE, 4);
                        for (int i = 0; i < vertexList.length; i++) {
                            vertexList[i].setBlendWeight(
                                    data.get() & 0xFF,
                                    data.get() & 0xFF,
                                    data.get() & 0xFF,
                                    data.get() & 0xFF);
    //                        (data.get() & 0xFF) / 255f,
    //                                (data.get() & 0xFF) / 255f,
    //                                (data.get() & 0xFF) / 255f,
    //                                (data.get() & 0xFF) / 255f);
                        }
                        break;
                }
            }

            return vertexList;
        }

        private static Triple<Vector3f, Quaternion, Vector3f>[] processFrameData(int[] frameData, IQMPose[] poses, IQMJoint[] joints, IQMHeader header) {
            if (frameData == null) {
                return null;
            }

            Triple<Vector3f, Quaternion, Vector3f>[] frames = new Triple[header.num_frames * header.num_poses];

            Matrix4f transform = new Matrix4f();
            int frameD = 0;
            for (int i = 0; i < header.num_frames; i++) {
                for (int j = 0; j < header.num_poses; j++) {
                    IQMPose p = poses[j];

                    int mask = p.mask;
                    float[] channelOffset = p.channeloffset;
                    float[] channelScale = p.channelscale;

                    float rotX, rotY, rotZ, rotW;
                    Quaternion rotate = new Quaternion();
                    Vector3f translate = new Vector3f();
                    Vector3f scale = new Vector3f();
                    frames[i * header.num_poses + j] = new Triple(translate, rotate, scale);

                    translate.x = channelOffset[0];
                    if ((mask & 0x01) != 0) {
                        translate.x += frameData[frameD++] * channelScale[0];
                    }
                    translate.y = channelOffset[1];
                    if ((mask & 0x02) != 0) {
                        translate.y += frameData[frameD++] * channelScale[1];
                    }
                    translate.z = channelOffset[2];
                    if ((mask & 0x04) != 0) {
                        translate.z += frameData[frameD++] * channelScale[2];
                    }

                    rotX = channelOffset[3];
                    if ((mask & 0x08) != 0) {
                        rotX += frameData[frameD++] * channelScale[3];
                    }
                    rotY = channelOffset[4];
                    if ((mask & 0x10) != 0) {
                        rotY += frameData[frameD++] * channelScale[4];
                    }
                    rotZ = channelOffset[5];
                    if ((mask & 0x20) != 0) {
                        rotZ += frameData[frameD++] * channelScale[5];
                    }
                    rotW = channelOffset[6];
                    if ((mask & 0x40) != 0) {
                        rotW += frameData[frameD++] * channelScale[6];
                    }
                    rotate.set(rotX, rotY, rotZ, rotW);

                    scale.x = channelOffset[7];
                    if ((mask & 0x80) != 0) {
                        scale.x += frameData[frameD++] * channelScale[7];
                    }
                    scale.y = channelOffset[8];
                    if ((mask & 0x100) != 0) {
                        scale.y += frameData[frameD++] * channelScale[8];
                    }
                    scale.z = channelOffset[9];
                    if ((mask & 0x200) != 0) {
                        scale.z += frameData[frameD++] * channelScale[9];
                    }

                    transform.setTransform(translate, scale, rotate.toRotationMatrix());

                    if (p.parent >= 0) {
                        Matrix4f parentTransform = joints[p.parent].baseframe;
                        parentTransform.mult(transform, transform);
                    }

                    transform.multLocal(joints[j].inverseBaseframe);
                    transform.toTranslationVector(translate);
                    transform.toRotationQuat(rotate);
                    transform.toScaleVector(scale);
                }
            }

            return frames;
        }

        private static int[] readFrameData(ByteBuffer data, IQMHeader header) {
            if (header.num_frames == 0) {
                return null;
            }

            data.position(header.ofs_frames);
            int[] frameData = new int[header.num_frames * header.num_framechannels];
            for (int i = 0; i < frameData.length; i++) {
                frameData[i] = data.getShort() & 0xFFFF;
            }
            return frameData;
        }

        private static Map<String, IQMAnim> readAnims(ByteBuffer data, IQMHeader header, Map<Integer, String> text) {
            if (header.num_anims == 0) {
                return null;
            }
            data.position(header.ofs_anims);
            Map<String, IQMAnim> anims = new HashMap<>();
            for (int i = 0; i < header.num_anims; i++) {
                IQMAnim anim = new IQMAnim(data);
                anims.put(text.get(anim.name), anim);
            }
            return anims;
        }

        private static IQMPose[] readPoses(ByteBuffer data, IQMHeader header) {
            if (header.num_poses == 0) {
                return null;
            }

            data.position(header.ofs_poses);
            IQMPose[] poses = new IQMPose[header.num_poses];
            for (int i = 0; i < poses.length; i++) {
                poses[i] = new IQMPose(data);
            }
            return poses;
        }

        private static IQMJoint[] readJoints(ByteBuffer data, IQMHeader header) {
            if (header.num_joints == 0) {
                return null;
            }

            data.position(header.ofs_joints);
            IQMJoint[] iqmJoints = new IQMJoint[header.num_joints];
            for (int i = 0; i < iqmJoints.length; i++) {
                IQMJoint iqmJoint = new IQMJoint(data, iqmJoints);
                iqmJoints[i] = iqmJoint;
            }

            return iqmJoints;
        }

        private static Map<Integer, String> getTextMap(ByteBuffer data, IQMHeader header) {
            data.position(header.ofs_text);

            byte[] rawBytes = new byte[header.num_text];
            data.get(rawBytes);
            String[] rawText = new String(rawBytes).split("\0");

            Map<Integer, String> text = new HashMap<>();
            int pos = 0;
            for (int i = 0; i < rawText.length; i++) {
                text.put(pos, rawText[i]);
                pos += rawText[i].length() + 1;
            }

            return text;
        }

        protected static Armature createArmature(IQMJoint[] iqmJoints, Map<Integer, String> text) {
            if (iqmJoints == null) {
                return null;
            }

            Joint[] jmeJoints = new Joint[iqmJoints.length];
            for (int i = 0; i < iqmJoints.length; i++) {
                IQMJoint iqmJoint = iqmJoints[i];

                Joint joint = new Joint(text.get(iqmJoint.name));

                joint.setLocalTranslation(iqmJoint.translate);
                joint.setLocalRotation(iqmJoint.rotate);
                joint.setLocalScale(iqmJoint.scale);
                jmeJoints[i] = joint;

                if (iqmJoint.parent >= 0) {
                    Joint parent = jmeJoints[iqmJoint.parent];
                    parent.addChild(joint);
                }
            }

            Armature armature = new Armature(jmeJoints);
            armature.update();
            armature.saveBindPose();
            armature.applyBindPose();
            armature.saveInitialPose();

            return armature;
        }
    }

IQMJoint:

/* 
 * Copyright (C) TBD - All Rights Reserved
 * Unauthorized copying of this file, via any medium is strictly prohibited
 * Proprietary and confidential
 * Written by Team TBD <EMAIL-REMOVED>
 */
package com.kudodev.multiplayergame.assets.iqm;

import com.jme3.math.Matrix3f;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import java.nio.ByteBuffer;

/**
 *
 * @author Ike Yousuf <EMAIL-REMOVED>
 */
class IQMJoint {

    protected final int name;
    protected final int parent;
    protected final Vector3f translate;
    protected final Quaternion rotate;
    protected final Vector3f scale;

    protected final Matrix4f baseframe;
    protected final Matrix4f inverseBaseframe;

    protected IQMJoint(ByteBuffer data, IQMJoint[] joints) {
        this.name = data.getInt();
        this.parent = data.getInt();
        this.translate = new Vector3f(data.getFloat(), data.getFloat(), data.getFloat());
        this.rotate = new Quaternion(data.getFloat(), data.getFloat(), data.getFloat(), data.getFloat()).normalizeLocal();
        this.scale = new Vector3f(data.getFloat(), data.getFloat(), data.getFloat());

        this.baseframe = new Matrix4f();
        this.inverseBaseframe = new Matrix4f();

        this.baseframe.setTransform(translate, scale, rotate.toRotationMatrix());
        this.baseframe.invert(this.inverseBaseframe);

        if (this.parent >= 0) {
            IQMJoint parentJoint = joints[this.parent];
            parentJoint.baseframe.mult(this.baseframe, this.baseframe);
            this.inverseBaseframe.multLocal(parentJoint.inverseBaseframe);
        }
    }

}

I’ve been in rapid fire trying to get it to work. So there could be random snippets around that i haven’t cleaned up.

I’ve figured out the problem. There were 2…

First: I was providing a faulty time array to TransformTrack.

I had something like this, by accident:

float frameTime = 0;
for (int j = 0; j < numPoses; j++) {
    float[] times = new float[anim.num_frames];
    Vector3f[] translations = new Vector3f[anim.num_frames];
    Quaternion[] rotations = new Quaternion[anim.num_frames];
    Vector3f[] scales = new Vector3f[anim.num_frames];

    for (int i = 0; i < anim.num_frames; i++) {        
        times[i] = frameTime;
        frameTime += frameDuration;
...
...
    }

    tracks[j] = new TransformTrack(armature.getJoint(j), times, translations, rotations, scales);
}

frameTime was outside the outer loop… I’ve also realized i could just make the time array a single time, and reuse it…

Second problem: I was applying the hierarchical transforms, and invBindMatrices for each frame of each joint. I guess in JME3 we don’t need to do this, and is done during animation time?

So overall seems like its working for now! Thanks for everything.

3 Likes