using System; using System.Runtime.CompilerServices; using Unity.Collections; using Unity.Mathematics; namespace UnityEngine.Perception.Randomization.Samplers { /// /// A set of utility functions for defining sampler interfaces /// public static class SamplerUtility { /// /// A large prime number /// public const uint largePrime = 0x202A96CF; /// /// The number of samples to generate per job batch in an IJobParallelForBatch job /// public const int samplingBatchSize = 64; /// /// Returns the sampler's display name /// /// The sampler type /// The display name public static string GetSamplerDisplayName(Type samplerType) { return samplerType.Name.Replace("Sampler", string.Empty); } /// /// Non-deterministically generates a non-zero random seed /// /// A non-deterministically generated random seed public static uint GenerateRandomSeed() { return (uint)Random.Range(1, uint.MaxValue); } /// /// Hashes using constants generated from a program that maximizes the avalanche effect, independence of /// output bit changes, and the probability of a change in each output bit if any input bit is changed. /// Source: https://github.com/h2database/h2database/blob/master/h2/src/test/org/h2/test/store/CalculateHashConstant.java /// /// Unsigned integer to hash /// The calculated hash value [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint Hash32(uint x) { x = ((x >> 16) ^ x) * 0x45d9f3b; x = ((x >> 16) ^ x) * 0x45d9f3b; x = (x >> 16) ^ x; return x; } /// /// Generates a 32-bit non-zero hash using an unsigned integer seed /// /// The unsigned integer to hash /// The calculated hash value [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint Hash32NonZero(uint seed) { var hash = Hash32(seed); return hash == 0u ? largePrime : hash; } /// /// Based on splitmix64: http://xorshift.di.unimi.it/splitmix64.c /// /// 64-bit value to hash /// The calculated hash value [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong Hash64(ulong x) { x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ul; x = (x ^ (x >> 27)) * 0x94d049bb133111ebul; x ^= (x >> 31); return x; } /// /// Generates new a new non-zero random state by deterministically hashing a base seed with an iteration index /// /// Usually the current scenario iteration or framesSinceInitialization /// The seed to be offset /// A new random state [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint IterateSeed(uint index, uint baseSeed) { var state = (uint)Hash64(((ulong)index << 32) | baseSeed); return state == 0u ? largePrime : state; } /// /// Source: https://www.johndcook.com/blog/csharp_phi/ /// static float NormalCdf(float x) { const float a1 = 0.254829592f; const float a2 = -0.284496736f; const float a3 = 1.421413741f; const float a4 = -1.453152027f; const float a5 = 1.061405429f; const float p = 0.3275911f; var sign = 1; if (x < 0) sign = -1; x = math.abs(x) / math.sqrt(2.0f); var t = 1.0f / (1.0f + p*x); var y = 1.0f - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t * math.exp(-x*x); return 0.5f * (1.0f + sign*y); } /// /// Source: https://www.johndcook.com/blog/csharp_phi_inverse/ /// static float RationalApproximation(float t) { const float c0 = 2.515517f; const float c1 = 0.802853f; const float c2 = 0.010328f; const float d0 = 1.432788f; const float d1 = 0.189269f; const float d2 = 0.001308f; return t - ((c2*t + c1)*t + c0) / (((d2*t + d1)*t + d0)*t + 1.0f); } /// /// Source: https://www.johndcook.com/blog/csharp_phi_inverse/ /// Note: generates NaN values for values 0 and 1 /// /// A uniform sample value between the range (0, 1) static float NormalCdfInverse(float uniformSample) { return uniformSample < 0.5f ? -RationalApproximation(math.sqrt(-2.0f * math.log(uniformSample))) : RationalApproximation(math.sqrt(-2.0f * math.log(1.0f - uniformSample))); } /// /// Generates samples from a truncated normal distribution. /// Further reading about this distribution can be found here: /// https://en.wikipedia.org/wiki/Truncated_normal_distribution /// /// A sample value between 0 and 1 generated from a uniform distribution /// The minimum possible value to generate /// The maximum possible value to generate /// The mean of the normal distribution /// The standard deviation of the normal distribution /// A value sampled from a truncated normal distribution /// public static float TruncatedNormalSample(float uniformSample, float min, float max, float mean, float stdDev) { if (min > max) throw new ArgumentException("Invalid range"); if (uniformSample == 0f) return min; if (uniformSample == 1f) return max; if (stdDev == 0f) return math.clamp(mean, min, max); var a = NormalCdf((min - mean) / stdDev); var b = NormalCdf((max - mean) / stdDev); var c = math.lerp(a, b, uniformSample); if (c == 0f) return max; if (c == 1f) return min; var stdTruncNorm = NormalCdfInverse(c); return math.clamp(stdTruncNorm * stdDev + mean, min, max); } /// /// Generate samples from probability distribution derived from a given AnimationCurve. /// /// Numerical integration representing the AnimationCurve /// A sample value between 0 and 1 generated from a uniform distribution /// The interval at which the original AnimationCurve was sampled in order to produce integratedCurve /// The time attribute of the first key of the original AnimationCurve /// The time attribute of the last key of the original AnimationCurve /// The generated sample public static float AnimationCurveSample(float[] integratedCurve, float uniformSample, float interval, float startTime, float endTime) { var scaledSample = uniformSample * integratedCurve[integratedCurve.Length - 1]; for (var i = 0; i < integratedCurve.Length - 1; i++) { if (scaledSample > integratedCurve[i] && scaledSample < integratedCurve[i + 1]) { var valueDifference = integratedCurve[i + 1] - integratedCurve[i]; var upperWeight = (scaledSample - integratedCurve[i]) / valueDifference; var lowerWeight = 1 - upperWeight; var matchingIndex = i * lowerWeight + (i + 1) * upperWeight; var matchingTimeStamp = startTime + matchingIndex * interval; return matchingTimeStamp; } } throw new ArithmeticException("Could not find matching timestamp."); } /// /// Numerically integrate a given AnimationCurve using the specified number of samples. /// Based on https://en.wikipedia.org/wiki/Numerical_integration and http://blog.s-schoener.com/2018-05-05-animation-curves/ /// Using the trapezoidal rule for numerical interpolation /// /// The array to fill with integrated values /// The animation curve to sample integrate /// public static void IntegrateCurve(float[] array, AnimationCurve curve) { if (curve.length == 0) { throw new ArgumentException("The provided Animation Curve includes no keys."); } var startTime = curve.keys[0].time; var endTime = curve.keys[curve.length - 1].time; var interval = (endTime - startTime) / (array.Length - 1); array[0] = 0; var previousValue = curve.Evaluate(startTime); for (var i = 1; i < array.Length; i++) { if (curve.length == 1) { array[i] = previousValue; } else { var currentTime = startTime + i * interval; var currentValue = curve.Evaluate(currentTime); array[i] = array[i-1] + (previousValue + currentValue) * interval / 2; previousValue = currentValue; } } } } }