Math & geometry
Vectors, matrices, quaternions, ray primitives, mesh topology, splines, noise, and robust geometric predicates in the engine's core math library.
The engine’s core math library lives under @woosh/meep-engine/src/core/geom/ and @woosh/meep-engine/src/core/math/. Every function in it follows the same design contract: results are written into a caller-supplied output parameter, never returned as new allocations. This keeps hot paths - physics, animation, rendering - allocation-free at steady state.
There are no wrapper classes for matrices. There is no Matrix4: a 4x4 matrix is a 16-element column-major array (number[], Float32Array or Float64Array), a 3x3 is nine elements, and every m4_* / m3_* function reads and writes those directly. Vector3, Quaternion and friends do exist as observable value objects, but the flat-array functions below never require them.
Naming and deprecated aliases
Function names follow a subject_verb / name_to_name convention: v3_dot, m4_invert, quat3_multiply. A number of alternative spellings ship as deprecated re-exports - v3_quat3_apply for v3_quaternion_apply, compose_matrix4_array for m4_compose, quat3_createFromAxisAngle for quat3_from_axis_angle, and so on. Every table below lists the canonical name; the deprecated file sits next to it and does nothing but re-export.
Vectors
Vectors are passed as loose scalars, not objects. Most functions take an output buffer plus an integer offset first, then the inputs as individual components.
Vec2
Utilities under core/geom/vec2/:
| Function | Description |
|---|---|
v2_dot(x0, y0, x1, y1) | Dot product - returns a scalar |
v2_length(x, y) | Euclidean length |
v2_length_sqr(x, y) | Squared length (avoids a square root) |
v2_distance(x0, y0, x1, y1) | Distance between two points |
v2_distance_sqr(x0, y0, x1, y1) | Squared distance |
v2_cross_product(x0, y0, x1, y1) | 2D cross product (scalar) |
v2_angle_between(x0, y0, x1, y1) | Angle between two vectors in radians |
v2_rotate(out, out_offset, x, y, angle) | Rotate a point about the origin |
v2_matrix3_cm_multiply(out, out_offset, x, y, m3) | Transform64 by a column-major 3x3 matrix |
Vec3
Utilities under core/geom/vec3/:
| Function | Description |
|---|---|
v3_dot(x0, y0, z0, x1, y1, z1) | Dot product |
v3_cross(result, result_offset, ax, ay, az, bx, by, bz) | Cross product |
v3_length(x, y, z) | Euclidean length |
v3_length_sqr(x, y, z) | Squared length |
v3_distance(x0, y0, z0, x1, y1, z1) / v3_distance_sqr(…) | Distance and squared distance |
v3_lerp(result, ax, ay, az, bx, by, bz, fraction) | Linear interpolation; writes through result.set(x, y, z) |
v3_slerp(result, ax, ay, az, bx, by, bz, fraction) | Spherical linear interpolation |
v3_angle(x0, y0, z0, x1, y1, z1) | Angle in radians (v3_angle_between is the deprecated alias) |
v3_displace_in_direction(result, distance, ox, oy, oz, dx, dy, dz) | Move a point along a direction; returns false for a zero-magnitude direction |
v3_quaternion_apply(out, out_offset, vx, vy, vz, qx, qy, qz, qw) | Rotate a vector by a unit quaternion |
v3_quaternion_apply_inverse(out, out_offset, vx, vy, vz, qx, qy, qz, qw) | Rotate by the conjugate (inverse rotation) |
v3_matrix4_multiply(out, out_offset, input, input_offset, m4) | Full 4x4 transform including translation |
v3_matrix4_rotate_unit(out, out_offset, x, y, z, m4) | Rotate a unit direction; handles non-uniform scale, output is normalised |
v3_matrix4_rotate_normal(out, out_offset, x, y, z, m4) | Rotate a surface normal (inverse-transpose semantics) |
The out-parameter convention is uniform: the output buffer and an integer offset come first, followed by the inputs. For example:
import { v3_quaternion_apply } from "@woosh/meep-engine/src/core/geom/vec3/v3_quaternion_apply.js";
const out = new Float32Array(3);
// Rotate (0, 1, 0) by a unit quaternion stored as (qx, qy, qz, qw):
v3_quaternion_apply(out, 0, 0, 1, 0, qx, qy, qz, qw);
No intermediate object is created. out may be a plain array, a Float32Array, or any array-like.
Vec4
Minimal utilities under core/geom/vec4/:
| Function | Description |
|---|---|
v4_dot(x0, y0, z0, w0, x1, y1, z1, w1) | Dot product |
v4_length(x, y, z, w) | Euclidean length |
v4_length_sqr(x, y, z, w) | Squared length |
v4_matrix4_multiply(result, input, mat4) | Multiply by a 4x4 matrix (v4_multiply_mat4 is the deprecated alias) |
Matrices
Mat3
Column-major 3x3 utilities under core/geom/mat3/:
| Function | Description |
|---|---|
m3_multiply(r, a, b) | Matrix product r = a x b |
m3_invert(out, a) | Invert a 3x3 matrix (m3_cm_invert is the deprecated alias) |
m3_determinant(a, b, c, d, e, f, g, h, i) | Determinant from nine loose scalars |
m3_cm_compose_transform(result, tX, tY, sX, sY, cx, cy, angle) | Build a 2D TRS matrix about a rotation centre |
m3_cm_extract_rotation(m3) | Read the rotation angle (a scalar) back out of a 2D transform |
m3_rm_extract_scale(result, m3) | Read the scale factors out of a row-major 2D transform |
m3_make_rotation_TBN(output, forward, up_dir) | Build an orthonormal basis from a forward and an up vector |
m3_scale_columns_by_vec3(out, a, v) | Scale each column by a vector (m3_multiply_vec3 is the deprecated alias - despite that name it does not transform a vector) |
Mat4
Column-major 4x4 utilities under core/geom/3d/mat4/:
| Function | Description |
|---|---|
m4_multiply(out, a, b) | Matrix product; returns out |
m4_invert(out, input) | General inverse |
m4_transpose(out, m) | Transpose; out may alias m. Also what carries a matrix between column-major and row-major |
m4_compose(result, position, rotation, scale) | Build T * R * S from {x,y,z} / {x,y,z,w} objects (compose_matrix4_array is the deprecated alias) |
m4_decompose(mat4, position, rotation, scale) | Decompose through the destinations’ setters, so change signals fire (decompose_matrix_4_array is the deprecated alias) |
m4_decompose_array(mat4, ...) | The same decomposition into flat arrays |
m4_from_rotation_translation_scale_scalar(result, qx, qy, qz, qw, tx, ty, tz, sx, sy, sz) | Compose from loose scalars |
m4_make_translation(output, translation) | Translation matrix from a 3-element array-like |
m4_make_scale(output, scale) | Scale matrix from a 3-element array-like |
m4_make_rotation_x(output, angle) / _y / _z | Right-handed rotation about one axis; compose several with m4_multiply |
m4_rotation_translation(output, rotation_matrix, translation) | Assemble from a 3x3 rotation and a 3-vector |
m4_inverse_rotation_translation(output, rotation_matrix, translation) | The inverse of the same, without a general invert |
m4_extract_scale(out, out_offset, mat4) | Extract scale factors |
m4_linear_matrix3(out, m) | The 3x3 linear part - carries a tangent direction; out and m must be distinct |
m4_normal_matrix3(out, m) | The inverse-transpose 3x3 - carries a normal (m4_compute_normal_matrix3 is the deprecated alias) |
m4_look_at(out, eye, center, up) | View matrix; up must not be parallel to center - eye |
m4_perspective(out, fov_y, aspect, near, far) | Right-handed perspective with a -1..1 clip depth, finite far |
m4_orthographic_off_center_z0(output, left, right, bottom, top, z_near, z_far) | Orthographic projection, depth mapped to 0..1 |
m4_transform_v3_buffer(source, source_offset, destination, destination_offset, vertex_count, mat4) | Batch-transform packed positions (apply_mat4_transform_to_v3_array is the deprecated alias) |
m4_transform_direction_v3_buffer(...) | The same for directions, ignoring translation (apply_mat4_transform_to_direction_v3_array is the deprecated alias) |
m4_rigidity_defect(mat4) | How far the upper 3x3 is from orthonormal: 0 for a rigid motion, |s - 1| for a uniform scale s, the cosine of the shear angle for a shear. A test statistic to check against a tolerance before handing a matrix to something that reads it as a rotation, such as dual_quat_from_m4. A reflection reports 0 - check the determinant’s sign for handedness |
m4_perspective is not the projection the renderer draws with. Shade’s cameras use an infinite reverse-Z with a 0..1 depth range, which is what WebGPU wants; m4_perspective exists for arithmetic that has to hold under either convention and for reading geometry authored against the OpenGL one.
Quaternions
Quaternions use (x, y, z, w) component order - w last - throughout. Utilities live under core/geom/3d/quaternion/.
| Function | Description |
|---|---|
quat3_from_axis_angle(axis, angle, result) | Build a quaternion from an axis vector and an angle in radians (quat3_createFromAxisAngle is the deprecated alias) |
quat3_multiply(out, out_offset, ax, ay, az, aw, bx, by, bz, bw) | Hamilton product out = a ⊗ b; applies b first then a |
quat3_nlerp(...) | Normalised linear interpolation |
quat3_integrate(...) | Advance a rotation by an angular velocity over a timestep |
quat3_to_matrix3(out, out_offset, qx, qy, qz, qw) | Write a 3x3 rotation matrix (nine floats) |
The quat3_multiply convention: to rotate a vector by q1 and then q2, compose q2 ⊗ q1.
import { quat3_multiply } from "@woosh/meep-engine/src/core/geom/3d/quaternion/quat3_multiply.js";
const out = [0, 0, 0, 0];
// Compose: apply q1 first, then q2.
quat3_multiply(out, 0, q2x, q2y, q2z, q2w, q1x, q1y, q1z, q1w);
Compact storage: quat3_encode_to_uint32(x, y, z, w) packs a unit quaternion into a single uint32 (the smallest-three encoding from Bungie’s Destiny animation talk) and quat3_decode_from_uint32(output, output_offset, value) unpacks it. quat_encode_to_uint32 / quat_decode_from_uint32 are the deprecated spellings.
Ray intersection primitives
Each primitive shape has a closed-form ray test under core/geom/3d/<shape>/. They share one signature - (outNormal, outOffset, ox, oy, oz, dx, dy, dz, tMax, …shape params) - and one convention:
- the ray direction is assumed unit length, so the returned
tis a true distance; - the first surface crossing at or after the origin within
tMaxis returned - a ray starting inside a solid returns its exit crossing; - on a miss the function returns
Infinityand leaves the normal output untouched.
| Function | Path | Shape (in its own local frame) |
|---|---|---|
box3_raycast | 3d/box/ | Box centred at the origin, from half-extents |
sphere_raycast | 3d/sphere/ | Sphere centred at the origin |
capsule_raycast | 3d/capsule/ | Y-aligned capsule |
cylinder3_raycast | 3d/cylinder/ | Y-aligned solid cylinder with flat caps; requires radius > 0 |
tri3_raycast | 3d/triangle/ | Two-sided Möller-Trumbore triangle |
point3_raycast | 3d/point/ | A point at the origin |
convex_polyhedron3_raycast | 3d/polyhedron/ | Convex solid as face half-spaces, Cyrus-Beck clipping |
import { cylinder3_raycast } from "@woosh/meep-engine/src/core/geom/3d/cylinder/cylinder3_raycast.js";
const normal = new Float64Array(3);
const t = cylinder3_raycast(
normal, 0,
ox, oy, oz, // ray origin, in the cylinder's local frame
dx, dy, dz, // unit direction
tMax,
radius, half_height
);
// t === Infinity on a miss; otherwise normal[0..2] is the unit outward normal
Two conventions are worth spelling out. tri3_raycast orients its normal to face the ray - a triangle is a surface, not a solid, so its winding must not decide whether a ray bounces or passes through. convex_polyhedron3_raycast requires outward-facing plane normals, the opposite of the inward-wound convention the frustum tests use; feeding it an inward set reports misses.
Axis-aligned boxes are the exception to the shared shape: aabb3_raycast_hit_point(result, result_offset, x0, y0, z0, x1, y1, z1, ox, oy, oz, dx, dy, dz) takes the box as world-space min/max, writes a six-tuple of hit position and normal, and returns a boolean rather than a distance. (aabb3_raycast is its deprecated alias.) For a pure “does it hit” test with no hit data, see aabb3_intersects_ray in Spatial acceleration.
These are the primitives the physics narrowphase refines a broadphase candidate against - see Spatial queries.
Mesh topology
BinaryTopology is the engine’s editable mesh representation: an interconnected vertex / edge / loop / face structure, modelled on Blender’s BMesh, stored in contiguous binary element pools rather than as objects. A “loop” is one corner of a face, and it is what threads the radial cycles (all faces around an edge) and the face cycles (all corners of a face).
import { BinaryTopology } from
"@woosh/meep-engine/src/core/geom/3d/topology/struct/binary/BinaryTopology.js";
import { bt_mesh_from_indexed_geometry } from
"@woosh/meep-engine/src/core/geom/3d/topology/struct/binary/io/bt_mesh_from_indexed_geometry.js";
import { bt_mesh_to_indexed_geometry } from
"@woosh/meep-engine/src/core/geom/3d/topology/struct/binary/io/bt_mesh_to_indexed_geometry.js";
const mesh = new BinaryTopology();
bt_mesh_from_indexed_geometry(mesh, indices, positions /*, normals */);
// … edit, query, simplify …
const { positions: out_positions, normals, indices: out_indices } =
bt_mesh_to_indexed_geometry(mesh);
bt_mesh_from_indexed_geometry clears the destination first and expects triangles. bt_mesh_to_indexed_geometry is the inverse: it renumbers vertices into a dense range (pools have holes once anything is released), emits Uint32Array indices regardless of vertex count, and throws if any face is not a triangle - run bt_mesh_triangulate first. Every allocated vertex is emitted, including ones no face uses; call bt_mesh_cleanup_faceless_references first to drop those.
Everything else is free functions taking the topology as the first argument, in two families under the same directory:
| Family | Directory | Examples |
|---|---|---|
| Editing | struct/binary/io/, plus io/edge/, io/face/, io/vertex/ | bt_mesh_triangulate, bt_mesh_resolve_t_junctions, bt_mesh_close_boundary_holes, bt_mesh_compact; edge/bt_edge_collapse, edge/bt_edge_split, edge/bt_edge_flip, edge/bt_mesh_fuse_duplicate_edges; face/bt_face_poke; vertex/bt_merge_vertices_by_distance |
| Query | struct/binary/query/ | bt_edge_is_manifold, bt_edge_is_boundary, bt_face_area, bt_face_read_triangle, bt_mesh_is_manifold, bt_mesh_compute_bounding_sphere, bt_mesh_compute_bounding_box, bt_mesh_build_face_bvh, bt_mesh_find_nearest_face, bt_mesh_walk_boundary_loops |
BinaryTopology and the bt_* functions are the only mesh topology in the engine; there is no object-based half-edge mesh.
Simplification and quadrics
bt_mesh_simplify(mesh, target_face_count, restricted_vertices, protected_faces) collapses edges in place until the face count reaches the target, driven by the quadric error metric. It returns the object-space distance the surface moved, or 0 when no collapse was performed.
import { bt_mesh_simplify } from
"@woosh/meep-engine/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js";
const deviation = bt_mesh_simplify(mesh, 2000);
Both optional Set<number> arguments are guards: restricted_vertices names vertices that must not move or be removed, protected_faces names faces that must still exist when the call returns. Pinning vertices does not by itself protect the edges between them - a collapse elsewhere in the fan can take away the face carrying such an edge - so a caller that needs a specific polyline to survive protects the faces that carry it. bt_mesh_simplify_with_quadrics is the variant that takes a caller-owned quadric set, which is how deviation is measured against a mesh several passes back rather than against this call’s input.
The quadric itself lives at core/geom/3d/quadric/. Quadric3 extends Float64Array and holds the upper triangle of a symmetric 4x4 form plus an accumulated weight; the quadric3_* functions (quadric3_add, quadric3_sub, quadric3_scale, quadric3_evaluate, quadric3_optimize, quadric3_distance, quadric3_to_tensor_m3, …) read that same layout out of any array-like at any offset, so a caller holding many quadrics keeps them in one flat buffer instead of one object each.
quadric3_evaluate returns the sum of squared distances to every plane folded in, so its magnitude grows with how much history the quadric carries. quadric3_distance divides by the weight first and takes the root, which is the value that is an object-space length.
Catmull-Rom splines
computeCatmullRomSpline samples a centripetal (or uniform) Catmull-Rom spline through an arbitrary set of N-dimensional control points.
import { computeCatmullRomSpline } from
"@woosh/meep-engine/src/core/math/spline/computeCatmullRomSpline.js";
const controlPoints = [0,0, 1,2, 3,1, 4,3]; // 4 points in 2D
const output = new Array(20 * 2); // 20 samples × 2 dimensions
computeCatmullRomSpline(
output,
controlPoints, 4, // input, point count
2, // dimensions per point
20, // number of output samples
0.5 // alpha (0 = uniform, 0.5 = centripetal, 1 = chordal)
);
The alpha parameter controls parameterisation: 0.5 (default) is centripetal and avoids cusps or self-intersections. A non-uniform variant computeNonuniformCatmullRomSplineSample lets you sample a single segment with explicit control points.
Related spline types with bounds and intersection tests are under the same directory: spline3_hermite, spline3_bezier, and their _bounds / _derivative / _integral companions.
Simplex and curl noise
Simplex noise
create_simplex_noise_2d returns a seeded 2D noise function with output in [-1, 1]:
import { create_simplex_noise_2d } from
"@woosh/meep-engine/src/core/math/noise/create_simplex_noise_2d.js";
const noise = create_simplex_noise_2d(Math.random);
const v = noise(x, y); // -1..1
The underlying 3D gradient-noise primitive sdnoise3 (from sdnoise.js) returns both the noise value and its analytical spatial derivative - useful for domain-warping and normal-map generation without finite differences.
noise_octaves layers multiple octaves of any noise function:
import { noise_octaves } from
"@woosh/meep-engine/src/core/math/noise/noise_octaves.js";
const value = noise_octaves(
noise, null, // noise function + `this` context for the call
x, y, z,
6, // octave count
0.5, // persistence (amplitude falloff)
2.0 // lacunarity (frequency multiplier)
);
Curl noise
curl_noise_3d computes a divergence-free 3D vector field from three offset simplex noise samples. The result is written into a caller-supplied array:
import { curl_noise_3d } from
"@woosh/meep-engine/src/core/math/noise/curl_noise_3d.js";
const result = [0, 0, 0];
curl_noise_3d(result, x, y, z);
// result[0], result[1], result[2] - divergence-free velocity
curl_noise_3dt adds a w (time) parameter for animated curl fields, displacing each noise sample along the time axis independently.
Robust geometric predicates
Two predicates delegate to the robust-predicates package for exact arithmetic, avoiding floating-point sign errors at degenerate configurations.
orient3d_robust(points, a, b, c, d) - tests whether point d is above, on, or below the plane defined by the counterclockwise triangle (a, b, c). points is a flat array of coordinates in stride-3 format ([x0, y0, z0, x1, y1, z1, ...]). Returns positive if d is above, negative if below, zero if coplanar.
in_sphere3d_robust(points, a, b, c, d, e) - tests whether point e is inside, on, or outside the circumsphere of the positively oriented tetrahedron (a, b, c, d). Returns positive if inside.
A fast (non-robust) variant orient3d_fast is available in the same directory for contexts where degenerate cases cannot occur.
import { orient3d_robust } from
"@woosh/meep-engine/src/core/geom/3d/plane/orient3d_robust.js";
import { in_sphere3d_robust } from
"@woosh/meep-engine/src/core/geom/3d/sphere/in_sphere3d_robust.js";
const pts = [/* flat x,y,z per point */];
const side = orient3d_robust(pts, 0, 1, 2, 3); // sign tells which side
const inside = in_sphere3d_robust(pts, 0, 1, 2, 3, 4); // sign tells in/out
These are used internally by the Delaunay/tetrahedral mesh code and the physics narrowphase.
Where to go next
- Spatial acceleration - the BVH and ray/frustum queries that consume these primitives.
- Color management - the color-space conversions built alongside this library.
- Spatial queries - physics raycasts and shape casts built on the BVH and the ray primitives above.
- Navigation meshes -
BinaryTopologyin anger: the navmesh is built, welded and simplified with thebt_*family.