VRMモデルのSpringBoneをコピーするツールを作りました

# はじめに
VRMモデルを作っていて、FBXから作り直しが発生したとき
VRM Spring Boneのつけ直しって大変じゃありませんか？
1発でコピーできるツールを作りました

![vrm.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/ed3bf142-4f4a-44ae-905e-acf38ae624b4.gif)

# とりあえずツールだけほしい方
[VRMモデルのSpringBoneをコピーするツール](https://broken55.booth.pm/items/1404532)
こちらからダウンロードできます

# 使い方
1.メニューのVRM→Spring Bone Copyでメニューを開きます
2. `元GameObject`にSpringBoneの設定されているオブジェクトを
　`コピー先GameObject`にSpringBoneの設定されていないオブジェクトを指定します
3.コピーボタンを押すことでコピーが完了します


# ソースコード
```c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.Reflection;

namespace VRM
{
	public class VRMSpringBoneCopy : EditorWindow 
	{

		[MenuItem("VRM/Spring Bone Copy")]
		static void Open()
		{
			GetWindow<VRMSpringBoneCopy> ();
		}

		GameObject srcGo;

		GameObject dstGo;
		/// <summary>
		/// OnGUI is called for rendering and handling GUI events.
		/// This function can be called multiple times per frame (one call per event).
		/// </summary>
		void OnGUI()
		{
			EditorGUILayout.LabelField("元GameObject");
			this.srcGo = EditorGUILayout.ObjectField(this.srcGo, typeof(GameObject), true) as GameObject;
			EditorGUILayout.LabelField("コピー先GameObject");
			this.dstGo = EditorGUILayout.ObjectField(this.dstGo, typeof(GameObject), true) as GameObject;

			if(GUILayout.Button("コピー"))
			{
				copyComponents();
				showResult();
			}
		}

		Transform[] transformList;

		List<string> colliderTargetError = new List<string>();
		List<string> springTargetBone = new List<string>();

		List<string> springBoneError = new List<string>();

		void copyComponents()
		{
			this.colliderTargetError.Clear();
			this.springTargetBone.Clear();
			this.springBoneError.Clear();

			deleteComponents();

			this.transformList = this.dstGo.GetComponentsInChildren<Transform>();

			var colliders = this.srcGo.GetComponentsInChildren<VRMSpringBoneColliderGroup>();
			foreach(var c in colliders)
			{
				var targetTransform = this.transformList.FirstOrDefault(x => x.name == c.transform.name);
				if(targetTransform != null)
				{
					var col = targetTransform.gameObject.AddComponent<VRMSpringBoneColliderGroup>();
					col.Colliders = new VRMSpringBoneColliderGroup.SphereCollider[c.Colliders.Length];
					copyField(c, col,  "m_gizmoColor");
					for(int i=0;i<col.Colliders.Length;i++)
					{
						col.Colliders[i] = new VRMSpringBoneColliderGroup.SphereCollider();
						col.Colliders[i].Offset = c.Colliders[i].Offset;
						col.Colliders[i].Radius = c.Colliders[i].Radius;
					}
				}
				else
				{
					Debug.LogWarning("Not found VRMSpringBoneColliderGroup bone->"+c.gameObject.name);
					this.colliderTargetError.Add(c.gameObject.name);
				}
			}

			var springbone = this.srcGo.GetComponentsInChildren<VRMSpringBone>();
			foreach(var c in springbone)
			{
				var targetTransform = this.transformList.FirstOrDefault(x => x.name == c.transform.name);
				if(targetTransform != null)
				{
					var spring = targetTransform.gameObject.AddComponent<VRMSpringBone>();
					spring.m_comment = c.m_comment;
					copyField(c, spring, "m_drawGizmo");
					copyField(c, spring, "m_gizmoColor");
					spring.m_stiffnessForce = c.m_stiffnessForce;
					spring.m_gravityPower = c.m_gravityPower;
					spring.m_gravityDir = c.m_gravityDir;
					spring.m_dragForce = c.m_dragForce;
					spring.m_center = searchTranform(c.m_center);
					foreach(var b in c.RootBones)
					{
						var bb = searchTranform(b);
						if(bb != null)
						{
							spring.RootBones.Add(bb);
						}
						else
						{
							this.springTargetBone.Add(b.name);
						}
					}
					spring.m_hitRadius = c.m_hitRadius;
					if(c.ColliderGroups.Length > 0)
					{
						spring.ColliderGroups = new VRMSpringBoneColliderGroup[c.ColliderGroups.Length];
						for(int i=0;i<spring.ColliderGroups.Length;i++)
						{
							var t = searchTranform(c.ColliderGroups[i].transform);
							if(t != null)
							{
								spring.ColliderGroups[i] = t.GetComponent<VRMSpringBoneColliderGroup>();
							}
							else
							{
								Debug.LogWarning("Not found VRMSpringBoneColliderGroup ->"+c.ColliderGroups[i].transform.name);
								this.springTargetBone.Add(c.ColliderGroups[i].transform.name);
							}
						}
					}
					
				}
				else
				{
					Debug.LogWarning("Not found VRMSpringBone ->"+c.gameObject.name);
					this.springBoneError.Add(c.gameObject.name);
				}
			}
		}

		void showResult()
		{
			if(this.colliderTargetError.Count == 0 &&
				this.springTargetBone.Count == 0 &&
				this.springBoneError.Count == 0)
			{
				EditorUtility.DisplayDialog("コピー完了", "正常にコピーが完了しました", "OK");
			}
			else
			{
				string message = "一部のコンポーネントのコピーができませんでした\n";
				if(this.colliderTargetError.Count > 0)
				{
					message += "VRMSpringBoneColliderGroup のボーンが見つかりませんでした\n";
					foreach(var e in this.colliderTargetError)
					{
						message += e+"\n";
					}
				}

				if(this.springBoneError.Count > 0)
				{
					message += "VRMSpringBoneのアタッチ先が見つかりませんでした\n";
					foreach(var e in this.springBoneError)
					{
						message += e+"\n";
					}
				}

				if(this.springTargetBone.Count > 0)
				{
					message += "揺れ骨が見つかりませんでした\n";
					foreach(var e in this.springTargetBone)
					{
						message += e+"\n";
					}
				}

				EditorUtility.DisplayDialog("コピー完了", message, "OK");
			}
		}

		Transform searchTranform(Transform t)
		{
			if(t == null)
			{
				return null;
			}

			var value = this.transformList.FirstOrDefault(x => x.name == t.name);

			return value;
		}

		void deleteComponents()
		{
			var cols = this.dstGo.GetComponentsInChildren<VRMSpringBoneColliderGroup>();
			foreach(var c in cols)
			{
				DestroyImmediate(c);
			}

			var springs = this.dstGo.GetComponentsInChildren<VRMSpringBone>();
			foreach(var s in springs)
			{
				DestroyImmediate(s);
			}
		}

		// private変数のコピー
		void copyField(object src, object dst, string fieldName)
		{
			var srcField = src.GetType().GetField(fieldName,BindingFlags.Public | BindingFlags.NonPublic |
									BindingFlags.Instance | BindingFlags.Static |
									BindingFlags.DeclaredOnly);

			if(srcField == null)
			{
				Debug.LogWarning("srcField is null");
				return;
			}

			var dstField = dst.GetType().GetField(fieldName,BindingFlags.Public | BindingFlags.NonPublic |
									BindingFlags.Instance | BindingFlags.Static |
									BindingFlags.DeclaredOnly);

			if(dstField == null)
			{
				Debug.LogWarning("dstField is null");
				return;
			}

			dstField.SetValue(dst, srcField.GetValue(src));
		}
	}

}

```

# 解説
- やっていることは本当に簡単で、コピー元のVRM Spring BoneがアタッチされているTransform名を取得し、コピー先の一致するTransformにAddComponentしてパラメータをコピーしているだけです

`this.transformList = this.dstGo.GetComponentsInChildren<Transform>();`
これでコピー先のTransform一覧を取得

```c#
var springbone = this.srcGo.GetComponentsInChildren<VRMSpringBone>();
foreach(var c in springbone)
{
    var targetTransform = this.transformList.FirstOrDefault(x => x.name == c.transform.name);
```
これでコピー元とTransform名が一致するコピー先のTransformを取得します

VRM Spring Boneは基本的にパラメータがpublicなのでそのままコピーできるのですが
SerializeFieldで表示しているパラメータはprivateになっているためコピーできません
そこで、リフレクションを使ってパラメータの取得と設定を行っています

```c#
// private変数のコピー
void copyField(object src, object dst, string fieldName)
{
    var srcField = src.GetType().GetField(fieldName,BindingFlags.Public | BindingFlags.NonPublic |
                            BindingFlags.Instance | BindingFlags.Static |
                            BindingFlags.DeclaredOnly);

    if(srcField == null)
    {
        Debug.LogWarning("srcField is null");
        return;
    }

    var dstField = dst.GetType().GetField(fieldName,BindingFlags.Public | BindingFlags.NonPublic |
                            BindingFlags.Instance | BindingFlags.Static |
                            BindingFlags.DeclaredOnly);

    if(dstField == null)
    {
        Debug.LogWarning("dstField is null");
        return;
    }

    dstField.SetValue(dst, srcField.GetValue(src));
}

```

# 参考
- [VRMSpringBone(VRM公式ドキュメント)](https://vrm.dev/univrm/components/univrm_secondary/)