From 21271314af2c07748dd4c3600388d64b25127e3d Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Wed, 5 Apr 2023 17:36:49 +0100 Subject: [PATCH] feat: add generic math where possible in MathUtility --- X10D/src/Math/BinaryIntegerExtensions.cs | 24 -- X10D/src/Math/MathUtility.cs | 395 +++++++++++++++-------- X10D/src/Math/NumberExtensions.cs | 104 ++++++ 3 files changed, 357 insertions(+), 166 deletions(-) create mode 100644 X10D/src/Math/NumberExtensions.cs diff --git a/X10D/src/Math/BinaryIntegerExtensions.cs b/X10D/src/Math/BinaryIntegerExtensions.cs index 94b3728..20eae95 100644 --- a/X10D/src/Math/BinaryIntegerExtensions.cs +++ b/X10D/src/Math/BinaryIntegerExtensions.cs @@ -1,6 +1,5 @@ #if NET7_0_OR_GREATER using System.Diagnostics.Contracts; -using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; using X10D.CompilerServices; @@ -31,28 +30,5 @@ public static class BinaryIntegerExtensions TInteger root = TInteger.Abs(value).Mod(nine); return int.CreateChecked(root == TInteger.Zero ? nine : root); } - - /// - /// Performs a modulo operation which supports a negative dividend. - /// - /// The dividend. - /// The divisor. - /// The result of dividend mod divisor. - /// - /// The % operator (commonly called the modulo operator) in C# is not defined to be modulo, but is instead - /// remainder. This quirk inherently makes it difficult to use modulo in a negative context, as x % y where x is - /// negative will return a negative value, akin to -(x % y), even if precedence is forced. This method provides a - /// modulo operation which supports negative dividends. - /// - /// ShreevatsaR, https://stackoverflow.com/a/1082938/1467293 - /// CC-BY-SA 2.5 - [Pure] - [MethodImpl(CompilerResources.MethodImplOptions)] - public static TInteger Mod(this TInteger dividend, TInteger divisor) - where TInteger : IBinaryInteger - { - TInteger r = dividend % divisor; - return r < TInteger.Zero ? r + divisor : r; - } } #endif diff --git a/X10D/src/Math/MathUtility.cs b/X10D/src/Math/MathUtility.cs index c5bc859..2ba9f21 100644 --- a/X10D/src/Math/MathUtility.cs +++ b/X10D/src/Math/MathUtility.cs @@ -1,4 +1,5 @@ using System.Diagnostics.Contracts; +using System.Numerics; using System.Runtime.CompilerServices; using X10D.CompilerServices; @@ -12,6 +13,7 @@ public static class MathUtility private const double DefaultGamma = 2.2; private const float DefaultGammaF = 2.2f; +#if !NET7_0_OR_GREATER /// /// Applies a simple bias function to value. /// @@ -40,80 +42,6 @@ public static class MathUtility return value / ((1.0 / bias - 2.0) * (1.0 - value) + 1.0); } - /// - /// Calculates exponential decay for a value. - /// - /// The value to decay. - /// A factor by which to scale the decay. - /// The decay amount. - /// The exponentially decayed value. - public static float ExponentialDecay(float value, float alpha, float decay) - { - return value * MathF.Exp(-decay * alpha); - } - - /// - /// Calculates exponential decay for a value. - /// - /// The value to decay. - /// A factor by which to scale the decay. - /// The decay amount. - /// The exponentially decayed value. - public static double ExponentialDecay(double value, double alpha, double decay) - { - return value * System.Math.Exp(-decay * alpha); - } - - /// - /// Converts a gamma-encoded value to a linear value using a gamma value of 2.2. - /// - /// The gamma-encoded value to convert. Expected range is [0, 1]. - /// The linear value. - [Pure] - [MethodImpl(CompilerResources.MethodImplOptions)] - public static float GammaToLinear(float value) - { - return GammaToLinear(value, DefaultGammaF); - } - - /// - /// Converts a gamma-encoded value to a linear value using the specified gamma value. - /// - /// The gamma-encoded value to convert. Expected range is [0, 1]. - /// The gamma value to use for decoding. - /// The linear value. - [Pure] - [MethodImpl(CompilerResources.MethodImplOptions)] - public static float GammaToLinear(float value, float gamma) - { - return MathF.Pow(value, 1.0f / gamma); - } - - /// - /// Converts a gamma-encoded value to a linear value using a gamma value of 2.2. - /// - /// The gamma-encoded value to convert. Expected range is [0, 1]. - /// The linear value. - [Pure] - [MethodImpl(CompilerResources.MethodImplOptions)] - public static double GammaToLinear(double value) - { - return GammaToLinear(value, DefaultGamma); - } - - /// - /// Converts a gamma-encoded value to a linear value using the specified gamma value. - /// - /// The gamma-encoded value to convert. Expected range is [0, 1]. - /// The gamma value to use for decoding. - /// The linear value. - [Pure] - [MethodImpl(CompilerResources.MethodImplOptions)] - public static double GammaToLinear(double value, double gamma) - { - return System.Math.Pow(value, 1.0 / gamma); - } - /// /// Returns the linear interpolation inverse of a value, such that it determines where a value lies between two other /// values. @@ -190,6 +118,167 @@ public static class MathUtility return ((1.0 - alpha) * value) + (alpha * target); } + /// + /// Performs smooth Hermite interpolation from one value to a target using a specified alpha. + /// + /// The interpolation source. + /// The interpolation target. + /// The interpolation alpha. + /// The interpolation result. + public static float SmoothStep(float value, float target, float alpha) + { + alpha = System.Math.Clamp(alpha, 0.0f, 1.0f); + alpha = -2.0f * alpha * alpha * alpha + 3.0f * alpha * alpha; + return target * alpha + value * (1.0f - alpha); + } + + /// + /// Performs smooth Hermite interpolation from one value to a target using a specified alpha. + /// + /// The interpolation source. + /// The interpolation target. + /// The interpolation alpha. + /// The interpolation result. + public static double SmoothStep(double value, double target, double alpha) + { + alpha = System.Math.Clamp(alpha, 0.0, 1.0); + alpha = -2.0 * alpha * alpha * alpha + 3.0 * alpha * alpha; + return target * alpha + value * (1.0 - alpha); + } + + /// + /// Converts a value from being a percentage of one range, to being the same percentage in a new range. + /// + /// The value to convert. + /// The old minimum value. + /// The old maximum value. + /// The new minimum value. + /// The new maximum value. + /// The scaled value. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static float ScaleRange(float value, float oldMin, float oldMax, float newMin, float newMax) + { + float oldRange = oldMax - oldMin; + float newRange = newMax - newMin; + float alpha = (value - oldMin) / oldRange; + return (alpha * newRange) + newMin; + } + + /// + /// Converts a value from being a percentage of one range, to being the same percentage in a new range. + /// + /// The value to convert. + /// The old minimum value. + /// The old maximum value. + /// The new minimum value. + /// The new maximum value. + /// The scaled value. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static double ScaleRange(double value, double oldMin, double oldMax, double newMin, double newMax) + { + double oldRange = oldMax - oldMin; + double newRange = newMax - newMin; + double alpha = (value - oldMin) / oldRange; + return (alpha * newRange) + newMin; + } + + /// + /// Returns the incremental sawtooth wave of a given value. + /// + /// The value to calculate. + /// The sawtooth wave of the given value. + public static float Sawtooth(float value) + { + return (value - MathF.Floor(value)); + } + + /// + /// Returns the incremental sawtooth wave of a given value. + /// + /// The value to calculate. + /// The sawtooth wave of the given value. + public static double Sawtooth(double value) + { + return (value - System.Math.Floor(value)); + } +#endif + + /// + /// Calculates exponential decay for a value. + /// + /// The value to decay. + /// A factor by which to scale the decay. + /// The decay amount. + /// The exponentially decayed value. + public static float ExponentialDecay(float value, float alpha, float decay) + { + return value * MathF.Exp(-decay * alpha); + } + + /// + /// Calculates exponential decay for a value. + /// + /// The value to decay. + /// A factor by which to scale the decay. + /// The decay amount. + /// The exponentially decayed value. + public static double ExponentialDecay(double value, double alpha, double decay) + { + return value * System.Math.Exp(-decay * alpha); + } + + /// + /// Converts a gamma-encoded value to a linear value using a gamma value of 2.2. + /// + /// The gamma-encoded value to convert. Expected range is [0, 1]. + /// The linear value. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static float GammaToLinear(float value) + { + return GammaToLinear(value, DefaultGammaF); + } + + /// + /// Converts a gamma-encoded value to a linear value using the specified gamma value. + /// + /// The gamma-encoded value to convert. Expected range is [0, 1]. + /// The gamma value to use for decoding. + /// The linear value. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static float GammaToLinear(float value, float gamma) + { + return MathF.Pow(value, 1.0f / gamma); + } + + /// + /// Converts a gamma-encoded value to a linear value using a gamma value of 2.2. + /// + /// The gamma-encoded value to convert. Expected range is [0, 1]. + /// The linear value. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static double GammaToLinear(double value) + { + return GammaToLinear(value, DefaultGamma); + } + + /// + /// Converts a gamma-encoded value to a linear value using the specified gamma value. + /// + /// The gamma-encoded value to convert. Expected range is [0, 1]. + /// The gamma value to use for decoding. + /// The linear value. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static double GammaToLinear(double value, double gamma) + { + return System.Math.Pow(value, 1.0 / gamma); + } + /// /// Converts a linear value to a gamma-encoded value using a gamma value of 2.2. /// @@ -288,64 +377,6 @@ public static class MathUtility #endif } - /// - /// Returns the incremental sawtooth wave of a given value. - /// - /// The value to calculate. - /// The sawtooth wave of the given value. - public static float Sawtooth(float value) - { - return (value - MathF.Floor(value)); - } - - /// - /// Returns the incremental sawtooth wave of a given value. - /// - /// The value to calculate. - /// The sawtooth wave of the given value. - public static double Sawtooth(double value) - { - return (value - System.Math.Floor(value)); - } - - /// - /// Converts a value from being a percentage of one range, to being the same percentage in a new range. - /// - /// The value to convert. - /// The old minimum value. - /// The old maximum value. - /// The new minimum value. - /// The new maximum value. - /// The scaled value. - [Pure] - [MethodImpl(CompilerResources.MethodImplOptions)] - public static float ScaleRange(float value, float oldMin, float oldMax, float newMin, float newMax) - { - float oldRange = oldMax - oldMin; - float newRange = newMax - newMin; - float alpha = (value - oldMin) / oldRange; - return (alpha * newRange) + newMin; - } - - /// - /// Converts a value from being a percentage of one range, to being the same percentage in a new range. - /// - /// The value to convert. - /// The old minimum value. - /// The old maximum value. - /// The new minimum value. - /// The new maximum value. - /// The scaled value. - [Pure] - [MethodImpl(CompilerResources.MethodImplOptions)] - public static double ScaleRange(double value, double oldMin, double oldMax, double newMin, double newMax) - { - double oldRange = oldMax - oldMin; - double newRange = newMax - newMin; - double alpha = (value - oldMin) / oldRange; - return (alpha * newRange) + newMin; - } - /// /// Calculates the sigmoid function for the given input value. /// @@ -374,18 +405,92 @@ public static class MathUtility return 1.0f / (1.0f + System.Math.Exp(-value)); } +#if NET7_0_OR_GREATER /// - /// Performs smooth Hermite interpolation from one value to a target using a specified alpha. + /// Applies a simple bias function to value. + /// + /// The value to which the bias function will be applied. + /// The bias value. Valid values range from 0-1. + /// The biased result. + /// + /// If is less than 0.5, will be shifted downward; otherwise, upward. + /// + public static TNumber Bias(TNumber value, TNumber bias) + where TNumber : INumber + { + TNumber identity = TNumber.MultiplicativeIdentity; + return value / ((identity / bias - TNumber.CreateChecked(2)) * (identity - value) + identity); + } + + /// + /// Returns the linear interpolation inverse of a value, such that it determines where a value lies between two other + /// values. + /// + /// The value whose lerp inverse is to be found. + /// The start of the range. + /// The end of the range. + /// A value determined by (alpha - start) / (end - start). + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static TNumber InverseLerp(TNumber alpha, TNumber start, TNumber end) + where TNumber : INumber + { + if (start == end) + { + return TNumber.Zero; + } + + return (alpha - start) / (end - start); + } + + /// + /// Linearly interpolates from one value to a target using a specified alpha. /// /// The interpolation source. /// The interpolation target. /// The interpolation alpha. - /// The interpolation result. - public static float SmoothStep(float value, float target, float alpha) + /// + /// The interpolation result as determined by (1 - alpha) * value + alpha * target. + /// + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static TNumber Lerp(TNumber value, TNumber target, TNumber alpha) + where TNumber : INumber { - alpha = System.Math.Clamp(alpha, 0.0f, 1.0f); - alpha = -2.0f * alpha * alpha * alpha + 3.0f * alpha * alpha; - return target * alpha + value * (1.0f - alpha); + // rookie mistake: a + t * (b - a) + // "precise" method: (1 - t) * a + t * b + return ((TNumber.MultiplicativeIdentity - alpha) * value) + (alpha * target); + } + + /// + /// Returns the incremental sawtooth wave of a given value. + /// + /// The value to calculate. + /// The sawtooth wave of the given value. + public static TNumber Sawtooth(TNumber value) + where TNumber : IFloatingPoint + { + return (value - TNumber.Floor(value)); + } + + /// + /// Converts a value from being a percentage of one range, to being the same percentage in a new range. + /// + /// The value to convert. + /// The old minimum value. + /// The old maximum value. + /// The new minimum value. + /// The new maximum value. + /// The scaled value. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static TNumber ScaleRange(TNumber value, TNumber oldMin, TNumber oldMax, TNumber newMin, TNumber newMax) + where TNumber : INumber + { + TNumber oldRange = oldMax - oldMin; + TNumber newRange = newMax - newMin; + TNumber alpha = (value - oldMin) / oldRange; + return (alpha * newRange) + newMin; } /// @@ -395,10 +500,16 @@ public static class MathUtility /// The interpolation target. /// The interpolation alpha. /// The interpolation result. - public static double SmoothStep(double value, double target, double alpha) + public static TNumber SmoothStep(TNumber value, TNumber target, TNumber alpha) + where TNumber : INumber { - alpha = System.Math.Clamp(alpha, 0.0, 1.0); - alpha = -2.0 * alpha * alpha * alpha + 3.0 * alpha * alpha; - return target * alpha + value * (1.0 - alpha); + TNumber one = TNumber.One; + TNumber two = one + one; + TNumber three = two + one; + + alpha = TNumber.Clamp(alpha, TNumber.Zero, TNumber.One); + alpha = -two * alpha * alpha * alpha + three * alpha * alpha; + return target * alpha + value * (one - alpha); } +#endif } diff --git a/X10D/src/Math/NumberExtensions.cs b/X10D/src/Math/NumberExtensions.cs new file mode 100644 index 0000000..12f00f8 --- /dev/null +++ b/X10D/src/Math/NumberExtensions.cs @@ -0,0 +1,104 @@ +#if NET7_0_OR_GREATER +using System.Diagnostics.Contracts; +using System.Numerics; +using System.Runtime.CompilerServices; +using X10D.CompilerServices; + +namespace X10D.Math; + +/// +/// Math-related extension methods for . +/// +public static class NumberExtensions +{ + /// + /// Returns a value indicating whether the current value is evenly divisible by 2. + /// + /// The value whose parity to check. + /// + /// if is evenly divisible by 2, or + /// otherwise. + /// + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static bool IsEven(this TNumber value) + where TNumber : INumber + { + return value % TNumber.CreateChecked(2) == TNumber.Zero; + } + + /// + /// Returns a value indicating whether the current value is not evenly divisible by 2. + /// + /// The value whose parity to check. + /// + /// if is not evenly divisible by 2, or + /// otherwise. + /// + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static bool IsOdd(this TNumber value) + where TNumber : INumber + { + return !value.IsEven(); + } + + /// + /// Performs a modulo operation which supports a negative dividend. + /// + /// The dividend. + /// The divisor. + /// The result of dividend mod divisor. + /// + /// The % operator (commonly called the modulo operator) in C# is not defined to be modulo, but is instead + /// remainder. This quirk inherently makes it difficult to use modulo in a negative context, as x % y where x is + /// negative will return a negative value, akin to -(x % y), even if precedence is forced. This method provides a + /// modulo operation which supports negative dividends. + /// + /// ShreevatsaR, https://stackoverflow.com/a/1082938/1467293 + /// CC-BY-SA 2.5 + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static TNumber Mod(this TNumber dividend, TNumber divisor) + where TNumber : INumber + { + TNumber r = dividend % divisor; + return r < TNumber.Zero ? r + divisor : r; + } + + /// + /// Returns an integer that indicates the sign of this number. + /// + /// A signed number. + /// + /// A number that indicates the sign of , as shown in the following table. + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// -1 + /// is less than zero. + /// + /// + /// 0 + /// is equal to zero. + /// + /// + /// 1 + /// is greater than zero. + /// + /// + /// + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static int Sign(this TNumber value) + where TNumber : INumber + { + return TNumber.Sign(value); + } +} +#endif