[BoneProxy] feat: add support for keeping only one of world position/rotation

This commit is contained in:
bd_ 2023-01-17 20:26:56 +09:00
parent 51f7c5936d
commit eb7af61a08
13 changed files with 443 additions and 13 deletions

View File

@ -0,0 +1,61 @@
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using NUnit.Framework.Internal;
using UnityEngine;
namespace modular_avatar_tests
{
public class BoneProxyTest : TestBase
{
[Test]
public void TestBoneProxy()
{
AssertAttachmentMode(BoneProxyAttachmentMode.AsChildAtRoot, expectSnapPos: true, expectSnapRot: true);
AssertAttachmentMode(BoneProxyAttachmentMode.Unset, expectSnapPos: true, expectSnapRot: true);
AssertAttachmentMode(BoneProxyAttachmentMode.AsChildKeepPosition, expectSnapPos: false,
expectSnapRot: true);
AssertAttachmentMode(BoneProxyAttachmentMode.AsChildKeepRotation, expectSnapPos: true,
expectSnapRot: false);
AssertAttachmentMode(BoneProxyAttachmentMode.AsChildKeepWorldPose, expectSnapPos: false,
expectSnapRot: false);
}
private void AssertAttachmentMode(BoneProxyAttachmentMode attachmentMode, bool expectSnapPos,
bool expectSnapRot)
{
var root = CreateRoot("root");
var bone = CreateChild(root, "bone");
var proxy = CreateChild(root, "proxy");
var boneProxy = proxy.AddComponent<ModularAvatarBoneProxy>();
boneProxy.target = bone.transform;
boneProxy.attachmentMode = attachmentMode;
bone.transform.localPosition = Vector3.one;
bone.transform.localRotation = Quaternion.Euler(123, 45, 6);
AvatarProcessor.ProcessAvatar(root);
Assert.AreEqual(proxy.transform.parent, bone.transform);
if (expectSnapPos)
{
Assert.LessOrEqual(Vector3.Distance(proxy.transform.localPosition, Vector3.zero), 0.0001f);
}
else
{
Assert.LessOrEqual(Vector3.Distance(proxy.transform.position, Vector3.zero), 0.0001f);
}
if (expectSnapRot)
{
Assert.LessOrEqual(Quaternion.Angle(proxy.transform.localRotation, Quaternion.identity), 0.0001f);
}
else
{
Assert.LessOrEqual(Quaternion.Angle(proxy.transform.rotation, Quaternion.identity), 0.0001f);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0fe447e8f462493d91a53487af54f2b9
timeCreated: 1673954075

View File

@ -0,0 +1,320 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &6304364033355940381
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4805466113894723065}
- component: {fileID: 7049391172907466529}
- component: {fileID: 7355211162906115147}
- component: {fileID: 515882129511000739}
m_Layer: 0
m_Name: MinimalAvatar
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4805466113894723065
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6304364033355940381}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -0.9999927, y: -0.76461804, z: -2.4018843}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &7049391172907466529
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6304364033355940381}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 542108242, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
m_Name:
m_EditorClassIdentifier:
Name:
ViewPosition: {x: 0, y: 1.6, z: 0.2}
Animations: 0
ScaleIPD: 1
lipSync: 0
lipSyncJawBone: {fileID: 0}
lipSyncJawClosed: {x: 0, y: 0, z: 0, w: 1}
lipSyncJawOpen: {x: 0, y: 0, z: 0, w: 1}
VisemeSkinnedMesh: {fileID: 0}
MouthOpenBlendShapeName: Facial_Blends.Jaw_Down
VisemeBlendShapes: []
unityVersion:
portraitCameraPositionOffset: {x: 0, y: 0, z: 0}
portraitCameraRotationOffset: {x: 0, y: 1, z: 0, w: -0.00000004371139}
customExpressions: 0
expressionsMenu: {fileID: 0}
expressionParameters: {fileID: 0}
enableEyeLook: 0
customEyeLookSettings:
eyeMovement:
confidence: 0.5
excitement: 0.5
leftEye: {fileID: 0}
rightEye: {fileID: 0}
eyesLookingStraight:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingUp:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingDown:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingLeft:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingRight:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidType: 0
upperLeftEyelid: {fileID: 0}
upperRightEyelid: {fileID: 0}
lowerLeftEyelid: {fileID: 0}
lowerRightEyelid: {fileID: 0}
eyelidsDefault:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsClosed:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsLookingUp:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsLookingDown:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsSkinnedMesh: {fileID: 0}
eyelidsBlendshapes:
customizeAnimationLayers: 0
baseAnimationLayers:
- isEnabled: 0
type: 0
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 4
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 5
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
specialAnimationLayers:
- isEnabled: 0
type: 6
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 7
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 8
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
AnimationPreset: {fileID: 0}
animationHashSet: []
autoFootsteps: 1
autoLocomotion: 1
collider_head:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_torso:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_footR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_footL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_handR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_handL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerIndexL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerMiddleL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerRingL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerLittleL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerIndexR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerMiddleR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerRingR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerLittleR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
--- !u!114 &7355211162906115147
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6304364033355940381}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3}
m_Name:
m_EditorClassIdentifier:
launchedFromSDKPipeline: 0
completedSDKPipeline: 0
blueprintId:
contentType: 0
assetBundleUnityVersion:
fallbackStatus: 0
--- !u!95 &515882129511000739
Animator:
serializedVersion: 3
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6304364033355940381}
m_Enabled: 1
m_Avatar: {fileID: 0}
m_Controller: {fileID: 0}
m_CullingMode: 0
m_UpdateMode: 0
m_ApplyRootMotion: 0
m_LinearVelocityBlending: 0
m_WarningMessage:
m_HasTransformHierarchy: 1
m_AllowConstantClipSamplingOptimization: 1
m_KeepAnimatorControllerStateOnDisable: 0

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 60d3416d1f6af4a47bf9056aefc38333
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -12,6 +12,7 @@ namespace modular_avatar_tests
public class TestBase
{
private List<GameObject> objects;
private const string MinimalAvatarGuid = "60d3416d1f6af4a47bf9056aefc38333";
[SetUp]
public void Setup()
@ -36,10 +37,10 @@ namespace modular_avatar_tests
protected GameObject CreateRoot(string name)
{
var go = new GameObject(name);
var path = AssetDatabase.GUIDToAssetPath(MinimalAvatarGuid);
var go = GameObject.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(path));
objects.Add(go);
// Needed for avatar path finding functions to work properly
go.AddComponent(typeof(VRCAvatarDescriptor));
return go;
}

View File

@ -46,11 +46,30 @@ namespace nadena.dev.modular_avatar.core.editor
var oldPath = RuntimeUtil.AvatarRootPath(proxy.gameObject);
Transform transform = proxy.transform;
transform.SetParent(proxy.target, true);
if (proxy.attachmentMode != BoneProxyAttachmentMode.AsChildKeepWorldPosition)
bool keepPos, keepRot;
switch (proxy.attachmentMode)
{
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
default:
case BoneProxyAttachmentMode.Unset:
case BoneProxyAttachmentMode.AsChildAtRoot:
keepPos = keepRot = false;
break;
case BoneProxyAttachmentMode.AsChildKeepWorldPose:
keepPos = keepRot = true;
break;
case BoneProxyAttachmentMode.AsChildKeepPosition:
keepPos = true;
keepRot = false;
break;
case BoneProxyAttachmentMode.AsChildKeepRotation:
keepRot = true;
keepPos = false;
break;
}
if (!keepPos) transform.localPosition = Vector3.zero;
if (!keepRot) transform.localRotation = Quaternion.identity;
}
Object.DestroyImmediate(proxy);

View File

@ -17,7 +17,7 @@ namespace nadena.dev.modular_avatar.core.editor
protected override string localizationPrefix => "boneproxy.attachment";
protected override Array enumValues => new object[]
{BoneProxyAttachmentMode.AsChildAtRoot, BoneProxyAttachmentMode.AsChildKeepWorldPosition};
{BoneProxyAttachmentMode.AsChildAtRoot, BoneProxyAttachmentMode.AsChildKeepWorldPose};
}
[CustomEditor(typeof(ModularAvatarBoneProxy))]
@ -130,7 +130,7 @@ namespace nadena.dev.modular_avatar.core.editor
Undo.RecordObject(boneProxy, "Configuring bone proxy attachment mode");
if (posDelta > 0.001f || rotDelta > 0.001f)
{
boneProxy.attachmentMode = BoneProxyAttachmentMode.AsChildKeepWorldPosition;
boneProxy.attachmentMode = BoneProxyAttachmentMode.AsChildKeepWorldPose;
}
else
{

View File

@ -56,7 +56,9 @@
"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.AsChildKeepWorldPose": "As child; keep position and rotation",
"boneproxy.attachment.AsChildKeepWorldPosition": "As child; keep position",
"boneproxy.attachment.AsChildKeepWorldRotation": "As child; keep rotation",
"pb_blocker.help": "This object will not be affected by PhysBones attached to parents.",
"hint.bad_vrcsdk": "Incompatible version of VRCSDK detected.\n\nPlease try upgrading your VRCSDK; if this does not work, check for a newer version of Modular Avatar as well."
}

View File

@ -54,7 +54,9 @@
"boneproxy.err.NotInAvatar": "アバター内のオブジェクトを指定してください。",
"boneproxy.attachment": "配置モード",
"boneproxy.attachment.AsChildAtRoot": "子として・ルートに配置",
"boneproxy.attachment.AsChildKeepWorldPose": "子として・ワールド位置と向きを維持",
"boneproxy.attachment.AsChildKeepWorldPosition": "子として・ワールド位置を維持",
"boneproxy.attachment.AsChildKeepWorldRotation": "子として・ワールド向きを維持",
"pb_blocker.help": "このオブジェクトは親のPhysBoneから影響を受けなくなります。",
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでもだめでしたら、Modular Avatarにも最新版が出てないかチェックしてください。"
}

View File

@ -59,8 +59,8 @@ namespace nadena.dev.modular_avatar.core.editor
var descriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
InitSessions(descriptor.baseAnimationLayers);
InitSessions(descriptor.specialAnimationLayers);
if (descriptor.baseAnimationLayers != null) InitSessions(descriptor.baseAnimationLayers);
if (descriptor.specialAnimationLayers != null) InitSessions(descriptor.specialAnimationLayers);
var toMerge = avatarGameObject.transform.GetComponentsInChildren<ModularAvatarMergeAnimator>(true);

View File

@ -46,7 +46,17 @@ namespace nadena.dev.modular_avatar.core
/// <summary>
/// Places the bone proxy object at the target, preserving world position and orientation.
/// </summary>
AsChildKeepWorldPosition,
AsChildKeepWorldPose,
/// <summary>
/// Places the bone proxy object at the target, preserving local rotation only.
/// </summary>
AsChildKeepRotation,
/// <summary>
/// Places the bone proxy object at the target, preserving local position only.
/// </summary>
AsChildKeepPosition,
}
[ExecuteInEditMode]

View File

@ -36,9 +36,12 @@ In the "As child at root" attachment mode, the object that the bone proxy is att
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,
In the "As child keep world pose" 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.
You can also opt to keep only one of the original position or rotation, allowing the other to match the target bone. This can sometimes
be useful for more advanced applications.
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

@ -34,7 +34,9 @@ Bone Proxyコンポーネントをプレハブの中のオブジェクトに追
「子として・ルートに配置」の設定では、Bone Proxyがアタッチされているオブジェクトがターゲットのオブジェクトの子になり、
位置や姿勢がその親と同じになります。アバターに依存しないプレハブに推奨されます。サンプルのClapやFingerpenもこのモードです。
「子として・ワールド位置を維持」の設定では、Bone Proxyがアタッチされているオブジェクトがターゲットのオブジェクトの子になりますが、
「子として・ワールド位置と向きを維持」の設定では、Bone Proxyがアタッチされているオブジェクトがターゲットのオブジェクトの子になりますが、
位置や姿勢がワールド座標で維持されます。このモードはアバターに依存してしまうが、例えばClothコライダーの配置などに便利かもしれません。
位置・向きの片方だけ元のままにして、もう片方をボーンに合わせることも可能です。複雑なギミックを作るときに役立つ場合もあるかもしれません。
Bone Proxyのターゲットを設定する時は、ターゲットとの相互位置や姿勢を参考に、配置モードが設定されていない場合は自動的に設定されます。