using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.Linq; using System.Reflection; using F10.StreamDeckIntegration; using F10.StreamDeckIntegration.Attributes; // Namespace handling for optional dependencies using MagicaCloth2; using VRM; namespace YAMO.Tools { [InitializeOnLoad] public class AvatarPhysicsMigrator : EditorWindow { static AvatarPhysicsMigrator() { StreamDeck.AddStatic(typeof(AvatarPhysicsMigrator)); } public static void ShowWindow() { GetWindow("Physics Migrator"); } [MenuItem("Tools/Avatar Physics Migrator")] [StreamDeckButton("PhysicsMigrator_Toggle")] public static void ToggleWindow() { var windows = Resources.FindObjectsOfTypeAll(); if (windows != null && windows.Length > 0) { windows[0].Close(); } else { ShowWindow(); } } private GameObject sourceAvatar; private GameObject targetAvatar; private Vector2 scrollPosition; private List logMessages = new List(); private bool isProcessing = false; private string preBuildFolderPath = "Assets/MagicaPreBuildData"; private AnalysisResult analysisResult; private class AnalysisResult { public int totalTransforms; public int nameMatchCount; public int duplicateCount; public List duplicateNames; public int magicaClothCount; public int vrmSpringCount; public int capsuleColliderCount; public int sphereColliderCount; public int planeColliderCount; public int vrmColliderGroupCount; } private void OnGUI() { GUILayout.Label("Avatar Physics Migrator", EditorStyles.boldLabel); EditorGUILayout.Space(); sourceAvatar = (GameObject)EditorGUILayout.ObjectField("Source Avatar (Armature)", sourceAvatar, typeof(GameObject), true); targetAvatar = (GameObject)EditorGUILayout.ObjectField("Target Avatar (Biped)", targetAvatar, typeof(GameObject), true); EditorGUILayout.Space(); if (GUILayout.Button("Analyze Source Avatar")) { if (ValidateInputs()) { Analyze(); } } if (analysisResult != null) { EditorGUILayout.Space(); GUILayout.Label("Analysis Results:", EditorStyles.boldLabel); EditorGUI.indentLevel++; // Name Match Rate float matchRate = analysisResult.totalTransforms > 0 ? (float)analysisResult.nameMatchCount / analysisResult.totalTransforms * 100f : 0f; EditorGUILayout.LabelField($"Name Match Rate: {analysisResult.nameMatchCount} / {analysisResult.totalTransforms} ({matchRate:F1}%)"); // Duplicate Check if (analysisResult.duplicateCount > 0) { EditorGUILayout.HelpBox($"Found {analysisResult.duplicateCount} duplicate names in Source Avatar!", MessageType.Warning); if (GUILayout.Button("Show Duplicates")) { foreach (var name in analysisResult.duplicateNames.Take(10)) // Show first 10 { Log($"Duplicate: {name}"); } if (analysisResult.duplicateNames.Count > 10) Log($"...and {analysisResult.duplicateNames.Count - 10} more."); } } else { EditorGUILayout.LabelField("Source Duplicates: None (OK)", EditorStyles.miniLabel); } EditorGUILayout.Space(); EditorGUILayout.LabelField($"Magica Cloths: {analysisResult.magicaClothCount}"); EditorGUILayout.LabelField($"VRM Spring Bones: {analysisResult.vrmSpringCount}"); EditorGUILayout.LabelField("Colliders:"); EditorGUI.indentLevel++; EditorGUILayout.LabelField($"Capsule: {analysisResult.capsuleColliderCount}"); EditorGUILayout.LabelField($"Sphere: {analysisResult.sphereColliderCount}"); EditorGUILayout.LabelField($"Plane: {analysisResult.planeColliderCount}"); EditorGUILayout.LabelField($"VRM Groups: {analysisResult.vrmColliderGroupCount}"); EditorGUI.indentLevel--; EditorGUI.indentLevel--; EditorGUILayout.Space(); } if (GUILayout.Button("Migrate Physics Components")) { if (ValidateInputs()) { Migrate(); } } if (targetAvatar != null) { EditorGUILayout.Space(); GUILayout.Label("MagicaCloth PreBuild Automation", EditorStyles.boldLabel); preBuildFolderPath = EditorGUILayout.TextField("Save Path", preBuildFolderPath); if (GUILayout.Button("Auto Create PreBuild Data (Target)")) { AutoCreatePreBuildData(); } } EditorGUILayout.Space(); GUILayout.Label("BlendShape Tools", EditorStyles.boldLabel); if (GUILayout.Button("Migrate BlendShapes")) { if (ValidateInputs()) { MigrateBlendShapes(); } } if (GUILayout.Button("Reset All BlendShapes (Selected)")) { ResetBlendShapes(); } EditorGUILayout.Space(); GUILayout.Label("Log:", EditorStyles.boldLabel); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(200)); foreach (var log in logMessages) { GUILayout.Label(log); } EditorGUILayout.EndScrollView(); } private void Log(string message) { logMessages.Add(message); Debug.Log($"[PhysicsMigrator] {message}"); Repaint(); } private void LogError(string message) { logMessages.Add($"[Error] {message}"); Debug.LogError($"[PhysicsMigrator] {message}"); Repaint(); } private bool ValidateInputs() { logMessages.Clear(); if (sourceAvatar == null || targetAvatar == null) { LogError("Please assign both Source and Target avatars."); return false; } if (sourceAvatar == targetAvatar) { LogError("Source and Target cannot be the same object."); return false; } // Check for duplicate names in Source if (HasDuplicateNames(sourceAvatar.transform)) { return false; } return true; } private bool HasDuplicateNames(Transform root) { var names = new HashSet(); var duplicates = new List(); void Traverse(Transform t) { if (!names.Add(t.name)) { duplicates.Add(t.name); } foreach (Transform child in t) Traverse(child); } Traverse(root); if (duplicates.Count > 0) { LogError($"Duplicate names found in Source Avatar: {string.Join(", ", duplicates.Distinct())}"); LogError("Please rename duplicate objects to ensure unique mapping."); return true; } return false; } private void Migrate() { isProcessing = true; Log("Starting migration..."); // 1. Build Bone Map var boneMap = BuildBoneMap(sourceAvatar.transform, targetAvatar.transform); if (boneMap == null) { isProcessing = false; return; } // 2. Copy Components CopyColliders(sourceAvatar.transform, boneMap); CopyPhysicsComponents(sourceAvatar.transform, boneMap); Log("Migration completed successfully!"); isProcessing = false; } private void Analyze() { Log("Analyzing source avatar..."); analysisResult = new AnalysisResult(); var sourceTransforms = sourceAvatar.GetComponentsInChildren(true); var targetTransforms = targetAvatar.GetComponentsInChildren(true); analysisResult.totalTransforms = sourceTransforms.Length; // 1. Name Match Rate var targetNames = new HashSet(targetTransforms.Select(t => t.name)); analysisResult.nameMatchCount = sourceTransforms.Count(t => targetNames.Contains(t.name)); // 2. Duplicate Check var nameCounts = new Dictionary(); foreach (var t in sourceTransforms) { if (!nameCounts.ContainsKey(t.name)) nameCounts[t.name] = 0; nameCounts[t.name]++; } analysisResult.duplicateNames = nameCounts.Where(kv => kv.Value > 1).Select(kv => kv.Key).ToList(); analysisResult.duplicateCount = analysisResult.duplicateNames.Count; // 3. Component Counts analysisResult.magicaClothCount = sourceAvatar.GetComponentsInChildren(true).Length; analysisResult.vrmSpringCount = sourceAvatar.GetComponentsInChildren(true).Length; analysisResult.capsuleColliderCount = sourceAvatar.GetComponentsInChildren(true).Length; analysisResult.sphereColliderCount = sourceAvatar.GetComponentsInChildren(true).Length; analysisResult.planeColliderCount = sourceAvatar.GetComponentsInChildren(true).Length; analysisResult.vrmColliderGroupCount = sourceAvatar.GetComponentsInChildren(true).Length; Log("Analysis completed."); } private Dictionary BuildBoneMap(Transform sourceRoot, Transform targetRoot) { var map = new Dictionary(); var sourceAnimator = sourceAvatar.GetComponent(); var targetAnimator = targetAvatar.GetComponent(); // Phase 1: Humanoid Mapping // Ensure Root is mapped map[sourceRoot] = targetRoot; if (sourceAnimator != null && sourceAnimator.isHuman && targetAnimator != null && targetAnimator.isHuman) { foreach (HumanBodyBones bone in System.Enum.GetValues(typeof(HumanBodyBones))) { if (bone == HumanBodyBones.LastBone) continue; var sBone = sourceAnimator.GetBoneTransform(bone); var tBone = targetAnimator.GetBoneTransform(bone); if (sBone != null && tBone != null) { map[sBone] = tBone; } } Log($"Mapped {map.Count} humanoid bones."); } else { Log("Warning: One or both avatars are not Humanoid. Skipping Humanoid mapping."); } // Phase 2: Name Mapping (for non-humanoid bones) // We traverse Source and try to find matching name in Target // Optimization: Cache Target transforms by name? // Issue: Target might have duplicates too? We assume Target is clean or we just find first match. // Better: Traverse Source, if not in map, search in Target. // To avoid finding wrong objects in Target, we should search relative to the mapped parent if possible, // but structure is different (Armature vs Biped). // So global name search in Target is the fallback. var targetTransforms = targetRoot.GetComponentsInChildren(true) .GroupBy(t => t.name) .ToDictionary(g => g.Key, g => g.First()); // Take first if duplicates exist in Target (User didn't ask to check Target duplicates, but good to know) void MapRecursive(Transform current) { if (!map.ContainsKey(current)) { if (targetTransforms.TryGetValue(current.name, out var targetMatch)) { map[current] = targetMatch; } } foreach (Transform child in current) { MapRecursive(child); } } MapRecursive(sourceRoot); Log($"Total mapped transforms: {map.Count}"); return map; } private void CopyColliders(Transform sourceRoot, Dictionary boneMap) { // Magica Cloth 2 Colliders // MagicaCapsuleCollider, MagicaSphereCollider, MagicaPlaneCollider // VRM SpringBoneColliderGroup // Collect only transforms that have the relevant components var transformsWithColliders = new HashSet(); void Collect() where T : Component { foreach (var c in sourceRoot.GetComponentsInChildren(true)) { transformsWithColliders.Add(c.transform); } } Collect(); Collect(); Collect(); Collect(); foreach (var src in transformsWithColliders) { // Check if we have a destination parent Transform destParent = null; if (boneMap.TryGetValue(src, out var mappedDest)) { destParent = mappedDest; } else { // Find nearest mapped parent var p = src.parent; while (p != null) { if (boneMap.TryGetValue(p, out var m)) { // Check if it already exists by name under the mapped parent var existing = m.Find(src.name); if (existing != null) destParent = existing; else { // Duplicate the source object with explicit World Position and Rotation // This effectively "unlinks" it from the source hierarchy during creation var newObj = Instantiate(src.gameObject, src.position, src.rotation); newObj.name = src.name; // Parent to the target bone, MAINTAINING world position/rotation newObj.transform.SetParent(m, true); destParent = newObj.transform; // Add to map for children boneMap[src] = destParent; // Since we instantiated, we already have the components! // We don't need to CopyComponent again for this object. // However, we must continue to the next iteration to avoid adding duplicates below. goto NextItem; } break; } p = p.parent; } } if (destParent == null) continue; // Copy Components CopyComponent(src, destParent); CopyComponent(src, destParent); CopyComponent(src, destParent); CopyComponent(src, destParent); NextItem:; } } private void AutoCreatePreBuildData() { if (targetAvatar == null) return; var cloths = targetAvatar.GetComponentsInChildren(true); if (cloths.Length == 0) { Log("No MagicaCloth components found on Target Avatar."); return; } string folderPath = $"{preBuildFolderPath}/{targetAvatar.name}"; if (!AssetDatabase.IsValidFolder(folderPath)) { // Create folder recursively if needed if (!AssetDatabase.IsValidFolder(preBuildFolderPath)) { string[] folders = preBuildFolderPath.Split('/'); string currentPath = folders[0]; for (int i = 1; i < folders.Length; i++) { if (!AssetDatabase.IsValidFolder($"{currentPath}/{folders[i]}")) { AssetDatabase.CreateFolder(currentPath, folders[i]); } currentPath += $"/{folders[i]}"; } } AssetDatabase.CreateFolder(preBuildFolderPath, targetAvatar.name); } int successCount = 0; foreach (var cloth in cloths) { try { var preBuildData = cloth.GetSerializeData2().preBuildData; // Enable PreBuild preBuildData.enabled = true; // Create ScriptableObject if missing if (preBuildData.preBuildScriptableObject == null) { string assetName = $"PreBuild_{cloth.name}_{System.Guid.NewGuid().ToString().Substring(0, 8)}.asset"; string assetPath = $"{folderPath}/{assetName}"; var sobj = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(sobj, assetPath); preBuildData.preBuildScriptableObject = sobj; EditorUtility.SetDirty(cloth); } // Run PreBuild var result = MagicaCloth2.PreBuildDataCreation.CreatePreBuildData(cloth, false); // false = no dialog if (result.IsSuccess()) { successCount++; Log($"[Success] PreBuild for '{cloth.name}'"); } else { LogError($"[Fail] PreBuild for '{cloth.name}': {result.GetResultString()}"); } } catch (System.Exception e) { LogError($"[Exception] PreBuild for '{cloth.name}': {e.Message}"); } } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Log($"Auto PreBuild Completed. Success: {successCount} / {cloths.Length}"); } private void CopyPhysicsComponents(Transform sourceRoot, Dictionary boneMap) { // Helper to get or create destination transform Transform GetOrCreateDestination(Transform src, bool ignoreParent = false) { if (boneMap.TryGetValue(src, out var d)) return d; Transform mappedParent = null; if (ignoreParent) { mappedParent = targetAvatar.transform; } else { // Find nearest mapped parent var p = src.parent; while (p != null) { if (boneMap.TryGetValue(p, out var m)) { mappedParent = m; break; } p = p.parent; } } // Fallback to root if still null if (mappedParent == null) mappedParent = targetAvatar.transform; // Check if it already exists by name under the mapped parent var existing = mappedParent.Find(src.name); if (existing != null) { boneMap[src] = existing; return existing; } // Create new var newObj = new GameObject(src.name); newObj.transform.SetParent(mappedParent, false); newObj.transform.localPosition = src.localPosition; newObj.transform.localRotation = src.localRotation; newObj.transform.localScale = src.localScale; boneMap[src] = newObj.transform; return newObj.transform; } // MagicaCloth2 var magicaCloths = sourceRoot.GetComponentsInChildren(true); foreach (var mc in magicaCloths) { // User requested to ignore parentage for MagicaCloth and put under Root var dest = GetOrCreateDestination(mc.transform, true); if (dest != null) { var newMc = GetOrAddComponent(dest.gameObject); EditorUtility.CopySerialized(mc, newMc); // Remap References var so = new SerializedObject(newMc); // Root Bones (Transforms) var rootListProp = so.FindProperty("serializeData.rootBones"); if (rootListProp != null) RemapTransformList(rootListProp, boneMap); else Log("Warning: Could not find 'serializeData.rootBones'."); // Colliders (Components) var colliderListProp = so.FindProperty("serializeData.colliderCollisionConstraint.colliderList"); if (colliderListProp != null) RemapComponentList(colliderListProp, boneMap); else Log("Warning: Could not find 'serializeData.colliderCollisionConstraint.colliderList'."); // Source Renderers (Components) // Try common property names for renderers var rendererListProp = so.FindProperty("serializeData.sourceRenderers"); if (rendererListProp != null) RemapComponentList(rendererListProp, boneMap); else { // Fallback or log Log("Info: Could not find 'serializeData.sourceRenderers'. Checking for 'sourceRenderers'..."); rendererListProp = so.FindProperty("sourceRenderers"); if (rendererListProp != null) RemapComponentList(rendererListProp, boneMap); } so.ApplyModifiedProperties(); } else { Log($"Warning: Could not find a place to copy MagicaCloth from '{mc.name}'."); } } // VRMSpringBone var vrmSprings = sourceRoot.GetComponentsInChildren(true); foreach (var vs in vrmSprings) { var dest = GetOrCreateDestination(vs.transform, false); if (dest != null) { var newVs = GetOrAddComponent(dest.gameObject); EditorUtility.CopySerialized(vs, newVs); newVs.RootBones = RemapList(vs.RootBones, boneMap); newVs.ColliderGroups = RemapColliderGroups(vs.ColliderGroups, boneMap); } else { Log($"Warning: Could not find a place to copy VRMSpringBone from '{vs.name}'."); } } } // Helpers private void CopyComponent(Transform src, Transform dest) where T : Component { var comps = src.GetComponents(); foreach (var comp in comps) { var newComp = GetOrAddComponent(dest.gameObject); EditorUtility.CopySerialized(comp, newComp); } } private T GetOrAddComponent(GameObject go) where T : Component { var comp = go.GetComponent(); if (comp == null) comp = go.AddComponent(); return comp; } private void RemapComponentList(SerializedProperty listProp, Dictionary map) { if (listProp == null) return; for (int i = listProp.arraySize - 1; i >= 0; i--) { var elem = listProp.GetArrayElementAtIndex(i); var originalComp = elem.objectReferenceValue as Component; if (originalComp == null) continue; if (map.TryGetValue(originalComp.transform, out var mappedTransform)) { // Try to find the same component type on the mapped transform var newComp = mappedTransform.GetComponent(originalComp.GetType()); if (newComp != null) { elem.objectReferenceValue = newComp; } else { Log($"Warning: Mapped transform '{mappedTransform.name}' does not have component '{originalComp.GetType().Name}'."); elem.objectReferenceValue = null; } } else { Log($"Warning: Could not map transform for component '{originalComp.name}' ({originalComp.GetType().Name})."); elem.objectReferenceValue = null; } } } private void RemapTransformList(SerializedProperty listProp, Dictionary map) { if (listProp == null) return; for (int i = listProp.arraySize - 1; i >= 0; i--) { var elem = listProp.GetArrayElementAtIndex(i); var original = elem.objectReferenceValue as Transform; if (original == null) continue; if (map.TryGetValue(original, out var mapped)) { elem.objectReferenceValue = mapped; } else { // If not mapped, remove? Or keep null? // Keeping it might cause errors. Removing is safer for physics. // But let's warn. Log($"Warning: Could not map transform '{original.name}' in list."); elem.objectReferenceValue = null; } } } private List RemapList(List sourceList, Dictionary map) { var newList = new List(); foreach (var t in sourceList) { if (t != null && map.TryGetValue(t, out var mapped)) { newList.Add(mapped); } } return newList; } private VRM.VRMSpringBoneColliderGroup[] RemapColliderGroups(VRM.VRMSpringBoneColliderGroup[] sourceList, Dictionary map) { var newList = new List(); foreach (var c in sourceList) { if (c != null && map.TryGetValue(c.transform, out var mappedTransform)) { var mappedCollider = mappedTransform.GetComponent(); if (mappedCollider != null) { newList.Add(mappedCollider); } } } return newList.ToArray(); } private void MigrateBlendShapes() { Log("Starting BlendShape migration..."); var sourceRenderers = sourceAvatar.GetComponentsInChildren(true); var targetRenderers = targetAvatar.GetComponentsInChildren(true); var targetDict = targetRenderers.ToDictionary(r => r.name, r => r); int migratedCount = 0; foreach (var sourceSMR in sourceRenderers) { if (targetDict.TryGetValue(sourceSMR.name, out var targetSMR)) { var sourceMesh = sourceSMR.sharedMesh; var targetMesh = targetSMR.sharedMesh; if (sourceMesh == null || targetMesh == null) continue; int shapeCount = sourceMesh.blendShapeCount; bool anyChanged = false; for (int i = 0; i < shapeCount; i++) { string shapeName = sourceMesh.GetBlendShapeName(i); float weight = sourceSMR.GetBlendShapeWeight(i); // Find index in target int targetIndex = targetMesh.GetBlendShapeIndex(shapeName); if (targetIndex != -1) { targetSMR.SetBlendShapeWeight(targetIndex, weight); anyChanged = true; } } if (anyChanged) { migratedCount++; } } } Log($"BlendShape migration completed. Updated {migratedCount} SkinnedMeshRenderers."); } private void ResetBlendShapes() { var selected = Selection.activeGameObject; if (selected == null) { Log("Please select a GameObject to reset BlendShapes."); return; } var renderers = selected.GetComponentsInChildren(true); int resetCount = 0; foreach (var smr in renderers) { if (smr.sharedMesh == null) continue; int count = smr.sharedMesh.blendShapeCount; if (count > 0) { for (int i = 0; i < count; i++) { smr.SetBlendShapeWeight(i, 0f); } resetCount++; } } Log($"Reset BlendShapes for {resetCount} renderers in '{selected.name}'."); } } }