View Full Version : Converting rotation values for BVH

 S-S03-20-2008, 12:42 PMHi! I wrote a BVH exporter for learning purposes. My exporter's "hierarchy parser" part is already done and is working ok and root positions and rotations are written into bvh file, and i can import it back. However, problem is that biovision stores rotations in angles. IMO It was relatively straightforward to read angles into object rotations (i also wrote importer part). But for exporting problem seems to be how to convert rotations to angles, in such manner that i can get the exactly same result back (max -> bvh -> max). Example: create a point helper rotate point 45 degrees along it's local x quatToEuler (\$point.rotation) shows: (eulerAngles -45 0 0) rotate point -45 degrees along it's local z quatToEuler (\$point.rotation) shows: (eulerAngles -45 -1.02453e-005 45) So far good... rotate point -45 degrees along it's local y Now, this is my problem: quatToEuler (\$point.rotation) shows: (eulerAngles -9.73564 30 54.7356) I no longer can just read then write the actual x,y and z values and then expect those to work later, when importing bvh file back to max. Result is an "almost there" walkcycle for example, when i compare it to original bvh file imported with my importer. I have tested results of my importer compared to other importers and it matches, so i think i'm quite sure that the problem is in how i write the rotation values. And what comes to from where i read the values; i use world oriented points, linked into joints of hierarchy, in setup frame. I read the rotations in "parent space" of each point, which is point linked to bone one level up in hierarchy. If someone has enlightening thoughts about rotations and how to solve this i would be delighted. I have tried different methods for getting rotations, not just converting quat rotation value to euler. But i seem to be stuck with this... Maybe i'm going into wrong direction.
TakuanDaikon
05-15-2008, 02:17 AM
I know it's been almost two months since you asked your question, did you ever find a resolution for this?

I am also currently working on a BVH exporter, and using quatToEuler to try to turn the joint rotations into the BVH format with the specified axis order, and everything works great until I either encounter a situation such as you describe above or the joint moves past 90 degrees (suggesting gimbal lock?).

Surely there's *some* way to extract an Euler representation that doesn't suffer these problems?

PEN
05-15-2008, 11:50 AM
This is definitly gimble that is causing the problem. What I'm thinking is you are going to have to get your angles from vectors instead of from rotations. I have not looked at the problem that Sami posted or BVH that close but I'm suspecting that you will need to get angles to be able to store angles correctly.

eek
05-15-2008, 06:50 PM
I'd check your rotation order too as BVH uses ZXY - I'm on holiday at the minute but I'll be looking into Bvh parsing when I'm back. (tools & mocap shoots etc)

TakuanDaikon
05-15-2008, 07:24 PM
Well in point of fact the particular BVH output that I require uses ZYX, XZY, or YZX, etc., based on the particular joint. I think I have all of that set up correctly, but I'm relatively inexperienced in this area so I might be missing something. I'll double-check.

Thanks for the replies!

[EDIT] I hope you have a great holiday, Eek!

eek
05-16-2008, 10:27 AM
Well in point of fact the particular BVH output that I require uses ZYX, XZY, or YZX, etc., based on the particular joint. I think I have all of that set up correctly, but I'm relatively inexperienced in this area so I might be missing something. I'll double-check.

Thanks for the replies!

[EDIT] I hope you have a great holiday, Eek!

The key is how you parse it though, it doesnt matter the rotation order of each joint you generally store the rotations always as ZXY. So if one joints rotation order is xyz and another is yxz you still grab the z then x then y.

Also remember how you parse it back using matrix math so, first you want to grab the z x y by. Getting the transform space relative the parent:

(quattoeuler (\$.transform * inverse \$.parent.transform)).z and the positon: tmPos = (\$.transform * inverse \$.parent.transform).pos

This will get the space the joint exists in and the rotation z value. And now you apply this back with \$.rotation[2].value = value.

You could also generate a matrix transformation which maybe a little complex (way i think i'll do it):

tm = (matrix3
((eulerangles value 0 0) as matrix3).row1 \
((eulerangles 0 value 0) as matrix3).row2 \
((eulerangles 0 0 value) as matrix3).row3 \
tmPos)

And rebuild the space: \$.transform = tm * \$.parent.transform

So basically at the header of the BVH file you store the direction of each joint i.e the offset position relative to the parent, then the rotations.

I think storing angles may get a little complex, your need vectors and a dot product but also have to use the cross product to determine if its in - or +.

From the web:

The BVH format now becomes a recursive definition. Each segment of the hierarchy contains some data relevant to just that segment then it recursively defines its children. The line following the ROOT keyword contains a single left curly brace '{', the brace is lined up with the "ROOT" keyword. The line following a curly brace is indented by one tab character, these indentations are mostly to just make the file more human readable but there are some BVH file parsers that expect the tabs so if you create a BVH file be sure to make them tabs and not merely spaces. The first piece of information of a segment is the offset of that segment from its parent, or in the case of the root object the offset will generally be zero. The offset is specified by the keyword "OFFSET" followed by the X,Y and Z offset of the segment from its parent. The offset information also indicates the length and direction used for drawing the parent segment. In the BVH format there isn't any explicit information about how a segment should be drawn. This is usually inferred from the offset of the first child defined for the parent. Typically, only the root and the upper body segments will have multiple children.

Interpreting the data

To calculate the position of a segment you first create a transformation matrix from the local translation and rotation information for that segment. For any joint segment the translation information will simply be the offset as defined in the hierarchy section. The rotation data comes from the motion section. For the root object, the translation data will be the sum of the offset data and the translation data from the motion section. The BVH format doesn't account for scales so it isn't necessary to worry about including a scale factor calculation.

A straightforward way to create the rotation matrix is to create 3 separate rotation matrices, one for each axis of rotation. Then concatenate the matrices from left to right Y, X and Z.

vR = vYXZ

An alternative method is to compute the rotation matrix directly. A method for doing this is described in Graphics Gems II, p 322.

Adding the offset information is simple, just poke the X,Y and Z translation data into into the proper locations of the matrix. Once the local transformation is created then concatenate it with the local transformation of its parent, then its grand parent, and so on.

vM = vMchildMparentMgrandparent…

http://www.cs.wisc.edu/graphics/Courses/cs-838-1999/Jeff/BVH.html

Im a little rusty, till i get back to my computer. So the first part:

OFFSET 0.0 5.0 10.0 is the xyz i.e the position of the joint relative to its parent ie the parents length. This is crucial as there us no real storing of scale.

CHANNELS ZRotation XRotation YRotation

so if we have an offset of 0 5 10 and say a rotation of 45 45 90 (my rotation only add up to 180!, use a quaternion to get this) we build a matrix:

((eulerangles 45 45 90) as matrix3).row*1,2,3... and for the fourth part we use the offset.
And then take this matrix and and transform it about its parent.

TakuanDaikon
05-16-2008, 01:19 PM
Very cool, thanks for the info.

One part I'm still curious about, though; You said "it doesnt matter the rotation order of each joint you generally store the rotations always as ZXY", and the example .bvh file on the site you linked to also had ZXY defined as the axis order for each joint (always the same in that example), but the bvh format that I need to create specifies a (possibly) different axis order on each joint like so :
[...snip...]

JOINT lThigh
{
OFFSET 9.509759 -2.926078 2.438399
CHANNELS 3 Xrotation Zrotation Yrotation
JOINT lShin
{
OFFSET -3.657599 -35.844474 0.000000
CHANNELS 3 Xrotation Zrotation Yrotation
JOINT lFoot
{
OFFSET 0.000000 -34.381443 -2.194559
CHANNELS 3 Xrotation Yrotation Zrotation
End Site
{
OFFSET 0.000000 -4.389119 8.290559
}
}
}
}

[...snip...]

JOINT rCollar
{
OFFSET -6.339839 11.948165 -1.463040
CHANNELS 3 Yrotation Zrotation Xrotation
JOINT rShldr
{
OFFSET -5.608320 0.000000 0.000000
CHANNELS 3 Zrotation Yrotation Xrotation
JOINT rForeArm
{
OFFSET -18.287998 0.000000 0.000000
CHANNELS 3 Yrotation Zrotation Xrotation
JOINT rHand
{
OFFSET -14.874242 0.000000 0.000000
CHANNELS 3 Zrotation Yrotation Xrotation
End Site
{
OFFSET -7.315200 0.000000 0.000000
}
}
}
}
}

[...snip...]

That shouldn't really complicate things though, right? I should be able to generalize what you said even to accomodate that?

I wrote an exporter for this format for another program and had absolutely zero problems with it, the entire thing was done in a day, it's just Max that is giving me problems :)

S-S
05-16-2008, 03:25 PM
TakuanDaikon:

Nope, i haven't touched the script for a while - and i don't have a solution yet, been doing other kind of rigging related stuff on weekends and evenings last two months!

I think i'll give this hobby project another try - after i have read all these posts! As far as i remember right now, i guess next step for me is to play a bit more with cubes, and try to understand these rotations a bit more.

Rotation order of thing seems to be important, but i noticed it's not just it causing the problem. I'm aware of ZXY axis order, but that itself doesn't seem to solve it at least for me (that's why i asked).

Also, i *guess* that gimbal lock isn't the problem in my first tests - it might be when one rotates object more. None of the axis had more that 45 degrees rotation in tests i ran.

But i might be wrong and i'll have to refresh my memory first! Thanks for replies guys!

TakuanDaikon
05-16-2008, 10:46 PM
Rotation order of thing seems to be important, but i noticed it's not just it causing the problem. I'm aware of ZXY axis order, but that itself doesn't seem to solve it at least for me (that's why i asked).
[EDIT] Post body removed, as it seems I may have been completely wrong, but I don't know how to delete my post :-[

TakuanDaikon
05-18-2008, 02:09 PM
I seem to have solved my problems, though the solution is completely unexpected.

First of all, thanks eek for the info, it helped put me on the right track. I don't understand this stuff well enough to know precisely why this works now, but it does :). It was a very different experience doing this in Max than in the other programs, because each program seems to have things it handles for you "behind the scenes" that the others don't, and that led me to make many incorrect assumptions.

For my situation, and I don't know if this applies generally or just to my specific rig, I had to go through each joint/bone (including the FK and IK setups) and set the axis order to what was specified in my .bvh template, meaning that I set the rotation controller's AxisOrder to 3 (YZX) for the left collarbone, etc.

After I'd double- and triple-checked all of these, it turned out that I could just do the standard "quatToEuler (bone.transform * inverse rot_reference.transform).rotation" code to extract the rotations, and could just store them in the .bvh file in ZYX order. I think that if the target environment used the same coordinate system as Max I could have used the more standard ZXY that eek mentioned above, but my target environment swaps Y and Z so ZYX seemed to work for me.

So it seems as if when all of the bone's AxisOrder properties are set the way the template BVH specifies, I can just extract and store the rotation information exactly as Eek described (with the exception of swapping Y and Z in my case) and everything works exactly as expected.

It is still possible to encounter gimbal lock, but in practice it doesn't happen. I assume, though I admit to not having a deep understanding of this stuff, that this is because the target .bvh template specifies each joint's axis order in a way that should minimize gimbal lock.

So for the right shoulder, for instance, I can rotate on what might be considered the main axis quite as far as I'd like with no problems. If I rotate too far on either of the other two axes I might encounter gimbal lock, but in practice shoulders don't do that in real life, so it's not likely to happen.

I hope that made at least a little sense, it's like 5:00AM and I haven't had enough coffee yet :)

In any case, I now have a working .bvh exporter for my custom rig, and I'm pleased as punch. Thanks for the input guys! And best of luck to S-S on getting it working!

.

eek
05-19-2008, 01:18 AM
BVH was designed for this very reason, its essentiallya raw format - you throw your data in that format and parse it how you want. Whats cooler is that any skeleton even broken ones can be parsed into the format.

I've worked with two off-sight studios now on mocap setups, so im getting the hang of it.

I really havent dont much, in terms of parsing yet - i've been on holiday thinking up a essentially a bvh toolset i want to write. This is what i tend to do nowadays - working on the system then implimenting it.

In theory axis order shouldnt affect transforms matrices though i.e if i extrapolate the YXZ rotations of an object with axis order xyz then put them on another object with axis order of zyx it shouldnt matter. You should i think get the same result.

tmX = (quatToEuler (\$.transform * inverse \$.parent.transform)).x
tmY = (quatToEuler (\$.transform * inverse \$.parent.transform)).y
tmZ = (quatToEuler (\$.transform * inverse \$.parent.transform)).z

tm = matrix3 tmX tmY tmZ offset

Now transforming the matrix tm about its parent with a new axis order shouldnt matter i thought.

\$.transform = tm * \$.parent.transform

but im not sure, in my view axis order is a system of interpolation of euler axis not there stucture. I.e y is still y when you change the axis order or is y x?

EDIT: Im not sure, i just got back, why might happen is if your values go past 180 your'll get gimbal as you interpolating on axis that arent meant to go past 180. I need to get back to my maching and look into it.

EDIT TWO: Hmm..

eek
05-19-2008, 01:39 AM
Ok so from a bunch of sites:

http://en.wikipedia.org/wiki/Euler_angles
http://en.wikipedia.org/wiki/Rotation_representation_(mathematics)
http://www.softhelp.ru/fileformat/bvh/bvh.htm

Axis order is important, now im wondering whether we need to change it or just build a transfrom space for it.

i.e say weve derived tmX Y and Z, can we just build a transfrom space matrix using this order i.e matrix3 z y x offset instead of x y z offset? A little test i think..

S-S
05-20-2008, 07:46 PM
eek:

Thanks for links eek! Today i had a little bit of time, and i cleaned up the exporter code, and it think the same problem is still the cause.

I fixed all other bugs (position of root joint was offset by one frame...) and such with one joint BVH file, i guess i'll have to read a bit more about eulers and angles... at this point it
s only choice as otherwise exporter works fine.
I checked my rotation code, it's nearly identical to what you suggested.

TakuanDaikon
05-20-2008, 08:58 PM
I checked my rotation code, it's nearly identical to what you suggested.I did as well, and despite my earlier enthusiasm and report of success, I seem to have been premature. I can export basic animations, but only within a very limited range. I don't know what I have done wrong, but there are a great many movements such as reaching with the hand across the body to the opposite shoulder that simply won't work.

I've decided to give up on creating a .bvh exporter for Max, for the time being. I did some "proof of concept" experiments last night, and managed within just a couple of hours to create an exporter that outputs the quaternion rotation values in a manner that I can easily and accurately read them back in using a matching importer I created for another program, apply them to a similar figure, and re-export them from there as .bvh using an exporter I had written previously. It's an added tool in the chain and more steps than I would like, but it only took a couple of hours, whereas I've been working on .bvh export in Max for over a week with no success.

I'd love to eliminate the extra steps and do it all in Max, but I simply don't have the time to invest or the knowledge to do so. I'll keep watching this and other threads, and I do hope to pick up on the missing pieces eventually.

Thanks for the info and links eek and PEN, it's very much appreciated, and best of luck to you Sami. I hope you manage where I have not :)

.

eek
05-21-2008, 03:47 PM
eek:

Thanks for links eek! Today i had a little bit of time, and i cleaned up the exporter code, and it think the same problem is still the cause.

I fixed all other bugs (position of root joint was offset by one frame...) and such with one joint BVH file, i guess i'll have to read a bit more about eulers and angles... at this point it
s only choice as otherwise exporter works fine.
I checked my rotation code, it's nearly identical to what you suggested.

So i did a very quick test, loading the same matrix transform onto the same object with different rotation orders. My initial hunch was correct, it produced the same result each time.

I'd ask how you guy's are passing the values, and whether or not the axis order is changing either through the animation or per bvh.

I think what might be happening is that your not compensating for the objects intial rotation relative to world space in respect to its pivot.

S-S
05-21-2008, 07:25 PM
EEK:

Thanks for interest. This is what i have done to test this problem

A. I have created simple ideal bvh file manually. It contains root, joint and "end site". It has two frames of animation,a neutral pose frame and a frame with rotations. Rotation values are float, i just wrote them as integer for the sake of clarity:

HIERARCHY
ROOT Hips
{
OFFSET 0.0 0.0 0.0
CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation
JOINT Chest
{
OFFSET 0.0 5.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
End Site
{
OFFSET 0.0 10.0 0.0
}
}
}
MOTION
Frames: 2
Frame Time: 0.0333333
0 0 0 0 0 0 0 0 0
1 2 4 1 2 4 1 2 4

B. Then, after i import this file i export it again, and this is the result:
(Now i have to guess that my importer works ok, but i have compared my imported joints to one imported with 3rd party importer and i think it's accurate)

HIERARCHY
ROOT Hips
{
OFFSET 0.0 0.0 0.0
CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation
JOINT Chest
{
OFFSET 0.0 5.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
End Site
{
OFFSET 0.0 10.0 0.0
}
}
}
MOTION
Frames: 2
Frame Time: 0.0333333
0.0 0.0 0.0 0.0 -6.83019e-006 0.0 0.0 0.0 0.0
1.0 2.0 4.0 1.13706 1.9254 3.99835 1.13705 1.92541 3.99835

This is the way i've tried to convert euler values to angles:

a_points_parent_node = (getNodeByName (a_points_parent.name + "_ref"))
rx = (quatToEuler ((the_ref_points[a_ref_point].transform * inverse a_points_parent_node.transform).rotation)).x
ry = (quatToEuler ((the_ref_points[a_ref_point].transform * inverse a_points_parent_node.transform).rotation)).y
rz = (quatToEuler ((the_ref_points[a_ref_point].transform * inverse a_points_parent_node.transform).rotation)).z
format "% % % " -ry rx rz to:outfile

S-S
05-21-2008, 07:36 PM
I'd like to add one thing - In exported file you'll see those "inaccuracies" like rotation of 1.0 degrees is 1.13706 and rotation of 2.0 degrees is 1.9254.

This wasn't that obvious until i compared BVH imported skeleton + animation to imported-exported-imported skeleton + animation.
Run cycle for example is nearly there but it's obvious this ain't acceptable and it only gets uglier further down the hierarchy, thanks to cumulative errors :)

TakuanDaikon
05-22-2008, 02:53 AM
So i did a very quick test, loading the same matrix transform onto the same object with different rotation orders. My initial hunch was correct, it produced the same result each time

At the risk of sounding dense, I'd like to ask for clarification here... If you have a limb that rotates only 90 degrees along a single axis (we'll pick X for example) and apply that to a second limb with a different axis order, you only get the same result so long as you apply X in the correct order, right? I mean, a rotation from a limb with XYZ rotation order and [90,0,0] rotation would be applied to a limb with ZXY order as [0,90,0], right?

Or am I getting this wrong?

.

eek
05-22-2008, 03:06 PM
At the risk of sounding dense, I'd like to ask for clarification here... If you have a limb that rotates only 90 degrees along a single axis (we'll pick X for example) and apply that to a second limb with a different axis order, you only get the same result so long as you apply X in the correct order, right? I mean, a rotation from a limb with XYZ rotation order and [90,0,0] rotation would be applied to a limb with ZXY order as [0,90,0], right?

Or am I getting this wrong?

.

Your'll get the same value with each different axis order, axis order doesnt matter in the end, only the orientation and position of the pivot. When this changes your'll need the 'space' difference.

This is why i said you have to take into acount the intial rotation of the object and its pivot. Axis order from my research doesnt affect anything other than the order of the interpolation of the matrix not the actual matrix order.

And to your last past yes - but this is automatic. I havent tried this but if you set an object rotation order to say ZYX and then ask for its first value i.e \$.rotation.controller[1].controller

does it return its xSubcontroller or Z - my hunch is X because its transformation matrix i dont think changes only the order of actions inside it.

Stroker
05-24-2008, 12:49 PM
I did a BVH exporter recently. To get around the rotation order and things, I went with a temporary object.

Create the temporary object, parent it, set the rotation, and get the angles in gimbal.

tempbox.rotation.controller.axisorder = 2
tempbox.parent = lshoulder
tempbox.rotation = lfore.rotation
na = in coordsys gimbal tempbox.rotation.y_rotation as float
nb = in coordsys gimbal tempbox.rotation.z_rotation as float
nc = in coordsys gimbal tempbox.rotation.x_rotation as float

A bit long-winded, but I haven't had anything go wrong with it yet. Maybe some day I'll know enough about MaxScript to refine it.

One little thing that I ran into is that the rotation order in the BVH files I was doing is actually in reverse order from what you tell Max the rotation order is. If the order in the BVH is YZX, then I had to tell Max that the rotation order is XZY. Then output the order in the same order as in the BVH. Threw me for a serious loop.

TakuanDaikon
05-24-2008, 01:42 PM
I tried that exact same code, having gotten it from the SL forums, and it works for a lot of cases but unfortunately not all of them. In particular, there were several animations I tried where the results were way off, such as drawing a hip or back-mounted sword. For walk and run cycles and a lot of common anims it seems to work fine though.

One little thing that I ran into is that the rotation order in the BVH files I was doing is actually in reverse order from what you tell Max the rotation order is. If the order in the BVH is YZX, then I had to tell Max that the rotation order is XZY. Then output the order in the same order as in the BVH. Threw me for a serious loop.
Yeah, I finally noticed that after bunch of tinkering with the script, though it took a while to discover because it wasn't commented :p

I wonder if one of the MaxScript/Maths gurus here could explain why this is?

.

eek
05-30-2008, 11:41 PM
You guys tried build a matrix?

Get the order from the bvh eg. ZXY from readLine:

Line = filterstring (readLine inFile) "\t, " splitEmptyTokens:false

ZRot = line[4] as float
XRot = line[5] as floatYRot = line[6] as float

Turn those in rotation Matrices:

ZRotationMatrix = rotateZMatrix zRot
XRotationMatrix = rotateXMatrix xRot
YRotationMatrix = rotateZMatrix yRot

build this matrix, in an opposite order.

FinalRotMatrix = YRotationMatrix * XRotationMatrix * ZRotationMatrix

Then apply this to the joint:

Joint[1].transform = Joint[1].transform * FinalRotMatrix

Before this, build an identity matrix with the offset as row4 and multiply this by the parent transform:

joint[1].transform = (matrix3 [1,0,0] [0,1,0] [0,0,1] jointOffsets[1]) * jointArray[1].parent.transform

cheers,

TakuanDaikon
05-31-2008, 06:44 PM
Speaking for myself, the biggest part of the problem (and the only reason for using .bvh format) is that I have absolutely no control over the destination environment and no visibility on how it applies the rotations from the file to the joints. For me the task is all about export, and import is completely out of my control.

The target application is a third-party application that has provided the .bvh specification they use (which includes the rotation orders for each joint), and that's all I get. The challenge is to create an exporter that properly conforms to that specification. I was able to do so without any problems in DAZ|Studio, which is kind of like a free Poser clone (but uses Javascript for plugins), and it works great aside from the fact that the animation tools in DS are nowhere as capable or sophisticated as 3ds Max.

So I'd love to be able to develop a Max exporter that can do the same, I just find myself unable to understand how to extract and write the rotation information in a way that it comes across unaltered in the target program. I am quite sure it's possible, I'm just missing some critical understanding of how Max and MaxScript work :(

.

CGTalk Moderation
05-31-2008, 06:44 PM
This thread has been automatically closed as it remained inactive for 12 months. If you wish to continue the discussion, please create a new thread in the appropriate forum.

1