Skip to content

Instantly share code, notes, and snippets.

@neon-izm
Forked from esperecyan/301 Moved Permanently.ja.md
Created July 17, 2019 17:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save neon-izm/cdd342d4c6730def317a6913d13aad11 to your computer and use it in GitHub Desktop.
Save neon-izm/cdd342d4c6730def317a6913d13aad11 to your computer and use it in GitHub Desktop.
『CopyVRMSettings.cs』 セットアップ済みのVRMプレハブから、正規化直後のVRMプレハブへ、UniVRMのコンポーネントの設定をコピーします。「Editor」という名前のフォルダをAssets内に作成し、その中にこのスクリプトを保存すると、上部メニューに次の項目が追加されます: VRM ▸ UniVRM ▸ CopyVRMSetting.cs / Copies the settings of UniVRM components from a set-up VRM prefab to a just normalized VRM prefab. You create the folder named “Editor” in Assets and save this scr…
using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor;
using UnityEditorInternal;
using UniGLTF;
using VRM;
namespace Esperecyan.Unity.CopyVRMSettings
{
/// <summary>
/// セットアップ済みのVRMプレハブから、正規化直後のVRMプレハブへ、UniVRMのコンポーネントの設定をコピーします。
/// </summary>
/// <remarks>
/// • モデル情報
/// • BlendShape
/// • 一人称視点
/// • 視線制御
/// • VRMSpringBoneとVRMSpringBoneColliderGroup
///
/// 動作確認バージョン: UniVRM 0.53.0, Unity 2017.4, Unity 2019.1
/// ライセンス: MIT License (MIT) <https://spdx.org/licenses/MIT.html>
/// 配布元: <https://gist.github.com/esperecyan/8a6d19738d4828f6df92a53138b7e315>
/// </remarks>
public class CopyVRMSettings
{
/// <summary>
/// 変換元のアバターのルートに設定されている必要があるコンポーネントと、そのフィールド名。
/// </summary>
public static readonly IDictionary<Type, string> RequiredComponentsAndFields = new Dictionary<Type, string> {
{ typeof(Animator), "" },
{ typeof(VRMHumanoidDescription), "Description" },
{ typeof(VRMMeta), "Meta" },
{ typeof(VRMBlendShapeProxy), "BlendShapeAvatar" },
};
/// <summary>
/// 当エディタ拡張のバージョン。
/// </summary>
/// <remarks>
/// 1.1.0 (2019-07-17)
/// オブジェクト名を参照していたために、BlendShapeが正常にコピーできていなかったバグを修正
/// Unity 2017.4 に対応
/// 1.0.2 (2019-06-17)
/// VRMSpringBoneとVRMSpringBoneColliderGroupのコピーで、オブジェクト名のみによる検索が行われていなかったのを修正
/// その他微修正
/// 1.0.1 (2019-06-15)
/// コピーしたBlendShapeがUnity終了時に失われる問題を修正
/// プリセット以外のBlendShapeが追加されない問題に対処
/// 1.0.0 (2019-06-13)
/// 公開
/// </remarks>
public const string Version = "1.1.0";
/// <summary>
/// VRMの設定をコピーします。
/// </summary>
/// <param name="source">ヒエラルキーのルート、もしくはプレハブのルートであるコピー元のアバター。</param>
/// <param name="destination">ヒエラルキーのルート、もしくはプレハブのルートであるコピー先のアバター。</param>
public static void Copy(GameObject source, GameObject destination)
{
GameObject destinationPrefab = null;
if (!SceneManager.GetActiveScene().GetRootGameObjects().Contains(destination.gameObject))
{
destinationPrefab = destination;
destination = PrefabUtility.InstantiatePrefab(destination) as GameObject;
}
CopyMeta.Copy(source: source, destination: destination);
CopyVRMBlendShapes.Copy(source: source, destination: destination);
var sourceSkeletonBones = BoneMapper.GetAllSkeletonBones(avatar: source);
CopyFirstPerson.Copy(source: source, destination: destination, sourceSkeletonBones: sourceSkeletonBones);
CopyEyeControl.Copy(source: source, destination: destination, sourceSkeletonBones: sourceSkeletonBones);
CopyVRMSpringBones.Copy(source: source, destination: destination, sourceSkeletonBones: sourceSkeletonBones);
if (destinationPrefab)
{
#if UNITY_2017
PrefabUtility.ReplacePrefab(destination, destinationPrefab, ReplacePrefabOptions.ConnectToPrefab);
#else
PrefabUtility.ApplyPrefabInstance(destination, InteractionMode.AutomatedAction);
#endif
UnityEngine.Object.DestroyImmediate(destination);
}
}
/// <summary>
/// 当エディタ拡張の名称。
/// </summary>
internal const string Name = "CopyVRMSettings.cs";
/// <summary>
/// プレハブのパスを返します。
/// </summary>
/// <param name="prefab">プレハブ、またはプレハブのインスタンス。</param>
/// <returns>「Assets/」から始まるパス。プレハブのインスタンスでなかった場合は空文字列。</returns>
internal static string GetPrefabAssetPath(GameObject prefab)
{
#if UNITY_2017
GameObject root = PrefabUtility.FindPrefabRoot(prefab);
if (!root)
{
return "";
}
return AssetDatabase.GetAssetPath(root);
#else
return PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(prefab);
#endif
}
}
/// <summary>
/// L10N。
/// </summary>
internal class Locales
{
[InitializeOnLoadMethod]
private static void Initialize()
{
Gettext.SetLocalizedTexts(localizedTexts: new Dictionary<string, IDictionary<string, string>> {
{ "ja", new Dictionary<string, string> {
{ "“{0}” is not root object.", "「{0}」はルートオブジェクトではありません。"},
{ "“{0}”and its VRMBlendShapes will be overwritten.", "「{0}」、およびそのVRMBlendShapeは上書きされます。" },
{ "“{0}” is not set “{1}” component.", "「{0}」に「{1}」コンポーネントが設定されていません。" },
{ "“{0}”’s “{1}” is null.", "「{0}」の「{1}」が null です。"},
{ "“{0}” and “{1}” are instances of same prefab.", "「{0」と「{1}」は同一のプレハブのインスタンスです。" },
{ "Copy and Paste", "コピー&ペースト" },
{ "Settings copying and pasting is completed.", "設定のコピー&ペーストが完了しました。" },
{ "OK", "OK" },
}}
});
Gettext.SetLocale(clientLang: Locales.ConvertToLangtagFromSystemLanguage(systemLanguage: Application.systemLanguage));
}
/// <summary>
/// <see cref="SystemLanguage"/>に対応するIETF言語タグを返します。
/// </summary>
/// <param name="systemLanguage"></param>
/// <returns><see cref="SystemLanguage.Unknown"/>の場合は「und」、未知の<see cref="SystemLanguage"/>の場合は空文字列を返します。</returns>
private static string ConvertToLangtagFromSystemLanguage(SystemLanguage systemLanguage)
{
switch (systemLanguage)
{
case SystemLanguage.Afrikaans:
return "af";
case SystemLanguage.Arabic:
return "ar";
case SystemLanguage.Basque:
return "eu";
case SystemLanguage.Belarusian:
return "be";
case SystemLanguage.Bulgarian:
return "bg";
case SystemLanguage.Catalan:
return "ca";
case SystemLanguage.Chinese:
return "zh";
case SystemLanguage.Czech:
return "cs";
case SystemLanguage.Danish:
return "da";
case SystemLanguage.Dutch:
return "nl";
case SystemLanguage.English:
return "en";
case SystemLanguage.Estonian:
return "et";
case SystemLanguage.Faroese:
return "fo";
case SystemLanguage.Finnish:
return "fi";
case SystemLanguage.French:
return "fr";
case SystemLanguage.German:
return "de";
case SystemLanguage.Greek:
return "el";
case SystemLanguage.Hebrew:
return "he";
case SystemLanguage.Hungarian:
return "hu";
case SystemLanguage.Icelandic:
return "is";
case SystemLanguage.Indonesian:
return "in";
case SystemLanguage.Italian:
return "it";
case SystemLanguage.Japanese:
return "ja";
case SystemLanguage.Korean:
return "ko";
case SystemLanguage.Latvian:
return "lv";
case SystemLanguage.Lithuanian:
return "lt";
case SystemLanguage.Norwegian:
return "no";
case SystemLanguage.Polish:
return "pl";
case SystemLanguage.Portuguese:
return "pt";
case SystemLanguage.Romanian:
return "ro";
case SystemLanguage.Russian:
return "ru";
case SystemLanguage.SerboCroatian:
return "sh";
case SystemLanguage.Slovak:
return "sk";
case SystemLanguage.Slovenian:
return "sl";
case SystemLanguage.Spanish:
return "es";
case SystemLanguage.Swedish:
return "sv";
case SystemLanguage.Thai:
return "th";
case SystemLanguage.Turkish:
return "tr";
case SystemLanguage.Ukrainian:
return "uk";
case SystemLanguage.Vietnamese:
return "vi";
case SystemLanguage.ChineseSimplified:
return "zh-Hans";
case SystemLanguage.ChineseTraditional:
return "zh-Hant";
case SystemLanguage.Unknown:
return "und";
}
return "";
}
}
/// <summary>
/// i18n。
/// </summary>
internal class Gettext
{
/// <summary>
/// 翻訳対象文字列 (msgid) の言語。IETF言語タグの「language」サブタグ。
/// </summary>
private static readonly string OriginalLocale = "en";
/// <summary>
/// クライアントの言語の翻訳リソースが存在しないとき、どの言語に翻訳するか。IETF言語タグの「language」サブタグ。
/// </summary>
private static readonly string DefaultLocale = "en";
/// <summary>
/// クライアントの言語。<see cref="Gettext.SetLocale"/>から変更されます。
/// </summary>
private static string langtag = "en";
/// <summary>
/// クライアントの言語のlanguage部分。<see cref="Gettext.SetLocale"/>から変更されます。
/// </summary>
private static string language = "en";
/// <summary>
/// 翻訳リソース。<see cref="Gettext.SetLocalizedTexts"/>から変更されます。
/// </summary>
private static IDictionary<string, IDictionary<string, string>> multilingualLocalizedTexts = new Dictionary<string, IDictionary<string, string>> { };
/// <summary>
/// 翻訳リソースを追加します。
/// </summary>
/// <param name="localizedTexts"></param>
internal static void SetLocalizedTexts(IDictionary<string, IDictionary<string, string>> localizedTexts)
{
Gettext.multilingualLocalizedTexts = localizedTexts;
}
/// <summary>
/// クライアントの言語を設定します。
/// </summary>
/// <param name="clientLang">IETF言語タグ (「language」と「language-REGION」にのみ対応)。</param>
internal static void SetLocale(string clientLang)
{
string[] splitedClientLang = clientLang.Split(separator: '-');
Gettext.language = splitedClientLang[0].ToLower();
Gettext.langtag = string.Join(
separator: "-",
value: splitedClientLang,
startIndex: 0,
count: Math.Min(2, splitedClientLang.Length)
);
if (Gettext.language == "ja")
{
// ja-JPをjaと同一視
Gettext.langtag = Gettext.language;
}
}
/// <summary>
/// テキストをクライアントの言語に変換します。
/// </summary>
/// <param name="message">翻訳前。</param>
/// <returns>翻訳語。</returns>
internal static string _(string message)
{
if (Gettext.langtag == Gettext.OriginalLocale)
{
// クライアントの言語が翻訳元の言語なら、そのまま返す
return message;
}
foreach (string langtag in new[] {
// クライアントの言語の翻訳リソースが存在すれば、それを返す
Gettext.langtag,
// 地域下位タグを取り除いた言語タグの翻訳リソースが存在すれば、それを返す
Gettext.language,
// 既定言語の翻訳リソースが存在すれば、それを返す
Gettext.DefaultLocale,
})
{
if (Gettext.multilingualLocalizedTexts.ContainsKey(langtag)
&& Gettext.multilingualLocalizedTexts[Gettext.langtag].ContainsKey(message)
&& Gettext.multilingualLocalizedTexts[Gettext.langtag][message] != "")
{
return Gettext.multilingualLocalizedTexts[Gettext.langtag][message];
}
}
return message;
}
}
/// <summary>
/// ダイアログ。
/// </summary>
public class Wizard : ScriptableWizard
{
/// <summary>
/// 追加するメニューアイテムの、「VRM」メニュー内の位置。
/// </summary>
public const int Priority = 1101;
/// <summary>
/// 設定のコピー元のアバター。
/// </summary>
[SerializeField]
private Animator sourceAvatar = null;
/// <summary>
/// 設定のコピー先のアバター。
/// </summary>
[SerializeField]
private Animator destinationAvatar = null;
/// <summary>
/// 選択されているアバターの変換ダイアログを開きます。
/// </summary>
[MenuItem("VRM/" + CopyVRMSettings.Name + "-" + CopyVRMSettings.Version, false, Wizard.Priority)]
private static void OpenWizard()
{
Wizard.Open();
}
/// <summary>
/// ダイアログを開きます。
/// </summary>
internal static void Open()
{
var wizard = DisplayWizard<Wizard>(
CopyVRMSettings.Name + " " + CopyVRMSettings.Version,
Gettext._("Copy and Paste")
);
}
protected override bool DrawWizardGUI()
{
base.DrawWizardGUI();
this.isValid = true;
EditorGUILayout.HelpBox(string.Format(
Gettext._("“{0}”and its VRMBlendShapes will be overwritten."),
"Destination Avatar"
), MessageType.None);
if (this.sourceAvatar && this.destinationAvatar
&& CopyVRMSettings.GetPrefabAssetPath(this.sourceAvatar.gameObject)
== CopyVRMSettings.GetPrefabAssetPath(this.destinationAvatar.gameObject))
{
EditorGUILayout.HelpBox(string.Format(
Gettext._("“{0}” and “{1}” are instances of same prefab."),
"Source Avatar",
"Destination Avatar"
), MessageType.Error);
this.isValid = false;
}
foreach (var labelAndAnimator in new Dictionary<string, Animator> {
{ "Source Avatar", this.sourceAvatar },
{ "Destination Avatar", this.destinationAvatar },
})
{
if (!labelAndAnimator.Value)
{
this.isValid = false;
continue;
}
Transform transform = labelAndAnimator.Value.transform;
if (transform != transform.root)
{
EditorGUILayout.HelpBox(
string.Format(Gettext._("“{0}” is not root object."), labelAndAnimator.Key),
MessageType.Error
);
this.isValid = false;
continue;
}
foreach (var typeAndPropertyName in CopyVRMSettings.RequiredComponentsAndFields)
{
var component = labelAndAnimator.Value.GetComponent(typeAndPropertyName.Key);
if (!labelAndAnimator.Value.GetComponent(typeAndPropertyName.Key))
{
EditorGUILayout.HelpBox(string.Format(
Gettext._("“{0}” is not set “{1}” component."),
labelAndAnimator.Key,
typeAndPropertyName.Key
), MessageType.Error);
this.isValid = false;
continue;
}
if (string.IsNullOrEmpty(typeAndPropertyName.Value))
{
continue;
}
if (typeAndPropertyName.Key.GetField(
name: typeAndPropertyName.Value,
bindingAttr: BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public
).GetValue(obj: component) == null)
{
EditorGUILayout.HelpBox(string.Format(
Gettext._("“{0}”’s “{1}” is null."),
labelAndAnimator.Key,
typeAndPropertyName.Key + "." + typeAndPropertyName.Value
), MessageType.Error);
this.isValid = false;
continue;
}
}
}
return true;
}
private void OnWizardCreate()
{
CopyVRMSettings.Copy(
source: this.sourceAvatar.gameObject,
destination: this.destinationAvatar.gameObject
);
EditorUtility.DisplayDialog(
CopyVRMSettings.Name + "-" + CopyVRMSettings.Version,
Gettext._("Settings copying and pasting is completed."),
Gettext._("OK")
);
}
}
internal class CopyMeta
{
/// <summary>
/// モデル情報をコピーします。
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
/// <returns></returns>
internal static void Copy(GameObject source, GameObject destination)
{
VRMMetaObject sourceMeta = source.GetComponent<VRMMeta>().Meta;
VRMMetaObject destinationMeta = destination.GetComponent<VRMMeta>().Meta;
if (sourceMeta == destinationMeta)
{
return;
}
CopyMeta.CopyInformation(source: source, destination: destination);
CopyMeta.CopyLicense(sourceMeta: sourceMeta, destinationMeta: destinationMeta);
CopyMeta.CopyRedistributionAndModificationsLicense(
sourceMeta: sourceMeta,
destinationMeta: destinationMeta
);
return;
}
/// <summary>
/// 情報をコピーします。
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
private static void CopyInformation(GameObject source, GameObject destination)
{
VRMMetaObject sourceMeta = source.GetComponent<VRMMeta>().Meta;
VRMMetaObject destinationMeta = destination.GetComponent<VRMMeta>().Meta;
destinationMeta.Title = sourceMeta.Title;
destinationMeta.Version = sourceMeta.Version;
destinationMeta.Author = sourceMeta.Author;
destinationMeta.ContactInformation = sourceMeta.ContactInformation;
destinationMeta.Reference = sourceMeta.Reference;
destinationMeta.Thumbnail = sourceMeta.Thumbnail;
if (!destinationMeta.Thumbnail)
{
return;
}
string sourceThumbnailPath = AssetDatabase.GetAssetPath(destinationMeta.Thumbnail);
if (UnityPath.FromUnityPath(sourceThumbnailPath).Parent.Value
!= UnityPath.FromAsset(source).GetAssetFolder(suffix: ".Textures").Value)
{
return;
}
string destinationPrefabPath = CopyVRMSettings.GetPrefabAssetPath(destination);
if (string.IsNullOrEmpty(destinationPrefabPath))
{
return;
}
string destinationThumbnailPath = UnityPath.FromUnityPath(destinationPrefabPath)
.GetAssetFolder(suffix: ".Textures")
.Child(Path.GetFileName(sourceThumbnailPath)).GenerateUniqueAssetPath().Value;
AssetDatabase.CopyAsset(sourceThumbnailPath, destinationThumbnailPath);
destinationMeta.Thumbnail = AssetDatabase.LoadAssetAtPath<Texture2D>(destinationThumbnailPath);
}
/// <summary>
/// 使用許諾・ライセンス情報をコピーします。
/// </summary>
/// <param name="sourceMeta"></param>
/// <param name="destinationMeta"></param>
private static void CopyLicense(VRMMetaObject sourceMeta, VRMMetaObject destinationMeta)
{
destinationMeta.AllowedUser = sourceMeta.AllowedUser;
destinationMeta.ViolentUssage = sourceMeta.ViolentUssage;
destinationMeta.SexualUssage = sourceMeta.SexualUssage;
destinationMeta.CommercialUssage = sourceMeta.CommercialUssage;
destinationMeta.OtherPermissionUrl = sourceMeta.OtherPermissionUrl;
}
/// <summary>
/// 再配布・改変に関する許諾範囲をコピーします。
/// </summary>
/// <param name="sourceMeta"></param>
/// <param name="destinationMeta"></param>
private static void CopyRedistributionAndModificationsLicense(
VRMMetaObject sourceMeta,
VRMMetaObject destinationMeta
)
{
destinationMeta.LicenseType = sourceMeta.LicenseType;
destinationMeta.OtherLicenseUrl = sourceMeta.OtherLicenseUrl;
}
}
internal class CopyVRMBlendShapes
{
/// <summary>
/// VRMBlendShapeをコピーします。
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
internal static void Copy(GameObject source, GameObject destination)
{
BlendShapeAvatar sourceBlendShapeAvatar = source.GetComponent<VRMBlendShapeProxy>().BlendShapeAvatar;
BlendShapeAvatar destinationBlendShapeAvatar
= destination.GetComponent<VRMBlendShapeProxy>().BlendShapeAvatar;
if (sourceBlendShapeAvatar == destinationBlendShapeAvatar)
{
return;
}
foreach (BlendShapeClip sourceClip in sourceBlendShapeAvatar.Clips)
{
if (!sourceClip)
{
continue;
}
CopyVRMBlendShapes.CopyBlendShapeClip(sourceClip: sourceClip, source: source, destination: destination);
}
}
/// <summary>
/// コピー元のアバターのBlendShapeClipを基に、コピー先のアバターのBlendShapeClipを書き替えます。
/// </summary>
/// <param name="sourceClip"></param>
/// <param name="source"></param>
/// <param name="destination"></param>
private static void CopyBlendShapeClip(BlendShapeClip sourceClip, GameObject source, GameObject destination)
{
BlendShapeAvatar destinationBlendShapeAvatar
= destination.GetComponent<VRMBlendShapeProxy>().BlendShapeAvatar;
BlendShapeClip destinationClip = destinationBlendShapeAvatar.GetClip(name: sourceClip.BlendShapeName);
if (sourceClip == destinationClip)
{
return;
}
if (!destinationClip)
{
destinationClip = BlendShapeAvatar.CreateBlendShapeClip(
path: UnityPath.FromAsset(destinationBlendShapeAvatar)
.Parent.Child(Path.GetFileName(AssetDatabase.GetAssetPath(sourceClip))).Value
);
destinationBlendShapeAvatar.Clips.Add(destinationClip);
EditorUtility.SetDirty(destinationBlendShapeAvatar);
}
destinationClip.Values = sourceClip.Values.Select(binding =>
CopyVRMBlendShapes.CopyBlendShapeBinding(binding: binding, source: source, destination: destination)
).ToArray();
destinationClip.MaterialValues = sourceClip.MaterialValues.ToArray();
EditorUtility.SetDirty(destinationClip);
}
/// <summary>
/// コピー元のアバターのBlendShapeBindingを基に、コピー先のアバターのBlendShapeBindingを生成します。
/// </summary>
/// <param name="sourceBinding"></param>
/// <param name="source"></param>
/// <param name="destination"></param>
/// <returns></returns>
private static BlendShapeBinding CopyBlendShapeBinding(
BlendShapeBinding binding,
GameObject source,
GameObject destination
)
{
Mesh sourceMesh = CopyVRMBlendShapes.GetMesh(binding: binding, avatar: source);
if (!sourceMesh)
{
return binding;
}
string shapeKeyName = sourceMesh.GetBlendShapeName(binding.Index);
Mesh destinationMesh = CopyVRMBlendShapes.GetMesh(relativePath: binding.RelativePath, avatar: destination);
if (destinationMesh)
{
int index = destinationMesh.GetBlendShapeIndex(shapeKeyName);
if (index != -1)
{
binding.Index = index;
return binding;
}
}
return CopyVRMBlendShapes.FindShapeKey(binding: binding, shapeKeyName: shapeKeyName, avatar: destination);
}
/// <summary>
/// 指定したパスからメッシュを取得します。
/// </summary>
/// <param name="binding"></param>
/// <param name="avatar"></param>
/// <returns></returns>
private static Mesh GetMesh(string relativePath, GameObject avatar)
{
Transform transform = avatar.transform.Find(relativePath);
if (!transform)
{
return null;
}
var renderer = transform.GetComponent<SkinnedMeshRenderer>();
if (!renderer)
{
return null;
}
return renderer.sharedMesh;
}
/// <summary>
/// BlendShapeBindingに対応するメッシュを返します。
/// </summary>
/// <param name="binding"></param>
/// <param name="avatar"></param>
/// <returns></returns>
private static Mesh GetMesh(BlendShapeBinding binding, GameObject avatar)
{
Mesh mesh = CopyVRMBlendShapes.GetMesh(relativePath: binding.RelativePath, avatar: avatar);
if (!mesh || binding.Index > mesh.blendShapeCount)
{
return null;
}
return mesh;
}
/// <summary>
/// 指定されたシェイプキー名を持つメッシュを探し、見つからなければ後方一致するものを探し、BlendShapeBindingを書き替えて返します。
/// </summary>
/// <param name="binding"></param>
/// <param name="shapeKeyName"></param>
/// <param name="avatar"></param>
/// <returns>見つからなかった場合は <c>binding</c> をそのまま返します。</returns>
private static BlendShapeBinding FindShapeKey(BlendShapeBinding binding, string shapeKeyName, GameObject avatar)
{
var renderers = avatar.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var renderer in renderers)
{
Mesh mesh = renderer.sharedMesh;
if (!mesh)
{
continue;
}
int index = mesh.GetBlendShapeIndex(shapeKeyName);
if (index == -1)
{
continue;
}
binding.RelativePath = renderer.transform.RelativePathFrom(root: avatar.transform);
binding.Index = index;
return binding;
}
foreach (var renderer in renderers)
{
Mesh mesh = renderer.sharedMesh;
if (!mesh)
{
continue;
}
for (var i = 0; i < mesh.blendShapeCount; i++)
{
string name = mesh.GetBlendShapeName(i);
if (!name.EndsWith(shapeKeyName) && !shapeKeyName.EndsWith(name))
{
continue;
}
binding.RelativePath = renderer.transform.RelativePathFrom(root: avatar.transform);
binding.Index = i;
return binding;
}
}
return binding;
}
}
internal class CopyFirstPerson
{
/// <summary>
/// 一人称表示の設定をコピーします。
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
/// <param name="sourceSkeletonBones"></param>
internal static void Copy(
GameObject source,
GameObject destination,
Dictionary<HumanBodyBones,Transform> sourceSkeletonBones
) {
var sourceFirstPerson = source.GetComponent<VRMFirstPerson>();
var destinationFirstPerson = destination.GetComponent<VRMFirstPerson>();
if (!sourceFirstPerson)
{
if (destinationFirstPerson)
{
UnityEngine.Object.DestroyImmediate(destinationFirstPerson);
}
return;
}
if (sourceFirstPerson.FirstPersonBone)
{
destinationFirstPerson.FirstPersonBone = BoneMapper.FindCorrespondingBone(
sourceBone: sourceFirstPerson.FirstPersonBone,
source: source,
destination: destination,
sourceSkeletonBones: sourceSkeletonBones
);
}
destinationFirstPerson.FirstPersonOffset = sourceFirstPerson.FirstPersonOffset;
foreach (VRMFirstPerson.RendererFirstPersonFlags sourceFlags in sourceFirstPerson.Renderers)
{
if (sourceFlags.FirstPersonFlag == FirstPersonFlag.Auto)
{
continue;
}
Mesh sourceMesh = sourceFlags.SharedMesh;
if (!sourceMesh)
{
continue;
}
string sourceMeshName = sourceMesh.name;
int index = destinationFirstPerson.Renderers.FindIndex(match: flags => {
Mesh destinationMesh = flags.SharedMesh;
return destinationMesh && destinationMesh.name == sourceMeshName;
});
if (index == -1)
{
continue;
}
VRMFirstPerson.RendererFirstPersonFlags destinationFlags = destinationFirstPerson.Renderers[index];
destinationFlags.FirstPersonFlag = sourceFlags.FirstPersonFlag;
destinationFirstPerson.Renderers[index] = destinationFlags;
}
}
}
internal class CopyEyeControl
{
/// <summary>
/// 視線制御の設定をコピーします。
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
/// <param name="sourceSkeletonBones"></param>
internal static void Copy(
GameObject source,
GameObject destination,
Dictionary<HumanBodyBones, Transform> sourceSkeletonBones
) {
if (!source.GetComponent<VRMFirstPerson>())
{
return;
}
var sourceLookAtHead = source.GetComponent<VRMLookAtHead>();
if (!sourceLookAtHead)
{
var destinationLookAtHead = destination.GetComponent<VRMLookAtHead>();
if (destinationLookAtHead)
{
UnityEngine.Object.DestroyImmediate(destinationLookAtHead);
}
return;
}
var sourceBoneApplyer = source.GetComponent<VRMLookAtBoneApplyer>();
if (sourceBoneApplyer)
{
ComponentUtility.CopyComponent(sourceBoneApplyer);
var destinationBoneApplyer = destination.GetOrAddComponent<VRMLookAtBoneApplyer>();
ComponentUtility.PasteComponentValues(destinationBoneApplyer);
if (destinationBoneApplyer.LeftEye.Transform)
{
destinationBoneApplyer.LeftEye.Transform = BoneMapper.FindCorrespondingBone(
sourceBone: destinationBoneApplyer.LeftEye.Transform,
source: source,
destination: destination,
sourceSkeletonBones: sourceSkeletonBones
);
}
if (destinationBoneApplyer.RightEye.Transform)
{
destinationBoneApplyer.RightEye.Transform = BoneMapper.FindCorrespondingBone(
sourceBone: destinationBoneApplyer.RightEye.Transform,
source: source,
destination: destination,
sourceSkeletonBones: sourceSkeletonBones
);
}
var blendShapeApplyer = destination.GetComponent<VRMLookAtBlendShapeApplyer>();
if (blendShapeApplyer)
{
UnityEngine.Object.DestroyImmediate(blendShapeApplyer);
}
return;
}
var sourceBlendShapeApplyer = source.GetComponent<VRMLookAtBlendShapeApplyer>();
if (sourceBlendShapeApplyer)
{
ComponentUtility.CopyComponent(sourceBlendShapeApplyer);
var destinationBlendShapeApplyer = destination.GetOrAddComponent<VRMLookAtBlendShapeApplyer>();
ComponentUtility.PasteComponentValues(destinationBlendShapeApplyer);
}
}
}
internal class CopyVRMSpringBones
{
/// <summary>
/// VRMSpringBone、およびVRMSpringBoneColliderGroupをコピーします。
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
/// <param name="sourceSkeletonBones"></param>
internal static void Copy(
GameObject source,
GameObject destination,
Dictionary<HumanBodyBones, Transform> sourceSkeletonBones
)
{
foreach (Component component in new[] { typeof(VRMSpringBone), typeof(VRMSpringBoneColliderGroup) }
.SelectMany(type => destination.GetComponentsInChildren(type)))
{
UnityEngine.Object.DestroyImmediate(component);
}
IDictionary<Transform, Transform> transformMapping = new Dictionary<Transform, Transform>();
foreach (var sourceSpringBone in source.GetComponentsInChildren<VRMSpringBone>())
{
if (sourceSpringBone.RootBones.Count == 0)
{
continue;
}
transformMapping = CopyVRMSpringBones.CopySpringBone(
sourceSpringBone: sourceSpringBone,
destination: destination,
sourceSkeletonBones: sourceSkeletonBones,
transformMapping: transformMapping
);
}
CopyVRMSpringBones.CopySpringBoneColliderGroupForVirtualCast(source: source, destination: destination);
}
/// <summary>
/// VRMSpringBone、およびVRMSpringBoneColliderGroupをコピーします。
/// </summary>
/// <param name="sourceSpringBone"></param>
/// <param name="destination"></param>
/// <param name="sourceSkeletonBones"></param>
/// <param name="transformMapping"></param>
/// <returns>更新された <c>transformMapping</c> を返します。</returns>
private static IDictionary<Transform,Transform> CopySpringBone(
VRMSpringBone sourceSpringBone,
GameObject destination,
Dictionary<HumanBodyBones,Transform> sourceSkeletonBones,
IDictionary<Transform,Transform> transformMapping
)
{
GameObject destinationSecondary = destination.transform.Find("secondary").gameObject;
ComponentUtility.CopyComponent(sourceSpringBone);
ComponentUtility.PasteComponentAsNew(destinationSecondary);
VRMSpringBone destinationSpringBone = destinationSecondary.GetComponents<VRMSpringBone>().Last();
for (var i = 0; i < destinationSpringBone.RootBones.Count; i++)
{
Transform sourceSpringBoneRoot = destinationSpringBone.RootBones[i];
Transform destinationBone = null;
if (sourceSpringBoneRoot)
{
if (transformMapping.ContainsKey(sourceSpringBoneRoot))
{
destinationBone = transformMapping[sourceSpringBoneRoot];
}
else
{
destinationBone = BoneMapper.FindCorrespondingBone(
sourceBone: sourceSpringBoneRoot,
source: sourceSpringBone.transform.root.gameObject,
destination: destination,
sourceSkeletonBones: sourceSkeletonBones
);
transformMapping.Add(key: sourceSpringBoneRoot, value: destinationBone);
}
}
destinationSpringBone.RootBones[i] = destinationBone;
}
for (var i = 0; i < destinationSpringBone.ColliderGroups.Length; i++)
{
VRMSpringBoneColliderGroup sourceColliderGroup = destinationSpringBone.ColliderGroups[i];
Transform destinationColliderGroupTransform = null;
if (sourceColliderGroup)
{
if (transformMapping.ContainsKey(sourceColliderGroup.transform))
{
destinationColliderGroupTransform = transformMapping[sourceColliderGroup.transform];
}
else
{
destinationColliderGroupTransform = BoneMapper.FindCorrespondingBone(
sourceBone: sourceColliderGroup.transform,
source: sourceSpringBone.transform.root.gameObject,
destination: destination,
sourceSkeletonBones: sourceSkeletonBones
);
transformMapping
.Add(key: sourceColliderGroup.transform, value: destinationColliderGroupTransform);
}
}
VRMSpringBoneColliderGroup destinationColliderGroup = null;
if (destinationColliderGroupTransform)
{
CopyVRMSpringBones.CopySpringBoneColliderGroups(
sourceBone: sourceColliderGroup.transform,
destinationBone: destinationColliderGroupTransform
);
destinationColliderGroup
= destinationColliderGroupTransform.GetComponent<VRMSpringBoneColliderGroup>();
}
destinationSpringBone.ColliderGroups[i] = destinationColliderGroup;
}
return transformMapping;
}
/// <summary>
/// コピー先にVRMSpringBoneColliderGroupが存在しなければ、コピー元のVRMSpringBoneColliderGroupをすべてコピーします。
/// </summary>
/// <param name="sourceBone"></param>
/// <param name="destinationBone"></param>
private static void CopySpringBoneColliderGroups(Transform sourceBone, Transform destinationBone)
{
if (destinationBone.GetComponent<VRMSpringBoneColliderGroup>())
{
return;
}
foreach (var colliderGroup in sourceBone.GetComponents<VRMSpringBoneColliderGroup>())
{
ComponentUtility.CopyComponent(colliderGroup);
ComponentUtility.PasteComponentAsNew(destinationBone.gameObject);
}
}
/// <summary>
/// バーチャルキャスト向けに、どのVRMSpringBoneにも関連付けられていないVRMSpringBoneColliderGroupをコピーします。
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
private static void CopySpringBoneColliderGroupForVirtualCast(GameObject source, GameObject destination)
{
var sourceAnimator = source.GetComponent<Animator>();
var destinationAnimator = destination.GetComponent<Animator>();
foreach (var humanoidBone in new [] { HumanBodyBones.LeftHand, HumanBodyBones.RightHand })
{
CopyVRMSpringBones.CopySpringBoneColliderGroups(
sourceBone: sourceAnimator.GetBoneTransform(humanoidBone),
destinationBone: destinationAnimator.GetBoneTransform(humanoidBone)
);
}
}
}
internal class BoneMapper
{
/// <summary>
/// すべてのスケルトンボーンを取得します。
/// </summary>
/// <param name="avatar"></param>
/// <returns></returns>
internal static Dictionary<HumanBodyBones, Transform> GetAllSkeletonBones(GameObject avatar)
{
var animator = avatar.GetComponent<Animator>();
return avatar.GetComponent<VRMHumanoidDescription>().Description.human
.Select(boneLimit => boneLimit.humanBone)
.ToDictionary(
keySelector: humanoidBone => humanoidBone,
elementSelector: humanoidBone => animator.GetBoneTransform(humanoidBone)
);
}
/// <summary>
/// コピー元のアバターの指定ボーンと対応する、コピー先のアバターのボーンを返します。
/// </summary>
/// <param name="sourceBoneRelativePath"></param>
/// <param name="source"></param>
/// <param name="destination"></param>
/// <param name="sourceSkeletonBones"></param>
/// <returns>見つからなかった場合は <c>null</c> を返します。</returns>
internal static Transform FindCorrespondingBone(
Transform sourceBone,
GameObject source,
GameObject destination,
Dictionary<HumanBodyBones, Transform> sourceSkeletonBones
)
{
if (!sourceBone.IsChildOf(source.transform))
{
return null;
}
string sourceBoneRelativePath = sourceBone.RelativePathFrom(root: source.transform);
Transform destinationBone = destination.transform.Find(sourceBoneRelativePath);
if (destinationBone)
{
return destinationBone;
}
if (!sourceBone.IsChildOf(source.GetComponent<Animator>().GetBoneTransform(HumanBodyBones.Hips)))
{
return null;
}
var humanoidAndSkeletonBone
= BoneMapper.ClosestSkeletonBone(bone: sourceBone, skeletonBones: sourceSkeletonBones);
Animator destinationAniamtor = destination.GetComponent<Animator>();
Transform destinationSkeletonBone = destinationAniamtor.GetBoneTransform(humanoidAndSkeletonBone.Key);
if (destinationSkeletonBone)
{
return null;
}
destinationBone
= destinationSkeletonBone.Find(sourceBone.RelativePathFrom(root: humanoidAndSkeletonBone.Value));
if (destinationBone)
{
return destinationBone;
}
return destinationSkeletonBone.GetComponentsInChildren<Transform>()
.FirstOrDefault(bone => bone.name == sourceBone.name);
}
/// <summary>
/// 祖先方向へたどり、指定されたボーンを含む直近のスケルトンボーンを取得します。
/// </summary>
/// <param name="bone"></param>
/// <param name="avatar"></param>
/// <param name="skeletonBones"></param>
/// <returns></returns>
private static KeyValuePair<HumanBodyBones, Transform> ClosestSkeletonBone(
Transform bone,
Dictionary<HumanBodyBones, Transform> skeletonBones
)
{
foreach (Transform parent in bone.Ancestors())
{
if (!skeletonBones.ContainsValue(parent))
{
continue;
}
return skeletonBones
.FirstOrDefault(predicate: humanoidAndSkeletonBone => humanoidAndSkeletonBone.Value == parent);
}
throw new ArgumentException();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment