diff --git a/.gitignore b/.gitignore index 09bd2548..3faa1a0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ *.DS_Store # Project for Asset Store -src/LitMotion.AssetStore/* \ No newline at end of file +src/LitMotion.AssetStore/* diff --git a/samples/LitMotion.Samples/Assets/Samples/0. Basic/7. Coroutine/Sample_0_Coroutine.cs b/samples/LitMotion.Samples/Assets/Samples/0. Basic/7. Coroutine/Sample_0_Coroutine.cs index d0d7c077..287b89d3 100644 --- a/samples/LitMotion.Samples/Assets/Samples/0. Basic/7. Coroutine/Sample_0_Coroutine.cs +++ b/samples/LitMotion.Samples/Assets/Samples/0. Basic/7. Coroutine/Sample_0_Coroutine.cs @@ -16,8 +16,7 @@ IEnumerator Start() var direction = i % 2 == 0 ? 1 : -1; yield return LMotion.Create(-5f * direction, 5f * direction, 2f) .WithEase(Ease.InOutSine) - .BindToPositionX(targets[i]) - .ToYieldInteraction(); + .BindToPositionX(targets[i]); } } } diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Adapters/SpringMotionAdapters.cs b/src/LitMotion/Assets/LitMotion/Runtime/Adapters/SpringMotionAdapters.cs new file mode 100644 index 00000000..d0f9a7df --- /dev/null +++ b/src/LitMotion/Assets/LitMotion/Runtime/Adapters/SpringMotionAdapters.cs @@ -0,0 +1,126 @@ +using Unity.Jobs; +using UnityEngine; +using Unity.Mathematics; +using LitMotion; +using LitMotion.Adapters; + +[assembly: RegisterGenericJobType(typeof(MotionUpdateJob))] +[assembly: RegisterGenericJobType(typeof(MotionUpdateJob))] +[assembly: RegisterGenericJobType(typeof(MotionUpdateJob))] +[assembly: RegisterGenericJobType(typeof(MotionUpdateJob))] + +namespace LitMotion.Adapters +{ + public readonly struct FloatSpringMotionAdapter : IMotionAdapter + { + public float Evaluate(ref float startValue, ref float endValue, ref SpringOptions options, in MotionEvaluationContext context) + { + options.TargetValue.x = endValue; + SpringUtility.SpringElastic( + (float)context.DeltaTime, + ref options.CurrentValue.x, + ref options.CurrentVelocity.x, + options.TargetValue.x, + options.TargetVelocity.x, + options.DampingRatio, + options.Stiffness + ); + return options.CurrentValue.x; + } + + public bool IsCompleted(ref float startValue, ref float endValue, ref SpringOptions options) + { + return SpringUtility.Approximately(options.CurrentValue.x, options.TargetValue.x); + } + + public bool IsDurationBased => false; + } + + /// + /// Spring motion adapter for Vector2 using float4 SpringElastic method. + /// + public readonly struct Vector2SpringMotionAdapter : IMotionAdapter + { + public Vector2 Evaluate(ref Vector2 startValue, ref Vector2 endValue, ref SpringOptions options, in MotionEvaluationContext context) + { + float deltaTime = (float)context.DeltaTime; + options.TargetValue.xy = endValue; + SpringUtility.SpringElastic( + deltaTime, + ref options.CurrentValue, + ref options.CurrentVelocity, + options.TargetValue, + options.TargetVelocity, + options.DampingRatio, + options.Stiffness + ); + return options.CurrentValue.xy; + } + + public bool IsCompleted(ref Vector2 startValue, ref Vector2 endValue, ref SpringOptions options) + { + return SpringUtility.Approximately(options.CurrentValue, options.TargetValue); + } + + public bool IsDurationBased => false; + } + + /// + /// Spring motion adapter for Vector3 using float4 SpringElastic method. + /// + public readonly struct Vector3SpringMotionAdapter : IMotionAdapter + { + public Vector3 Evaluate(ref Vector3 startValue, ref Vector3 endValue, ref SpringOptions options, in MotionEvaluationContext context) + { + float deltaTime = (float)context.DeltaTime; + options.TargetValue.xyz = endValue; + SpringUtility.SpringElastic( + deltaTime, + ref options.CurrentValue, + ref options.CurrentVelocity, + options.TargetValue, + options.TargetVelocity, + options.DampingRatio, + options.Stiffness + ); + return options.CurrentValue.xyz; + } + + public bool IsCompleted(ref Vector3 startValue, ref Vector3 endValue, ref SpringOptions options) + { + return SpringUtility.Approximately(options.CurrentValue, options.TargetValue); + } + + public bool IsDurationBased => false; + } + + /// + /// Spring motion adapter for Vector4 using float4 SpringElastic method. + /// + public readonly struct Vector4SpringMotionAdapter : IMotionAdapter + { + public Vector4 Evaluate(ref Vector4 startValue, ref Vector4 endValue, ref SpringOptions options, in MotionEvaluationContext context) + { + float deltaTime = (float)context.DeltaTime; + options.TargetValue = endValue; + SpringUtility.SpringElastic( + deltaTime, + ref options.CurrentValue, + ref options.CurrentVelocity, + options.TargetValue, + options.TargetVelocity, + options.DampingRatio, + options.Stiffness + ); + + return options.CurrentValue; + } + + public bool IsCompleted(ref Vector4 startValue, ref Vector4 endValue, ref SpringOptions options) + { + return SpringUtility.Approximately(options.CurrentValue, options.TargetValue); + } + + public bool IsDurationBased => false; + } +} diff --git a/src/LitMotion/Assets/LitMotion/Runtime/IMotionAdapter.cs b/src/LitMotion/Assets/LitMotion/Runtime/IMotionAdapter.cs index f3313e3d..83df8a5b 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/IMotionAdapter.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/IMotionAdapter.cs @@ -18,5 +18,12 @@ public interface IMotionAdapter /// Animation context /// Current value TValue Evaluate(ref TValue startValue, ref TValue endValue, ref TOptions options, in MotionEvaluationContext context); + + bool IsCompleted(ref TValue startValue, ref TValue endValue, ref TOptions options) + { + return false; + } + + bool IsDurationBased => true; } } \ No newline at end of file diff --git a/src/LitMotion/Assets/LitMotion/Runtime/IMotionOptions.cs b/src/LitMotion/Assets/LitMotion/Runtime/IMotionOptions.cs index d1ddaa7f..7e498e6d 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/IMotionOptions.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/IMotionOptions.cs @@ -3,5 +3,8 @@ namespace LitMotion /// /// Implement this interface to define special options that can be applied to motion. /// - public interface IMotionOptions { } + public interface IMotionOptions + { + void Restart() { } + } } \ No newline at end of file diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs index 54405a7a..7764ca0d 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs @@ -54,10 +54,11 @@ public readonly double TotalDuration public MotionState State; public MotionParameters Parameters; + public double CurrentLoopStartTime; public readonly double TimeSinceStart => State.Time - Parameters.Delay; - public void Update(double time, out float progress) + public void UpdateDurationBasedState(double time, out float progress) { State.PrevCompletedLoops = State.CompletedLoops; State.PrevStatus = State.Status; @@ -172,6 +173,41 @@ public void Update(double time, out float progress) } } + public void UpdateIterationState(double time, double deltaTime,bool isOnceCompleted) + { + State.PrevCompletedLoops = State.CompletedLoops; + State.PrevStatus = State.Status; + State.Time = time; + bool isDelayed; + if(time == 0 || (isOnceCompleted && State.CompletedLoops <= Parameters.Loops - 1)) + { + CurrentLoopStartTime = time; + } + if (Parameters.DelayType == DelayType.FirstLoop) + isDelayed = TimeSinceStart < 0f; + else + { + isDelayed = State.Time - CurrentLoopStartTime - Parameters.Delay < 0f; + } + + if (isOnceCompleted) + { + State.CompletedLoops++; + } + if (isOnceCompleted && Parameters.Loops >= 0 && State.CompletedLoops > Parameters.Loops - 1) + { + State.Status = MotionStatus.Completed; + } + else if (isDelayed || State.Time < 0) + { + State.Status = MotionStatus.Delayed; + } + else + { + State.Status = MotionStatus.Playing; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Complete(out float progress) { @@ -224,34 +260,99 @@ internal struct MotionData public TValue EndValue; public TOptions Options; - public void Update(double time, out TValue result) + public void Update(double time, double deltaTime, out TValue result) where TAdapter : unmanaged, IMotionAdapter { - Core.Update(time, out var progress); - - result = default(TAdapter).Evaluate(ref StartValue, ref EndValue, ref Options, new MotionEvaluationContext() + bool isDurationBased = default(TAdapter).IsDurationBased; + if (isDurationBased) { - Progress = progress, - Time = time, - }); + Core.UpdateDurationBasedState(time, out var progress); + result = default(TAdapter).Evaluate(ref StartValue, ref EndValue, ref Options, new MotionEvaluationContext() + { + Progress = progress, + Time = time, + DeltaTime = deltaTime, + }); + } + else + { + bool isOnceCompleted = false; + if (time <= 0 || Core.State.Status == MotionStatus.Scheduled) + Core.UpdateIterationState(time, deltaTime, false); + + if (Core.State.Status == MotionStatus.Delayed) + result = StartValue; + else if (Core.State.Status == MotionStatus.Completed) + result = EndValue; + else + { + if (Core.Parameters.LoopType == LoopType.Restart) + { + result = default(TAdapter).Evaluate(ref StartValue, ref EndValue, ref Options, new MotionEvaluationContext() + { + Time = time, + DeltaTime = deltaTime, + }); + isOnceCompleted = default(TAdapter).IsCompleted(ref StartValue, ref EndValue, ref Options); + if (isOnceCompleted && Core.State.Status == MotionStatus.Playing) + Options.Restart(); + } + else if (Core.Parameters.LoopType == LoopType.Flip || Core.Parameters.LoopType == LoopType.Yoyo) + { + var isOdd = Core.State.CompletedLoops % 2 == 1; + result = isOdd + ? default(TAdapter).Evaluate(ref EndValue, ref StartValue, ref Options, new MotionEvaluationContext() + { + Time = time, + DeltaTime = deltaTime, + }) + : default(TAdapter).Evaluate(ref StartValue, ref EndValue, ref Options, new MotionEvaluationContext() + { + Time = time, + DeltaTime = deltaTime, + }); + isOnceCompleted = isOdd + ? default(TAdapter).IsCompleted(ref EndValue, ref StartValue, ref Options) + : default(TAdapter).IsCompleted(ref StartValue, ref EndValue, ref Options); + } + else + { + + result = default(TAdapter).Evaluate(ref StartValue, ref EndValue, ref Options, new MotionEvaluationContext() + { + Time = time, + DeltaTime = deltaTime, + }); + isOnceCompleted = default(TAdapter).IsCompleted(ref StartValue, ref EndValue, ref Options); + } + } + Core.UpdateIterationState(time, deltaTime, isOnceCompleted); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Complete(out TValue result) where TAdapter : unmanaged, IMotionAdapter { + bool isDurationBased = default(TAdapter).IsDurationBased; Core.Complete(out var progress); - - result = default(TAdapter).Evaluate( - ref StartValue, - ref EndValue, - ref Options, - new() - { - Progress = progress, - Time = Core.State.Time, - } - ); + if (isDurationBased) + { + result = default(TAdapter).Evaluate( + ref StartValue, + ref EndValue, + ref Options, + new() + { + Progress = progress, + Time = Core.State.Time, + } + ); + } + else + { + result = EndValue; + } } } } \ No newline at end of file diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionManager.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionManager.cs index f1dfe1b5..3e7eb0c3 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionManager.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionManager.cs @@ -25,6 +25,14 @@ public static ref MotionData GetDataRef(MotionHandle handle, bool checkIsInSeque CheckTypeId(handle); return ref list[handle.StorageId].GetDataRef(handle, checkIsInSequence); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref MotionData GetTypeDataRef(MotionHandle handle, bool checkIsInSequence = true) + where TValue : unmanaged + where TOptions : unmanaged, IMotionOptions + { + CheckTypeId(handle); + return ref list[handle.StorageId].GetTypeDataRef(handle, checkIsInSequence); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref ManagedMotionData GetManagedDataRef(MotionHandle handle, bool checkIsInSequence = true) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs index 1c926052..4884c585 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs @@ -28,6 +28,9 @@ internal interface IMotionStorage void Complete(MotionHandle handle, bool checkIsInSequence = true); void SetTime(MotionHandle handle, double time, bool checkIsInSequence = true); ref MotionData GetDataRef(MotionHandle handle, bool checkIsInSequence = true); + ref MotionData GetTypeDataRef(MotionHandle handle, bool checkIsInSequence = true) + where ValueType : unmanaged + where OptionsType : unmanaged, IMotionOptions; ref ManagedMotionData GetManagedDataRef(MotionHandle handle, bool checkIsInSequence = true); void AddToSequence(MotionHandle handle, out double motionDuration); MotionDebugInfo GetDebugInfo(MotionHandle handle); @@ -387,7 +390,7 @@ public unsafe void SetTime(MotionHandle handle, double time, bool checkIsInSeque if (checkIsInSequence && state.IsInSequence) Error.MotionIsInSequence(); - dataPtr->Update(time, out var result); + dataPtr->Update(time, time - state.Time, out var result); var status = state.Status; ref var managedData = ref managedDataArray[denseIndex]; @@ -454,6 +457,14 @@ public ref MotionData GetDataRef(MotionHandle handle, bool checkIsInSequence = t return ref UnsafeUtility.As, MotionData>(ref unmanagedDataArray[slot.DenseIndex]); } + public ref MotionData GetTypeDataRef(MotionHandle handle, bool checkIsInSequence = true) + where ValueType : unmanaged + where OptionsType : unmanaged, IMotionOptions + { + ref var slot = ref GetSlotWithVarify(handle, checkIsInSequence); + return ref UnsafeUtility.As, MotionData>(ref unmanagedDataArray[slot.DenseIndex]); + } + public MotionDebugInfo GetDebugInfo(MotionHandle handle) { ref var slot = ref GetSlotWithVarify(handle, false); diff --git a/src/LitMotion/Assets/LitMotion/Runtime/LMotion.Spring.cs b/src/LitMotion/Assets/LitMotion/Runtime/LMotion.Spring.cs new file mode 100644 index 00000000..082c85b1 --- /dev/null +++ b/src/LitMotion/Assets/LitMotion/Runtime/LMotion.Spring.cs @@ -0,0 +1,70 @@ +using UnityEngine; +using LitMotion.Adapters; +using Unity.Mathematics; +namespace LitMotion +{ + public static partial class LMotion + { + /// + /// API for creating Spring motions. + /// + public static class Spring + { + /// + /// Create a builder for building Spring motion. + /// + /// Start value + /// End value + /// Spring options + /// Created motion builder + public static MotionBuilder Create(float startValue, float endValue, SpringOptions options = default) + { + options.Init(new float4(startValue, 0.0f, 0.0f, 0.0f), new float4(endValue, 0.0f, 0.0f, 0.0f)); + return Create(startValue, endValue, 0.0f) + .WithOptions(options); + } + + /// + /// Create a builder for building Vector2 Spring motion. + /// + /// Start value + /// End value + /// Spring options + /// Created motion builder + public static MotionBuilder Create(Vector2 startValue, Vector2 endValue, SpringOptions options = default) + { + options.Init(new float4(startValue, 0.0f, 0.0f), new float4(endValue, 0.0f, 0.0f)); + return Create(startValue, endValue, 0.0f) + .WithOptions(options); + } + + /// + /// Create a builder for building Vector3 Spring motion. + /// + /// Start value + /// End value + /// Spring options + /// Created motion builder + public static MotionBuilder Create(Vector3 startValue, Vector3 endValue, SpringOptions options = default) + { + options.Init(new float4(startValue, 0.0f), new float4(endValue, 0.0f)); + return Create(startValue, endValue, 0.0f) + .WithOptions(options); + } + + /// + /// Create a builder for building Vector4 Spring motion. + /// + /// Start value + /// End value + /// Spring options + /// Created motion builder + public static MotionBuilder Create(Vector4 startValue, Vector4 endValue, SpringOptions options = default) + { + options.Init(new float4(startValue), new float4(endValue)); + return Create(startValue, endValue, 0.0f) + .WithOptions(options); + } + } + } +} diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionEvaluationContext.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionEvaluationContext.cs index fe1ed479..a82206bf 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionEvaluationContext.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionEvaluationContext.cs @@ -14,5 +14,10 @@ public struct MotionEvaluationContext /// Current motion time /// public double Time; + + /// + /// Delta time for this frame + /// + public double DeltaTime; } } \ No newline at end of file diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs index 46dbd84b..04592721 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs @@ -112,6 +112,34 @@ public readonly float PlaybackSpeed } } + public TValue GetEndValue(bool checkIsInSequence = true) + where TValue : unmanaged + where TOptions : unmanaged, IMotionOptions + { + return MotionManager.GetTypeDataRef(this, checkIsInSequence).EndValue; + } + + public void SetEndValue(TValue value, bool checkIsInSequence = true) + where TValue : unmanaged + where TOptions : unmanaged, IMotionOptions + { + MotionManager.GetTypeDataRef(this, checkIsInSequence).EndValue = value; + } + + public ref TValue GetEndValueRef(bool checkIsInSequence = true) + where TValue : unmanaged + where TOptions : unmanaged, IMotionOptions + { + return ref MotionManager.GetTypeDataRef(this, checkIsInSequence).EndValue; + } + + public ref TOptions GetOptions(bool checkIsInSequence = true) + where TValue : unmanaged + where TOptions : unmanaged, IMotionOptions + { + return ref MotionManager.GetTypeDataRef(this, checkIsInSequence).Options; + } + public override readonly string ToString() { return $"MotionHandle`{StorageId} ({Index}:{Version})"; diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs index f61a4b2b..a8650e95 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs @@ -45,8 +45,9 @@ public void Execute([AssumeRange(0, int.MaxValue)] int index) _ => default }; - var time = state.Time + deltaTime * state.PlaybackSpeed; - ptr->Update(time, out var result); + deltaTime *= state.PlaybackSpeed; + var time = state.Time + deltaTime; + ptr->Update(time, deltaTime, out var result); Output[index] = result; } else if ((!state.IsPreserved && state.Status is MotionStatus.Completed) || state.Status is MotionStatus.Canceled) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Options/SpringOptions.cs b/src/LitMotion/Assets/LitMotion/Runtime/Options/SpringOptions.cs new file mode 100644 index 00000000..8488086f --- /dev/null +++ b/src/LitMotion/Assets/LitMotion/Runtime/Options/SpringOptions.cs @@ -0,0 +1,132 @@ +using System; +using Unity.Mathematics; +using UnityEngine; + +namespace LitMotion +{ + /// + /// Options for spring motion. + /// + [Serializable] + public struct SpringOptions : IEquatable, IMotionOptions + { + private float4 _startValue; + private float4 _startVelocity; + public float4 CurrentValue; + public float4 CurrentVelocity; + public float4 TargetValue; + public float4 TargetVelocity; + public float Stiffness; + public float DampingRatio; + + /// + /// Creates a new SpringOptions with specified parameters. + /// + /// Spring stiffness (higher = faster convergence) + /// Damping ratio (1.0 = critical damping) + /// Initial velocity + /// Target velocity + public SpringOptions(float stiffness = 10.0f, float dampingRatio = 1.0f, float4 startVelocity = default, float4 targetVelocity = default) + { + Stiffness = stiffness; + DampingRatio = dampingRatio; + CurrentValue = new float4(0.0f, 0.0f, 0.0f, 0.0f); + CurrentVelocity = startVelocity; + TargetValue = new float4(0.0f, 0.0f, 0.0f, 0.0f); + TargetVelocity = targetVelocity; + _startValue = new float4(0.0f, 0.0f, 0.0f, 0.0f); + _startVelocity = startVelocity; + } + + /// + /// Critical damping configuration (fastest convergence without oscillation). + /// + public static SpringOptions Critical + { + get + { + return new SpringOptions() + { + Stiffness = 10.0f, + DampingRatio = 1.0f, + CurrentVelocity = new float4(0.0f, 0.0f, 0.0f, 0.0f), + TargetVelocity = new float4(0.0f, 0.0f, 0.0f, 0.0f), + CurrentValue = new float4(0.0f, 0.0f, 0.0f, 0.0f) + }; + } + } + + /// + /// Overdamped configuration (smooth, slow convergence without oscillation). + /// + public static SpringOptions Overdamped + { + get + { + return new SpringOptions() + { + Stiffness = 10.0f, + DampingRatio = 1.2f, + CurrentVelocity = new float4(0.0f, 0.0f, 0.0f, 0.0f), + TargetVelocity = new float4(0.0f, 0.0f, 0.0f, 0.0f), + CurrentValue = new float4(0.0f, 0.0f, 0.0f, 0.0f) + }; + } + } + + /// + /// Underdamped configuration (bouncy, oscillating motion before settling). + /// + public static SpringOptions Underdamped + { + get + { + return new SpringOptions() + { + Stiffness = 10.0f, + DampingRatio = 0.6f, + CurrentVelocity = new float4(0.0f, 0.0f, 0.0f, 0.0f), + TargetVelocity = new float4(0.0f, 0.0f, 0.0f, 0.0f), + CurrentValue = new float4(0.0f, 0.0f, 0.0f, 0.0f) + }; + } + } + + public void Init(float4 startValue, float4 targetValue, float4 startVelocity = default, float4 targetVelocity = default) + { + CurrentValue = startValue; + TargetValue = targetValue; + CurrentVelocity = startVelocity; + TargetVelocity = targetVelocity; + _startValue = startValue; + _startVelocity = startVelocity; + } + + public void Restart() + { + CurrentValue = _startValue; + CurrentVelocity = _startVelocity; + } + + public readonly bool Equals(SpringOptions other) + { + return Stiffness == other.Stiffness && + DampingRatio == other.DampingRatio && + math.all(TargetVelocity == other.TargetVelocity) && + math.all(CurrentVelocity == other.CurrentVelocity) && + math.all(CurrentValue == other.CurrentValue); + } + + public override readonly bool Equals(object obj) + { + if (obj is SpringOptions options) return Equals(options); + return false; + } + + public override readonly int GetHashCode() + { + return HashCode.Combine(Stiffness, DampingRatio, TargetVelocity, CurrentVelocity, CurrentValue); + } + } + +} diff --git a/src/LitMotion/Assets/LitMotion/Runtime/SpringUtility.cs b/src/LitMotion/Assets/LitMotion/Runtime/SpringUtility.cs new file mode 100644 index 00000000..f2d622ce --- /dev/null +++ b/src/LitMotion/Assets/LitMotion/Runtime/SpringUtility.cs @@ -0,0 +1,632 @@ +using System; +using Unity.Burst; +using Unity.Burst.CompilerServices; +using Unity.Mathematics; +using UnityEngine; +using static Unity.Mathematics.math; +namespace LitMotion +{ + [BurstCompile] + public static class SpringUtility + { + /// + /// Simple spring-damper system with critical damping only (no over-damping or under-damping). + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Natural frequency (stiffness is used directly as natural frequency) + public static void SpringSimple( + in float deltaTime, + ref float currentValue, + ref float currentVelocity, + in float targetValue, + in float stiffness = 10.0f) + { + // SpringSimple is a simplified version designed specifically for critical damping + // Uses stiffness directly as natural frequency, where half of damping coefficient equals natural frequency + float naturalFreq = stiffness; + + // Critical damping calculation logic + float displacementFromTarget = currentValue - targetValue; // Displacement from target position + float velocityWithDamping = currentVelocity + displacementFromTarget * naturalFreq; // Initial velocity with damping + float exponentialDecay = FastNegExp(naturalFreq * deltaTime); // Exponential decay factor + + // Critical damping update formula + currentValue = exponentialDecay * (displacementFromTarget + velocityWithDamping * deltaTime) + targetValue; // New position + currentVelocity = exponentialDecay * (currentVelocity - velocityWithDamping * naturalFreq * deltaTime); // New velocity + } + + /// + /// Elastic spring-damper system with over-damping, under-damping, and critical damping support. + /// Uses high-performance approximation algorithms. + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Target velocity when reaching the target position + /// Damping ratio: 0.6 = bouncy, 1.0 = critical, 1.2 = slow + /// Natural frequency (stiffness is used directly as natural frequency). Examples: 5 = 1s, 10 = 0.5s, 16.5 = 0.2s + public static void SpringElastic( + in float deltaTime, + ref float currentValue, + ref float currentVelocity, + in float targetValue, + in float targetVelocity = 0.0f, + in float dampingRatio = 0.5f, + in float stiffness = 10.0f) + { + float eps = dampingRatio < 1.0f ? 1e-5f : 1e-2f; + if (Hint.Unlikely(Approximately(currentValue, targetValue, eps))) + { + currentValue = targetValue; + currentVelocity = 0.0f; + return; + } + float targetPosition = targetValue; + float targetVel = targetVelocity; + // Rename stiffness parameter to naturalFreq for internal calculation + float naturalFreq = stiffness; + float stiffnessValue = naturalFreq * naturalFreq; // Stiffness value = naturalFreq² + float dampingHalf = dampingRatio * naturalFreq; + float dampingCoeff = 2.0f * dampingHalf; // Damping coefficient = 2 * damping ratio * naturalFreq + float adjustedTargetPosition = targetPosition + (dampingCoeff * targetVel) / stiffnessValue; + + if (Math.Abs(dampingRatio - 1.0f) < 1e-5f) // Critically Damped + { + float initialDisplacement = currentValue - adjustedTargetPosition; + float initialVelocityWithDamping = currentVelocity + initialDisplacement * dampingHalf; + + float exponentialDecay = FastNegExp(dampingHalf * deltaTime); + + currentValue = initialDisplacement * exponentialDecay + deltaTime * initialVelocityWithDamping * exponentialDecay + adjustedTargetPosition; + currentVelocity = -dampingHalf * initialDisplacement * exponentialDecay - dampingHalf * deltaTime * initialVelocityWithDamping * exponentialDecay + initialVelocityWithDamping * exponentialDecay; + } + else if (dampingRatio < 1.0f) // Under Damped + { + float dampedFrequency = math.sqrt(stiffnessValue - (dampingCoeff * dampingCoeff) / 4.0f); + float displacementFromTarget = currentValue - adjustedTargetPosition; + float amplitude = math.sqrt(FastSquare(currentVelocity + dampingHalf * displacementFromTarget) / (dampedFrequency * dampedFrequency) + FastSquare(displacementFromTarget)); + float phase = FastAtan((currentVelocity + displacementFromTarget * dampingHalf) / (-displacementFromTarget * dampedFrequency)); + + amplitude = displacementFromTarget > 0.0f ? amplitude : -amplitude; + + float exponentialDecay = FastNegExp(dampingHalf * deltaTime); + + currentValue = amplitude * exponentialDecay * math.cos(dampedFrequency * deltaTime + phase) + adjustedTargetPosition; + currentVelocity = -dampingHalf * amplitude * exponentialDecay * math.cos(dampedFrequency * deltaTime + phase) - dampedFrequency * amplitude * exponentialDecay * math.sin(dampedFrequency * deltaTime + phase); + } + else // Over Damped (dampingRatio > 1.0f) + { + float fastDecayRate = (dampingCoeff + math.sqrt(dampingCoeff * dampingCoeff - 4f * stiffnessValue)) / 2.0f; + float slowDecayRate = (dampingCoeff - math.sqrt(dampingCoeff * dampingCoeff - 4f * stiffnessValue)) / 2.0f; + // Calculate over-damped coefficients: fastDecayCoeff corresponds to fastDecayRate, slowDecayCoeff corresponds to slowDecayRate + float fastDecayCoeff = (adjustedTargetPosition * fastDecayRate - currentValue * fastDecayRate - currentVelocity) / (slowDecayRate - fastDecayRate); + float slowDecayCoeff = currentValue - fastDecayCoeff - adjustedTargetPosition; + + float fastExponentialDecay = FastNegExp(fastDecayRate * deltaTime); + float slowExponentialDecay = FastNegExp(slowDecayRate * deltaTime); + + // Over-damped position update: slowDecayCoeff uses fastExponentialDecay, fastDecayCoeff uses slowExponentialDecay + currentValue = slowDecayCoeff * fastExponentialDecay + fastDecayCoeff * slowExponentialDecay + adjustedTargetPosition; + currentVelocity = -fastDecayRate * slowDecayCoeff * fastExponentialDecay - slowDecayRate * fastDecayCoeff * slowExponentialDecay; + } + } + /// + /// Precise spring-damper system with over-damping, under-damping, and critical damping support. + /// Uses exact algorithms for higher accuracy. + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Target velocity when reaching the target position + /// Damping ratio + /// Natural frequency (stiffness is used directly as natural frequency) + public static void SpringPrecise( + in float deltaTime, + ref float currentValue, + ref float currentVelocity, + in float targetValue, + in float targetVelocity = 0.0f, + in float dampingRatio = 0.5f, + in float stiffness = 10.0f) + { + float naturalFreq = stiffness; + + float adjustedTargetPosition = targetValue; + if (math.abs(targetVelocity) > 1e-5f) + { + // Adjust target position to achieve specified velocity at target: c = g + (d * q) / s + // where d = 2 * dampingRatio * naturalFreq, s = naturalFreq^2 + float dampingCoeff = 2.0f * dampingRatio * naturalFreq; + float stiffnessValue = naturalFreq * naturalFreq; + adjustedTargetPosition = targetValue + (dampingCoeff * targetVelocity) / stiffnessValue; + } + + float adjustedDisplacement = currentValue - adjustedTargetPosition; + float dampingRatioSquared = dampingRatio * dampingRatio; + float r = -dampingRatio * naturalFreq; + + float displacement; + float calculatedVelocity; + + if (dampingRatio > 1) + { + // Over-damped + float s = naturalFreq * math.sqrt(dampingRatioSquared - 1); + float gammaPlus = r + s; + float gammaMinus = r - s; + + float coeffB = (gammaMinus * adjustedDisplacement - currentVelocity) / (gammaMinus - gammaPlus); + float coeffA = adjustedDisplacement - coeffB; + displacement = coeffA * FastExp(gammaMinus * deltaTime) + coeffB * FastExp(gammaPlus * deltaTime); + calculatedVelocity = coeffA * gammaMinus * FastExp(gammaMinus * deltaTime) + + coeffB * gammaPlus * FastExp(gammaPlus * deltaTime); + } + else if (math.abs(dampingRatio - 1.0f) < 1e-5f) + { + // Critically damped + float coeffA = adjustedDisplacement; + float coeffB = currentVelocity + naturalFreq * adjustedDisplacement; + float nFdT = -naturalFreq * deltaTime; + displacement = (coeffA + coeffB * deltaTime) * FastExp(nFdT); + calculatedVelocity = ((coeffA + coeffB * deltaTime) * FastExp(nFdT) * (-naturalFreq)) + + coeffB * FastExp(nFdT); + } + else + { + // Under-damped + float dampedFreq = naturalFreq * math.sqrt(1 - dampingRatioSquared); + float cosCoeff = adjustedDisplacement; + float sinCoeff = (1.0f / dampedFreq) * ((-r * adjustedDisplacement) + currentVelocity); + float dFdT = dampedFreq * deltaTime; + displacement = FastExp(r * deltaTime) * (cosCoeff * math.cos(dFdT) + sinCoeff * math.sin(dFdT)); + calculatedVelocity = displacement * r + + (FastExp(r * deltaTime) * + ((-dampedFreq * cosCoeff * math.sin(dFdT) + + dampedFreq * sinCoeff * math.cos(dFdT)))); + } + + currentValue = displacement + adjustedTargetPosition; + currentVelocity = calculatedVelocity; + } + /// + /// Velocity smoothing spring-damper system that supports continuous motion with target velocity. + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Intermediate position (maintains state) + /// Smoothing velocity (linear velocity to smooth) + /// Natural frequency (stiffness is used directly as natural frequency) + public static void SpringSimpleVelocitySmoothing( + in float deltaTime, + ref float currentValue, + ref float currentVelocity, + in float targetValue, + ref float intermediatePosition, + in float smothingVelocity = 2f, + in float stiffness = 10.0f) + { + // According to the original design, use stiffness directly as natural frequency + float naturalFreq = stiffness; + + // Calculate difference between target and intermediate position + float targetIntermediateDiff = targetValue - intermediatePosition; + float absTargetIntermediateDiff = math.abs(targetIntermediateDiff); + + // Calculate velocity direction + float velocityDirection = (targetIntermediateDiff > 0.0f ? 1.0f : -1.0f) * smothingVelocity; + + float anticipatedTime = 1f / naturalFreq; + + // Calculate future target position + float futureTargetPosition = absTargetIntermediateDiff > anticipatedTime * smothingVelocity ? + intermediatePosition + velocityDirection * anticipatedTime : targetValue; + + // Directly call SpringSimple function + SpringSimple(deltaTime, ref currentValue, ref currentVelocity, futureTargetPosition, stiffness); + + // Update intermediate position + intermediatePosition = absTargetIntermediateDiff > deltaTime * smothingVelocity ? + intermediatePosition + velocityDirection * deltaTime : targetValue; + } + + /// + /// Duration-limited spring-damper system that automatically calculates appropriate stiffness based on target arrival time. + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Target arrival time in seconds + public static void SpringSimpleDurationLimit( + in float deltaTime, + ref float currentValue, + ref float currentVelocity, + in float targetValue, + in float durationSeconds = 0.2f) + { + // For critically damped systems, convergence time is approximately 3-4 times the time constant. + // Time constant = 1 / naturalFreq, so naturalFreq = 4.6 / durationSeconds + // This achieves approximately 99% convergence within durationSeconds + float naturalFreq = 4.6f / durationSeconds; + + // Use target value directly, no complex intermediate target logic needed + // Let the spring converge directly to the final target + SpringSimple(deltaTime, ref currentValue, ref currentVelocity, targetValue, naturalFreq); + } + + /// + /// Double smoothing spring-damper system using two cascaded spring systems for smoother motion. + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Intermediate position (maintains state) + /// Intermediate velocity (maintains state) + /// Natural frequency (stiffness is used directly as natural frequency) + public static void SpringSimpleDoubleSmoothing( + in float deltaTime, + ref float currentValue, + ref float currentVelocity, + in float targetValue, + ref float intermediatePosition, + ref float intermediateVelocity, + in float stiffness = 10.0f) + { + float floatStiffness = 2.0f * stiffness; + + // First spring: from intermediate position to target position + SpringSimple(deltaTime, ref intermediatePosition, ref intermediateVelocity, targetValue, floatStiffness); + + // Second spring: from current position to intermediate position + SpringSimple(deltaTime, ref currentValue, ref currentVelocity, intermediatePosition, floatStiffness); + } + + #region float4 Spring Functions + + /// + /// Simple spring-damper system with critical damping only (float4 version). + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Natural frequency (stiffness is used directly as natural frequency) + [BurstCompile] + public static void SpringSimple( + in float deltaTime, + ref float4 currentValue, + ref float4 currentVelocity, + in float4 targetValue, + in float stiffness = 1.0f) + { + // Check if already converged to target value + if (Hint.Unlikely(Approximately(currentValue, targetValue))) + { + currentValue = targetValue; + currentVelocity = new float4(0.0f); + return; + } + + // Use stiffness directly as natural frequency, where half of damping coefficient equals natural frequency + float naturalFreq = stiffness; + + // Critical damping calculation logic + float4 displacementFromTarget = currentValue - targetValue; // Displacement from target position + float4 velocityWithDamping = currentVelocity + displacementFromTarget * naturalFreq; // Initial velocity with damping + float4 exponentialDecay = (float4)FastNegExp(naturalFreq * deltaTime); // Exponential decay factor + + // Critical damping update formula + currentValue = exponentialDecay * (displacementFromTarget + velocityWithDamping * deltaTime) + targetValue; // New position + currentVelocity = exponentialDecay * (currentVelocity - velocityWithDamping * naturalFreq * deltaTime); // New velocity + } + + /// + /// Elastic spring-damper system with over-damping, under-damping, and critical damping support (float4 version). + /// Uses high-performance approximation algorithms. + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Target velocity when reaching the target position + /// Damping ratio: 0.6 = bouncy, 1.0 = critical, 1.2 = slow + /// Natural frequency (stiffness is used directly as natural frequency). Examples: 5 = 1s, 10 = 0.5s, 16.5 = 0.2s + [BurstCompile] + public static void SpringElastic( + in float deltaTime, + ref float4 currentValue, + ref float4 currentVelocity, + in float4 targetValue, + in float4 targetVelocity, + in float dampingRatio = 0.5f, + in float stiffness = 10.0f) + { + // Check if already converged to target value + if (Hint.Unlikely(Approximately(currentValue, targetValue))) + { + currentValue = targetValue; + currentVelocity = new float4(0.0f); + return; + } + + // Rename stiffness parameter to naturalFreq for internal calculation + float naturalFreq = stiffness; + float stiffnessValue = naturalFreq * naturalFreq; // Stiffness value = naturalFreq² + float dampingHalf = dampingRatio * naturalFreq; + float dampingCoeff = 2.0f * dampingHalf; // Damping coefficient = 2 * damping ratio * naturalFreq + float4 adjustedtargetValue = targetValue + (dampingCoeff * targetVelocity) / stiffnessValue; + + if (math.abs(dampingRatio - 1.0f) < 1e-5f) // Critically Damped + { + float4 initialDisplacement = currentValue - adjustedtargetValue; + float4 initialVelocityWithDamping = currentVelocity + initialDisplacement * dampingHalf; + + float exponentialDecay = (float)FastNegExp((float)(dampingHalf * deltaTime)); + + currentValue = initialDisplacement * exponentialDecay + deltaTime * initialVelocityWithDamping * exponentialDecay + adjustedtargetValue; + currentVelocity = -dampingHalf * initialDisplacement * exponentialDecay - dampingHalf * deltaTime * initialVelocityWithDamping * exponentialDecay + initialVelocityWithDamping * exponentialDecay; + } + else if (dampingRatio < 1.0f) // Under Damped + { + float dampedFrequency = math.sqrt(stiffnessValue - (dampingCoeff * dampingCoeff) / 4.0f); + float4 displacementFromTarget = currentValue - adjustedtargetValue; + float4 amplitude = math.sqrt(Square(currentVelocity + dampingHalf * displacementFromTarget) / (dampedFrequency * dampedFrequency) + Square(displacementFromTarget)); + float4 phase = FastAtan((currentVelocity + displacementFromTarget * dampingHalf) / (-displacementFromTarget * dampedFrequency)); + + amplitude = math.select(-amplitude, amplitude, displacementFromTarget > 0.0f); + + float exponentialDecay = (float)FastNegExp((float)(dampingHalf * deltaTime)); + + currentValue = amplitude * exponentialDecay * math.cos(dampedFrequency * deltaTime + phase) + adjustedtargetValue; + currentVelocity = -dampingHalf * amplitude * exponentialDecay * math.cos(dampedFrequency * deltaTime + phase) - dampedFrequency * amplitude * exponentialDecay * math.sin(dampedFrequency * deltaTime + phase); + } + else // Over Damped (dampingRatio > 1.0f) + { + float fastDecayRate = (dampingCoeff + math.sqrt(dampingCoeff * dampingCoeff - 4f * stiffnessValue)) / 2.0f; + float slowDecayRate = (dampingCoeff - math.sqrt(dampingCoeff * dampingCoeff - 4f * stiffnessValue)) / 2.0f; + // Calculate over-damped coefficients: fastDecayCoeff corresponds to fastDecayRate, slowDecayCoeff corresponds to slowDecayRate + float4 fastDecayCoeff = (adjustedtargetValue * fastDecayRate - currentValue * fastDecayRate - currentVelocity) / (slowDecayRate - fastDecayRate); + float4 slowDecayCoeff = currentValue - fastDecayCoeff - adjustedtargetValue; + + float fastExponentialDecay = (float)FastNegExp((float)(fastDecayRate * deltaTime)); + float slowExponentialDecay = (float)FastNegExp((float)(slowDecayRate * deltaTime)); + + // Over-damped position update: slowDecayCoeff uses fastExponentialDecay, fastDecayCoeff uses slowExponentialDecay + currentValue = slowDecayCoeff * fastExponentialDecay + fastDecayCoeff * slowExponentialDecay + adjustedtargetValue; + currentVelocity = -fastDecayRate * slowDecayCoeff * fastExponentialDecay - slowDecayRate * fastDecayCoeff * slowExponentialDecay; + } + } + + /// + /// Velocity smoothing spring-damper system that supports continuous motion with target velocity (float4 version). + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Intermediate position (maintains state) + /// Smoothing velocity + /// Natural frequency (stiffness is used directly as natural frequency) + [BurstCompile] + public static void SpringSimpleVelocitySmoothing( + in float deltaTime, + ref float4 currentValue, + ref float4 currentVelocity, + in float4 targetValue, + ref float4 intermediatePosition, + in float smothingVelocity = 2f, + in float stiffness = 1.0f) + { + // According to the original design, use stiffness directly as natural frequency + float naturalFreq = stiffness; + + // Calculate difference between target and intermediate position + float4 targetIntermediateDiff = targetValue - intermediatePosition; + float4 absTargetIntermediateDiff = math.abs(targetIntermediateDiff); + + // Calculate velocity direction + float4 velocityDirection = math.sign(targetIntermediateDiff) * smothingVelocity; + + // Calculate anticipated time + float anticipatedTime = 1f / naturalFreq; + + // Calculate future target position + float4 futureTargetPosition = math.select(targetValue, + intermediatePosition + velocityDirection * anticipatedTime, + absTargetIntermediateDiff > anticipatedTime * smothingVelocity); + + // Directly call SpringSimple function + SpringSimple(deltaTime, ref currentValue, ref currentVelocity, futureTargetPosition, stiffness); + + // Update intermediate position + intermediatePosition = math.select(targetValue, + intermediatePosition + velocityDirection * deltaTime, + absTargetIntermediateDiff > deltaTime * smothingVelocity); + } + + /// + /// Duration-limited spring-damper system that automatically calculates appropriate stiffness based on target arrival time (float4 version). + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Target arrival time in seconds + [BurstCompile] + public static void SpringSimpleDurationLimit( + in float deltaTime, + ref float4 currentValue, + ref float4 currentVelocity, + ref float4 targetValue, + in float durationSeconds = 0.2f) + { + // For critically damped systems, convergence time is approximately 3-4 times the time constant. + // Time constant = 1 / naturalFreq, so naturalFreq = 4.6 / durationSeconds + // This achieves approximately 99% convergence within durationSeconds + float naturalFreq = 4.6f / durationSeconds; + + SpringSimple(deltaTime, ref currentValue, ref currentVelocity, targetValue, naturalFreq); + } + + /// + /// Double smoothing spring-damper system using two cascaded spring systems for smoother motion (float4 version). + /// + /// Time step in seconds + /// Current position value + /// Current velocity value + /// Target position value + /// Intermediate position (maintains state) + /// Intermediate velocity (maintains state) + /// Natural frequency (stiffness is used directly as natural frequency) + [BurstCompile] + public static void SpringSimpleDoubleSmoothing( + in float deltaTime, + ref float4 currentValue, + ref float4 currentVelocity, + in float4 targetValue, + ref float4 intermediatePosition, + ref float4 intermediateVelocity, + in float stiffness = 1.0f) + { + float floatStiffness = 2.0f * stiffness; + + // First spring: from intermediate position to target position + SpringSimple(deltaTime, ref intermediatePosition, ref intermediateVelocity, targetValue, floatStiffness); + + // Second spring: from current position to intermediate position + SpringSimple(deltaTime, ref currentValue, ref currentVelocity, intermediatePosition, floatStiffness); + } + + #endregion + + + + private static float HalfLifeToDamping(float halfLife, float eps = 1e-5f) + { + return (4.0f * 0.6931471805599453f) / (halfLife + eps); + } + + private static float DampingToHalfLife(float damping, float eps = 1e-5f) + { + return (4.0f * 0.6931471805599453f) / (damping + eps); + } + + private static float DampingRatioToStiffness(float ratio, float damping) + { + return Square(damping / (ratio * 2.0f)); + } + + private static float DampingRatioToDamping(float ratio, float stiffness) + { + return ratio * 2.0f * math.sqrt(stiffness); + } + + private static float FrequencyToStiffness(float frequency) + { + return Square(2.0f * math.PI * frequency); + } + + /// + /// Converts half-life to natural frequency (for critical damping). + /// + /// Half-life in seconds + /// Natural frequency in rad/s + private static float HalflifeToNaturalFreq(float halflife) + { + return 0.69314718056f / halflife; // ω₀ = ln(2) / τ₁/₂ + } + + /// + /// Converts natural frequency to half-life (for critical damping). + /// + /// Natural frequency in rad/s + /// Half-life in seconds + private static float NaturalFreqToHalflife(float naturalFreq) + { + return 0.69314718056f / naturalFreq; // τ₁/₂ = ln(2) / ω₀ + } + private static float FastNegExp(float x) + { + return 1.0f / (1.0f + x + 0.48f * x * x + 0.235f * x * x * x); + } + + private static float Square(float x) + { + return x * x; + } + + private static float4 Square(float4 x) + { + return x * x; + } + + /// + /// Fast arctangent approximation, 2-5x faster than Math.Atan with minimal precision loss. + /// + private static float FastAtan(float x) + { + float z = math.abs(x); + float w = z > 1.0f ? 1.0f / z : z; + float y = (math.PI / 4.0f) * w - w * (w - 1) * (0.2447f + 0.0663f * w); + return math.sign(x) * (z > 1.0f ? math.PI / 2.0f - y : y); + } + + /// + /// Fast arctangent approximation (float4 version). + /// + private static float4 FastAtan(float4 x) + { + float4 z = math.abs(x); + float4 w = math.select(1.0f / z, z, z <= 1.0f); + float4 y = (math.PI / 4.0f) * w - w * (w - 1.0f) * (0.2447f + 0.0663f * w); + return math.sign(x) * math.select(math.PI / 2.0f - y, y, z <= 1.0f); + } + + /// + /// Fast square function with same performance as direct multiplication + /// + private static float FastSquare(float x) + { + return x * x; + } + + private static float FastExp(float x) + { + // Fast exponential function approximation for Burst + return 1.0f / (1.0f - x + 0.5f * x * x - 0.1667f * x * x * x); + } + /// + /// Checks if two float values are approximately equal. + /// + /// First value + /// Second value + /// Precision threshold + /// True if approximately equal + [BurstCompile] + public static bool Approximately(in float a, in float b, in float precision = 1e-2f) + { + return math.abs(b - a) < math.max(1E-06f * math.max(math.abs(a), math.abs(b)), precision); + } + + /// + /// Checks if two float4 values are approximately equal. + /// + /// First value + /// Second value + /// Precision threshold + /// True if all components are approximately equal + [BurstCompile] + public static bool Approximately(in float4 a, in float4 b, in float precision = 1e-2f) + { + return math.all(math.abs(b - a) < math.max(1E-06f * math.max(math.abs(a), math.abs(b)), precision)); + } + } +}