A utility class to handle weighted blending of values, created for use in my transition to data-driven animations rather than hard-coded ones. A Blendable<T> is just a wrapper around a T value and a float weight with some convenience functions to allow blending between them and applying the value from a starting point (the default animation pose, if you will).

Background/History

I've posted in the past about my semi-procedural animations project. That project handles walking and jumping and such well. To allow attack animations, though, another key feature for my game, I ended up just hard-coding using some enumerations a few allowed attack animations:

enum class ATTACK {
    NONE = 0, /// Not attacking.
    SLASH, /// Slash attack, like a sword swipe.
    PUNCH, /// Punches.
    TACKLE, /// Bend body for tackle. Does not handle forward motion.
    KICK, /// Kicks.
    BITE, /// Bite motion.
    TAIL_WHIP, /// Attacks with tail. Probably involves spinning around.
    PROJECTILE, /// Launches a projectile, probably from mouth, maybe from cannon. Alternatively, forms a projectile near a raised hand, then throws it. Might be split to breath/beam_attack and throw_projectile.
    SPECIAL_AOE, /// Motion/Dance for invoking an Area of Effect attack.
    FACE_ENEMY /// Attacks that involve facing the enemy, such as eye based attacks like leer.
}

This approach isn't actually that bad, but it requires a not-insignificant change to the animation code to add a new animation, which is unreasonable if the animation is only used for a couple of the 'mon. Which is made worse by the fact that all of the animations tend to follow the same approach in how they handle actually applying the animation. They have a different Stride or two for the arms/legs involved, they Lerp (linear-interpolation) between the two leading into the attack and then out of the attack (so the weight/time in the Lerp is typically a triangle wave or the square root of one if I need it to linger near the new Stride longer). Additionally, they might apply some sort of tilt to the body or spine. And lastly they will drive the phase in the overall animation (i.e. the position in the Stride) following some pattern.

You can see from that description that this is a fairly good candidate for just writing one set of code that simply handles a few options do I have an arm stride or a leg stride or both, do I tilt the body and how much, and then how does the phase change as the time through the attack animation changes. So we make a CharacterIKParams structure to hold all of those values, and in general they will be stored as `std::optional values for all the 'do I' parts, and something else for the phase information.

BUT we want to be able to apply multiple animations at once! And these animations will probably need different weights as they progress, for example fading out one animation while fading in the other. And to do this, we need some way to blend between the different the different values in the CharacterIKParams structure. So basically, we have a float w for the weighting of the different animations. Which is fairly easy to handle you just Lerp(value a, value b, float w) and you get the blended value. Which works great with two definite values for the parameter: Animation A tilt's the body 30 degrees, and B tilt's it 10 degrees, so at 50% blending we get a tilt of 20 degrees. But how do we handle the case when Animation B does not adjust the tilt at all? This is the key concept of the Blendable.

Blendable: Optional with a Real rather than Boolean weight

optional\<T> can essentially be thought of as a structure with a T value and a bool weight, where weight=true means that we have the value and should replace the base one with the animation's override, and weight=false means that we don't have the value and should keep the base one. Blendable\<T> is an extension of this idea, allowing the weight to take on values between 0 (false) and 1 (true). This allows us to handle blending between two different animations, one of which does override the leg Stride and one of which does not, by adjusting the weight value between 0 and 1, and keeping only the Stride from the one that specified it (weight=1) and discarding the other (weight=0). And as a bonus, when we are already storing the weights for the different values, we can re-use that to apply the weighting for the animation itself (the triangle wave discussed above going to the new value and then back to the base).

How it Works

So, how does the math work out for this object? What are some test cases we can think of?

First, some notation: We will denote the blending between two values A and B with weight t as A..t..B (we could instead write it as the linear interpolation Lerp(A,B,t)). We will denote a Blendable, i.e. a value and a weight, as Blendable = Value:Weight. We will denote applying a Blendable A to a base value v as A@v.

As some test cases, we can take a few obvious examples to try and discern how the math should work:

  • a:0 .. t .. b:0 -> 0:0 Blending between two values with no weight will return a value with no weight
  • a:w .. t .. b:w -> a..t..b:w Blending between two values with the same weight will return the blending of the values with the same weight as the start.
  • a:0 .. t .. b:1 -> b:t Blending between a value with no weight and another value with weight 1 will return the other value with a weight equal to the blending parameter t.

I will add one more constraint to these cases before I share my answer to the problem: When we blend between two Blendables, and apply the result to the same base, we should get the same result as if we applied the two Blendables individual to the same base and then blended the result. In other words

  • (A..t..B)@v = (A@v)..t..(B@v).

From some experimenting in Python, I came up with this as the algorithm for the Blending operation that gave me the above results:

Blend(Blendable A=a:u,Blendable B=b:v,float t) = A..t..B
    w=Blend(u,v,t)
    if w:
        c=Blend(a*u/w,b*v/w,w)
    else:
        c=0
    return w:c

You can see the results of this algorithm in the following image. blendables

Previous Post Next Post