math3d
import math3d from "math3d";
// a model 3 units in front of a camera, turned 30 degrees, drawn in perspective
let model = math3d.mat4_trs(math3d.vec3(0.0, 0.0, -3.0),
math3d.quat_from_axis_angle(math3d.vec3(0.0, 1.0, 0.0), math3d.radians(30.0)),
math3d.splat3(1.0));
let eye_view = math3d.mat4_look_at(math3d.vec3(0.0, 1.0, 2.0), math3d.splat3(0.0), math3d.vec3(0.0, 1.0, 0.0));
let proj = math3d.mat4_perspective(math3d.radians(60.0), 16.0 / 9.0, 0.1, 100.0);
let mvp = proj * eye_view * model; // a Mat4 a gpu uniform takes as it is
let clip = mvp * math3d.vec4(0.5, 0.5, 0.5, 1.0);math3d is pure and freestanding: it allocates nothing and keeps no
state, so every profile can use it, kernel code included. Everything is
f32. The same types are gpu’s: gpu.Vec3 is
math3d.Vec3, so what the CPU computes goes to a shader with nothing
converted.
The conventions are the GPU’s:
- right-handed, y up, a camera looking down -z;
- column vectors:
m * v, soa * bdoesbfirst (quaternions too); - matrices stored column by column,
c0toc3, as GLSL and MSL keep them (a glTF matrix’s 16 floats copy straight in); - angles in radians (
radians(deg),degrees(rad)); - clip space with y up and depth from 0 (near) to 1 (far).
Vectors
Vec2, Vec3, Vec4 (f32), IVec2..IVec4 (i32) and
UVec2..UVec4 (u32): plain structs with x, y, z, w.
let a = math3d.vec3(1.0, 2.0, 3.0); // vec2, vec4, ivec3, uvec2, ...
let b = math3d.splat3(2.0); // (2, 2, 2); splat2, isplat4, usplat3, ...
let c = a + b * 0.5 - math3d.vec3(1.0, 0.0, 0.0); // (1, 3, 4)
let d = a.dot(b); // 12
let n = a.cross(math3d.vec3(0.0, 0.0, 1.0)); // (2, -1, 0)
let xy = math3d.vec4(1.0, 2.0, 3.0, 4.0).xy; // (1, 2); .xyz, .zyx, .yx ...
let i = math3d.ivec2(7, 3) % math3d.isplat2(4); // (3, 3)| On a vector | Gives |
|---|---|
+ - * / with a vector or a lane value (either side), unary -, % (integers) | Lane by lane |
== | Whether every lane is equal |
v.min(o), v.max(o), v.clamp(lo, hi), v.abs() | Lane by lane |
v.floor(), ceil(), round(), fract(), sign() | Lane by lane (f32) |
v.to_i(), to_u(), to_f() | The other lane types, as on each lane |
v.dot(o), v.length(), v.length_sq() | The dot product; the length; its square (no root) |
v.normalize() | v at length 1 (a zero vector gives NaN) |
v.distance(o), v.distance_sq(o) | How far apart; its square |
v.mix(o, t) | v + (o - v) * t |
v.reflect(n) | v mirrored in the plane with unit normal n (GLSL’s reflect) |
v.project(onto) | The part of v along onto |
v.approx_eq(o, eps) | Whether every lane is within eps |
v.step(edge), v.smoothstep(e0, e1) | GLSL’s, lane by lane |
v.ddx(), v.ddy(), v.fwidth() | In a pixel stage, how it changes across pixels (#shader); on the CPU they stop the program |
a.cross(b) (Vec3) | The cross product |
a.cross(b) (Vec2) | The 2D cross product, a scalar: positive when b is counterclockwise of a |
v.perp() (Vec2) | v turned a quarter counterclockwise: (-y, x) |
a.angle(b) (Vec2, Vec3) | The angle between them, 0..pi |
v.extend(z) (Vec2), v.extend(w) (Vec3) | The vector one longer |
let r = math3d.vec3(1.0, -1.0, 0.0).reflect(math3d.vec3(0.0, 1.0, 0.0)); // (1, 1, 0)
let p = math3d.vec3(2.0, 3.0, 0.0).project(math3d.vec3(1.0, 0.0, 0.0)); // (2, 0, 0)
let t = math3d.vec2(3.0, 4.0).perp(); // (-4, 3)
let k = math3d.vec2(1.0, 0.0).cross(math3d.vec2(0.0, 1.0)); // 1
let q = math3d.vec3(1.0, 0.0, 0.0).angle(math3d.vec3(0.0, 1.0, 0.0)); // pi / 2
let near = math3d.vec3(1.0, 2.0, 3.0).approx_eq(math3d.vec3(1.0, 2.0, 3.000001), 1e-5); // true
let h = math3d.vec2(1.0, 2.0).extend(0.0).extend(1.0); // (1, 2, 0, 1)Vectors, matrices and quaternions take compound assignment wherever their operator gives back the same type:
pos += vel * dt; // Vec3 + (Vec3 * f32)
color *= 0.5;
model *= math3d.mat4_translate(step); // Mat4 * Mat4
orientation *= turn; // Quat * QuatMatrices
Mat4 { c0; c1; c2; c3 } holds four Vec4 columns; Mat3 { c0; c1; c2 } three Vec3 columns.
| Function | Gives |
|---|---|
mat4_identity(), mat3_identity() | The matrix that changes nothing |
m * v, m * n | A vector transformed; the product (n first) |
m == n, m.approx_eq(n, eps) | Exactly equal; every entry within eps |
m.transpose(), m.determinant() | Rows and columns swapped; the determinant |
m.inverse() -> ?Mat4 (?Mat3) | The matrix that undoes m; null when it squashes space flat (abs(det) < SINGULAR_EPSILON) |
m.transform_point(p) | A Vec3 point moved by a transform (w = 1) |
m.transform_vector(v) | A Vec3 direction turned and scaled, not moved (w = 0) |
m.project_point(p) | A point through a projection, divided by w |
m.normal_matrix() | The Mat3 normals transform by (the inverse transpose of the upper left 3x3) |
m.to_mat3(), m3.to_mat4() | The upper left 3x3; a 3x3 in a 4x4 |
let m = math3d.mat4_translate(math3d.vec3(1.0, 2.0, 3.0)) * math3d.mat4_scale(math3d.vec3(2.0, 2.0, 2.0));
let p = m.transform_point(math3d.vec3(1.0, 1.0, 1.0)); // (3, 4, 5): scaled, then moved
let v = m.transform_vector(math3d.vec3(1.0, 1.0, 1.0)); // (2, 2, 2): not moved
let back = m.inverse() or { return; }; // null only for a flat matrix
let same = (m * back).approx_eq(math3d.mat4_identity(), 1e-6);
let det = m.determinant(); // 8
let nm = m.normal_matrix(); // a Mat3 for the shader's normals
let flat = math3d.mat4_scale(math3d.vec3(1.0, 0.0, 1.0)).inverse(); // nullTransforms
| Function | Gives |
|---|---|
mat4_translate(t), mat4_scale(s) | A move; a scale about the origin |
mat4_rotate_x(a), mat4_rotate_y(a), mat4_rotate_z(a) | A turn about an axis, counterclockwise seen from its positive end |
mat4_rotate_axis(axis, a) | A turn about any axis |
mat4_trs(t, r, s) | Scale by s, turn by the quaternion r, move by t (a glTF node’s transform) |
Trs { t; r; s; }.to_mat4() | The same from a Trs |
m.decompose_trs() | The Trs a transform is made of |
let spin = math3d.mat4_rotate_z(math3d.PI / 2.0);
let up = spin.transform_vector(math3d.vec3(1.0, 0.0, 0.0)); // (0, 1, 0), to rounding
let tilt = math3d.mat4_rotate_axis(math3d.vec3(1.0, 1.0, 0.0), 0.5);
let node = math3d.mat4_trs(math3d.vec3(0.0, 1.0, 0.0), math3d.quat_identity(), math3d.splat3(2.0));
let parts = node.decompose_trs(); // t (0, 1, 0), r identity, s (2, 2, 2)
let again = parts.to_mat4();decompose_trs expects a transform without shear (what mat4_trs and
products of turns, moves and positive scales make). A mirror comes back
as a negative s.x; a zero scale leaves no rotation to find, and r is
the identity.
Cameras
| Function | Gives |
|---|---|
mat4_look_at(eye, target, up) | The view matrix of a camera at eye looking at target |
mat4_perspective(fov_y, aspect, near, far) | A perspective projection: vertical field of view, width / height, the near and far distances |
mat4_ortho_3d(left, right, bottom, top, near, far) | An orthographic projection of a box in front of the camera |
mat4_ortho(left, right, bottom, top) | The 2D one: pixels to clip space, z kept |
let eye_view = math3d.mat4_look_at(math3d.vec3(0.0, 0.0, 5.0), math3d.splat3(0.0), math3d.vec3(0.0, 1.0, 0.0));
// the same as mat4_translate(vec3(0, 0, -5)): the camera looks down -z
let proj = math3d.mat4_perspective(math3d.PI / 2.0, 2.0, 1.0, 101.0);
let at_near = proj.project_point(math3d.vec3(0.0, 0.0, -1.0)); // depth 0
let at_far = proj.project_point(math3d.vec3(0.0, 0.0, -101.0)); // depth 1
let box = math3d.mat4_ortho_3d(-2.0, 2.0, -1.0, 1.0, 1.0, 11.0);
let pixels = math3d.mat4_ortho(0.0, 800.0, 0.0, 600.0);Depth runs from 0 at near to 1 at far, and y points up in clip space:
the convention gpu gives on every backend.
Quaternions
A Quat { x; y; z; w; } is a rotation: x, y, z the vector part, w
the scalar (glTF’s order). Keep them at length 1; q and -q are the
same rotation.
| Function | Gives |
|---|---|
quat(x, y, z, w), quat_identity() | A quaternion from its parts; no turn |
quat_from_axis_angle(axis, a) | A turn of a radians about axis |
quat_from_euler(x, y, z) | Pitch, yaw, roll about x, y, z, applied z first, then x, then y |
quat_from_mat3(m), quat_from_mat4(m) | The rotation of a rotation matrix (no scale) |
quat_from_to(a, b) | The shortest turn from direction a to direction b |
quat_look_rotation(forward, up) | The rotation that points -z along forward, y toward up |
a * b | b, then a |
q * v, q.rotate(v) | A Vec3 turned |
q.to_mat3(), q.to_mat4() | The rotation matrix |
q.conjugate(), q.inverse() | The opposite turn (unit q); the inverse of any non-zero q |
q.normalize(), q.length(), a.dot(b), -q | Length 1; the length; the dot product; every part negated |
a.slerp(b, t) | The constant-speed turn from a (t = 0) to b (t = 1), the shorter way |
a.nlerp(b, t) | The normalized straight blend, the shorter way: cheaper, not constant speed |
a == b, a.approx_eq(b, eps) | Exactly equal; every part within eps |
let yaw = math3d.quat_from_axis_angle(math3d.vec3(0.0, 1.0, 0.0), math3d.radians(90.0));
let fwd = yaw.rotate(math3d.vec3(0.0, 0.0, -1.0)); // (-1, 0, 0) to rounding: turned left
let both = yaw * math3d.quat_from_euler(0.2, 0.0, 0.0); // pitch, then yaw
let half = math3d.quat_identity().slerp(yaw, 0.5); // 45 degrees about y
let quick = math3d.quat_identity().nlerp(yaw, 0.5);
let aim = math3d.quat_from_to(math3d.vec3(1.0, 0.0, 0.0), math3d.vec3(0.0, 1.0, 0.0));
let cam = math3d.quat_look_rotation(math3d.vec3(0.0, 0.0, -1.0), math3d.vec3(0.0, 1.0, 0.0)); // identity
let m = yaw.to_mat4();
let from_m = math3d.quat_from_mat4(m); // yaw again (or -yaw)
let undo = yaw.conjugate();
let same = (yaw * undo).approx_eq(math3d.quat_identity(), 1e-6);slerp gives a and b exactly at t = 0 and 1. When a.dot(b) is
negative it blends toward -b, the same rotation the short way round, so
t = 1 gives -b.
Scalar functions
| Function | Gives |
|---|---|
PI, TAU, radians(deg), degrees(rad) | The constants in f32; angle conversion |
approx_eq(a, b, eps) | abs(a - b) <= eps |
abs, sign, floor, ceil, round, fract, sqrt, inverse_sqrt | On an f32; round takes a half away from zero (round(2.5) is 3.0, round(-0.25) is -0.0), as math.round_f32 does |
min, max, clamp(x, lo, hi), mix(a, b, t), step(edge, x), smoothstep(e0, e1, x), fmod(x, y) | GLSL’s, on f32 (fmod is GLSL’s mod) |
sin, cos, tan, asin, acos, atan, atan2(y, x), pow, exp, log | math’s f32 functions |
min_i, max_i, clamp_i, abs_i, min_u, max_u, clamp_u | On i32 and u32 |
let a = math3d.radians(180.0); // PI
let s = math3d.smoothstep(0.0, 1.0, 0.5); // 0.5
let w = math3d.fmod(-1.0, 3.0); // 2
let ok = math3d.approx_eq(math3d.sin(math3d.PI), 0.0, 1e-6);
let n = math3d.clamp_i(12, 0, 10); // 10On the GPU
The vectors and Mat4 cross to a GPU buffer as they are: each has the
size and alignment GLSL’s std430 and Metal give it (a Vec3 is 16 bytes,
a Mat3 three 16-byte columns, a Quat a Vec4’s 16). A Vec3 in a
struct that goes to the GPU is an error there (std430 packs a scalar
after it); use a Vec4.
In a #shader stage, the vector operations and
methods gpu lists, Mat4 * Vec4, mat4_identity, mat4_ortho and the
scalar functions compile to the GPU, written through math3d or gpu.
The rest (inverses, projections, quaternions) is CPU code: compute the
matrices in the program and pass them as uniforms.
Last modified: