Co-ordinate Spaces

(This is a bit of long post but I would be really greatful if someone could shed some light on this issue, its more about visualising the co-ordinate system in Panda)

A slight problem with getting my head around rotation co-ordinates in relation to bone orientation.

If I set up a T-pose skeleton in Max and import into Panda, it seems the .egg file will store each bone with an orientation that matches that bone orientation in Max. It seems as though a bone with Hpr(0,0,0) will be pointing to the right and facing the sky. So my chest bone that faces forwards (towards the screen) and points upwards (inside Max), is stored with a default orientation as (90,0,-90) in the .egg file so Panda can display the bone correctly.

Ok, this is fine but what happens when I want to feed in rotational values to this bone? I want to move the bone +10degrees around the Z-axis.

hips.setHpr(10,0,0) = FAIL (the bone moves plus 10degrees around the Z-axis but in relation to the default bone position: pointing to the right and facing up + 10degrees in Z)

So I must:

self.hipsChannelOrder.setHpr(self.float[5],self.float[4],self.float[3])
self.hipsCorrection.setHpr(self.hipsChannelOrder, 90,0,-90)

This yields the correct result.

Now what is strange and what I cannot figure out:

The child of this bone, chest, is already aligned correctly (pointing up, facing forward) because it has enherited the ‘hipsCorrection’ from the Hips bone. However, to now rotate around the Z-axis I must apply values to the ‘Pitch’ axis (rotations around X) instead of ‘Heading’ (rotations around Z):

self.chestC.setHpr(Point3(self.float[33],self.float[35],self.float[34]*-1))
self.neckC.setHpr(Point3(self.float[60],self.float[62],self.float[61]*-1))
self.headC.setHpr(Point3(self.float[63],self.float[65],self.float[64]*-1))

I must multiply the value in ‘Roll’ by -1 to yield a correct result. Local co-ordinate space is changing but how comes it doesn’t affect the bone which I applied the transformation to but it does to its children? Does anyone know how this transformation or any transformation for that matter affects co-ordinate space. If I rotate by (90,180,90) how does that affect Hpr? The hpr directions are obviously changing (i did a little experiment).

These values are being inserted into the float array from a BVH file. (BVH Rot order = ZXY:

float[3] = hips Z
float[4] = hips X
float[5] = hips Y
float[33] = chest Z
float[34] = chest X
float[35] = chest Y etc)

The order of ZXY remains constant throughout the file.

I understand that the co-ordinate system from the BVH must be matched to Panda. So maybe the channels arn’t applied Hpr(float[3],float[4],float[5]) but perhaps Hpr[float[4],float[3].float[5]*-1) to match t he co-ordinate space.

WHAT I DONT GET:

BUT i do not understand why the order changes when I rotate a bone to give it that correct first orientation?

Plus, the Hpr order on the bone i rotate (hips) stays the same but it is the child(ren) whose co-ordinate system changes!

Does anyone understand what I’m trying to illustrate? My BVH values map correctly to my character and it looks awesome but there is no logic to the order and I must get my head around these bone orientations. Getting the hips and chest working wasn’t too much effort but once 90degree changes happen for both shoulders then both upperArms (to get into the T-pose) it gets crazily confusing. Plus the foreArms needed crazy default orientations to work even though they face the same direction as their parent.

#Apply the data to model
    def applyMotion(self):
        
        #Hips
        #Channel order BVH: ZXY
        #To convert to Panda: Hpr(Y,Z,X)
        self.leftH.setHpr(self.float[5],self.float[3],self.float[4])       
        #This correction must be made to orientate the bone correctly
        self.hipsC.setHpr(self.leftH, 90,0,-90)
                
        #Chest
        #Channel order must now change to Hpr(Z,Y,X*-1) to display correctly???
        #No correction is needed as the bone is orientated correctly (following the correction applied to its parent - self.hips)
        self.chestC.setHpr(Point3(self.float[33],self.float[35]*-1,self.float[34]*-1))
        
        #Neck
        #Channel order same as Chest
        self.neckC.setHpr(Point3(self.float[60],self.float[62],self.float[61]*-1))
        
        #Head
        #Channel order same as Chest
        self.headC.setHpr(Point3(self.float[63],self.float[65],self.float[64]*-1))
        
        #LeftShoulder
        #Channel order doesn't change although we have orientated the bone differently!:
        self.leftH.setHpr(self.float[36],self.float[38],self.float[37]*-1)
        #This correction must be applied to orientate the bone correctly
        self.leftShoulderC.setHpr(-90,0,0)
        
        #LeftArm
        #Channel order changes!
        self.leftA.setHpr(self.float[39],self.float[40],self.float[41])
        #Channel correction must be applied to orientate the bone correctly
        self.leftArmC.setHpr(self.leftA, -90,0,0)
        
        #LeftForeArm
        #Although this bone is facing in the same direction as leftArm we must do some crazy things with Hpr to display properly!
        #First correction
        self.leftForeArmC.setHpr(90,180,0)
        #Apply channels
        self.leftA.setHpr(self.leftForeArmC,self.float[44],self.float[43],self.float[42])
        #Second correction
        self.leftForeArmC.setHpr(self.leftA, 90,180,0)
        
        #Left Hand
        #Channel order different again!
        self.leftHandC.setHpr(Point3(self.float[45],self.float[47]*-1,self.float[46]))
        
        #Right Shoulder
        self.rightS.setHpr(self.float[48],self.float[50],self.float[49]*-1)
        self.rightShoulderC.setHpr(self.rightS, 90,0,0)
        
        #Right Arm
        self.rightS.setHpr(self.float[51],self.float[52]*-1,self.float[53]*-1)
        self.rightArmC.setHpr(self.rightS, 90,0,0)
        
        #Right ForeArm
        self.rightForeArmC.setHpr(90,180,0)
        self.rightS.setHpr(self.rightForeArmC, self.float[56],self.float[55],self.float[54])
        self.rightForeArmC.setHpr(self.rightS, 90, 180,0)
        
        #Right Arm
        self.rightHandC.setHpr(Point3(self.float[58],self.float[59],self.float[57]))

Could using Quaternions be an answer? The foreArms didn’t require initial orientation and I believe that I got the channel order correct but flipping occured at certain angles, I think this might have been due to gimble lock or the fact that with euler’s: (X=90,Y=180,Z=100) != (Y=180,Z=100,X=90) Anyhow I had to mess around with the orientation so that I could apply the channel’s in a differently order…make sense? though not.

No, seriously, if anyone has had any experience with what I’m talking about please shed some light on this issue!?

Thankyou.

Could someone shed some light on this please?

One suggestion that might make it easier for you. Try hand-editing your egg files from Max, including the model and all its animations, to change the first line from:

<CoordinateSystem> { Y-Up }

or whatever it says now, to:

<CoordinateSystem> { Z-Up }

The reason I suggest this is that what’s confusing the joint rotations for you is probably the coordinate-system correction that Panda has to apply to rotate the egg file into its native Z-up coordinate system. If you tell it that the egg file is already in a Z-up coordinate system, it won’t have to unrotate the joints, and what you see in-game will more nearly match what you see in Max, and it will probably make a lot more sense to you. Of course, your character will be lying on his back and you’ll be looking at his feet or something like that, but you can fix that by putting the whole character node under a 90-degree rotation.

David

Hi,

The .egg file is already Z-up. I experiemented with other co-ordinate systems (with rotating my character) but it seems as though Z yielded the best results. Max has Z-up as well.

I think these images could help demonstrate my point clearer:

My Max skeleton:

I export to .egg and get this is Panda:

Perfect! Although to get this representation in Panda, the .egg file must store the hips hpr at (90,0,-90) and the right angle joint as (-90,0,0). Okay that is fine as the .egg file does that all automatically so I don’t need to worry.

However, my default pose, which matches the skeleton system in Max, from my mocap has all joints set to hpr(0,0,0). If I apply the first frame of mocap data (everything set to 0,0,0) I get:

Oh NO! Must apply corrections in code manually to get back to intial pose:

#HIPS
self.hipsCorrection.setH(self.hipsCorrection.getH() + 0.1)
#A correction of 90,0,-90 has to be made to this bone to orientate it correctly.
#This operation changes the channel order from (H,P,R) to (?,?,?) to correlate to Panda's world co-ordinates
self.boneC.setHpr(self.hipsCorrection, 90,0,-90)

Result for first frame, everything set to 0,0,0:

Back to original position. So all is good :slight_smile:

BUT! If we apply next frame etc (or in this example start adding 0.1 degrees to the ‘Heading’ every frame:

    def go(self, task):
        
        #HIPS
        self.hipsCorrection.setH(self.hipsCorrection.getH() + 0.1)
        #A correction of 90,0,-90 has to be made to this bone to orientate it correctly.
        #This operation changes the channel order from (H,P,R) to (?,?,?) to correlate to Panda's world co-ordinates
        self.boneC.setHpr(self.hipsCorrection, 90,0,-90)
        
        #Right Angle Bone
        #self.bone2
        
        self.bone2C.setHpr(self.hipsCorrection, -90,0,0)
        
        #Head
        self.bone3C.setHpr(self.hipsCorrection, 0,0,0)

OH NO! So Hips is still rotating around Z, but the children bones above it is now rotating around Y? So now channel order for Hips is like (Z,X,Y) but for chest it must now be something different (X,Z,Y) etc? I would expect the channel order to remain the same or to change when I alter a bones orientation (like I did with the Hips). What seems to happen is that the bone that is orientated differntly still operates the same but its children then change in someway. Make sense?

No, that doesn’t make sense. There’s no magic in the joint transforms; they compose just like any other parent-child relationship. It’s not possible for a joint’s rotation not to affect its children.

If this is the behavior you’re seeing, perhaps it’s due to some strange structuring by Max or the Max converter, for instance, maybe the joints aren’t actually parented to each other in the hierarchy you believe they are? Or maybe there are some intervening joints you’re not aware of that are counter-rotating the children in some unexpected way?

You can use actor.listJoints() to show the hierarchy. You can also use egg-optchar -lsv, this will show some additional information like “static” or otherwise. Note that you should not normally run egg-optchar on your files, unless you also specify the -keepall option, since egg-optchar’s default behavior without -keepall will be to apply exactly the kind of weirdness that you’re encountering, in the name of optimizing the static playback of animation.

David

For you first point, sorry I think it wasn’t clear in the diagrams but the first bone’s rot around Z (heading) is passed on to the children. So if I do not manipulate the childrens bones Hpr in anyway they rot around Z as well, which is what you would expect from a parent/child relationship. I looked at the schematic view in Max and relationships are intact with no additional bones, and this is reinforced by the behaviour I have mentioned above.

The last diagram tried to illustrate that if I apply:

#Bottom bone
self.bone1.setH(self.bone1.getH() + 0.1) = Rot around Z

#Right Angle Bone
self.bone2.setH(self.bone2.getH() + 0.1) = Rot around Y

#Top bone
self.bone3.setH(self.bone3.getH() + 0.1) = Rot around Y

(in the last diagram I’m applying 0.1 to all bone’s headings so bone2&3 are rotating around Z(from relationship with parent (bone1)) & around Y (from their own setH).

I know that the (90,0,-90) addition that we HAVE to apply to bone1 is effecting the co-ordinate space, I just don’t get why and the result?

Bone2 has also been given an addition (-90,0,0) so I’m positive that if it had a child, the child’s co-ordinate space would also be changed.

For the last point you made I will look into it but there does seem to be a pattern and I think its more about getting my head around it. If the Max exporter didn’t require a starting orientation (to define bone’s default pos is eg. hpr(180,90,0) from the default bone position) in Panda and could somehow map the bone default orientations without affecting Hpr then I think that there wouldn’t be a problem, unfortuantly for me it does.

Thanks for your help

character

Bone01 hpr 90 -9.33469e-006 -90
Bone02 hpr -90 -5.00896e-006 -5.00896e-006 trans 60 -9.77524e-006 0
Bone03 hpr -6.83017e-006 0 2.50447e-006 trans 70 -1.82789e-005 -6.1196
e-006

(this is the print out from actor.listJoints() at default pos (without any setH going on etc). As I expected, for panda to render the default pos correctly the Hpr values are manipulated)

On the egg-optchar -lsv i get:

Character: character
Bone01 (static) (top)
Bone02 (static) (empty)
Bone03 (static) (empty)
3 joints.

What does the ‘static’ keyword specify? Cannot find any information on egg-optcar -lsv on the web.

“static” simply means there is no animation table for the joint. Since you presumably don’t have an animation file to go along with your model, this is not surprising.

Re-reading your original post, I think I now understand your problem a little better. You want to set your joints to some arbitrary rotation, but you are thwarted by their local coordinate space, which makes the required local transform confusing.

Why not let Panda do the matrix math for you? If you set up your controlJoint nodes so that the are in a hierarchy matching the actual joint hierarchy, then you can use Panda’s relative scene-graph operations to manipulation your joints relative to any transform you like.

For instance:

Bone01 = actor.controlJoint(None, 'Bone01')
Bone02 = actor.controlJoint(Bone01.attachNewNode('Bone02'), 'Bone02')
Bone03 = actor.controlJoint(Bone02.attachNewNode('Bone03'), 'Bone03')
Bone03.setHpr(actor, 10, 0, 0)

I’m assuming that your joint hierarchy is linear: Bone01 is the top, Bone02 is a child of Bone01, and Bone03 is a child of Bone02. The loss of indentation in the egg-optchar output makes it hard to tell what the actual parent-child relationship is.

David

If I apply your example to my test, as shown before, the bone’s local space still operates in the same way.

Rotating chest around H = rot around Z.
Rotating head around H = rot around Y.

I do not understand this? Both bones are orientated the same way. They are aligned in Max the same way. The only difference is that Chest needed a correction of (90,0,-90) applied so that it accepts the motion values (eg:(0,0,0) for default pose) without changing orientation.
It seems as though applying (90,0,-90) before any motion data is applied changes local space, which I would assume. But this local space isn’t matched by it’s child, head. Why? Shouldn’t this be inherited by the child?

Applying .attachNewNode seems to mess up translation offsets as well, like each bone is connected to the previous by the root rather than tip resulting in every bone originating from the same point in 3D space:

But should look like:

I could change all translation offsets manually but the original problem still seems present…

Yes I would like to set each bone to a rotation that matches my mocap system (i.e the default pose). The foreArm’s local space is calculated by the upperArm’s local space but I don’t calculate this, the mocap system does. I believe that Panda will operate in the same way so no problem.

To conform to my mocap rig my chest bone must be facing forwards and up.

If I set up chest bone is Max and export to Panda, this bone is given the Hpr value (90,0,-90) (in egg file).

If I apply first frame of mocap data, essentially the default pose, Hpr(0,0,0) to my chest bone it loses the original position it first had.

I must add (90,0,-90) to this frame of data to get the chest bone back to facing forwars and pointing up.

Works perfect. But chest’s child; head, doesn’t have the same co-ordinate space even though it is pointing and facing in the same direction. Data must now be applied setHpr(P value, H value, R value) to get correct result. If I’m manipulating the chest how comes the local space doesn’t change but it’s children’s does? OR, why does the chest local space change but the children’s don’t? Shouldn’t local space adjust according to their parents orientation?

Thanks for taking the time to understand this problem, I kinda already have a solution as it works but there seems no logic. I just put in random channel orders (hpr),(phr),(rph) etc for each bone until I got the correct result.

Note that when you replace a transform via controlJoint(), you are responsible for preserving the original transform that was applied on the model in the first place if you want the model to retain its same original rest pose. In particular, this means you need to (a) preserve the original translation on every joint, and (b) the rest pose of your mocap input must exactly match the original rotation on your joints.

That is to say, when your mocap input is reporting (0, 0, 0) for every joint, your motion actor is holding some particular pose. Presumably this is the rest pose, the same pose in which your 3-D model was created. If the initial joint angles of every joint in your 3-D modeling software is not (0, 0, 0) for the corresponding pose, you have a problem. You will need to correct the model, for instance by freezing out the transforms.

But don’t freeze out the translations; those are needed to provide the separation between each joint. Usually translations are not included in mocap data, but you have to be sure that when you apply the mocap input onto your skeleton, you do not inadvertently zero out the translations.

David