I wrote previously about how I had made a native Unity importer for Milkshape 3D model files — well, half of one. I managed to import geometry, beautifully and reliably, but I failed to get animations working; mainly because Unity’s animation systems are utterly opaque and incredibly poorly documented. No worries, I thought, direct Milkshape geometry importing is still a really useful addition and has saved me a lot of pain and annoyance over the months since I installed it.
But I am, if nothing else, a dreamer. I just knew that getting all the way woud be amazing, and so I decided it was time to try again. Those animations will go to the ball.
My Milkshape Brings All the Models to the Unity (Part II)
I actually did all the groundwork for animations the first time around. For all I only managed to get meshes out the other end, I did still read in the joint structures and their animation keyframe data. I got as far as converting the Milkshape joints into GameObjects, all positioned and parented correctly to create the skeleton, but applying the animations on top eluded me.
The situation is painfully convoluted. You need to make an “Avatar”. You need to make an AnimationClip. AnimationClips need AnimationCurves. AnimationCurves need keyframes. Then you need to go back to the mesh and assign “bind poses” for each bone.
If all of that sounds like meaningless technobabble, then yes, most of it is. Creating an Avatar is a one-line method call, and while an Avatar is apparently vital it doesn’t actually seem to do anything. AnimationCurves are linked to objects by a relative path, but there is no information about what that path must be relative to. And what the ever-living fuck is a bind pose?
Get any one of these linkages wrong, or one step out of sequence, and your model will either not animate at all, animate only in the preview panel but not the game itself, or wig out in mildly amusing ways. There is no real feedback and the docs are useless.
I eventually put two copies of the same model side by side: one as the real MS3D file going through my importer, and one as an exported FBX file going through their importer. Even though the Animator/Avatar/AnimationClip/SkinnedMeshRenderer system is completely opaque, having a baseline for comparison helped me to narrow down when I actually did have a particular combination of properties and links right.
After all that, it seems like you need to give the AvatarBuilder a root object that already has an Animator on it — the documentation and the API do not explain this in any way, I only spotted it from the tiny little code sample they have. I think all the AnimationCurve binding paths are relative to the root of the Avatar. And bind poses seem to be the joint transform’s worldToLocal matrix * the root transform’s localToWorld matrix… whatever that means.
Okay, fine, once I got all that lined up, it turned out I had actually made a few boo-boos with the animation data.
Milkshape doesn’t store rotation angles in degrees like I thought, so I had to paste in the msViewer’s quaternion converter to feed them through (it’s probably radians, but I don’t care to dig further now that it works).
Translation was also hilarious, because movement is relative to the starting position of each joint — not the origin or the parent! Since I have lots of keyframes with “no” movement (i.e. start at zero, move somewhere else, then back to zero), it was snapping every joint to the origin instead of leaving them in their expected positions. I had to adjust each translation key to be an offset from its parent joint’s position to fix that. Gosh this stuff gets confusing fast.
For a while there I was getting excited that maybe I’d be able to make all of this work at runtime, and thus open the doors of true moddability, but alas… While most of the logic on show here is absolutely doable in a shipped game, things break down the closer you get to Unity’s “Mecanim” systems. The alternative is to migrate (back) to the “legacy” animation system, but, well, given it’s called “legacy” it sounds likely to get deleted eventually so that’s not exactly a safe option. Mind you, I barely scratch the surface of Mecanim’s capabilities and indeed fight against some of them, so it’s not as awful an approach as it sounds at first.
Or, I could go absolutely crazy and try to build my own animation system. It’s just linearly interpolating between keyframes, how hard could it be? (Famous last words.) A package called Animancer has also been suggested, which might be a more reasonable way forward.
I am absolutely not committing to making Exon moddable, but it is slightly frustrating to be able to construct so much of a model at runtime, only to be stopped by a handful of editor-only functions in the animation control system. Everything else that I’ve built is primed for runtime usage — Terragne can theoretically generate landscapes entirely in-game, A* Pathfinding can scan at runtime, and there’s nothing explicitly offline about my Trigger system either. It used to seem impossible that I could build a level editor but it’s starting to look more and more viable — I even manage most of these tools through custom user interfaces, so why not build them in-game instead of as editor windows?
So now that I have everything in, what’s next?
Well, it’s currently quite a laborious process to turn a Milkshape model into a “unit”. It’s not difficult and it’s basically rote — separate meshes get cut up into chunks that make up a mech’s “costume”, the animation timeline is chopped up into sequences, selection bounds are added, and so on. I am working on baking the configuration required for this stuff into the Milkshape file’s comments (or at least the ScriptedImporter properties), so that a model can come in as a fully formed and functional unit. Initial trials have been positive, which has got my mind racing — what other templated stuff could I store as external files to be “imported” as fully-functional objects? Weapons? Unit types? Woah.
In the meantime, maybe I should just publish this importer script. There might not be any other Milkshapers left alive out there, but even so, it could be a good reference for people to make their own importers for other assets. Sell it on the Asset Store? Eugh.