Data Formats & Versions
Awaken3D uses three distinct serialization formats, each with its own version number and migration path. They share the same scene graph but differ in how assets are stored and what they are for. Alongside them sit the small portable content files (.awakenfx, .awakenmat, .awakenprefab, …) you import and export one asset at a time - covered at the end of this page.
| Format | Version const | Where it lives | Encoding |
|---|---|---|---|
In-memory scene (SceneJSON) | SCENE_VERSION = 4 | inside every project + ship file | plain JSON |
Binary project (.awaken) | PROJECT_VERSION = 3 | the editor's save file | FRGE binary container |
Ship scene (SceneFile) | SCENE_FILE_VERSION = 1 | the exported scene.awaken.json | JSON (self-contained) |
The key idea: the scene graph is one shape (SceneJSON), embedded verbatim in both a project and a ship file. What changes between formats is how the assets those entities reference - meshes and textures - are packed, and what extra editor/game metadata rides along.
In-memory scene (SceneJSON v4)
Defined in @awaken/core. A scene is a version number plus a flat list of entities; each entity records its id, name, parent, optional active/static flags, and a map of component data.
json
{
"version": 4,
"entities": [
{ "id": 0, "name": "Ground", "parent": null, "static": true,
"components": { "Transform": { "position": {"x":0,"y":-0.25,"z":0} }, "MeshRenderer": { "mesh": "cube" } } },
{ "id": 1, "name": "Player", "parent": null,
"components": { "Transform": {}, "Script": { "scripts": [ { "name": "controller", "params": {}, "enabled": true } ] } } }
]
}Fields are driven by reflection: every component built with defineComponent serializes each FieldSpec automatically (so a new field ships with no serializer changes). Special field types remap on save/load - entityRef/entityRefMap/scriptList store entity handles as positional save-ids that resolve back to live entities on load.
Scene migrations
SCENE_VERSION has advanced from 1 to 4; a loaded scene is forward-migrated through SCENE_MIGRATIONS at the single migrateScene chokepoint every load passes through. A scene authored by a newer build loads best-effort with a console warning instead of being rejected.
| Step | What changed |
|---|---|
| 1 → 2 | Collider gained a per-part enable/trigger toggle (mesh colliders became a parts list). |
| 2 → 3 | Collider gained an independent box size (half-extents) + center offset. |
| 3 → 4 | An object can carry multiple colliders and multiple scripts - Collider flat fields became a shapes list; Script flat fields became a scripts list. |
An unversioned legacy file is treated as v1. Missing/misbehaving migration steps stop the chain gracefully (load what we can) rather than refusing the file.
Binary project (.awaken, v3)
Defined in @awaken/assets. The editor's save file is a glTF-.glb-style container: a 12-byte header, a JSON metadata chunk, then one aligned binary blob holding every mesh/texture's raw bytes, referenced from the metadata by [offset, length] spans.
┌─────────────────────────────────────────────┐
│ magic "FRGE" (u32) · version (u32) · jsonLen │ 12-byte header
├─────────────────────────────────────────────┤
│ JSON metadata (scene, scenes, prefabs, │ scene graph + all
│ scripts, materials, render, dock, mesh/tex │ editor metadata
│ span tables) │
├─────────────────────────────────────────────┤
│ binary blob: mesh buffers + texture pixels │ 4-byte-aligned spans
│ (referenced by [offset,length]) │
└─────────────────────────────────────────────┘The metadata JSON embeds the SceneJSON verbatim under scene (plus the scene library, prefabs, scripts, materials, animation graphs, particle effects, and editor render/dock settings). Version 2 onward stores meshes quantized (positions u16 over the AABB, normals octahedral int8, uvs u16 over the UV bbox); v1 stored raw Float32. Either way decodeProject dequantizes back to Float32Array, so the in-memory MeshData - and the renderer's vertex layout - is identical regardless of version. Indices and vertex colours are kept exact. Version 3 added the skeletal assets (skeletons, baked clips, the rig-space clip library, skins) to the metadata; particleEffects rides as an optional metadata field with no binary-layout change, so older projects still load and simply have none.
Particle effects are the one asset kind saved wholesale: they are content with no engine built-ins, so anything dropped on save would be lost forever.
Only assets actually referenced by the scene/prefab/material graph are packed, so unused imports don't bloat the file. See Projects.
Ship scene (SceneFile JSON, v1)
Defined in @awaken/assets. This is the self-contained JSON the standalone player fetches as scene.awaken.json. Unlike the project, it must JSON-stringify cleanly and stand alone, so mesh buffers are base64 typed-array bytes (MeshJSON, optionally quantized) rather than a binary blob.
SceneFile carries what a shipped game needs and nothing editor-only:
scene(the boot world) +scenes(the openable library) +entry(which scene boots first),meshes(base64/quantizedMeshJSON),textures(base64 WebP/PNG with arole),scripts(id →{ name, source }) andcompiled(id → precompiled JS, so the player needs no TS compiler),prefabs,materials/meshMaterials,shaders(hook materials), and the gametitle,- the runtime asset maps a shipped game needs to actually play:
skeletons,clips,skins,animGraphs,particleEffects, anduiClips(UI animation clips) - all JSON-safe, registered into the player's asset store byloadSkeletalAssets, fonts(imported.ttf/.otf, base64'd) - registered as@font-faceby the player so aUINode.fontrenders in the pack's typeface.
Particle effects are shipped wholesale rather than reference-filtered: a script fires one with api.burst("sparks"), and that id lives inside a string no walk of the component data can see, so filtering would silently ship a game whose bursts do nothing. Effects are a handful of gradient/curve keys each, so carrying them all costs nothing measurable.
The export seam: bundleToSceneFile
bundleToSceneFile(bundle) is the pure function that converts an editor ProjectBundle into a shippable SceneFile. It re-encodes meshes to MeshJSON, keeps only the entry scene's referenced data, and drops empty optional maps to stay lean.
Because it is pure (no browser canvas), it currently ships geometry + vertex/material colour but not albedo textures - encoding base64 PNG from raw texel bytes needs a canvas the editor supplies as a separate step before shipping. An optional binary ship container (encodeSceneContainer/decodeSceneContainer) wraps a SceneFile + its meshes into one blob for the compressed export path.
See Export to a game file for the full build pipeline.
Portable content files
Single assets also move between projects as small hand-editable JSON files, each with its own version. Most carry a awaken discriminator so a wrong file is rejected with a clear message rather than half-imported. The editor accepts them from Assets ▸ Import or a drag-and-drop.
| File | awaken tag | Carries |
|---|---|---|
.awakenfx | "particleEffect" | one particle effect blueprint |
.awakenprefab | "prefab" | one prefab's SceneJSON (references-only, points at its assets by id) |
.awakenpkg | (binary) | a self-contained package: any asset selection plus every dependency, in one binary bundle (package: true) |
.awakenscript | "script" | one script's id, name and TypeScript source (kind may be runtime, editor, or plugin) |
.awakenanimgraph | "animgraph" | one animation state machine |
.awakenmat | (none) | one hook material - validated by shape (id, name, hooks, params) |
Particle effect (.awakenfx)
json
{
"awaken": "particleEffect",
"version": 1,
"id": "sparks",
"name": "Sparks",
"effect": {
"rate": 0,
"bursts": [{ "time": 0, "count": 40 }],
"maxParticles": 64,
"lifetime": 0.45,
"shape": { "kind": "cone", "angleDeg": 35, "radius": 0.02 },
"speed": 3.5,
"gravity": [0, -9.8, 0],
"color": { "stops": [{ "t": 0, "color": [1, 1, 0.9, 1] }, { "t": 1, "color": [0.4, 0.05, 0, 0] }] },
"sizeCurve": { "keys": [{ "t": 0, "v": 1 }, { "t": 1, "v": 0 }] },
"blend": "additive",
"texture": ""
}
}The id is the binding key - a ParticleEmitter.effect field and every api.burst("<id>") resolve against it, never against the file name. On both serialize and parse the file's id/name are forced onto the inner effect, so a hand-renamed file can't disagree with the id the asset store keys it under. Importing is keyed by that id too: re-importing an edited effect updates every emitter already pointing at it instead of forking a copy.
Parsing is forgiving about what is present and strict about what is valid:
- Every field of
effectis optional and falls back to the engine default, so a two-line "just make it red" effect imports fine. - Numbers must be finite (a
NaNposition would never die - it would just render nothing forever), and out-of-range values are clamped rather than rejected (drag: -1becomes0). - Gradient
stopsand curvekeysmust be non-empty, within[0,1], and sorted ascending byt- the integrator samples them assuming that contract, so a bad file is rejected at import rather than sampled wrong. Gradient alpha is clamped to[0,1]; RGB is not clamped above 1 (additive blending legitimately over-drives). shape.kindmust bepoint|sphere|box|cone, and each kind's own fields are required (a cone missingangleDegwould spawnNaNdirections).blendmust beadditiveoralpha.burstsmust be sorted ascending bytime; counts are rounded and floored at 0.- A
versionnewer than the reader's (1) is rejected outright.
Prefabs declare their effect dependencies
A .awakenprefab stores component data, not the assets it points at. prefabDependencies(scene) walks its entities and reports four kinds of referenced id - scripts, materials, animation graphs, and particle effects - and the importer resolves each missing one by asking you to locate its file before the prefab lands. This exists because an unresolved reference fails silently: a prefab whose ParticleEmitter names an effect the project doesn't have imports "successfully" and then emits nothing at all. Anything you skip is listed as unresolved in the console, and the prefab imports inert until you add it later. (Built-in animation graph ids, prefixed builtin:, are never reported - they are always available.)
Drag in a whole folder and dependencies resolve automatically, since folders are read recursively. A .awakenpkg package sidesteps the locate step entirely: assemblePackage runs the same dependency walk at export time and embeds every referenced asset, so importing one only merges (deduplicating by id) and never prompts.
How the three relate
- Author in the editor → the live
WorldisSceneJSON(v4) in memory. - Save project →
encodeProjectwrites the.awakenbinary (v3), embedding thatSceneJSON. - Export game →
bundleToSceneFileconverts the bundle to aSceneFile(v1), embedding the sameSceneJSON, which the playerloadScenes.
Each format version bumps independently: a scene-graph change bumps SCENE_VERSION; a change to how the project packs assets bumps PROJECT_VERSION; a change to the ship file bumps SCENE_FILE_VERSION. Each has its own migration/tolerance path so old files keep loading.
See also
- @awaken/core → serialize - the
SceneJSONAPI and migration functions. - @awaken/assets → persistence -
ProjectBundle,SceneFile, encode/decode. - Scenes & Game Structure - the scene/library/project model in plain terms.
- Particles - what the
.awakenfxfields actually do. - Export to a game file - where
bundleToSceneFileruns. - The Player Runtime - what consumes the
SceneFile.