Math.md
June 2, 2026 ยท View on GitHub
A wide variety of game math.
Functions
lerp(a, b, t),smoothstep([a, b, ]t), andsmootherstep([a, b, ]t)implement the common game interpolation functions.smoothstepandsmootherstepoptionally take just atvalue (implicitly usinga=0andb=1).inv_lerp(a, b, x)performs the inverse oflerp:lerp(a, b, inv_lerp(a, b, x) == x.fract(f)gets the fractional part off. Negative values wrap around; for examplefract(-0.1) == 0.9within floating-point error.saturate(x)is equal toclamp(x, 0, 1).square(x)is equal tox*x.typemin_finite(T)andtypemax_finite(T)are an alternative to Julia'stypemin(T)andtypemax(T)that returns finite values for float types.round_up_to_multiple(v, multiple)rounds an integer (orVec{<:Integer}) up to the next multiple of some other integer (orVec{<:Integer}). For example,round_up_to_multiple(7, 5) == 10.wraparound(a, b, x)wraps the valuexaround the range[a, b). It behaves well whena==b, by just returninga. It also provides the same output when swappingaandb, except whenx==band it wraps around toa.solve_quadratic(a, b, c)returns eithernothingor the two solutions to the quadratic equationax^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)::Tperforms linear interpolation; it defaults tolerp()and also usesq_slerpfor quaternions.curve_print(io::IO, x::T)optionally customizes how the type is printed inside a curve's printout. It defaults toprint.
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:
CurveLinearSlopelinearly interpolates from the current keyframe to the next.CurveExponentialSlopecurves the time (liket = pow(t, exponent)) before linearly interpolating.CurveBezierSlopecurves the time using a Bezier Curve with control points at either end, for exampleCurveBezierSlope(-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))::Fgets the point along the ray (as itstscalar 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 oftvalues.
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
centerandsize, 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, thecentervalue is put on the max side of the range.
- Construct the bounding box of some points and boxes (and other
AbstractShapetypes) withboundary(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 functionBase.size())center(box)corners(box)calculates a tuple of all2^Ncorner points.
Some other box/interval functions (again, not including the AbstractShape interface) are:
is_empty(box)::Boolreturns whether the box's volume is <= 0is_inside(box, point)::Boolreturns 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)::Boolchecks whether the first box totally contains the second.Base.intersect(boxes::Box...)::Boxgets the intersection of one or more boxes. You can callis_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)::Fbounds(s)::Box{N, F}(Box is a specific type ofAbstractShape, described below)center(s)::Vec{N, F}is_touching(s, p::Vec{N, F})::Boolclosest_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-randomVec3within the unit sphere, given three uniform-random values.- To generate the three values from an RNG, call
rand_in_sphere([rng][, F = Float32]).
- To generate the three values from an RNG, call
perlin(pos::Union{Real, Vec})generates N-dimensional Perlin noise.- This function is highly customizable; refer to its doc-string.