0
\$\begingroup\$

I have a series of connected bones in a straight line. I want to be able to rotate each bone vertically and/or horizontally so that it can slither and arch randomly kinda like a snake mixed with an inchworm. When I do this, however, the axes get entirely botched. If I rotate one bone horizontally (i.e. on Y), then vertically (i.e. on X), then back horizontally the the other way, the bone is now rotated on the third axis (i.e. Z). This is further compounded when all the bones are trying to do the same thing. How can I keep these pieces "upright" while doing all these random rotations?

Edit: I think it comes down to this concept. If transform.right = (0.9, 0.2, -0.4) and transform.forward is (-0.3, -0.2, -0.9), how do I calculate how much I should rotate about transform.forward to make transform.right = (x, 0, z) from (0.9, 0.2, -0.4)?

\$\endgroup\$
  • \$\begingroup\$ If you're rotating the bones through code, can you show us a sample of that code (or pseudo-code)? Seeing how you're computing & compounding your rotations can help identify potential fixes. Or, if these are animated rotations, can you walk us through your animation setup? \$\endgroup\$ – DMGregory Aug 13 '18 at 13:15
  • \$\begingroup\$ I've tried many different methods via code only to find them all have the same issue. So the short question is, how to undo the roll caused by pitch and yaw or to prevent the roll to begin with? All I'm doing in code is saying "pitch x amount" and then later saying "yaw y amount" in a particular direction for a random duration before randomly picking a new direction. \$\endgroup\$ – CodeMonkey Aug 13 '18 at 13:54
  • \$\begingroup\$ Which way are your segments oriented in their local space? (Not world space) Ie. if you add a marker object as a child of the bone, and move it "downstream" toward the direction of the next child bone, or toward the "top" of the segment, how do its local coordinates change? \$\endgroup\$ – DMGregory Aug 16 '18 at 17:19
  • \$\begingroup\$ Initially, all the bones are pointing in a straight positive Z direction. So moving it down the bone chain would increase the global value of z (or local y) by the length of the bone. This would be the same as moving it toward its local "up" vector which is generally "forward" in Unity space. Blender and Unity just "had" to make "up" a different axis :-P. Still we should be able to just do the math noted above regardless of what the vector is called. I will know to use "up" instead of "forward". Rotate on axis A to get axis B to alter it's second element Y from 0.2 to 0. \$\endgroup\$ – CodeMonkey Aug 16 '18 at 17:25
  • \$\begingroup\$ I think we're talking past each other. If you really can't share any images of your actual setup, what about making a small toy example with cube assets that uses the same axis conventions? A picture is worth a thousand words, as they say, and especially with spatial/graphical problems it's easy for verbal descriptions to get convoluted. \$\endgroup\$ – DMGregory Aug 16 '18 at 17:29
1
\$\begingroup\$

When you perform a local yaw or pitch in one frame, then a local pitch or yaw in the next frame, the earlier rotation moves the axes on which the next rotation happens, which can make the results different than expected.

There are a couple common ways to fix this:

  1. Pitch locally, yaw globally

    transform.Rotate(pitchIncrement, 0, 0, Space.Local);
    transform.Rotate(0, yawIncrement, 0, Space.World);

    This keeps the yaw axis fixed in world space, so it can't stray sideways and cause a roll.

    It's also equivalent to totalling up your pitch and yaw and applying them from scratch in a single operation:

    transform.localEulerAngles = new Vector3(totalPitch, totalYaw, 0);

  2. Using basis vectors

    Keep track of the forward direction that you want your bone to point, then construct an orientation that points in that direction, while keeping the local up vector vertical.

    Vector3 forward = new Vector3(leftRightRandomization, upDownRanfomization, 1);
    Vector3 worldForward = parentRotation * forward;
    transform.rotation = Quaternion.LookRotation(worldForward);

    You can also pass a different up vector as the second argument to LookRotation if you want a different twist.

\$\endgroup\$
  • \$\begingroup\$ I've actually tried the first method previously and it works great for one bone individually, but once I start doing it for all the bones, it begins getting wonky again because the global axis is no longer correct for its new orientation given the previous bone's rotation causing it to move much like a translation. Part 2 I've also tried, but I don't really know what "up" is, per se. If it's pitched 45 degrees, "up" is a 45 degree vector, if it rolls a little, up is in a strange orientation. How do I know how to get it back to the 45-degree "up" that it should be? \$\endgroup\$ – CodeMonkey Aug 13 '18 at 18:44
  • \$\begingroup\$ This is why I asked to see a sample of your code or setup, so that we can be more specific about adapting these general principles to your use case. \$\endgroup\$ – DMGregory Aug 13 '18 at 18:46
  • \$\begingroup\$ Unfortunately, I can't share, but imagine there is a line of spikes on the top of a worm with 5 bones controlling it. Bone 1 pitchs up which will lift bones 2-5 as well and bone 3 might then pitch down which makes 4 and 5 move down from the new higher position pivoted at bone 3. Add some yaw in there for each bone independently and I want to somehow ensure that all spikes on the top of the worm are entirely... not rolled... The spike might not point to true "up" but it should not point left or right with respect to where the bone is currently pointed given its pitch and yaw. Make sense? \$\endgroup\$ – CodeMonkey Aug 14 '18 at 12:28
  • \$\begingroup\$ It looks like using the basis vector approach, and transforming your forward vector by the parent's rotation (but keeping the "up" fixed in world space) would do the trick. See the edit in point 2 (substitute an identity quaternion for parentRotation if you have a segment with no parent). \$\endgroup\$ – DMGregory Aug 15 '18 at 13:24
  • \$\begingroup\$ I've determined that "forward" is initially global negative Y (usually -up), right is positive X and up is positive Z (usually forward). So I want to keep "rolling" on its local Y axis (its 'up') until its local X axis has no GLOBAL Y (up/down) direction. So if my "right" direction on a global scale is currently (0.9, 0.2, -0.4) then the 0.2 is a problem. If my up/y axis is (-0.3, -0.2, -0.9), how much do I rotate on its local Y to make the 0.2 go to zero? Some kinda crazy math equation, I presume? I really hope that makes sense... \$\endgroup\$ – CodeMonkey Aug 15 '18 at 22:23
0
\$\begingroup\$

What you are experiencing is Gimbal Lock this is usually 'fixed' by Quaternions.

Unity does use them, so consider building your rotations with AngleAxis then apply them to your transforms.

Eg. bone.Transform * Quaternion.AngleAxis(30, Vector3.Up) bone.Transform * Quaternion.AngleAxis(30, Vector3.Left)

\$\endgroup\$
  • \$\begingroup\$ Gimbal lock is a situation where a rotational system loses a degree of freedom (ie. able to look up/down and roll, but not turn left/right), which does not match OP's description, so I don't think gimbal lock is the problem here. Instead, I think OP refers to the phenomenon where compounded pitch and yaw rotations can become roll rotations. This is not due to the Euler/quaternion representation, and is intrinsic to the space of 3D rotations themselves. So doing the same sequence of rotations with quaternions will not resolve the issue. \$\endgroup\$ – DMGregory Aug 13 '18 at 13:14
  • \$\begingroup\$ @DMGregory you're right, reading through your post reminded me of the rest of the rotation work we did in the engine. Namely storing the rotation as separate values and applying them to the transform matrix similar to the total pitch/yaw. \$\endgroup\$ – Skibisky Aug 13 '18 at 14:16
0
\$\begingroup\$

To "unroll" a bone that is pointing in the desired vector F (for forward) with the 90-degree vector R (for right) which needs the rolling (to avoid any upward or Y-value direction), the following math can be applied to find the degrees D to roll to achieve the flattened Goal vector G which R will align with after rotating about F.

// Solving for Goal Vector Values
// The angle at which the Goal vector G would cross the global axis of the 
// desired plane as a function of being perpendicular to F. The global axis
// (a straight line) is 180 degrees. F to G is 90 degrees of that, F to the 
// axis is a portion we can calculate, and with that we can know G's degrees 
// off the axis by subtracting the other two angles from 180. 
G.angle = 90 - Atan( F.z / F.x ) // Using Output as Degrees

// With the angle from the global axis and knowing we want zero rise, we can 
// calculate G's vector values themselves.
G.x = Sin(G.angle) // Assumes normalized vector whose length is 1
G.z = Cos(G.angle)
G.y = 0 // Of course

// The Cosine of the angle between R and G is now the dot product of R and G 
dProd = R.x * G.x + R.y * G.y + R.z * G.z
D = ArcCos(dProd) // Degrees to roll around vector F.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.