Blending Characters At Runtime

Here's a script guide designed to help you blend between multiple characters at runtime.

Blending Between Avatars (at Runtime)

If you are exposing the ability for users of your application to morph between two (or more) characters at runtime, it is essential to understand how to blend between avatars. Each avatar contains a set of definitions, such as how long each bone is, and the maximum extents allowed on each muscle group.

Creating new avatars at runtime is complex, and blending between two existing ones can be a substantially simpler and more reliable process. It is worth noting, however, that Unity generally does not like modifying Mecanim internals at runtime, and here be dragons.

This sample code only works in Unity 2019.1 or newer. Earlier versions lack the required Avatar.humanDescription property.

    static class LerpHumanoid
    {
        public static Avatar LerpAvatars(GameObject targetHierarchy, Avatar a, Avatar b, float t)
        {
            HumanDescription aH = default, bH = default;
#if UNITY_2019_1_OR_NEWER
            aH = a.humanDescription;
            bH = b.humanDescription;
#else
            throw new InvalidOperationException("This function requires Unity 2019.1 or newer.");
#endif
            var humanDescription = new HumanDescription
            {
                armStretch = Mathf.Lerp(aH.armStretch, bH.armStretch, t),
                feetSpacing = Mathf.Lerp(aH.feetSpacing, bH.feetSpacing, t),
                hasTranslationDoF = t > 0.5f ? bH.hasTranslationDoF : aH.hasTranslationDoF,
                legStretch = Mathf.Lerp(aH.legStretch, bH.legStretch, t),
                lowerArmTwist = Mathf.Lerp(aH.lowerArmTwist, bH.lowerArmTwist, t),
                upperArmTwist = Mathf.Lerp(aH.upperArmTwist, bH.upperArmTwist, t),
                lowerLegTwist = Mathf.Lerp(aH.lowerLegTwist, bH.lowerLegTwist, t),
                upperLegTwist = Mathf.Lerp(aH.upperLegTwist, bH.upperLegTwist, t)
            };

            var aHSkeleton = aH.skeleton;
            var aHHuman = aH.human;
            var bHSkeleton = bH.skeleton;
            var bHHuman = bH.human;

            var skeletonBones = new SkeletonBone[aHSkeleton.Length];
            var humanBones = new HumanBone[aHHuman.Length];

            for (int i = 0; i < humanDescription.skeleton.Length; i++)
            {
                if (aHSkeleton[i].name != bHSkeleton[i].name)
                {
                    throw new InvalidOperationException("Skeleton arrays do not match, were these produced from the same original skeleton?");
                }

                var skelement = new SkeletonBone();
                skelement.rotation = Quaternion.Lerp(aHSkeleton[i].rotation, bHSkeleton[i].rotation, t);
                skelement.position = Vector3.Lerp(aHSkeleton[i].position, bHSkeleton[i].position, t);
                skelement.scale = Vector3.Lerp(aHSkeleton[i].scale, bHSkeleton[i].scale, t);
                skelement.name = aHSkeleton[i].name;
                
                skeletonBones[i] = skelement;
            }

            for(int i = 0; i < humanDescription.human.Length; i++)
            {
                if (aHHuman[i].boneName != bHHuman[i].boneName)
                {
                    throw new InvalidOperationException("Skeleton arrays do not match, were these produced from the same original skeleton?");
                }

                var bone = new HumanBone
                {
                    boneName = aHHuman[i].boneName,
                    humanName = aHHuman[i].humanName,
                    limit = new HumanLimit
                    {
                        max = Vector3.Lerp(aHHuman[i].limit.max, bHHuman[i].limit.max, t),
                        axisLength = Mathf.Lerp(aHHuman[i].limit.axisLength, bHHuman[i].limit.axisLength, t),
                        center = Vector3.Lerp(aHHuman[i].limit.center, bHHuman[i].limit.center, t),
                        min = Vector3.Lerp(aHHuman[i].limit.min, bHHuman[i].limit.min, t),
                        useDefaultValues = t > 0.5f ? aHHuman[i].limit.useDefaultValues : bHHuman[i].limit.useDefaultValues
                    }
                };

                humanBones[i] = bone;
            }

            humanDescription.skeleton = skeletonBones;
            humanDescription.human = humanBones;

            return AvatarBuilder.BuildHumanAvatar(targetHierarchy, humanDescription);
        }
    }

To use this code, simply pass your two avatars into the function and use the generated result on your Animator.

Important note: The Animator needs to be reset each time you replace the avatar. The easiest way to do this is simply to delete and regenerate the component, but if this is not possible, callingAnimator.Rebind()will work too.

When you lerp the avatar, it is also important that you modify all the blend shapes for the character by equal measure. You can find the names of the blend shapes you want to edit using the Index visible in the SkinnedMeshRenderer property, or alternatively via skinnedMeshRenderer.sharedMesh.GetBlendShapeName()

See Synchronizing Character & Clothing Morphs for a script that you can possibly use for that task.

Last updated