AnimationClip Recovery Improvements (#1210)

* - improved Slope/Tangent calculation
- recover Slopes for all Float curves
- tweaked Dense and Constant curves generation
- fixed erroneously generating Float curves out of PPtr curves, on MonoBehaviours and some Engine components

* remove duplicated bindings cache

* don't get stuck on broken Transform animation, and minor tweaks

* implementing change requests

* implementing new change requests

* edit comment
This commit is contained in:
FACS01-01 2024-03-01 20:36:52 -03:00 committed by GitHub
parent be4e38218e
commit c3586ab772
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 280 additions and 266 deletions

View File

@ -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<StreamedFrame> 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<StreamedFrame> streamFrames, IAnimationClipBindingConstant bindings, float sampleRate)
private void ProcessStreams(IReadOnlyList<StreamedFrame> streamedFrames)
{
Span<float> curveValues = [0, 0, 0, 0];
Span<float> inSlopeValues = [0, 0, 0, 0];
Span<float> 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<float> slopeValues = [0, 0, 0, 0]; // no slopes - 0 values
float[] rentedArray = ArrayPool<float>.Shared.Rent(dense.SampleArray.Count);
dense.SampleArray.CopyTo(rentedArray);
ReadOnlySpan<float> curveValues = new ReadOnlySpan<float>(rentedArray, 0, dense.SampleArray.Count);
ReadOnlySpan<float> curveValues = new(rentedArray, 0, dense.SampleArray.Count);
ReadOnlySpan<float> 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<float>.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<float>.Shared.Rent(constant.Data.Count);
constant.Data.CopyTo(rentedArray);
ReadOnlySpan<float> curveValues = new ReadOnlySpan<float>(rentedArray, 0, constant.Data.Count);
ReadOnlySpan<float> curveValues = new(rentedArray, 0, constant.Data.Count);
ReadOnlySpan<float> 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<float>.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<StreamedFrame> 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<IGenericBinding> 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<StreamedFrame> 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<StreamedFrame> 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_";
/// <summary>
/// Used to detect when a StreamedFrame is from index 0.
/// </summary>
private const float FrameIndex0Time = float.MinValue;
/// <summary>
/// The default weight for a keyframe.
/// </summary>
@ -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<int, IGenericBinding> m_bindingsCache = new(); //cache results from GetBinding(curveID)
private IAnimationClipBindingConstant ClipBindingConstant => m_clip.ClipBindingConstant_C74!; //This class only supports 4.3 and newer.
}
}

View File

@ -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)
{

View File

@ -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<IKeyframe_Single> 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)
}
/// <summary>
/// Calculate value between two KeyframeTpl Float at given time
/// Index for its GenericBinding inside AnimationClip's BindingConstant.
/// </summary>
public int Index { get; private set; }
/// <summary>
/// Coefficients of the Cubic Bezier Equation between this Key and the next, for the same Curve.
/// </summary>
public Vector3 Coefficient { get; private set; }
/// <summary>
/// Value of the binded property during this keyframe.
/// </summary>
/// /// <remarks>
/// This value could change after Slope calculation.
/// </remarks>
public float Value { get => RightSidedLimit; private set => RightSidedLimit = value; }
/// <summary>
/// Value of the key, when approached from its right side.
/// </summary>
/// <remarks>
/// This calculates a unit interval cubic Hermite spline.<br />
/// <see href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline"/><br />
/// <see href="https://en.wikipedia.org/wiki/Hermite_interpolation"/>
/// outSlope=-Infinity creates a discontinuity on the curve,
/// making the CurveKey value differ from its left side.
/// </remarks>
/// <param name="deltaTimeFraction">(time - leftTime) / (rightTime - leftTime)</param>
/// <param name="leftValue">lhs.Value</param>
/// <param name="leftTangent">lhs.OutSlope * (rightTime - leftTime)</param>
/// <param name="rightValue">rhs.Value</param>
/// <param name="rightTangent">rhs.OutSlope * (rightTime - leftTime)</param>
/// <returns>Value between two keyframes</returns>
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; }
/// <summary>
/// Value of the key, when approached from its left side
/// </summary>
/// <remarks>
/// outSlope=-Infinity creates a discontinuity on the curve,
/// making the CurveKey value differ from its right side.
/// </remarks>
private float LeftSidedLimit { get; set; }
public float InSlope { get; private set; }
public float OutSlope { get; private set; }
}
}

View File

@ -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<IGenericBinding> 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));
}
}
}