diff --git a/.docs/content/docs/reference/merge-physbone/index.ja.md b/.docs/content/docs/reference/merge-physbone/index.ja.md index f9835713c..2862abaa2 100644 --- a/.docs/content/docs/reference/merge-physbone/index.ja.md +++ b/.docs/content/docs/reference/merge-physbone/index.ja.md @@ -39,6 +39,9 @@ MultiChildTypeはIgnoreになります。 それぞれの項目について、統合対象の項目から値をコピーする場合は`Copy`(すべての統合対象で値が同じ場合のみ有効)、 代わりに新しい値を設定する場合は`Override`を選択してください。 -コライダーについては、`Merge`を選択して統合対象のコライダー一覧を統合することも出来ます。 +コライダーについては、`Merge`を選択して統合対象のコライダー一覧を統合することができます。 -Endpoint Positionについては、`Clear`を選択して[Clear Endpoint Position](../clear-endpoint-position)を使用することもできます。 +Endpoint Positionについては、`Clear`を選択して[Clear Endpoint Position](../clear-endpoint-position)を使用することができます。 + +角度制限では、`Fix`を選択することで、ボーンに対する捻るような回転(Roll)の値を自動で揃えられます。 +これにより、Rollの値だけが異なっているような場合に角度制限を纏めて適用することができます。 \ No newline at end of file diff --git a/.docs/content/docs/reference/merge-physbone/index.md b/.docs/content/docs/reference/merge-physbone/index.md index 5fcd8f1d4..d0bc77977 100644 --- a/.docs/content/docs/reference/merge-physbone/index.md +++ b/.docs/content/docs/reference/merge-physbone/index.md @@ -41,3 +41,6 @@ For each property, you may select `Copy` to copy value from source property For colliders, you can select `Merge` to merge colliders array from source physbones. For Endpoint Position, you can select `Clear` to apply [Clear Endpoint Position](../clear-endpoint-position) + +For Limit Rotation, you can select `Fix` to fix different roll axis on the model. +If your model has different roll bones, you can limit their rotations together with this option. diff --git a/CHANGELOG-PRERELEASE.md b/CHANGELOG-PRERELEASE.md index 1f4bacd26..eadac080b 100644 --- a/CHANGELOG-PRERELEASE.md +++ b/CHANGELOG-PRERELEASE.md @@ -14,6 +14,11 @@ The format is based on [Keep a Changelog]. - AAO 1.8.0 introduced BlendShape support for Merge Skinned Mesh, but new default mode "Rename to avoid conflicts" would increase number of BlendShape. - This feature is added to relax this problem by automatically merging multiple BlendShapes of one Mesh. - With this feature, you can use rename mode without performance loss. +- Fix mode for PhysBone Limits in Merge PhysBone `#665` + - In addition to existing `Copy` and `Override`, we added `Fix` mode. + - This mode will try to correct roll axis by rotating bone. + - This feature allows you to configure the mode for PhysBone Limits in Merge PhysBone. + - This is useful if all configuration is same but roll axis is different. ### Changed diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d27e0586..5c8bb4326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,11 @@ The format is based on [Keep a Changelog]. - AAO 1.8.0 introduced BlendShape support for Merge Skinned Mesh, but new default mode "Rename to avoid conflicts" would increase number of BlendShape. - This feature is added to relax this problem by automatically merging multiple BlendShapes of one Mesh. - With this feature, you can use rename mode without performance loss. +- Fix mode for PhysBone Limits in Merge PhysBone `#665` + - In addition to existing `Copy` and `Override`, we added `Fix` mode. + - This mode will try to correct roll axis by rotating bone. + - This feature allows you to configure the mode for PhysBone Limits in Merge PhysBone. + - This is useful if all configuration is same but roll axis is different. ### Changed - Skip Enablement Mismatched Renderers is now disabled by default `#1169` diff --git a/Editor/.MergePhysBoneEditorModificationUtils.ts b/Editor/.MergePhysBoneEditorModificationUtils.ts index 8174d252f..120a5a5c2 100644 --- a/Editor/.MergePhysBoneEditorModificationUtils.ts +++ b/Editor/.MergePhysBoneEditorModificationUtils.ts @@ -14,15 +14,6 @@ const config: Config = { ['Curve', 'curve'], ], }, - CurveVector3ConfigProp: { - base: "OverridePropBase", - values: [ - ['Value', 'value'], - ['CurveX', 'curveX'], - ['CurveY', 'curveY'], - ['CurveZ', "curveZ"] - ], - }, PermissionConfigProp: { base: "OverridePropBase", values: [ diff --git a/Editor/Inspector/MergePhysBoneEditor.cs b/Editor/Inspector/MergePhysBoneEditor.cs index e41b187df..b06e89eae 100644 --- a/Editor/Inspector/MergePhysBoneEditor.cs +++ b/Editor/Inspector/MergePhysBoneEditor.cs @@ -307,14 +307,81 @@ protected override void Pb3DCurveProp(string label, string pbXCurveLabel, string pbYCurveLabel, string pbZCurveLabel, CurveVector3ConfigProp prop, bool forceOverride = false) { - PbPropImpl(label, prop, forceOverride, (rect, merged, labelContent) => + var (rect, overrideRect) = SplitRect(EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight), OverrideWidth); + + switch (prop.GetOverride(forceOverride)) { - var (valueRect, buttonRect) = SplitRect(rect, CurveButtonWidth); + case MergePhysBone.CurveVector3Config.CurveOverride.Copy: + { + var valueProp = prop.SourceValue!; + var xCurveProp = prop.SourceCurveX!; + var yCurveProp = prop.SourceCurveY!; + var zCurveProp = prop.SourceCurveZ!; - var valueProp = prop.GetValueProperty(merged); - var xCurveProp = prop.GetCurveXProperty(merged); - var yCurveProp = prop.GetCurveYProperty(merged); - var zCurveProp = prop.GetCurveZProperty(merged); + EditorGUI.BeginDisabledGroup(true); + DrawProperties(rect, new GUIContent(label), valueProp, xCurveProp, yCurveProp, zCurveProp); + EditorGUI.EndDisabledGroup(); + + if (valueProp.hasMultipleDifferentValues + || xCurveProp.hasMultipleDifferentValues + || yCurveProp.hasMultipleDifferentValues + || zCurveProp.hasMultipleDifferentValues) + { + EditorGUILayout.HelpBox(AAOL10N.Tr("MergePhysBone:error:differValueSingle"), MessageType.Error); + } + } + break; + case MergePhysBone.CurveVector3Config.CurveOverride.Override: + { + var valueProp = prop.OverrideValue; + var xCurveProp = prop.OverrideCurveX; + var yCurveProp = prop.OverrideCurveY; + var zCurveProp = prop.OverrideCurveZ; + + DrawProperties(rect, new GUIContent(label), valueProp, xCurveProp, yCurveProp, zCurveProp); + } + break; + case MergePhysBone.CurveVector3Config.CurveOverride.Fix: + { + EditorGUI.LabelField(rect, label, AAOL10N.Tr("MergePhysBone:message:fix-yaw-pitch")); + + if (SourcePhysBones.Any()) + { + foreach (var physBone in SourcePhysBones) + physBone.InitTransforms(force: false); + + // skew scaling is disallowed + if (MergePhysBoneValidator.SkewBones(SourcePhysBones) is { Count: > 0 }) + EditorGUILayout.HelpBox(AAOL10N.Tr("MergePhysBone:error:LimitRotationFix:SkewScaling"), MessageType.Error); + + // error if there is different limit / rotation + if (MergePhysBoneValidator.HasDifferentYawPitch(SourcePhysBones)) + EditorGUILayout.HelpBox(AAOL10N.Tr("MergePhysBone:error:LimitRotationFix:DifferRotation"), MessageType.Error); + + // endpoint position must be zero + switch ((MergePhysBone.EndPointPositionConfig.Override)EndpointPosition.OverrideProperty.enumValueIndex) + { + case MergePhysBone.EndPointPositionConfig.Override.Copy when EndpointPosition.PhysBoneValue!.vector3Value != Vector3.zero: + case MergePhysBone.EndPointPositionConfig.Override.Override when EndpointPosition.ValueProperty.vector3Value != Vector3.zero: + EditorGUILayout.HelpBox(AAOL10N.Tr("MergePhysBone:error:LimitRotationFix:NonZeroEndpointPosition"), MessageType.Error); + break; + } + } + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + + EditorGUI.BeginProperty(overrideRect, null, prop.OverrideProperty); + var selected = PopupNoIndent(overrideRect, prop.OverrideProperty.enumValueIndex, prop.OverrideProperty.enumDisplayNames); + if (selected != prop.OverrideProperty.enumValueIndex) + prop.OverrideProperty.enumValueIndex = selected; + EditorGUI.EndProperty(); + + void DrawProperties(Rect rect, GUIContent labelContent, SerializedProperty valueProp, SerializedProperty xCurveProp, SerializedProperty yCurveProp, SerializedProperty zCurveProp) + { + var (valueRect, buttonRect) = SplitRect(rect, CurveButtonWidth); void DrawCurve(string curveLabel, SerializedProperty curveProp) { @@ -337,7 +404,7 @@ void DrawCurve(string curveLabel, SerializedProperty curveProp) { // without curve: constant EditorGUI.PropertyField(valueRect, valueProp, labelContent); - + if (GUI.Button(buttonRect, "C")) { var curve = new AnimationCurve(); @@ -348,12 +415,7 @@ void DrawCurve(string curveLabel, SerializedProperty curveProp) zCurveProp.animationCurveValue = curve; } } - - return valueProp.hasMultipleDifferentValues - || xCurveProp.hasMultipleDifferentValues - || yCurveProp.hasMultipleDifferentValues - || zCurveProp.hasMultipleDifferentValues; - }); + } } private static readonly string[] CopyOverride = { "C:Copy", "O:Override" }; @@ -568,6 +630,9 @@ protected override void BeginPbConfig() { if (SourcePhysBones.Count() <= 1) BuildLog.LogError("MergePhysBone:error:oneSource"); + + foreach (var vrcPhysBoneBase in SourcePhysBones) + vrcPhysBoneBase.InitTransforms(true); } protected override bool BeginSection(string name, string docTag) => true; @@ -580,8 +645,6 @@ protected override void EndPbConfig() { if (_usingCopyCurve) { - foreach (var vrcPhysBoneBase in SourcePhysBones) - vrcPhysBoneBase.InitTransforms(true); var maxLength = SourcePhysBones.Max(x => x.BoneChainLength()); if (SourcePhysBones.Any(x => x.BoneChainLength() != maxLength)) BuildLog.LogWarning("MergePhysBone:warning:differChainLength", @@ -646,17 +709,93 @@ protected override void Pb3DCurveProp(string label, string pbXCurveLabel, string pbYCurveLabel, string pbZCurveLabel, CurveVector3ConfigProp prop, bool forceOverride = false) { - if (forceOverride || prop.IsOverride) return; + switch (prop.GetOverride(forceOverride)) + { + case MergePhysBone.CurveVector3Config.CurveOverride.Copy: + if (prop.SourceValue!.hasMultipleDifferentValues + || prop.SourceCurveX!.hasMultipleDifferentValues + || prop.SourceCurveY!.hasMultipleDifferentValues + || prop.SourceCurveZ!.hasMultipleDifferentValues) + _differProps.Add(label); + + _usingCopyCurve |= prop.SourceCurveX!.animationCurveValue.length > 0; + _usingCopyCurve |= prop.SourceCurveY!.animationCurveValue.length > 0; + _usingCopyCurve |= prop.SourceCurveZ!.animationCurveValue.length > 0; + break; + case MergePhysBone.CurveVector3Config.CurveOverride.Override: + break; + case MergePhysBone.CurveVector3Config.CurveOverride.Fix: + if (SourcePhysBones.Any()) + { + // skew scaling is disallowed + if (SkewBones(SourcePhysBones) is { Count: > 0 } skewBones) + BuildLog.LogError("MergePhysBone:error:LimitRotationFix:SkewScaling", skewBones); + + // error if there is different limit / rotation + if (HasDifferentYawPitch(SourcePhysBones)) + BuildLog.LogError("MergePhysBone:error:LimitRotationFix:DifferRotation"); + + // endpoint position must be zero + switch ((MergePhysBone.EndPointPositionConfig.Override)EndpointPosition.OverrideProperty.enumValueIndex) + { + case MergePhysBone.EndPointPositionConfig.Override.Copy when EndpointPosition.PhysBoneValue!.vector3Value != Vector3.zero: + case MergePhysBone.EndPointPositionConfig.Override.Override when EndpointPosition.ValueProperty.vector3Value != Vector3.zero: + BuildLog.LogError("MergePhysBone:error:LimitRotationFix:NonZeroEndpointPosition"); + break; + } + } - if (prop.SourceValue!.hasMultipleDifferentValues - || prop.SourceCurveX!.hasMultipleDifferentValues - || prop.SourceCurveY!.hasMultipleDifferentValues - || prop.SourceCurveZ!.hasMultipleDifferentValues) - _differProps.Add(label); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public static List SkewBones(IEnumerable sourcePhysBones) + { + // skew scaling is disallowed + return sourcePhysBones + .SelectMany(x => x.GetAffectedTransforms()) + .Where(x => !Utils.ScaledEvenly(x.localScale)) + .ToList(); + } - _usingCopyCurve |= prop.GetCurveXProperty(false).animationCurveValue.length > 0; - _usingCopyCurve |= prop.GetCurveYProperty(false).animationCurveValue.length > 0; - _usingCopyCurve |= prop.GetCurveZProperty(false).animationCurveValue.length > 0; + public static bool HasDifferentYawPitch(IEnumerable sourcePhysBones) + { + var longestPhysBone = sourcePhysBones.MaxBy(x => x.BoneChainLength()); + + var fixedRotations = Enumerable.Range(0, longestPhysBone.BoneChainLength()) + .Select(index => + { + var time = (float)index / longestPhysBone.BoneChainLength() - 1; + + var rotation = longestPhysBone.CalcLimitRotation(time); + + return Processors.MergePhysBoneProcessor.ConvertRotation(rotation) + with + { + y = 0 + }; + }) + .ToList(); + + var differRotation = sourcePhysBones + .Any(physBone => + { + return Enumerable.Range(0, physBone.BoneChainLength()).Any(index => + { + var time = (float)index / physBone.BoneChainLength() - 1; + var rotation = longestPhysBone.CalcLimitRotation(time); + var fixedRot = Processors.MergePhysBoneProcessor.ConvertRotation(rotation)with + { + y = 0 + }; + + return fixedRot != fixedRotations[index]; + }); + }); + + return differRotation; } protected override void PbPermissionProp(string label, PermissionConfigProp prop, bool forceOverride = false) diff --git a/Editor/MergePhysBoneEditorModificationUtils.cs b/Editor/MergePhysBoneEditorModificationUtils.cs index 2d4b74c1f..4beeccf4f 100644 --- a/Editor/MergePhysBoneEditorModificationUtils.cs +++ b/Editor/MergePhysBoneEditorModificationUtils.cs @@ -377,6 +377,61 @@ internal override void UpdateSource(SerializedObject sourcePb) PhysBoneValue = sourcePb.FindProperty(PhysBoneValueName); } } + + // Very Special Case + protected partial class CurveVector3ConfigProp : PropBase + { + public readonly SerializedProperty OverrideProperty; + public readonly SerializedProperty OverrideValue; + public SerializedProperty? SourceValue { get; private set; } + public readonly string PhysBoneValueName; + public readonly SerializedProperty OverrideCurveX; + public SerializedProperty? SourceCurveX { get; private set; } + public readonly string PhysBoneCurveXName; + public readonly SerializedProperty OverrideCurveY; + public SerializedProperty? SourceCurveY { get; private set; } + public readonly string PhysBoneCurveYName; + public readonly SerializedProperty OverrideCurveZ; + public SerializedProperty? SourceCurveZ { get; private set; } + public readonly string PhysBoneCurveZName; + + public CurveVector3ConfigProp( + SerializedProperty rootProperty + , string physBoneValueName + , string physBoneCurveXName + , string physBoneCurveYName + , string physBoneCurveZName + ) : base(rootProperty) + { + OverrideProperty = rootProperty.FindPropertyRelative("override"); + OverrideValue = rootProperty.FindPropertyRelative("value"); + PhysBoneValueName = physBoneValueName; + OverrideCurveX = rootProperty.FindPropertyRelative("curveX"); + PhysBoneCurveXName = physBoneCurveXName; + OverrideCurveY = rootProperty.FindPropertyRelative("curveY"); + PhysBoneCurveYName = physBoneCurveYName; + OverrideCurveZ = rootProperty.FindPropertyRelative("curveZ"); + PhysBoneCurveZName = physBoneCurveZName; + } + + internal override void UpdateSource(SerializedObject sourcePb) + { + SourceValue = sourcePb.FindProperty(PhysBoneValueName); + SourceCurveX = sourcePb.FindProperty(PhysBoneCurveXName); + SourceCurveY = sourcePb.FindProperty(PhysBoneCurveYName); + SourceCurveZ = sourcePb.FindProperty(PhysBoneCurveZName); + } + + public MergePhysBone.CurveVector3Config.CurveOverride GetOverride(bool forceOverride) => + forceOverride + ? MergePhysBone.CurveVector3Config.CurveOverride.Override + : (MergePhysBone.CurveVector3Config.CurveOverride)OverrideProperty.enumValueIndex; + + public SerializedProperty GetValueProperty(bool @override) => @override ? OverrideValue : SourceValue!; + public SerializedProperty GetCurveXProperty(bool @override) => @override ? OverrideCurveX : SourceCurveX!; + public SerializedProperty GetCurveYProperty(bool @override) => @override ? OverrideCurveY : SourceCurveY!; + public SerializedProperty GetCurveZProperty(bool @override) => @override ? OverrideCurveZ : SourceCurveZ!; + } } } diff --git a/Editor/MergePhysBoneEditorModificationUtils.generated.cs b/Editor/MergePhysBoneEditorModificationUtils.generated.cs index 082318e2c..227565b65 100644 --- a/Editor/MergePhysBoneEditorModificationUtils.generated.cs +++ b/Editor/MergePhysBoneEditorModificationUtils.generated.cs @@ -40,51 +40,6 @@ internal override void UpdateSource(SerializedObject sourcePb) public SerializedProperty GetValueProperty(bool @override) => @override ? OverrideValue : SourceValue!; public SerializedProperty GetCurveProperty(bool @override) => @override ? OverrideCurve : SourceCurve!; } - protected partial class CurveVector3ConfigProp : OverridePropBase - { - public readonly SerializedProperty OverrideValue; - public SerializedProperty? SourceValue { get; private set; } - public readonly string PhysBoneValueName; - public readonly SerializedProperty OverrideCurveX; - public SerializedProperty? SourceCurveX { get; private set; } - public readonly string PhysBoneCurveXName; - public readonly SerializedProperty OverrideCurveY; - public SerializedProperty? SourceCurveY { get; private set; } - public readonly string PhysBoneCurveYName; - public readonly SerializedProperty OverrideCurveZ; - public SerializedProperty? SourceCurveZ { get; private set; } - public readonly string PhysBoneCurveZName; - - public CurveVector3ConfigProp( - SerializedProperty rootProperty - , string physBoneValueName - , string physBoneCurveXName - , string physBoneCurveYName - , string physBoneCurveZName - ) : base(rootProperty) - { - OverrideValue = rootProperty.FindPropertyRelative("value"); - PhysBoneValueName = physBoneValueName; - OverrideCurveX = rootProperty.FindPropertyRelative("curveX"); - PhysBoneCurveXName = physBoneCurveXName; - OverrideCurveY = rootProperty.FindPropertyRelative("curveY"); - PhysBoneCurveYName = physBoneCurveYName; - OverrideCurveZ = rootProperty.FindPropertyRelative("curveZ"); - PhysBoneCurveZName = physBoneCurveZName; - } - - internal override void UpdateSource(SerializedObject sourcePb) - { - SourceValue = sourcePb.FindProperty(PhysBoneValueName); - SourceCurveX = sourcePb.FindProperty(PhysBoneCurveXName); - SourceCurveY = sourcePb.FindProperty(PhysBoneCurveYName); - SourceCurveZ = sourcePb.FindProperty(PhysBoneCurveZName); - } - public SerializedProperty GetValueProperty(bool @override) => @override ? OverrideValue : SourceValue!; - public SerializedProperty GetCurveXProperty(bool @override) => @override ? OverrideCurveX : SourceCurveX!; - public SerializedProperty GetCurveYProperty(bool @override) => @override ? OverrideCurveY : SourceCurveY!; - public SerializedProperty GetCurveZProperty(bool @override) => @override ? OverrideCurveZ : SourceCurveZ!; - } protected partial class PermissionConfigProp : OverridePropBase { public readonly SerializedProperty OverrideValue; diff --git a/Editor/Processors/MergeBoneProcessor.cs b/Editor/Processors/MergeBoneProcessor.cs index f7fbad319..158ca7925 100644 --- a/Editor/Processors/MergeBoneProcessor.cs +++ b/Editor/Processors/MergeBoneProcessor.cs @@ -33,7 +33,7 @@ public static void Validate(MergeBone mergeBone, GameObject root) if (AnyNotMergedBone(mergeBone.transform)) { // if the bone has non-merged bones, uneven scaling is not supported. - if (!ScaledEvenly(mergeBone.transform.localScale)) + if (!Utils.ScaledEvenly(mergeBone.transform.localScale)) BuildLog.LogWarning("MergeBone:validation:unevenScaling"); } @@ -48,13 +48,6 @@ bool AnyNotMergedBone(Transform bone) } } - public static bool ScaledEvenly(Vector3 localScale) - { - bool CheckScale(float scale) => 0.995 < scale && scale < 1.005; - return CheckScale(localScale.x / localScale.y) && CheckScale(localScale.x / localScale.z) && - CheckScale(localScale.y / localScale.z); - } - protected override void Execute(BuildContext context) { // merge from -> merge into diff --git a/Editor/Processors/MergePhysBoneProcessor.cs b/Editor/Processors/MergePhysBoneProcessor.cs index 8257cd4ad..6390eb4be 100644 --- a/Editor/Processors/MergePhysBoneProcessor.cs +++ b/Editor/Processors/MergePhysBoneProcessor.cs @@ -5,6 +5,7 @@ using System.Linq; using Anatawa12.AvatarOptimizer.AnimatorParsersV2; using nadena.dev.ndmf; +using Unity.Mathematics; using UnityEditor; using UnityEngine; using VRC.Dynamics; @@ -122,6 +123,70 @@ internal static void DoMerge(MergePhysBone merge, BuildContext? context) default: throw new ArgumentOutOfRangeException(); } + // == Limits == + + // yaw / pitch fix + if (merge.limitRotationConfig.@override == MergePhysBone.CurveVector3Config.CurveOverride.Fix) + { + var newIgnores = new List(); + // fix rotations + foreach (var physBone in sourceComponents) + FixYawPitch(physBone, root, context, newIgnores); + + // fix configurations + merged.ignoreTransforms = merged.ignoreTransforms.Concat(newIgnores).ToList(); + + var sourceComponent = sourceComponents[0]; + var chainLength = sourceComponent.BoneChainLength(); + var yaws = new float[chainLength]; + float fixedRollOfLastBone = 0; + var pitches = new float[chainLength]; + + for (var i = 0; i < chainLength; i++) + { + var rotationSpecified = sourceComponent.CalcLimitRotation((float)i / (chainLength - 1)); + var rotation = ConvertRotation(rotationSpecified); + pitches[i] = rotation.x; + fixedRollOfLastBone = rotation.y; + yaws[i] = rotation.z; + } + + var maxPitch = pitches.Select(Mathf.Abs).Max(); + var maxYaw = yaws.Select(Mathf.Abs).Max(); + + merged.limitRotation = new Vector3(maxPitch, 0, maxYaw); + + if (maxPitch != 0 || maxYaw != 0) + { + // avoid NaN + if (maxPitch == 0) maxPitch = 1; + if (maxYaw == 0) maxYaw = 1; + + var pitchCurve = new AnimationCurve(); + var yawCurve = new AnimationCurve(); + + pitchCurve.AddKey(0, pitches[0] / maxPitch); + yawCurve.AddKey(0, yaws[0] / maxYaw); + + for (var i = 0; i < chainLength; i++) + { + var time = (float)(i + 1) / chainLength; + pitchCurve.AddKey(time, pitches[i] / maxPitch); + yawCurve.AddKey(time, yaws[i] / maxYaw); + } + + merged.limitRotationXCurve = pitchCurve; + merged.limitRotationZCurve = yawCurve; + } + + if (merged.endpointPosition != Vector3.zero) + { + // TODO: this Endpoint Fix might not enough + // Rotation fix will conflict with this fix + merged.endpointPosition = Quaternion.Euler(0, -fixedRollOfLastBone, 0) * merged.endpointPosition; + } + } + // == Options == merged.isAnimated = merge.isAnimatedConfig.value || sourceComponents.Any(x => x.isAnimated); @@ -143,7 +208,181 @@ internal static void DoMerge(MergePhysBone merge, BuildContext? context) } } } - + + // To preserve bone reference, we keep original bone and create new GameObject for it. + // and later Trace and Object remove unused objects will merge original bones + public static void FixYawPitch( + VRCPhysBoneBase physBone, + Transform root, + BuildContext? context, + List newIgnores) + { + // Already fixed; nothing to do! + if (physBone.limitRotation.Equals(Vector3.zero)) return; + + physBone.InitTransforms(true); + var maxChainLength = physBone.BoneChainLength(); + + var ignoreTransforms = new HashSet(physBone.ignoreTransforms); + + RotateRecursive(physBone, physBone.GetTarget(), root, maxChainLength, 0, ignoreTransforms, newIgnores); + } + + /* + RotateRecursive will transform + Parent <= Parent + `- Root <= Transform + +- Bone1 + | +- Bone2 + | +- Bone3 + `- Bone4 + +- Bone5 + into + Parent + `- Root (AAO Merge Proxy) + +- Root + +- Bone1 (AAO Merge Proxy) + | +- Bone1 + | `- Bone2 (AAO Merge Proxy) + | +- Bone2 + | `- Bone3 (AAO Merge Proxy) + | +- Bone3 + `- Bone4 (AAO Merge Proxy) + +- Bone4 + `- Bone5 (AAO Merge Proxy) + +- Bone5 + + One pass of this method will transform into + + Parent + `- Root (AAO Merge Proxy) <= New Parent + `- Root + +- Bone1 <= New Transform + | +- Bone2 + | +- Bone3 + `- Bone4 <= New Transform + +- Bone5 + and calls RotateRecursive with new set of bones to complete + + */ + + private static void RotateRecursive(VRCPhysBoneBase physBone, + Transform transform, + Transform parent, + int totalDepth, + int depth, + HashSet ignoreTransforms, + List newIgnores) + { + Vector3 targetLocation; + + var activeChildren = Enumerable.Range(0, transform.childCount) + .Select(transform.GetChild) + .Where(child => !ignoreTransforms.Contains(child)) + .ToArray(); + + switch (activeChildren.Length) + { + case 0: + // end bone + if (physBone.endpointPosition != Vector3.zero) + targetLocation = physBone.endpointPosition; + else + targetLocation = Vector3.up; + break; + case 1: + targetLocation = activeChildren[0].localPosition; + break; + default: + switch (physBone.multiChildType) + { + case VRCPhysBoneBase.MultiChildType.Ignore: + targetLocation = Vector3.up; + break; + case VRCPhysBoneBase.MultiChildType.First: + targetLocation = activeChildren[0].localPosition; + break; + case VRCPhysBoneBase.MultiChildType.Average: + targetLocation = + activeChildren.Aggregate(Vector3.zero, (current, child) => current + child.localPosition) / + activeChildren.Length; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + break; + } + + var specifiedRotation = physBone.CalcLimitRotation((float)depth / totalDepth); + var rotation = ConvertRotation(specifiedRotation).y; + + // if the bone is at (0, -x, 0), we have infinite rotation for `FromToRotation` and + // `Quaternion.FromToRotation`'s choice is not happy for logic below. + // We need special handling for this case. + var dot = Vector3.Dot(Vector3.up, math.normalizesafe(targetLocation)); + var critical = dot <= -1; + + //Debug.Log($"is critical: {critical}, dot: {dot}, transform: {transform.name}"); + var thisRotation = !critical ? rotation : -rotation; + + // create new (actual) bone + var newBone = new GameObject($"{transform.name} (AAO Merge Proxy)"); + + // new bone should be at exactly same transform as the original bone + newBone.transform.parent = transform; + newBone.transform.localPosition = Vector3.zero; + newBone.transform.localRotation = Quaternion.identity; + newBone.transform.localScale = Vector3.one; + + // move to parent + newBone.transform.SetParent(parent, true); + + // rotate newBone to fix roll + newBone.transform.Rotate(Vector3.up, thisRotation, Space.Self); + + // move old bone to child of newBone + transform.SetParent(newBone.transform, true); + + newIgnores.Add(transform); + + //var rotationQuaternion = Quaternion.Euler(0, -thisRotation, 0); + + foreach (var child in activeChildren) + { + //child.localPosition = rotationQuaternion * child.localPosition; + //child.localRotation = rotationQuaternion * child.localRotation; + + if (ignoreTransforms.Contains(child)) continue; + RotateRecursive(physBone, child, newBone.transform, totalDepth, depth + 1, ignoreTransforms, newIgnores); + } + } + + public static Vector3 ConvertRotation(Vector3 limitRotation) + { + // XYZ is the order used in VRCPhysBone + var quat = quaternion.EulerXYZ(limitRotation * Mathf.Deg2Rad); + return QuaternionToEulerXZY(quat) * Mathf.Rad2Deg; + } + + private static Vector3 QuaternionToEulerXZY(Quaternion q) + { + // Quaternion to Euler + // https://qiita.com/aa_debdeb/items/abe90a9bd0b4809813da + // YZX Order in the article. (XZY in Unity) + // We use different perspective to represent same order of Euler order between Unity and the article. + var sz = 2 * q.x * q.y + 2 * q.z * q.w; + var unlocked = Mathf.Abs(sz) < 0.99999f; + Debug.Log("unlocked: " + unlocked); + return new Vector3( + unlocked ? Mathf.Atan2(-(2 * q.y * q.z - 2 * q.x * q.w), 2 * q.w * q.w + 2 * q.y * q.y - 1) : 0, + unlocked + ? Mathf.Atan2(-(2 * q.x * q.z - 2 * q.y * q.w), 2 * q.w * q.w + 2 * q.x * q.x - 1) + : Mathf.Atan2(2 * q.x * q.z + 2 * q.y * q.w, 2 * q.w * q.w + 2 * q.z * q.z - 1), + Mathf.Asin(sz) + ); + } + private static readonly string[] TransformRotationAndPositionAnimationKeys = { "m_LocalRotation.x", "m_LocalRotation.y", "m_LocalRotation.z", "m_LocalRotation.w", @@ -236,26 +475,38 @@ protected override void PbCurveProp(string label, CurveConfigProp prop, bool for protected override void Pb3DCurveProp(string label, string pbXCurveLabel, string pbYCurveLabel, string pbZCurveLabel, CurveVector3ConfigProp prop, bool forceOverride = false) { - var @override = forceOverride || prop.IsOverride; - _mergedPhysBone.FindProperty(prop.PhysBoneValueName).vector3Value = - prop.GetValueProperty(@override).vector3Value; - if (@override) - { - _mergedPhysBone.FindProperty(prop.PhysBoneCurveXName).animationCurveValue = - prop.GetCurveXProperty(@override).animationCurveValue; - _mergedPhysBone.FindProperty(prop.PhysBoneCurveYName).animationCurveValue = - prop.GetCurveYProperty(@override).animationCurveValue; - _mergedPhysBone.FindProperty(prop.PhysBoneCurveZName).animationCurveValue = - prop.GetCurveZProperty(@override).animationCurveValue; - } - else + switch (prop.GetOverride(forceOverride)) { - _mergedPhysBone.FindProperty(prop.PhysBoneCurveXName).animationCurveValue = - FixCurve(prop.GetCurveXProperty(@override).animationCurveValue); - _mergedPhysBone.FindProperty(prop.PhysBoneCurveYName).animationCurveValue = - FixCurve(prop.GetCurveYProperty(@override).animationCurveValue); - _mergedPhysBone.FindProperty(prop.PhysBoneCurveZName).animationCurveValue = - FixCurve(prop.GetCurveZProperty(@override).animationCurveValue); + case MergePhysBone.CurveVector3Config.CurveOverride.Copy: + _mergedPhysBone.FindProperty(prop.PhysBoneValueName).vector3Value = + prop.SourceValue!.vector3Value; + _mergedPhysBone.FindProperty(prop.PhysBoneCurveXName).animationCurveValue = + FixCurve(prop.SourceCurveX!.animationCurveValue); + _mergedPhysBone.FindProperty(prop.PhysBoneCurveYName).animationCurveValue = + FixCurve(prop.SourceCurveY!.animationCurveValue); + _mergedPhysBone.FindProperty(prop.PhysBoneCurveZName).animationCurveValue = + FixCurve(prop.SourceCurveZ!.animationCurveValue); + break; + case MergePhysBone.CurveVector3Config.CurveOverride.Override: + _mergedPhysBone.FindProperty(prop.PhysBoneValueName).vector3Value = + prop.OverrideValue.vector3Value; + _mergedPhysBone.FindProperty(prop.PhysBoneCurveXName).animationCurveValue = + prop.OverrideCurveX.animationCurveValue; + _mergedPhysBone.FindProperty(prop.PhysBoneCurveYName).animationCurveValue = + prop.OverrideCurveY.animationCurveValue; + _mergedPhysBone.FindProperty(prop.PhysBoneCurveZName).animationCurveValue = + prop.OverrideCurveZ.animationCurveValue; + break; + case MergePhysBone.CurveVector3Config.CurveOverride.Fix: + // Fixing rotation is proceeded before. + // We just reset the value and curve. + _mergedPhysBone.FindProperty(prop.PhysBoneValueName).vector3Value = Vector3.zero; + _mergedPhysBone.FindProperty(prop.PhysBoneCurveXName).animationCurveValue = new AnimationCurve(); + _mergedPhysBone.FindProperty(prop.PhysBoneCurveYName).animationCurveValue = new AnimationCurve(); + _mergedPhysBone.FindProperty(prop.PhysBoneCurveZName).animationCurveValue = new AnimationCurve(); + break; + default: + throw new ArgumentOutOfRangeException(); } } diff --git a/Editor/Processors/TraceAndOptimize/FindUnusedObjectsProcessor.cs b/Editor/Processors/TraceAndOptimize/FindUnusedObjectsProcessor.cs index 34d29a6f3..e19f64ccb 100644 --- a/Editor/Processors/TraceAndOptimize/FindUnusedObjectsProcessor.cs +++ b/Editor/Processors/TraceAndOptimize/FindUnusedObjectsProcessor.cs @@ -375,7 +375,7 @@ private void MergeBone(GCComponentInfoHolder componentInfos) // if this is not identity transform, animating children is not good return NotMerged(); - if (!MergeBoneProcessor.ScaledEvenly(localScale)) + if (!Utils.ScaledEvenly(localScale)) // non even scaling is not possible to reproduce in children return NotMerged(); } diff --git a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef index 72816d14f..b27602886 100644 --- a/Editor/com.anatawa12.avatar-optimizer.editor.asmdef +++ b/Editor/com.anatawa12.avatar-optimizer.editor.asmdef @@ -14,6 +14,7 @@ "com.anatawa12.avatar-optimizer.internal.meshinfo2", "com.anatawa12.avatar-optimizer.internal.utils", "Unity.Burst", + "Unity.Mathematics", "nadena.dev.ndmf", "nadena.dev.ndmf.runtime", "nadena.dev.ndmf.reactive-query.core", diff --git a/Internal/Utils/Utils.cs b/Internal/Utils/Utils.cs index 50076f0c1..38b9c1674 100644 --- a/Internal/Utils/Utils.cs +++ b/Internal/Utils/Utils.cs @@ -404,5 +404,41 @@ public static Color SafeGetColor(this Material material, string propertyName) => // Exception-safe swap public static void Swap(ref T a, ref T b) => (a, b) = (b, a); + + /// + /// Returns whether the given local scale is scaled evenly. + /// + /// If the scale is skewed, this returns false. + /// + /// the local scale to check + /// whether the given local scale is scaled evenly + public static bool ScaledEvenly(Vector3 localScale) + { + bool CheckScale(float scale) => 0.995 < scale && scale < 1.005; + return CheckScale(localScale.x / localScale.y) && CheckScale(localScale.x / localScale.z) && + CheckScale(localScale.y / localScale.z); + } + + public static TSource MaxBy(this IEnumerable source, + Func selector) + where TComparable : IComparable + { + using var enumerator = source.GetEnumerator(); + if (!enumerator.MoveNext()) throw new InvalidOperationException("Sequence is empty"); + var max = enumerator.Current; + var maxComparable = selector(max); + while (enumerator.MoveNext()) + { + var current = enumerator.Current; + var currentComparable = selector(current); + if (currentComparable.CompareTo(maxComparable) > 0) + { + max = current; + maxComparable = currentComparable; + } + } + + return max; + } } } diff --git a/Localization/en-us.po b/Localization/en-us.po index e2a887462..a39c86c53 100644 --- a/Localization/en-us.po +++ b/Localization/en-us.po @@ -147,12 +147,15 @@ msgid "MergePhysBone:error:oneSource" msgstr "There is only one source PhysBone. You must specify two or more merge source PhysBones." msgid "MergePhysBone:error:multiChildType" -msgstr "Some PysBone has multi child type != Ignore" +msgstr "Some PhysBone has multi child type != Ignore" msgid "MergePhysBone:error:unsupportedPbVersion" msgstr "The PhysBone Version is not supported (yet) by Avatar Optimizer.\n" "Please tell author on twitter (@anatawa12_vrc) or GitHub (anatawa12/AvatarOptimizer)!" +msgid "MergePhysBone:message:fix-yaw-pitch" +msgstr "Fix Roll with rotating bones" + msgid "MergePhysBone:error:differValues" msgstr "The values is differ between two or more sources. You have to set same value OR override this property: {0}" @@ -162,6 +165,26 @@ msgstr "The value is differ between two or more sources. You have to set same va msgid "MergePhysBone:warning:differChainLength" msgstr "The chain length is differ between two or more sources. Shorter chain will be thicker than original." +msgid "MergePhysBone:error:LimitRotationFix:SkewScaling" +msgstr "" +"Skew scaling is not supported with Limit Rotation mode Fix.\n" +"Please change the Limit Rotation mode to other than Fix, or fix the skew scaling." + +msgid "MergePhysBone:error:LimitRotationFix:DifferRotation" +msgstr "" +"Limit Rotation of source PhysBones differs in unfixable way.\n" +"Please fix Limit Rotation of source PhysBones to same value, or change to other mode." + +msgid "MergePhysBone:error:LimitRotationFix:DifferRotation:description" +msgstr "" +"Limit Rotation Fix can fix Roll axis of different rotation but we cannot fix difference in other axis.\n" +"Roll axis in this context is local X axis, and might be differs from Roll config on PhysBone inspector if there is Yaw rotation." + +msgid "MergePhysBone:error:LimitRotationFix:NonZeroEndpointPosition" +msgstr "" +"Endpoint Position is not zero while Limit Rotation mode is Fix.\n" +"Please set Endpoint Position to zero, set Endpoint Position mode to Clear, or change Limit Rotation mode to other than Fix." + msgid "MergePhysBone:dialog:versionInfo:title" msgstr "Version Info" diff --git a/Localization/ja-jp.po b/Localization/ja-jp.po index 11a255775..e70681b2f 100644 --- a/Localization/ja-jp.po +++ b/Localization/ja-jp.po @@ -156,6 +156,9 @@ msgid "MergePhysBone:error:unsupportedPbVersion" msgstr "このPhysBoneバージョンは(まだ)Avatar Optimizerによって対応されていません。\n" "作者にtwitter (@anatawa12_vrc)またはGitHub (anatawa12/AvatarOptimizer)で連絡してください!" +msgid "MergePhysBone:message:fix-yaw-pitch" +msgstr "ボーンを回転させてRollの値を揃える" + msgid "MergePhysBone:error:differValues" msgstr "複数の統合対象の間で値に差異があります。以下の値は同じ値にするかOverrideする必要があります: {0}" @@ -165,6 +168,26 @@ msgstr "複数の統合対象の間で値に差異があります。同じ値に msgid "MergePhysBone:warning:differChainLength" msgstr "複数の統合対象の間でチェーンの長さが異なります。短いチェーンのPBは元よりも太くなる可能性があります。" +msgid "MergePhysBone:error:LimitRotationFix:SkewScaling" +msgstr "" +"Scaleが均一でないボーンの角度制限を統合することはサポートされていません。\n" +"角度制限の設定をFix以外にするか、Scaleを均一にしてください。" + +msgid "MergePhysBone:error:LimitRotationFix:DifferRotation" +msgstr "" +"角度制限を統合できる状態ではありません。\n" +"統合対象の角度制限の値を揃えるか、角度制限の設定をFix以外にしてください。" + +msgid "MergePhysBone:error:LimitRotationFix:DifferRotation:description" +msgstr "" +"角度制限の統合は、ボーンに対する捻るような回転(Roll)の値は揃えることができますが、他の軸の回転は揃えることができません。\n" +"Roll回転の軸はローカル座標系におけるX軸になります。Yaw回転が含まれている場合はPBのInspectorにあるRollの表示とは異なることがあります。" + +msgid "MergePhysBone:error:LimitRotationFix:NonZeroEndpointPosition" +msgstr "" +"角度制限の設定がFixですが、Endpoint Positionが空ではありません。\n" +"Endpoint Positionを空にするか、Endpoint Positionの設定をClearにするか、角度制限の設定をFix以外にしてください。" + msgid "MergePhysBone:dialog:versionInfo:title" msgstr "バージョンについて" diff --git a/Runtime/MergePhysBone.cs b/Runtime/MergePhysBone.cs index baf0a9879..c30dceb0b 100644 --- a/Runtime/MergePhysBone.cs +++ b/Runtime/MergePhysBone.cs @@ -150,11 +150,19 @@ public struct CurveNoLimitConfig [Serializable] public struct CurveVector3Config { - public bool @override; + public CurveOverride @override; public Vector3 value; public AnimationCurve curveX; public AnimationCurve curveY; public AnimationCurve curveZ; + + public enum CurveOverride + { + Copy, + Override, + // Change bone angle to match the curve + Fix, + } } [Serializable]