Install

HypeHype Animation Devlog #1 by Christian Jäger

I’m happy to report that the HypeHype animation tech will be receiving a long-overdue update, and in this post I’ll share some of the groundwork that me and the team have been doing!

The animation tech in HypeHype is fairly rudimentary. Basically, it’s possible to play a single skeletal animation on a character. There’s been a long list of feature requests piling up, but given the scope of the entire HypeHype app and high-prio tasks, animation needed to take a back seat.

Finally, however, we can make a business case for better animation tech: There are internal projects like the Hypet (an omni-present virtual companion) and in-house games that can only fullfill their vision with higher fidelity animation. On top of that, the demand for UGC content on the platform is growing, so we have to invest in the asset import pipeline and retargeting for animation reuse.

There’s lots to do, but taking over a legacy part of the system obviously requires some work in the beginning to lay down a solid foundation. In the following, I give a small peek behind the curtain of what went into the last update.

HypeHype is a live service with tons of user generated content that we must not break, so every change needs to be planned carefully and often requires programmatic patching. I’ll skip over patching here because it’s quite painful and the wounds are still fresh.

In no particular order, lets start talking about animation!

Skeleton Visualization

The very first thing I added was a debug visualization for skeletons, which is absolutely crucial for dealing with any kind of animation issue.

The job of the animation system is to produce skeleton poses, and if something does go wrong, a completely bent-out-of-shape skinned mesh leaves no clue to what has happened to the skeleton. Especially in normal in-game lighting conditions. Therefore, having a simple skeleton representation separate from the skinned mesh is absolutely mandatory for development.

hh_anim_skelviz.jpg

The skeleton visualization itself can be quite simple, because the only information we must have is the skeleton hierarchy (simple lines indicating parent-child relation) and the joint transformations (coordinate systems with three axes).

A skeleton visualization also helps separating animation issues from skinning issues. For example, we had one issue where the mesh importer tampered with skinning weights and breaking the mesh, even though the animation played fine.

hh_anim_pitstop.jpg

A good debug visualization also reveals issues you weren't even aware of. One curious find was a racing game Hype in which the racing car carries around with it a fully animated but invisible pitstop crew! That's obviously bad for performance and something that the animation system needs to deal with in a UGC platform.

thumbnail background
Open Wheel Sim 2•HypeRing Time Challenge thumbnail

Importer Fixes

HypeHype ships with an animation importer for FBX files but the process isn’t documented anywhere and depends on certain naming conventions to tie everything together. The bottom line is that animation importing is only used internally, so all animations you find in the asset store are made in-house or by contractors.

The goal is, naturally, to make the animation import pipeline available for all creators.

As a first step, I did some bugfixing in the FBX importer because some assets did not show up as authored in Blender. A dependable importer is the first step in a robust animation pipeline.

Asset importers are notoriously finicky because all DCC tools use different convenients to represent the data. Flexible interchange formats like FBX place the burden on the importer rather than the exporter (GLTF luckily does the opposite).

My best advice is to treat all coordinate system transformations in a systematic way. The source of great sorrow in importer code is a bunch of ad-hoc coordinate shuffles and sign flips.

A great tool for this is the concept of a change of basis. I leave a link here to Fabian Giesen's blog for a practical introduction.

Retargeting

Hypes feature a wide range of goofy character with different proportions and limbs. We want to reuse animations as much as possible but playing an animation on a rig that’s different from the one that the animation was authored may give bad results. Differences in proportions make feet sink into the ground or arms to penetrate. There’s no silver bullet, but some animations can be retargeted to new rigs. In HypeHype, a user could enter a manual scaling number when importing a new rig, but obviously manually entering a relative scale is not ideal.

What I added is that an animation now stores the rig it was authored with so we can retarget animations automatically by matching joint proportions. It’s essentially Unreal's AnimationScaled option. In the GIF below you see on the left how a playing the same animation on every pet would pull the head into the neck on smaller pets, whereas a retargeted animation reduced the travel of the head.

retargeting.gif

Existing animations were matched with skeletons on a best effort basis during a mass patching of our asset lib and existing hypes.

In the future, we want to add more advanced retargeting with IK so that we can preserve animations more accurately and support a wider range of rigs that creators can use.

Runtime data structures

Skeleton are hierarchies are tree-like structures where each joint can have a number of child joints. The old HypeHype code, therefore, stored skeletons as vectors-of-vectors:

<pre>
<code style='tab-size: 4; font-size: 0.8em; line-height: 1.2'>
<span style='color: red'>struct</span> BoneJoint {
<span style='color: darkorange'>uint8_t</span> m_id;
BoneJoint* m_parent;
<span style='color: orange'>std::vector</span>&lt;BoneJoint&gt; m_children;
<span style='color: gray'>// ...</span>
};
</code>
</pre>

There several problems with this kind of data layout regarding performance and memory overhead. First of all, small vectors with data scattered in memory is not good for cache performance.

Another point to consider is cache utilization. Addressing a parent bone with an 8-byte pointers is wasteful when we can count joints with smaller integers (16-bit is typically more than enough). Also consider that every vector stores typically a size and a capacity on top of a pointer.

Keep in mind that having a recursive data structure also means that most algorithms need to be written recursively. In our case, this means lots of short loops over children and function calls. As an example, the previous bone lookup by ID was a recursive tree traversal.

As a side note I’d like to add that I’m a fan of Scheme, OCaml, and functional programming in general, but in C++ land the data structure of choice is almost always a simple array (or vector).

I refactored the code to this new layout:

<pre>
<code style='tab-size: 4; font-size: 0.8em; line-height: 1.2'>
<span style='color: red'>struct</span> BoneTree {
<span style='color: darkorange'>uint16_t</span>* m_parentIndices;
SimdPR* m_inverseBindPose;
SimdPR* m_bindPose;
SimdVec m_retargetScale; <span style='color: gray'>// deprecated!
</span> <span style='color: darkorange'>uint16_t</span> m_boneCount;

<span style='color: darkorange'>char</span>* m_nameBuffer;
<span style='color: darkorange'>uint16_t</span> m_nameBufferLength;

<span style='color: darkorange'>uint32_t</span>* m_nameHashes; <span style='color: gray'>// Not serialized. Computed after load
</span>};
</code>
</pre>

Data is now stored in flat arrays and the bone indices are implicit. The parent-child relationship is preserved in the parentIndices array, but also the arrays are sorted such that parents come before children, so that the pose transforms can be accumulated in a single loop.

Speaking of transforms, I added SIMD-ified math types to the code base, which the animation code uses throughout (SimdPR stores a position and a rotation as a vector and quarternion).

I'm planning to follow-up with an article specifically about perf, backing up the design decisions with data. In the meantime, for more information about processing hierarchical data structures in a data-oriented way, check out Stefan Reinalter’s excellent post here.

Looking Ahead

You’ve now read about some of the foundational work but rest assured that we are cooking at HypeHype! Animation is a multi-disciplinary effort between engineering and art and when it comes together just right it’s very exciting! In the meantime, go make some Hypes!

--

Written by: Christian Jäger | Senior Software Engineer | HypeHype