Skip to main content

Attaching Animated Clothing

Expected Output

User/player should be able to change clothing and then the clothing should be animated following the character they are attached to.

Key Problem

When attaching prefabs that need to use the same animations to the character (e.g clothing items), simply parenting the instantiated prefab into character wont make the prefab animated like the parent. Calling the same animation on the clothing using the same animation the character used might work, but it’s better to make the clothing use the same animation rig as the character.

Key Solution

Use the model stitching to parent the instantiated clothing prefabs to bind them to the character

GitHub - masterprompt/ModelStitching: Example of Unity SkinnedMesh stitching

The Stitcher.cs code

using UnityEngine;
using System.Collections.Generic;

public class Stitcher
{
    /// <summary>
    /// Stitch clothing onto an avatar.  Both clothing and avatar must be instantiated however clothing may be destroyed after.
    /// </summary>
    /// <param name="sourceClothing"></param>
    /// <param name="targetAvatar"></param>
    /// <returns>Newly created clothing on avatar</returns>
    public GameObject Stitch(GameObject sourceClothing, GameObject targetAvatar)
    {
        var boneCatalog = new TransformCatalog(targetAvatar.transform);
        var skinnedMeshRenderers = sourceClothing.GetComponentsInChildren<SkinnedMeshRenderer>();
        var targetClothing = AddChild(sourceClothing, targetAvatar.transform);

        foreach (var sourceRenderer in skinnedMeshRenderers)
        {
            var targetRenderer = AddSkinnedMeshRenderer(sourceRenderer, targetClothing);
            targetRenderer.bones = TranslateTransforms(sourceRenderer.bones, boneCatalog);
        }
        return targetClothing;
    }

		private GameObject AddChild(GameObject source, Transform parent)
    {
        source.transform.parent = parent;

        //The problem why "Stitch" cant happens in one frame is because Destroy happens at end of frame
        //So when another Stitch is called, the children overlap with previous children

        //Collect all children that need to be destroyed
        List<Transform> transformToDestroy = new List<Transform>();
        foreach (Transform child in source.transform)
        {
            if(child.GetComponent<Rigidbody>() == null)
                transformToDestroy.Add(child);
        }

        //Detach child from their parent so the next "Stitch" doesnt overlap with current child
        //Destroy child
        foreach (Transform child in transformToDestroy)
        {
            child.SetParent(null);
            Object.Destroy(child.gameObject);
        }
        
        return source;
    }

    private SkinnedMeshRenderer AddSkinnedMeshRenderer(SkinnedMeshRenderer source, GameObject parent)
    {
        GameObject meshObject = new GameObject(source.name);
        meshObject.transform.parent = parent.transform;

        var target = meshObject.AddComponent<SkinnedMeshRenderer>();
        target.sharedMesh = source.sharedMesh;
        target.materials = source.materials;
        return target;
    }

    private Transform[] TranslateTransforms(Transform[] sources, TransformCatalog transformCatalog)
    {
        var targets = new Transform[sources.Length];
        for (var index = 0; index < sources.Length; index++)
            targets[index] = DictionaryExtensions.Find(transformCatalog, sources[index].name);
        return targets;
    }

    #region TransformCatalog
    private class TransformCatalog : Dictionary<string, Transform>
    {
        #region Constructors
        public TransformCatalog(Transform transform)
        {
            Catalog(transform);
        }
        #endregion

        #region Catalog
        private void Catalog(Transform transform)
        {
            if (ContainsKey(transform.name))
            {
                Remove(transform.name);
                Add(transform.name, transform);
            }
            else
                Add(transform.name, transform);
            foreach (Transform child in transform)
                Catalog(child);
        }
        #endregion
    }
    #endregion

    #region DictionaryExtensions
    private class DictionaryExtensions
    {
        public static TValue Find<TKey, TValue>(Dictionary<TKey, TValue> source, TKey key)
        {
            TValue value;
            source.TryGetValue(key, out value);
            return value;
        }
    }
    #endregion

}

Just simply call

Stitcher stitcher;
...
Awake()
{
  stitcher = new Stitcher();
	...
}
...
//clothing is the clothing prefab to be attached to the character
stitcher.Stitch(clothing, character);

Prerequisites

Prefab model needs the same rigs as the character

Limitation

Stitching multiple prefab at the same frame is still not possible, ending with missing part except the first prefab. Current solution is to spread stitching to multiple frames. Need to investigate further

[Update]

The problem why "Stitch" cant happens in one frame is because Destroy happens at end of frame. So when another Stitch is called, the children from the new part overlap with previous children. The solution is to unparent the previous part’s children before destroying them so they wont overlap with the new part’s children.

Known Issue

When changing animation in the animator, and some of the stitched object is out of view, the out of view stitched object animation might be out of sync.

This can be solved by either setting the object’s skinnedmeshrenderer updateWhenOffscreen flag (not recommended), or by adjusting the bound to encapsulate other skinnedmeshrenderers attached to the character

void RecalculateBounds()
{
	SkinnedMeshRenderer[] skinnedMeshRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();

	//Create bounds that envelope all skinnedmeshenderer
	for(int i = 0; i < skinnedMeshRenderers.Length; i++)
		bounds.Encapsulate(skinnedMeshRenderers[i].sharedMesh.bounds);

	//Assign skinnedMeshRenderer bounds
	for(int i = 0; i < skinnedMeshRenderers.Length; i++)
		skinnedMeshRenderers[i].localBounds = bounds;}

Heads Up

The Stitcher class seems to strip anything except for SkinnedMeshRenderer in their child. This means that any script attached to the child of stitched gameobject will be missing. Current solution is to attach the script in the gameObject itself, or duplicating the script into the attached gameObject child manually.