Math.md

June 2, 2026 ยท View on GitHub

A wide variety of game math.

Functions

  • lerp(a, b, t), smoothstep([a, b, ]t), and smootherstep([a, b, ]t) implement the common game interpolation functions. smoothstep and smootherstep optionally take just a t value (implicitly using a=0 and b=1).
  • inv_lerp(a, b, x) performs the inverse of lerp: lerp(a, b, inv_lerp(a, b, x) == x.
  • fract(f) gets the fractional part of f. Negative values wrap around; for example fract(-0.1) == 0.9 within floating-point error.
  • saturate(x) is equal to clamp(x, 0, 1).
  • square(x) is equal to x*x.
  • typemin_finite(T) and typemax_finite(T) are an alternative to Julia's typemin(T) and typemax(T) that returns finite values for float types.
  • round_up_to_multiple(v, multiple) rounds an integer (or Vec{<:Integer}) up to the next multiple of some other integer (or Vec{<:Integer}). For example, round_up_to_multiple(7, 5) == 10.
  • wraparound(a, b, x) wraps the value x around the range [a, b). It behaves well when a == b, by just returning a. It also provides the same output when swapping a and b, except when x==b and it wraps around to a.
  • solve_quadratic(a, b, c) returns either nothing or the two solutions to the quadratic equation ax^2 + bx + c = 0.

Vectors, Matrices, Quaternions

These objects are described in separate documents.

Curves

A Curve{T} is an animated value of type T over some time range. They can automatically add some Perlin noise to make each instance of the curve organic and randomized. They can also be looped when reading from them.

Curves are broken into CurveKey{T}s, which are specific points the curve passes through. Each key has a timestamp, value, and slope towards the next keyframe.

Curves are constructed with either a Vector of the keys, or individual keys as arguments. It can be evaluated at a time t with curve_eval(curve, t, perlin_seeds_tuple = (0x1, ); looping::Bool=false).

If you modify a curve after construction, call curve_sanitize!(c) to keep its keyframes in chronological order. A Dear ImGUI editor for Curve{T} is planned but not implemented.

Curve-able data

Any type T can be put in a Curve, as long as it implements the following interface:

  • curve_coord_lerp(a::T, b::T, t::Float32)::T performs linear interpolation; it defaults to lerp() and also uses q_slerp for quaternions.
  • curve_print(io::IO, x::T) optionally customizes how the type is printed inside a curve's printout. It defaults to print.

Keyframes

A CurveKey{T} can be constructed with a time Float32, value, and a slope (which defaults to linear). If the value is scalar or Vec, then you can also provide the time + value as a single Vec where the time is the X axis (and YZ... are the value).

The slope describes the transition from this keyframe to the next. If the curve does not loop around, then the last keyframe's slope is meaningless.

Slopes

Slopes are of the union type CurveSlope. Each keyframe (CurveKey{T}) holds the slope that will be used to carry it to the next keyframe (meaning the last one's slope is useless if the curve does not loop). The following kinds of slopes are supported:

  • CurveLinearSlope linearly interpolates from the current keyframe to the next.
  • CurveExponentialSlope curves the time (like t = pow(t, exponent)) before linearly interpolating.
  • CurveBezierSlope curves the time using a Bezier Curve with control points at either end, for example CurveBezierSlope(-2, 2) will have the value meander away from the end keyframe initially, and end by overshooting it and doubling back.

Note that slopes are defined entirely as modifications of the interpolant t; that way they are agnostic as to what kind of data is being interpolated through. This also means that the curve's output never takes on a value outside the original curve, no matter how strong the noise gets.

Perlin noise

When constructing a Curve, you can define how much variation it can have via Perlin noise. Then, when calling curve_eval(curve, t), you can provide a tuple of RNG seeds as a third argument! Make sure to use the same seed every time you evaluate the curve to actually get the organic Perlin smoothness.

The perlin noise is configured with the struct CurvePerlin. The noise can fade in at the start of the curve, and fade out at the end, with a certain duration and exponential curve (e.g. curve of 2 means a steep dropoff towards the ends of the curve). The scale of the noise is defined in t units; for example a scale of 2 means that the Perlin noise has a gradient at t=0,2,4,....

If the curve is looping, then the window only happens near the beginning of the curve (t=curve.keyframes[1].point[1]), and the perlin noise itself does not loop.

Rays

Ray{N, F<:AbstractFloat} defines an N-dimensional ray. You can construct it with a start position and direction vector.

Aliases exist for dimensionality: Ray2D{F}, Ray3D{F}, Ray4D{F}.

Aliases also exist assuming Float3: Ray2, Ray3, Ray4.

  • ray_at(ray, t) gets the point on the ray at the given distance from the origin (proportional to the ray direction's magnitude).
  • closest_point(ray, pos; min_t=0, max_t=typemax_finite(F))::F gets the point along the ray (as its t scalar value) which is closest to the given position. By default, limits the output to be >= 0 like a true ray, but you can change the allowed range of t values.

Ray intersection calculations can be found in the Shapes section of this document.

Box and Interval

Contiguous spaces can be represented by Box{N, T}, and contiguous number ranges represented by Interval{T}. All Box functions are also implemented by Interval, using scalar data instead of vector data.

Box is part of the AbstractShape system (see the Shapes section), but this section focuses on the more general utility of Box (and Interval).

Like Vec, boxes have many aliases based on type and dimensionality. For example, Box2Du, Box4Df, BoxT. Intervals have aliases for common types: IntervalI, IntervalU, IntervalF, and IntervalD.

They can be constructed in several ways:

  • Provide two named arguments from: min, max (inclusive), size, center. For example, Box2Df(min=v2f(0, 0), max=v2f(1.5, 2.2)).
    • One of the parameters may be a scalar, which represents the same value for every component. If the Box's type parameters have been provided then both parameters may be a scalar.
    • If providing a center and size, you can also explicitly state whether to use integer division (Int) or float division (Float) to get the half-size. For example: Box(@ano_value(Int), center=Vec(2, 3), size=Vec(5, 5)). By default it'll use integer division for integer parameters and float division for float prameters. If using integer division and an even size, the center value is put on the max side of the range.
  • Construct the bounding box of some points and boxes (and other AbstractShape types) with boundary(elements...).
  • Construct a box covering a VecRange: Box(v2i(1, 2) : v2i(5, 5)).
  • Create a box with the min and size set to 0 by constructing it with only its type arguments. For example, Box2Di().

You can get the properties of a box with the following getters:

  • min_inclusive(box)
  • max_exclusive(box)
  • max_inclusive(box) (only really useful for integer-type boxes)
  • min_exclusive(box) (only really useful for integer-type boxes)
  • size(box) (a new overload of the built-in function Base.size())
  • center(box)
  • corners(box) calculates a tuple of all 2^N corner points.

Some other box/interval functions (again, not including the AbstractShape interface) are:

  • is_empty(box)::Bool returns whether the box's volume is <= 0
  • is_inside(box, point)::Bool returns whether the point is inside the box, not touching its edges. This is only really useful for integer-type boxes.
  • contains(outer_box, inner_box)::Bool checks whether the first box totally contains the second.
  • Base.intersect(boxes::Box...)::Box gets the intersection of one or more boxes. You can call is_empty(result) to check if there is no intersection.
  • Base.reshape(box, new_dimensionality::Integer; ...) removes or adds dimensions to the box. Added dimensions have a specific min and size (specified in named parameters).

Integer boxes and intervals can be used to slice into a multidimensional array. For example, myArr[Box(min=v3i(3, 4, 5), max=v3i(5, 5, 5))] is equivalent to myArr[3:5, 4:5, 5:5]. You can also get a box covering an array's integer indices with box_indices(a, I=Int).

Boxes can be serialized and deserialized. Deserialization can come from any pair of constuctor parameters: min, max, size, center. For example, you can deserialize a Box2Df from this JSON string using the JSON3 package: "{ "min": [ 1, 2 ], "size": [5, 5] }". The max parameter is inclusive.

Shapes

AbstractShape{N, F} is some N-dimensional shape using numbers of type F. Its interface is as follows:

  • volume(s)::F
  • bounds(s)::Box{N, F} (Box is a specific type of AbstractShape, described below)
  • center(s)::Vec{N, F}
  • is_touching(s, p::Vec{N, F})::Bool
  • closest_point(s, p::Vec{N, F})::Vec{N, F}
  • intersections(s, r::Ray{N, F}; ...)::UpTo{M, F}
  • collides(a::AbstractShape{N, F}, b::AbstractShape{N, F})::Bool
    • Important note: mostly unimplemented as of time of writing

Important note: shapes are still under development, so significant chunks of it are untested or unimplemented, and the API is a work-in-progress.

Ray intersections

The interface for ray intersections is a bit wonky (and, as mentioned above, still under development).

By default, intersections(shape, ray) returns UpTo{N, F} intersections. If you also want the surface normal at the closest intersection, pass a third parameter Val(true). In this case, it instead returns a Tuple{UpTo{N, F}, Vec3{F}}.

Box

Box, as mentioned above, is a particularly useful shape. For more info on the rest of its interface, unrelated to AbstractShape, see the linked section. Note that Interval does not implement AbstractShape.

Contiguous

Contiguous{T} is an alias for any nested container of T that appears to be contiguous in memory. For example, a Vector{v2i} is both a Contiguous{Int32} and a Contiguou{v2i}.

The number of T in a Contiguous{T} is found with contiguous_length(data, T).

Contiguous data can be turned into a Ref for interop using contiguous_ref(values, T, idx=1). It can also be turned directly into a pointer with coniguous_ptr(values, T, idx=1).

Random Generation

  • rand_in_sphere(u, v, r) generates a uniform-random Vec3 within the unit sphere, given three uniform-random values.
    • To generate the three values from an RNG, call rand_in_sphere([rng][, F = Float32]).
  • perlin(pos::Union{Real, Vec}) generates N-dimensional Perlin noise.
    • This function is highly customizable; refer to its doc-string.