Skip to content

Instantly share code, notes, and snippets.

@autch
Created July 3, 2024 16:48
Show Gist options
  • Save autch/05f30a5892a04dea22ab70bfe516b3c4 to your computer and use it in GitHub Desktop.
Save autch/05f30a5892a04dea22ab70bfe516b3c4 to your computer and use it in GitHub Desktop.
PopulateScaleAdjusters: 素体のArmatureにつけたMA Scale Adjusterを対応する服の同名ボーンにコピーするエディタ拡張

PopulateScaleAdjusters

アバターの GameObject を指定すると、その Armature 以下にある MA ScaleAdjuster を、 MA Merge Armature が置かれた Armature に対して、それぞれ同名のボーンにコピーします。

Modular Avatar #910 が実装されるまでのつなぎなので、VPM対応とかはしないつもり。

背景

https://x.com/autch/status/1807837123082703199

https://x.com/autch/status/1808168689256354077

インストール

Modular Avatar が必要です。

Assets/ 以下のどこかの階層に Editor というフォルダを作って、そこに PSAUtility.cs と PSAMainWindow.cs ファイルを置いてください。

つかいかた

素体と服をセットアップし、Setup Outfit 相当までを済ませてください。 服の Armature に MA Merge Armature がアタッチされているはずです。服の Prefab に設定済みかもしれないし、自力で設定しても構いません。

素体側の Armature に、MA Scale Adjuster を設定しておきます。

エディタの Tools メニュー→Autch→PopulateScaleAdjusters を選んで PopulateScaleAdjusters ウィンドウを開きます。

image

ウィンドウの「アバターの GameObject」に、素体側のトップレベルオブジェクト(Animatorコンポーネントがアタッチされているはず)を指定します。

「このアバターのArmature直下のScaleAdjuster」には、素体側 Armature に設定されている MA Scale Adjuster がリストされます。 ここに出てこないものは服側にコピーされません。

「このアバターのアイテムのScaleAdjuster」には、服側の Armature に設定されている MA Scale Adjuster がリストされます。 これはすでに服側に設定されているものなので、後述の「既存のものはScaleを更新する」にチェックしていない限り変更されません。 ここに服がリストされるには服の Armature に MA Merge Armature が設定されていなければいけません。

「このアバターのアイテムのScaleAdjuster未適用のボーン」には、素体側のボーンに MA Scale Adjuster がついているのに、 対応する服側のボーンについてまだ MA Scale Adjuster がついていないものがリストされます。 後述の「存在しない場合は作成する」にチェックがついていると、これらについて MA Scale Adjuster をアタッチして 素体側の MA Scale Adjuster のスケールを適用します。

「既存のものはScaleを更新する」オプションは、チェックするとすでに服側に設定されている MA Scale Adjuster に対して、 対応する素体側の MA Scale Adjuster のスケール値を上書き適用します。素体と服のスケールをそろえる場合はこれにチェックします。

非対応服を着せるためなどで服側には素体と異なるスケールを適用する必要があるときは、服側の Armature に手動で MA Scale Adjuster を設定して 必要なスケール値を適用し、本ツールを使うときは「既存のものはScaleを更新する」オプションをオフにして運用します。

「存在しない場合は作成する」オプションは、チェックすると素体側のボーンに MA Scale Adjuster がついているのに、 服側のまだ MA Scale Adjuster がついていないボーンに対して、MA Scale Adjuster を追加してスケール値を素体側からコピーします。 チェックしなければ未設定のボーンはそのままです。

以上の適用対象を確認して「コピーする」を押すと、素体ボーンの MA Scale Adjuster が対応する服のボーンへ適用されます。

ライセンス

PSAUtility.cs の一部は ModularAvatarMergeArmature コンポーネント由来のソース を含みます。

私が書いた部分については CC0 とします。

using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.core;
using UnityEditor;
using UnityEngine;
namespace Autch.Editor.PopulateScaleAdjusters
{
public class PSAMainWindow : EditorWindow
{
[@MenuItem("Tools/Autch/PopulateScaleAdjusters")]
public static void OpenWindow()
{
var w = GetWindow<PSAMainWindow>();
w.titleContent = new GUIContent("PopulateScaleAdjusters");
w.Show();
}
private GameObject _avatarObject;
private Animator _animator;
private Transform _hips;
private bool _createIfMissing = true;
private bool _updateExisting = true;
private Vector2 _scrollPosition = Vector2.zero;
private bool _showAdjusters = true, _showChildAdjusters = true, _showChildrenToPopulate = true;
private List<GameObject> _parentAdjusters = new();
private readonly List<(string, GameObject)> _childAdjusters = new(); // (path, bone having SA)
private readonly List<(string, GameObject)> _childrenToPopulate = new(); // (path, bone without SA)
public void OnGUI()
{
EditorGUILayout.Space();
GUILayout.Label("Populate Scale Adjusters", new GUIStyle()
{
fontSize = 16,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleCenter,
normal =
{
textColor = EditorGUIUtility.isProSkin ? Color.white : Color.black
}
});
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"アバターの GameObject を指定して、その Armature 以下にある MA ScaleAdjuster を、 MA Merge Armature が置かれた Armature 以下の同名のボーンにコピーします",
MessageType.None, true);
var changed = false;
EditorGUILayout.BeginHorizontal();
var newAvatar =
EditorGUILayout.ObjectField("アバターのGameObject", _avatarObject, typeof(GameObject), true) as GameObject;
if (newAvatar != _avatarObject)
{
changed = true;
_avatarObject = newAvatar;
}
if (GUILayout.Button(EditorGUIUtility.TrIconContent("Refresh", "Reload"), GUILayout.Width(30)))
{
// ボタンが押された時の処理
changed = true;
}
EditorGUILayout.EndHorizontal();
if (changed)
{
if (_avatarObject != null)
{
_animator = _avatarObject.GetComponent<Animator>();
if (_animator != null)
{
_hips = _animator.GetBoneTransform(HumanBodyBones.Hips);
if (_hips != null)
_parentAdjusters = PSAUtility.EnumerateScaleAdjusters(_hips);
}
}
else
{
_parentAdjusters.Clear();
}
}
if (_animator == null)
{
EditorGUILayout.HelpBox("Animatorを含むアバターのGameObjectを指定してください", MessageType.Error);
_parentAdjusters.Clear();
}
if (_hips == null)
{
EditorGUILayout.HelpBox("アバターにHipsボーンが見つかりませんでした", MessageType.Error);
_parentAdjusters.Clear();
}
EditorGUILayout.Space();
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
if (_parentAdjusters.Count > 0)
{
_showAdjusters = EditorGUILayout.BeginFoldoutHeaderGroup(_showAdjusters,
$"このアバターのArmature直下のScaleAdjuster ({_parentAdjusters.Count})");
if (_showAdjusters)
{
EditorGUI.BeginDisabledGroup(true);
foreach (var t in _parentAdjusters)
{
EditorGUILayout.ObjectField(t, typeof(GameObject), true);
}
EditorGUI.EndDisabledGroup();
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
else if (_animator != null && _hips != null)
{
EditorGUILayout.HelpBox("このアバターのArmature直下にScaleAdjusterは見つかりませんでした", MessageType.Info);
}
EditorGUILayout.Space();
if (_avatarObject != null && _animator != null && _hips != null &&
_avatarObject.GetComponentsInChildren<ModularAvatarMergeArmature>(true).Length == 0)
{
EditorGUILayout.HelpBox("このアバターにはMA Merge Armatureが設定されていません", MessageType.Info);
}
if (_parentAdjusters.Count > 0)
{
if (changed)
PSAUtility.EnumerateChildAdjusters(_avatarObject, _childAdjusters, _childrenToPopulate);
_showChildAdjusters = ListChildAdjusters(_showChildAdjusters, _childAdjusters,
"このアバターのアイテムのScaleAdjuster ({0})", "アイテムにScaleAdjusterは見つかりませんでした");
EditorGUILayout.Space();
_showChildrenToPopulate = ListChildAdjusters(_showChildrenToPopulate, _childrenToPopulate,
"このアバターのアイテムのScaleAdjuster未適用のボーン ({0})", "アイテムにScaleAdjuster未適用のボーンは見つかりませんでした");
}
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
EditorGUILayout.LabelField("オプション", EditorStyles.boldLabel);
_createIfMissing = EditorGUILayout.Toggle("存在しない場合は作成する", _createIfMissing);
_updateExisting = EditorGUILayout.Toggle("既存のものはScaleを更新する", _updateExisting);
GUI.enabled = _avatarObject != null && _parentAdjusters.Count > 0 &&
(_childAdjusters.Count > 0 || _childrenToPopulate.Count > 0);
if (GUILayout.Button("コピーする", GUILayout.Height(30)))
{
PSAUtility.PopulateScaleAdjusters(_avatarObject, _createIfMissing, _updateExisting);
}
GUI.enabled = true;
}
private bool ListChildAdjusters(bool toggle, List<(string, GameObject)> childAdjusters, string labelTitle,
string labelIfNone)
{
var t = EditorGUILayout.BeginFoldoutHeaderGroup(toggle, string.Format(labelTitle, childAdjusters.Count));
if (t)
{
if (childAdjusters.Count > 0)
{
EditorGUI.BeginDisabledGroup(true);
foreach (var c in childAdjusters)
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField(c.Item1);
EditorGUILayout.ObjectField(c.Item2, typeof(GameObject), true);
}
}
EditorGUI.EndDisabledGroup();
}
else
{
EditorGUILayout.LabelField(labelIfNone);
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
return t;
}
}
}
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.core;
using nadena.dev.ndmf.runtime;
using UnityEditor;
using UnityEngine;
namespace Autch.Editor.PopulateScaleAdjusters
{
public class PSAUtility
{
public static List<GameObject> EnumerateScaleAdjusters(Transform root)
{
return root.GetComponentsInChildren<ModularAvatarScaleAdjuster>(true).Select(c => c.gameObject).ToList();
}
public static void EnumerateChildAdjusters(GameObject avatarObject, List<(string, GameObject)> childAdjusters,
List<(string, GameObject)> childrenToPopulate)
{
childAdjusters.Clear();
childrenToPopulate.Clear();
foreach (var mama in avatarObject.GetComponentsInChildren<ModularAvatarMergeArmature>(true))
{
var path = RuntimeUtil.RelativePath(avatarObject, mama.gameObject);
var boneMap = GetBonesForLock(mama, mama.mergeTargetObject);
foreach (var (baseBone, mergeBone) in boneMap)
{
var baseFound = baseBone.gameObject.TryGetComponent<ModularAvatarScaleAdjuster>(out _);
var mergeFound = mergeBone.gameObject.TryGetComponent<ModularAvatarScaleAdjuster>(out _);
if (!baseFound) continue;
if (!mergeFound)
childrenToPopulate.Add((path, mergeBone.gameObject));
else
childAdjusters.Add((path, mergeBone.gameObject));
}
}
}
public static void PopulateScaleAdjusters(GameObject avatarObject, bool createIfMissing, bool updateExisting)
{
foreach (var mama in avatarObject.GetComponentsInChildren<ModularAvatarMergeArmature>(true))
{
var boneMap = GetBonesForLock(mama, mama.mergeTargetObject);
foreach (var (baseBone, mergeBone) in boneMap)
{
var baseFound = baseBone.gameObject.TryGetComponent<ModularAvatarScaleAdjuster>(out var saBase);
var mergeFound = mergeBone.gameObject.TryGetComponent<ModularAvatarScaleAdjuster>(out var saMerge);
if (!baseFound) continue;
if (!mergeFound)
{
if (createIfMissing)
{
var saNew = Undo.AddComponent<ModularAvatarScaleAdjuster>(mergeBone.gameObject);
Undo.RecordObject(saNew, "Update Scale");
saNew.Scale = saBase.Scale;
}
}
else
{
if (updateExisting)
{
Undo.RecordObject(saMerge, "Update Scale");
saMerge.Scale = saBase.Scale;
}
}
}
}
}
#region Code stolen from ModularAvatarMergeArmature
// https://github.com/bdunderscore/modular-avatar/blob/main/Runtime/ModularAvatarMergeArmature.cs
public static List<(Transform, Transform)> GetBonesForLock(ModularAvatarMergeArmature mama, GameObject baseRoot)
{
if (mama == null) return null;
if (baseRoot == null) return null;
var mergeRoot = mama.transform;
List<(Transform, Transform)> mergeBones = new();
ScanHierarchy(mergeRoot, baseRoot.transform);
return mergeBones;
void ScanHierarchy(Transform merge, Transform baseBone)
{
foreach (Transform t in merge)
{
var subMerge = t.GetComponent<ModularAvatarMergeArmature>();
if (subMerge != null && subMerge != mama) continue;
var baseChild = FindCorrespondingBone(mama, t, baseBone);
if (baseChild != null)
{
mergeBones.Add((baseChild, t));
ScanHierarchy(t, baseChild);
}
}
}
}
private static Transform FindCorrespondingBone(ModularAvatarMergeArmature mama, Transform bone,
Transform baseParent)
{
var childName = bone.gameObject.name;
if (!childName.StartsWith(mama.prefix) || !childName.EndsWith(mama.suffix)
|| childName.Length == mama.prefix.Length + mama.suffix.Length)
return null;
var targetObjectName = childName.Substring(mama.prefix.Length,
childName.Length - mama.prefix.Length - mama.suffix.Length);
return baseParent.Find(targetObjectName);
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment