using System.Collections.Generic;
using UnityEngine;
///
/// Place this on the character root (the one with the Animator).
/// Automatically remaps clothing bones to the character's skeleton — works in
/// both Edit Mode and Play Mode. Triggers on Awake and whenever a child is added.
///
[ExecuteAlways]
public class CharacterClothingSync : MonoBehaviour
{
[Tooltip("The character's main body SkinnedMeshRenderer. Auto-detected if left empty.")]
public SkinnedMeshRenderer bodyRenderer;
private Dictionary _boneMap;
void Reset()
{
AutoDetectBodyRenderer();
}
void Awake()
{
Sync();
}
// Fires in both Edit Mode and Play Mode when a child is added or removed.
void OnTransformChildrenChanged()
{
Sync();
}
#if UNITY_EDITOR
void OnValidate()
{
// Defer so Unity finishes its own validation pass first.
UnityEditor.EditorApplication.delayCall += () =>
{
if (this != null) Sync();
};
}
#endif
/// Sync all clothing children to the character skeleton.
[ContextMenu("Sync All Clothing")]
public void Sync()
{
if (bodyRenderer == null)
AutoDetectBodyRenderer();
if (bodyRenderer == null)
{
Debug.LogError("[CharacterClothingSync] No body SkinnedMeshRenderer found. Assign it manually.", this);
return;
}
BuildBoneMap();
SyncAllChildren();
}
/// Call after instantiating a new outfit at runtime.
public void RegisterOutfit(GameObject outfitRoot)
{
if (_boneMap == null || _boneMap.Count == 0)
{
if (bodyRenderer == null) AutoDetectBodyRenderer();
if (bodyRenderer == null) return;
BuildBoneMap();
}
foreach (SkinnedMeshRenderer smr in outfitRoot.GetComponentsInChildren(true))
SyncOutfit(smr);
}
void AutoDetectBodyRenderer()
{
// Body mesh is a direct child of the character root.
// Outfit SkinnedMeshRenderers are nested deeper inside outfit containers.
foreach (Transform child in transform)
{
SkinnedMeshRenderer smr = child.GetComponent();
if (smr != null)
{
bodyRenderer = smr;
return;
}
}
}
void BuildBoneMap()
{
_boneMap = new Dictionary();
foreach (Transform bone in bodyRenderer.bones)
{
if (bone != null && !_boneMap.ContainsKey(bone.name))
_boneMap[bone.name] = bone;
}
}
void SyncAllChildren()
{
foreach (SkinnedMeshRenderer smr in GetComponentsInChildren(true))
{
if (smr == bodyRenderer) continue;
SyncOutfit(smr);
}
}
///
/// Re-applies body blend shape weights to all clothing children.
/// Call this whenever the body's blend shapes change (e.g. during customization).
///
[ContextMenu("Update Blend Shapes")]
public void UpdateBlendShapes()
{
foreach (SkinnedMeshRenderer smr in GetComponentsInChildren(true))
{
if (smr == bodyRenderer) continue;
SyncBlendShapes(smr);
}
}
// Returns true if the outfit uses the same skeleton structure as the character
// (e.g. Root > Hips). Returns false for custom skeletons like hair (Root > CenterHair).
bool HasMatchingSkeletonStructure(SkinnedMeshRenderer smr)
{
Transform outfitRoot = smr.rootBone;
if (outfitRoot == null) return false;
foreach (Transform child in outfitRoot)
{
if (_boneMap.ContainsKey(child.name))
return true;
}
return false;
}
void SyncOutfit(SkinnedMeshRenderer smr)
{
if (!HasMatchingSkeletonStructure(smr))
{
// Custom skeleton (hair, accessories) — children of Root don't match
// the character's skeleton, so don't touch bones[]. The outfit's own
// rig is already correct; just sync blend shapes.
SyncBlendShapes(smr);
Debug.Log($"[CharacterClothingSync] '{smr.gameObject.name}' has custom skeleton — skipped bone sync.", this);
return;
}
// Full body outfit — replace bones by name using the character's skeleton.
Transform[] originalBones = smr.bones;
Transform[] newBones = new Transform[originalBones.Length];
for (int i = 0; i < originalBones.Length; i++)
{
Transform b = originalBones[i];
if (b == null) continue;
if (_boneMap.TryGetValue(b.name, out Transform match))
{
newBones[i] = match;
}
else
{
// Bone exists in the outfit but not in the character skeleton
// (e.g. an extra deformation bone). Reparent it under the nearest
// character bone by world-space distance so it still animates.
ReparentUnderNearestCharacterBone(b);
newBones[i] = b;
}
}
smr.bones = newBones;
if (smr.rootBone != null && _boneMap.TryGetValue(smr.rootBone.name, out Transform rootMatch))
smr.rootBone = rootMatch;
SyncBlendShapes(smr);
Debug.Log($"[CharacterClothingSync] '{smr.gameObject.name}' synced OK.", this);
}
void ReparentUnderNearestCharacterBone(Transform bone)
{
Transform nearest = null;
float minDist = float.MaxValue;
foreach (Transform charBone in _boneMap.Values)
{
float dist = Vector3.Distance(bone.position, charBone.position);
if (dist < minDist)
{
minDist = dist;
nearest = charBone;
}
}
if (nearest != null && bone.parent != nearest)
bone.SetParent(nearest, worldPositionStays: true);
}
void SyncBlendShapes(SkinnedMeshRenderer smr)
{
Mesh bodyMesh = bodyRenderer.sharedMesh;
Mesh outfitMesh = smr.sharedMesh;
if (bodyMesh == null || outfitMesh == null) return;
for (int i = 0; i < outfitMesh.blendShapeCount; i++)
{
string shapeName = outfitMesh.GetBlendShapeName(i);
int bodyIndex = bodyMesh.GetBlendShapeIndex(shapeName);
if (bodyIndex >= 0)
smr.SetBlendShapeWeight(i, bodyRenderer.GetBlendShapeWeight(bodyIndex));
}
}
}