2

tl;dr: When animating a model, each joint moves correctly, but not relative to its parent joint.

enter image description here

I am working on a skeletal animation system using a custom built IQE loader and renderer in Lua. Nearly everything is working at this point, except the skeleton seems to be disjointed when animating. Each joint translates, rotates, and scales correctly but is not taking the position of its parent into account, thus creating some awful problems.

In referencing the IQM spec and demo, I cannot for the life of me find out what is going wrong. My Lua code is (as far as I can tell) identical to the reference C++.

Calculating Base Joint Matrices:

local base = self.active_animation.base
local inverse_base = self.active_animation.inverse_base

for i, joint in ipairs(self.data.joint) do
    local pose = joint.pq

    local pos = { pose[1], pose[2], pose[3] }
    local rot = matrix.quaternion(pose[4], pose[5], pose[6], pose[7])
    local scale = { pose[8], pose[9], pose[10] }

    local m = matrix.matrix4x4()
    m = m:translate(pos)
    m = m:rotate(rot)
    m = m:scale(scale)

    local inv = m:invert()

    if joint.parent > 0 then
        base[i] = base[joint.parent] * m
        inverse_base[i] = inv * inverse_base[joint.parent]
    else
        base[i] = m
        inverse_base[i] = inv
    end
end

Calculating Animation Frame Matrices

local buffer = {}
local base = self.active_animation.base
local inverse_base = self.active_animation.inverse_base
for k, pq in ipairs(self.active_animation.frame[self.active_animation.current_frame].pq) do
    local joint = self.data.joint[k]
    local pose = pq

    local pos = { pose[1], pose[2], pose[3] }
    local rot = matrix.quaternion(pose[4], pose[5], pose[6], pose[7])
    local scale = { pose[8], pose[9], pose[10] }

    local m = matrix.matrix4x4()
    m = m:translate(pos)
    m = m:rotate(rot)
    m = m:scale(scale)

    local f = matrix.matrix4x4()

    if joint.parent > 0 then
        f = base[joint.parent] * m * inverse_base[k]
    else
        f = m * inverse_base[k]
    end

    table.insert(buffer, f:to_vec4s())
end

The full code is here for further examination. The relevant code is in /libs/iqe.lua and is near the bottom in the functions IQE:buffer() and IQE:send_frame(). This code runs on a custom version of the LOVE game framework, and a Windows binary (and batch file) is included.

Final note: Our matrix code has been verified against other implementations and several tests.

Karai17
  • 923
  • 2
  • 9
  • 26

1 Answers1

2

Transformations of parent bones should effect transformations of their children. Indeed, this is achieved, by specifying transformation of particular bone in a frame of it's parent. So, usually transformation of bones are specified in their local coordinate system, that depends on it's parent. If any of the parents transformed, this transformation would effect all children, even if their local transformations didn't changed.

In your case, you once cache all absolute (relative to the root, to be precise) transformation of each node. Then you update local transforms of each node using the cache, and do not update your cache. So, how the change of local transform of a node would effect it's child, if when you update the child you use cache instead of the actual parent transform?

There is one more issue. Why you do the following thing?

f = base[joint.parent] * m * inverse_base[k]

I mean, usually it would be just:

f = base[joint.parent] * m

I guess, that transformations recorded in animation is absolute (relative to the root, to be precise). It is very strange. Usually every transformation is local. Check this issue, because this will add you lots of problems.

More over, I don't see any need to cache something in your case (except of inverse_base, that is usually not needed).

Change your IQE:send_frame() function as follows:

local buffer = {}
local transforms = {}
local inverse_base = self.active_animation.inverse_base
for k, pq in ipairs(self.active_animation.frame[self.active_animation.current_frame].pq) do
    local joint = self.data.joint[k]
    local pose = pq

    local pos = { pose[1], pose[2], pose[3] }
    local rot = matrix.quaternion(pose[4], pose[5], pose[6], pose[7])
    local scale = { pose[8], pose[9], pose[10] }

    local m = matrix.matrix4x4()
    m = m:translate(pos)
    m = m:rotate(rot)
    m = m:scale(scale)

    local f = matrix.matrix4x4()

    if joint.parent > 0 then
        transforms[k] = transforms[joint.parent] * m
        f = transforms[k] * inverse_base[k]
    else
        f = m  * inverse_base[k]
        transforms[k] = m
    end

    table.insert(buffer, f:to_vec4s())
end

This works good for me. Try to get rid of the inverse_base and you would be able to remove all animation related code from your IQE:buffer() function

P.S. Usually, all nodes are updated by traversing down the tree. However, you update nodes by going though the list. You should be aware, that you must guarantee somehow that for any node it's children would go after it.

Pidhorskyi
  • 1,562
  • 12
  • 19
  • First off I want to say thank you so much for your help! When I test your code, it ALMOST works! Much better than my code did, anyway! I get this weird bug with one of my animations, "blah", and I was wondering if this is because of the way I parse the list which may cause some bones to not be updated properly before being used by their children? https://dl.dropboxusercontent.com/u/12958391/misc/mk2.png – Karai17 Oct 11 '14 at 18:00
  • I've looked at the "blah" animation, it seems that the transformation of topmost parent of the leg is wrong. I'm not sure, but perhaps this issue related to this strange *inverse_base*. Try to rewrite the code without multiplying by *inverse_base[k]* and tweak the setting of the export tool you are using. – Pidhorskyi Oct 11 '14 at 19:01
  • You've said that some bones might not be updated before being used by their children. If so, it would definitely break the animation. See the post scriptum I've wrote earlier. – Pidhorskyi Oct 11 '14 at 19:06
  • After some more research, I've found that the IQM spec writes bones linearly, a child will never be calculated before a parent if calculating in list order. I'll try to rewrite some code to remove inverse_base, but I will note that the official IQM reference demo has code written the same was I wrote it. – Karai17 Oct 11 '14 at 20:51
  • Here are the relevant IQM demo sections: https://github.com/lsalzman/iqm/blob/master/demo/gpu-demo.cpp#L167 https://github.com/lsalzman/iqm/blob/master/demo/gpu-demo.cpp#L262 – Karai17 Oct 11 '14 at 21:24