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)); } } }