using UnityEngine; using UnityEditor; using System.Linq; using System.Collections.Generic; using VRM; using MagicaCloth2; using MagicaClothType = MagicaCloth2.MagicaCloth; using UnityEngine.Animations; public class AvatarComponetCopier : EditorWindow { GameObject sourcePrefab; GameObject destinationPrefab; [MenuItem("Tools/Avatar Component Mover (SpringBone+MagicaCloth2)")] static void ShowWindow() { GetWindow("Avatar Component Mover"); } void OnGUI() { GUILayout.Label("Move VRMSpringBone/MagicaCloth2 & Collider Components", EditorStyles.boldLabel); sourcePrefab = (GameObject)EditorGUILayout.ObjectField("Source Object", sourcePrefab, typeof(GameObject), true); destinationPrefab = (GameObject)EditorGUILayout.ObjectField("Target Object", destinationPrefab, typeof(GameObject), true); if (GUILayout.Button("Spring본콜라이더 복사")) { if (sourcePrefab == null || destinationPrefab == null) { EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); return; } CopySpringBoneAndMagicaColliders(); } if (GUILayout.Button("Spring본/Cloth 옮기기")) { if (sourcePrefab == null || destinationPrefab == null) { EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); return; } MoveSpringBonesAndMagicaCloth(); } if (GUILayout.Button("Constraint 값 복사")) { if (sourcePrefab == null || destinationPrefab == null) { EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); return; } CopyConstraints(); } if (GUILayout.Button("BlendShapeCopy")) { if (sourcePrefab == null || destinationPrefab == null) { EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); return; } CopyBlendShapes(); } if (GUILayout.Button("ActiveStateCopy")) { if (sourcePrefab == null || destinationPrefab == null) { EditorUtility.DisplayDialog("Error", "Source and Target Objects must be set.", "OK"); return; } CopyActiveStates(); } } void CopySpringBoneAndMagicaColliders() { // VRMSpringBoneColliderGroup var srcColliders = sourcePrefab.GetComponentsInChildren(true) .OrderBy(c => GetTransformPath(c.transform, sourcePrefab.transform)).ToArray(); foreach (var srcCollider in srcColliders) { string path = GetTransformPath(srcCollider.transform, sourcePrefab.transform); var tgtTransform = destinationPrefab.transform.Find(path); if (tgtTransform == null) continue; if (tgtTransform.GetComponent() != null) continue; var tgtCollider = tgtTransform.gameObject.AddComponent(); CopyColliderGroupParameters(srcCollider, tgtCollider); } // MagicaCloth2 Colliders CopyMagicaCollider(); CopyMagicaCollider(); CopyMagicaCollider(); Debug.Log("VRMSpringBoneColliderGroup & MagicaCloth2 Collider 복사가 완료되었습니다."); } void CopyMagicaCollider() where T : Component { var srcColliders = sourcePrefab.GetComponentsInChildren(true) .OrderBy(c => GetTransformPath(((Component)c).transform, sourcePrefab.transform)).ToArray(); foreach (var srcCollider in srcColliders) { var srcTr = ((Component)srcCollider).transform; string path = GetTransformPath(srcTr, sourcePrefab.transform); var tgtTransform = destinationPrefab.transform.Find(path); if (tgtTransform == null) continue; if (tgtTransform.GetComponent() != null) continue; var tgtCollider = tgtTransform.gameObject.AddComponent(); CopyMagicaColliderComponents(srcCollider, tgtCollider); } } void CopyMagicaColliderComponents(Component src, Component tgt) { var fields = src.GetType().GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); foreach (var field in fields) { var value = field.GetValue(src); if (typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType)) { Object refObj = value as Object; if (refObj == null) { field.SetValue(tgt, null); continue; } if (refObj is Transform tr) { string refPath = GetTransformPath(tr, sourcePrefab.transform); var tgtTr = destinationPrefab.transform.Find(refPath); if (tgtTr != null) field.SetValue(tgt, tgtTr); } else if (refObj is GameObject go) { string refPath = GetTransformPath(go.transform, sourcePrefab.transform); var tgtGo = destinationPrefab.transform.Find(refPath); if (tgtGo != null) field.SetValue(tgt, tgtGo.gameObject); } else if (refObj is Component comp) { string refPath = GetTransformPath(comp.gameObject.transform, sourcePrefab.transform); var tgtGo = destinationPrefab.transform.Find(refPath); if (tgtGo != null) { var tgtComps = tgtGo.GetComponents(comp.GetType()); int compIdx = comp.gameObject.GetComponents(comp.GetType()).ToList().IndexOf(comp); if (compIdx >= 0 && compIdx < tgtComps.Length) field.SetValue(tgt, tgtComps[compIdx]); } } else { // 기타 UnityEngine.Object 타입은 그대로 복사 field.SetValue(tgt, refObj); } } else { // 값 타입, enum, string 등은 그대로 복사 field.SetValue(tgt, value); } } } void MoveSpringBonesAndMagicaCloth() { // VRMSpringBone var srcSpringBones = sourcePrefab.GetComponentsInChildren(true) .OrderBy(s => GetTransformPath(s.transform, sourcePrefab.transform)).ToArray(); foreach (var srcSpringBone in srcSpringBones) { string path = GetTransformPath(srcSpringBone.transform, sourcePrefab.transform); var tgtTransform = destinationPrefab.transform.Find(path); if (tgtTransform == null) continue; var tgtSpringBone = tgtTransform.gameObject.AddComponent(); CopyVRMSpringBoneComponents(srcSpringBone, tgtSpringBone); DestroyImmediate(srcSpringBone); } // MagicaCloth var srcMagicaCloths = sourcePrefab.GetComponentsInChildren(true) .OrderBy(s => GetTransformPath(s.transform, sourcePrefab.transform)).ToArray(); foreach (var srcCloth in srcMagicaCloths) { string path = GetTransformPath(srcCloth.transform, sourcePrefab.transform); var tgtTransform = destinationPrefab.transform.Find(path); if (tgtTransform == null) continue; var tgtCloth = tgtTransform.gameObject.AddComponent(); CopyMagicaClothComponents(srcCloth, tgtCloth); DestroyImmediate(srcCloth); } Debug.Log("VRMSpringBone & MagicaCloth 옮기기가 완료되었습니다."); } void CopyColliderGroupParameters(VRMSpringBoneColliderGroup source, VRMSpringBoneColliderGroup target) { target.Colliders = source.Colliders.Select(c => new VRMSpringBoneColliderGroup.SphereCollider { Offset = c.Offset, Radius = c.Radius }).ToArray(); } void CopyVRMSpringBoneComponents(VRMSpringBone original, VRMSpringBone copy) { copy.m_comment = original.m_comment; copy.m_stiffnessForce = original.m_stiffnessForce; copy.m_gravityPower = original.m_gravityPower; copy.m_gravityDir = original.m_gravityDir; copy.m_dragForce = original.m_dragForce; copy.m_center = FindCorrespondingTransform(original.m_center, destinationPrefab.transform); copy.m_hitRadius = original.m_hitRadius; copy.m_updateType = original.m_updateType; copy.RootBones = original.RootBones .Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform)) .Where(t => t != null) .ToList(); copy.ColliderGroups = original.ColliderGroups .Select(colliderGroup => FindCorrespondingColliderGroup(colliderGroup, destinationPrefab.transform)) .Where(cg => cg != null) .ToArray(); } Transform FindCorrespondingTransform(Transform original, Transform searchRoot) { if (original == null) return null; var path = GetTransformPath(original, sourcePrefab.transform); return searchRoot.Find(path); } string GetTransformPath(Transform current, Transform root) { if (current == root) return ""; var path = current.name; while (current.parent != null && current.parent != root) { current = current.parent; path = current.name + "/" + path; } return path; } VRMSpringBoneColliderGroup FindCorrespondingColliderGroup(VRMSpringBoneColliderGroup original, Transform searchRoot) { if (original == null) return null; var path = GetTransformPath(original.transform, sourcePrefab.transform); var correspondingTransform = searchRoot.Find(path); return correspondingTransform != null ? correspondingTransform.GetComponent() : null; } void CopyMagicaClothComponents(MagicaClothType src, MagicaClothType tgt) { var fields = typeof(MagicaClothType).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); foreach (var field in fields) { var value = field.GetValue(src); // ClothType 우선 복사 및 분기 처리 if (field.Name == "serializeData" && value is MagicaCloth2.ClothSerializeData sdata) { var tdata = new MagicaCloth2.ClothSerializeData(); // ClothType을 먼저 복사 tdata.clothType = sdata.clothType; // ClothType에 따라 분기 if (sdata.clothType == MagicaCloth2.ClothProcess.ClothType.MeshCloth) { // sourceRenderers 복사 tdata.sourceRenderers = sdata.sourceRenderers .Select(r => { if (r == null) return null; string path = GetTransformPath(r.transform, sourcePrefab.transform); var tgtTr = destinationPrefab.transform.Find(path); return tgtTr ? tgtTr.GetComponent() : null; }) .Where(r => r != null) .ToList(); } else if (sdata.clothType == MagicaCloth2.ClothProcess.ClothType.BoneCloth || sdata.clothType == MagicaCloth2.ClothProcess.ClothType.BoneSpring) { // rootBones 복사 tdata.rootBones = sdata.rootBones .Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform)) .Where(t => t != null) .ToList(); } // colliderCollisionConstraint 복사 if (sdata.colliderCollisionConstraint != null) { var srcCol = sdata.colliderCollisionConstraint; var tgtCol = new MagicaCloth2.ColliderCollisionConstraint.SerializeData(); tgtCol.mode = srcCol.mode; tgtCol.friction = srcCol.friction; tgtCol.limitDistance = srcCol.limitDistance; // colliderList 변환 tgtCol.colliderList = srcCol.colliderList .Select(c => { if (c == null) return null; string path = GetTransformPath(c.transform, sourcePrefab.transform); var tgtTr = destinationPrefab.transform.Find(path); return tgtTr ? tgtTr.GetComponent() : null; }) .Where(c => c != null) .ToList(); // collisionBones 변환 tgtCol.collisionBones = srcCol.collisionBones .Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform)) .Where(t => t != null) .ToList(); tdata.colliderCollisionConstraint = tgtCol; } // 공통 필드 복사 tdata.paintMaps = new List(sdata.paintMaps); tdata.paintMode = sdata.paintMode; tdata.connectionMode = sdata.connectionMode; tdata.rotationalInterpolation = sdata.rotationalInterpolation; tdata.rootRotation = sdata.rootRotation; tdata.updateMode = sdata.updateMode; tdata.animationPoseRatio = sdata.animationPoseRatio; tdata.reductionSetting = sdata.reductionSetting; tdata.customSkinningSetting = sdata.customSkinningSetting; tdata.normalAlignmentSetting = sdata.normalAlignmentSetting; tdata.cullingSettings = sdata.cullingSettings; tdata.normalAxis = sdata.normalAxis; tdata.gravity = sdata.gravity; tdata.gravityDirection = sdata.gravityDirection; tdata.gravityFalloff = sdata.gravityFalloff; tdata.stablizationTimeAfterReset = sdata.stablizationTimeAfterReset; tdata.blendWeight = sdata.blendWeight; tdata.damping = sdata.damping; tdata.radius = sdata.radius; // 기타 값들도 필요시 추가 복사 field.SetValue(tgt, tdata); continue; } if (field.Name == "serializeData2" && value is MagicaCloth2.ClothSerializeData2 sdata2) { var tdata2 = new MagicaCloth2.ClothSerializeData2(); // boneAttributeDict 변환 tdata2.boneAttributeDict = sdata2.boneAttributeDict.ToDictionary( kvp => FindCorrespondingTransform(kvp.Key, destinationPrefab.transform), kvp => kvp.Value ); // selectionData 등 나머지 값은 그대로 복사 tdata2.selectionData = sdata2.selectionData; tdata2.preBuildData = sdata2.preBuildData; field.SetValue(tgt, tdata2); continue; } // customSkinningSetting.skinningBones 변환 if (field.Name == "customSkinningSetting" && value != null) { var srcSkin = value; var skinField = value.GetType().GetField("skinningBones"); if (skinField != null) { var srcList = skinField.GetValue(srcSkin) as List; if (srcList != null) { var tgtList = srcList .Select(bone => FindCorrespondingTransform(bone, destinationPrefab.transform)) .Where(t => t != null) .ToList(); skinField.SetValue(srcSkin, tgtList); } } field.SetValue(tgt, srcSkin); continue; } // 이하 기존 로직 if (typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType)) { Object refObj = value as Object; if (refObj == null) { field.SetValue(tgt, null); continue; } if (refObj is Transform tr) { string refPath = GetTransformPath(tr, sourcePrefab.transform); var tgtTr = destinationPrefab.transform.Find(refPath); if (tgtTr != null) field.SetValue(tgt, tgtTr); } else if (refObj is GameObject go) { string refPath = GetTransformPath(go.transform, sourcePrefab.transform); var tgtGo = destinationPrefab.transform.Find(refPath); if (tgtGo != null) field.SetValue(tgt, tgtGo.gameObject); } else if (refObj is Component comp) { string refPath = GetTransformPath(comp.gameObject.transform, sourcePrefab.transform); var tgtGo = destinationPrefab.transform.Find(refPath); if (tgtGo != null) { var tgtComps = tgtGo.GetComponents(comp.GetType()); int compIdx = comp.gameObject.GetComponents(comp.GetType()).ToList().IndexOf(comp); if (compIdx >= 0 && compIdx < tgtComps.Length) field.SetValue(tgt, tgtComps[compIdx]); } } else { // 기타 UnityEngine.Object 타입은 그대로 복사 field.SetValue(tgt, refObj); } } else { // 값 타입, enum, string 등은 그대로 복사 field.SetValue(tgt, value); } } } void CopyConstraints() { var srcTransforms = sourcePrefab.GetComponentsInChildren(true); foreach (var srcTr in srcTransforms) { string path = GetTransformPath(srcTr, sourcePrefab.transform); var tgtTr = destinationPrefab.transform.Find(path); if (tgtTr == null) continue; // PositionConstraint var srcPos = srcTr.GetComponent(); if (srcPos != null) CopyConstraintComponent(srcPos, tgtTr); // RotationConstraint var srcRot = srcTr.GetComponent(); if (srcRot != null) CopyConstraintComponent(srcRot, tgtTr); // ParentConstraint var srcParent = srcTr.GetComponent(); if (srcParent != null) CopyConstraintComponent(srcParent, tgtTr); } Debug.Log("Constraint 값 복사가 완료되었습니다."); } void CopyConstraintComponent(T srcConstraint, Transform tgtTr) where T : Behaviour { // 이미 있으면 삭제 후 새로 추가 var tgtConstraint = tgtTr.GetComponent(); if (tgtConstraint != null) DestroyImmediate(tgtConstraint); tgtConstraint = tgtTr.gameObject.AddComponent(); if (srcConstraint is PositionConstraint srcPos && tgtConstraint is PositionConstraint tgtPos) { tgtPos.weight = srcPos.weight; tgtPos.constraintActive = srcPos.constraintActive; tgtPos.locked = srcPos.locked; for (int i = 0; i < srcPos.sourceCount; i++) { var src = srcPos.GetSource(i); var srcTr = src.sourceTransform; if (srcTr == null) continue; string srcPath = GetTransformPath(srcTr, sourcePrefab.transform); var tgtSourceTr = destinationPrefab.transform.Find(srcPath); if (tgtSourceTr == null) continue; var newSource = src; newSource.sourceTransform = tgtSourceTr; tgtPos.AddSource(newSource); } tgtPos.translationAtRest = srcPos.translationAtRest; tgtPos.translationOffset = srcPos.translationOffset; tgtPos.translationAxis = srcPos.translationAxis; } else if (srcConstraint is RotationConstraint srcRot && tgtConstraint is RotationConstraint tgtRot) { tgtRot.weight = srcRot.weight; tgtRot.constraintActive = srcRot.constraintActive; tgtRot.locked = srcRot.locked; for (int i = 0; i < srcRot.sourceCount; i++) { var src = srcRot.GetSource(i); var srcTr = src.sourceTransform; if (srcTr == null) continue; string srcPath = GetTransformPath(srcTr, sourcePrefab.transform); var tgtSourceTr = destinationPrefab.transform.Find(srcPath); if (tgtSourceTr == null) continue; var newSource = src; newSource.sourceTransform = tgtSourceTr; tgtRot.AddSource(newSource); } tgtRot.rotationAtRest = srcRot.rotationAtRest; tgtRot.rotationOffset = srcRot.rotationOffset; tgtRot.rotationAxis = srcRot.rotationAxis; } else if (srcConstraint is ParentConstraint srcParent && tgtConstraint is ParentConstraint tgtParent) { tgtParent.weight = srcParent.weight; tgtParent.constraintActive = srcParent.constraintActive; tgtParent.locked = srcParent.locked; for (int i = 0; i < srcParent.sourceCount; i++) { var src = srcParent.GetSource(i); var srcTr = src.sourceTransform; if (srcTr == null) continue; string srcPath = GetTransformPath(srcTr, sourcePrefab.transform); var tgtSourceTr = destinationPrefab.transform.Find(srcPath); if (tgtSourceTr == null) continue; var newSource = src; newSource.sourceTransform = tgtSourceTr; tgtParent.AddSource(newSource); // Offset도 복사 tgtParent.SetTranslationOffset(i, srcParent.GetTranslationOffset(i)); tgtParent.SetRotationOffset(i, srcParent.GetRotationOffset(i)); } tgtParent.translationAtRest = srcParent.translationAtRest; tgtParent.rotationAtRest = srcParent.rotationAtRest; tgtParent.translationAxis = srcParent.translationAxis; tgtParent.rotationAxis = srcParent.rotationAxis; } } void CopyBlendShapes() { var srcRenderers = sourcePrefab.GetComponentsInChildren(true); foreach (var srcRenderer in srcRenderers) { string path = GetTransformPath(srcRenderer.transform, sourcePrefab.transform); var tgtTransform = destinationPrefab.transform.Find(path); if (tgtTransform == null) continue; var tgtRenderer = tgtTransform.GetComponent(); if (tgtRenderer == null) continue; int blendShapeCount = srcRenderer.sharedMesh != null ? srcRenderer.sharedMesh.blendShapeCount : 0; for (int i = 0; i < blendShapeCount; i++) { string shapeName = srcRenderer.sharedMesh.GetBlendShapeName(i); int tgtIndex = tgtRenderer.sharedMesh != null ? tgtRenderer.sharedMesh.GetBlendShapeIndex(shapeName) : -1; if (tgtIndex >= 0) { float value = srcRenderer.GetBlendShapeWeight(i); tgtRenderer.SetBlendShapeWeight(tgtIndex, value); } } } Debug.Log("BlendShape 값 복사가 완료되었습니다."); } void CopyActiveStates() { var srcTransforms = sourcePrefab.GetComponentsInChildren(true); foreach (var srcTr in srcTransforms) { string path = GetTransformPath(srcTr, sourcePrefab.transform); var tgtTr = destinationPrefab.transform.Find(path); if (tgtTr == null) continue; tgtTr.gameObject.SetActive(srcTr.gameObject.activeSelf); } Debug.Log("ActiveState 복사가 완료되었습니다."); } }