tl;dr: When animating a model, each joint moves correctly, but not relative to its parent joint.
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.
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.