Skip to content

@awaken/core

The engine foundation: math, the entity–component World, transforms, scene serialization, and a pure skeletal-animation core. It has no dependencies - no WebGPU, no DOM, no Rapier - so every symbol here is unit-testable in isolation, and every other package builds on it.

Everything is re-exported from the package barrel:

ts
import { World, Transform, mat4FromTRS, serializeWorld } from "@awaken/core";

The package exports a VERSION = "0.0.0" string constant. The modules below map one-to-one onto files in packages/core/src.

math

Column-major 4×4 matrices (WebGPU/WGSL convention) stored as Float32Array; vectors and quaternions are plain objects. All matrix functions write into an out argument and return it, so you can reuse buffers in hot loops.

SymbolSignatureDescription
Vec3{ x: number; y: number; z: number }3-component vector (plain object).
Quat{ x, y, z, w: number }Quaternion (plain object), w = 1 identity.
Mat4Float32Array (length 16)Column-major 4×4 matrix.
vec3(x?, y?, z?) => Vec3Construct a vector (defaults 0).
quat(x?, y?, z?, w?) => QuatConstruct a quaternion (defaults identity).
snap(v, step) => numberRound v to the nearest multiple of step (step ≤ 0 → unchanged).
mat4Identity(out?) => Mat4Write the identity matrix.
mat4Multiply(out, a, b) => Mat4out = a · b (column-major).
mat4FromTRS(out, t, r, s) => Mat4Compose translation/rotation/scale into a matrix.
mat4Invert(out, m) => Mat4Invert m (identity fallback for a singular matrix).
mat4OrthoZO(out, l, r, b, t, near, far) => Mat4Orthographic projection, WebGPU clip space (z ∈ [0,1]).
quatFromEuler(out, x, y, z) => QuatEuler (XYZ, radians) → quaternion.
quatToEuler(q) => Vec3Quaternion → Euler angles (radians), inverse of quatFromEuler.
quatMultiply(out, a, b) => QuatQuaternion product.
quatFromAxisAngle(out, ax, ay, az, angle) => QuatRotation of angle radians about an axis.
quatNormalize(out) => QuatNormalise in place (guards against drift).
quatRotateVec3(out, q, v) => Vec3Rotate a vector by a quaternion.

Projection helpers perspectiveZO, lookAt, cameraPositionFromView, and transformPoint live in @awaken/render (the camera module), not here.

entity

An Entity is a plain number that packs a 24-bit id with a generation counter (id + gen · 2²⁴), so it stays below 2⁴⁸ and remains an exact JS integer. The generation lets a freed id be reused without stale handles silently resolving to the new occupant.

SymbolSignatureDescription
EntitynumberPacked id + generation handle.
NULL_ENTITYEntity (0)The "no entity" sentinel (also used for "no parent").
entityId(e) => numberExtract the low 24-bit id.
entityGen(e) => numberExtract the generation.
EntityAllocatorclassAllocates/frees entities; allocate(), free(e), isAlive(e). Owned by World.

component

Components are data-only (no methods) and described by a reflection spec, so they serialize and appear in the Inspector for free. defineComponent returns a ComponentType<T> that the World stores and the serializer walks.

SymbolSignatureDescription
FieldTypeunionOne of "number" | "bool" | "string" | "vec3" | "quat" | "color" | "enum" | "assetRef" | "entityRef" | "colliderShapes" | "scriptList" | "scriptParams" | "entityRefMap".
FieldSpec{ name; type: FieldType; default; options?; showIf? }One field's reflection metadata. options lists enum values; showIf(values) hides a field in the Inspector conditionally.
ComponentType<T>{ name: string; fields: FieldSpec[]; create: () => T }A registered component type.
ComponentBase{ enabled?: boolean }Base every component interface extends - the per-component enable toggle.
ENABLED_FIELDFieldSpecThe enabled field (type:"bool", default true) auto-appended to every component so it serializes; hidden from the Inspector body.
defineComponent<T>(name, fields, create) => ComponentType<T>Define a component; appends ENABLED_FIELD automatically.

The enabled flag follows Unity's Behaviour.enabled: it is stored only when explicitly false, and systems skip a disabled component. See Component Reference for how each registered type uses this.

world

World is the ECS container: a sparse-set store per component type, plus a parent/child hierarchy, per-entity name/active/static flags, and change-tracking for the renderer. Component lookups are by ComponentType, not string.

Entity lifecycle

MethodSignatureDescription
createEntity() => EntityAllocate a new entity.
destroyEntity(e) => voidFree an entity, detach it from the hierarchy, and remove all its components.
isAlive(e) => booleanWhether the handle still refers to a live entity.
clear() => voidRemove every entity and component (resets the allocator).

Components

MethodSignatureDescription
add<T>(e, type, init?) => TAdd a component (created from type.create(), overlaid with init).
get<T>(e, type) => T | undefinedRead a component.
has(e, type) => booleanWhether the entity has the component.
remove(e, type) => voidRemove a component.
entitiesWith(type) => Entity[]Every entity carrying type.
query(...types) => Entity[]Entities carrying all given types (iterates the smallest store).
registryMap<string, ComponentType>Every component type the world has seen, keyed by name - the serializer's type list.

Hierarchy, names & flags

MethodSignatureDescription
setParent(child, parent) => voidReparent (NULL_ENTITY = detach). Rejects cycles.
getParent(e) => EntityParent, or NULL_ENTITY.
getChildren(e) => Entity[]Direct children (a copy).
getName / setName(e) => string / (e, name)Per-entity display name.
isActive / setActive(e) => boolean / (e, active)This entity's own enable flag.
isActiveInHierarchy(e) => booleanTrue only if the entity and every ancestor are active.
isStatic / setStatic(e) => boolean / (e, value)The entity-level Static flag (non-moving scenery; drives batching + physics). See Static Batching.

Change tracking

Two mechanisms let the renderer skip work on unchanged scenes:

MemberTypeDescription
structureRevnumberBumped on any structural change - entity create/destroy, component add/remove, active/static/parent change. A renderer compares it frame-to-frame; unchanged ⇒ the draw-cache topology is stable. Direct field writes (t.position) do not bump it.
markMoved(e) => voidFlag an entity whose Transform a system/script changed this frame.
movedEntitiesReadonlySet<Entity>The set of flagged entities (read by the render loop).
clearMoved() => voidClear the moved set (called once per frame by the sim owner).

This is what makes a scene of 15k static props plus one rotator cost O(1) per frame instead of O(15k) - see the render-frame performance notes.

transform

The Transform component (local position/rotation/scale) plus world-matrix computation.

SymbolSignatureDescription
TransformComponentType + interface{ position: Vec3; rotation: Quat; scale: Vec3 }. Defaults: pos 0, rot identity, scale 1.
worldTRS(world, e) => { position, rotation, scale }The entity's world TRS, composing every ancestor. Physics uses this so a nested collider lands on its mesh, not near the origin.
computeWorldMatrices(world) => Map<Entity, Mat4>World matrix for every entity with a Transform (full recompute; cycle-guarded).
updateWorldMatrices(world, out, moved) => Entity[]Incremental: recompute only the moved entities + descendants into out, reusing every other cached matrix. Returns the entities touched.

A Transform-less ancestor breaks the chain (the entity roots there rather than composing past the gap) - worldTRS and computeWorldMatrices agree on this so physics matches the render.

gameobject

A thin OO convenience wrapper over World + Entity, mirroring Unity's GameObject. Everything it does is a World call underneath, so you can mix the two freely.

SymbolSignatureDescription
GameObjectclassWraps (world, entity). name, transform, parent, children accessors + addComponent/getComponent/hasComponent/removeComponent.
createGameObject(world, name?) => GameObjectCreate an entity, name it, and ensure it has a Transform.

serialize

Scene ↔ JSON. SCENE_VERSION = 4 is the current scene-graph format; older scenes migrate forward on load. See Data Formats for the full shape and migration chain.

SymbolSignatureDescription
SCENE_VERSION4Current scene-graph format version.
SceneJSON{ version: number; entities: EntityJSON[] }A serialized scene.
EntityJSON{ id; name; parent; active?; static?; components }One serialized entity; active/static omitted at their defaults.
serializeWorld(world) => SceneJSONSerialize every entity in the world.
serializeEntities(world, entities, types?) => SceneJSONSerialize a specific set (parent links outside the set become null).
instantiateScene(world, json, types) => Entity[]Add a scene's entities to a world (no clear); returns the new root entities. Migrates + clones object fields so instances never alias.
applySceneToWorld(world, json, types) => voidworld.clear() then instantiateScene.
deserializeWorld(json, types) => WorldBuild a fresh World from a scene.
migrateScene(json) => SceneJSONForward-migrate a loaded scene to SCENE_VERSION (the single chokepoint every load passes through).
applyMigrations(json, migrations, current) => SceneJSONPure migration runner (injectable/testable).
SCENE_MIGRATIONSRecord<number, SceneMigration>The registered v1→v2→v3→v4 steps (collider + multi-script format changes).

instantiateScene runs in three passes - create entities, apply components (remapping entityRef/scriptList handles), then wire the hierarchy - so references resolve regardless of entity order.

animation

A pure animation core with zero GPU dependency: skinning (clip + time → local pose → world bone matrices → skinning matrices → CPU-skinned positions), pose blending for crossfades, per-frame root-motion extraction, a state-graph evaluator, and foot IK. The runtime's animationSystem wires it to the renderer.

SymbolSignature / ShapeDescription
Skeleton{ bones: Bone[] }A bone hierarchy; each Bone has name, parent (−1 = root), bind TRS, and inverseBind matrix.
AnimationClip{ name; duration; channels: AnimationChannel[] }A named clip; each channel drives one bone's translation/rotation/scale over keyframes.
SkinData{ meshId; joints; weights; basePositions }Per-vertex skin binding + read-only rest geometry for one skinned mesh.
LocalPose{ t: Vec3[]; r: Quat[]; s: Vec3[] }Per-bone local TRS for one sampled instant.
Interp"linear" | "step"Keyframe interpolation (cubicspline sampled as linear).
samplePose(skel, clip, time) => LocalPoseSample a clip → per-bone local pose (undriven bones keep their bind pose).
poseMatrices(skel, pose) => Mat4[]Forward kinematics → per-bone world matrices.
skinningMatrices(skel, world) => Mat4[]world · inverseBind per bone (the matrices LBS multiplies by).
skinPositions(positions, joints, weights, skin, influences?) => Float32ArrayLinear-blend-skin vertex positions on the CPU.
blendPoses(a, b, w) => LocalPosePer-bone slerp/lerp blend of two local poses - the primitive behind clip crossfades.
sampleRootMotion(clip, time) => { x, z }Sample a clip's baked horizontal root-motion track (the per-frame delta the controller applies).
AnimGraph / nextTransition / graphState / conditionPassesdata + evaluatorPure animation state-graph: states, declarative-condition transitions, "*" any-state, one-shot triggers.
applyFootIK / FootIKConfig(skel, pose, cfg, groundY) => Mat4[]Two-bone leg IK + pelvis adjust + plant weight that grounds planted feet.
vec3Lerp, quatSlerp, sampleVec3, sampleQuathelpersInterpolation + channel sampling primitives.

particles

The particle system's data + simulation core - also GPU-free. simulateEmitter is a pure, deterministic integrator: it advances a live-particle pool and writes a flat instance buffer that the renderer uploads. This is the seam a future GPU-compute backend would replace; everything above it (the effect asset, the component, the 16-float record) stays put.

SymbolSignature / ShapeDescription
ParticleEffectconfig objectOne effect's full definition: emission (rate, bursts, maxParticles, duration, loop, oneShot), per-particle init (lifetime+variance, shape, speed+variance, spreadDeg), forces (gravity, drag), look over life (color gradient, sizeStart, sizeCurve, rotationSpeed), and render (blend, texture, worldSpace, softness, stretch).
EmissionShape{ kind: "point" } | { kind: "sphere"; radius } | { kind: "box"; half } | { kind: "cone"; angleDeg; radius }Where particles spawn, in emitter-local space.
ColorGradient / Curve{ stops: { t; color }[] } / { keys: { t; v }[] }Colour and scalar over normalized lifetime (t 0→1, sorted). Sampled on the CPU, so the GPU never needs a LUT.
ParticleEmitterComponentType<{ effect: string; playing: boolean }>Component placing an effect on an entity; effect is a ParticleEffect asset id. See Components.
EmitterStateSoA poolOne emitter's live particles (position/velocity/life/rotation/size arrays) + a seeded PRNG. Side-state - never serialized.
makeEmitterState(cfg, seed?) => EmitterStateAllocate a pool sized to cfg.maxParticles with a deterministic PRNG (never Math.random).
simulateEmitter(state, cfg, dt, worldMatrix, out) => countAdvance by dt and write count × PARTICLE_FLOATS floats into out; returns the live count. out must hold state.capacity × PARTICLE_FLOATS.
sampleGradient / sampleCurve(g, t) => [r,g,b,a] / (c, t) => numberLinear sampling, t clamped to 0..1.
PARTICLE_FLOATS12Per-particle GPU instance record: pos.xyz, size, rgba, rotation, vel.xyz.

See also

Awaken — browser-native WebGPU game engine.