The character in WOTT is a humanoid, which provides a number of advantages. One is that animations don’t have to share the same rig as the final character. That gives me the flexibility to make changes to the rig without having to remake previous animations, and also provides the ability to create animations using a variety of software packages if needed.
Another big advantage is that humanoid animations can be mirrored in Unity with a single checkbox. This halves the animation workload for unilateral, or Left/Right exercises which is important because there will eventually be thousands of exercise animations in WOTT.
With so many animations, it’s important that the users only download the animations they need for the routines they use – there’s no point downloading or storing animations they don’t use. To accomplish this I needed a way to create animation graphs on the fly which can’t be done with Unity’s Mecanim
animation system. The only way to do this with the necessary flexibility is to use AnimationPlayables
. So I needed to create my own animation system.
I originally had a month scheduled to create the animation system in WOTT, after which I had a month scheduled to create the database and backend. After a month I didn’t get everything done that I had planned, but I was happy enough with where the animation system was.
The fundamental elements of WOTT’s animation system are the ability to create downloadable animations, and then plug them into an animation graph when needed. Completing the animation system was always a long term project, but for now I just needed those fundamentals working so I could create exercise animations while I worked on the backend. Then, in a month, once the backend was done I could continue working on the animation system.
There were just two problems with that plan. The first was finding the time to create animations while trying to get the backend working as quickly as possible – two competing goals that the backend usually won. The second problem kind of snuck up on me around the time I felt like the backend was getting close to being complete, 5 months after starting work on it.
The backend took way longer than I’d anticipated, or scheduled, and I couldn’t just abandon it the way I’d done with the animation system because it is so important to the core functionality of the app. Then there was public testing and, long story short, nearly a year after I abandoned the animation system I started animating a unilateral move before realizing I hadn’t added support for mirrored animations. I’d thought about it a lot, but hadn’t actually done anything about it. So instead of creating animations, it was time to add support for mirrored animations.
Thanks to Unity’s built in support for mirroring humanoid animation, it didn’t take too long to add that to the animation system. Unfortunately using Animation Playables instead of Mecanim means that I can’t just set a clip to be mirrored at run-time, so I need to include mirrored clips in the data. Then it’s just a matter of deciding which variation to use. I realized that also needed to add another type of animation, to swap from left to right without leaving the exercise (in case there is little or no rest between left/right sets, or for intermittent left/right reps).
For animation that doesn’t need IK, I’m done. This is working great. But lots of animations need IK so the next step was to mirror the animation of the IK targets.
The IK targets are an odd hierarchy to support future character customization so it wasn’t as easy as it would be if all the IK targets had the same parent.
First I tried swapping values between the left and right sides, reversing the relevant channels. That didn’t work because, as I realized, their starting rotations aren’t mirrored which means, because it’s a hierarchy, their starting positions aren’t either.
Then I thought if I record the starting position of each target, then get the vector of change from the starting position to the current position, I could use the mirror of that vector to add to the opposite target. Unfortunately that didn’t work either. It was close, but the rotations still weren’t right, and it was getting complex trying to manage the parent/child relationships.
I realized that a solution that I’d been avoiding, because it seemed to have more elements, would actually be much simpler and completely avoid the issues with hierarchy. Here’s what I ended up with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
public class Mirror : MonoBehaviour { // The parent of the hierarchy public Transform ikBase; // The following fields should be arrays if // you have more than one object on each side // The transforms you want to mirror public Transform targetLT, targetRT; // These hold the temporary mirrored values before applying // them to the transforms. See the Trans class below. MiniTransform tempLT, tempRT; // These hold the difference between a transform's mirrored // value and the actual rotation of it's opposite partner. If your // left and right sides are mirrored to start with, you don't need these. Quaternion LTOffset, RTOffset; // Use this for initialization void Start () { // Calculate the offsets // This is the formula for calculating the offset // because with Quaternions, the order matters. // Left = Right * Offset // Offset = Right(inv) * Left // Calculate Left side offset Quaternion q = targetLT.rotation; // To mirror a Quaternion, reverse the values corresponding // to the vectors of the plane you want to mirror across. // In this case we're mirroring along the x axis, across the yz plane q.y *= -1; q.z *= -1; // Apply the formula LTOffset = Quaternion.Inverse(q) * targetRT.rotation; // Do the same for the right side q = targetRT.rotation; q.y *= -1; q.z *= -1; RTOffset = Quaternion.Inverse(q) * targetLT.rotation; } // LateUpdate occurs after animation, but before IK void LateUpdate () { // Remove the parent transform values // This gets the transform values as if the parent // is at (0,0,0) so it works anywhere in the scene tempLT.position = ikBase.InverseTransformPoint(targetLT.position); tempRT.position = ikBase.InverseTransformPoint(targetRT.position); tempLT.rotation = Quaternion.Inverse(ikBase.rotation) * targetLT.rotation; tempRT.rotation = Quaternion.Inverse(ikBase.rotation) * targetRT.rotation; // Mirror the values across the x axis tempLT.position = new Vector3(-tempLT.position.x, tempLT.position.y, tempLT.position.z); tempRT.position = new Vector3(-tempRT.position.x, tempRT.position.y, tempRT.position.z); tempLT.rotation.y *= -1; tempLT.rotation.z *= -1; tempRT.rotation.y *= -1; tempRT.rotation.z *= -1; // Apply the offsets using our formula: // Left = Right * Offset (Offset * Right will give a different result) tempLT.rotation = tempLT.rotation * LTOffset; tempRT.rotation = tempRT.rotation * RTOffset; // Restore the parent transform values // This sets the world position to where it // would be if it was a child of ikBase tempLT.position = ikBase.TransformPoint(tempLT.position); tempRT.position = ikBase.TransformPoint(tempRT.position); tempLT.rotation = ikBase.rotation * tempLT.rotation; tempRT.rotation = ikBase.rotation * tempRT.rotation; // With a hierarchy of transforms, make sure you calculate // all the positions (above) in one pass, and then apply those // positions to the transforms (below) in a second pass // Apply mirrored positions targetLT.position = tempRT.position; targetRT.position = tempLT.position; // Apply mirrored rotations targetLT.rotation = tempRT.rotation; targetRT.rotation = tempLT.rotation; } } // This just gives us a single object to store temporary position and rotation values public class MiniTransform { public Vector3 position; public Quaternion rotation; } |
This is just my test, but extended to arrays of transforms it works to mirror any hierarchy, anywhere in the scene. It does this by moving each transform into the equivalent of local space, mirroring along the x axis, then moving the transform back to it’s parent space.
You can test this code by only applying the mirrored position/rotation to the left side. Then you can move the right side to see the left object mirror the motion.
You could also store the offsets as MiniTransforms
which would allow you to offset position and rotation.
It took me a while to wrap my head around all this. My first pass worked perfectly when the character rotation was (0,0,0), but broke when the character turned. That prompted me to learn what Transform.TransformPoint
and Transform.InverseTransformPoint
are for.
There was also some trial and error involved in getting the right order for the Quaternion operations. Speaking of which, I should also note that there seems to be two ways to mirror quaternions, complex and simple (mathematicians probably call these correct and incorrect). This uses the simple way, because we’re mirroring along a world axis. If you need to mirror across an arbitrary plane, you need to use the more complex method which is discussed in this forum thread.
I’m looking forward to updating to Unity 2018 so I can hopefully make all this a whole lot more efficient, but for now I can mirror animations whether they use IK or not, which is awesome.