Add support for world-locking bone proxies (useful for cloth setups)

This commit is contained in:
bd_ 2022-11-23 19:20:31 -08:00
parent 8af7b0d5d6
commit d8b1183c30
12 changed files with 212 additions and 42 deletions

View File

@ -45,9 +45,13 @@ namespace nadena.dev.modular_avatar.core.editor
{
var oldPath = RuntimeUtil.AvatarRootPath(proxy.gameObject);
Transform transform = proxy.transform;
transform.SetParent(proxy.target, false);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.SetParent(proxy.target, true);
if (proxy.attachmentMode != BoneProxyAttachmentMode.AsChildKeepWorldPosition)
{
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
}
PathMappings.Remap(oldPath, new PathMappings.MappingEntry()
{
path = RuntimeUtil.AvatarRootPath(proxy.gameObject),

View File

@ -1,6 +1,8 @@
using UnityEditor;
using System;
using UnityEditor;
using UnityEngine;
using static nadena.dev.modular_avatar.core.editor.Localization;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
@ -9,6 +11,15 @@ namespace nadena.dev.modular_avatar.core.editor
public Transform target;
}
[CustomPropertyDrawer(typeof(BoneProxyAttachmentMode))]
internal class BoneProxyAttachmentModeDrawer : EnumDrawer<BoneProxyAttachmentMode>
{
protected override string localizationPrefix => "boneproxy.attachment";
protected override Array enumValues => new object[]
{BoneProxyAttachmentMode.AsChildAtRoot, BoneProxyAttachmentMode.AsChildKeepWorldPosition};
}
[CustomEditor(typeof(ModularAvatarBoneProxy))]
[CanEditMultipleObjects]
internal class BoneProxyEditor : MAEditorBase
@ -81,15 +92,51 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
for (int i = 0; i < targets.Length; i++)
{
CheckAttachmentMode(targets[i] as ModularAvatarBoneProxy);
}
serializedObject.UpdateIfRequiredOrScript();
var p_attachmentMode = serializedObject.FindProperty(nameof(ModularAvatarBoneProxy.attachmentMode));
EditorGUILayout.PropertyField(p_attachmentMode, G("boneproxy.attachment"));
foldout = EditorGUILayout.Foldout(foldout, G("boneproxy.foldout.advanced"));
if (foldout)
{
EditorGUI.indentLevel++;
DrawDefaultInspector();
var p_boneReference = serializedObject.FindProperty(nameof(ModularAvatarBoneProxy.boneReference));
var p_subPath = serializedObject.FindProperty(nameof(ModularAvatarBoneProxy.subPath));
EditorGUILayout.PropertyField(p_boneReference, new GUIContent("Bone reference"));
EditorGUILayout.PropertyField(p_subPath, new GUIContent("Sub path"));
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
Localization.ShowLanguageUI();
}
private void CheckAttachmentMode(ModularAvatarBoneProxy boneProxy)
{
if (boneProxy.attachmentMode == BoneProxyAttachmentMode.Unset && boneProxy.target != null)
{
float posDelta = Vector3.Distance(boneProxy.transform.position, boneProxy.target.position);
float rotDelta = Quaternion.Angle(boneProxy.transform.rotation, boneProxy.target.rotation);
Undo.RecordObject(boneProxy, "Configuring bone proxy attachment mode");
if (posDelta > 0.001f || rotDelta > 0.001f)
{
boneProxy.attachmentMode = BoneProxyAttachmentMode.AsChildKeepWorldPosition;
}
else
{
boneProxy.attachmentMode = BoneProxyAttachmentMode.AsChildAtRoot;
}
}
}
}
}

View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
internal abstract class EnumDrawer<T> : PropertyDrawer where T : Enum
{
protected abstract string localizationPrefix { get; }
protected virtual Array enumValues => Enum.GetValues(typeof(T));
private Dictionary<int, int> _enumToContentIndex;
private Dictionary<int, int> _contentIndexToEnum;
private Dictionary<T, int> _objectToEnumIndex;
private GUIContent[] _content;
private string _cachedLanguage;
internal EnumDrawer()
{
var rawValues = Enum.GetValues(typeof(T));
_objectToEnumIndex = new Dictionary<T, int>();
int i = 0;
foreach (var val in rawValues)
{
_objectToEnumIndex.Add((T) val, i);
i++;
}
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (_content == null || _cachedLanguage != Localization.GetSelectedLocalization())
{
var values = enumValues;
_content = new GUIContent[values.Length];
_enumToContentIndex = new Dictionary<int, int>();
_contentIndexToEnum = new Dictionary<int, int>();
int i = 0;
foreach (var val in values)
{
_enumToContentIndex.Add(_objectToEnumIndex[(T) val], i);
_contentIndexToEnum.Add(i, _objectToEnumIndex[(T) val]);
_content[i++] = Localization.G(localizationPrefix + "." + val);
}
_cachedLanguage = Localization.GetSelectedLocalization();
}
EditorGUI.BeginProperty(position, label, property);
EditorGUI.BeginChangeCheck();
var currentIndex = -1;
if (_enumToContentIndex.ContainsKey(property.enumValueIndex))
{
currentIndex = _enumToContentIndex[property.enumValueIndex];
}
var value = EditorGUI.Popup(position, label, currentIndex, _content);
if (EditorGUI.EndChangeCheck())
{
property.enumValueIndex = _contentIndexToEnum[value];
}
EditorGUI.EndProperty();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e56bad0e98de493cb97f8d31a8873fea
timeCreated: 1669258889

View File

@ -1,30 +1,13 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomPropertyDrawer(typeof(MergeAnimatorPathMode))]
class PathModeDrawer : PropertyDrawer
class PathModeDrawer : EnumDrawer<MergeAnimatorPathMode>
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
EditorGUI.BeginChangeCheck();
var value = EditorGUI.Popup(position, label, property.enumValueIndex, new GUIContent[]
{
G("path_mode.Relative"), G("path_mode.Absolute")
});
if (EditorGUI.EndChangeCheck())
{
property.enumValueIndex = value;
}
EditorGUI.EndProperty();
}
protected override string localizationPrefix => "path_mode";
}
[CustomEditor(typeof(ModularAvatarMergeAnimator))]

View File

@ -84,7 +84,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
private static string GetSelectedLocalization()
public static string GetSelectedLocalization()
{
return EditorPrefs.GetString("nadena.dev.modularavatar.lang", "en");
}

View File

@ -47,5 +47,8 @@
"blendshape.target": "Target blendshape",
"hint.not_in_avatar": "This component needs to be placed inside your avatar to work properly.",
"boneproxy.err.MovingTarget": "You cannot specify a target object that will be moved by other Modular Avatar components",
"boneproxy.err.NotInAvatar": "You must specify an object that is in the avatar"
"boneproxy.err.NotInAvatar": "You must specify an object that is in the avatar",
"boneproxy.attachment": "Attachment mode",
"boneproxy.attachment.AsChildAtRoot": "As child; at root",
"boneproxy.attachment.AsChildKeepWorldPosition": "As child; keep position"
}

View File

@ -47,5 +47,8 @@
"blendshape.target": "このメッシュのブレンドシェープ",
"hint.not_in_avatar": "このコンポーネントが正しく動作するには、アバター内に配置する必要があります。",
"boneproxy.err.MovingTarget": "他のモジュラーアバターコンポーネントで移動されるオブジェクトを指定できません。",
"boneproxy.err.NotInAvatar": "アバター内のオブジェクトを指定してください。"
"boneproxy.err.NotInAvatar": "アバター内のオブジェクトを指定してください。",
"boneproxy.attachment": "配置モード",
"boneproxy.attachment.AsChildAtRoot": "子として・ルートに配置",
"boneproxy.attachment.AsChildKeepWorldPosition": "子として・ワールド位置を維持"
}

View File

@ -28,6 +28,27 @@ using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
public enum BoneProxyAttachmentMode
{
/// <summary>
/// Initial state - this will be updated automatically by the bone proxy inspector, based on checking whether
/// the proxy is located near the base bone.
///
/// If somehow we run a build with this still on default, we'll use AsChildAtRoot.
/// </summary>
Unset,
/// <summary>
/// Places the bone proxy object at the target, with localPosition and localRotation zeroed.
/// </summary>
AsChildAtRoot,
/// <summary>
/// Places the bone proxy object at the target, preserving world position and orientation.
/// </summary>
AsChildKeepWorldPosition,
}
[ExecuteInEditMode]
[DisallowMultipleComponent]
[AddComponentMenu("Modular Avatar/MA Bone Proxy")]
@ -62,6 +83,7 @@ namespace nadena.dev.modular_avatar.core
public HumanBodyBones boneReference = HumanBodyBones.LastBone;
public string subPath;
public BoneProxyAttachmentMode attachmentMode = BoneProxyAttachmentMode.Unset;
void OnValidate()
{
@ -76,7 +98,7 @@ namespace nadena.dev.modular_avatar.core
private void Update()
{
if (!RuntimeUtil.isPlaying && target != null)
if (!RuntimeUtil.isPlaying && target != null && attachmentMode == BoneProxyAttachmentMode.AsChildAtRoot)
{
var targetTransform = target.transform;
var myTransform = transform;

View File

@ -120,6 +120,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
boneReference: 44
subPath:
attachmentMode: 1
--- !u!1 &2929086333716922431
GameObject:
m_ObjectHideFlags: 0

View File

@ -2,26 +2,43 @@
![Bone Proxy](bone-proxy-compare.png)
Bone Proxyは、プレハブのオブジェクトを元アバターのオブジェクトの中に配置させるためのコンポーネントです。
たとえば、[Clapのサンプル](../samples/#clap)では、コンタクトをアバターの手の中に仕込むために使われます。
The Bone Proxy allows you to place objects from your prefab inside of objects that are part of the original avatar.
For example, in the [Clap sample](../samples/#clap), this is used to place contacts inside the avatar's hands.
オブジェクトの元の位置を参照するアニメーションも新しいパスを引用するように修正されます。
Bone Proxy will also adjust any animators referencing the old location of the objects so that they reference the
new paths after the objects are moved.
## いつ使うのか
## When should I use it?
アバターの既存オブジェクトの中にオブジェクトを配置したいときに使います。
Bone Proxy should be used when you have objects that you want to place inside of existing objects inside the avatar.
## 非推奨の使い方
## When shouldn't I use it?
衣装の導入には向いていません。代わりに[Merge Armature](merge-armature.md)を使いましょう。
Bone Proxy isn't intended to be used to configure clothing. Try using [Merge Armature](merge-armature.md) instead.
## 使い方
## Setting up Bone Proxy
Bone Proxyコンポーネントをオブジェクトに追加して、移動先をターゲット欄で指定します。
追加されたオブジェクトがターゲットオブジェクトの中に移動されます。
Add the Bone Proxy component to an object in your prefab, and drag the destination of this object to the "Target" field.
The Bone Proxy-annotated object will then be placed inside the target object.
### プレハブ内の使い方
### Usage in prefabs
指定されたオブジェクトがヒューマノイドボーンとその下のパスとして保存されるので、プレハブ化してもターゲットの引用を復元できます。
The Bone Proxy component automatically translates the object you specify into a humanoid bone and relative path reference.
This ensures that it can restore this reference automatically after it is saved in a prefab.
内部設定を見たい場合は、詳細設定の中で見れます。
If you want to adjust the internal references, you can see them in the Advanced foldout.
### Attachment mode
Bone proxy can be attached in two different ways, depending on use case.
In the "As child at root" attachment mode, the object that the bone proxy is attached to will be reparented to the target object, and
its local position and orientation will be zeroed out. This will place it at the same position and orientation as the target object.
This mode is recommended for prefabs that are not avatar-specific.
In the "As child keep world position" attachment mode, the object that the bone proxy is attached to will be reparented to the target object,
but its world position and orientation will be preserved. This is usually only useful for avatar-specific prefabs, where you want to
place an object at a precise position relative to the parent bone. For example, it can be used to place colliders for cloth components.
When you set the target for a bone proxy component, the attachment mode will be automatically set based on whether the object is
currently at the target bone's position and orientation.

View File

@ -25,4 +25,16 @@ Bone Proxyコンポーネントをプレハブの中のオブジェクトに追
指定したオブジェクトを元に、自動的にヒューマノイドボーンとその先の相対パスに変換するため、
プレハブとして保存してもオブジェクトの引用を復元できます。
内部設定を直接いじりたい場合は詳細設定を開いてください。
内部設定を直接いじりたい場合は詳細設定を開いてください。
### 配置モード
使い方に応じて、配置するモードが二つあります。
「子として・ルートに配置」の設定では、Bone Proxyがアタッチされているオブジェクトがターゲットのオブジェクトの子になり、
位置や姿勢がその親と同じになります。アバターに依存しないプレハブに推奨されます。サンプルのClapやFingerpenもこのモードです。
「子として・ワールド位置を維持」の設定では、Bone Proxyがアタッチされているオブジェクトがターゲットのオブジェクトの子になりますが、
位置や姿勢がワールド座標で維持されます。このモードはアバターに依存してしまうが、例えばClothコライダーの配置などに便利かもしれません。
Bone Proxyのターゲットを設定する時は、ターゲットとの相互位置や姿勢を参考に、配置モードが設定されていない場合は自動的に設定されます。