diff --git a/.github/ProjectRoot/vpm-manifest-2022.json b/.github/ProjectRoot/vpm-manifest-2022.json index 1be7eb3c..4ed96635 100644 --- a/.github/ProjectRoot/vpm-manifest-2022.json +++ b/.github/ProjectRoot/vpm-manifest-2022.json @@ -1,7 +1,7 @@ { "dependencies": { "com.vrchat.avatars": { - "version": "3.5.0" + "version": "3.7.0" }, "nadena.dev.ndmf": { "version": "1.4.0" @@ -9,13 +9,13 @@ }, "locked": { "com.vrchat.avatars": { - "version": "3.6.1", + "version": "3.7.0", "dependencies": { - "com.vrchat.base": "3.6.1" + "com.vrchat.base": "3.7.0" } }, "com.vrchat.base": { - "version": "3.6.1", + "version": "3.7.0", "dependencies": {} }, "nadena.dev.ndmf": { diff --git a/Editor/Inspector/MAConvertConstraintsEditor.cs b/Editor/Inspector/MAConvertConstraintsEditor.cs new file mode 100644 index 00000000..1b4a133e --- /dev/null +++ b/Editor/Inspector/MAConvertConstraintsEditor.cs @@ -0,0 +1,14 @@ +using UnityEditor; + +namespace nadena.dev.modular_avatar.core.editor +{ + [CustomEditor(typeof(ModularAvatarConvertConstraints))] + [CanEditMultipleObjects] + internal class MAConvertConstraintsEditor : MAEditorBase + { + protected override void OnInnerInspectorGUI() + { + // no UI + } + } +} \ No newline at end of file diff --git a/Editor/Inspector/MAConvertConstraintsEditor.cs.meta b/Editor/Inspector/MAConvertConstraintsEditor.cs.meta new file mode 100644 index 00000000..31981fc9 --- /dev/null +++ b/Editor/Inspector/MAConvertConstraintsEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 484ea04548b945ce9cf5fd6d49b50244 +timeCreated: 1723778102 \ No newline at end of file diff --git a/Editor/OptimizationPasses/ConstraintConverterPass.cs b/Editor/OptimizationPasses/ConstraintConverterPass.cs new file mode 100644 index 00000000..10c8dccc --- /dev/null +++ b/Editor/OptimizationPasses/ConstraintConverterPass.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using nadena.dev.ndmf; +using UnityEditor; +#if MA_VRCSDK3_AVATARS_3_7_0_OR_NEWER +using UnityEngine; +using UnityEngine.Animations; +using VRC.SDK3.Avatars; +using System.Linq; +using nadena.dev.modular_avatar.animation; +using VRC.Dynamics; +#endif + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class ConstraintConverterPass : Pass + { +#if MA_VRCSDK3_AVATARS_3_7_0_OR_NEWER + [InitializeOnLoadMethod] + private static void Init() + { + AvatarDynamicsSetup.IsUnityConstraintAutoConverted += constraint => + { + var component = constraint as Component; + if (component == null) return false; + + var converted = component.GetComponentInParent(); + + return converted != null && RuntimeUtil.FindAvatarInParents(converted.transform) == + RuntimeUtil.FindAvatarInParents(component.transform); + }; + + AvatarDynamicsSetup.OnConvertUnityConstraintsAcrossGameObjects += (constraints, isAutoFix) => + { + if (!isAutoFix) return false; + + var avatars = constraints.Select(c => RuntimeUtil.FindAvatarInParents(c.transform)).Distinct(); + + foreach (var avatar in avatars) Undo.AddComponent(avatar.gameObject); + + return true; + }; + } + + protected override void Execute(ndmf.BuildContext context) + { + var converters = context.AvatarRootObject.GetComponentsInChildren(true) + .Select(c => c.gameObject) + .ToHashSet(new ObjectIdentityComparer()); + if (converters.Count == 0) return; + + var constraintGameObjects = context.AvatarRootObject.GetComponentsInChildren(true) + .Select(c => (c as Component)?.gameObject) + .Distinct() + .Where(go => go.GetComponentsInParent(true) + .Select(c => c.gameObject) + .Any(converters.Contains) + ).ToArray(); + var targetConstraintComponents = + constraintGameObjects.SelectMany(go => go.GetComponents()).ToArray(); + + AvatarDynamicsSetup.DoConvertUnityConstraints(targetConstraintComponents, null, false); + + var asc = context.Extension(); + + // Also look for preexisting VRCConstraints so we can go fix up any broken animation clips from people who + // clicked auto fix :( + var existingVRCConstraints = converters.SelectMany(c => c.GetComponentsInChildren(true)) + .Select(c => c.gameObject) + .Distinct(); + + var targetPaths = constraintGameObjects + .Union(existingVRCConstraints) + .Select(c => asc.PathMappings.GetObjectIdentifier(c)) + .ToHashSet(); + + // Update animation clips + var clips = targetPaths.SelectMany(tp => asc.AnimationDatabase.ClipsForPath(tp)) + .ToHashSet(); + + foreach (var clip in clips) RemapSingleClip(clip, targetPaths); + } + + private void RemapSingleClip(AnimationDatabase.ClipHolder clip, HashSet targetPaths) + { + var motion = clip.CurrentClip as AnimationClip; + if (motion == null) return; + + var bindings = AnimationUtility.GetCurveBindings(motion); + var toUpdateBindings = new List(); + var toUpdateCurves = new List(); + + foreach (var ecb in bindings) + { + if (!targetPaths.Contains(ecb.path)) continue; + if (typeof(IConstraint).IsAssignableFrom(ecb.type)) + if (AvatarDynamicsSetup.TryGetSubstituteAnimationBinding(ecb.type, ecb.propertyName, + out var newType, out var newProp, out var isArray)) + { + var newBinding = new EditorCurveBinding + { + path = ecb.path, + type = newType, + propertyName = newProp + }; + var curve = AnimationUtility.GetEditorCurve(motion, ecb); + if (curve != null) + { + toUpdateBindings.Add(newBinding); + toUpdateCurves.Add(curve); + + toUpdateBindings.Add(ecb); + toUpdateCurves.Add(null); + } + } + } + + if (toUpdateBindings.Count == 0) return; + AnimationUtility.SetEditorCurves(motion, toUpdateBindings.ToArray(), toUpdateCurves.ToArray()); + } + +#else + protected override void Execute(ndmf.BuildContext context) {} +#endif + } +} \ No newline at end of file diff --git a/Editor/OptimizationPasses/ConstraintConverterPass.cs.meta b/Editor/OptimizationPasses/ConstraintConverterPass.cs.meta new file mode 100644 index 00000000..363abaee --- /dev/null +++ b/Editor/OptimizationPasses/ConstraintConverterPass.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5c172d4eac3d4902826a96656cf1ce34 +timeCreated: 1723776385 \ No newline at end of file diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs index 72704346..02ca727d 100644 --- a/Editor/PluginDefinition/PluginDefinition.cs +++ b/Editor/PluginDefinition/PluginDefinition.cs @@ -70,6 +70,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin seq.Run(BlendshapeSyncAnimationPluginPass.Instance); #endif seq.Run(GameObjectDelayDisablePass.Instance); + seq.Run(ConstraintConverterPass.Instance); }); #if MA_VRCSDK3_AVATARS seq.Run(MenuInstallPluginPass.Instance); diff --git a/Editor/nadena.dev.modular-avatar.core.editor.asmdef b/Editor/nadena.dev.modular-avatar.core.editor.asmdef index 76a60059..5d7819cc 100644 --- a/Editor/nadena.dev.modular-avatar.core.editor.asmdef +++ b/Editor/nadena.dev.modular-avatar.core.editor.asmdef @@ -8,7 +8,8 @@ "nadena.dev.ndmf", "nadena.dev.ndmf.vrchat", "nadena.dev.ndmf.reactive-query.core", - "nadena.dev.ndmf.runtime" + "nadena.dev.ndmf.runtime", + "VRC.SDK3A.Editor" ], "includePlatforms": [ "Editor" @@ -19,7 +20,6 @@ "precompiledReferences": [ "Newtonsoft.Json.dll", "System.Collections.Immutable.dll", - "System.Memory.dll", "VRCSDKBase.dll", "VRCSDKBase-Editor.dll", "VRCSDK3A.dll", @@ -48,6 +48,11 @@ "name": "com.vrchat.avatars", "expression": "3.5.2", "define": "MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER" + }, + { + "name": "com.vrchat.avatars", + "expression": "3.7.0-beta.2", + "define": "MA_VRCSDK3_AVATARS_3_7_0_OR_NEWER" } ], "noEngineReferences": false diff --git a/Runtime/ModularAvatarConvertConstraints.cs b/Runtime/ModularAvatarConvertConstraints.cs new file mode 100644 index 00000000..1440fa25 --- /dev/null +++ b/Runtime/ModularAvatarConvertConstraints.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +namespace nadena.dev.modular_avatar.core +{ + [DisallowMultipleComponent] +#if MA_VRCSDK3_AVATARS + [AddComponentMenu("Modular Avatar/MA Convert Constraints")] +#else + [AddComponentMenu("")] +#endif + [HelpURL("https://modular-avatar.nadena.dev/docs/reference/convert-constraints?lang=auto")] + public class ModularAvatarConvertConstraints : AvatarTagComponent + { + } +} \ No newline at end of file diff --git a/Runtime/ModularAvatarConvertConstraints.cs.meta b/Runtime/ModularAvatarConvertConstraints.cs.meta new file mode 100644 index 00000000..0899b020 --- /dev/null +++ b/Runtime/ModularAvatarConvertConstraints.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e362b3df8a3d478c82bf5ffe18f622e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/docs~/docs/reference/convert-constraints.md b/docs~/docs/reference/convert-constraints.md new file mode 100644 index 00000000..48df4f43 --- /dev/null +++ b/docs~/docs/reference/convert-constraints.md @@ -0,0 +1,17 @@ +# Convert Constraints + +The Convert Constraints component directs Modular Avatar to nondestructively convert Unity constraints to VRChat +constraints on build. It will convert any constraints (and animations referencing them) on the same object it is +attached to, and any children of that object. It will also attempt to fix animations broken by using VRCSDK's Auto Fix +button with older versions of Modular Avatar. + +## When should I use this? + +It's probably a good idea to put this on your avatar root in most cases, as preconverting constraints improves +performance significantly. When MA is installed, the VRChat Auto Fix button will automatically add this component to +your avatar root if it's not already there. + +## When should I not use this? + +This component is primarily provided to allow users to disable this functionality (by removing this component) if it is +suspected to be causing problems. \ No newline at end of file diff --git a/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/convert-constraints.md b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/convert-constraints.md new file mode 100644 index 00000000..4fa9259a --- /dev/null +++ b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/convert-constraints.md @@ -0,0 +1,15 @@ +# Convert Constraints + +Convert Constraintsコンポーネントは、ビルド時にUnityのConstraintsをVRChatのConstraintに非破壊的に変換するようにModular +Avatarに指示します。 +アタッチされているオブジェクトとその子オブジェクトにあるConstraint、およびそれらを参照しているアニメーションを変換します。 +また、VRCSDKのAuto Fixを古いバージョンのModular Avatarで使用して壊れたアニメーションも修正しようとします。 + +## いつ使うもの? + +あらかじめ変換するとパフォーマンスが大幅に向上するため、ほとんどの場合はアバタールートにこれを配置するのが良いでしょう。MAがインストールされている場合、 +VRChatのAuto Fixボタンは、アバタールートにこのコンポーネントがまだ存在しない場合、自動的にこのコンポーネントを追加します。 + +## 非推奨の場合 + +このコンポーネントは、問題の原因となる可能性がある場合に、このコンポーネントを削除してこの機能を無効にするために提供されています。 \ No newline at end of file diff --git a/package.json b/package.json index ee96288e..1cdf983f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "com.unity.nuget.newtonsoft-json": "2.0.0" }, "vpmDependencies": { - "com.vrchat.avatars": ">=3.6.1", + "com.vrchat.avatars": ">=3.7.0", "nadena.dev.ndmf": ">=1.5.0-beta.3 <2.0.0-a" } }