Hi Latch!
What you said was very interesting, but I'm afraid I hadn't explained myself properly. According to what I've read, quaternions represent points on the surface of a 4-dimensional sphere, centered about an origin. Since any 3 points may be linked by a triangle (origin, quaternion a and quanternion b) there will be an angle theta between the two quaternions. If theta<180, the slerp function finds the shortest route between the quaternions. But if this angle theta = 180 then there is no shortest route and so the algorithm chooses a route - unfortunately for me (or so I thought) the wrong one.
It turns out that I was barking up the wrong tree - the function I wrote used a variable called CosTheta but if this is <0 then I have to invert one of the quaternions (invert the signs of all the vector's components) and CosTheta. From what I've read, it's because (w,x,y,z) and (-w,-x,-y,-z) are the same rotation. Since cosTheta = qa.w * qb.w + qa.x * qb.x + qa.y * qb.y + qa.z * qb.z inverting either of the quaternions (qa or qb) will invert cosTheta, but then aCos is called. This returns 0 to 90deg for +ve inputs and 90 to 180deg for -ve inputs, so to return the shortest angle (as DirectX does) we need CosTheta to be positive, which can be achieved by inverting one of the quaternions. So the only error was a minus sign!
In case you're interested, here's the code that will load a DirectX animation into DB, converting the Quaternions to Euler angles, generating all the inbetween frames, and also handling the limb offsets. Thanks for all your help!
Dim ReturnAngle#(2)
Save Object Animation Left$(StartupDirectory$, 1) + ":\dbTemp\LL_Loadanim.tmp", 1
Open To Read 1, Left$(StartupDirectory$, 1) + ":\dbTemp\LL_Loadanim.tmp"
LimbCounter = 0
Repeat
rem Find the first limb's "Animation" header
Repeat
Read String 1, Temp$
Until Left$(Temp$, 11) = " Animation "
rem Get the limb name
Read String 1, Temp$
Read String 1, Temp$
Temp$ = Right$(Temp$, Len(Temp$)-4)
Temp$ = Left$(Temp$, Len(Temp$)-2)
rem Find the limb number
For T = 1 to NumberOfLimbs
If Limb$(T) = Temp$ then L = T : T = NumberOfLimbs+1
Next T
rem For each AnimationKey type
For A = 1 to 3
Repeat
Read String 1, Temp$
Until Temp$ = " AnimationKey {"
rem Find the animation type
Read String 1, Temp$
Temp$ = Right$(Temp$, Len(Temp$)-3)
Temp$ = Left$(Temp$, Len(Temp$)-1)
AnimType = Val(Temp$)
If AnimType = 0
rem No of keys for this limb
Read String 1, Temp$
Temp$ = Right$(Temp$, Len(Temp$)-3)
Temp$ = Left$(Temp$, Len(Temp$)-1)
NoKeys = Val(Temp$)
LastFrame# = 0
For N = 1 to NoKeys
rem Read quaternion line
Read String 1, Temp$
Repeat
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) <> " "
Key$ = ""
Repeat
Key$ = Key$ + Left$(Temp$, 1)
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) = ";"
Temp$ = Right$(Temp$, Len(Temp$) - 3)
W$ = ""
Repeat
W$ = W$ + Mid$(Temp$, 1)
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) = ","
Temp$ = Right$(Temp$, Len(Temp$) - 1)
X$ = ""
Repeat
X$ = X$ + Mid$(Temp$, 1)
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) = ","
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Y$ = ""
Repeat
Y$ = Y$ + Mid$(Temp$, 1)
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) = ","
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Z$ = ""
Repeat
Z$ = Z$+Mid$(Temp$, 1)
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) = ";"
Temp$ = Right$(Temp$, Len(Temp$) - 1)
oldqw# = qw#
oldqx# = qx#
oldqy# = qy#
oldqz# = qz#
qw# = Str_To_Float(W$)
qx# = Str_To_Float(X$)
qy# = Str_To_Float(Y$)
qz# = Str_To_Float(Z$)
rem Convert and store the quaternion
QuaternionToEuler(qw#, qx#, qy#, qz#)
ObjectLimbData#(Val(Key$)+1, L, 4) = ReturnAngle#(0)
ObjectLimbData#(Val(Key$)+1, L, 5) = ReturnAngle#(1)
ObjectLimbData#(Val(Key$)+1, L, 6) = ReturnAngle#(2)
rem Fill in the gaps - genereate inbetween frames
If N>1
Frame# = Val(Key$)
FrameCounter = 1
rem If there are no frames between keys, slerping is meaningless
If LastFrame#+1<Frame#
For T = LastFrame#+1 to Frame#-1
QuaternionSlerping(oldqw#, oldqx#, oldqy#, oldqz#, qw#, qx#, qy#, qz#, LastFrame#, Frame#, FrameCounter)
ObjectLimbData#(T+1, L, 4) = ReturnAngle#(0)
ObjectLimbData#(T+1, L, 5) = ReturnAngle#(1)
ObjectLimbData#(T+1, L, 6) = ReturnAngle#(2)
Inc FrameCounter
Next T
endif
LastFrame# = Frame#
endif
Next N
rem If animation continues beyond last rotational keyframe, clone the rotational keyframe values
For N = (Int(Frame#) + 1) to MaxAssignedKeyframes
ObjectLimbData#(N, L, 4) = ObjectLimbData#(Int(Frame#), L, 4)
ObjectLimbData#(N, L, 5) = ObjectLimbData#(Int(Frame#), L, 5)
ObjectLimbData#(N, L, 6) = ObjectLimbData#(Int(Frame#), L, 6)
Next N
endif
rem Positional keyframes
If AnimType = 2
rem No of keys for this limb
Read String 1, Temp$
Temp$ = Right$(Temp$, Len(Temp$)-3)
Temp$ = Left$(Temp$, Len(Temp$)-1)
NoKeys = Val(Temp$)
LastFrame# = 0
For N = 1 to NoKeys
rem Read Data line
Read String 1, Temp$
Repeat
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) <> " "
Key$ = ""
Repeat
Key$ = Key$ + Left$(Temp$, 1)
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) = ";"
Temp$ = Right$(Temp$, Len(Temp$) - 3)
X$ = ""
Repeat
X$ = X$ + Mid$(Temp$, 1)
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) = ","
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Y$ = ""
Repeat
Y$ = Y$ + Mid$(Temp$, 1)
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) = ","
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Z$ = ""
Repeat
Z$ = Z$+Mid$(Temp$, 1)
Temp$ = Right$(Temp$, Len(Temp$) - 1)
Until Left$(Temp$, 1) = ";"
Temp$ = Right$(Temp$, Len(Temp$) - 1)
x# = Str_To_Float(x$)
y# = Str_To_Float(y$)
z# = Str_To_Float(z$)
ObjectLimbData#(Val(Key$)+1, L, 1) = x#
ObjectLimbData#(Val(Key$)+1, L, 2) = y#
ObjectLimbData#(Val(Key$)+1, L, 3) = z#
rem Fill in the gaps - genereate inbetween frames
If N>1
Frame# = Val(Key$)
FrameCounter = 1
rem If there are frames between keys
If LastFrame#+1<Frame#
xMove# = (ObjectLimbData#(Frame#+1, L, 1) - ObjectLimbData#(LastFrame#+1, L, 1))/(Frame# - LastFrame#)
yMove# = (ObjectLimbData#(Frame#+1, L, 2) - ObjectLimbData#(LastFrame#+1, L, 2))/(Frame# - LastFrame#)
zMove# = (ObjectLimbData#(Frame#+1, L, 3) - ObjectLimbData#(LastFrame#+1, L, 3))/(Frame# - LastFrame#)
For T = LastFrame#+1 to Frame#-1
ObjectLimbData#(T+1, L, 1) = ObjectLimbData#(LastFrame#+1, L, 1) + (xMove#*FrameCounter)
ObjectLimbData#(T+1, L, 2) = ObjectLimbData#(LastFrame#+1, L, 2) + (yMove#*FrameCounter)
ObjectLimbData#(T+1, L, 3) = ObjectLimbData#(LastFrame#+1, L, 3) + (zMove#*FrameCounter)
Inc FrameCounter
Next T
endif
LastFrame# = Frame#
endif
Next N
rem If animation continues beyond last positional keyframe, clone the positional keyframe values
For N = (Val(Key$) + 2) to MaxAssignedKeyframes+1
ObjectLimbData#(N, L, 1) = ObjectLimbData#(Val(Key$)+1, L, 1)
ObjectLimbData#(N, L, 2) = ObjectLimbData#(Val(Key$)+1, L, 2)
ObjectLimbData#(N, L, 3) = ObjectLimbData#(Val(Key$)+1, L, 3)
Next N
endif
Next A
Inc LimbCounter
Until LimbCounter = NumberOfLimbs
Close File 1
Delete File Left$(StartupDirectory$, 1) + ":\dbTemp\LL_Loadanim.tmp"
rem Final tidy up - give the object its keyframes so it can animate
For K = 0 to (MaxAssignedKeyframes-1)
ObjectLimbData#(K+1) = K
For L = 1 to NumberOfLimbs
Rotate Limb 1, L, ObjectLimbData#(K+1, L, 4), ObjectLimbData#(K+1, L, 5), ObjectLimbData#(K+1, L, 6)
Offset Limb 1, L, ObjectLimbData#(K+1, L, 1), ObjectLimbData#(K+1, L, 2), ObjectLimbData#(K+1, L, 3)
Next L
Set Object Keyframe 1, K
Next K
Function QuaternionSlerping(qaw#, qax#, qay#, qaz#, qbw#, qbx#, qby#, qbz#, StartFrame#, FinalFrame#, Frame)
rem Calculate how far quaternion is to be moved
t# = Frame*(1/(FinalFrame# - StartFrame#))
rem Calculate Cos(angle between quaternions)
CosTheta# = (qaw# * qbw#) + (qax# * qbx#) + (qay# * qby#) + (qaz# * qbz#)
If CosTheta#<0
qbw# = 0-qbw#
qbx# = 0-qbx#
qby# = 0-qby#
qbz# = 0-qbz#
CosTheta# = 0-cosTheta#
endif
Theta# = acos(CosTheta#)
rem If Theta = 0 then return the original quaternion (qa)
If (abs(theta#) < 0.001)
QuaternionToEuler(qaw#, qax#, qay#, qaz#)
ExitFunction
endif
rem Calculate the temporary values
sinTheta# = sqrt(1.0 - (CosTheta#*CosTheta#))
rem Theta*2 = 180 degrees - result is undefined
If (abs(sinTheta#) < 0.001)
rem Convert both quaternions to euler angles
QuaternionToEuler(qaw#, qax#, qay#, qaz#)
ax# = ReturnAngle#(0) : ay# = ReturnAngle#(1) : az# = ReturnAngle#(2)
QuaternionToEuler(qbw#, qbx#, qby#, qbz#)
bx# = ReturnAngle#(0) : by# = ReturnAngle#(1) : bz# = ReturnAngle#(2)
xMove# = EulerLerping(ax#, bx#, StartFrame#, FinalFrame#)
yMove# = EulerLerping(ay#, by#, StartFrame#, FinalFrame#)
zMove# = EulerLerping(az#, bz#, StartFrame#, FinalFrame#)
ReturnAngle#(0) = WrapValue(ax# + (Frame*xMove#))
ReturnAngle#(1) = WrapValue(ay# + (Frame*yMove#))
ReturnAngle#(2) = WrapValue(az# + (Frame*zMove#))
ExitFunction
endif
ratioA# = (sin((1 - t#) * theta#)) / sinTheta#
ratioB# = (sin(t# * theta#)) / sinTheta#
rem Calculate Quaternion
qw# = (qaw# * ratioA# + qbw# * ratioB#)
qx# = (qax# * ratioA# + qbx# * ratioB#)
qy# = (qay# * ratioA# + qby# * ratioB#)
qz# = (qaz# * ratioA# + qbz# * ratioB#)
QuaternionToEuler(qw#, qx#, qy#, qz#)
EndFunction
Function EulerLerping(StartAngle#, FinalAngle#, StartFrame#, FinalFrame#)
Direction = 0
rem Handle angles of exactly 180 in same manner as DirectX
If ((FinalAngle# - StartAngle# > 179.950) and (FinalAngle# - StartAngle# < 180.050)) or ((FinalAngle# - StartAngle# > -179.950) and (FinalAngle# - StartAngle# < -180.050))
If FinalAngle#>StartAngle# then Direction = -1 else Direction = 1
endif
rem If limb moves more than 180, make final angle -ve so
rem limb moves shorter route in opposite direction
If (StartAngle# + 180.000)<FinalAngle#
FinalAngle# = FinalAngle#-360
else
If (StartAngle# - 180.000)>FinalAngle#
StartAngle# = StartAngle#-360
endif
endif
MoveAmount# = (FinalAngle# - StartAngle#)/(FinalFrame# - StartFrame#)
If (Direction = -1 and MoveAmount#>0) or (Direction = 1 and MoveAmount#<0)
MoveAmount# = 0-MoveAmount#
endif
EndFunction MoveAmount#
Function QuaternionToEuler(qw#, qx#, qy#, qz#)
rem Set up variables
r00# = 0 : r01# = 0 : r02# = 0
r10# = 0 : r11# = 0 : r12# = 0
r20# = 0 : r21# = 0 : r22# = 0
thetax# = 0 : thetay# = 0 : thetaz# = 0
r00# = 1 - (2*qy#*qy#) - (2*qz#*qz#)
r01# = (2*qx#*qy#) + (2*qw#*qz#)
r02# = (2*qx#*qz#) - (2*qw#*qy#)
r10# = (2*qx#*qy#) - (2*qw#*qz#)
r11# = 1 - (2*qx#*qx#) - (2*qz#*qz#)
r12# = (2*qy#*qz#) + (2*qw#*qx#)
r20# = (2*qx#*qz#) + (2*qw#*qy#)
r21# = (2*qy#*qz#) - (2*qw#*qx#)
r22# = 1 - (2*qx#*qx#) - (2*qy#*qy#)
if (r02# < 1)
if (r02# > -1)
thetay# = asin(r02#)
thetax# = atanfull((0-r12#), r22#)
thetaz# = atanfull((0-r01#), r00#)
else
`r02# = -1
`Not a unique solution: thetaz# - thetax# = atan2(r10#,r11#)
thetay# = 270
thetax# = 0-atanfull(r10#,r11#)
thetaz# = 0
endif
else
`r02# = +1
`// Not a unique solution: thetaz# + thetax# = atan2(r10#,r11#)
thetay# = 90
thetax# = atanfull(r10#,r11#)
thetaz# = 0
endif
ReturnAngle#(0) = WrapValue(thetax#)
ReturnAngle#(1) = WrapValue(thetay#)
ReturnAngle#(2) = WrapValue(thetaz#)
EndFunction
"I wish I was a spaceman, the fastest guy alive. I'd fly you round the universe, in Fireball XL5..."