Vectors, matrices, quaternions and geometry for MoonBit: column-major, right-handed, and ready to copy into a GPU buffer, with a special-function subpackage in double and arbitrary precision.
let eye = @moonmath.Vec3::new(0.0, 1.6, 6.0)
let view = @moonmath.Mat4::look_at(eye, @moonmath.Vec3::zero(), @moonmath.Vec3::unit_y())
let projection = @moonmath.Mat4::perspective(@moonmath.to_radians(60.0), 16.0 / 9.0, 0.1, 100.0)
let view_projection = projection * view
let on_screen = view_projection.transform_point(@moonmath.Vec3::zero())moon add moonbitstack/moonmathlet turn = @moonmath.Quat::from_axis_angle(@moonmath.Vec3::unit_y(), @moonmath.quarter_pi)
let half_way = @moonmath.Quat::identity().slerp(turn, 0.5)
let ahead = turn.rotate(@moonmath.Vec3::unit_z())
let matrix = turn.to_mat3()
// Euler angles, in one of the twelve orders, by name.
let from_yaw = @moonmath.Quat::from_euler(
@moonmath.EulerOrder::Zyx,
@moonmath.Vec3::new(0.0, 0.0, @moonmath.to_radians(90.0)),
)
let back = from_yaw.to_euler(@moonmath.EulerOrder::Zyx)let precessing = @moonmath.Quat::from_euler_turns(
@moonmath.EulerOrder::Xyx,
@moonmath.Turns::new(0.3, 1.2, -0.4),
)
let turns = precessing.to_euler_turns(@moonmath.EulerOrder::Xyx)let placement = @moonmath.Trs::new(
@moonmath.Vec3::new(0.0, 2.0, 0.0),
@moonmath.Quat::from_axis_angle(@moonmath.Vec3::unit_y(), @moonmath.half_pi),
@moonmath.Vec3::splat(0.5),
)
let model = @moonmath.Mat4::from_trs(placement)
// And back out again, for an editor or an animation to edit.
let editable = model.to_trs().unwrap()let room = @moonmath.Aabb3::new(
@moonmath.Vec3::new(-4.0, 0.0, -4.0),
@moonmath.Vec3::new(4.0, 3.0, 4.0),
)
let ray = @moonmath.Ray3::new(
@moonmath.Vec3::new(0.0, 2.0, 0.0),
@moonmath.Vec3::new(0.0, -1.0, -1.0).normalize(),
)
match ray.intersect_aabb(room) {
Some(t) => println("the room is \{t} away along the ray, at \{ray.at(t)}")
None => println("the ray leaves the room alone")
}
let frustum = @moonmath.Frustum::from_matrix(view_projection).unwrap()
if frustum.intersects_aabb(room) {
println("there is something to draw")
}let far_away = @f64.Vec3::new(1.0e9, 0.0, 0.0)
let scaled = far_away.scale(2.0)let packed = @moonmath.Half::from_float(1.5)
let restored = packed.to_float()
let raw = packed.to_bits()let red = @moonmath.Color3::new(1.0, 0.0, 0.0)
let hue = @moonmath.rgb_to_hsv(red)
let skew = @moonmath.Shear::new(0.3, 0.0, 0.0).to_mat3()
let span = @moonmath.Interval::new(0.0, 1.0).union(
@moonmath.Interval::new(2.0, 3.0),
)
// The two values are a uniform random source the caller owns.
let on_sphere = @moonmath.Vec3::random_on_sphere(0.25, 0.75)| Signature | Meaning |
|---|---|
| to_radians(degrees : Float) -> Float | degrees to radians |
| to_degrees(radians : Float) -> Float | radians to degrees |
| lerp(a, b, t) -> Float | a at t = 0, b at t = 1, extrapolating outside |
| inverse_lerp(start, end, value) -> Float | where a value sits in a range; zero for an empty range |
| remap(value, from_start, from_end, to_start, to_end) -> Float | the same position in another range |
| smoothstep(start, end, value) -> Float | the cubic easing with zero slope at both ends |
| wrap(value, span) -> Float | folded into 0..span |
| wrap_range(value, start, end) -> Float | folded into start..end |
| approx(a, b, eps? : Float) -> Bool | equal to within eps, or within default_epsilon |
| approx_angle(a, b, eps? : Float) -> Bool | the same, for angles that wrap at tau |
| clamp01(value) -> Float | pinned to 0..1 |
| pi, tau, half_pi, quarter_pi, default_epsilon | the constants |
| Signature | Meaning |
|---|---|
| Vec::new(...), splat(v), zero(), one(), unit_x(), unit_y(), unit_z(), unit_w() | constructors; unit_z and unit_w where the type has them |
| dot(other) -> Float | the dot product |
| length(), length_squared() -> Float | the length, and its square |
| distance(other), distance_squared(other) -> Float | between two points |
| normalize() -> Vec | to length one; the zero vector stays zero |
| try_normalize() -> Vec?, normalize_or(fallback) -> Vec | the same, reporting the case instead |
| scale(factor), divide(divisor) -> Vec | multiplied and divided by a scalar |
| lerp(other, t), min(other), max(other), clamp(low, high) -> Vec | component-wise |
| abs(), floor(), ceil(), round(), fract() -> Vec | component-wise rounding; a half rounds up |
| to_array() -> FixedArray[Float] | the components in order |
| is_finite() -> Bool | no NaN and no infinity |
| approx(other, eps? : Float) -> Bool | equal to within eps |
| Type | Signature | Meaning |
|---|---|---|
| Vec2 | cross(other) -> Float | the signed area the two span |
| perp() -> Vec2 | a quarter turn counter-clockwise | |
| angle_between(other) -> Float | the signed angle from this to that, in -pi..pi | |
| yx() -> Vec2 | the components swapped | |
| to_vec3(z), to_vec4(z, w) | lifted into a higher dimension | |
| Vec3 | cross(other) -> Vec3 | the perpendicular, right-handed |
| angle_between(other) -> Float | the unsigned angle, in 0..pi | |
| xy(), xz(), yz() -> Vec2 | a plane's worth of components | |
| zyx(), xzy(), yxz(), yzx(), zxy() -> Vec3 | the six orders of the three components | |
| to_vec4(w) -> Vec4 | lifted into 4D | |
| Vec4 | xy() -> Vec2, xyz() -> Vec3 | the leading components |
| wzyx() -> Vec4 | all four backwards |
| Signature | Meaning |
|---|---|
| new(...), splat(v), zero(), one() | constructors |
| dot(other) -> Int (or UInt) | the dot product |
| min(other), max(other), clamp(low, high) | component-wise, inclusive |
| to_array() | the components in order |
| IVec::abs() -> IVec | absolute value |
| IVec::as_vec2/3/4(), UVec::as_vec2/3/4() | the same values as floats, exactly |
| IVec::from_vec2/3/4(v) | the floats truncated towards zero |
| Signature | Meaning |
|---|---|
| identity(), zero() | the two that need no arguments |
| from_cols(...), from_rows(...) | built one way or the other |
| from_array(values) -> Mat | from the column-major array a buffer holds; aborts on the wrong length |
| col(i) -> Vec, row(i) -> Vec, get(row, column) -> Float | reading it |
| transpose() -> Mat | rows and columns exchanged |
| determinant() -> Float | the volume it scales by |
| inverse() -> Mat? | None when it is singular |
| mul_vec2/3/4(v) -> Vec | applied to a vector |
| to_array() -> FixedArray[Float] | column-major, ready for a buffer |
| is_finite(), approx(other, eps?) -> Bool |
| Type | Signature | Meaning |
|---|---|---|
| Mat2 | from_scale(Vec2), from_angle(radians), to_angle() -> Float | a scaling, a rotation, and the angle back out |
| Mat3 | from_scale(Vec3), from_axis_angle(axis, radians), from_quat(q), from_euler(order, angles), from_euler_turns(order, turns) | |
| from_mat4(m) | the upper-left block, without the translation | |
| Mat4 | from_scale(Vec3), from_translation(Vec3), from_axis_angle(axis, radians), from_quat(q), from_euler(order, angles), from_euler_turns(order, turns) | |
| from_mat3(m) | the 3×3 with w = 1 and no translation | |
| from_trs(trs), to_trs() -> Trs? | scale, then rotate, then move; and back | |
| transform_point(p) -> Vec3 | a point, divided by w when there is one | |
| transform_vector(v) -> Vec3 | a direction, untouched by the translation | |
| look_at(eye, target, up) -> Mat4 | the view matrix of a camera | |
| perspective(fov_y, aspect, near, far) | depth 0..1 | |
| perspective_infinite(fov_y, aspect, near) | depth 0..1, no far plane | |
| orthographic(left, right, bottom, top, near, far) | depth 0..1 | |
| frustum(left, right, bottom, top, near, far) | depth 0..1, off-centre | |
| perspective_gl, perspective_infinite_gl, orthographic_gl, frustum_gl | the same four, depth -1..1 | |
| Shear | new(xy, xz, yz), zero(), to_mat3(), to_mat4() | the off-diagonal terms as a linear map |
| Signature | Meaning |
|---|---|
| Quat::identity(), Quat::new(x, y, z, w) | no rotation, and a raw one |
| Quat::from_axis_angle(axis, radians) | the right-hand rule about an axis |
| Quat::from_euler(order, angles), Quat::to_euler(order) -> Vec3 | the turns about x, y and z, by axis; aborts for an order that turns about one axis twice |
| Quat::from_euler_turns(order, turns), Quat::to_euler_turns(order) -> Turns | the same, with the three turns in order, for all twelve orders |
| Quat::from_rotation_arc(from, to) | the shortest rotation between two directions |
| Quat::from_mat3(m), Quat::from_mat4(m) | out of a matrix |
| Quat::to_mat3(), Quat::to_mat4() | into one |
| Quat::to_axis_angle() -> (Vec3, Float) | the axis and the angle |
| Quat::rotate(v) -> Vec3 | the vector turned |
| Quat::inverse() -> Quat?, conjugate(), normalize(), length(), length_squared(), dot(other) | |
| Quat::slerp(other, t), Quat::nlerp(other, t) | at constant angular speed, and along the straight line |
| Quat::to_array(), is_finite(), approx(other, eps?) |
| Signature | Meaning |
|---|---|
| Affine2::new(linear, translation), Affine3::new(linear, translation) | |
| identity(), from_translation(v), from_scale(v) | |
| Affine2::from_angle(radians), from_angle_translation(radians, offset) | |
| Affine3::from_rotation(q), from_axis_angle(axis, radians), from_trs(trs), to_trs() -> Trs? | |
| transform_point(p), transform_vector(v) | |
| to_mat3() / to_mat4(), from_mat3(m) -> Affine2? / from_mat4(m) -> Affine3? | to a matrix and back; None when the matrix is not affine |
| inverse() -> Affine? | None when the linear part is singular |
| is_finite(), approx(other, eps?) |
| Type | Signature | Meaning |
|---|---|---|
| Rect | new(origin, size), from_xywh(x, y, w, h), from_edges(left, top, right, bottom) | a corner and an extent |
| min(), max(), center() -> Vec2, width(), height(), area() -> Float | ||
| contains(point), intersects(other), intersection(other) -> Rect?, union(other) | ||
| inset(amount), translate(offset), is_empty(), corners(), to_aabb() -> Aabb2 | ||
| Aabb2, Aabb3 | new(min, max), from_center_half_extents(center, extents), from_points(points) -> Aabb?, empty() | |
| center(), size(), extents(), area() / volume(), surface_area() | ||
| contains_point(p), contains(other), intersects(other), intersection(other) -> Aabb?, union(other), expand(amount) | ||
| corners(), is_empty(), Aabb3::transform(m : Mat4) -> Aabb3 | the box around the transformed box | |
| Ray2, Ray3 | new(origin, direction), at(t) -> Vec | a start and a direction; t may be any distance |
| intersect_aabb(box) -> Float?, intersect_aabb_interval(box) -> (Float, Float)? | the first crossing, and the stretch inside | |
| Ray2::intersect_rect(rect), Ray2::intersect_circle(center, radius) | ||
| Ray3::intersect_sphere(sphere), intersect_plane(plane), intersect_triangle(triangle) | ||
| Plane | new(normal, distance), from_point_normal(point, normal) -> Plane?, from_points(a, b, c) -> Plane? | dot(normal, p) == distance |
| signed_distance(p), project(p) -> Vec3, intersect_ray(ray) -> Float?, flip() | ||
| Sphere | new(center, radius), contains_point(p), intersects_sphere(other), intersects_aabb(box), surface_area(), volume() | |
| Triangle | new(a, b, c), normal() -> Vec3, area(), barycentric(p) -> Vec3?, intersect_ray(ray) -> Float? | |
| Frustum | from_matrix(view_projection) -> Frustum?, from_matrix_gl(view_projection) -> Frustum? | six planes facing inwards |
| planes() -> FixedArray[Plane], plane(i) -> Plane | left, right, bottom, top, near, far | |
| contains_point(p), contains_aabb(box), intersects_aabb(box), contains_sphere(s), intersects_sphere(s) | ||
| Line3 | new(point, direction), at(t), closest_point_to(p), distance_to_point(p), closest_point_to_line(line), distance_to_line(line), intersect_plane(plane) -> Vec3? | an infinite line |
| Interval | new(min, max), empty(), size(), center(), contains(x), intersects(other), intersection(other) -> Interval?, union(other), extend_by(x), expand(amount) | a one-dimensional range |
| Type | Signature | Meaning |
|---|---|---|
| Color3, Color4 | new(...), splat(v), zero(), one() | red, green and blue, and alpha |
| min(other), max(other), lerp(other, t) | component-wise | |
| to_array(), is_finite(), approx(other, eps?) | ||
| rgb_to_hsv(c), hsv_to_rgb(c) | between the two forms; the hue in turns |
| Function | Meaning |
|---|---|
| gamma(x), lgamma(x) | the gamma function and its log; Stirling with the reflection below one half |
| beta(a, b), lbeta(a, b) | the beta function and its log |
| digamma(x), polygamma(n, x) | the logarithmic derivative of gamma, and its n-th derivative |
| gammainc(a, x), gammaincc(a, x) | the regularised incomplete gamma, lower and upper |
| incbeta(a, b, x) | the regularised incomplete beta |
| erf(x), erfc(x), erfcx(x), erfi(x) | the error function, its complement, the scaled complement, the imaginary one |
| erfinv(x), erfcinv(x) | the inverses of the first two |
| bessel_j(v, x), bessel_y(v, x) | the Bessel functions of the first and second kind, real order |
| bessel_i(v, x), bessel_k(v, x) | the modified ones |
| ellipk(m), ellipe(m), ellipkm1(m) | the complete elliptic integrals, m the parameter |
| ellipkinc(phi, m), ellipeinc(phi, m) | the incomplete ones |
| elliprf, elliprd, elliprc, elliprj, elliprg | the Carlson symmetric forms the others are built on |
| hyper0f1(b, x), hyper1f1(a, b, x), hyper2f1(a, b, c, x) | the hypergeometric functions |
| zeta(s) | the Riemann zeta, above and below one |
let root_pi = @special.gamma(0.5)
let tail = @special.erfc(2.0)
let drum = @special.bessel_j(0.0, 2.4)let bits = @moonnum.Fixed::bits_for_digits(50)
let ops = @generic.fixed_ops(bits)
let x = @moonnum.Fixed::of_decimal("0.5", bits)
let value = @generic.gamma(x, ops)members = [
".",
"../moonnum",
]| Example | What it shows |
|---|---|
| src/examples/01-tour | the whole surface in one run |
| src/examples/02-camera | a camera, what lands on screen, frustum culling, and a pick |
| src/examples/03-layout | rectangles, insets and hit testing for a window of panes |
| src/examples/04-collision | a ray narrowed from a sphere to a triangle, and a bounce |
| Operation | ns | per second |
|---|---|---|
| Vec3::dot | 1.9 | 540,000,000 |
| Vec4::dot | 1.7 | 600,000,000 |
| Frustum::intersects_aabb | 21 | 47,000,000 |
| Vec3::normalize | 56 | 18,000,000 |
| Mat4::transform_point | 56 | 18,000,000 |
| Vec3::cross | 61 | 16,000,000 |
| Quat::rotate | 64 | 16,000,000 |
| Quat::slerp | 96 | 10,000,000 |
| Ray3::intersect_aabb | 97 | 10,000,000 |
| Ray3::intersect_sphere | 111 | 9,000,000 |
| Quat::to_mat3 | 220 | 4,500,000 |
| Mat4 * Mat4 | 282 | 3,500,000 |
| Mat4::inverse | 330 | 3,000,000 |
test {
let projection = @moonmath.Mat4::perspective(
@moonmath.half_pi, 1.0, 0.1, 100.0,
)
let frustum = @moonmath.Frustum::from_matrix(projection).unwrap()
// A point just in front of the camera is inside, one behind it is not.
assert_true(frustum.contains_point(@moonmath.Vec3::new(0.0, 0.0, -1.0)))
assert_false(frustum.contains_point(@moonmath.Vec3::new(0.0, 0.0, 1.0)))
}test {
let scale : @moonmath.Vec3 = @moonmath.Vec3::splat(2.0)
let offset = @moonmath.Vec3::new(1.0, 0.0, 0.0)
let m = @moonmath.Mat4::from_trs(
@moonmath.Trs::new(offset, @moonmath.Quat::identity(), scale),
)
// One, scaled by two, then moved one along x.
assert_true(
m
.transform_point(@moonmath.Vec3::one())
.approx(@moonmath.Vec3::new(3.0, 2.0, 2.0)),
)
}test {
let m = @moonmath.Mat4::perspective(@moonmath.half_pi, 1.0, 0.1, 100.0)
// The near plane lands on depth 0 and the far plane on depth 1.
let near = m.mul_vec4(@moonmath.Vec4::new(0.0, 0.0, -0.1, 1.0))
assert_true(@moonmath.approx(near.z / near.w, 0.0))
let far = m.mul_vec4(@moonmath.Vec4::new(0.0, 0.0, -100.0, 1.0))
assert_true(@moonmath.approx(far.z / far.w, 1.0))
}test {
let quarter = @moonmath.Quat::from_axis_angle(
@moonmath.Vec3::unit_z(),
@moonmath.half_pi,
)
assert_true(
quarter.rotate(@moonmath.Vec3::unit_x()).approx(@moonmath.Vec3::unit_y()),
)
}test {
// Zyx turns about z first, so the third component is the first turn.
let turn = @moonmath.Quat::from_euler(
@moonmath.EulerOrder::Zyx,
@moonmath.Vec3::new(0.0, 0.0, @moonmath.half_pi),
)
assert_true(
turn.rotate(@moonmath.Vec3::unit_y()).approx(-@moonmath.Vec3::unit_x()),
)
}test {
let turn = @moonmath.Quat::from_rotation_arc(
@moonmath.Vec3::unit_x(),
@moonmath.Vec3::unit_y(),
)
assert_true(
turn.rotate(@moonmath.Vec3::unit_x()).approx(@moonmath.Vec3::unit_y()),
)
}test {
let turn = @moonmath.Quat::from_axis_angle(
@moonmath.Vec3::unit_x(),
@moonmath.quarter_pi,
)
let angles = turn.to_euler(@moonmath.EulerOrder::Yxz)
assert_true(@moonmath.approx(angles.x, @moonmath.quarter_pi))
}test {
let x : @moonmath.Vec2 = @moonmath.Vec2::unit_x()
let y = @moonmath.Vec2::unit_y()
assert_true(@moonmath.approx(x.angle_between(y), @moonmath.half_pi))
}test {
let x : @moonmath.Vec3 = @moonmath.Vec3::unit_x()
let y = @moonmath.Vec3::unit_y()
assert_true(@moonmath.approx(x.angle_between(y), @moonmath.half_pi))
assert_true(@moonmath.approx(x.angle_between(-x), @moonmath.pi))
}test {
let down : @moonmath.Vec3 = @moonmath.Vec3::new(0.0, -1.0, 0.0)
let up = @moonmath.Vec3::new(0.0, 1.0, 0.0)
// Straight down onto a surface and straight on through.
let through = down.refract(up, 0.5).unwrap()
assert_true(through.approx(down))
}fn approx(a : Float, b : Float, eps? : Float) -> Booltest {
let a : Float = 0.1 + 0.2
assert_true(@moonmath.approx(a, 0.3))
assert_false(@moonmath.approx(a, 0.31))
assert_true(@moonmath.approx(a, 0.31, eps=0.02))
}fn approx_angle(a : Float, b : Float, eps? : Float) -> Boollet default_epsilon : Floatfn inverse_lerp(start : Float, end : Float, value : Float) -> Floatfn lerp(a : Float, b : Float, t : Float) -> Floatlet quarter_pi : Floatfn remap(value : Float, from_start : Float, from_end : Float, to_start : Float, to_end : Float) -> Floatfn smoothstep(start : Float, end : Float, value : Float) -> Floatfn wrap(value : Float, span : Float) -> Floattest {
assert_eq(@moonmath.wrap(7.5, 5.0), 2.5)
assert_eq(@moonmath.wrap(-1.0, 5.0), 4.0)
}fn wrap_range(value : Float, start : Float, end : Float) -> FloatInstall
Download zipVectors, matrices, quaternions and geometry for MoonBit: column-major, right-handed, and ready to copy into a GPU buffer, with a special-function subpackage in double and arbitrary precision.