Jump to content

Blender To HKX Development Thread


Recommended Posts

I'll have a look, thanks.

 

I think the problem that I'm having is not so much with the math, as with Blender having several different sets of co-ordinate systems. Pose bones have separate geometry from edit bones, and Euler rotations have no effect if the bone is in Quaternion mode, and even if all that is taken into account, sometimes you tall a bone to move and nothing happens.

 

It makes it hard to tell if you have the maths wrong, or if you're just altering the wrong co-ordinate set.

 

That said, I'll take any help I can get at the moment :)

Link to comment

Things are not ideal in RL at the moment,  and I'm not getting nearly as much time to focus on this as I would like. That said, I did some test scripting.  In particular I had the idea to separate the math from Blender's API. So I did this:

 


print("\n*** Hypothetically ...\n\n")
root_q = Quaternion([0,0,0,1])
com_q = Quaternion([0.707106, 0.0, 0.707106, 0.0])
pq("Root Q", root_q)
pq("COM Q", com_q)
pq("COM Splat Root", com_q @ root_q)

diff_q = com_q.rotation_difference(root_q)
pq("Com diff Root", diff_q)
pq("Com diff Root Splat Root", diff_q @ root_q)

 

The "Splat" (it's the wrong word - technically a "splat" is "*" but anyway) the "@" operator is matrix mutiplication


 

*** Hypothetically ...


Root Q: <Quaternion (w=1.0000, x=0.0000, y=0.0000, z=0.0000)>
COM Q: <Quaternion (w=1.0000, x=0.0000, y=0.0000, z=0.0000)>
COM Splat Root: <Quaternion (w=1.0000, x=0.0000, y=0.0000, z=0.0000)>
Com diff Root: <Quaternion (w=1.0000, x=0.0000, y=0.0000, z=0.0000)>
Com diff Root Splat Root: <Quaternion (w=1.0000, x=0.0000, y=0.0000, z=0.0000)>

 

So rotation diff and splatting seems to let me add and subtract rotations. Either that or they're doing something utterly different that just happens to gvie the same results in this one special case... but since one of the operators is "rotation_difference" I'm inclined to optimistic here.

 

So If I can get the skeleton into the right initial position, then I can probably get rotatons out of it consistently and manipulate them as needed.

 

Where it all stands to come to grief, of course, is when I come to rotate 3d co-ordinate tuples to reflect the fact that the bones move in 3d space as well as changing their facing. But one problem at a time.

 

 

 

Link to comment

This is promising.

 

image.png

 

It might not be obvious, but she's floating about two foot off the ground, which was the intention. She's the right way up, the right scale and she hasn't displaced off into the distance. The only issue is that she rotated 90 degrees clockwise when the idle activated, which I'm pretty sure everyone can work around.

 

Interesting question is going to be whether I can negate that rotation without ending up with her floating parallel to the ground, or some such rubbush. I'm worried there's a deeper problem here and that I'm only treating the symptoms. Then again, successfully treating the symptoms seems like a victory in itself at the moment.

 

[edit]

 

... but no, rotate the psoe so she should face the camera, and she vanishes off into the floor.

 

OK. This is a geometry problem, nothing more. The co-ordinates of everything need a rotation to convert between blender and nif-space. I've solved harder problems than that already.

 

I'm starting to think that this is why Anton added a Controller_Base bone as parent to Root. He could use that bone's rotation to move everything else on export. So that's possibly the way to go, although I still don't understand why he didn't just apply a corrective rotation if he knew what it was.

 

I should take a closer look at Anton's dummy.blend file. The controller_base appears to be aligned with root, but I wonder what the roll on it is.

 

 

 

Link to comment

I was trying to work through different rotations, systematically snapshotting them and I though to myself "Why am I doing this? This is animation! I can do them all in one idle!" So that's what I did.

 

 

As you can see, it pretty much confirms my hypothesis I'm rotating around the wrong angle.

 

But more than that, it's also the first proof I've had that actual animations are going to work. Still loads of scope for things to go wrong, but I still feel happy about it.

 

(The weird thing with the legs isn't animated in - it's the engine's IK trying to keep her feet on the gound for as long as possible)

Link to comment

tenor.gif?itemid=18659433

 

Jokes aside, I don't have anything useful to add here but I have to say it's great to see progress on this, being able to animate for FO4 with Blender instead of needing Max will probably make animating easier for anyone that may want to dip their toes in it, specially so if they are more familiar with Blender. :classic_shy:

Link to comment
3 hours ago, Blaze69 said:

tenor.gif?itemid=18659433

 

:D

 

Can anyone see what's intended by "Compensation" here? This is Anton's logic, reformatted a little and with the comments translated

 

#
#               Get the rotation matrix of the parent and invert it
#
                PPP = Bone.parent.matrix.copy()
                PPP.invert ()
#
#               remove the parent contributions to vector and rotation
#
                BoneCoordinates -= Bone.parent.head.copy()
                BoneCoordinates.rotate(PPP)
#
#               if we're not the root bone (the one with controller_base as a parent)
#               then we're done
#
                if (Bone.parent.name != "Controller_BASE"):
                        return BoneCoordinates[:]
#
#               The idea seems to be that you should use the controller bone if you want to move the animation as a whole.
#               So, while normally a bone would have its parent displacement removed,
#               if that parent is Controller_BASE, then it doesn't.
#
#               so we start by taking the bone co-ords again, skipping the backward translation on account of the parent
#               we still undo any rotation inherited from Controller_BASE however
#
                BoneCoordinates = Bone.head.copy()
                BoneCoordinates.rotate(PPP)
#
#               Slope compensation skeleton NIF-e 90" because preferencekey to "NPC Root [Root]" IK bones "Controller_BASE"
#
                Compensation = SelectedObject.pose.bones['Controller_BASE'].matrix.to_3x3().copy()
                BoneCoordinates.rotate(Compensation)

 

As far as I can tell, we take the rotation of the bone, then remove any rotation due to the parent bone. And then, if the parent bone is called "Controller_BASE", we work out the rotation of that bone and rotate by it again, effectively undoing the previous correction. Which makes sense, but it seems like an awful long winded way to go about it. The comment suggests it's something to do with the NIF format's 90 degree rotation, and that seems relevant to my current axis issues, but I can't see what net effect it's having here.

 

Anyone see any reason why I shouldn't just leave it out entirely?

 

(For any Russian speakers here, this is Anton's original. Yandex is good, but it managed to get the meaning of at least one of his comments back to front, so I may well be misunderstanding this one).

 

       # Перебираем все КАДРЫ анимации в заданном Диапазоне с заданным Интервалом
        for AnimKey in range(FirstFrameNumber, LastFrameNumber + 1, FrameStep):
            # Выберем кадр, для которого будем читать ключи анимации
            Scene.frame_set(AnimKey)
            # Записываем ключ Времени (в секундах) которое соответствует выбранному кадру анимации
            AnimationTime = (AnimKey - FirstFrameNumber) / FrameRate
            MyFileWrite("%10.6f    " % AnimationTime)
            
            # ------------------------------------------------------
            # Сохраняем ПЕРЕМЕЩЕНИЕ косточек в пространстве
            # ------------------------------------------------------
            # Сохраняем марицу поворота родителя, для последующей компенсации его влияния на дочернюю косточку
            if Bone.parent: 
                PPP = Bone.parent.matrix.copy()
                PPP.invert()
            # Берём текущие координаты косточки и компенсируем их изменение косточкой-родителем
            BoneCoordinates = Bone.head.copy()
            if Bone.parent:
                # Получаем Вектор для данной косточки относительно родительской косточки
                BoneCoordinates -= Bone.parent.head.copy()
                # Поворачиваем наш Вектор (по МОДИФИЦИРОВАННОЙ матрице прочитанной из НИФ файла)
                BoneCoordinates.rotate(PPP)
            
            # Получаем смещение косточки в пространстве (это нужно ТОЛЬКО для САМОЙ ПЕРВОЙ косточки скелета, ибо только она имеет право СВОБОДНО двигаться) в нашем случае - это (Bone.name == "NPC Root [Root]") имеющий родителем IK косточку "Controller_BASE"
            if Bone.parent: 
                if (Bone.parent.name == "Controller_BASE"):
                    BoneCoordinates = Bone.head.copy()
                    BoneCoordinates.rotate(PPP)
            # Компенсация наклона скелета в НИФ-е на 90" из за приПеренченной к "NPC Root [Root]" IK косточки "Controller_BASE"
            if Bone.parent: 
                if (Bone.parent.name == "Controller_BASE"):
                    #Compensation = SelectedObject.data.bones['Controller_BASE'].matrix.to_3x3().copy()
                    Compensation = SelectedObject.pose.bones['Controller_BASE'].matrix.to_3x3().copy()
                    BoneCoordinates.rotate(Compensation)
            # Записываем ключ Перемещения косточки в файл
            MyFileWrite("%10.6f %10.6f %10.6f    " % BoneCoordinates[:])

 

 

Link to comment

Tip for anyone else struggling with such things:

 

bone.rotation_quaternion == bone's rotation with respect to its parent

bone.matrix.to_quaternion() == bone's rotation with respect to the armature as a whole.

 

Of course the armature has its own co-ordinate space! Because Blender doesn't have nearly enough confusing co-ordinate systems already.

 

So that explains why I could have two bones at right angles apparently returning the same quat. They did have the same quat - they hadn't been rotated from their original position.

 

I will tame this beast yet!

Link to comment

OK. So where am I?

 

I've been experimenting with a Controller_BASE. Turns out that yes, adding that bone and changing its angle changes the axes used by the final animation, much as changing the direction of the root bone does. Trouble is, I'm back to guessing at what orientation I need for the bones, and as before I'm getting nowhere fast.

 

More to the point, there are 24 possible 90 degree quaternions. There's your basic X+, X-, Y+, Y-, Z+, Z-, and then each of those has four variations according how the bone is rotated around the axis it points down. So that's 24 possible alingments for root, and the same again for Controller_BASE and we get 576 combinations. Add to that that setting up the bones is fiddly and error prone, and that axis length rotation difficult to determine, and that there's a non-trivial time to export, compile, copy and test each combo ... and I'm unlikely to do this blindly. Neither picking at random nor by brute force.

 

On the other hand, I don't need to generate all the possibles. I know what the rotation for Root and COM need to look like. So I can iterate over all the combinations and compare the results against the target HKX file. Any that don't match, I discard. That should hopefully give me a unique solution, but even if not, it will drastically winnow the possible candidates.

 

So that's the next thing to do :)

Link to comment

Here's the start of that: first quat has the bone pointing in the +Z direction and the three @ operator each rotate it either 90 or 180 degrees.

 

import math
import numpy
from pyquaternion import Quaternion

sin45 = math.sin(math.radians(45))
print("sin45 = %f" % sin45)
print("sin45 = %f" % -sin45)

com = Quaternion((0, 0, sin45, sin45))

qs = [
        Quaternion((1, 0, 0, 0)),                                               # default - pointing to +Z
        Quaternion((1, 0, 0, 0)) @ Quaternion((1, 0.0, 1, 0.0)),                # same rotated 90 round Y axis
        Quaternion((1, 0, 0, 0)) @ Quaternion((1, 0.0, -1, 0.0)),               # same rotated -90 round Y axis
        Quaternion((1, 0, 0, 0)) @ Quaternion((0.0, 0.0, 1.0, 0.0)),            # same rotated 180 round Y axis
]

 

That's just one axis, but getting it right has been a learning experience.

 

What I did was a took a cube in blender and labeled the faces:

 

image.png

 

Ground breaking stuff, right? ;) Anyway, I attached that to a bone, and now I can use it to check my quats
 

Spoiler

 

image.png

 

image.png

 

image.png

 

image.png

 

 

 

 

 

Anyway, couple of things arising from that.

 

Firstly, Quat numers are WXYZ. The Q part is how far to rotate and XYZ part is a vector defining an axis of rotation. So (1,0,0,1 means rotate 180 degrees around the Z axis). The thing you need to know is that Blender quats always use the Y axis as pointing out of the screen, regardless of the orientation of the bone. The trouble is, Blender bones are created by default aligned with Z axis, which means get some weird results, like to rotate around the Z axis on screen, you have to specify the Y axis in the quat.

 

Long story short, make sure the bone is pointing along Y+ in edit mode. Then go back to pose mode and it will work as it should.

 

Another fun thing:  those 90 degree rotations are usually found as 0.707, 0, 0.707, 0. Someone once said you can think if quaternion numbers as weights, with W weighting for the unrotated position.  So the thing here is that W and Y have to have the same value. It tends to reg reduced to 0.7071 because that's Sin(54) which is the length of the X and Y sides of a 45 degree triangle.

 

So: I need five more of those sets of quats, and I can crunch some angles. Shouldn't be too bad. I do believe I am finally getting my head around this quaternion business

 

[edit]

 

Also, something I never really noticed before: Blender's Num1 hotkey aligns the viewport with the Y axis, facing towards negative Y. So most of that works on the assumption that the Y axis is the wrong way around.

 

Every time I think I've got a handle on this, I learn something that invalidates everything up to this point. If anyone reads this who understands this stuff, they must be having a right old laugh.

Link to comment

I have no idea why she's sunk halfway into the floor.

 

 

... but on the other hand, she's the right way up and rotating around the correct axis. So progress.

 

I just wish I didn't feel like I was flailing around at random quite so much.

 

Never mind. Let's throw a few more bones into the mix and see what happens.

 

Link to comment
1 hour ago, DocClox said:

I have no idea why she's sunk halfway into the floor.

 

I've encountered serveral odd situations when fiddling around with animations which included Z axis translations.

 

Wouldn't it be easier to make an animation that rotates the COM bone X +90 > X-90 > Y+90 > Y-90 > Z+90> Z-90 ?

This way you should be able to tell which axis is off ?

 

 

Link to comment
5 hours ago, Vader666 said:

Wouldn't it be easier to make an animation that rotates the COM bone X +90 > X-90 > Y+90 > Y-90 > Z+90> Z-90 ?

 

That's ... more or less what I'm doing. I have a list of quats

 

cardinals = [
	Quaternion((1, 0, 0, 0)),						# +Y - default
	Quaternion((0, 0, 0, 1)),						# -Y
	Quaternion((1, 0, 0, -1)),						# +X
	Quaternion((1, 0, 0, 1)),						# -X
	Quaternion((1, 1, 0, 0)),						# +Z
	Quaternion((1, -1, 0, 0)),						# +Z
]

for i, q in enumerate(cardinals):
	file = "pose{}.xml".format(11 + i)
	write_file(file, q)

 

And I'm generating a separate xml file for each quat, which I then use to rotate COM. Now I just need to play the idles and see if any of them work.

 

[edit]

 

Well, that was disappointing.  most of them didn't do anything, and the two that did just moved the location of the actor.

 

... so then I thought if Pitch and Yaw didn't do the job, Roll probably will.

 

 

That's just a two bone animation, so the legs look a little weird, but everything is the right way up and moving in the right direction. I'm going to add a few more bones to the mix and see what breaks.

 

Oh, and if anyone's interested (for instance me in three weeks time when I can't remember how I solved it), the transformation to fix it was

 

Quaternion((1, 0.0, -1, 0.0))

 

[edit]

 

I re-enabled all the bones.

 

 

 

Needs work.

 

[edit]

 

Just Root, COM and Pelvis for this one.

 

image.png

 

So there we have it. I've got this whole thing ass-backwards. Well, sideways, anyway.

 

I restricted the 90 degree correction to Root and COM, just to see if it was causing the big ball of chaos in the earlier clip. Let's put it back and see if it helps, now that I can see what's going on.

 

[edit]

 

Turns out, I was wrong. This is what "ass backwards" looks like:

 

image.png

 

On the other hand, I found a major bug. I split the main transform loop into functions, and the one that calculated the rotation was using the raw rotation instead of compensating for the rotation of the parent.

 

So now it's only wrong around one axis....

 

Link to comment

It's been a rough old couple of days. From thinking I was doing pretty well, I made one small change, undid what progress I'd made, and then couldn't get back to where I was. It's taken me two and a half days to get back to being able to make her stand upright. Still, at least I learned a few things along the way. "Sadder but wiser", I feel.

 

In the end, I stopped trying to position the model just so and just calculated the corrective rotation I needed. That worked for root, so I did the same thing for COM and made an interesting discovery: matrix multiplication is not commutative, and X @ Y does not yield the same result as Y @ X. Which is probably the source of a lot of the weirdness I've been experiencing.

 

Current state of play: it's easier to just show you

 

 

Good news is the bouncing up and down is along the correct axis. Bad news is that rotating around that axis spins her around the Y axis.

 

And her Pelvis is 90 degrees to COM. Which is a pain, because at this rate I'll need a special case rotation for each bone, and that's not going to be feasible.

 

So the plan is to go back over my existing transformations and check the order or operands in my matrix multiplication. I'm hoping that the "compensate for the parent bone's rotation" bit is back to front. If so, I might be able to find a single correction needed by all the bones, and maybe an additional adjustment for root. If not, I'm not at all sure what's left, and I'll likely end up posting my code and seeing if someone else can see where I'm going wrong.

 

Anyway. Later, all.

Link to comment

OK. A little bit of hope. I think it may have been a geometry problem. The skeleton I was using had been monkeyed around with in a great many ways, in the course of trying to work out how to get the numbers I need, so I imported a clean one. Then I wrote a script to list all the bones and their quats. No adjustments, just the directions.

 

Now the thing is: for each bone, you need to subtract from its rotation, the rotation of the parent bone, with 1,0,0,0 being "no rotation" and also straight up. So I thought perhaps the thing to do would be to list each bone and its rotation adjusted for its parent.  Then I added in a controller bone as a parent to root, and pointed root and the controller along the z-axis as per Anton's original.

 

This is what I get:

 

   3 - Root                 : Controller_BASE     : <Quaternion (w=1.0000, x=0.0000, y=0.0000, z=-0.0000)>
   9 - COM                  : Root                : <Quaternion (w=1.0000, x=-0.0000, y=0.0000, z=-0.0000)>
  10 - SPINE1               : COM                 : <Quaternion (w=0.9981, x=-0.0614, y=-0.0000, z=0.0000)>
 106 - Pelvis               : COM                 : <Quaternion (w=1.0000, x=0.0000, y=0.0000, z=-0.0000)>

 

Root and COM are pointing the correct direction, as is Pelvis. SPINE1 is a shade off vertical, but so is the bone, reflecting the curvature of the spine.

 

Now I haven't tried packing this into a HKX file yet, and the numbers here are not those in the sample HKX file I generated. But for the first time, I'm seeing something that might be corrected by a global transformation.

 

Which means this mad project may be achievable after all.

Link to comment

I am beginning to feel like the doomed hero in some HP Lovecraft tale: I have studied  the numbers until they tell me how the Real can twist itself into the Imaginary and vice versa. I have caught the merest glimpse of the mysteries of Complex Space, and now my mind teeters on the Brink of the Abyss. I fear the Hounds of Tindalos cannot be far away.

 

What the numbers have not done, is tell me how to export Blender to fucking Havok. That said...

 

The problem with trying to read hkx xml, even uncompressed formats, is that the frame data tends to look like this.

 

Spoiler


(0.0 0.0 0.0 0.0)(0.0 0.0 0.0 0.9999999403953552)(0.9999998211860657 0.9999998211860657 0.9999998211860657 0.0)
(1.7612956071388908E-5 7.786029527778737E-6 68.9112777709961 5.9604644775390625E-8)(2.247644559361106E-8 -0.7071065306663513 2.247644559361106E-8 0.7071068286895752)(0.9999999403953552 1.0000001192092896 0.9999999403953552 0.0)
(3.814697265625E-5 6.366462912410498E-12 9.094947017729282E-12 1.1191160410817247E-5)(1.5987211554602254E-14 0.0 1.7763568394002505E-15 0.9999999403953552)(1.0 1.0 1.0 0.0)
(7.62939453125E-6 4.3082228512503207E-4 6.615063667297363 4.418032185640186E-4)(-0.00830288976430893 0.9833554029464722 0.16899023950099945 -0.0662207305431366)(0.9999998807907104 1.000000238418579 0.9999998807907104 0.0)
(31.59518814086914 3.4809112548828125E-5 -4.76837158203125E-5 -17.325931549072266)(0.08159720152616501 -0.015378564596176147 -0.061147190630435944 0.9946689605712891)(1.0000001192092896 1.000000238418579 1.0 0.0)
(31.94289779663086 -0.002079486846923828 4.6062469482421875E-4 2.351137161254883)(-0.06106068938970566 -0.02225548028945923 0.5224660634994507 0.8501796126365662)(1.0000005960464478 1.0 1.0000004768371582 0.0)
(2.288818359375E-5 4.30822343332693E-4 -6.6150712966918945 4.422238271217793E-4)(-0.00470493920147419 0.9838697910308838 -0.1688995212316513 0.058743178844451904)(1.0 1.0000001192092896 1.0 0.0)
(31.595111846923828 2.765655517578125E-5 3.0517578125E-5 17.271665573120117)(-0.0814121812582016 0.002263963222503662 -0.06673096865415573 0.9944413900375366)(1.000000238418579 1.0 1.000000238418579 0.0)
(31.942575454711914 -7.038116455078125E-4 1.1444091796875E-5 13.323387145996094)(0.06357887387275696 0.02693772315979004 0.5248010158538818 0.8484195470809937)(0.9999998211860657 1.0 0.9999999403953552 0.0)
(3.7920150756835938 -0.002946233144029975 -7.177344741648994E-6 -0.002935162279754877)(-0.021384770050644875 -0.0014015436172485352 0.061363935470581055 0.9978852868080139)(1.0 1.0000001192092896 0.9999999403953552 0.0)
(8.704673767089844 -3.814697265625E-6 1.341104507446289E-5 -5.080862045288086)(0.020409289747476578 0.0021199285984039307 -0.08762488514184952 0.9959419965744019)(1.0 1.000000238418579 1.0000001192092896 0.0)
(9.956291198730469 0.0 -7.690861821174622E-6 6.309678077697754)(9.415671229362488E-4 -8.174777030944824E-5 0.009184627793729305 0.9999572038650513)(1.000000238418579 1.0000001192092896 1.0 0.0)
(22.083984375 -3.7671027183532715 -2.3888424038887024E-6 0.06528425216674805)(-3.864802420139313E-5 -1.220405101776123E-4 0.2089291661977768 0.9779307842254639)(1.0000001192092896 1.0000001192092896 1.000000238418579 0.0)
(8.224334716796875 7.62939453125E-6 -9.164214134216309E-6 -48.66511535644531)(3.978610038757324E-5 1.214444637298584E-4 -0.19211825728416443 0.9813716411590576)(1.0 1.0000001192092896 1.0 0.0)
(19.153244018554688 -0.5104186534881592 1.6950812339782715 3.4003729820251465)(-0.07328110188245773 0.8276925086975098 0.13196444511413574 -0.5405000448226929)(0.9999998211860657 0.9999999403953552 1.0 0.0)
(12.536575317382812 1.9073486328125E-6 1.52587890625E-5 -3.8940629959106445)(0.04264157637953758 -0.1568361073732376 0.13149522244930267 0.9779022336006165)(1.000000238418579 1.0000003576278687 0.9999998807907104 0.0)
(17.968284606933594 1.621246337890625E-5 -1.52587890625E-5 -2.188324213027954)(-0.03973409906029701 -0.024011433124542236 0.2680768072605133 0.9622781276702881)(1.0000004768371582 1.0000001192092896 1.000000238418579 0.0)
(6.151594161987305 -1.1444091796875E-5 -3.0517578125E-5 -9.95677375793457)(0.0032633841037750244 0.0 7.450580596923828E-9 0.9999945759773254)(1.0000003576278687 1.0000004768371582 1.000000238418579 0.0)
(6.151588439941406 -6.4849853515625E-5 0.0 -4.9590911865234375)(0.006055697798728943 2.9802322387695312E-8 -5.9604644775390625E-8 0.9999815225601196)(1.0 0.9999999403953552 1.0 0.0)

 

 

... where the first four numbers are the co-ords of the bone head, plus something I know not what it is, followed by another four with are the rotations as a Quaternion WXYZ, and then the scaling factors in XYZ and another number that seems to be always zero.

 

However, knowing that doesn't help much because the whole thing is an ungodly mess of digits. So I wrote a little bit of Python to reprint it with a limited number of decimal places and numbers very close to zero replaced by zeros.

 

Spoiler


0   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
1   (   0.000    0.000   68.911    0.000)(   0.000   -0.707    0.000    0.707)(   1.000    1.000    1.000    0.000)
2   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
0   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
1   (   0.000    0.000   69.503    0.000)(   0.000   -0.707    0.000    0.707)(   1.000    1.000    1.000    0.000)
2   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
0   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
1   (   0.000    0.000   71.247    0.000)(   0.000   -0.707    0.000    0.707)(   1.000    1.000    1.000    0.000)
2   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
0   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
1   (   0.000    0.000   74.095    0.000)(   0.000   -0.707    0.000    0.707)(   1.000    1.000    1.000    0.000)
2   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
0   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
1   (   0.000    0.000   77.999    0.000)(   0.000   -0.707    0.000    0.707)(   1.000    1.000    1.000    0.000)
2   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
0   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
1   (   0.000    0.000   82.911    0.000)(   0.000   -0.707    0.000    0.707)(   1.000    1.000    1.000    0.000)
2   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
0   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)
1   (   0.000    0.000   88.783    0.000)(   0.000   -0.707    0.000    0.707)(   1.000    1.000    1.000    0.000)
2   (   0.000    0.000    0.000    0.000)(   0.000    0.000    0.000    1.000)(   1.000    1.000    1.000    0.000)

 

 

That was an animation where I dragged the COM bone up by 500 units. Lo and behold, if you want to represent movement in the Z direction, you do it with increasing values of positive Z. The same holds true for the X and Y axis. In many respects the whole thing is remarkably uncomplicated. Which is mildly annoying when I consider all the overthink I've put into this project. Thing is: NIF animations would seem to use incremental changes to angles and vectors, so Anton needed a lot of adjustments from Blender which stored values relative the the armature. Havok doesn't need these corrections, and in blindly following Anton's lead I've managed to confuse the issue (and myself) no end.

 

A couple of other things: Bad Dog's importer imports skeletons 10x too big for most purposes, but for animation, the scale they come in at makes the maths work out right. That's one reason I've been getting weird scrunched up poses, as the game's IK tries to solve for a pelvis a quarter inch off the floor.

 

There's more, but accidentally overwrote a file I needed and I'm too tired to recreate it tonight. I just wanted to get some of this stuff down.

Link to comment

This has a certain strange beauty to it

 

 

That's supposed to look like this:

 

 

 

 

Notes:

 

I started this trying to get the geometry right, thinking that if I didn't, I'd end up having to apply a fiddle factor each bone in turn to get it to work out right. Yesterday, looking for inspiration, I looked at the exporter code in the ZeX rig and ... there's a great long list of matrices for individual bones. A fiddle factor for each bone! So whatever works, right?

 

So I wrote these little gadgets:

 

Spoiler

class Fiddle:
        def __init__(self, name=None, swap_xz=False, tuples=None):
                log("Creating Fiddle for " + name)
                self.name       = name
                if tuples:
                        self.hct_q      = Quaternion(tuples["havok"])
                        self.blend_q    = Quaternion(tuples["blender"])
                        self.factor     = inv(self.blend_q) @ self.hct_q
                self.swap_xz    = swap_xz

                log("   Havok: {}".format(self.hct_q))
                log("   Blender: {}".format(self.blend_q))
                log("   Factor: {}".format(self.factor))

class FiddleFactors:
        def __init__(self, ll):
                self.list = ll

        def lookup(self, name):
                log("FiddleFactors:Lookup("+ name + ")")
                for f in self.list:
                        if f.name == name:
                                log("   Returning: {}".format(
                                        f.factor
                                ))
                                return f
                return None

        def factor(self, name):
                ff = self.lookup(name)
                if not ff:
                        return None
                return ff.factor

        def swap_xz(self, name):
                ff = self.lookup(name)
                if not ff:
                        return False
                return ff.swap_xz

 

 

The idea being you can take an "at rest" pose, and if you have a bone that isn't rotated right you can enter what Blender thinks it should be, and what havok thinks it should be, and this will generate a quaternion to convert between the two, Plus a handy lookup func to find it in a transform loop.

 

I ended up with this:

 

Spoiler

factors = FiddleFactors([

        Fiddle(
                name = "Root",
                tuples = {
                        "havok"         : (0, 0, 0, 1),
                        "blender"       : (0.7071,    0.0000,    0.0000,   -0.7071)
                }
        ),

        Fiddle(
                name = "COM",
                tuples = {
                        "havok"         : (0.0,    -0.7071,    0.0000,    0.7071),
                        "blender"       : (0.5,0.5,0.5,0.5),
                }
        ),

        Fiddle(
                name = "Pelvis",
                tuples = {
                        "havok"         : (0.000, 0.000, 0.000, 1.000),
                        "blender"       : (1.0000, 0.0000, 0.0000, 0.0000)
                }
        ),

        Fiddle(
                name = "Leg_Thigh.L",
                tuples = {
                        "havok"         : ( -0.00830288976430893, 0.9833554029464722, 0.16899023950099945, -0.0662207305431366),
                        "blender"       : (0.01516, 0.99567, -0.06489, -0.06489)
                },
                swap_xz = True
        ),

        Fiddle(
                name = "Leg_Calf.L",
                tuples = {
                        "havok"         : (0.082,  -0.015,  -0.061,   0.995 ),
                        "blender"       : (0.997,  -0.058,   0.033,  -0.025),
                },
                swap_xz = True
        ),
        # and so on...

 

 

And that works like like a charm. So do that for 95 bones, and we're good right? Tedious but finite and at the end we can at least animate human bodies? Well, it's not that simple.

 

What I found was that some of the co-ordinate positions were wrong. As in, X and Z transposed, and one of them needing a -1 factor. That raises an old worry of mine: Chirality. You get left handed and right handed co-ordinate systems. Historically, mathematicians have always used one, and most 3D software followed those conventions. The Microsoft came along and deliberately made their 3D code work the other way just to fuck everyone else up. The thing is, you can't rotate between the two: you need a reflection. And since I don't know what handedness Havok expects, I don't know if I need one or not. This was on my mind when I was trying to find a single rotation that would map Blender's quats onto HCT's quats - it might just not be possible.

 

Anyway, these swapping of co-ords and the inverting of one axis sounds a like like switching handedness, and in fact, looking at the ZeX code, I find someone has already come to a similar conclusion, at least in so far as there is similar swapping and inverting of co-ords. Except not for everything. ZeX swaps them on the spinal bones, I see the need to swap them on the non-spinal ones. OK: the vertical bones are one or at worst two dimensional, so maybe they can work as a special case.

 

The trouble now is that not everything needs the same axes inverted, or so it seem. Hence the weird feet and ankles on the clip above. Now it's possible I just missed a minus sign (it happens) and it I correct the typo it works. And I could have a flag for each axis inversion case, although that way ends with me writing a transformation matrix for each bone, to go with the rotation I already have, and that sounds tedious beyond belief.

 

That said, I am getting interested in the fourth vector co-ordinate for the Havok data. Basically each transform is recorded as

 

(translation)(rotation)(scale)

 

Where scale is three numbers for X,Y and Z scale, rotation is four numbers for the WXYZ quaternion, and translation is four numbers for X,Y,Z and ... something else. That something else has always bothered me. For the vertical bones, the fourth number has always been zero and I've been hoping that this meant I could ignore it. But as bones rotate away from the center line, the fourth number play more and more of a role, and the first three seem to be an increasingly poor match for the needed position.

 

So I did some googling for vectors and 4-tuples and I ended up with this concept of Dual Quaternions. As if the single ones were not bad enough. This startedr out as a way to make it so you could mutiply quats and co-ordinate vectors and ended up with a pair of quats, one of them being a "normal" rotation quaternion, and the other being the co-ordinates multiplied by the rotation values in ways I'm probably never going to understand unless a read three or four academic papers.

 

But apparently Dual-Qs have some advantages, including unquely specifying a point's position and rotation in 3D space in a way that would usually need a 4x4 matrix, but with half the number of values needed to do it.

 

So maybe that's what's going on here. There are algorithms for converting between the two so it's not too hard to convert a few back and forth and see if I can see what's going on.

 

If that doesn't pan out ... I am running out of steam, folks.

Link to comment

I did a little test scripting. According to A BeginnersGuide to Dual-Quaternions, the way to make a dual quat is to

 

  1. Turn the vector into a quat by adding a fourth zero value at the end
  2. mutiply that quat by the rotation
  3. divide by 2

Therefore:

 

Spoiler

from mathutils import Quaternion

#
# dual quaternions: calcuate the "dual" part from a vector and q rotation ("real") quat
#
# http://wscg.zcu.cz/wscg2012/short/A29-full.pdf
#
def dual_q(xyz, rot_q):
    qxyz = Quaternion(xyz[:] + (0,))    
    dual = (qxyz @ rot_q) * 0.5
    return dual

#
# make two quats for real vector values and rotation
#
q_xyz = Quaternion((19.9267, -4.7962, -24.5000, 0))
q_rot = Quaternion((0.06376, 0.02759, 0.52466, 0.84797))

#
# quick test to see which operator I should be using
#
print(q_xyz * q_rot * 0.5)
print(q_xyz @ q_rot * 0.5)
print ("")

#
# print co-ords and "dual" quat for comparison 
#
print(19.9267, -4.7962, -24.5000)
print(dual_q((19.9267, -4.7962, -24.5000), q_rot))

#
# OK. Basic "t-pose" in blender and HCT: This is the right  foot 
#
# Blender:
#
#   XYZ = 2.3756, -4.9953, -31.4600
#   Rot = 0.9474, -0.2975, 0.0000, -0.1180
#
# HCT@
#
#   XYZ = 31.94257164001465 -7.047653198242188E-4 1.52587890625E-5 13.323389053344727
#   Rot = 0.06357214599847794 0.026946544647216797 0.5248612761497498 0.8483824729919434
#
# So the test is to plug the HCT quat into the Blender co-ords and see if the result looks plausible
#
hct_q = Quaternion((0.06357214599847794, 0.026946544647216797, 0.5248612761497498, 0.8483824729919434))
dq = dual_q((2.3756, -4.9953, -31.4600), hct_q)

print("wanted: {}".format(
    Quaternion((31.94257164001465, -7.047653198242188E-4, 1.52587890625E-5, 13.323389053344727))
))
print("got   : {}".format(dq))

 

 

That yields:

 

Spoiler


19.9267 -4.7962 -24.5
<Quaternion (w=7.1285, x=-10.2656, y=6.4798, z=7.5284)>
wanted: <Quaternion (w=31.9426, x=-0.0007, y=0.0000, z=13.3234)>
got   : <Quaternion (w=8.3989, x=-13.4718, y=1.7424, z=0.1207)>

 

 

I'd have settled for "close enough", but that's nowhere near. I can try a few variations on the co-ord system perhaps. Maybe use the python dual quaternion package and test some of my assumptions.  I mean is that first digit in the final 4-tuple X? Or W? Or are they all just numbers?

 

This rabbit hole just keeps on getting deeper. Is it too late to take the Blue Pill?

Link to comment

Well, seems like youre doing a great job so far! You managed to start something many people didnt even try or gave up uppon, so props to you. Would be nice to have more animators for this game, since 3DS Max is a hassle to get working...

 

Thank for your contribution and hard work :)

Link to comment
12 hours ago, Stormer said:

Thank for your contribution and hard work :)

 

Cheers! Much appreciated :)

 

I don't think I'm about ready to give up on this just yet, mind. I just think I need to back off on it a bit.

 

I might post what I have here anyway. If I do get sidetracked onto something else, it might at least give a head start to anyone else who wants to try.

Link to comment

I've been doing some toolsmithing. I mentioned before I wrote something to pretty print the bone date from a hkx xml file. If I take the default, at-rest pose and filter for hands, I get this:

 

LArm_Hand               (   6.152   -0.000   -0.000   -2.494)(  -0.703   -0.066   -0.037    0.707)(   1.000    1.000    1.000    0.000)
RArm_Hand               (   6.153    0.000   -0.000  -59.088)(   0.703    0.066   -0.037    0.707)(   1.000    1.000    1.000    0.000)

 

You can see the sort of problem I have: both hands have almost the same X-Axis displacement.  Which seems odd.

 

Anyway, I was fooling around and I wrote a python script to just save the raw co-ordinates and rotations to a text file.

 

Then I wrote something to read that text (JSON actually), compare each bone with the parent and adjust the co-ords for the parent position. Got an output like this:

 

Arm_Hand.L           [ -38.1178   7.7111  83.9789][   0.8988  -0.3277  -0.0000   0.2911]
(Arm_ForeArm3.L    ) [ -34.1482   4.2316  87.1376][   0.8848  -0.2902  -0.0000   0.3647]
                     [  -3.9696   3.4794  -3.1588]

 

So now, I thought why not put those numbers into a dual quaternion?

 

>>> from dual_quaternions import DualQuaternion
>>> from  pyquaternion import Quaternion
>>> r = Quaternion(0.8848,  -0.2902,  -0.0000,   0.3647)
>>> d = Quaternion(-3.9696,   3.4794,  -3.1588, 0)
>>> dq = DualQuaternion(r, d)
>>> dq
<DualQuaternion: Quaternion(0.8848, -0.2902, -0.0, 0.3647) + Quaternion(-3.9696, 3.4794, -3.1588, 0.0)e>
>>> dq.translation()
[6.157219120000001, -3.05193812, 4.72879376]

 

Notice that X co-ord? 6.157 .... Damn close to the Havok value. Of course, then it goes and ruins it by having non-zero Y and Z values, and I can't work out where to get  the fourth number from. Still, that's close enough to make me think I'm on the right track here.

 

And even more interestingly

 

>>> d2 = Quaternion( 3.9691,   3.4798,  -3.1616, 0)
>>> r2 = Quaternion(0.8847,  -0.2904,   0.0000,  -0.3646)
>>> dq2 = DualQuaternion(r2, d2)
>>> dq2
<DualQuaternion: Quaternion(0.8847, -0.2904, 0.0, -0.3646) + Quaternion(3.9691, 3.4798, -3.1616, 0.0)e>
>>> dq2.translation()
[6.156972680000001, -8.1316052, 4.730525]

 

... doing the other hand results in the same, positive X value. Same sign, same value, and odd in the same way. Definitely on the right track.

 

 

Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue. For more information, see our Privacy Policy & Terms of Use