KybernetikGames / animancer

Documentation for the Animancer Unity Plugin.
63 stars 8 forks source link

Help Needed with Character Animation Blending in Unity with animancer #328

Closed PixelFireXY closed 1 month ago

PixelFireXY commented 6 months ago

Hi,

I'm facing a challenge in a Unity project involving a humanoid character with separate animation layers using Mixers and AnimancerLayer, and I'm hoping to find some insights here.

My character uses two distinct animation layers. One layer[0] controls the whole body for movements like walking and running. The other layer manages the upper body for rifle-related actions, such as aiming and shooting.

During movement sequences, the character's upper body displays an odd wiggling or swaying motion. It seems as if the upper body animation is overly influenced by the hip rotations of the lower body, leading to an unrealistic walking appearance.

What I've Done So Far:

The individual animations look normal when played independently. I've played around with various avatar mask settings in Unity but haven't achieved a satisfactory resolution.

Are there specific techniques or settings within Unity, particularly with Animancer, that could enhance the blending of these two animation types?

https://www.youtube.com/watch?v=OVWUlojrwKc

Thanks in advance! Matt

KybernetikGames commented 6 months ago

Layer blending is handled internally by Unity so you should see the same result as if you played those animations in an Animator Controller.

The only way to get more control over the blending than the Layer Mask would be to use a Weighted Mask Mixer which lets you weight each bone individually. Or you could modify it to blend differently if you can come up with a blending calculation that suits you better.

PixelFireXY commented 6 months ago

Thank you I will try the weighted mixer after work. I was thinking also the IK because I will use it for the foot and for the hand grip of the rifle. Do you think this could solve the problem too?

PixelFireXY commented 6 months ago

It works very nicely, the problem seems solved.

I receive lots of errors for some reason but besides that, it's working.

Errors: image

Result: https://youtu.be/IyxWilB5Aks

KybernetikGames commented 6 months ago

I'm not sure how it could be working with those exceptions since they should break it entirely.

Perhaps you have a second copy of that script in the scene which isn't configured correctly?

PixelFireXY commented 6 months ago

No, just the script example that was together in the pack. I tried more things:

image

In the image you can see the bones with no weight are the ones from the legs to the feet and as element 0 there is the pelvis to prevent the wiggling thing.

What I noticed is that there are lots of warnings for some reason:

image

Just in case here's the full model skeleton: image

KybernetikGames commented 6 months ago

Are you using a Humanoid Rig and only getting those warnings from bones which aren't mapped to the avatar definition? It could be that the warning from Unity is misleading and the usable bones actually need to be part of the avatar to be used, not just children in the hierarchy.

PixelFireXY commented 6 months ago

I probably found the problem. Turns out that the object inside the character hierarchy cannot be moved where I want.

This is my character prefab: image

This is the fbx: image

A time ago I reordered the objects inside the model to make it tidier but apparently, it is not allowed. Now the errors are gone.

source: Unity forum

I will make more tests between today and tomorrow but the problem should be solved thanks.

KybernetikGames commented 6 months ago

Yes, that's very likely the source of the problem.

If you want to modify the hierarchy you could try using the FBX Exporter to re-save it with the modifications so Unity doesn't see it as modified. Otherwise, you'd need to bring it into a modelling program like Blender.

PixelFireXY commented 6 months ago

Hi, I made a couple of "stress tests" to try different setups and I came up with this that works like a charm. I hope it will be useful for you in the future if you need it.

  1. First, I set the 'movement' animations on layer 0 as I had previously with the AnimancerLayer and the 'combat' animations on layer 1
  2. After that I assigned all the lower body bones at 0 and 0.7 weight to the pelvis in the TransformWeight array
  3. Everything worked decently except that now the pivot was on the hips for some reason and the character had lots of foot sliding also when it crouched the lower body moved toward the pivot resulting in having a flying character.
  4. After that I tried different setups and what worked was to assign the 'combat' layer to layer 0 and the 'movement' layer to layer 1.
  5. Also, instead of assigning the lower part as I did previously to the array I assigned all the upper parts and set the weight to 0.
  6. The result was even better than expected because the animation was organic and realistic.
  7. The problem now was that the transition was snappy so I made a fade between 0 and 1
  8. Also because there are 44 bones to edit the weights I used a dictionary to cache each one and change it when I needed it

This is the result https://youtu.be/e-GGpji-YjM

private Coroutine smoothCoroutine;

// Call this in awake
private void InitializeBoneIndexCache()
    {
        boneIndexCache = new Dictionary<Transform, int>();

        for (int i = 0; i < boneWeights.Length; i++)
        {
            var bone = boneWeights[i].transform;
            var index = weightedAnimancerLayers.IndexOf(bone);
            if (index >= 0)
            {
                boneIndexCache[bone] = index;
            }
        }
    }

// This is called from the states
    public void StartSmoothTransition(float targetWeight, float transitionDuration)
    {
        if (smoothCoroutine != null)
            StopCoroutine(smoothCoroutine);

        smoothCoroutine = StartCoroutine(SmoothTransitionWeights(targetWeight, transitionDuration));
    }

// Used to smooth transition
    private IEnumerator SmoothTransitionWeights(float targetWeight, float transitionDuration)
    {
        // Directly access the BoneWeights once before the conditional checks and loops
        var weights = weightedAnimancerLayers.BoneWeights;

        if (transitionDuration <= 0)
        {
            // If transitionDuration is 0 or negative, set weights instantly.
            for (var i = 0; i < boneWeights.Length; ++i)
            {
                if (boneIndexCache.TryGetValue(boneWeights[i].transform, out var index))
                {
                    weights[index] = targetWeight; // Use local reference to assign the new weight
                    boneWeights[i].weight = targetWeight; // Assuming you want to update this for consistency.
                }
            }
        }
        else
        {
            // Prepare for a smooth transition.
            float elapsedTime = 0;

            float[] initialWeights = new float[boneWeights.Length];
            for (int i = 0; i < boneWeights.Length; i++)
            {
                if (boneIndexCache.TryGetValue(boneWeights[i].transform, out var index))
                {
                    initialWeights[i] = weights[index]; // Use the local reference for initial weights
                }
            }

            while (elapsedTime < transitionDuration)
            {
                elapsedTime += Time.deltaTime;
                float fraction = elapsedTime / transitionDuration; // Calculate fraction of the transition completed.

                for (var i = 0; i < boneWeights.Length; ++i)
                {
                    if (boneIndexCache.TryGetValue(boneWeights[i].transform, out var index))
                    {
                        // Interpolate weight based on the fraction of the transition duration elapsed.
                        float newWeight = Mathf.Lerp(initialWeights[i], targetWeight, fraction);
                        weights[index] = newWeight; // Apply interpolated weight using the local reference

                        boneWeights[i].weight = newWeight; // Update local representation of the weight.
                    }
                }

                yield return null;
            }
        }

        // After transition (instant or smooth), ensure all weights are exactly at the target value.
        for (var i = 0; i < boneWeights.Length; ++i)
        {
            if (boneIndexCache.TryGetValue(boneWeights[i].transform, out var index))
            {
                weights[index] = targetWeight; // Use local reference to confirm final weight
                boneWeights[i].weight = targetWeight; // Ensure local consistency.
            }
        }
    }

It's a bit bummer that I had to assign the combat layer to layer 0 instead of the movement one but it works. Also, I hope editing 44 bones it's not too performance-heavy (I didn't measure it)

KybernetikGames commented 6 months ago

I wouldn't expect to see a significant performance hit from some simple blending calculations, especially if you don't have too many characters using it at once, but obviously measuring it would be the only way to know for sure.

Would you mind sending your full script(s) to animancer@kybernetik.com.au so I can see how you're using it overall? I'm considering adding the weighted mask mixer as a proper example in the next version of Animancer and it would be good to have a real use case to base it around.

PixelFireXY commented 6 months ago

Sure, I'll send you the files this weekend!

Also, I noticed that if I use the WeightedMaskLayerList, the IK Pass (OnAnimatorIK callback) is not called for some reason.

I'm trying to use the IK to point the character's chest toward the aim point (like in the IKPuppetLookTarget example) but the moment I set the WeightedMaskLayerList to the AnimancerComponent the character does not follow the point anymore (same in the Animancer example).

Tomorrow I'll try to play with the IK more, Maybe I have to use just the IK for everything... just guessing.

KybernetikGames commented 6 months ago

I'm not sure how the layer mixer could be able to interfere with IK because IK is enabled on each individual AnimationClipPlayable and shouldn't have anything to do with the mixer. You could also make a PlayableGraph without a layer mixer at all and IK would presumably still work (though I haven't ever tested it).

PixelFireXY commented 6 months ago

I sent the files. 👍

About the IK pass, I tried to enable /disable things and the OnAnimatorIK callback stop to be called immediately when I called those lines:

Immagine 2024-02-11 113037

I don't have so much knowledge about the Playable API unfortunately and I don't know how to fix it properly but thanks to this post: Unity forum I tried to add an empty Animator Controller with IK pass enabled on a layer and the callback magically works.

The only problem is I have lots of warnings and the pivot of the character is on the hips again.

image

image

https://youtu.be/KQget_53pH0

Could be a bug in Unity? Animancer? I don't know. Today I'll try full IK to try to fix it without the need for the WeightedMaskLayer, maybe I can find a workaround.

daenius commented 6 months ago

That is not a bug in Animancer nor Unity and it is by design when you are using Humanoid

If you want to directly change the Transform of bones inside a Humanoid you can so easily with either MuscleHandles or just the standard TransformStreamHandles in an IAnimationJob instead.

I do this myself in my work and luckily Animancer also offers the cleanest way (that I know of) to get that going, especially if you want to control the parameters you are feeding into the IAnimationJob from a UI: https://kybernetik.com.au/animancer/docs/examples/jobs/lean/

PixelFireXY commented 6 months ago

I see, thank you for the suggestion!

I tried to counter the hip rotation with a counter rotation using the Animancer job lean: lean example

But this is the result: Jojo pose

Before your suggestion, I tried also to use the IK to force a bone to 'point' in the right direction but with no good results.

How would you fix this posture problem? At this point maybe it's not to move the bone the real problem but to make it behave more naturally.

Keep in mind after this fix I will have to use IK to rotate the bone toward the aim point so I cannot freeze completely a bone but rather restrain one specific axis (left and right).

It's been 4 weeks I've been working on this, this is really frustrating T_T

Thanks!

KybernetikGames commented 1 month ago

Animancer v8.0 is now available for Alpha Testing. It includes a proper implementation of Weighted Mask Layers with a barebones undocumented example scene.

PixelFireXY commented 1 month ago

Thank you but I solved the problem by moving to Unreal.