Math Utils

47 min read

Numeric helpers for float-safe comparison, clamping, lerp, angle math, easing curves, coordinate conversions, and the small bits of geometry and complex math that show up in physics and procedural work.

Summary

The Scylla math utilities cover most of the routine numeric work that game development requires: safely comparing floats, clamping values, lerping and remapping, doing modular arithmetic that respects sign, wrapping angles, easing curves between key poses, smoothly damping toward a moving target, converting between coordinate systems, and solving the small geometric questions (closest point on a line, ray-plane intersection, plane snapping) that come up in collision, AI, and camera code. The surface is split across two layers:

  • NumberUtil and its companion NumberExtensions cover scalar arithmetic on every primitive type.
  • MathUtil, Interpolation, Complex, Polynomial, and Monomial cover the higher-level shapes built on top.

Float equality is the single most common bug in gameplay math. The IEEE 754 (binary floating-point standard) representation makes two floats that the algebra says are equal almost never bit-equal after a few operations, so a == b returns false even when the values are “the same number”. NumberUtil.Approximately(a, b) solves this with an explicit tolerance (the default EPSILON = 1e-6f matches the precision typically retained after a small number of float operations); the framework’s code-style rule enforces it over raw == in code review. The same module provides Clamp and Clamp01 for every primitive type (with a clear ArgumentException when min > max), IsBetween with inclusive/exclusive switching, Snap and RoundToDecimals for tidy display values, SafeDivide to avoid divide-by-zero, Mod that returns a non-negative result for negative inputs (unlike %), and Wrap and Repeat for cyclic ranges. Every helper also exists as an extension method through NumberExtensions so a fluent style is available where it reads better.

Interpolation adds the easing and spline functions that lerp alone cannot produce: SmoothStep and SmootherStep for the classic ease-in-out curves, LerpSmooth / LerpSmoothStart / LerpSmoothEnd for one-sided ease variants (scalars and Vector2 / Vector3), Cubic and CatmullRom for splines that pass through control points smoothly, LerpExponential for arbitrary easing curves, and MoveTowards for snap-on-arrival animation that does not overshoot. For animated transitions driven by a tween system, see Tween. MathUtil.SmoothDamp is the critically damped spring used by camera, UI, and any “track a target that moves” code; it is framerate-independent and never overshoots. MathUtil.ConvergeToValue is the linear-rate counterpart for UI meters and health bars where a constant speed reads better than a damped curve.

MathUtil also handles the spatial work: yaw-pitch direction round-trips (ToSpherical / ToCartesian), full coordinate-system conversions (CartesianToSpherical, CartesianToCylindrical, CartesianToPolar and inverses), grid snapping in 2D and 3D, projecting a point onto a plane, finding the closest point on a line, intersecting three planes, fetching the near-plane corners of a Camera, flattening to the XZ plane (normalized and raw), and a Gram-Schmidt fix for a quaternion that has accumulated roll (CorrectRotationUp). Complex, Polynomial, and Monomial are the mathematical types for signal processing and curve-driven gameplay: a Complex struct with full operator overloading and polar / root-of-unity construction; a Polynomial with derivatives, antiderivatives, integration, root finding, and Newton’s method; and a Monomial that combines into polynomials via operator arithmetic.

Use the math utilities for:

  • Safe float comparison everywhere == would be wrong: distance checks, velocity thresholds, near-zero detection, finite/NaN guards.
  • Clamping, range checks, and snapping in input handling, UI bars, level scoring, and procedural placement.
  • Lerp, inverse lerp, and remap to convert between value ranges (raw input to gameplay value, gameplay value to UI value, frame time to animation progress).
  • Modular wrap, repeat, and angle normalization so values stay in their canonical range without surprise sign flips.
  • Easing and spline interpolation for camera arcs, UI transitions, AI tweening, and animation that needs a curve plain lerp does not produce.
  • Critically damped follow (SmoothDamp) for camera, UI, and any target-tracking system that needs to feel smooth and never overshoot.
  • Coordinate conversions and 3D spatial geometry: closest-point queries, plane projection, plane intersections, grid snapping, axis flattening.
  • Mathematical types for signal processing (Complex), curve fitting and root finding (Polynomial), and motion authoring (Monomial-based polynomial construction).

Features

  • Float-safe comparison with Approximately. Default tolerance EPSILON = 1e-6f for floats and 1e-12 for doubles, plus IsNearlyZero, IsFinite, EnsureFinite, and EnsureFiniteNonNegative guards. The framework’s code-style rule enforces Approximately over raw == in code review.
  • Scalar arithmetic across every primitive. Clamp, Clamp01, IsBetween, Snap, RoundToDecimals, FloorToInt/CeilToInt/RoundToInt, Mod, Repeat, Wrap, Sqr, Cube, SignNonZero, AbsDiff, IsEven, IsOdd, SafeDivide, IsPowerOfTwo, NextPowerOfTwo, AlignDown, AlignUp. Each works on every numeric primitive type.
  • Sign-correct modular arithmetic. Mod, Repeat, and Wrap return non-negative results for negative inputs (unlike C#’s %), so cyclic ranges (array indices, calendar days, angles) stay in their canonical range without surprise sign flips.
  • Angle math with both unit systems. Degree and radian conversions, NormalizeAngleDeg360/Deg180/Rad2Pi/RadPi, DeltaAngleDeg/Rad for shortest signed delta, and LerpAngleDeg for angle blending that takes the short way around.
  • Easing curves and splines. SmoothStep, SmootherStep, LerpSmooth/Start/End (scalar, Vector2, Vector3), LerpExponential, MoveTowards, EaseInOutCubic, cubic Hermite, and Catmull-Rom for camera arcs, UI transitions, and AI tweening that a plain lerp cannot produce.
  • Critically damped follow. MathUtil.SmoothDamp is the framerate-independent spring used by camera, UI, and any target-tracking system. Never overshoots; constant ease-in regardless of frame time. MathUtil.ConvergeToValue is the constant-rate counterpart for UI meters and health bars.
  • Coordinate-system conversions. Round-trip helpers between Cartesian, Spherical, Cylindrical, and Polar coordinates plus yaw-pitch direction conversions (ToSpherical/ToCartesian). The right primitive for camera orbits, AI line-of-sight, and 2D radar overlays.
  • 3D spatial geometry. ProjectOnPlane, ClosestPointOnLine, Intersection3Planes, GetNearPlaneCorners (for a Camera), SnapToGrid (2D and 3D), FlattenXZ and FlattenXZRaw for top-down conversion, CorrectRotationUp (Gram-Schmidt orthogonalization on a quaternion that has accumulated roll).
  • NumberExtensions companion. Every NumberUtil operation also exists as an extension method (value.Approximately(other), value.Clamp01(), value.Snap(0.25f)). Pick the call style that reads better at the call site; the results are identical.
  • Complex number struct. Real and imaginary fields, conjugate, modulus, argument, FromPolar, RootOfUnity, Cis factories, square / cube roots, arbitrary powers, all four arithmetic operators, and an implicit cast from float. Useful for FFT work, signal processing, and 2D rotations expressed as multiplication.
  • Polynomial with calculus. Variadic and dictionary constructors, Bernstein basis factory (for Bezier work), Evaluate, GetDerivative, GetAntiderivative, Integrate over [a, b], closed-form GetRoots for degree <= 4, GetDiscriminant, and NewtonsMethod for higher degrees. Full operator overloading combines Polynomial, Monomial, and float.
  • Monomial for term-by-term authoring. Single-term polynomial (coefficient + degree) that combines with operators to build larger polynomials. Useful when authoring polynomial expressions reads more naturally term-by-term than as a coefficient array.

Components

The surface is split across seven public types, organized by responsibility. The two scalar-arithmetic classes (NumberUtil plus its extension companion NumberExtensions) cover everything that operates on a primitive number; the spatial / camera / smoothing class (MathUtil) covers vector and quaternion math; the interpolation class (Interpolation) covers easing and splines; and three structs (Complex, Polynomial, Monomial) cover the algebraic types.

A few terms appear repeatedly. Epsilon is the tolerance used when comparing floats: two floats are “approximately equal” when their absolute difference is below this value. The Scylla defaults are 1e-6 for float and 1e-12 for double, suitable for most gameplay math. Critically damped describes a spring system tuned so the value approaches its target as quickly as possible without overshooting; the corresponding helper is SmoothDamp. Framerate-independent means the result is the same regardless of frame time: feeding the function Time.deltaTime produces the same trajectory on a 30 fps and a 144 fps machine. Spherical coordinates describe a 3D direction with a radial distance, a polar angle from the positive Y axis, and an azimuthal angle around the Y axis; cylindrical coordinates describe the same direction with a radial distance from the Y axis, an angle around the Y axis, and a Y-axis height; polar coordinates describe a 2D position with a radial distance and an angle. Scylla provides round-trip conversions for all three.

ComponentRoleNotes
NumberUtilStatic class with scalar arithmetic helpers for every primitive numeric typeApproximately, IsNearlyZero, Clamp, Clamp01, IsBetween, Snap, RoundToDecimals, FloorToInt / CeilToInt / RoundToInt, Mod, Repeat, Wrap, Sqr, Cube, SignNonZero, AbsDiff, IsEven, IsOdd, SafeDivide, IsPowerOfTwo, NextPowerOfTwo, AlignDown, AlignUp, plus angle helpers (DegToRad, RadToDeg, NormalizeAngleDeg360/180, DeltaAngleDeg, LerpAngleDeg, NormalizeAngleRad2Pi/Pi, DeltaAngleRad).
NumberExtensionsExtension method companion to NumberUtilSame operations exposed as value.Approximately(other), value.Clamp01(), value.Snap(0.25f), value.Mod(360), value.Wrap(0, 8), etc. Use either form; pick the one that reads better at the call site.
InterpolationEasing curves and spline functions on scalars and Unity vectorsSmoothStep, SmootherStep, LerpSmooth / LerpSmoothStart / LerpSmoothEnd (scalar, Vector2, Vector3), LerpExponential, MoveTowards, Cubic Hermite, Catmull-Rom, EaseInOutCubic.
MathUtilVector / quaternion / camera / smoothing helpersSmoothDamp, ConvergeToValue, Swap, Normalize / Denormalize, ExpN, ToPIRange, ToSpherical / ToCartesian (yaw-pitch), Cartesian / Spherical / Cylindrical / Polar conversions, SnapToGrid, ProjectOnPlane, ClosestPointOnLine, Intersection3Planes, GetNearPlaneCorners, FlattenXZ / FlattenXZRaw, CorrectRotationUp.
ComplexComplex number struct with arithmetic and standard transcendentalsReal / Imaginary fields; Conjugate, Modulus, SqrModulus, Argument; FromPolar, RootOfUnity, Cis factories; Sqrt, Cbrt, Pow; all four arithmetic operators; implicit cast from float. Useful for FFT, signal processing, and 2D rotations expressed as multiplication.
PolynomialPolynomial struct backed by a degree -> coefficient mapVariadic and dictionary constructors; Bernstein basis factory (for Bezier work); Evaluate, GetDerivative, GetAntiderivative, Integrate (definite over [a, b]); GetRoots (closed-form for degree <= 4), GetDiscriminant, NewtonsMethod for higher degrees; full operator overloading with Polynomial, Monomial, and float.
MonomialSingle-term polynomial (coefficient + degree)Construct standalone or via implicit cast from float; combine with operators to build larger polynomials; same Evaluate / Derivative / Integrate surface as Polynomial. Useful when authoring polynomial expressions in code reads more naturally term-by-term than as a coefficient array.

The two scalar classes (NumberUtil and NumberExtensions) cover the same operations from two different call-site shapes. Use NumberUtil.Approximately(a, b) when the namespace is already imported and the variable name is short, or when the operation is in a generic helper. Use a.Approximately(b) when chained with other extension calls or when the method form reads more naturally for a particular variable name. The two forms produce identical results.

Recipes

These recipes cover the entire public surface across the seven types. Each one is self-contained: scan the names, find the one that matches what you are building, and copy the solution. If you are new to the module, start with the float comparison recipe since every other recipe depends on that discipline.

Compare floats safely

Problem. You have a distance check in your pathfinder, a velocity threshold in your movement controller, or a near-zero guard in your physics helper. Anywhere you write a == b on floats, you are setting up a bug that will surface once the values have been through a few multiply-adds and the bit patterns no longer match even though the numbers are “the same”. NumberUtil.Approximately is the canonical fix, and the framework’s code-style rule enforces it over raw == at code review.

Solution. Approximately accepts an optional explicit tolerance so you can tighten it for clean inputs (fresh positions straight from the Inspector) or loosen it for accumulated values (a velocity that has been integrated over 300 frames). IsNearlyZero is the convenience shorthand for the most common case. IsFinite, EnsureFinite, and EnsureFiniteNonNegative guard against NaN (Not a Number) and infinity that have leaked in from a divide-by-zero or an Mathf.Asin called with an out-of-range argument.

using Scylla.Core.Util;

/* Default epsilon (1e-6f). Suitable for typical single-precision drift.
 * Use this for distance checks, velocity thresholds, and most comparison sites. */
if (NumberUtil.Approximately(distance, targetDistance))
{
    /* Arrived. */
}

/* Custom epsilon: tighter for clean inputs, looser for accumulated values. */
bool tight = NumberUtil.Approximately(a, b, epsilon: 1e-8f);
bool loose = NumberUtil.Approximately(a, b, epsilon: 1e-3f);

/* Double overload uses EPSILON_D (1e-12) by default. */
bool ok = NumberUtil.Approximately(doubleA, doubleB);

/* Convenience: "is this float effectively zero?". The canonical check for
 * a stopped rigidbody, an exhausted timer, or a depleted resource pool. */
if (NumberUtil.IsNearlyZero(velocity.x))
{
    velocity.x = 0f; /* snap to a clean zero for downstream logic */
}

/* Finite check (false on NaN, +Inf, -Inf). Use at AI and physics boundaries
 * where a divide-by-distance can produce infinity if the target is exactly here. */
if (!NumberUtil.IsFinite(computed))
{
    Log.Warning("Non-finite result; falling back to default.", LogCategory.Core);
    computed = 0f;
}

/* Guard at API boundaries: throws ArgumentException with the parameter name
 * baked into the message when the value is NaN or infinite. */
float safe       = NumberUtil.EnsureFinite(input,    name: nameof(input));
float safeNonNeg = NumberUtil.EnsureFiniteNonNegative(distance, name: nameof(distance));

/* Extension form. Same operations, different call shape. Both are identical. */
if (distance.Approximately(0f)) { /* ... */ }
if (velocity.x.IsNearlyZero())  { /* ... */ }

Clamp and validate value ranges

Problem. Your damage formula can produce negatives on a heavy debuff, your health bar’s fill amount can exceed 1.0 on an overheal, and your ability cooldown percentage can drift past its bounds after enough delta-time accumulation. You need a clamp that is reliable, covers every numeric type, and catches the argument-order mistake that produces silent garbage when someone writes Clamp(value, 99, 1) instead of Clamp(value, 1, 99).

Solution. Clamp exists for every primitive numeric type (int, long, uint, ulong, float, double, decimal). It throws ArgumentException when min > max – the safety guard most ad-hoc clamps forget. Clamp01 is the common-case shortcut for the [0, 1] range used by lerp and normalized values. IsBetween answers the range-check question with an inclusive switch so the same call works for [min, max] and (min, max).

/* Integer clamp. The ArgumentException fires immediately when min > max,
 * so you catch the argument-order mistake at the call site, not during play. */
int level = NumberUtil.Clamp(rawLevel, 1, 99);

/* Float clamp into [0, 1]. The canonical call for a lerp parameter, a
 * normalized health fraction, or a UI fill amount. */
float healthFill = NumberUtil.Clamp01(rawHP / maxHP);

/* Range check, inclusive by default. Use for ability-activation windows,
 * damage bands that trigger different animations, or boss phase thresholds. */
if (NumberUtil.IsBetween(playerHP, 1, 99))
{
    /* In the band where the boss aura activates. */
}

/* Exclusive form for open intervals (the boundary value does not qualify). */
bool strictlyInside = NumberUtil.IsBetween(value, 0f, 1f, inclusive: false);

/* Argument validation: a min > max call throws ArgumentException at runtime. */
try
{
    int broken = NumberUtil.Clamp(level, 99, 1);
}
catch (ArgumentException ex)
{
    /* Caught: "min cannot be greater than max." */
}

/* Extension form. Reads naturally when chained with other operations. */
int safeLevel  = rawLevel.Clamp(1, 99);
float fraction = rawFraction.Clamp01();
bool inBand    = playerHP.IsBetween(1, 99);

Round and snap values for display and grids

Problem. A lot of UI values look noisy without rounding: a damage number that reads 12.4173 instead of 12, a coordinate readout that wobbles in the third decimal place every frame, an inventory weight that ends in a recurring decimal. And your tile-placement system needs values snapped to the nearest grid cell, not just floored to an integer.

Solution. Rounding to integer comes in three flavours (FloorToInt, CeilToInt, RoundToInt) covering both float and double inputs. RoundToDecimals rounds to a specified number of decimal places, useful for UI displays. Snap rounds to the nearest multiple of a step, useful for grid-style values like inventory weights or coordinate axes.

/* Floor / ceil / round to int. Use FloorToInt for tile coordinates,
 * CeilToInt for "at least N items", RoundToInt for display values. */
int below = NumberUtil.FloorToInt(2.7f);  /* 2 */
int above = NumberUtil.CeilToInt(2.1f);   /* 3 */
int near  = NumberUtil.RoundToInt(2.5f);  /* 2 (banker's rounding on .NET) */

/* Round a float to N decimal places. Useful for UI text or stable hashing.
 * The damage tooltip shows "12.35", not "12.34567431". */
float price = NumberUtil.RoundToDecimals(12.34567f, 2); /* 12.35 */
double sci  = NumberUtil.RoundToDecimals(0.123456789, 4);

/* Decimal overload uses System.Math.Round (no float intermediate). */
decimal money = NumberUtil.RoundToDecimals(12.345m, 2);

/* Snap a value to the nearest multiple of `step`. The canonical grid-placement
 * call: a tile editor that works in 0.25-unit increments, a rhythm game that
 * snaps beat times to the nearest 60 Hz frame boundary. */
float gridX = NumberUtil.Snap(1.7f, step: 0.25f);          /* 1.75 */
double tick = NumberUtil.Snap(seconds, step: 1d / 60d);    /* nearest frame at 60 fps */

/* Extension form. */
float price2 = 12.34567f.RoundToDecimals(2);
float gridX2 = 1.7f.Snap(0.25f);

Remap input ranges to gameplay values

Problem. You are converting a raw gamepad trigger value (0.0 to 1.0) into a throttle percentage (0 to 100), or a world-space distance (say 0 to 50 meters) into a fog density (1.0 to 0.0). These are all the same operation: mapping a value from one range to another, and sometimes clamping the output when the input falls outside the expected band. Plain lerp gets you partway there but you need inverse lerp and remap to complete the picture.

Solution. Lerp blends between two values by a [0, 1] parameter; the unclamped variant accepts any parameter for extrapolation. InverseLerp is the inverse operation: given a value and a range, it returns the [0, 1] parameter that produced it. Remap is the combined form that maps a value from one range to another in a single call. MathUtil.Normalize and MathUtil.Denormalize are the named-parameter spellings of InverseLerp and Lerp when the code reads better with explicit “normalize this raw value” / “denormalize this 0..1 value” intent.

/* Clamped lerp: parameter is clamped to [0, 1] before blending. The safe
 * default for UI fills, health bars, and animation progress. */
float blended = NumberUtil.Lerp(a: 100f, b: 200f, t: 0.25f); /* 125 */

/* Unclamped: parameter can be negative or > 1 (extrapolation). Use when
 * you want to overshoot the range intentionally, e.g. a bounce easing. */
float extrapolated = NumberUtil.LerpUnclamped(a: 100f, b: 200f, t: 1.5f); /* 250 */

/* Inverse lerp: where does `value` fall in [a, b]? Returns a 0..1 fraction.
 * The canonical "how full is this health bar" or "how far through this
 * time window is this event" call. */
float t = NumberUtil.InverseLerp(a: 100f, b: 200f, value: 150f); /* 0.5 */

/* Remap: convert from one range to another in one call. The optional clamp
 * keeps the output inside [outMin, outMax] when the input falls outside
 * [inMin, inMax]. Use for converting a raw input axis to a gameplay curve. */
float throttle = NumberUtil.Remap(
    value:  rawTrigger,
    inMin:  0f, inMax:  1f,
    outMin: 0f, outMax: 100f,
    clamp:  true
);

/* Named-parameter forms when the "normalize" or "denormalize" intent matters.
 * The right call when the variable names in context are "raw value" and
 * "normalized fraction" rather than generic a/b/t. */
float normalized   = MathUtil.Normalize(value: 75f, min: 50f, max: 100f);    /* 0.5 */
float denormalized = MathUtil.Denormalize(normalized: 0.5f, min: 50f, max: 100f); /* 75 */

Wrap values for cyclic ranges and headings

Problem. C#’s % operator returns a result with the sign of the dividend, so -1 % 360 == -1, which is rarely what gameplay code wants. A heading that just crossed 0 degrees from the east should read as 359, not as -1. A cyclic inventory index that decremented past 0 should wrap to the last slot, not to minus one.

Solution. NumberUtil.Mod is true mathematical modulo: the result has the sign of the divisor, so Mod(-1, 360) == 359. Repeat is the [0, length) form for floats (the parameter is the period). Wrap is the integer counterpart for half-open [min, max) ranges.

/* True modulo: always returns a non-negative result for a positive divisor.
 * The canonical fix for a heading angle that crossed 0 during a clockwise turn. */
int posDeg  = NumberUtil.Mod(-30, 360);   /* 330, not -30 */
float t     = NumberUtil.Mod(-0.25f, 1f); /* 0.75 */

/* Repeat over a positive length. Equivalent to Mod for float; named for
 * the "wrap this UV or position around the world" intent. */
float looped = NumberUtil.Repeat(t: -0.25f, length: 1f); /* 0.75 */

/* Integer wrap over a half-open range [minInclusive, maxExclusive). The
 * canonical cyclic inventory index: always a valid slot, even when the
 * player presses "previous" on the first item. */
int cyclic = NumberUtil.Wrap(value: -1, minInclusive: 0, maxExclusive: 8); /* 7 */
uint cyc2  = NumberUtil.Wrap(value: 17u, minInclusive: 0u, maxExclusive: 8u); /* 1 */

/* Extension form. */
int cyclic2 = (-1).Wrap(0, 8); /* 7 */
float t2    = (-0.25f).Repeat(1f);

Work with integer math, powers of two, and bit alignment

Problem. You need the usual small integer helpers that do not quite fit into a standard Unity or .NET API: squares and cubes, a sign that never returns zero, unsigned absolute difference for score comparisons, power-of-two checks for texture sizes and audio buffer allocations, and alignment rounding for memory layout work.

Solution. Sqr and Cube for the obvious shapes, SignNonZero that returns +1 or -1 (never 0 for floats inside the epsilon), AbsDiff for unsigned subtraction without overflow, IsEven / IsOdd for parity, SafeDivide for divide-by-zero protection, and MathUtil.Swap for exchanging two variables. IsPowerOfTwo / NextPowerOfTwo / AlignUp / AlignDown are the bit-twiddling helpers used by memory layouts, mesh chunks, and texture sizes.

/* Squares and cubes. The named form is clearer than multiplying manually,
 * especially inside a larger expression. */
int   area = NumberUtil.Sqr(side);  /* side * side */
float vol  = NumberUtil.Cube(edge);

/* Non-zero sign: returns +1 or -1, never 0 (for floats within EPSILON).
 * Use when the sign drives a direction flip and a zero sign would stall movement. */
int dir = NumberUtil.SignNonZero(velocity.x);

/* Unsigned absolute difference. Avoids the overflow that int subtraction
 * produces when one score is near int.MinValue and the other is near int.MaxValue. */
int diff = NumberUtil.AbsDiff(scoreA, scoreB);

/* Parity. Useful for alternating tile tints in a checkerboard, even/odd frame
 * logic, or "every other enemy in this wave has a shield". */
if (NumberUtil.IsEven(frame))
{
    /* Even-frame logic. */
}

/* Safe division with explicit fallback for the zero case. Use wherever the
 * denominator might legitimately be zero at runtime, e.g. a per-unit ratio
 * when no units are alive yet. */
float perRatio = NumberUtil.SafeDivide(score, total, fallback: 0f);

/* Swap two values without a temp variable. Useful in sort passes and
 * anywhere two refs need to exchange. */
int a = 1, b = 2;
MathUtil.Swap(ref a, ref b); /* a=2, b=1 */

/* Power-of-two checks for texture size validation, mip level counts, and
 * audio buffer allocations that require PO2 sizes. */
bool isPo2 = NumberUtil.IsPowerOfTwo((uint)textureWidth);
uint next  = NumberUtil.NextPowerOfTwo((uint)512); /* 512 (already PO2) */
uint next2 = NumberUtil.NextPowerOfTwo((uint)513); /* 1024 */

/* Alignment. Round up or down to the nearest multiple of `alignment`.
 * Use for memory page alignment and GPU buffer size rounding. */
uint pageAligned = NumberUtil.AlignUp(addr,   alignment: 4096); /* round up to next 4K */
uint pageStart   = NumberUtil.AlignDown(addr, alignment: 4096); /* round down */

Normalize and blend angles

Problem. Angles cause more bugs than any other numeric domain because there are three independent units (degrees / radians / turns), four normalization conventions ([0, 360), [-180, 180], [0, 2pi), [-pi, pi]), and the shortest-path-between-two-angles problem is non-trivial when the values straddle the wrap point. A turret that is aiming at 350 degrees and wants to rotate to 10 degrees should turn 20 degrees clockwise, not 340 degrees counter-clockwise.

Solution. Scylla covers every common angle case explicitly: unit conversion, all four normalization forms, the shortest signed delta between two angles in both degrees and radians, and an angle lerp that always takes the short way around.

/* Unit conversion. */
float rad = NumberUtil.DegToRad(180f);  /* PI */
float deg = NumberUtil.RadToDeg(MathF.PI);

/* Normalize an angle into one of the four canonical ranges. Pick the one
 * your downstream code expects: 360 for headings, 180 for signed deltas,
 * 2pi / pi for radians-based math. */
float in360 = NumberUtil.NormalizeAngleDeg360(720f);  /* 0 */
float in180 = NumberUtil.NormalizeAngleDeg180(270f);  /* -90 */
float in2pi = NumberUtil.NormalizeAngleRad2Pi(7f);    /* 7 - 2pi */
float inPi  = NumberUtil.NormalizeAngleRadPi(4f);     /* 4 - 2pi (which is in [-pi, pi]) */

/* MathUtil.ToPIRange wraps a radians value into [-pi, pi]; available in
 * both return-the-value and ref-in-place forms. */
float r1 = MathUtil.ToPIRange(4f);
float r2 = 4f;
MathUtil.ToPIRange(ref r2);

/* Shortest signed delta between two angles. Always returns a value in
 * [-180, 180] (degrees) or [-pi, pi] (radians). The canonical "which way
 * should the turret rotate" call. */
float delta = NumberUtil.DeltaAngleDeg(current: 350f, target: 10f); /* 20, not -340 */
float dRad  = NumberUtil.DeltaAngleRad(current, target);

/* Interpolate between two angles taking the shortest path around the circle.
 * Use for blending a character's heading or a camera's yaw. */
float blended = NumberUtil.LerpAngleDeg(a: 350f, b: 10f, t: 0.5f); /* 0 or 360, not 180 */

Add easing and splines to movement and transitions

Problem. Anyone who has tried to fake game feel with a plain lerp knows the motion never quite lands right; the curve is too linear, the start and end feel mechanical instead of weighted. A door that slides open at constant velocity reads as a prop; the same motion on a smooth ease-in-out reads as a mechanism. A camera that lerps to its framing target looks floaty; that same camera on a smoothstep feels intentional.

Solution. Interpolation provides the curves that lerp alone cannot produce. SmoothStep is the cubic Hermite blend (the classic ease-in-out); SmootherStep is the quintic version with even smoother derivative behaviour. LerpSmooth / LerpSmoothStart / LerpSmoothEnd apply the smoothstep curve to a normal lerp call, available in scalar, Vector2, and Vector3 flavours. LerpExponential raises t to a power for arbitrary easing shapes. MoveTowards advances toward a target at a fixed speed and snaps when within one step. Cubic is cubic Hermite interpolation through four control points; CatmullRom is the spline variant that passes through p1 and p2 with derivatives derived from p0 and p3.

For fully authored, timeline-driven animations rather than procedural per-frame blends, see Tween.

using Scylla.Core.Util;

/* Smoothstep curve on a [0, 1] parameter. The output starts and ends with
 * zero derivative, producing the classic ease-in-out shape that game feel
 * requires for UI transitions, door slides, and cutscene pans. */
float eased = Interpolation.SmoothStep(t: 0.5f);    /* 0.5 */
float ease2 = Interpolation.SmootherStep(t: 0.5f);  /* 0.5, but with zero second derivative too */

/* Apply the smoothstep to a lerp between two values. The result is a
 * smoothly-eased version of NumberUtil.Lerp(a, b, t). */
float eased1 = Interpolation.LerpSmooth(a: 0f, b: 100f, t: 0.5f); /* 50, but eased */

/* One-sided ease variants. SmoothStart is ease-in only (slow start, fast end);
 * SmoothEnd is ease-out only (fast start, slow settle). Use these for
 * fade-ins, item pickups that accelerate toward the player, and settling
 * UI elements. */
float fadeIn  = Interpolation.LerpSmoothStart(0f, 1f, t);
float fadeOut = Interpolation.LerpSmoothEnd(0f, 1f, t);

/* Vector overloads work componentwise. The right call for easing a
 * camera rig or a UI panel into position. */
Vector3 cameraOffset = Interpolation.LerpSmooth(restPos, focusPos, t);

/* Exponential easing: t^n is the shaping curve. n > 1 gives ease-in
 * (slow start), 0 < n < 1 gives ease-out (fast start), n = 1 is plain lerp. */
float exp = Interpolation.LerpExponential(a: 0f, b: 1f, t: 0.5f, n: 2f); /* t^2 ease-in */

/* MoveTowards: advance at fixed speed; snap when within `speed * deltaTime`.
 * Use for enemy movement that should never overshoot its waypoint. */
float pos = Interpolation.MoveTowards(
    current:   currentPos,
    target:    desiredPos,
    speed:     2f,
    deltaTime: Time.deltaTime
);

/* Vector forms. */
transform.position = Interpolation.MoveTowards(transform.position, targetPos, speed: 3f, Time.deltaTime);

/* Cubic Hermite through four control points. The smooth path through the
 * middle two points, with the outer two setting the tangent at each end. */
Vector3 onCurve = Interpolation.Cubic(y0: p0, y1: p1, y2: p2, y3: p3, t: 0.5f);

/* Catmull-Rom: the spline passes through p1 and p2; p0 and p3 control the
 * tangent at each endpoint. Use four consecutive waypoints to produce a
 * smooth path from waypoint[1] to waypoint[2]. The canonical call for
 * patrol paths and rail-camera movements. */
Vector3 pathPoint = Interpolation.CatmullRom(p0, p1, p2, p3, t: 0.5f);

/* Penner-style ease-in-out cubic with explicit begin/change/duration. */
float anim = Interpolation.EaseInOutCubic(t: elapsed, b: 0f, c: 100f, d: 1f);

/* ExpN is a stand-alone power helper from MathUtil. */
float bend = MathUtil.ExpN(x: 0.5f, n: 3f); /* 0.5^3 = 0.125 */

Smooth-follow a moving target without overshoot

Problem. There is a particular feel in good camera work that comes from the camera easing toward the target rather than snapping or lerping there mechanically. The same pattern shows up in every “track a moving target without overshoot” use case: camera follow, UI value chase, AI gaze tracking, a health bar that drains smoothly toward the current value. A homemade currentPos = Lerp(currentPos, target, dt * k) looks like damping but is not framerate-independent: the same code on 30 fps and 144 fps produces different curves and feels different on different machines.

Solution. MathUtil.SmoothDamp is the critically damped spring that Unity’s own Mathf.SmoothDamp also implements. It is framerate-independent and never overshoots; the caller owns a velocity variable that the function updates in place. MathUtil.ConvergeToValue is the constant-rate counterpart for UI meters where a linear approach reads better than an eased one.

/* Camera follow: SmoothDamp toward the player position.
 * The velocity variable lives on the caller and is updated in place.
 * Do not reset it between calls; it carries the spring state. */
private float _cameraVelocityX;

private void LateUpdate()
{
    float smoothed = MathUtil.SmoothDamp(
        current:         camera.position.x,
        target:          player.position.x,
        currentVelocity: ref _cameraVelocityX,
        smoothTime:      0.3f,                   /* time to roughly reach the target */
        maxSpeed:        float.PositiveInfinity, /* no speed cap */
        deltaTime:       Time.deltaTime
    );

    camera.position = new Vector3(smoothed, camera.position.y, camera.position.z);
}

/* Constant-rate convergence: useful for health bars or UI meters where the
 * eye expects a steady tick rather than an eased decay. A health bar that
 * drains 50 HP per second reads cleaner than one that curves exponentially. */
private float _displayedHP;

private void Update()
{
    _displayedHP = MathUtil.ConvergeToValue(
        target:    player.HP,
        current:   _displayedHP,
        deltaTime: Time.deltaTime,
        speed:     50f /* HP per second */
    );
}

Convert between coordinate systems for spatial queries

Problem. You end up asking the same handful of geometric questions across gameplay code: where this thing sits in spherical coordinates relative to the player, where this ray hits this plane, what the closest point on this line is, where the corners of the camera frustum land in world space. Inline atan2 + acos + sqrt blocks are dense to read and easy to get wrong on edge cases.

Solution. MathUtil covers the routine geometric questions: yaw / pitch direction round-trips, full coordinate-system conversions (Cartesian / Spherical / Cylindrical / Polar in 2D and 3D), grid snapping in 2D and 3D, projecting a point onto a plane, finding the closest point on a line, intersecting three planes, and a Gram-Schmidt orthogonalization fix for a quaternion that has accumulated roll.

using Scylla.Core.Util.Math;

/* Direction <-> yaw/pitch (azimuth / elevation in degrees). The convention
 * matches Unity's Quaternion.Euler ordering: yaw rotates around Y, pitch
 * rotates around X. Use for AI line-of-sight checks and radar overlays. */
MathUtil.ToSpherical(direction: transform.forward, out float yaw, out float pitch);
Vector3 dir = MathUtil.ToCartesian(yaw, pitch);

/* Full Cartesian <-> Spherical (radial / theta / phi). Inputs and outputs
 * are both Vector3; the spherical form is interpreted as (r, theta, phi). */
Vector3 spherical = MathUtil.CartesianToSpherical(cartesian: position);
Vector3 back      = MathUtil.SphericalToCartesian(spherical);

/* Cartesian <-> Cylindrical (radial / theta / y). Useful for orbiting
 * cameras and circular volumes around a vertical axis. */
Vector3 cyl  = MathUtil.CartesianToCylindrical(position);
Vector3 cart = MathUtil.CylindricalToCartesian(cyl);

/* 2D polar conversions in both Vector3 (z = 0) and Vector2 forms. */
Vector2 polar2D = MathUtil.CartesianToPolar(cartesian: new Vector2(1f, 1f));
Vector2 back2D  = MathUtil.PolarToCartesian(polar2D);
Vector3 back3D  = MathUtil.PolarToCartesian3D(polar2D);

/* Closest point on an infinite line (not a segment - the result may lie
 * outside the [lineStart, lineEnd] span). For the "nearest point on a
 * patrol path segment" query, parameterize and clamp to [0, 1]. */
Vector3 foot = MathUtil.ClosestPointOnLine(
    point:     queryPos,
    lineStart: a,
    lineEnd:   b
);

/* Project a point onto a plane. Use to flatten an enemy position onto the
 * ground plane for a minimap, or to find where a physics object will rest. */
Vector3 onPlane = MathUtil.ProjectOnPlane(
    point: pos,
    plane: new Plane(normal: Vector3.up, point: Vector3.zero)
);

/* Snap a position to a grid. The result is the centre of the cell that
 * contains the position. The right primitive for a tile-placement editor
 * or a tactics-game movement grid. */
Vector3 snapped3 = MathUtil.SnapToGrid(position: pos, gridSize: 1f);
Vector2 snapped2 = MathUtil.SnapToGrid(position: pos2, gridSize: 0.5f);

/* Intersect three planes. Returns null when the planes are parallel or
 * coincident (degenerate intersection). The common use is finding the
 * corner of a camera frustum from three bounding planes. */
Vector3? intersection = MathUtil.Intersection3Planes(p0: top, p1: side, p2: front);
if (intersection.HasValue)
{
    /* Use the corner point. */
}

/* The four corners of a camera's near clipping plane in world space.
 * Order: top-left, top-right, bottom-right, bottom-left. */
Vector3[] corners = MathUtil.GetNearPlaneCorners(Camera.main);

/* Flatten a 3D vector to the XZ plane. FlattenXZ normalizes; FlattenXZRaw
 * preserves the original magnitude. Use for top-down movement directions
 * and billboard orientation. */
Vector3 horizDir = MathUtil.FlattenXZ(velocity);     /* unit XZ direction, or zero */
Vector3 horizVel = MathUtil.FlattenXZRaw(velocity);  /* y=0, magnitude preserved */

/* Correct a rotation that has drifted off-axis. Forward is preserved; up is
 * realigned to world up via Gram-Schmidt orthogonalization. The fix for a
 * character controller that has accumulated roll through physics. */
Quaternion drifted = Quaternion.Euler(15f, 30f, 45f); /* has roll */
MathUtil.CorrectRotationUp(ref drifted);              /* roll removed in place */

Work with complex numbers for signal processing

Problem. Most gameplay code never touches complex numbers, but signal processing (FFT-based audio analysis, filter design for procedural sound) and certain procedural-generation techniques cannot do without them. You need a value-type that carries real and imaginary parts, supports the full arithmetic operator set, and provides the standard transcendentals – all without a heap allocation per operation.

Solution. Complex is a value-type complex number with two float fields, full operator overloading, the standard transcendentals (Sqrt, Cbrt, Pow), and factories for polar and roots-of-unity construction. For noise-based procedural content, see Noise and Procedural Map Gen; Complex is for the lower-level numeric work those systems build on.

using Scylla.Core.Util.Math;

/* Construct directly or from polar coordinates. */
var z = new Complex(real: 3f, imaginary: 4f); /* 3 + 4i */
var p = Complex.FromPolar(r: 5f, theta: MathF.PI / 4f);
var c = Complex.Cis(theta: MathF.PI / 6f); /* unit complex at 30 degrees */

/* Implicit cast from float. */
Complex real = 2.5f;

/* Constants. */
Complex zero  = Complex.Zero;
Complex one   = Complex.One;
Complex i     = Complex.I;

/* Roots of unity: the kth root of unity for n. Use for FFT butterfly passes. */
Complex root4 = Complex.RootOfUnity(n: 4, k: 1); /* i */

/* Properties. */
float re   = z.Real;
float im   = z.Imaginary;
float r    = z.Modulus;          /* sqrt(re^2 + im^2) */
float rsq  = z.SqrModulus;       /* re^2 + im^2 (cheaper if magnitude is not needed) */
float arg  = z.Argument;         /* atan2(im, re) */
Complex zc = z.Conjugate;        /* 3 - 4i */

bool isReal = z.IsReal;
bool isImag = z.IsPurelyImaginary;

/* Static accessors. */
float reS = Complex.Re(z);
float imS = Complex.Im(z);
float abs = Complex.Abs(z);

/* Transcendental functions. */
Complex sqrt = Complex.Sqrt(z);
Complex cbrt = Complex.Cbrt(z);
Complex pow  = Complex.Pow(z, p: 0.5f);

/* Operator overloads: +, -, *, / between two Complex; * and / between a
 * Complex and a float; comparison; implicit float -> Complex.
 * Multiplying by Complex.I rotates a 2D vector 90 degrees - a compact
 * alternative to a 2x2 rotation matrix for top-down games. */
var sum  = z + new Complex(1f, 0f);
var prod = z * Complex.I; /* rotate by 90 degrees */
var div  = z / new Complex(0f, 1f);

if (z == new Complex(3f, 4f))
{
    /* Equal. */
}

Author and solve polynomial curves

Problem. Polynomials show up wherever a curve needs to be evaluated, differentiated, integrated, or solved: a bullet trajectory with non-linear drag, an ability damage curve authored as a cubic, a CRPG’s experience-to-level formula. You want a type that lets you write the curve symbolically in code, evaluate it on demand, and get its derivative for free without re-deriving the formula by hand.

Solution. Polynomial is a struct backed by a sparse degree-to-coefficient map. Monomial is the single-term form (coefficient plus integer degree). Both expose Evaluate, GetDerivative, GetAntiderivative, and Integrate (definite over [a, b]); Polynomial additionally has GetRoots (closed-form for degrees 1 through 4), GetDiscriminant, and NewtonsMethod for higher degrees. Operator overloads cover arithmetic between Polynomial, Monomial, and float so authoring expressions in code reads naturally.

using Scylla.Core.Util.Math;

/* Construct via variadic coefficients in highest-degree-first order.
 * The example below is 2x^3 + 3x - 1. */
var p = new Polynomial(2f, 0f, 3f, -1f);

/* Construct via a degree-keyed dictionary for sparse polynomials. Useful
 * for a damage formula with a very high-degree term and a constant. */
var sparse = new Polynomial(new Dictionary<int, float>
{
    { 5, 1f },  /* x^5 */
    { 0, -2f }  /* + constant */
});

/* Construct from monomial terms; useful when authoring symbolic expressions.
 * The formula for "experience to next level" as an explicit sum of terms. */
var xpCurve = new Polynomial(new[]
{
    new Monomial(coefficient: 1f,  degree: 2), /* x^2 */
    new Monomial(coefficient: -3f, degree: 1), /* -3x */
    new Monomial(coefficient: 2f,  degree: 0)  /* +2 */
});

/* Bernstein basis polynomials: the i-th of degree n, used as the basis
 * for Bezier curves. */
Polynomial bernstein = Polynomial.Bernstein(i: 2, n: 4);
Polynomial[] basis   = Polynomial.BernsteinBasis(n: 4); /* full degree-4 basis */

/* Constant and identity. */
Polynomial zero = Polynomial.Zero;

/* Read or write a coefficient at a specific degree. */
float a3 = p[3]; /* coefficient of x^3 */

/* Inspect shape. */
int  deg    = p.Degree;
bool isEven = p.IsEven; /* only even-degree non-zero terms */
bool isOdd  = p.IsOdd;

/* Evaluate at a point. The canonical "what is the damage at this attack
 * power level" or "what is the XP threshold at this character level" call. */
float y = p.Evaluate(x: 2f); /* 2 * 8 + 3 * 2 - 1 = 21 */

/* Derivative and antiderivative. GetDerivative gives the rate-of-change
 * curve; useful for finding the steepest point on a damage ramp or the
 * zero-crossing of an acceleration profile. */
Polynomial deriv = p.GetDerivative();           /* 6x^2 + 3 */
Polynomial anti  = p.GetAntiderivative();       /* 0.5x^4 + 1.5x^2 - x + C, C = 0 */

/* Definite integral over [a, b]. Use for "total XP accumulated between
 * level a and level b" or "total impulse over this animation keyframe span". */
float area = p.Integrate(a: 0f, b: 1f);

/* Find roots. Returns a float[] with all real roots; closed-form for
 * degrees 1, 2, 3, 4. For higher degrees, use NewtonsMethod with an
 * initial guess. */
float[] roots = xpCurve.GetRoots();          /* { 1, 2 } for x^2 - 3x + 2 */
float disc    = xpCurve.GetDiscriminant();   /* > 0 for two real roots */

if (p.NewtonsMethod(out float root, initialGuess: 0f, delta: 1e-4f, maxIterations: 50))
{
    /* Converged. */
}

/* Operator arithmetic: + - * / between Polynomial and Polynomial, Monomial,
 * or float. Implicit conversion from float and Monomial keeps expressions
 * readable. */
var combined = p + 2f * xpCurve - new Monomial(0.5f, 4);

/* Build a polynomial term-by-term using monomial arithmetic. */
var built = new Monomial(1f, 2) + new Monomial(-3f, 1) + new Monomial(2f, 0);

/* Monomial Evaluate / Derivative / Integrate work the same way. */
var    m      = new Monomial(coefficient: 2f, degree: 3); /* 2x^3 */
float  y2     = m.Evaluate(x: 2f);                        /* 16 */
Monomial dm   = m.GetDerivative();                        /* 6x^2 */
float  defInt = m.Integrate(a: 0f, b: 1f);               /* 0.5 */

/* ToString with a custom variable name for inspection or generated source. */
string text = p.ToString(variableName: "t"); /* "2t^3 + 3t - 1" */

API Sketch

The seven public types split cleanly by responsibility. NumberUtil and NumberExtensions cover scalar arithmetic; Interpolation covers easing and splines; MathUtil covers vectors, quaternions, and spatial geometry; Complex, Polynomial, and Monomial are the algebraic types. Operator overloads are listed compactly.

namespace Scylla.Core.Util
{
    public static class NumberUtil
    {
        public const float  EPSILON   = 1e-6f;
        public const double EPSILON_D = 1e-12;

        /* Float comparison and finite checks. */
        public static bool Approximately(float a, float b, float epsilon = EPSILON);
        public static bool Approximately(double a, double b, double epsilon = EPSILON_D);
        public static bool IsNearlyZero(float value, float epsilon = EPSILON);
        public static bool IsNearlyZero(double value, double epsilon = EPSILON_D);
        public static bool IsFinite(float value);
        public static bool IsFinite(double value);
        public static float  EnsureFinite(float value, string name);
        public static double EnsureFinite(double value, string name);
        public static float  EnsureFiniteNonNegative(float value, string name);
        public static double EnsureFiniteNonNegative(double value, string name);

        /* Clamp / range. */
        public static int     Clamp(int value, int min, int max);
        public static long    Clamp(long value, long min, long max);
        public static uint    Clamp(uint value, uint min, uint max);
        public static ulong   Clamp(ulong value, ulong min, ulong max);
        public static float   Clamp(float value, float min, float max);
        public static double  Clamp(double value, double min, double max);
        public static decimal Clamp(decimal value, decimal min, decimal max);
        public static float   Clamp01(float value);
        public static double  Clamp01(double value);
        public static bool    IsBetween(int value, int min, int max, bool inclusive = true);
        /* + IsBetween overloads for long / uint / ulong / float / double / decimal */

        /* Rounding and snapping. */
        public static int     FloorToInt(float value);
        public static int     FloorToInt(double value);
        public static int     CeilToInt (float value);
        public static int     CeilToInt (double value);
        public static int     RoundToInt(float value);
        public static int     RoundToInt(double value);
        public static float   RoundToDecimals(float value, int decimals);
        public static double  RoundToDecimals(double value, int decimals);
        public static decimal RoundToDecimals(decimal value, int decimals);
        public static float   Snap(float value, float step);
        public static double  Snap(double value, double step);
        public static decimal Snap(decimal value, decimal step);

        /* Lerp / InverseLerp / Remap. */
        public static float  Lerp           (float a, float b, float t);
        public static double Lerp           (double a, double b, double t);
        public static float  LerpUnclamped  (float a, float b, float t);
        public static double LerpUnclamped  (double a, double b, double t);
        public static float  InverseLerp    (float a, float b, float value);
        public static double InverseLerp    (double a, double b, double value);
        public static float  InverseLerpUnclamped(float a, float b, float value);
        public static double InverseLerpUnclamped(double a, double b, double value);
        public static float  Remap(float value, float inMin, float inMax, float outMin, float outMax, bool clamp = false);
        public static double Remap(double value, double inMin, double inMax, double outMin, double outMax, bool clamp = false);

        /* Modular arithmetic and wrapping. */
        public static int     Mod(int value, int m);
        public static long    Mod(long value, long m);
        public static float   Mod(float value, float m);
        public static double  Mod(double value, double m);
        public static decimal Mod(decimal value, decimal m);
        public static float   Repeat(float t, float length);
        public static double  Repeat(double t, double length);
        public static int     Wrap(int value, int minInclusive, int maxExclusive);
        public static long    Wrap(long value, long minInclusive, long maxExclusive);
        public static uint    Wrap(uint value, uint minInclusive, uint maxExclusive);
        public static ulong   Wrap(ulong value, ulong minInclusive, ulong maxExclusive);

        /* Integer arithmetic helpers. */
        public static float   SafeDivide(float numerator, float denominator, float fallback = 0f);
        public static double  SafeDivide(double numerator, double denominator, double fallback = 0d);
        public static decimal SafeDivide(decimal numerator, decimal denominator, decimal fallback = 0m);
        public static int     Sqr(int value);  /* + long / float / double / decimal */
        public static int     Cube(int value); /* + long / float / double / decimal */
        public static int     SignNonZero(int value);   /* + long / float (eps) / double (eps) */
        public static int     AbsDiff(int a, int b);    /* + long / uint / ulong */
        public static bool    IsEven(int value);        /* + long / uint / ulong */
        public static bool    IsOdd (int value);        /* + long / uint / ulong */

        /* Angles. */
        public static float DegToRad(float degrees);
        public static float RadToDeg(float radians);
        public static float NormalizeAngleDeg360(float angle);
        public static float NormalizeAngleDeg180(float angle);
        public static float DeltaAngleDeg(float current, float target);
        public static float LerpAngleDeg(float a, float b, float t);
        public static float NormalizeAngleRad2Pi(float angle);
        public static float NormalizeAngleRadPi(float angle);
        public static float DeltaAngleRad(float current, float target);

        /* Power of two and alignment. */
        public static bool  IsPowerOfTwo(uint value);
        public static bool  IsPowerOfTwo(ulong value);
        public static uint  NextPowerOfTwo(uint value);
        public static ulong NextPowerOfTwo(ulong value);
        public static uint  AlignDown(uint value, uint alignment);
        public static ulong AlignDown(ulong value, ulong alignment);
        public static uint  AlignUp(uint value, uint alignment);
        public static ulong AlignUp(ulong value, ulong alignment);
    }

    public static class NumberExtensions
    {
        /* Extension-method form of the NumberUtil surface. */
        public static int    Clamp(this int value, int min, int max);
        public static float  Clamp01(this float value);
        public static bool   IsBetween(this float value, float min, float max, bool inclusive = true);
        public static bool   Approximately(this float value, float other, float epsilon = NumberUtil.EPSILON);
        public static bool   IsNearlyZero(this float value, float epsilon = NumberUtil.EPSILON);
        public static float  RoundToDecimals(this float value, int decimals);
        public static float  Snap(this float value, float step);
        public static int    Mod(this int value, int m);
        public static float  Repeat(this float value, float length);
        public static int    Wrap(this int value, int minInclusive, int maxExclusive);
        /* + overloads for long / uint / ulong / double / decimal */
    }

    public static class Interpolation
    {
        /* Hermite and Catmull-Rom splines through four control points. */
        public static Vector3 Cubic     (Vector3 y0, Vector3 y1, Vector3 y2, Vector3 y3, float t);
        public static Vector3 CatmullRom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t);

        /* Penner-style cubic ease in/out (begin, change, duration). */
        public static float EaseInOutCubic(float t, float b, float c, float d);

        /* Smoothstep curves. */
        public static float SmoothStep   (float t);
        public static float SmootherStep (float t);

        /* Eased lerps (scalar, Vector2, Vector3). */
        public static float   LerpSmooth     (float a, float b, float t);
        public static float   LerpSmoothEnd  (float a, float b, float t);
        public static float   LerpSmoothStart(float a, float b, float t);
        public static Vector2 LerpSmooth     (Vector2 a, Vector2 b, float t);
        public static Vector2 LerpSmoothEnd  (Vector2 a, Vector2 b, float t);
        public static Vector2 LerpSmoothStart(Vector2 a, Vector2 b, float t);
        public static Vector3 LerpSmooth     (Vector3 a, Vector3 b, float t);
        public static Vector3 LerpSmoothEnd  (Vector3 a, Vector3 b, float t);
        public static Vector3 LerpSmoothStart(Vector3 a, Vector3 b, float t);

        /* Exponential ease: t^n on a normal lerp. */
        public static float LerpExponential(float a, float b, float t, float n);

        /* Constant-speed move with snap on arrival (scalar, Vector2, Vector3). */
        public static float   MoveTowards(float   current, float   target, float speed, float deltaTime);
        public static Vector2 MoveTowards(Vector2 current, Vector2 target, float speed, float deltaTime);
        public static Vector3 MoveTowards(Vector3 current, Vector3 target, float speed, float deltaTime);
    }
}

namespace Scylla.Core.Util.Math
{
    public static class MathUtil
    {
        /* Scalar shape helpers. */
        public static float ExpN          (float x, float n);
        public static float Normalize     (float value, float min, float max);
        public static float Denormalize   (float normalized, float min, float max);
        public static void  ToPIRange     (ref float angle);
        public static float ToPIRange     (float angle);

        /* Yaw / pitch direction round-trips. */
        public static void    ToSpherical (Vector3 direction, out float yaw, out float pitch);
        public static void    ToCartesian (float yaw, float pitch, out Vector3 direction);
        public static Vector3 ToCartesian (float yaw, float pitch);

        /* Full coordinate-system conversions. */
        public static Vector3 CartesianToCylindrical(Vector3 cartesian);
        public static Vector3 CylindricalToCartesian(Vector3 cylindrical);
        public static Vector3 CartesianToSpherical  (Vector3 cartesian);
        public static Vector3 SphericalToCartesian  (Vector3 spherical);
        public static Vector2 CartesianToPolar      (Vector3 cartesian);
        public static Vector2 CartesianToPolar      (Vector2 cartesian);
        public static Vector3 PolarToCartesian3D    (Vector2 polar);
        public static Vector2 PolarToCartesian      (Vector2 polar);

        /* Smoothing and convergence. */
        public static float ConvergeToValue(float target, float current, float deltaTime, float speed);
        public static float SmoothDamp(float current, float target, ref float currentVelocity,
            float smoothTime, float maxSpeed, float deltaTime);

        /* Generic swap. */
        public static void Swap<T>(ref T a, ref T b);

        /* Vector operations. */
        public static Vector3 FlattenXZ   (Vector3 v); /* normalized; zero on near-vertical */
        public static Vector3 FlattenXZRaw(Vector3 v); /* y = 0, magnitude preserved */

        /* Rotation fix. */
        public static void CorrectRotationUp(ref Quaternion rotation);

        /* Camera and geometry. */
        public static Vector3[] GetNearPlaneCorners(Camera camera);
        public static Vector3?  Intersection3Planes(Plane p0, Plane p1, Plane p2);
        public static Vector3   SnapToGrid       (Vector3 position, float gridSize);
        public static Vector2   SnapToGrid       (Vector2 position, float gridSize);
        public static Vector3   ProjectOnPlane   (Vector3 point, Plane plane);
        public static Vector3   ClosestPointOnLine(Vector3 point, Vector3 lineStart, Vector3 lineEnd);
    }

    public struct Complex : IEquatable<Complex>
    {
        public static readonly Complex Zero;
        public static readonly Complex One;
        public static readonly Complex I;

        public float Real;
        public float Imaginary;

        public Complex Conjugate         { get; }
        public float   Modulus           { get; }
        public float   SqrModulus        { get; }
        public float   Argument          { get; }
        public bool    IsReal            { get; }
        public bool    IsPurelyImaginary { get; }

        public Complex(float real, float imaginary);

        public static Complex FromPolar(float r, float theta);
        public static Complex RootOfUnity(int n, int k = 1);
        public static Complex Cis(float theta);

        public static float   Re(Complex z);
        public static float   Im(Complex z);
        public static float   Abs(Complex z);
        public static Complex Sqrt(Complex z);
        public static Complex Sqrt(float f);
        public static Complex Cbrt(Complex z);
        public static Complex Pow(Complex z, float p);

        /* Operators: == != + - * / (Complex,Complex / Complex,float); implicit float -> Complex. */
    }

    public struct Polynomial : IEnumerable<Monomial>, IEquatable<Polynomial>
    {
        public static Polynomial Zero { get; }

        public int  Degree { get; }
        public bool IsEven { get; }
        public bool IsOdd  { get; }

        public float this[int degree] { get; set; }

        public Polynomial(params float[] coefficients);
        public Polynomial(IDictionary<int, float> coefficients);
        public Polynomial(IEnumerable<Monomial> terms);

        public static Polynomial   Bernstein     (int i, int n);
        public static Polynomial[] BernsteinBasis(int n);
        public static Polynomial   Expand        (Polynomial p, uint power);

        public float       Evaluate         (float x);
        public Polynomial  GetDerivative    ();
        public Polynomial  GetAntiderivative();
        public float       Integrate        (float a, float b);
        public float[]     GetRoots         ();
        public float       GetDiscriminant  ();
        public bool        NewtonsMethod    (out float root, float initialGuess = 0f, float delta = 0.01f, int maxIterations = 30);

        public override string ToString();
        public string ToString(string variableName);

        /* Operators: == != + - * / between Polynomial, Monomial, and float;
         * implicit float -> Polynomial; implicit Monomial -> Polynomial. */
    }

    public struct Monomial : IEquatable<Monomial>
    {
        public float Coefficient;
        public int   Degree;

        public Monomial(float coefficient, int degree);

        public float    Evaluate         (float x);
        public Monomial GetDerivative    ();
        public Monomial GetAntiderivative();
        public float    Integrate        (float a, float b);

        public override string ToString();
        public string ToString(string variableName);

        /* Operators: == != + - * / between Monomial and Monomial / float;
         * implicit float -> Monomial; arithmetic with two Monomials may
         * return a Polynomial when degrees differ. */
    }
}

See the API reference for full signatures, remarks, and exception contracts on every member.

Constants and defaults reference

The four pieces of named state that callers can rely on without reading the source:

ConstantValuePurpose
NumberUtil.EPSILON1e-6fDefault tolerance for Approximately / IsNearlyZero on float. Tight enough to catch real differences, loose enough to absorb typical single-precision drift after a small number of operations. Increase for accumulated values.
NumberUtil.EPSILON_D1e-12Default tolerance for double comparison helpers. Suitable for double-precision math retaining about 12 significant decimal digits.
Polynomial.NewtonsMethod defaultsinitialGuess = 0f, delta = 0.01f, maxIterations = 30Newton iteration converges quickly for smooth functions near a root; raise maxIterations for ill-conditioned polynomials or supply an initialGuess close to the expected root.
Polynomial.GetRoots reachdegrees 1-4Closed-form solutions for linear, quadratic, cubic, and quartic. Degree-5 and higher polynomials must use NewtonsMethod or external solvers.

Best Practices

  • Use Approximately for every float comparison. Raw == between floats is a bug waiting to happen; the framework’s code-style rule catches it, and so does ReSharper. The default EPSILON = 1e-6 covers typical drift; specify a custom epsilon when comparing accumulated values or values from a different precision domain.
  • Use Mod (not %) when the dividend may be negative. C#’s % operator returns the sign of the dividend, so -1 % 360 == -1. NumberUtil.Mod returns the mathematical modulo with the sign of the divisor, giving the expected 359. The difference matters for cyclic indices, angle normalization, and time-rewind logic.
  • Use LerpAngleDeg for blending headings. Plain Lerp(350, 10, 0.5) returns 180; the angle-aware variant returns 0 (or 360), which is what gameplay code wants when crossing the wrap point.
  • Prefer SmoothDamp over manual lerp for target-tracking. A homemade currentPos = Lerp(currentPos, target, dt * k) looks like damping but is not framerate-independent: the same code on 30 fps and 144 fps produces different curves. SmoothDamp is critically damped, framerate-independent, and never overshoots.
  • Own the velocity field for SmoothDamp. The velocity state lives between calls; declare it as a private field on the caller and pass it by ref on every call. Resetting it to zero each frame produces discontinuous motion.
  • Pick the right ease for the visual intent. SmoothStep / SmootherStep for symmetric ease-in-out, LerpSmoothStart for slow start (fade-in), LerpSmoothEnd for slow stop (settling), LerpExponential with n > 1 for emphasized ease-in, MoveTowards for snap-on-arrival, SmoothDamp for spring-style follow. Picking the wrong curve is the most common cause of “feels off” gameplay.
  • Clamp throws on min > max; do not catch the exception, fix the call site. The throw is a fail-fast guard against a programming mistake (often a swapped argument order). Catching it hides the bug; reading the call and correcting the order solves it once.
  • Cache SmoothDamp velocity per-target. Camera tracking one entity uses one velocity; tracking three uses three. Sharing a single velocity field between unrelated targets cross-pollinates the spring state and produces visible jitter.
  • Use the MathUtil coordinate conversions over ad-hoc trig. CartesianToSpherical, CartesianToCylindrical, and CartesianToPolar (and inverses) are tested, branch-safe, and named for intent. Inline atan2 + acos + sqrt blocks are dense to read and easy to get wrong on edge cases.
  • ClosestPointOnLine is for infinite lines. For segment-style queries (the foot may not lie on the [a, b] span), parameterize the result and clamp to [0, 1] before reconstructing the position.
  • Use MathUtil.GetNearPlaneCorners for frustum work. Manually computing the four corners requires the camera’s near plane distance, FOV (field of view), and aspect; the helper handles all three correctly and respects the camera’s transform.
  • Reach for Polynomial.GetRoots when the degree is 1 to 4. The closed-form solutions are exact (within float precision); Newton’s method is iterative and may converge slowly or to the wrong root. Use the closed-form first and fall back to NewtonsMethod only when the degree exceeds 4.
  • Use Complex for rotation composition when it reads better than quaternions. A 2D rotation by theta is multiplication by Cis(theta); chaining rotations is multiplication; the inverse is the conjugate. For 2D-only gameplay (top-down or side-scrolling), Complex is often clearer than Quaternion and faster than a Matrix2x2.

Pitfalls