math

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

math

math

Write data-oriented TypeScript on top of the npm
math
package: plain data, free functions, no classes, no allocation in hot paths.
API docs are in
API.md
— every export with its signature, grouped by module and flat enough to grep. Find it at
node_modules/math/API.md
in a consuming project, or at the repo root when working on
math
itself.
基于 npm
math
包编写面向数据的 TypeScript:使用纯数据和自由函数,不使用 class,也不在热路径中分配内存。
API 文档位于
API.md
——包含每个导出项及其签名,按模块分组,结构扁平,便于 grep。在消费方项目中可于
node_modules/math/API.md
查找;直接开发
math
本身时,则位于仓库根目录。

Types

类型

Every type is a plain fixed-length tuple of numbers — no classes, no wrappers, and not a typed array:
  • Vec2
    [x, y]
    ,
    Vec3
    [x, y, z]
    ,
    Vec4
    [x, y, z, w]
  • Quat
    [x, y, z, w]
    ,
    Quat2
    [x, y, z, w, x2, y2, z2, w2]
  • Euler
    [x, y, z, order?]
    , radians, order defaulting to
    'xyz'
  • Mat2
    (4),
    Mat2d
    (6),
    Mat3
    (9),
    Mat4
    (16) — contiguous and column-major, with
    Mat4
    translation in
    m[12]
    ,
    m[13]
    ,
    m[14]
  • Polar
    [r, theta]
    ,
    Spherical
    [r, theta, phi]
每种类型都是普通的定长数字元组——不使用 class、wrapper,也不是 typed array:
  • Vec2
    [x, y]
    、
    Vec3
    [x, y, z]
    、
    Vec4
    [x, y, z, w]
  • Quat
    [x, y, z, w]
    、
    Quat2
    [x, y, z, w, x2, y2, z2, w2]
  • Euler
    [x, y, z, order?]
    ,使用弧度,order 默认为
    'xyz'
  • Mat2
    (4 个元素)、
    Mat2d
    (6 个元素)、
    Mat3
    (9 个元素)、
    Mat4
    (16 个元素)——连续存储并采用列主序,
    Mat4
    的平移分量位于
    m[12]
    、
    m[13]
    、
    m[14]
  • Polar
    [r, theta]
    、
    Spherical
    [r, theta, phi]

Style

风格

  • Functions over data. Export
    function
    declarations that take typed data and operate on it. Never classes for data, never closures that hold state. The one exception is a small fixed set of classes implementing a single structural type — a collector handed to a query to receive its hits, say — where the call site is polymorphic and a stable hidden class keeps it fast.
  • out
    first, return
    out
    for composite results:
    fn(out: Vec3, a: Vec3, b: Vec3): Vec3
    . Scalars and booleans return directly.
  • Use result objects and
    out
    params over returning new objects.
    When a result doesn't fit a vector, define a result type with a
    createXResult()
    factory beside it; report failure with a boolean or status enum rather than
    out | null
    .
  • Assume the caller aliases — the same array may arrive as both
    out
    and an input, as in
    vec3.normalize(v, v)
    or
    vec3.cross(a, a, b)
    . Read every input component into a local before the first write to
    out
    , so a write can't clobber an input still needed.
  • Caller-owned state. Long-lived state is plain data the caller allocates and owns. Functions receive it, mutate it in place, and return it. The library never owns the data lifecycle, so allocation happens once, ownership is explicit, and the object keeps one stable shape. Naming and file layout are up to the codebase. One common shape:
ts
export function createWorld(capacity: number) {
    return { capacity, count: 0, positions: new Float32Array(capacity * 3) };
}
export type World = ReturnType<typeof createWorld>;

export function stepWorld(world: World, delta: number): World { /* mutate, return world */ }
export function getWorldPosition(out: Vec3, world: World, i: number): Vec3 { /* write out, return it */ }
  • Monomorphic state. Build the object with the same keys in the same order every time. No optional fields, no keys added later.
  • Allocate at creation, never per call. Preallocate flat or typed arrays to capacity. When full, return a count, sentinel, or status rather than growing inside a hot loop.
  • Compose
    math
    primitives
    (
    vec3
    ,
    mat4
    ,
    quat
    , and the
    math/shapes
    ,
    math/geometry
    ,
    math/noise
    ,
    math/random
    ,
    math/time
    subpaths).
  • Hoist invariants out of loops
  • 以函数处理数据。 导出接收类型化数据并进行操作的
    function
    声明。绝不为数据使用 class,也不要使用持有状态的闭包。唯一的例外是少量固定的 class,它们实现单一结构类型——例如传给 query 以接收命中的 collector——在这些场景中,调用点是多态的,而稳定的 hidden class 可保持性能。
  • 对于复合结果,
    out
    放在首位,并返回
    out
    :
    fn(out: Vec3, a: Vec3, b: Vec3): Vec3
    。标量和布尔值直接返回。
  • 优先使用结果对象和
    out
    参数,而不是返回新对象。
    当结果无法用向量表示时,定义结果类型,并在其旁提供
    createXResult()
    工厂函数;通过布尔值或状态枚举报告失败,而不是使用
    out | null
    。
  • 假设调用方可能传入别名——同一个数组可能同时作为
    out
    和输入,例如
    vec3.normalize(v, v)
    或
    vec3.cross(a, a, b)
    。在第一次写入
    out
    之前,将每个输入分量都读入局部变量,以防写入覆盖仍需使用的输入。
  • 状态由调用方拥有。 长期状态是由调用方分配并拥有的纯数据。函数接收这些数据,原地修改后返回。库永不拥有数据的生命周期,因此只分配一次,所有权明确,对象保持稳定结构。命名和文件布局由代码库决定。一种常见形式如下:
ts
export function createWorld(capacity: number) {
    return { capacity, count: 0, positions: new Float32Array(capacity * 3) };
}
export type World = ReturnType<typeof createWorld>;

export function stepWorld(world: World, delta: number): World { /* mutate, return world */ }
export function getWorldPosition(out: Vec3, world: World, i: number): Vec3 { /* write out, return it */ }
  • 单态状态。 每次都使用相同顺序的相同键构建对象。不使用可选字段,也不后续添加键。
  • 仅在创建时分配,绝不按调用分配。 按容量预分配扁平数组或 typed array。空间已满时,返回计数、哨兵值或状态,而不是在热循环中扩容。
  • 组合使用
    math
    基础原语
    (
    vec3
    、
    mat4
    、
    quat
    ,以及
    math/shapes
    、
    math/geometry
    、
    math/noise
    、
    math/random
    、
    math/time
    子路径)。
  • 将不变量提取到循环外。

Gotchas

注意事项

  • Module-level scratch is named
    _owner_purpose
    . Grow-once buffers carry an explicit size counter rather than
    push
    /
    pop
    /
    length = 0
    .
  • Module-level scratch variables are not reentrant. Pass caller-owned workspace for recursive, nested, or worker code.
  • One epsilon does not fit every operation or scale. Choose each tolerance and say why.
  • Define behavior for empty input, zero-length vectors, degenerate geometry, NaN, and exact boundary contact.
  • Compare squared distances; reach for
    squaredLength
    /
    squaredDistance
    over their square-rooted pairs.
  • Integers in the range [-2^30, 2^30) are stored in the pointer itself (V8 Smi), with no heap object. Use them for indices, handles, packed IDs, bitmasks, and counts.
  • Where possible avoid plain array element kind transitions (small integers, doubles, array with holes). Especially avoid unnecessary SMI or PACKED_DOUBLE to holey transitions when holey arrays are not desired.
  • Keep hot-path storage consistent. Mixing plain arrays and typed arrays in the same math function can make element accesses polymorphic and reduce optimization. The tuple types help enforce this consistency. Casting a
    Float32Array
    to
    Vec3
    doesn't change its runtime storage. Marshal at boundaries when needed to keep the hot path monomorphic.
  • Don't assume a typed array is faster. A packed plain array is already unboxed and can still grow. Typed arrays buy footprint, a fixed layout, and zero-copy interop with workers, Wasm, and the GPU; they cost a capacity fixed up front and, for
    Float32Array
    , a narrowing conversion on every write. Choose one for interop or memory, not on a hunch about speed.
  • In a library, annotate module-level factory calls
    /* @__PURE__ */
    so a consumer's bundler can drop the scratch when the function is tree-shaken out. Application code does not need it.
Deliver the implementation with its assumptions, complexity, edge cases, and focused tests.
  • 模块级 scratch 变量命名为
    _owner_purpose
    。仅扩容一次的缓冲区使用显式大小计数器,而不是
    push
    /
    pop
    /
    length = 0
    。
  • 模块级 scratch 变量不可重入。对于递归、嵌套或 Worker 代码,应传入调用方拥有的 workspace。
  • 单一 epsilon 并不适用于每种运算或规模。应为每种运算选择合适的容差并说明原因。
  • 定义空输入、零长度向量、退化几何、NaN 和恰好处于边界接触状态时的行为。
  • 比较距离平方;优先使用
    squaredLength
    /
    squaredDistance
    ,而不是对应的开平方版本。
  • [-2^30, 2^30)
    范围内的整数直接存储在指针本身(V8 Smi)中,不产生堆对象。将其用于索引、句柄、打包 ID、位掩码和计数。
  • 尽可能避免普通数组的 element kind 转换(小整数、双精度数、带洞数组)。尤其在不需要 holey 数组时,避免不必要的 SMI 或 PACKED_DOUBLE 到 holey 的转换。
  • 保持热路径存储一致。 在同一个数学函数中混用普通数组和 typed array,可能使元素访问变成多态并降低优化效果。元组类型有助于维持这种一致性。将
    Float32Array
    强制转换为
    Vec3
    并不会改变其运行时存储方式。必要时在边界处进行数据转换,以保持热路径单态。
  • 不要想当然地认为 typed array 更快。紧凑普通数组已经采用非装箱存储,并且仍可增长。Typed array 的优势在于内存占用更小、布局固定,以及可与 Worker、Wasm 和 GPU 进行零拷贝互操作;其代价是容量必须预先固定,并且使用
    Float32Array
    时,每次写入都需要进行窄化转换。应根据互操作或内存需求进行选择,而不是凭直觉判断速度。
  • 在库中,为模块级工厂调用添加
    /* @__PURE__ */
    注解,这样当函数被 tree-shaking 移除时,消费方的 bundler 也可以丢弃 scratch。应用代码无需这样做。
交付实现时,应说明其假设、复杂度、边界情况,并提供针对性测试。

Working with other libs

与其他库协作

Marshal in, compute, marshal out — and allocate on neither crossing. Keep the scratch
math
types at module scope, fill them from the other library's values, run the algorithm as plain
math
calls, then write the results back. The seam is a few lines at each end of a function; everything between them is flat data.
  • Marshal with whatever writes into memory you already own. For flat buffers — an instanced attribute, a packed particle array — that's
    vec3.fromBuffer(out, buffer, i * 3)
    and
    vec3.toBuffer(buffer, v, i * 3)
    (also on
    vec2
    ,
    vec4
    ,
    quat
    ), or
    buffer.set(m, i * 16)
    for a whole matrix, since
    TypedArray.set
    takes any array-like.
  • State that crosses a worker, Wasm, or GPU boundary lives in a typed array from the start. A plain-array
    Vec3
    can't be transferred or shared, so back the long-lived data with
    Float32Array
    /
    SharedArrayBuffer
    at creation and marshal at the edges.
传入时进行数据转换、执行计算、传出时进行数据转换——且在两个转换边界处都不分配内存。将 scratch
math
类型保留在模块作用域,从其他库的值填充这些类型,以普通
math
调用运行算法,再将结果写回。衔接层只是函数首尾的几行代码;中间部分全部是扁平数据。
  • 使用能够直接写入你已拥有的内存的方式进行数据转换。 对于扁平缓冲区——例如实例化属性或紧凑粒子数组——可以使用
    vec3.fromBuffer(out, buffer, i * 3)
    和
    vec3.toBuffer(buffer, v, i * 3)
    (
    vec2
    、
    vec4
    、
    quat
    也同样适用);对于整个矩阵,可以使用
    buffer.set(m, i * 16)
    ,因为
    TypedArray.set
    接受任何 array-like。
  • 跨越 Worker、Wasm 或 GPU 边界的状态从一开始就应存放在 typed array 中。 普通数组形式的
    Vec3
    无法 transfer 或 share,因此应在创建时使用
    Float32Array
    /
    SharedArrayBuffer
    承载长期数据,并在边界处进行数据转换。

three.js

three.js

Choose the integration based on who drives the hot path:
  • Extend when math updates many objects each frame. Use
    extend(scene)
    from
    math/three
    and cached transform records.
  • Don't extend when mostly using Three's transform API, frequently rebuilding the hierarchy, or only needing occasional math. Keep stock objects and marshal through reusable scratch.
The
math/three
helpers for matrices, instancing, attributes, and culling work either way. See
API.md
for their usage. Instance matrix views use float32 storage, so keep simulation state separate.
What gets faster. The tuple path can substantially speed up CPU transform updates by eliminating copies between math and Three, deferring quaternion-to-Euler conversion until Euler angles are read, and composing local matrices and propagating world matrices in a flat pass. Gains depend on the workload, so compare representative frames.
Use the hot path. For objects already in
scene
, extend and cache records at setup. Mutate their tuples directly in the loop.
rotation
is a quaternion.
ts
import { quat, vec3, type Vec3 } from 'math';
import { extend, transformOf } from 'math/three';

extend(scene);
const transforms = objects.map(transformOf);

function step(velocities: Vec3[], delta: number): void {
    for (let i = 0; i < transforms.length; i++) {
        const t = transforms[i];
        vec3.scaleAndAdd(t.position, t.position, velocities[i], delta);
        quat.rotateY(t.rotation, t.rotation, delta);
    }
}
Rendering propagates matrices automatically. Call
propagate(scene)
once before queries that need current world matrices. Cache records only while objects stay extended.
What costs more. Extending allocates records and replaces transform properties with views. Adding, removing, or reparenting objects requires rebuilding the flat traversal. Three-style property access goes through accessors, and Euler reads check for quaternion changes and derive angles when needed. Heavy use of these paths can offset the tuple gains. Custom
updateMatrixWorld
overrides retain their own traversal.
Without extension. This performs the same update on stock Three objects. Allocate scratch once, marshal in, compute, and marshal out. Always pass a target to
toArray
to avoid allocation.
ts
import { quat, vec3, type Vec3 } from 'math';
import type { Object3D } from 'three';

const _step_position = vec3.create();
const _step_rotation = quat.create();

function step(object: Object3D, velocity: Vec3, delta: number): void {
    object.position.toArray(_step_position);
    object.quaternion.toArray(_step_rotation);
    vec3.scaleAndAdd(_step_position, _step_position, velocity, delta);
    quat.rotateY(_step_rotation, _step_rotation, delta);
    object.position.fromArray(_step_position);
    object.quaternion.fromArray(_step_rotation);
}
根据谁驱动热路径选择集成方式:
  • 扩展适用于 math 每帧更新许多对象的情况。使用
    math/three
    中的
    extend(scene)
    和缓存的 transform 记录。
  • 不要扩展的情况包括:主要使用 Three 的 transform API、频繁重建层级,或只需要偶尔进行数学计算。此时应保留原生对象,并通过可复用的 scratch 进行数据转换。
math/three
提供的矩阵、实例化、属性和剔除辅助函数在两种方式下都可使用。用法见
API.md
。实例矩阵视图使用 float32 存储,因此应将仿真状态分开保存。
哪些部分会更快。 元组路径可以通过消除 math 与 Three 之间的复制、推迟 quaternion 到 Euler 的转换直至读取 Euler 角度,以及在扁平遍历中组合局部矩阵并传播世界矩阵,显著加快 CPU transform 更新。实际收益取决于工作负载,因此应对代表性帧进行比较。
使用热路径。 对于已经位于
scene
中的对象,在初始化时执行扩展并缓存记录。在循环中直接修改它们的元组。
rotation
是四元数。
ts
import { quat, vec3, type Vec3 } from 'math';
import { extend, transformOf } from 'math/three';

extend(scene);
const transforms = objects.map(transformOf);

function step(velocities: Vec3[], delta: number): void {
    for (let i = 0; i < transforms.length; i++) {
        const t = transforms[i];
        vec3.scaleAndAdd(t.position, t.position, velocities[i], delta);
        quat.rotateY(t.rotation, t.rotation, delta);
    }
}
渲染会自动传播矩阵。在执行需要当前世界矩阵的查询之前,调用一次
propagate(scene)
。仅在对象保持扩展状态期间缓存记录。
哪些部分成本更高。 扩展会分配记录,并用视图替换 transform 属性。添加、删除或重新设置父级都需要重建扁平遍历。Three 风格的属性访问会经过 accessor,而读取 Euler 时会检查四元数是否发生变化,并在需要时推导角度。大量使用这些路径可能抵消元组带来的收益。自定义的
updateMatrixWorld
覆写仍保留自身的遍历逻辑。
不进行扩展时。 在原生 Three 对象上执行相同更新。只分配一次 scratch,将数据转换后传入,执行计算,再转换后写出。始终为
toArray
提供目标参数,以避免分配。
ts
import { quat, vec3, type Vec3 } from 'math';
import type { Object3D } from 'three';

const _step_position = vec3.create();
const _step_rotation = quat.create();

function step(object: Object3D, velocity: Vec3, delta: number): void {
    object.position.toArray(_step_position);
    object.quaternion.toArray(_step_rotation);
    vec3.scaleAndAdd(_step_position, _step_position, velocity, delta);
    quat.rotateY(_step_rotation, _step_rotation, delta);
    object.position.fromArray(_step_position);
    object.quaternion.fromArray(_step_rotation);
}