diff --git a/Source/AssetRipper.Processing/AnimationClips/AnimationClipConverter.cs b/Source/AssetRipper.Processing/AnimationClips/AnimationClipConverter.cs index 863c08abc..54e397bad 100644 --- a/Source/AssetRipper.Processing/AnimationClips/AnimationClipConverter.cs +++ b/Source/AssetRipper.Processing/AnimationClips/AnimationClipConverter.cs @@ -1,5 +1,6 @@ using AssetRipper.Assets.Cloning; using AssetRipper.Assets.Collections; +using AssetRipper.Assets.Generics; using AssetRipper.Checksum; using AssetRipper.IO.Endian; using AssetRipper.Processing.AnimationClips.Editor; @@ -44,120 +45,129 @@ namespace AssetRipper.Processing.AnimationClips public static void Process(IAnimationClip clip, PathChecksumCache checksumCache) { - AnimationClipConverter converter = new AnimationClipConverter(clip, checksumCache); + AnimationClipConverter converter = new(clip, checksumCache); converter.ProcessInner(); } private void ProcessInner() { - if (m_clip.Has_MuscleClip_C74() && m_clip.Has_ClipBindingConstant_C74()) + if (m_clip.Has_ClipBindingConstant_C74()) { IClip clip = m_clip.MuscleClip_C74.Clip.Data; - IAnimationClipBindingConstant bindings = m_clip.ClipBindingConstant_C74; IReadOnlyList streamedFrames = GenerateFramesFromStreamedClip(clip.StreamedClip); - float lastDenseFrame = clip.DenseClip.FrameCount / clip.DenseClip.SampleRate; - float lastSampleFrame = streamedFrames.Count > 1 ? streamedFrames[streamedFrames.Count - 2].Time : 0.0f; - float lastFrame = Math.Max(lastDenseFrame, lastSampleFrame); + ProcessStreams(streamedFrames); + + int streamedCurveCount = clip.StreamedClip.CurveCount(); + ProcessDenses(clip.DenseClip, streamedCurveCount); - ProcessStreams(streamedFrames, bindings, clip.DenseClip.SampleRate); - ProcessDenses(clip, bindings); if (clip.Has_ConstantClip()) { - ProcessConstant(clip, clip.ConstantClip, bindings, lastFrame); - } - if (m_clip.Has_MuscleClipInfo_C74()) - { - m_clip.MuscleClipInfo_C74.Initialize(m_clip.MuscleClip_C74); + int preConstantCurves = streamedCurveCount + (int)clip.DenseClip.CurveCount; + float lastConstantTime = CalculateLastConstantTime(streamedFrames, m_clip.MuscleClip_C74.StopTime); + ProcessConstant(clip.ConstantClip, preConstantCurves, lastConstantTime); } + + m_clip.MuscleClipInfo_C74.Initialize(m_clip.MuscleClip_C74); } } - private void ProcessStreams(IReadOnlyList streamFrames, IAnimationClipBindingConstant bindings, float sampleRate) + private void ProcessStreams(IReadOnlyList streamedFrames) { Span curveValues = [0, 0, 0, 0]; Span inSlopeValues = [0, 0, 0, 0]; Span outSlopeValues = [0, 0, 0, 0]; - float interval = 1.0f / sampleRate; - // first (index [0]) stream frame is for slope calculation for the first real frame (index [1]) - // last one (index [count - 1]) is +Infinity - // it is made for slope processing, but we don't need them - bool frameIndex0 = true; - for (int frameIndex = 0; frameIndex < streamFrames.Count - 1; frameIndex++) + if (streamedFrames.Count > 1) { - StreamedFrame frame = streamFrames[frameIndex]; - for (int curveIndex = 0; curveIndex < frame.Curves.Length;) - { - StreamedCurveKey curve = frame.Curves[curveIndex]; - IGenericBinding binding = bindings.FindBinding(curve.Index); + streamedFrames[0].Time = 0f; // fix first frame for PPtrCurves to have Time=0 instead of float.MinValue + } + int frameCount = streamedFrames.Count - 1; // last StreamedFrame is dummy so must be skipped + for (int frameIdx = 0; frameIdx < frameCount; frameIdx++) + { + // last real frame doesn't need outSlope calculation, and will have inSlope from previous iteration + bool doSlopeCalc = frameIdx != frameCount - 1; + StreamedFrame frame = streamedFrames[frameIdx]; + for (int curveIdx = 0; curveIdx < frame.Curves.Length;) + { + int curveID = frame.Curves[curveIdx].Index; + IGenericBinding binding = GetBinding(curveID); string path = GetCurvePath(binding.Path); + StreamedCurveKey curve; if (binding.IsTransform()) { - if (frameIndex0) + int transformDim = binding.TransformType().GetDimension(); + if (frameIdx == 0) // first StreamedFrame only contains PPtrCurves, skip { - curveIndex = GetNextCurve(frame, curveIndex); + curveIdx += transformDim; continue; } - GetPreviousFrame(streamFrames, curve.Index, frameIndex, out int prevFrameIndex, out int prevCurveIndex); - int dimension = binding.TransformType().GetDimension(); - for (int key = 0; key < dimension; key++) + for (int offset = 0; offset < transformDim; offset++) { - StreamedCurveKey keyCurve = frame.Curves[curveIndex];//index out of bounds - StreamedFrame prevFrame = streamFrames[prevFrameIndex]; - StreamedCurveKey prevKeyCurve = prevFrame.Curves[prevCurveIndex + key]; - float deltaTime = frame.Time - prevFrame.Time; - curveValues[key] = keyCurve.Value; - inSlopeValues[key] = prevKeyCurve.CalculateNextInSlope(deltaTime, keyCurve.Value); - outSlopeValues[key] = keyCurve.OutSlope; - curveIndex = GetNextCurve(frame, curveIndex); + curve = frame.Curves[curveIdx]; + if (doSlopeCalc) + { + if (TryGetNextFrame(streamedFrames, frameIdx, curveID, out StreamedFrame? nextFrame, out int nextCurveIdx)) + { + StreamedCurveKey nextCurve = nextFrame.Curves[nextCurveIdx + offset]; + curve.CalculateSlopes(frame.Time, nextFrame.Time, nextCurve); + } + } + curveValues[offset] = curve.Value; + inSlopeValues[offset] = curve.InSlope; + outSlopeValues[offset] = curve.OutSlope; + curveIdx++; } - AddTransformCurve(frame.Time, binding.TransformType(), curveValues, inSlopeValues, outSlopeValues, 0, path); + continue; } - else if (binding.CustomType == (byte)BindingCustomType.None) + curve = frame.Curves[curveIdx]; + if (!binding.IsPPtrCurve()) // Skip slope calculation for PPtrCurves { - if (frameIndex0) + if (frameIdx == 0) // first StreamedFrame only contains PPtrCurves, skip { - curveIndex = GetNextCurve(frame, curveIndex); + curveIdx++; continue; } - AddDefaultCurve(binding, path, frame.Time, frame.Curves[curveIndex].Value); - curveIndex = GetNextCurve(frame, curveIndex); + if (doSlopeCalc) + { + if (TryGetNextFrame(streamedFrames, frameIdx, curveID, out StreamedFrame? nextFrame, out int nextCurveIdx)) + { + StreamedCurveKey nextCurve = nextFrame.Curves[nextCurveIdx]; + curve.CalculateSlopes(frame.Time, nextFrame.Time, nextCurve); + } + } + } + if (binding.CustomType == (byte)BindingCustomType.None) + { + AddDefaultCurve(binding, path, frame.Time, curve.Value, curve.InSlope, curve.OutSlope); } else { - AddCustomCurve(bindings, binding, path, frame.Time, frame.Curves[curveIndex].Value); - curveIndex = GetNextCurve(frame, curveIndex); + AddCustomCurve(binding, path, frame.Time, curve.Value, curve.InSlope, curve.OutSlope); } - } - if (frameIndex0) - { - frameIndex0 = false; + curveIdx++; } } } - private void ProcessDenses(IClip clip, IAnimationClipBindingConstant bindings) + private void ProcessDenses(DenseClip dense, int preDenseCurves) { - DenseClip dense = clip.DenseClip; + ReadOnlySpan slopeValues = [0, 0, 0, 0]; // no slopes - 0 values float[] rentedArray = ArrayPool.Shared.Rent(dense.SampleArray.Count); dense.SampleArray.CopyTo(rentedArray); - ReadOnlySpan curveValues = new ReadOnlySpan(rentedArray, 0, dense.SampleArray.Count); + ReadOnlySpan curveValues = new(rentedArray, 0, dense.SampleArray.Count); - ReadOnlySpan slopeValues = [0, 0, 0, 0]; // no slopes - 0 values - - int streamCount = clip.StreamedClip.CurveCount(); for (int frameIndex = 0; frameIndex < dense.FrameCount; frameIndex++) { - float time = frameIndex / dense.SampleRate; + float time = frameIndex / dense.SampleRate + dense.BeginTime; int frameOffset = frameIndex * (int)dense.CurveCount; for (int curveIndex = 0; curveIndex < dense.CurveCount;) { - int index = streamCount + curveIndex; - IGenericBinding binding = bindings.FindBinding(index); + int index = preDenseCurves + curveIndex; + IGenericBinding binding = GetBinding(index); string path = GetCurvePath(binding.Path); int framePosition = frameOffset + curveIndex; if (binding.IsTransform()) @@ -172,7 +182,7 @@ namespace AssetRipper.Processing.AnimationClips } else { - AddCustomCurve(bindings, binding, path, time, dense.SampleArray[framePosition]); + AddCustomCurve(binding, path, time, dense.SampleArray[framePosition]); curveIndex++; } } @@ -180,25 +190,22 @@ namespace AssetRipper.Processing.AnimationClips ArrayPool.Shared.Return(rentedArray); } - private void ProcessConstant(IClip clip, IConstantClip constant, IAnimationClipBindingConstant bindings, float lastFrame) + private void ProcessConstant(IConstantClip constant, int preConstantCurves, float lastFrame) { float[] rentedArray = ArrayPool.Shared.Rent(constant.Data.Count); constant.Data.CopyTo(rentedArray); - ReadOnlySpan curveValues = new ReadOnlySpan(rentedArray, 0, constant.Data.Count); + ReadOnlySpan curveValues = new(rentedArray, 0, constant.Data.Count); ReadOnlySpan slopeValues = [0, 0, 0, 0]; // no slopes - 0 values - int streamCount = clip.StreamedClip.CurveCount(); - int denseCount = (int)clip.DenseClip.CurveCount; - - // only first and last frames - float time = 0.0f; - for (int i = 0; i < 2; i++, time += lastFrame) + float time = 0f; + int Is1or2Frames = time == lastFrame ? 1 : 2; // a constant curve can be made with 1 or 2 frames + for (int i = 0; i < Is1or2Frames; i++, time += lastFrame) { for (int curveIndex = 0; curveIndex < constant.Data.Count;) { - int index = streamCount + denseCount + curveIndex; - IGenericBinding binding = bindings.FindBinding(index); + int index = preConstantCurves + curveIndex; + IGenericBinding binding = GetBinding(index); string path = GetCurvePath(binding.Path); if (binding.IsTransform()) { @@ -212,7 +219,7 @@ namespace AssetRipper.Processing.AnimationClips } else { - AddCustomCurve(bindings, binding, path, time, constant.Data[curveIndex]); + AddCustomCurve(binding, path, time, constant.Data[curveIndex]); curveIndex++; } } @@ -220,35 +227,24 @@ namespace AssetRipper.Processing.AnimationClips ArrayPool.Shared.Return(rentedArray); } - private void AddCustomCurve(IAnimationClipBindingConstant bindings, IGenericBinding binding, string path, float time, float value) + private void AddCustomCurve(IGenericBinding binding, string path, float time, float value, float inTangent = 0, float outTangent = 0) { - bool ProcessStreams_frameIndex0 = time != FrameIndex0Time; switch ((BindingCustomType)binding.CustomType) { case BindingCustomType.AnimatorMuscle: - if (ProcessStreams_frameIndex0) - { - AddAnimatorMuscleCurve(binding, time, value); - } - + AddAnimatorMuscleCurve(binding, time, value, inTangent, outTangent); break; default: string attribute = m_customCurveResolver.ToAttributeName((BindingCustomType)binding.CustomType, binding.Attribute, path); + CurveData curve = new(path, attribute, binding.GetClassID(), binding.Script.TryGetAsset(m_clip.Collection)); if (binding.IsPPtrCurve()) { - if (!ProcessStreams_frameIndex0) - { - time = 0.0f; - } - - CurveData curve = new CurveData(path, attribute, binding.GetClassID(), binding.Script.TryGetAsset(m_clip.Collection)); - AddPPtrKeyframe(curve, bindings, time, (int)value); + AddPPtrKeyframe(curve, time, (int)value); } - else if (ProcessStreams_frameIndex0) + else { - CurveData curve = new CurveData(path, attribute, binding.GetClassID(), binding.Script.TryGetAsset(m_clip.Collection)); - AddFloatKeyframe(curve, time, value); + AddFloatKeyframe(curve, time, value, inTangent, outTangent); } break; } @@ -411,24 +407,20 @@ namespace AssetRipper.Processing.AnimationClips } } - private void AddDefaultCurve(IGenericBinding binding, string path, float time, float value) + private void AddDefaultCurve(IGenericBinding binding, string path, float time, float value, float inTangent = 0, float outTangent = 0) { switch (binding.GetClassID()) { case ClassIDType.GameObject: - { - AddGameObjectCurve(binding, path, time, value); - } + AddGameObjectCurve(binding, path, time, value); break; case ClassIDType.MonoBehaviour: - { - AddScriptCurve(binding, path, time, value); - } + AddScriptCurve(binding, path, time, value, inTangent, outTangent); break; default: - AddEngineCurve(binding, path, time, value); + AddEngineCurve(binding, path, time, value, inTangent, outTangent); break; } } @@ -437,19 +429,18 @@ namespace AssetRipper.Processing.AnimationClips { if (GameObject.TryGetPath(binding.Attribute, out string? propertyName)) { - CurveData curve = new CurveData(path, propertyName, ClassIDType.GameObject); - AddFloatKeyframe(curve, time, value); - return; + CurveData curve = new(path, propertyName, ClassIDType.GameObject); + AddFloatKeyframe(curve, time, value, 0, 0); } else { // that means that dev exported animation clip with missing component - CurveData curve = new CurveData(path, GetReversedPath(MissedPropertyPrefix, binding.Attribute), ClassIDType.GameObject); - AddFloatKeyframe(curve, time, value); + CurveData curve = new(path, GetReversedPath(MissedPropertyPrefix, binding.Attribute), ClassIDType.GameObject); + AddFloatKeyframe(curve, time, value, 0, 0); } } - private void AddScriptCurve(IGenericBinding binding, string path, float time, float value) + private void AddScriptCurve(IGenericBinding binding, string path, float time, float value, float inTangent, float outTangent) { if (binding.Script.TryGetAsset(m_clip.Collection) is IMonoScript script) { @@ -461,33 +452,45 @@ namespace AssetRipper.Processing.AnimationClips propertyName = GetReversedPath(ScriptPropertyPrefix, binding.Attribute); } - CurveData curve = new CurveData(path, propertyName, ClassIDType.MonoBehaviour, binding.Script.TryGetAsset(m_clip.Collection)); + CurveData curve = new(path, propertyName, ClassIDType.MonoBehaviour, binding.Script.TryGetAsset(m_clip.Collection)); - AddFloatKeyframe(curve, time, value); - } - - private void AddEngineCurve(IGenericBinding binding, string path, float time, float value) - { - if (!FieldHashes.TryGetPath(binding.GetClassID(), binding.Attribute, out string? propertyName)) + if (binding.IsPPtrCurve()) { - CurveData curve = new(path, GetReversedPath(TypeTreePropertyPrefix, binding.Attribute), binding.GetClassID()); - AddFloatKeyframe(curve, time, value); + AddPPtrKeyframe(curve, time, (int)value); } else { - CurveData curve = new(path, propertyName, binding.GetClassID()); - AddFloatKeyframe(curve, time, value); + AddFloatKeyframe(curve, time, value, inTangent, outTangent); } } - private void AddAnimatorMuscleCurve(IGenericBinding binding, float time, float value) + private void AddEngineCurve(IGenericBinding binding, string path, float time, float value, float inTangent, float outTangent) { - string attributeString = HumanoidMuscleTypeExtensions.ToAttributeString(binding.GetHumanoidMuscle(Version)); - CurveData curve = new CurveData(string.Empty, attributeString, ClassIDType.Animator); - AddFloatKeyframe(curve, time, value); + if (!FieldHashes.TryGetPath(binding.GetClassID(), binding.Attribute, out string? propertyName)) + { + propertyName = GetReversedPath(TypeTreePropertyPrefix, binding.Attribute); + } + + CurveData curve = new(path, propertyName, binding.GetClassID()); + + if (binding.IsPPtrCurve()) + { + AddPPtrKeyframe(curve, time, (int)value); + } + else + { + AddFloatKeyframe(curve, time, value, inTangent, outTangent); + } } - private void AddFloatKeyframe(in CurveData curveData, float time, float value) + private void AddAnimatorMuscleCurve(IGenericBinding binding, float time, float value, float inTangent, float outTangent) + { + string attributeString = HumanoidMuscleTypeExtensions.ToAttributeString(binding.GetHumanoidMuscle(Version)); + CurveData curve = new(string.Empty, attributeString, ClassIDType.Animator); + AddFloatKeyframe(curve, time, value, inTangent, outTangent); + } + + private void AddFloatKeyframe(in CurveData curveData, float time, float value, float inTangent, float outTangent) { if (!m_floats.TryGetValue(curveData, out IFloatCurve? curve)) { @@ -502,10 +505,10 @@ namespace AssetRipper.Processing.AnimationClips } IKeyframe_Single floatKey = curve.Curve.Curve.AddNew(); - floatKey.SetValues(Version, time, value, DefaultFloatWeight); + floatKey.SetValues(Version, time, value, inTangent, outTangent, DefaultFloatWeight); } - private void AddPPtrKeyframe(in CurveData curveData, IAnimationClipBindingConstant bindings, float time, int index) + private void AddPPtrKeyframe(in CurveData curveData, float time, int index) { if (!m_pptrs.TryGetValue(curveData, out IPPtrCurve? curve)) { @@ -524,41 +527,62 @@ namespace AssetRipper.Processing.AnimationClips m_pptrs.Add(curveData, curve); } - IPPtr_Object value = bindings.PptrCurveMapping[index]; + IPPtr_Object value = ClipBindingConstant.PptrCurveMapping[index]; IPPtrKeyframe key = curve.Curve.AddNew(); key.Time = time; key.Value.CopyValues(value, new PPtrConverter(m_clip)); } - private static void GetPreviousFrame(IReadOnlyList streamFrames, int curveID, int currentFrame, out int frameIndex, out int curveIndex) + private IGenericBinding GetBinding(int index) { - for (frameIndex = currentFrame - 1; frameIndex >= 0; frameIndex--) + if (m_bindingsCache.TryGetValue(index, out IGenericBinding? binding)) { - StreamedFrame frame = streamFrames[frameIndex]; - for (curveIndex = 0; curveIndex < frame.Curves.Length; curveIndex++) + return binding; + } + int curves = 0; + AccessListBase bindings = ClipBindingConstant.GenericBindings; + for (int i = 0; i < bindings.Count; i++) + { + binding = bindings[i]; + if (binding.GetClassID() == ClassIDType.Transform) { - StreamedCurveKey curve = frame.Curves[curveIndex]; + curves += binding.TransformType().GetDimension(); + } + else + { + curves += 1; + } + if (curves > index) + { + m_bindingsCache[index] = binding; + if (binding.IsTransform() && binding.TransformType().GetDimension() < 1) + { + // If an animation was malformed, this avoids the possibility of an infinite FOR loop when processing Transform bindings + throw new IndexOutOfRangeException("Transform AnimationCurve can't have Dimension less than 1."); + } + return binding; + } + } + throw new ArgumentException($"Binding with index {index} hasn't been found", nameof(index)); + } + + private static bool TryGetNextFrame(IReadOnlyList streamedFrames, int currentFrame, int curveID, [MaybeNullWhen(false)] out StreamedFrame nextFrame, out int curveIdx) + { + for (int frameIndex = currentFrame + 1; frameIndex < streamedFrames.Count; frameIndex++) + { + nextFrame = streamedFrames[frameIndex]; + for (curveIdx = 0; curveIdx < nextFrame.Curves.Length; curveIdx++) + { + StreamedCurveKey curve = nextFrame.Curves[curveIdx]; if (curve.Index == curveID) { - return; + return true; } } } - throw new Exception($"There is no curve with index {curveID} in any of previous frames"); - } - - private static int GetNextCurve(StreamedFrame frame, int currentCurve) - { - StreamedCurveKey curve = frame.Curves[currentCurve]; - int i = currentCurve + 1; - for (; i < frame.Curves.Length; i++) - { - if (frame.Curves[i].Index != curve.Index) - { - return i; - } - } - return i; + nextFrame = null; + curveIdx = -1; + return false; } private string GetCurvePath(uint hash) @@ -580,7 +604,7 @@ namespace AssetRipper.Processing.AnimationClips AssetCollection collection = m_clip.Collection; CopyDataToBuffer(clip, collection, buffer); - EndianSpanReader reader = new EndianSpanReader(buffer, collection.EndianType); + EndianSpanReader reader = new(buffer, collection.EndianType); while (reader.Position < reader.Length) { StreamedFrame frame = new(); @@ -619,6 +643,34 @@ namespace AssetRipper.Processing.AnimationClips } } + private float CalculateLastConstantTime(IReadOnlyList streamedFrames, float stopTime) + { + if (stopTime == 0f || streamedFrames.Count <= 1) // streamedFrames[streamedFrames.Count-1] has dummy Infinity Time + { + return stopTime; + } + float sampleRate = m_clip.SampleRate_C74; + // using frame indexes for precise calculation + int lastFrame = (int)float.Round(stopTime * sampleRate); + StreamedFrame lastStreamedFrame = streamedFrames[streamedFrames.Count - 2]; + // careful of streamedFrames[0], has Time=float.MinValue + int lastSFFrame = lastStreamedFrame.Time > 0 ? (int)float.Round(lastStreamedFrame.Time * sampleRate) : 0; + if (lastFrame - lastSFFrame == 1) + { + // check if last StreamedFrame has a PPtrCurve, because it adds 1 extra frame to MuscleClip.StopTime + foreach (StreamedCurveKey curve in lastStreamedFrame.Curves) + { + IGenericBinding binding = GetBinding(curve.Index); + if (binding.IsPPtrCurve()) + { + // careful of streamedFrames[0], has Time=float.MinValue + return lastStreamedFrame.Time > 0 ? lastStreamedFrame.Time : 0f; + } + } + } + return stopTime; + } + private static string GetReversedPath([ConstantExpected] string prefix, uint hash) { return Crc32Algorithm.ReverseAscii(hash, $"{prefix}0x{hash:X}_"); @@ -631,11 +683,6 @@ namespace AssetRipper.Processing.AnimationClips private const string ScriptPropertyPrefix = "script_"; private const string TypeTreePropertyPrefix = "typetree_"; - /// - /// Used to detect when a StreamedFrame is from index 0. - /// - private const float FrameIndex0Time = float.MinValue; - /// /// The default weight for a keyframe. /// @@ -655,5 +702,7 @@ namespace AssetRipper.Processing.AnimationClips private readonly PathChecksumCache m_checksumCache; private readonly IAnimationClip m_clip; private readonly CustomCurveResolver m_customCurveResolver; + private readonly Dictionary m_bindingsCache = new(); //cache results from GetBinding(curveID) + private IAnimationClipBindingConstant ClipBindingConstant => m_clip.ClipBindingConstant_C74!; //This class only supports 4.3 and newer. } } diff --git a/Source/AssetRipper.Processing/AnimationClips/CustomCurveResolver.cs b/Source/AssetRipper.Processing/AnimationClips/CustomCurveResolver.cs index 4da3f4961..6a630693a 100644 --- a/Source/AssetRipper.Processing/AnimationClips/CustomCurveResolver.cs +++ b/Source/AssetRipper.Processing/AnimationClips/CustomCurveResolver.cs @@ -21,7 +21,7 @@ using System.Text.RegularExpressions; namespace AssetRipper.Processing.AnimationClips { - public sealed partial class CustomCurveResolver + public partial struct CustomCurveResolver { public CustomCurveResolver(IAnimationClip clip) { diff --git a/Source/AssetRipper.Processing/AnimationClips/Editor/StreamedCurveKey.cs b/Source/AssetRipper.Processing/AnimationClips/Editor/StreamedCurveKey.cs index 55ff63b71..35c4418c5 100644 --- a/Source/AssetRipper.Processing/AnimationClips/Editor/StreamedCurveKey.cs +++ b/Source/AssetRipper.Processing/AnimationClips/Editor/StreamedCurveKey.cs @@ -1,6 +1,4 @@ using AssetRipper.IO.Endian; -using AssetRipper.SourceGenerated.Subclasses.AnimationCurve_Single; -using AssetRipper.SourceGenerated.Subclasses.Keyframe_Single; using System.Numerics; namespace AssetRipper.Processing.AnimationClips.Editor @@ -12,96 +10,98 @@ namespace AssetRipper.Processing.AnimationClips.Editor { Index = index; Coefficient = coefficient; - Value = value; - } - - public static StreamedCurveKey CalculateStreamedFrame(IAnimationCurve_Single curve, int lhsIndex, int rhsIndex, float timeOffset) - { - IReadOnlyList keyframes = curve.Curve; - IKeyframe_Single lhs = keyframes[lhsIndex]; - int curveKeyIndex = lhsIndex; - IKeyframe_Single rhs = keyframes[rhsIndex]; - float frameTime = lhs.Time + timeOffset; - //TimeEnd = rhs.Time + timeOffset; - float deltaTime = rhs.Time - lhs.Time; - if (deltaTime < 0.00009999999747378752) - { - deltaTime = 0.000099999997f; - } - float deltaValue = rhs.Value - lhs.Value; - float inverseTime = 1.0f / (deltaTime * deltaTime); - float outTangent = lhs.OutSlope * deltaTime; - float inTangent = rhs.InSlope * deltaTime; - float curveKeyCoefX = (inTangent + outTangent - deltaValue - deltaValue) * inverseTime / deltaTime; - float curveKeyCoefY = inverseTime * (deltaValue + deltaValue + deltaValue - outTangent - outTangent - inTangent); - float curveKeyCoefZ = lhs.OutSlope; - float curveKeyValue = lhs.Value; - if (lhs.OutSlope == float.PositiveInfinity || rhs.InSlope == float.PositiveInfinity) - { - curveKeyCoefX = 0.0f; - curveKeyCoefY = 0.0f; - curveKeyCoefZ = 0.0f; - curveKeyValue = lhs.Value; - } - Vector3 curveKeyCoef = new Vector3(curveKeyCoefX, curveKeyCoefY, curveKeyCoefZ); - StreamedCurveKey curveKey = new StreamedCurveKey(curveKeyIndex, curveKeyCoef, curveKeyValue); - return curveKey; + RightSidedLimit = LeftSidedLimit = value; } public void Read(ref EndianSpanReader reader) { Index = reader.ReadInt32(); - Coefficient = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Value = reader.ReadSingle(); + Coefficient = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + RightSidedLimit = reader.ReadSingle(); + LeftSidedLimit = RightSidedLimit; + } + + public void CalculateSlopes(float time, float nextTime, StreamedCurveKey nextKey) + { + if (Coefficient != default) // != (0,0,0) => regular slopes + { + OutSlope = Coefficient.Z; + float deltaX = nextTime - time; + float _CoeffX = Coefficient.X * deltaX * deltaX; + float _CoeffY = Coefficient.Y * deltaX; + nextKey.InSlope = 3 * _CoeffX + 2 * _CoeffY + Coefficient.Z; // may need some kind of rounding if wanting to get an inSlope of exactly 0 + //nextKey.inSlope = float.Round(nextKey.inSlope, 4); // amount of decimal places may be more dynamic + if (nextKey.Coefficient == default) // calculate LSL only if next slope calculation needs it + { + nextKey.LeftSidedLimit = (_CoeffX + _CoeffY + Coefficient.Z) * deltaX + RightSidedLimit; + } + return; + } + if (RightSidedLimit != LeftSidedLimit) + { + // check percentage difference + float diff = RightSidedLimit > LeftSidedLimit ? RightSidedLimit - LeftSidedLimit : LeftSidedLimit - RightSidedLimit; + float AbsVal = RightSidedLimit < 0 ? -RightSidedLimit : RightSidedLimit; + float AbsLSV = LeftSidedLimit < 0 ? -LeftSidedLimit : LeftSidedLimit; + float div = AbsVal > AbsLSV ? AbsVal : AbsLSV; + const float ROUNDING_ERROR = 1e-5f; // arbitrary small value + // normally the difference between Right and Left Sided Limits should be "big"/much greater than a rounding error. + // if the check fails and INCORRECTLY skips this IF block, the next statements + // will still produce a good approximation for the expected curve + if (diff/div > ROUNDING_ERROR) + { + OutSlope = float.NegativeInfinity; + nextKey.InSlope = 0f; + nextKey.LeftSidedLimit = RightSidedLimit; + RightSidedLimit = LeftSidedLimit; + return; + } + } + if (RightSidedLimit == nextKey.RightSidedLimit) + { + OutSlope = 0f; + nextKey.InSlope = 0f; + nextKey.LeftSidedLimit = RightSidedLimit; + return; + } + OutSlope = float.PositiveInfinity; + nextKey.InSlope = 0f; + // don't do nextKey.LeftSidedLimit=RightSidedLimit here, because having 2 consecutive keys + // with outSlope +Inf and -Inf is illegal (Editor corrects it) } /// - /// Calculate value between two KeyframeTpl Float at given time + /// Index for its GenericBinding inside AnimationClip's BindingConstant. + /// + public int Index { get; private set; } + /// + /// Coefficients of the Cubic Bezier Equation between this Key and the next, for the same Curve. + /// + public Vector3 Coefficient { get; private set; } + /// + /// Value of the binded property during this keyframe. + /// + /// /// + /// This value could change after Slope calculation. + /// + public float Value { get => RightSidedLimit; private set => RightSidedLimit = value; } + /// + /// Value of the key, when approached from its right side. /// /// - /// This calculates a unit interval cubic Hermite spline.
- ///
- /// + /// outSlope=-Infinity creates a discontinuity on the curve, + /// making the CurveKey value differ from its left side. ///
- /// (time - leftTime) / (rightTime - leftTime) - /// lhs.Value - /// lhs.OutSlope * (rightTime - leftTime) - /// rhs.Value - /// rhs.OutSlope * (rightTime - leftTime) - /// Value between two keyframes - public static float HermiteInterpolate(float deltaTimeFraction, float leftValue, float leftTangent, float rightValue, float rightTangent) - { - float tt = deltaTimeFraction * deltaTimeFraction; - float ttt = tt * deltaTimeFraction; - float tttx2 = ttt * 2.0f; - float ttx3 = tt * 3.0f; - float v1 = (deltaTimeFraction + ttt - 2.0f * tt) * leftTangent + (tttx2 - ttx3 + 1.0f) * leftValue; - float v2 = (ttt - tt) * rightTangent; - float v3 = ttx3 - tttx2; - return v1 + v2 + v3 * rightValue; - } - - public float CalculateNextInSlope(float deltaTime, float nextValue) - { - if (deltaTime >= 3.40282347e38f) - { - return 0; - } - if (deltaTime < 0.00009999999747378752) - { - deltaTime = 0.000099999997f; - } - float deltaValue = nextValue - Value; - float inverseTime = 1.0f / (deltaTime * deltaTime); - float outTangent = OutSlope * deltaTime; - float inTangent = deltaValue + deltaValue + deltaValue - outTangent - outTangent - Coefficient.Y / inverseTime; - return inTangent / deltaTime; - } - - public float OutSlope => Coefficient.Z; - - public int Index { get; set; } - public Vector3 Coefficient { get; set; } - public float Value { get; set; } + private float RightSidedLimit { get; set; } + /// + /// Value of the key, when approached from its left side + /// + /// + /// outSlope=-Infinity creates a discontinuity on the curve, + /// making the CurveKey value differ from its right side. + /// + private float LeftSidedLimit { get; set; } + public float InSlope { get; private set; } + public float OutSlope { get; private set; } } } diff --git a/Source/AssetRipper.SourceGenerated.Extensions/AnimationClipBindingConstantExtensions.cs b/Source/AssetRipper.SourceGenerated.Extensions/AnimationClipBindingConstantExtensions.cs deleted file mode 100644 index 917382ff9..000000000 --- a/Source/AssetRipper.SourceGenerated.Extensions/AnimationClipBindingConstantExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using AssetRipper.Assets.Generics; -using AssetRipper.SourceGenerated.Extensions.Enums.AnimationClip.GenericBinding; -using AssetRipper.SourceGenerated.Subclasses.AnimationClipBindingConstant; -using AssetRipper.SourceGenerated.Subclasses.GenericBinding; - -namespace AssetRipper.SourceGenerated.Extensions -{ - public static class AnimationClipBindingConstantExtensions - { - public static IGenericBinding FindBinding(this IAnimationClipBindingConstant constant, int index) - { - int curves = 0; - AccessListBase bindings = constant.GenericBindings; - - for (int i = 0; i < bindings.Count; i++) - { - IGenericBinding gb = bindings[i]; - if (gb.GetClassID() == ClassIDType.Transform) - { - curves += gb.TransformType().GetDimension(); - } - else - { - curves += 1; - } - - if (curves > index) - { - return gb; - } - } - throw new ArgumentException($"Binding with index {index} hasn't been found", nameof(index)); - } - } -}