Skip to content

DUznanski/vornmath

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

56 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vornmath

Vorn's Lua Math Library

Vornmath is (will be) a comprehensive vector and complex math library for lua. It works on lua versions 5.1 through 5.4 as well as luajit.

Installing and Using

Vornmath is a single file, pure Lua library; it does not add any symbols to global. Just put vornmath.lua somewhere convenient in your source tree and do

local vm = require('vornmath')

You don't even need to bring a license file alongside, there's one onboard!

Basic concepts

Returns and outvars

All Vornmath functions that return objects of types that are made of tables - so anything that would return a vector, matrix, complex, or quaternion - accept out variables. The out variable has to be the same type; the object merely gets its fields filled in. The object is also returned when an outvar is provided; because there are some cases where giving varying types to a function may only sometimes result in a type where an out variable actually successfully changes its target, it is a good idea to not only pass the out variable but also assign the result to the same place:

result_vec = vm.add(left_vec, right_vec, result_vec)

For functions like atan where some parameters are optional, the outvar is still in the same position, with nil taking the place of those optional things.

result = vm.atan(angle, nil, result)

Generic and specific forms

All functions in vornmath come in two forms: one in which it is generic and accepts arguments of any types that are valid for the function, and one in which the types being passed in are already specified as part of the name. For instance, the multiplication function mul can accept a great variety of types for its inputs, so it has many signatures, some of which are:

mul
mul_number_number_nil
mul_vec3_cvec3_nil
mul_mat4_number_mat4

When passing an outvar to a specific-form function, the out variable's type is part of the signature, and so is included in the name.

The specified ones have one particular advantage: they are a little faster because they don't do additional function calls to perform dispatch into the correct function. On the other hand they do have long annoying names and cannot accept varying types.

In situations where a function has one signature that is a prefix of another, the shorter signature will include a nil:

  • atan_number_nil accepts a single number, and
  • atan_number_number accepts two.

The presence or absence of nil in the signature can be annoying to remember; it is generally a better idea to use vm.utils.bake to find the correct function.

All of these are in the vornmath object. Or would be, except...

Laziness

Functions in Vornmath do not exist prior to being named. This is mostly because there are a lot of functions, and building every single one would make the code huge: fill has over ten million signatures!

So instead, we use some objects called bakeries to describe the patterns that functions fall into and construct these functions the first time they are used. Because of this, examining the vornmath table will not actually name every function available. You will have to rely on this documentation, or examine the bakeries. Technical info about bakeries is down below.

Creating objects

to create an object, call its constructor.

local a = vm.complex() -- creates a complex number, initialized to 0+0i.
local b = vm.complex(3) -- creates a complex number, initialized to 3+0i.
local c = vm.complex(2,-5) -- creates a complex number, initialized to 2-5i.
local d = vm.complex(c) -- creates a duplicate of c.

fill

If you already have an object and wish to replace its contents completely, use fill. Usually, this edits the underlying object, but if the object in question is a number or otherwise immutable, it won't be able to change it and you'll instead get a fresh object anyway. to ward off bad consequences of this, only use fill on things not used elsewhere, and when you use fill, assign the result of fill to a value!

fill can be used in any way that a constructor can be used: if you have a constructor for an object, you can replace that constructor with fill and just put the object you want to fill in as the first argument.

local a = vm.complex(1,2) -- a is 1+2i
local b = vm.fill(a,3,4) -- a is now 3+4i, and b is the same object as a. 

Operators

Operators exist! They use the most generic form of the function because they can't be relied upon to be called on a particular object in the chain.

Most importantly, however, due to limitations in the way lua implements ==, it is not possible to make a thing that compares non-number objects to numbers! if you expect to be comparing a number and some other type (say, because you want to see if it's 0), you will have to use vm.eq instead.

Types

number

vm.number() --> 0
vm.number(n) --> n
vm.number(str, [base]) --> string as number in given base (default 10)

this is Lua's built-in number type. There's not much to say about it!

The interpretation of strings as numbers comes directly from Lua.

complex

vm.complex() --> 0 + 0i
vm.complex(a) --> a + 0i
vm.complex(a, b) --> a + bi
vm.complex(a + bi) --> a + bi

Complex numbers, of the form a + bi. They have fields a, the real part, and b, the imaginary part. Some functions (in particular logarithms and sqrt) will behave slightly differently for complex numbers than regular numbers: these functions have some values in which no real answer is possible, and so will not work when given a number, but will when given a complex of equivalent value. In addition, complex numbers do not have a natural ordering, so < and its friends will not work even on real-valued complexes.

quaternion

vm.quat() --> 0 + 0i + 0j + 0k
vm.quat(a) --> a + 0i + 0j + 0k
vm.quat(a,b,c,d) --> a + bi + cj + dk
vm.quat(a+bi) --> a + bi + 0j + 0k
vm.quat(a+bi, c+di) --> a + bi + cj + dk
vm.quat(a+bi+cj+dk) --> a + bi + cj + dk
vm.quat(vm.vec3(b, c, d), angle) --> cos(angle/2) + sin(angle/2)*(bi + cj + dk) 
vm.quat(a+bi, vm.vec3(c, d, e)) --> a + b * (ci + dj + ek)
vm.quat(vm.vec3(...), vm.vec3(...))

Higher dimensional complex numbers, of the form a + bi + cj + dk. Fields a, b, c, and d access the various components. Somehow, many things that work with complex numbers also work with quaternions! (I know, I was surprised too) However, quaternion multiplication is non-commutative: if x and y are quaternions, then x * y and y * x usually give different results.

The two vector constructor produces the shortest rotation that takes the first vector to the second.

The axis-angle, complex-axis, and two-vector constructors all expect (but neither enforce nor convert to) a unit vector; you might get unexpected results if you pass something else.

boolean

vm.boolean() --> false
vm.boolean(x) --> x

This is Lua's built-in boolean type. Not much to say about it either!

vectors

vm.vec2() --> <0, 0>
vm.vec3(a) --> <a, a, a>
vm.cvec4(vm.vec2(a,b), vm.cvec3(c,d,e)) --> <complex(a), complex(b), c, d>
vm.bvec2() --> <false, false>
vm.vec4({a,b,c,d}) --> <a, b, c, d>

Vectors. There are actually 9 vector types: vec2, vec3, and vec4 are 2-, 3-, and 4-dimensional vectors with numbers as components; cvec2, cvec3, and cvec4 use complex numbers, and bvec2, bvec3, and bvec4 use booleans.

Vectors are indexed numerically, starting at 1.

The general constructor for a vector can take any number of scalar, vector, or matrix arguments for which the numeric type is convertible to the vector's type and which together provide enough components to completely fill the vector so long as the last component of the vector lands in the last argument.

Swizzling

In addition to numeric indices, vectors can be indexed via swizzles, strings of letters that describe a list of indices.

There are three alphabets for swizzling: xyzw (best for position), rgba (best for color), and stpq (best for parametric coordinates). They cannot be mixed.

Swizzles can be used for both reading and writing.

local v = vm.vec3(1,2,3)
v.x --> 1
v.bg --> <3,2>
v.sp = vm.vec2(4,5) --> v = <4,2,5>

This functionality can also be accessed as a function, which allows outvars. For this, the swizzle string is included as part of the name of the function.

These functions always use the xyzw alphabet.

local out = vm.vec2()
swizzleReadx(v) --> 1
out = swizzleReadyx(v, out) --> out = <2,1>
swizzleWritezy(v, vm.vec2(6,7)) --> v = <4,7,6>

matrices

vm.cmat2() --> [[1+0i,0+0i], [0+0i,1+0i]]
vm.mat3(a) --> [[a,0,0], [0,a,0], [0,0,a]]
vm.mat2x3(a,b,c,d,e,f) --> [[a,b,c], [d,e,f]]
vm.mat3(vm.mat2x3(a,b,c,d,e,f)) --> [[a,b,c], [d,e,f], [0,0,1]]
vm.mat3(vm.quat(...)) --> rotation matrix
vm.mat4(vm.quat(...)) --> rotation matrix
vm.mat3x2({a,b,c,d,e,f}) --> [[a,b], [c,d], [e,f]]

Matrices. There's 18 of these! They can use numbers or complexes, can be 2 to 4 columns, and can be 2 to 4 rows. Like vectors, a letter before mat describes the type of number it stores (nothing for numbers, c for complex), and the number(s) after it describe its size: columns first, then x, then rows. mat2x4 is a matrix with two columns and four rows, filled with numbers; cmat3x2 is a matrix with three columns and two rows, filled with complex numbers. Square matrices, with the same number of rows as columns, have shorter aliases: mat4 is equivalent to mat4x4, cmat3 is equivalent to cmat3x3. When used in function signatures, always use the longer name, not the alias.

Matrices are indexed numerically by column, starting at 1; each column is a vector in its own right.

The matrix constructor will fill any blank spaces in the result with 0 except for entries on the diagonal which will receive 1.

The general constructor can take any number of scalar or vector (not matrix!) arguments which together provide enough components to completely fill the matrix so long as the last component of the matrix lands in the last argument.

The quaternion constructors produce a 3d rotation matrix; the mat4 version simply augments it with the identity so it works with the larger matrix.

Functions

Operators

The various operators can be accessed through their function names, and have their signatures included to skip dispatch, or can be used directly as operators.

add (a + b)

a + b --> a + b
vm.add(a, b[, c]) --> c = a + b

Domain: number, number => number, complex, complex => complex, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, scalar, matrix, matrix, scalar, matrix, matrix

Addition! If applied to a vector and a scalar, or a matrix and a scalar, or two vectors of the same size, or two matrices of the same size, it operates componentwise: 3 + vec3(5, 6, 7) => vec3(8, 9, 10), for instance.

sub (a - b)

a - b --> a - b
vm.sub(a, b[, c]) --> c = a - b

Domain: number, number => number, complex, complex => complex, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, scalar, matrix, matrix, scalar, matrix, matrix

Subtraction! Just like addition, but using the negation of the second argument.

unm (-a)

-a --> -a
vm.unm(a[, b]) --> b = -a

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector, matrix

Unary negation! Works on all numeric types.

mul (a * b)

a * b --> a * b
vm.mul(a, b[, c]) --> c = a * b 

Domain: number, number => number, complex, complex => complex, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, scalar, matrix, matrix, scalar

Special: vector, matrix => vector, matrix, vector => vector, matrix, matrix => matrix, quat, vec3 => vec3

Multiplication! If applied to a vector and a scalar, or a matrix and a scalar, or two vectors of the same size, it operates componentwise, just like addition.

If applied to a matrix and a vector or two matrices, it performs linear algebraic multiplication: each entry of the result takes the matching row of the left operand and the matching column of the right operand, multiplies them together component wise, and takes the sum. In order for this to work, the column count of the left operand and row count of the right operand must be the same: for matrices this means that they must follow the pattern matαxβ * matγxα = matγxβ, where the greek letters are replaced by numbers. Using a vector as the left operand acts like a row vector matαx1, and as the right operand acts like a column vector mat1xα. If you need componentwise matrix multiplication, see matrixCompMult. If you need algebraic multiplication of two vectors, see dot to get a scalar or outerProduct to get a matrix.

Multiplying a quat by a vec3 results in the vector rotated by the quat.

div (a / b)

a / b --> a / b
vm.div(a, b[, c]) --> c = a / b

Domain: number, number => number, complex, complex => complex, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, scalar, matrix, matrix, scalar, matrix, matrix

Division! Uses the same rules as addition. For quaternions, non-commutative multiplication technically means there are two different forms of division: Vornmath uses p * (1/q), sometimes called right division.

mod (a % b)

a % b --> a % b
vm.div(a, b[, c]) --> c = a % b

Domain: number, number => number

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, scalar, matrix, matrix, scalar, matrix, matrix

Modulus! Gives the remainder of division, p/q - floor(p/q).

pow (a ^ b)

a ^ b --> a ^ b
vm.pow(a, b[, c]) --> c = a ^ b

Domain: number, number => number, complex, complex => complex, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, scalar, matrix, matrix, scalar, matrix, matrix

Exponentiation! Some things that are illegal in real numbers will work when done in complex numbers: -1 ^ 0.5 is undefined in real numbers but complex(-1) ^ 0.5 works and gives i. pow does not work on matrices at all.

eq (a == b and a ~= b)

a == b --> a == b
vm.eq(a, b) --> a == b

Domain: boolean, boolean => boolean number, number => boolean, complex, complex => boolean, quat, quat => boolean, vector, vector => boolean, matrix, matrix => boolean

Equality! Works on anything; will return true if all elements are equal. For differing number types, will implicitly convert to the necessary type, so vm.eq(5, complex(5,0)) is true. If you want componentwise comparison of vectors, see equal.

WARNING: using the symbolic equals == on number and a type other than number doesn't work correctly and will always return false, due to limitations in Lua's metatable system. Instead, use eq if you really need to do that.

tostring

vm.tostring(a) --> a string representation of a

Domain: anything => string

Technically this isn't an operator, but it is a thing that gets a metamethod. Turns a thing into a string! The representations provided by this are not valid Lua code: they're designed to be reasonable to look at.

Trigonometric functions

All trigonometric functions act componentwise on vectors. Angles are always assumed to be in radians unless otherwise specified.

rad

vm.rad(angle_in_degrees[, x]) --> x = angle in radians

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Converts angle values from degrees to radians.

deg

vm.deg(angle_in_radians[, x]) --> x = angle in degrees

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Converts angle values from radians to degrees.

sin

vm.sin(phi[, x]) --> x = sin(phi)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the sine of the given angle.

cos

vm.cos(phi[, x]) --> x = cos(phi)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the cosine of the given angle.

cis

vm.cis(phi[, z]) --> z = cos(phi) + i * sin(phi)

Domain: number => complex

Componentwise: scalar, vector

Computes the cis function, $\text{cis} \theta = \cos \theta + i \sin \theta = e^{i\theta}$; if $z = \text{cis} \theta$, then $|z| = 1$ and $\arg z = \theta$.

tan

vm.tan(phi[, x]) --> x = tan(phi)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the tangent of the given angle.

asin

vm.asin(phi[, x]) --> x = asin(phi)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the inverse sine or arcsine of the given value. For real inputs, will return an angle between 0 and π.

acos

vm.acos(phi[, x]) --> x = acos(phi)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the inverse cosine or arccosine of the given angle. For real inputs, will return an angle between -π/2 and π/2.

atan

vm.atan(y[, nil, phi]) --> phi = angle
vm.atan(y, x[, phi]) --> phi = angle

Domain: number, nil => number, complex, nil => complex, quat, nil => quat, number, number => number, complex, complex => complex, quat, quat => quat

Componentwise: scalar, nil, vector, nil, scalar, scalar, vector, vector

Computes the inverse tangent or arctangent of the given value. For numbers, optionally accepts two parameters such that vm.atan(y, x) will give the correct angle across the whole circle, equivalent to atan2. the out variable is the third parameter for this function because of this. For real inputs, will return an angle between -π/2 and π/2 for the single-input version, or an angle between -π and π for the two-input version.

sinh

vm.sinh(x[, y]) --> y = sinh(x)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the hyperbolic sine of the given value.

cosh

vm.cosh(x[, y]) --> y = cosh(x)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the hyperbolic cosine of the given value.

tanh

vm.tanh(x[, y]) --> y = tanh(x)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the hyperbolic tangent of the given value.

asinh

vm.asinh(x[, y]) --> y = asinh(x)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the inverse hyperbolic sine of the given value.

acosh

vm.acosh(x[, y]) --> y = acosh(x)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the inverse hyperbolic cosine of the given value.

atanh

vm.atanh(x[, y]) --> y = atanh(x)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the inverse hyperbolic tangent of the given value.

polarComplex

vm.polarComplex(r, theta[, z]) --> z = r cis(theta)

Domain: number, number => complex

Componentwise: scalar, vector

Produces a complex number with a given absolute value $r$ and argument $\theta$: $\text {polarComplex}(r, \theta) = r \text {cis}\theta = re^{i\theta}$

polarVec2

vm.polarVec2(r, theta[, v]) --> v = r * <sin(theta), cos(theta)>

Domain: number, number => vec2

Produces a vec2 with the given magnitude and direction.

cylindricalVec3

vm.cylindricalVec3(r, theta, z[, v]) --> v = <r * sin(theta), r * cos(theta), z>

Domain: number, number, number => vec3

Produces a cartesian vec3 from cylindrical coordinates.

`sphericalVec3

vm.sphericalVec3(r, theta, phi[, v])
--> v = <r * sin(theta) * cos(phi), r * cos(theta) * cos(phi), z * sin(phi)>

Produces a cartesian vec3 from spherical coordinates.

Exponential functions

All these functions act componentwise on vectors.

exp

vm.exp(x[, y]) --> y = e^x

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, scalar, vector, vector

Computes the exponential function e^z.

exp2

vm.exp2(x[, y]) --> y = 2^x

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the base-2 exponential function 2^z.

log

vm.log(x[, nil, y]) --> y = ln x
vm.log(x, b[, y]) --> y = log_b x

Domain: number, nil => number, complex, nil => complex, quat, nil => quat, number, number => number, complex, complex => complex, quat, quat => quat

Componentwise: scalar, nil, vector, nil, scalar, scalar, vector, vector

Computes the logarithm. For single-argument calls, this is the natural log. The second argument changes the base: vm.log(8,2) = 3 because 2^3 = 8.

log2

vm.log2(x[, y]) --> y = log_2 x

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the base-2 logarithm.

log10

vm.log10(x[, y]) --> y = log_10 x

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the base-10 logarithm.

sqrt

vm.sqrt(x[, y]) --> y = sqrt(x)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the square root. Fails if given a negative number; given a negative real complex or quat it will produce some positive multiple of $i$. All numbers (other than zero) have two distinct candidates for their square root; this function produces the one with a positive real part.

inversesqrt

vm.inversesqrt(x[, y]) --> y = 1 / sqrt(x)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the inverse square root, the reciprocal of the square root.

hypot

vm.hypot(x, y[, z]) --> z = sqrt(|x^2| + |y^2|)

Domain: number, number => number, complex, complex => number, quat, quat => number

Componentwise: scalar, scalar, vector, vector

Gives the length of the hypotenuse of a right triangle with legs length x and y. Uses the absolute value to prevent silly results in complexes and quaternions.

Complex and Quaternion functions

All these act componentwise on vectors.

arg

vm.arg(a+bi[, x]) --> x = atan(b, a)

Domain: number => number, complex => number, quat => number

Componentwise: scalar, vector

Computes the argument or phase of a complex number, the angle the complex number makes with the positive real line. Also works on regular numbers and quaternions.

conj

vm.arg(a+bi[, z]) --> z = a-bi
vm.arg(a+bi+cj+dk[, z]) --> z = a-bi-cj-dk

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Computes the conjugate of a complex number or quaternion, which is the same number except with all the signs on the complex parts switched.

This works on matrices as well as vectors.

axisDecompose

vm.axisDecompose(a+bi+cj+dk[, cpx, axis]) --> ...
-- local l = sqrt(b^2 + c^2 + d^2)
-- cpx = a + li
-- axis = <b, c, d> / l

Domain: quat => complex, vec3

decomposes a quaternion into a complex number and a unit axis. These can in turn be fed back into vm.quat to reconstruct the original quaternion.

Common functions

All these act componentwise on vectors.

abs

vm.abs(x[, y]) --> y = |x|

Domain: number => number, complex => number, quat => number

Componentwise: scalar, vector

Returns the absolute value, the positive real number with the same magnitude as the number given.

sqabs

vm.sqabs(x[, y]) --> y = |x|^2

Domain: number => number, complex => number, quat => number

Componentwise: scalar, vector

Returns the square of the absolute value.

copysign

vm.copysign(sign, mag[, result]) --> |result| = |mag|, has same sign as sign

Domain: number, number => number

Componentwise: scalar, scalar, vector, vector

Copys the sign of sign onto mag.

sign

vm.sign(x, result) --> result = x/abs(x)

Domain: number => number, complex => complex, quat => quat

Componentwise: scalar, vector

Returns a value with magnitude 1 that has the same sign as x, unless x is 0, in which case returns 0. Also works on complexes and quaternions, giving values with the same argument and vector as x. Notably this means that all results of sign are unit except for when the input is 0.

floor

vm.floor(x[, y]) --> y <= x < y + 1; y is integer

Domain: number => number

Componentwise: scalar, vector

Computes the floor, the highest integer that is at most x.

ceil

vm.ceil(x[, y]) --> y - 1 < x <= y; y is integer

Domain: number => number

Componentwise: scalar, vector

Computes the ceiling, the lowest integer that is at least x.

trunc

vm.trunc(x[, y]) -- 0 <= y <= x < y + 1 or y - 1 < x <= y <= 0; y is integer

Domain: number => number

Componentwise: scalar, vector

Truncates a number, removing any fractional part; selects the nearest integer towards 0.

round

vm.round(x[, y]) -- |x - y| <= 0.5; y is integer

Domain: number => number

Componentwise: scalar, vector

Rounds a number to the nearest integer. If the fractional part of x is exactly 0.5, rounds up. This is somewhat faster than roundEven, but has a slight bias.

roundEven

vm.roundEven(x[, y]) -- |x - y| <= 0.5; y is integer

Domain: number => number

Componentwise: scalar, vector

Rounds a number to the nearest integer. If the fractional part of x is exactly 0.5, rounds to the nearest even number. This is somewhat slower than round, but is not biased.

fract

vm.fract(x[, y]) --> y = x - trunc(x)

Domain: number => number

Componentwise: scalar, vector

Gives the fractional part of x, with the same sign as x. Equivalent to the second return value of modf.

modf

vm.modf(x[, whole, fractional]) --> whole + fractional = x

Domain: number => number, number

Componentwise: scalar, vector

Separates a number into whole and fractional parts. Both parts have the same sign as the original number, so this works as truncating division instead of the usual flooring division.

fmod

vm.fmod(x, y[, remainder]) --> remainder of division

Domain: number, number => number

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector

Gets the remainder of division such that the quotient takes the sign of the numerator; this is different from % where it takes the sign of the denominator.

min

vm.min(x, y[, result]) --> smaller of x and y

Domain: number, number => number

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector

Finds the minimum of the two inputs. Unlike math.min, this only accepts two inputs!

max

vm.max(x, y[, result]) --> larger of x and y

Domain: number, number => number

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector

Finds the maximum of the two inputs. Unlike math.max, this only accepts two inputs!

clamp

vm.clamp(x, lo, hi) --> min(max(x, lo), hi)

Domain: number, number, number => number

Componentwise: scalar, scalar, scalar, vector, scalar, scalar, vector, vector, vector

Finds the closest value to x that's also between lo and hi inclusive.

mix

vm.mix(a, b, t[, r]) --> r = (1-t)*a + t*b
vm.mix(a, b, flags[, r]) --> r[i] = b[i] if flags[i] is true, a[i] otherwise

Domain: number, number, number => number, complex, complex, complex => complex, quat, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, vector

Linear or boolean interpolation: if t is a scalar or non-boolean vector, it will do $(1-t)a + tb$ componentwise. If instead it's a boolean vector, it will select between a and b based on truth value; this helps to avoid problems with NaNs and infinities messing with results in cases where that is possible. Also called "lerp".

unmix

vm.unmix(a, b, r[, t]) --> t = (r - a) / (b - a)

Domain: number, number, number => number, complex, complex, complex => complex, quat, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, vector

The inverse of linear interpolation: $\text{mix}(a, b, \text{unmix}(a, b, r)) = r$.
Also called "inverse lerp".

geometricMix

vm.geometricMix(a, b, t[, r]) --> r = a^(1-t) * b^t

Domain: number, number, number => number, complex, complex, complex => complex, quat, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, vector

Geometric interpolation: $\text{geometricMix}(a,b,t) = a^{1-t}b^t$.

For unit complexes and quaternions, this is often called "slerp". If you want to do spherical interpolation between individual vectors, use the function actually called slerp.

geometricUnmix

vm.geometricUnmix(a, b, x[, r]) --> r = (log(x) - log(a)) / (log(b) - log(a))

The inverse of geometric interplation: $\text{geometrixMix}(a, b, \text{geometricUnmix}(a, b, r)) = r$.

decay

vm.decay(a, b, t[, r]) --> r = mix(b, a, 2^-t)

Domain: number, number, number => number, complex, complex, complex => complex, quat, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, vector

Exponential decay: move from a toward b, slowing down exponentially; t is the number of half-lives moved.

step

vm.step(edge, x[, r]) --> r = 0 if x < edge, 1 otherwise

Domain: number, number => number

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector

Gives 0 for x values smaller than edge and 1 for equal or larger values.

smoothStep

vm.smoothStep(lo, hi, x[, r]) --> cubic easing from 0 to 1 as x goes from lo to hi

Domain: number, number, number => number

Componentwise: scalar, scalar, scalar, vector, vector, scalar, vector, vector, vector

Cubic easing from lo to hi:

$$\begin{aligned} t &= \text{clamp}\left(\frac{x-lo}{hi-lo},0,1\right)\\ r &= 3t^2-2t^3 \end{aligned}$$

isnan

vm.isnan(x) --> true if x is NaN.

Domain: number => boolean, complex => boolean, quat => boolean

Componentwise: scalar, vector

check for NaN values; if applied to a complex or quat will be true if any component is NaN.

isinf

vm.isinf(x) --> true if x is infinite.

Domain: number => boolean, complex => boolean, quat => boolean

Componentwise: scalar, vector

check for infinite values; if applied to a complex or quat will be true if any component is infinite.

fma

vm.fma(a, b, c[, r]) --> r = a * b + c

Domain: number, number, number => number, complex, complex, complex => complex, quat, quat, quat => quat

Componentwise: scalar, scalar, scalar, vector, vector, vector

Fused multiply-add. This exists for compatibility: it doesn't do anything special as far as precision or operation count goes.

frexp

vm.frexp(x[, mantissa, exponent]) --> mantissa * 2 ^ exponent = x

Domain: number => number, number

Componentwise: scalar, vector

Separates a number into a mantissa with absolute value in 0.5 <= x < 1 and an exponent such that mantissa * 2 ^ exponent = x.

ldexp

vm.ldexp(mantissa, exponent[, x]) --> x = mantissa * 2 ^ exponent

Domain: number, number => number

Componentwise: scalar, scalar, vector, vector

puts a number separated via frexp back together.

Vector functions

length

vm.length(v) --> ||v||

Domain: vecα => number, cvecα => number

Returns the length of a vector. For complex vectors, this uses the absolute value, because using straight squaring will cause lengths of some non-zero vectors to be 0, which is not desirable.

distance

vm.distance(a,b) --> ||b - a||

Domain: vecα, vecα => number, cvecα, cvecα => number

Finds the distance between two points. Equivalent to vm.length(b-a).

dot

vm.dot(a, b[, r]) --> r = a · b

Domain: vecα, vecα => number, cvecα, cvecα => complex

Finds the dot product of the two vectors. For complex numbers, this takes the conjugate of b: without this, a · a could be zero and that's not great.

cross

vm.cross(a, b[, r]) --> r = a × b

Domain: vec3, vec3 => vec3, cvec3, cvec3 => cvec3

Finds the cross product of the two vectors. Unlike dot this doesn't take the conjugate because it turns out fine.

minComponent

vm.minComponent(a) --> min(unpack(a))

Finds the smallest component of a vector.

maxComponent

vm.minComponent(a) --> max(unpack(a))

Finds the largest component of a vector.

normalize

vm.normalize(a[, r]) --> r = a / ||a||

Domain: vecα => vecα, cvecα => cvecα

Computes a vector in the same direction as the input, but with length 1. For zero vectors, returns a vector full of NaN.

homogeneousNormalize

vm.homogeneousNormalize(a[, r]) --> r = a/a[last] if a[last] ~= 0; a/length(a) otherwise 

Domain: vecα => vecα, cvecα => cvecα

Computes a vector in the same direction as the input, but either

  1. with last component 1, or
  2. with length 1 if the last component is 0.

This makes the result vector normalized homogeneous coordinates.

hesseNormalize

vm.hesseNormalize(a[, r]) --> r = a / length(demote(a))

Domain: vecα => vecα, cvecα => cvecα, α > 2

Computes a vector in the same direction as the input, but with all but the last component forming a unit vector. This makes the vector usable as a line/plane in Hesse normal form.

cubeNormalize

vm.cubeNormalize(a[, r]) --> r = a / maxComponent(abs(a))

Domain: vecα => vecα, cvecα => cvecα

Computes a vector in the same direction as the input, but with the highest magnitude of any one component as 1.

This places the vector on a cube of edge length 2 around the origin.

faceForward

vm.faceForward(n, i, nref[, r]) --> r = -n * sign(dot(i, nref))

Domain: vecα, vecα, vecα => vecα

Gives -n or n depending on whether nref is in the same or opposite direction as i.

reflect

vm.reflect(i, n[, r]) --> r = i - 2 * dot(n, i) * n

Domain: vecα, vecα => vecα

gives the direction of the resultant ray after reflecting an incident ray with direction i off a surface with normal n. i and n must both be unit vectors for this to work correctly.

refract

vm.refract(i, n, eta[, r]) --> r = ...complicated

Domain: vecα, vecα, number => vecα

gives the direction of the resultant ray after refracting an incident ray with direction i through a surface with normal n and ratio (after / before) of indices of refraction eta. if eta > 1 and the angle of incidence is high enough, it is possible for the result to be total internal reflection: in this case, the function returns a zero vector.

Both n and i must be unit vectors for this to work correctly.

The actual formula for refraction is

$$\begin{aligned} k &= 1 - \eta^2\left(1-\left(n\cdot i\right)^2\right)\\ r &= \begin{cases} 0 &k < 0\\ \eta i - \left(\eta n\cdot i + \sqrt k\right)n&\text{otherwise} \end{cases} \end{aligned}$$

slerp

vm.slerp(a, b, t[, r]) --> theta = acos(dot(normalize(a), normalize(b)))
-- r = a * sin(theta*(1-t))/sin(theta) + b * sin(theta*t)/sin(theta)

Domain: vecα, vecα, number => vecα

Interpolates between two vectors with constant angular speed, following an ellipse through the two vectors.

Matrix functions

matrixCompMult

vm.matrixCompMult(a, b[, r]) --> r[i][j] = a[i][j] * b[i][j]

Domain: matαxβ, matαxβ => matαxβ, cmatαxβ, cmatαxβ => cmatαxβ

Componentwise multiplication of two matrices. If you want linear algebraic multiplication, use mul or the * operator.

outerProduct

vm.outerProduct(col, row[, r]) --> r[i][j] = col[i] * row[j]

Domain: vecα * vecβ = matβxα, cvecα * cvecβ = cmatβxα

Linear algebraic product of a column vector col and a row vector row, producing a matrix.

transpose

vm.transpose(m[, r]) --> r = mᵀ

Domain: matαxβ => matβxα, cmatαxβ => cmatβxα

Transposes the matrix: swaps the meaning of rows and columns.

determinant

vm.determinant(m[, r]) --> r = |m|

Domain: matαxβ => number, cmatαxβ => complex

Calculates the determinant of the matrix.

inverse

vm.inverse(m[, r]) --> r = m⁻¹

Domain: matαxα => matαxα, cmatαxα => cmatαxα

Calculates the inverse of the matrix.

Vector relational functions

The ones named for various comparison relations are componentwise for vectors: instead of returning a single boolean, they return a bvec where each component is the result of applying that relation to the matching components

equal

vm.equal(a,b) --> a bvec with true for equal components and false for unequal

Domain: vecα, vecα => bvecα, cvecα, cvecα => bvecα

Componentwise vector equality comparison. If you want a single boolean, check eq instead.

notEqual

vm.notEqual(a,b) --> a bvec with true for unequal components and false for equal

Domain: vecα, vecα => bvecα, cvecα, cvecα => bvecα

Componentwise vector inequality comparison. If you want a single boolean, use not eq(a,b) instead.

greaterThan

vm.greaterThan(a,b) --> a bvec with true for components where a[i] > b[i]

Domain: vecα, vecα => bvecα

Componentwise vector comparison using >.

greaterThanEqual

vm.greaterThanEqual(a,b) --> a bvec with true for components where a[i] >= b[i]

Domain: vecα, vecα => bvecα

Componentwise vector comparison using >=.

lessThan

vm.lessThan(a,b) --> a bvec with true for components where a[i] < b[i]

Domain: vecα, vecα => bvecα

Componentwise vector comparison using <.

lessThanEqual

vm.lessThanEqual(a,b) --> a bvec with true for components where a[i] <= b[i]

Domain: vecα, vecα => bvecα

Componentwise vector comparison using <=.

any

vm.any(v) --> logical OR of all components

Domain: bvecα => boolean

Returns true if any of the components of v are true; otherwise, false.

all

vm.all(v) --> logical AND of all components

Domain: bvecα => boolean

Returns true if all of the components of v are true; otherwise, false.

logicalAnd

vm.logicalAnd(a,b) --> componentwise logical AND

Domain: bvecα, bvecα => bvecα

Returns true for each component that is true in both a and b. This does not short-circuit: both inputs are evaluated regardless of result.

logicalOr

vm.logicalOr(a,b) --> componentwise logical OR

Domain: bvecα, bvecα => bvecα

Returns true for each component that is true in either a and b. This does not short-circuit: both inputs are evaluated regardless of result.

logicalNot

vm.logicalNot(a) --> componentwise logical NOT

Domain: bvecα => bvecα

Returns true for each component that is false. and vice versa.

Color functions

All colors in vornmath are assumed to be stored in vec4s, with alpha as the fourth coordinate. The way these vectors are interpreted is based on the default color space, which is srgb when vornmath loads.

vornmath is currently aware of the following color spaces:

  • srgb
  • linearrgb
  • hsl
  • hsv
  • hwb

colorParse

vm.colorParse(s[, r]) --> parse a color

Domain: string => vec4

Parses the color string and turns it into a color vector in the default color space. Currently accepts hex codes of length 3, 4, 6, or 8 such as #ffa500ff or named CSS colors such as aliceblue.

colorFrom

vm.colorFrom(c, space[, r]) --> convert a color from a given space

Domain: vec4, string => vec4

Converts a color from the named color space to the default color space. Alpha is maintained.

colorTo

vm.colorFrom(c, space[, r]) --> convert a color into a given space

Domain: vec4, string => vec4

Converts a color from the default color space to the named color space. Alpha is maintained.

Technical Details

The Bakery

The heart of Vornmath's architecture is the bakery: a system that will generate any requested function the first time it is asked for and store it permanently for later use, and prepare simple dispatch functions to enable its use from the generic function name.

Structure of a bakery

A bakery for a function is a simple table placed in vm.bakeries[function_name] and is composed of three functions, each of which accepts a table of type names:

  • signature_check returns true if this particular bakery handles functions with this signature; if it does return true, it may edit the types table to trim it or add 'nil' to help distinguish it from other signatures.
  • create returns a function that actually performs the requested operation. Since it is exclusively called after signature_check, it does not need to check whether the types are actually correct.
  • return_type returns the type name(s) returned by the function. Like create it need not check whether it actually works. If a function returns a list of things, this function will do so as well.

Why?

Let's look at multiplication. mul has, including filling and return-only versions, 594 distinct valid signatures, in a dozen or so patterns, all of which have to actually work. This is already too many to have each one represented directly in the source file - I know, because I tried it: it would be about 1/3 the size as the vornmath library is as a whole right now. Worse still would be fill, which has tens of millions of signatures, almost none of which will ever actually get used, and I'm not about to try to judge which ones are actually sane. So these have to get generated at some point at runtime, and the moment they're actually needed is the best choice.

Meanwhile, the work required to calculate which function to call in the first place (and indeed whether that is a usable function!) is quite complicated. By placing simple dispatch functions for already-known signatures, the complicated work is avoided as much as possible.

hasBakery

vm.utils.hasBakery(name, {typenames}) --> bakery

hasBakery will go through the bakeries for a named function and find one that matches the types passed. It will also modify the typenames table, typically by adding nil to the signature or by deleting extraneous types. If it doesn't find a bakery it will return false (if a function by that name exists but not with that signature) or nil (if the function doesn't exist), so it also works as a boolean.

bake

vm.utils.bake(name, {typenames}) --> function

bake actually generates the function with the required signature, and also generates any proxies required to reach the signature function when the generic is called. It returns the function generated. Note: this does call hasBakery, so the typenames table may be modified. Will raise an error if no such bakery exists.

vm.utils.bakeByCall(name, ...) --> function

bakeByCall extracts the types of each argument passed as part of ... and uses them to bake.

Metatables amd types

every type used by vornmath gets a metatable. In addition to operator overloads and the metameta that enables lazy generation of functions, this metatable contains some readable information about the type itself:

  • vm_storage is the name of the underlying numeric type
  • vm_shape is the shape of the type: 'scalar', 'vector', or 'matrix'
  • vm_dim is the dimensions of the type: 1 for scalars, a number for vectors, and {width, height} for matrices.
  • vm_type has it all together as the official typename of the type.

number, boolean, string, table and nil also get their own "metatables". Though they do not get attached to the types, they do get used by vornmath (via the utility functions) to get necessary information about the types when baking.

Utility functions

type

vm.utils.type(obj) --> typename

Returns the name of the vornmath type (if it exists) or the lua type (if not).

getmetatable

vm.utils.getmetatable(obj) --> metatable

Returns the vornmath metatable of the object: for built-in types where the metatable doesn't exist or is fixed, will return the fake metatable created for vornmath.

findTypeByData

vm.utils.findTypeByData(shape, dim, storage) --> typename

Returns the typename that matches the given information. Will return nil if there isn't one.

consensusStorage

vm.utils.consensusStorage(types) --> typename

Finds the consensus storage type, the numeric type that can represent every type of number found in the given types.

componentWiseConsensusType

vm.utils.componentWiseConsensusType(types) --> typename

Finds the "consensus type", the type that would be returned by a componentwise function that is passed arguments of these types: it will have the smallest storage that fits the data, and will be a vector if there are vector types or a matrix if there are matrix types. Will return nil instead if there are both matrix and vector types, or if there are matrices or vectors of different dimensions, or if the type required isn't suported.

Expansion Bakeries

Expansion bakeries are generic functions that create additional bakeries to expand the abilities of a function in common ways.

ComponentWiseReturnOnlys

vm.utils.componentWiseReturnOnlys(function_name, arity, forced_storage) --> bakery

Most vornmath functions accept an "out variable" that it fills in with the results, that it also returns. However if we want to actually create a new object, that out variable isn't required; this expander creates the functions that create a fresh object before doing the main operation.

This works on things where the numeric type coming out is the the one produced by componentWiseConsensusType: it doesn't work for length because that isn't componentwise, and it doesn't work for abs because that only makes numberish things.

twoMixedScalars

vm.utils.twoMixedScalars(function_name) --> bakery

This bakery accepts things such as add(number, quat) and adds casts to get it to use the same underlying function as add(quat, quat).

componentWiseExpander

vm.utils.componentWiseExpander(function_name, shapes)

Generates a bakery that expands a function to accept the various shapes as inputs. shapes is a table of vm_shape values, scalar, vector, and matrix; function_name is the name of the function this bakery is part of. This is easiest to explain by example: add is already defined so it can add two numbers; vm.utils.componentWiseExpander('add', {'vector', 'number'}) makes it so a vector and a number can be added: vm.vec3(2,3,4) + 5 will now give vm.vec3(7,8,9).

quatOperatorFromComplex

vm.utils.quatOperatorFromComplex(function_name) --> bakery

For a function that accepts and returns a single complex number, there is a simple way to make it also work for quaternions. Use this bakery to enable that.

genericConstructor

vm.utils.genericConstructor(function_name) --> bakery

This allows the use of any fill function as a constructor as well: since, for instance, fill(complex, number, number) is a valid signature for fill, complex(number, number) is a valid signature for complex

Simple signature checks

justNilTypeCheck

vm.utils.justNilTypeCheck

Mostly used for constructors, a bakery that gets this function as its signature_check will accept a completely blank signature.

clearingExactTypeCheck

vm.utils.clearingExactTypeCheck(types) --> signature_check function

Will match a signature that are exactly the list of types given, and clear out any further types from the table. This clearing has the effect of mitigating the effects of accidentally calling a function with too many arguments, which should work just fine and get ignored just like a regular Lua function.

nilFollowingExactTypeCheck

vm.utils.nilFollowingExactTypeCheck(types) --> signature_check function

much like clearingExactyTypeCheck, will match signatures that are exactly the list given. This one however pads it out with a 'nil', which will get included in the signature used for the specific function. This is used primarily for return-only versions of a function, which look almost exactly like the ones that include out variables; add_complex_complex_complex's existence means that add_complex_complex doesn't work correctly, but add_complex_complex_nil would, and that's what it's here for.

Color details

Color conversion functions

Additional color spaces can be added; conversion functions should go in vm.colorConversions and look like this:

vm.colorConversions.foo.bar = function(from, to)
  -- convert "from" in foo space
  -- to "to" in bar space
  return to
end

A new color space should get one function that converts to it from a known space, and one function that converts from it to a known space. Any space that is connected via conversion functions to the other spaces can be used freely.

If you add your own color spaces, you should call prepareColorConverters.

prepareColorConverters

vm.utils.prepareColorConverters()

Generates conversion functions to and from the default color space, for use by fromColor and toColor. This is called automatically when vornmath is loaded and also whenever settings.setColorSpace is called, so you only need to call it yourself if you add your own color space conversions.

settings.setColorSpace

vm.settings.setColorSpace(new_space) --> set the default color space

Changes the default color space to a different space. This does not change the actual numbers in existing color vectors.

settings.getColorSpace

vm.settings.getColorSpace() --> get the current default color space

Returns the current default color space.

Other functions

unmProxy

vm.utils.unmProxy

It turns out that the usual __unm metamethod gets its argument passed twice to it, which interferes with the out variable setup vornmath uses. This function is used in the metatables for vornmath types to avoid this problem.

vectorNilConstructor

vm.utils.vectorNilConstructor(storage,d) --> bakery

A default constructor for a vector; tell it the storage type and the size of the vector and this bakery will be used to initialize storage for such a vector.

matrixNilConstructor

vm.utils.matrixNilConstructor(storage,w,h) --> bakery

Like vectorNilConstructor but for matrices instead.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published