diff --git a/Editor/API/AnimatorServices.meta b/Editor/API/AnimatorServices.meta new file mode 100644 index 0000000..3d6c451 --- /dev/null +++ b/Editor/API/AnimatorServices.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5340b8ecb97e49089a6d95bc16f28fce +timeCreated: 1730064812 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/ECBComparator.cs b/Editor/API/AnimatorServices/ECBComparator.cs new file mode 100644 index 0000000..9ace0a6 --- /dev/null +++ b/Editor/API/AnimatorServices/ECBComparator.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using UnityEditor; + +namespace nadena.dev.ndmf.animator +{ + internal class ECBComparator : IComparer, IEqualityComparer + { + internal static ECBComparator Instance { get; } = new(); + + private ECBComparator() + { + } + + public int Compare(EditorCurveBinding x, EditorCurveBinding y) + { + var pathComparison = string.Compare(x.path, y.path, StringComparison.Ordinal); + if (pathComparison != 0) return pathComparison; + var propertyNameComparison = string.Compare(x.propertyName, y.propertyName, StringComparison.Ordinal); + if (propertyNameComparison != 0) return propertyNameComparison; + var isPPtrCurveComparison = x.isPPtrCurve.CompareTo(y.isPPtrCurve); + if (isPPtrCurveComparison != 0) return isPPtrCurveComparison; + var isDiscreteCurveComparison = x.isDiscreteCurve.CompareTo(y.isDiscreteCurve); + if (isDiscreteCurveComparison != 0) return isDiscreteCurveComparison; + return x.isSerializeReferenceCurve.CompareTo(y.isSerializeReferenceCurve); + } + + public bool Equals(EditorCurveBinding x, EditorCurveBinding y) + { + return x.path == y.path && x.propertyName == y.propertyName && x.isPPtrCurve == y.isPPtrCurve && + x.isDiscreteCurve == y.isDiscreteCurve && + x.isSerializeReferenceCurve == y.isSerializeReferenceCurve && Equals(x.type, y.type); + } + + public int GetHashCode(EditorCurveBinding obj) + { + return HashCode.Combine(obj.path, obj.propertyName, obj.isPPtrCurve, obj.isDiscreteCurve, + obj.isSerializeReferenceCurve, obj.type); + } + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/ECBComparator.cs.meta b/Editor/API/AnimatorServices/ECBComparator.cs.meta new file mode 100644 index 0000000..d973bbe --- /dev/null +++ b/Editor/API/AnimatorServices/ECBComparator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cadf93f628a44bf3ab0b0e8c09942f11 +timeCreated: 1730067342 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/ICommitable.cs b/Editor/API/AnimatorServices/ICommitable.cs new file mode 100644 index 0000000..b29bd0f --- /dev/null +++ b/Editor/API/AnimatorServices/ICommitable.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace nadena.dev.ndmf.animator +{ + internal interface ICommitable + { + /// + /// Allocates the destination unity object, but does not recurse back into the CommitContext. + /// + /// + /// + T Prepare(CommitContext context); + + /// + /// Fills in all fields of the destination unity object. This may recurse back into the CommitContext. + /// + /// + void Commit(CommitContext context, T obj); + } + + internal class CommitContext + { + private readonly Dictionary _commitCache = new(); + + internal R CommitObject(ICommitable obj) + { + if (_commitCache.TryGetValue(obj, out var result)) return (R)result; + + var resultObj = obj.Prepare(this); + _commitCache[obj] = resultObj; + + obj.Commit(this, resultObj); + + return resultObj; + } + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/ICommitable.cs.meta b/Editor/API/AnimatorServices/ICommitable.cs.meta new file mode 100644 index 0000000..e414626 --- /dev/null +++ b/Editor/API/AnimatorServices/ICommitable.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dc6d34409a5d49f9a0e152f159cb9a2f +timeCreated: 1730066316 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/IPlatformAnimatorBindings.cs b/Editor/API/AnimatorServices/IPlatformAnimatorBindings.cs new file mode 100644 index 0000000..f9b45aa --- /dev/null +++ b/Editor/API/AnimatorServices/IPlatformAnimatorBindings.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace nadena.dev.ndmf.animator +{ + internal interface IPlatformAnimatorBindings + { + /// + /// If true, the motion asset should be maintained as-is without replacement or modification. + /// + /// + /// + bool IsSpecialMotion(Motion m); + + /// + /// Returns any animator controllers that are referenced by platform-specific assets (e.g. VRCAvatarDescriptor). + /// The bool flag indicates whether the controller is overridden (true) or left as default (false). + /// + /// + IEnumerable<(object, RuntimeAnimatorController, bool)> GetInnateControllers(); + + /// + /// Updates any innate controllers to reference new animator controllers. + /// + /// + void CommitInnateControllers(IEnumerable<(object, RuntimeAnimatorController)> controllers); + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/IPlatformAnimatorBindings.cs.meta b/Editor/API/AnimatorServices/IPlatformAnimatorBindings.cs.meta new file mode 100644 index 0000000..1bc2a43 --- /dev/null +++ b/Editor/API/AnimatorServices/IPlatformAnimatorBindings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0d015c4cdd524a2785fc4d5c9c24373a +timeCreated: 1730064961 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualAnimatorController.cs b/Editor/API/AnimatorServices/VirtualAnimatorController.cs new file mode 100644 index 0000000..da2dfec --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualAnimatorController.cs @@ -0,0 +1,21 @@ +using UnityEditor.Animations; + +namespace nadena.dev.ndmf.animator +{ + /// + /// Represents an animator controller that has been indexed by NDMF for faster manipulation. This class also + /// guarantees that certain assets have been cloned, specifically: + /// - AnimatorController + /// - StateMachine + /// - AnimatorState + /// - AnimatorStateTransition + /// - BlendTree + /// - AnimationClip + /// - Any state behaviors attached to the animator controller + /// + public class VirtualAnimatorController + { + private BuildContext _context; + private AnimatorController _controller; + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualAnimatorController.cs.meta b/Editor/API/AnimatorServices/VirtualAnimatorController.cs.meta new file mode 100644 index 0000000..007e9d0 --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualAnimatorController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f51e941f974849c89bab41a4370c6b90 +timeCreated: 1730064821 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualClip.cs b/Editor/API/AnimatorServices/VirtualClip.cs new file mode 100644 index 0000000..a62f3aa --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualClip.cs @@ -0,0 +1,429 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace nadena.dev.ndmf.animator +{ + /// + /// An abstraction over Unity AnimationClips. This class is designed to allow for low-overhead mutation of animation + /// clips, and in particular provides helpers for common operations (e.g. rewriting all paths in a clip). + /// + [PublicAPI] + public class VirtualClip : VirtualMotion + { + private AnimationClip _clip; + + public string Name + { + get => _clip.name; + set => _clip.name = value; + } + + /// + /// True if this is a marker clip; in this case, the clip is immutable and any attempt to mutate it will be + /// ignored. The clip will not be cloned on commit. + /// + public bool IsMarkerClip { get; private set; } + + /// + /// True if this clip has been modified since it was cloned or created. + /// + public bool IsDirty { get; private set; } + + /// + /// Controls the (unexposed) High Quality Curve setting on the animation clip. + /// + public bool UseHighQualityCurves { get; set; } + + /// + /// Controls the `legacy` setting on the animation clip. + /// + public bool Legacy + { + get => _clip.legacy; + set => _clip.legacy = value; + } + + public Bounds LocalBounds + { + get => _clip.localBounds; + set => _clip.localBounds = value; + } + + public AnimationClipSettings Settings + { + get => AnimationUtility.GetAnimationClipSettings(_clip); + set + { + if (value.additiveReferencePoseClip != null) + { + throw new ArgumentException("Use the AdditiveReferencePoseClip property instead", + nameof(value.additiveReferencePoseClip)); + } + + AnimationUtility.SetAnimationClipSettings(_clip, value); + } + } + + public VirtualMotion AdditiveReferencePoseClip { get; set; } + + public float AdditiveReferencePoseTime + { + get => Settings.additiveReferencePoseTime; + set + { + var settings = Settings; + settings.additiveReferencePoseTime = value; + Settings = settings; + } + } + + public WrapMode WrapMode + { + get => _clip.wrapMode; + set => _clip.wrapMode = value; + } + + public float FrameRate + { + get => _clip.frameRate; + set => _clip.frameRate = value; + } + + private Dictionary> _curveCache = new(ECBComparator.Instance); + + private Dictionary> _pptrCurveCache = + new(ECBComparator.Instance); + + private struct CachedCurve + { + // If null and Dirty is false, the curve has not been cached yet. + // If null and Dirty is true, the curve has been deleted. + public T Value; + public bool Dirty; + + public override string ToString() + { + return $"CachedCurve<{typeof(T).Name}> {{ Value = {Value}, Dirty = {Dirty} }}"; + } + } + + /// + /// Creates a VirtualClip representing a "marker" clip. This is a clip which must be preserved, as-is, in the + /// final avatar. For example, VRChat's proxy animations fall under this category. Any attempt to mutate a + /// marker clip will be ignored. + /// + /// + /// + public static VirtualClip FromMarker(AnimationClip clip) + { + return new VirtualClip(clip, true); + } + + /// + /// Clones an animation clip into a VirtualClip. The provided BuildContext is used to determine which platform + /// to use to query for marker clips; if a marker clip is found, it will be treated as immutable. + /// + /// + /// + /// + public static VirtualClip Clone(BuildContext context, AnimationClip clip) + { + // TODO: Probably pass some dedicated context rather than BuildContext + + var newClip = Object.Instantiate(clip); + newClip.name = clip.name; + + VirtualClip refPoseClip = null; + var settings = AnimationUtility.GetAnimationClipSettings(clip); + if (settings.additiveReferencePoseClip != null) + { + // TODO: Cache in build context? + refPoseClip = Clone(context, settings.additiveReferencePoseClip); + } + + var virtualClip = new VirtualClip(newClip, false); + virtualClip.AdditiveReferencePoseClip = refPoseClip; + virtualClip.AdditiveReferencePoseTime = settings.additiveReferencePoseTime; + + return virtualClip; + } + + /// + /// Clones a VirtualClip. The new VirtualClip is backed by an independent copy of the original clip. + /// + /// + public VirtualClip Clone() + { + var newClip = Object.Instantiate(_clip); + newClip.name = _clip.name; + + var virtualClip = new VirtualClip(newClip, IsMarkerClip); + virtualClip.UseHighQualityCurves = UseHighQualityCurves; + virtualClip.AdditiveReferencePoseClip = AdditiveReferencePoseClip; + virtualClip.AdditiveReferencePoseTime = AdditiveReferencePoseTime; + virtualClip.IsDirty = IsDirty; + virtualClip._curveCache = + _curveCache.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, ECBComparator.Instance); + virtualClip._pptrCurveCache = + _pptrCurveCache.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, ECBComparator.Instance); + + return virtualClip; + } + + public static VirtualClip Create(string name) + { + var clip = new AnimationClip { name = name }; + return new VirtualClip(clip, false); + } + + private VirtualClip(AnimationClip clip, bool isMarker) + { + _clip = clip; + IsDirty = false; + IsMarkerClip = isMarker; + + // This secret property can be changed by SetCurves calls, so preserve its current value. + UseHighQualityCurves = new SerializedObject(clip).FindProperty("m_UseHighQualityCurve").boolValue; + + foreach (var binding in AnimationUtility.GetCurveBindings(clip)) + { + _curveCache.Add(binding, new CachedCurve()); + } + + foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip)) + { + _pptrCurveCache.Add(binding, new CachedCurve()); + } + } + + public IEnumerable GetFloatCurveBindings() + { + return _curveCache + .Where(kvp => kvp.Value.Value != null || !kvp.Value.Dirty) + .Select(kvp => kvp.Key).ToList(); + } + + public IEnumerable GetObjectCurveBindings() + { + return _pptrCurveCache + .Where(kvp => kvp.Value.Value != null || !kvp.Value.Dirty) + .Select(kvp => kvp.Key).ToList(); + } + + /// + /// Edit the paths of all bindings in this clip using the provided function. If this results in a path collision, + /// it is indeterminate which binding will be preserved. If null is returned, the binding will be deleted. + /// + /// + public void EditPaths(Func pathEditor) + { + if (IsMarkerClip) return; + + _curveCache = Transform(_curveCache, AnimationUtility.GetEditorCurve); + _pptrCurveCache = Transform(_pptrCurveCache, AnimationUtility.GetObjectReferenceCurve); + + Dictionary> Transform( + Dictionary> cache, Func getter) + { + var newCache = new Dictionary>(ECBComparator.Instance); + foreach (var kvp in cache) + { + var binding = kvp.Key; + var newBinding = binding; + newBinding.path = pathEditor(binding.path); + + if (ECBComparator.Instance.Equals(binding, newBinding)) + { + newCache[newBinding] = kvp.Value; + continue; + } + + IsDirty = true; + + // Any binding originally present needs some kind of presence in the new cache; start off by + // inserting a deleted entry, we'll overwrite it later if appropriate. + if (!newCache.ContainsKey(binding)) + { + newCache[binding] = new CachedCurve + { + Dirty = true + }; + } + + if (newBinding.path == null) + { + // Delete the binding + continue; + } + + // Load cache entry if not loaded + var entry = kvp.Value; + if (!entry.Dirty && entry.Value == null) + { + entry.Value = getter(_clip, binding); + entry.Dirty = true; + } + + newCache[newBinding] = entry; + } + + return newCache; + } + } + + public AnimationCurve GetFloatCurve(EditorCurveBinding binding) + { + if (_curveCache.TryGetValue(binding, out var cached)) + { + if (cached.Dirty == false && cached.Value == null) + { + cached.Value = AnimationUtility.GetEditorCurve(_clip, binding); + _curveCache[binding] = cached; + } + } + + return cached.Value; + } + + public ObjectReferenceKeyframe[] GetObjectCurve(EditorCurveBinding binding) + { + if (_pptrCurveCache.TryGetValue(binding, out var cached)) + { + if (cached.Dirty == false && cached.Value == null) + { + cached.Value = AnimationUtility.GetObjectReferenceCurve(_clip, binding); + _pptrCurveCache[binding] = cached; + } + } + + return cached.Value; + } + + public void SetFloatCurve(EditorCurveBinding binding, AnimationCurve curve) + { + if (binding.isPPtrCurve || binding.isDiscreteCurve) + { + throw new ArgumentException("Binding must be a float curve", nameof(binding)); + } + + if (IsMarkerClip) return; + + if (!_curveCache.TryGetValue(binding, out var cached)) + { + cached = new CachedCurve(); + } + + cached.Value = curve; + cached.Dirty = true; + IsDirty = true; + + _curveCache[binding] = cached; + } + + public void SetObjectCurve(EditorCurveBinding binding, ObjectReferenceKeyframe[] curve) + { + if (!binding.isPPtrCurve) + { + throw new ArgumentException("Binding must be a PPtr curve", nameof(binding)); + } + + if (IsMarkerClip) return; + + if (!_pptrCurveCache.TryGetValue(binding, out var cached)) + { + cached = new CachedCurve(); + } + + cached.Value = curve; + cached.Dirty = true; + IsDirty = true; + + _pptrCurveCache[binding] = cached; + } + + protected override Motion Prepare(object context) + { + return _clip; + } + + protected override void Commit(object context_, Motion obj) + { + if (IsMarkerClip || !IsDirty) return; + + var context = (CommitContext)context_; + + var clip = (AnimationClip)obj; + + // WORKAROUND: AnimationUtility.SetEditorCurves doesn't actually delete curves when null, despite the + // documentation claiming it will. Fault in all uncached curves, then clear everything. + foreach (var curve in _curveCache.ToList()) + { + if (!curve.Value.Dirty && curve.Value.Value == null) GetFloatCurve(curve.Key); + } + + foreach (var curve in _pptrCurveCache.ToList()) + { + if (!curve.Value.Dirty && curve.Value.Value == null) GetObjectCurve(curve.Key); + } + + clip.ClearCurves(); + + var changedBindings = _curveCache.Where(c => c.Value.Dirty || c.Value.Value != null).ToList(); + var changedPptrBindings = _pptrCurveCache.Where(c => c.Value.Dirty || c.Value.Value != null).ToList(); + + if (changedBindings.Count > 0) + { + var bindings = changedBindings.Select(c => c.Key).ToArray(); + var curves = changedBindings.Select(c => c.Value.Value).ToArray(); + + AnimationUtility.SetEditorCurves(clip, bindings, curves); + } + + if (changedPptrBindings.Count > 0) + { + var bindings = changedPptrBindings.Select(c => c.Key).ToArray(); + var curves = changedPptrBindings.Select(c => c.Value.Value).ToArray(); + + AnimationUtility.SetObjectReferenceCurves(clip, bindings, curves); + } + + // Restore HighQualityCurves value + var serializedObject = new SerializedObject(clip); + serializedObject.FindProperty("m_UseHighQualityCurve").boolValue = UseHighQualityCurves; + serializedObject.ApplyModifiedPropertiesWithoutUndo(); + + // Restore additive reference pose + var settings = AnimationUtility.GetAnimationClipSettings(clip); + settings.additiveReferencePoseClip = AdditiveReferencePoseClip != null + ? (AnimationClip)context.CommitObject(AdditiveReferencePoseClip) + : null; + settings.additiveReferencePoseTime = AdditiveReferencePoseTime; + AnimationUtility.SetAnimationClipSettings(clip, settings); + } + + public AnimationCurve GetFloatCurve(string path, Type type, string prop) + { + return GetFloatCurve(EditorCurveBinding.FloatCurve(path, type, prop)); + } + + public ObjectReferenceKeyframe[] GetObjectCurve(string path, Type type, string prop) + { + return GetObjectCurve(EditorCurveBinding.PPtrCurve(path, type, prop)); + } + + public void SetFloatCurve(string path, Type type, string prop, AnimationCurve curve) + { + SetFloatCurve(EditorCurveBinding.FloatCurve(path, type, prop), curve); + } + + public void SetObjectCurve(string path, Type type, string prop, ObjectReferenceKeyframe[] curve) + { + SetObjectCurve(EditorCurveBinding.PPtrCurve(path, type, prop), curve); + } + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualClip.cs.meta b/Editor/API/AnimatorServices/VirtualClip.cs.meta new file mode 100644 index 0000000..5dce41d --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualClip.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5783bb0dfce34aceab5778a205e8399e +timeCreated: 1730067145 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualLayer.cs b/Editor/API/AnimatorServices/VirtualLayer.cs new file mode 100644 index 0000000..f80f5e2 --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualLayer.cs @@ -0,0 +1,31 @@ +using UnityEditor.Animations; +using UnityEngine; + +namespace nadena.dev.ndmf.animator +{ + /// + /// A layer within a VirtualAnimatorController + /// + public class VirtualLayer + { + private VirtualStateMachine _stateMachine; + + /// + /// Returns a "virtual layer index" which can be used to map to the actual layer index in the animator controller, + /// even if layer order changes. This will typically be a very large value (>2^16). + /// + public int VirtualLayerIndex { get; } + + public AvatarMask AvatarMask { get; set; } + public AnimatorLayerBlendingMode BlendingMode { get; set; } + public float DefaultWeight { get; set; } + public bool IKPass { get; set; } + + public string Name { get; set; } + // State machine + // public VirtualStateMachine StateMachine { get; set; } + + public bool SyncedLayerAffectsTiming { get; set; } + public VirtualLayer SyncedLayer { get; set; } + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualLayer.cs.meta b/Editor/API/AnimatorServices/VirtualLayer.cs.meta new file mode 100644 index 0000000..ebe4247 --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualLayer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e601f7ba786f4371b9dec591a0253bb0 +timeCreated: 1730065090 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualMotion.cs b/Editor/API/AnimatorServices/VirtualMotion.cs new file mode 100644 index 0000000..1b60603 --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualMotion.cs @@ -0,0 +1,28 @@ +using UnityEngine; + +namespace nadena.dev.ndmf.animator +{ + public abstract class VirtualMotion : ICommitable + { + internal VirtualMotion() + { + } + + + [ExcludeFromDocs] + protected abstract Motion Prepare(object /* CommitContext */ context); + + [ExcludeFromDocs] + protected abstract void Commit(object /* CommitContext */ context, Motion obj); + + Motion ICommitable.Prepare(CommitContext context) + { + return Prepare(context); + } + + void ICommitable.Commit(CommitContext context, Motion obj) + { + Commit(context, obj); + } + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualMotion.cs.meta b/Editor/API/AnimatorServices/VirtualMotion.cs.meta new file mode 100644 index 0000000..7e96ae0 --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualMotion.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b8334ee9be934f45825a1b375d3b036a +timeCreated: 1730067047 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualState.cs b/Editor/API/AnimatorServices/VirtualState.cs new file mode 100644 index 0000000..c6255b9 --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualState.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEditor.Animations; +using UnityEngine; + +namespace nadena.dev.ndmf.animator +{ + public class VirtualState : ICommitable + { + private AnimatorState _state; + + public List Behaviours { get; set; } + + public float CycleOffset + { + get => _state.cycleOffset; + set => _state.cycleOffset = value; + } + + [CanBeNull] + public string CycleOffsetParameter + { + get => _state.cycleOffsetParameterActive ? _state.cycleOffsetParameter : null; + set + { + _state.cycleOffsetParameterActive = value != null; + _state.cycleOffsetParameter = value ?? ""; + } + } + + public bool IKOnFeet + { + get => _state.iKOnFeet; + set => _state.iKOnFeet = value; + } + + public bool Mirror + { + get => _state.mirror; + set => _state.mirror = value; + } + + [CanBeNull] + public string MirrorParameter + { + get => _state.mirrorParameterActive ? _state.mirrorParameter : null; + set + { + _state.mirrorParameterActive = value != null; + _state.mirrorParameter = value ?? ""; + } + } + + // public VirtualMotion Motion; + + public float Speed + { + get => _state.speed; + set => _state.speed = value; + } + + [CanBeNull] + public string SpeedParameter + { + get => _state.speedParameterActive ? _state.speedParameter : null; + set + { + _state.speedParameterActive = value != null; + _state.speedParameter = value ?? ""; + } + } + + public string Tag + { + get => _state.tag; + set => _state.tag = value; + } + + [CanBeNull] + public string TimeParameter + { + get => _state.timeParameterActive ? _state.timeParameter : null; + set + { + _state.timeParameterActive = value != null; + _state.timeParameter = value ?? ""; + } + } + + public List Transitions { get; set; } + + public bool WriteDefaultValues + { + get => _state.writeDefaultValues; + set => _state.writeDefaultValues = value; + } + + // Helpers + + // AddExitTransition + // AddStateMachineBehaviour + // AddTransition + // RemoveTransition + + + AnimatorState ICommitable.Prepare(CommitContext context) + { + throw new NotImplementedException(); + } + + void ICommitable.Commit(CommitContext context, AnimatorState obj) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualState.cs.meta b/Editor/API/AnimatorServices/VirtualState.cs.meta new file mode 100644 index 0000000..4d44078 --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 752085aab816498d89a77dc3e80de8de +timeCreated: 1730065672 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualStateMachine.cs b/Editor/API/AnimatorServices/VirtualStateMachine.cs new file mode 100644 index 0000000..2e3f536 --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualStateMachine.cs @@ -0,0 +1,24 @@ +using System; +using UnityEditor.Animations; + +namespace nadena.dev.ndmf.animator +{ + /// + /// Represents a state machine in a virtual layer. + /// + public class VirtualStateMachine : ICommitable + { + private AnimatorStateMachine _stateMachine; + + + AnimatorStateMachine ICommitable.Prepare(CommitContext context) + { + throw new NotImplementedException(); + } + + void ICommitable.Commit(CommitContext context, AnimatorStateMachine obj) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualStateMachine.cs.meta b/Editor/API/AnimatorServices/VirtualStateMachine.cs.meta new file mode 100644 index 0000000..3ea5eea --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualStateMachine.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 98c18be0e6364dc5b5a47c79c4cc9bfc +timeCreated: 1730065344 \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualTransition.cs b/Editor/API/AnimatorServices/VirtualTransition.cs new file mode 100644 index 0000000..53a35e5 --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualTransition.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using UnityEditor.Animations; + +namespace nadena.dev.ndmf.animator +{ + public class VirtualTransition : ICommitable + { + private AnimatorTransitionBase _transition; + + private AnimatorStateTransition _stateTransition + { + get + { + if (_transition is AnimatorStateTransition ast) return ast; + throw new InvalidOperationException("Transition is not an AnimatorStateTransition"); + } + } + + // AnimatorStateTransition + public bool CanTransitionToSelf + { + get => _stateTransition.canTransitionToSelf; + set => _stateTransition.canTransitionToSelf = value; + } + + public float Duration + { + get => _stateTransition.duration; + set => _stateTransition.duration = value; + } + + public float? ExitTime + { + get => _stateTransition.hasExitTime ? _stateTransition.exitTime : null; + set + { + _stateTransition.hasExitTime = value.HasValue; + _stateTransition.exitTime = value ?? 0; + } + } + + public bool HasFixedDuration + { + get => _stateTransition.hasFixedDuration; + set => _stateTransition.hasFixedDuration = value; + } + + public TransitionInterruptionSource InterruptionSource + { + get => _stateTransition.interruptionSource; + set => _stateTransition.interruptionSource = value; + } + + public float Offset + { + get => _stateTransition.offset; + set => _stateTransition.offset = value; + } + + public bool OrderedInterruption + { + get => _stateTransition.orderedInterruption; + set => _stateTransition.orderedInterruption = value; + } + + // AnimatorTransitionBase + + public List Conditions { get; set; } + + public void SetDestination(VirtualState state) + { + DestinationState = state; + DestinationStateMachine = null; + _stateTransition.isExit = false; + } + + public void SetDestination(VirtualStateMachine stateMachine) + { + DestinationState = null; + DestinationStateMachine = stateMachine; + _stateTransition.isExit = false; + } + + public void SetExitDestination() + { + DestinationState = null; + DestinationStateMachine = null; + _stateTransition.isExit = true; + } + + public VirtualState DestinationState { get; private set; } + public VirtualStateMachine DestinationStateMachine { get; private set; } + + public bool ExitIsDestination => _stateTransition.isExit; + + public bool Mute + { + get => _stateTransition.mute; + set => _stateTransition.mute = value; + } + + public bool Solo + { + get => _stateTransition.solo; + set => _stateTransition.solo = value; + } + + AnimatorTransitionBase ICommitable.Prepare(CommitContext context) + { + return _transition; + } + + void ICommitable.Commit(CommitContext context, AnimatorTransitionBase _) + { + if (DestinationState != null) + { + _stateTransition.destinationState = context.CommitObject(DestinationState); + } + else if (DestinationStateMachine != null) + { + _stateTransition.destinationStateMachine = context.CommitObject(DestinationStateMachine); + } + } + } +} \ No newline at end of file diff --git a/Editor/API/AnimatorServices/VirtualTransition.cs.meta b/Editor/API/AnimatorServices/VirtualTransition.cs.meta new file mode 100644 index 0000000..f5b2aed --- /dev/null +++ b/Editor/API/AnimatorServices/VirtualTransition.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a8a9026c7034405d8064127214c480bc +timeCreated: 1730065684 \ No newline at end of file diff --git a/UnitTests~/AnimationServices.meta b/UnitTests~/AnimationServices.meta new file mode 100644 index 0000000..eed60df --- /dev/null +++ b/UnitTests~/AnimationServices.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d7558ad5ddc4496bb1d853be8ea29994 +timeCreated: 1730069545 \ No newline at end of file diff --git a/UnitTests~/AnimationServices/HQ_OFF.anim b/UnitTests~/AnimationServices/HQ_OFF.anim new file mode 100644 index 0000000..2d9ed07 --- /dev/null +++ b/UnitTests~/AnimationServices/HQ_OFF.anim @@ -0,0 +1,98 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!74 &7400000 +AnimationClip: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: HQ_OFF + serializedVersion: 6 + m_Legacy: 0 + m_Compressed: 0 + m_UseHighQualityCurve: 0 + m_RotationCurves: [] + m_CompressedRotationCurves: [] + m_EulerCurves: [] + m_PositionCurves: [] + m_ScaleCurves: [] + m_FloatCurves: + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: Infinity + outSlope: Infinity + tangentMode: 103 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_Enabled + path: + classID: 114 + script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + m_PPtrCurves: [] + m_SampleRate: 60 + m_WrapMode: 0 + m_Bounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 0, y: 0, z: 0} + m_ClipBindingConstant: + genericBindings: + - serializedVersion: 2 + path: 0 + attribute: 3305885265 + script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + typeID: 114 + customType: 24 + isPPtrCurve: 0 + pptrCurveMapping: [] + m_AnimationClipSettings: + serializedVersion: 2 + m_AdditiveReferencePoseClip: {fileID: 0} + m_AdditiveReferencePoseTime: 0 + m_StartTime: 0 + m_StopTime: 0 + m_OrientationOffsetY: 0 + m_Level: 0 + m_CycleOffset: 0 + m_HasAdditiveReferencePose: 0 + m_LoopTime: 0 + m_LoopBlend: 0 + m_LoopBlendOrientation: 0 + m_LoopBlendPositionY: 0 + m_LoopBlendPositionXZ: 0 + m_KeepOriginalOrientation: 0 + m_KeepOriginalPositionY: 1 + m_KeepOriginalPositionXZ: 0 + m_HeightFromFeet: 0 + m_Mirror: 0 + m_EditorCurves: + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: Infinity + outSlope: Infinity + tangentMode: 103 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_Enabled + path: + classID: 114 + script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + m_EulerEditorCurves: [] + m_HasGenericRootTransform: 0 + m_HasMotionFloatCurves: 0 + m_Events: [] diff --git a/UnitTests~/AnimationServices/HQ_OFF.anim.meta b/UnitTests~/AnimationServices/HQ_OFF.anim.meta new file mode 100644 index 0000000..2a9cb98 --- /dev/null +++ b/UnitTests~/AnimationServices/HQ_OFF.anim.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ef6f162083551c84c8d82cab46f87eab +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 7400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnitTests~/AnimationServices/HQ_ON.anim b/UnitTests~/AnimationServices/HQ_ON.anim new file mode 100644 index 0000000..930700f --- /dev/null +++ b/UnitTests~/AnimationServices/HQ_ON.anim @@ -0,0 +1,98 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!74 &7400000 +AnimationClip: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: HQ_ON + serializedVersion: 6 + m_Legacy: 0 + m_Compressed: 0 + m_UseHighQualityCurve: 1 + m_RotationCurves: [] + m_CompressedRotationCurves: [] + m_EulerCurves: [] + m_PositionCurves: [] + m_ScaleCurves: [] + m_FloatCurves: + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: Infinity + outSlope: Infinity + tangentMode: 103 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_Enabled + path: + classID: 114 + script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + m_PPtrCurves: [] + m_SampleRate: 60 + m_WrapMode: 0 + m_Bounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 0, y: 0, z: 0} + m_ClipBindingConstant: + genericBindings: + - serializedVersion: 2 + path: 0 + attribute: 3305885265 + script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + typeID: 114 + customType: 24 + isPPtrCurve: 0 + pptrCurveMapping: [] + m_AnimationClipSettings: + serializedVersion: 2 + m_AdditiveReferencePoseClip: {fileID: 0} + m_AdditiveReferencePoseTime: 0 + m_StartTime: 0 + m_StopTime: 0 + m_OrientationOffsetY: 0 + m_Level: 0 + m_CycleOffset: 0 + m_HasAdditiveReferencePose: 0 + m_LoopTime: 0 + m_LoopBlend: 0 + m_LoopBlendOrientation: 0 + m_LoopBlendPositionY: 0 + m_LoopBlendPositionXZ: 0 + m_KeepOriginalOrientation: 0 + m_KeepOriginalPositionY: 1 + m_KeepOriginalPositionXZ: 0 + m_HeightFromFeet: 0 + m_Mirror: 0 + m_EditorCurves: + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: Infinity + outSlope: Infinity + tangentMode: 103 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_Enabled + path: + classID: 114 + script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + m_EulerEditorCurves: [] + m_HasGenericRootTransform: 0 + m_HasMotionFloatCurves: 0 + m_Events: [] diff --git a/UnitTests~/AnimationServices/HQ_ON.anim.meta b/UnitTests~/AnimationServices/HQ_ON.anim.meta new file mode 100644 index 0000000..74bd7a8 --- /dev/null +++ b/UnitTests~/AnimationServices/HQ_ON.anim.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 02136d7a0efc77b468045eb159f2762a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 7400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnitTests~/AnimationServices/VirtualClipTest.cs b/UnitTests~/AnimationServices/VirtualClipTest.cs new file mode 100644 index 0000000..d225cf5 --- /dev/null +++ b/UnitTests~/AnimationServices/VirtualClipTest.cs @@ -0,0 +1,250 @@ +using System; +using System.Linq; +using nadena.dev.ndmf; +using nadena.dev.ndmf.animator; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace UnitTests.AnimationServices +{ + public class VirtualClipTest : TestBase + { + private GameObject avatarRoot; + private BuildContext context; + + [SetUp] + public void Setup() + { + avatarRoot = CreateRoot("root"); + context = CreateContext(avatarRoot); + } + + AnimationClip Commit(VirtualClip clip) + { + return TrackObject((AnimationClip) new CommitContext().CommitObject(clip)); + } + + [Test] + public void PreservesInitialCurves() + { + var material = NewTestMaterial(); + AnimationClip ac = TrackObject(new AnimationClip()); + ac.name = "foo"; + var originalCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1)); + ac.SetCurve("abc", typeof(GameObject), "m_IsActive", originalCurve); + + AnimationUtility.SetObjectReferenceCurve(ac, EditorCurveBinding.PPtrCurve("def", typeof(MeshRenderer), "m_Materials"), new ObjectReferenceKeyframe[] + { + new() {time = 0, value = material}, + }); + + VirtualClip vc = VirtualClip.Clone(context, ac); + var committedClip = Commit(vc); + + var bindings = AnimationUtility.GetCurveBindings(committedClip).ToList(); + Assert.AreEqual(1, bindings.Count); + Assert.AreEqual("abc", bindings[0].path); + Assert.AreEqual(originalCurve, AnimationUtility.GetEditorCurve(committedClip, bindings[0])); + + var objBindings = AnimationUtility.GetObjectReferenceCurveBindings(committedClip); + Assert.AreEqual(1, objBindings.Length); + Assert.AreEqual("def", objBindings[0].path); + Assert.AreEqual(material, AnimationUtility.GetObjectReferenceCurve(committedClip, objBindings[0])[0].value); + } + + [Test] + public void EditExistingFloatCurve() + { + AnimationClip ac = TrackObject(new AnimationClip()); + ac.name = "foo"; + var originalCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1)); + ac.SetCurve("abc", typeof(GameObject), "m_IsActive", originalCurve); + + VirtualClip vc = VirtualClip.Clone(context, ac); + + var bindings = vc.GetFloatCurveBindings().ToList(); + Assert.AreEqual(1, bindings.Count); + Assert.AreEqual("abc", bindings[0].path); + Assert.AreEqual(typeof(GameObject), bindings[0].type); + Assert.AreEqual("m_IsActive", bindings[0].propertyName); + + var existingCurve = vc.GetFloatCurve("abc", typeof(GameObject), "m_IsActive"); + Assert.IsNotNull(existingCurve); + AssertEqualNotSame(existingCurve, originalCurve); + + // Replace the curve and see if it commits + var newCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 2)); + vc.SetFloatCurve("abc", typeof(GameObject), "m_IsActive", newCurve); + + var committedClip = Commit(vc); + var newCommittedCurve = AnimationUtility.GetEditorCurve(committedClip, bindings[0]); + AssertEqualNotSame(newCommittedCurve, newCurve); + Assert.AreEqual(committedClip.name, ac.name); + } + + [Test] + public void CreateDeleteFloatCurve() + { + AnimationClip ac = TrackObject(new AnimationClip()); + ac.SetCurve("abc", typeof(GameObject), "m_IsActive", new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1))); + + VirtualClip vc = VirtualClip.Clone(context, ac); + vc.SetFloatCurve("abc", typeof(GameObject), "m_IsActive", null); + vc.SetFloatCurve("def", typeof(GameObject), "m_IsActive", new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1))); + + Assert.AreEqual(vc.GetFloatCurveBindings().Count(), 1); + + var committedClip = Commit(vc); + var bindings = AnimationUtility.GetCurveBindings(committedClip).ToList(); + Assert.AreEqual(1, bindings.Count); + Assert.AreEqual("def", bindings[0].path); + } + + [Test] + public void EditExistingObjectCurve() + { + var m1 = TrackObject(NewTestMaterial()); + var m2 = TrackObject(NewTestMaterial()); + + AnimationClip ac = TrackObject(new AnimationClip()); + AnimationUtility.SetObjectReferenceCurve(ac, new EditorCurveBinding() + { + path = "abc", + type = typeof(MeshRenderer), + propertyName = "m_Materials.Array.data[0]" + }, new ObjectReferenceKeyframe[] + { + new() {time = 0, value = m1}, + }); + + VirtualClip vc = VirtualClip.Clone(context, ac); + vc.SetObjectCurve("abc", typeof(MeshRenderer), "m_Materials.Array.data[0]", new ObjectReferenceKeyframe[] + { + new() {time = 0, value = m2}, + }); + + var committedClip = Commit(vc); + var newCommittedCurve = AnimationUtility.GetObjectReferenceCurve(committedClip, new EditorCurveBinding() + { + path = "abc", + type = typeof(MeshRenderer), + propertyName = "m_Materials.Array.data[0]" + }); + Assert.IsNotNull(newCommittedCurve); + Assert.AreEqual(1, newCommittedCurve.Length); + Assert.AreEqual(0, newCommittedCurve[0].time); + Assert.AreEqual(m2, newCommittedCurve[0].value); + + // check that the original clip is not modified + var originalCurve = AnimationUtility.GetObjectReferenceCurve(ac, new EditorCurveBinding() + { + path = "abc", + type = typeof(MeshRenderer), + propertyName = "m_Materials.Array.data[0]" + }); + Assert.IsNotNull(originalCurve); + Assert.AreEqual(m1, originalCurve[0].value); + } + + private Material NewTestMaterial() + { + Shader s = Shader.Find("Unlit/Color"); + return new Material(s); + } + + [Test] + public void CreateDeleteObjectCurve() + { + var m1 = TrackObject(NewTestMaterial()); + + var ac = TrackObject(new AnimationClip()); + AnimationUtility.SetObjectReferenceCurve(ac, new EditorCurveBinding() + { + path = "abc", + type = typeof(MeshRenderer), + propertyName = "m_Materials.Array.data[0]" + }, new ObjectReferenceKeyframe[] + { + new() {time = 0, value = m1}, + }); + + VirtualClip vc = VirtualClip.Clone(context, ac); + vc.SetObjectCurve("abc", typeof(MeshRenderer), "m_Materials.Array.data[0]", null); + vc.SetObjectCurve("def", typeof(MeshRenderer), "m_Materials.Array.data[0]", new ObjectReferenceKeyframe[] + { + new() {time = 0, value = m1}, + }); + + Assert.AreEqual(1, vc.GetObjectCurveBindings().Count()); + + var committedClip = Commit(vc); + var bindings = AnimationUtility.GetObjectReferenceCurveBindings(committedClip); + Assert.AreEqual(1, bindings.Length); + Assert.AreEqual("def", bindings[0].path); + } + + [Test] + public void TestEditPath() + { + AnimationClip ac = TrackObject(new AnimationClip()); + ac.SetCurve("abc", typeof(GameObject), "m_IsActive", new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1))); + ac.SetCurve("DEF", typeof(GameObject), "m_IsActive", new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1))); + ac.SetCurve("x", typeof(GameObject), "m_IsActive", new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1))); + ac.SetCurve("X", typeof(GameObject), "m_IsActive", new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1))); + AnimationUtility.SetObjectReferenceCurve(ac, new EditorCurveBinding() + { + path = "foo", + type = typeof(MeshRenderer), + propertyName = "m_Materials.Array.data[0]" + }, new ObjectReferenceKeyframe[] + { + new() {time = 0, value = new Material(Shader.Find("Standard"))}, + }); + + VirtualClip vc = VirtualClip.Clone(context, ac); + vc.EditPaths(s => s.ToUpperInvariant()); + + Assert.AreEqual(new[] { "ABC", "DEF", "X" }, vc.GetFloatCurveBindings().Select(b => b.path).OrderBy(b => b).ToArray()); + Assert.AreEqual(new[] { "FOO" }, vc.GetObjectCurveBindings().Select(b => b.path).OrderBy(b => b).ToArray()); + + var committedClip = Commit(vc); + var bindings = AnimationUtility.GetCurveBindings(committedClip).Select(b => b.path).OrderBy(b => b).ToList(); + Assert.AreEqual(new[] { "ABC", "DEF", "X" }, bindings); + + var objBindings = AnimationUtility.GetObjectReferenceCurveBindings(committedClip).Select(b => b.path).OrderBy(b => b).ToList(); + Assert.AreEqual( new[] { "FOO" }, objBindings); + } + + [Test] + public void PreservesHighQualityMode([Values("HQ_ON.anim", "HQ_OFF.anim")] string testAsset) + { + AnimationClip ac = LoadAsset(testAsset); + bool hq = new SerializedObject(ac).FindProperty("m_UseHighQualityCurve").boolValue; + + VirtualClip vc = VirtualClip.Clone(context, ac); + + vc.SetFloatCurve(EditorCurveBinding.FloatCurve("abc", typeof(GameObject), "m_IsActive"), + new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1))); + + var committedClip = Commit(vc); + + Assert.AreEqual(hq, new SerializedObject(committedClip).FindProperty("m_UseHighQualityCurve").boolValue); + } + + // TODO: additive reference pose, animation clip settings/misc properties tests + + private static void AssertEqualNotSame(AnimationCurve newCommittedCurve, AnimationCurve newCurve) + { + Assert.IsNotNull(newCommittedCurve); + Assert.AreNotSame(newCommittedCurve, newCurve); + Assert.AreEqual(newCurve.keys.Length, newCommittedCurve.keys.Length); + for (int i = 0; i < newCurve.keys.Length; i++) + { + Assert.AreEqual(newCurve.keys[i].time, newCommittedCurve.keys[i].time); + Assert.AreEqual(newCurve.keys[i].value, newCommittedCurve.keys[i].value); + } + } + } +} \ No newline at end of file diff --git a/UnitTests~/AnimationServices/VirtualClipTest.cs.meta b/UnitTests~/AnimationServices/VirtualClipTest.cs.meta new file mode 100644 index 0000000..156a958 --- /dev/null +++ b/UnitTests~/AnimationServices/VirtualClipTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0e8efd4fa18b4639888637ff7eae35a1 +timeCreated: 1730069550 \ No newline at end of file diff --git a/UnitTests~/AnimationServices/clipProperties.anim b/UnitTests~/AnimationServices/clipProperties.anim new file mode 100644 index 0000000..a6adfef --- /dev/null +++ b/UnitTests~/AnimationServices/clipProperties.anim @@ -0,0 +1,98 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!74 &7400000 +AnimationClip: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: clipProperties + serializedVersion: 6 + m_Legacy: 0 + m_Compressed: 0 + m_UseHighQualityCurve: 1 + m_RotationCurves: [] + m_CompressedRotationCurves: [] + m_EulerCurves: [] + m_PositionCurves: [] + m_ScaleCurves: [] + m_FloatCurves: + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: Infinity + outSlope: Infinity + tangentMode: 103 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_Enabled + path: + classID: 114 + script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + m_PPtrCurves: [] + m_SampleRate: 60 + m_WrapMode: 0 + m_Bounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 0, y: 0, z: 0} + m_ClipBindingConstant: + genericBindings: + - serializedVersion: 2 + path: 0 + attribute: 3305885265 + script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + typeID: 114 + customType: 24 + isPPtrCurve: 0 + pptrCurveMapping: [] + m_AnimationClipSettings: + serializedVersion: 2 + m_AdditiveReferencePoseClip: {fileID: 0} + m_AdditiveReferencePoseTime: 0 + m_StartTime: 0 + m_StopTime: 0 + m_OrientationOffsetY: 0 + m_Level: 0 + m_CycleOffset: 0 + m_HasAdditiveReferencePose: 0 + m_LoopTime: 0 + m_LoopBlend: 0 + m_LoopBlendOrientation: 0 + m_LoopBlendPositionY: 0 + m_LoopBlendPositionXZ: 0 + m_KeepOriginalOrientation: 0 + m_KeepOriginalPositionY: 1 + m_KeepOriginalPositionXZ: 0 + m_HeightFromFeet: 0 + m_Mirror: 0 + m_EditorCurves: + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: Infinity + outSlope: Infinity + tangentMode: 103 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_Enabled + path: + classID: 114 + script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3} + m_EulerEditorCurves: [] + m_HasGenericRootTransform: 0 + m_HasMotionFloatCurves: 0 + m_Events: [] diff --git a/UnitTests~/AnimationServices/clipProperties.anim.meta b/UnitTests~/AnimationServices/clipProperties.anim.meta new file mode 100644 index 0000000..8997deb --- /dev/null +++ b/UnitTests~/AnimationServices/clipProperties.anim.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dc1092af50978ab4dbfe252aab93b26a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 7400000 + userData: + assetBundleName: + assetBundleVariant: