01 — Architecture
Subsystem diagram: architecture.html · Overview: index.html · Index: 00 — Engine index
Writing behaviors? Skim the two-artifact table below, then jump to 02 — Runtime Basics and 14 — API Guide. Come back here when you need GUID matching or the repo layout.
The core idea: two artifacts
If glTF can already express it, the glb owns it and the engine writes no code for it. If it can't, it goes in the manifest and a small subsystem applies it.
For how the loaded Level hooks into Babylon's render loop (runRenderLoop, OnUpdate, delta time), read 02 — Runtime Basics next.
| Artifact | Carries | Consumed by |
|---|---|---|
level.glb | geometry, transforms, hierarchy, materials (standard PBR), lights, cameras, animation clips | Babylon's glTF importer |
level.scene.json | components (TAG/RENDERING_GROUP/LAYER_MASK/COLLISION_LAYER/COLLIDER/RIGIDBODY/SCRIPT/CAMERA/AUDIO/CONSTRAINT/…), optional materials[] (Node Material Editor overrides), per-light/camera/scene settings, scene.inputActions + scene.defaultInputMap, scene.collisionLayers, schema v4 | our LevelLoader |
The manifest never duplicates the glb — it only adds. This is why there's a subsystems/physics/ but no meshes.ts: meshes ride entirely in the glb.
Entity identity (GUIDs)
Every addressable Blender object gets a GUID in a custom property (bjs_id, defined once as ID_KEY on both sides). The exporter writes it into the glTF node's extras; the manifest references the same GUID. At load it surfaces at node.metadata.gltf.extras.bjs_id (via the ExtrasAsMetadata loader extension) and the loader matches manifest entity → glb node by GUID, name as a legacy fallback.
A GUID is what makes an object an entity; pure geometry without one just lives in the glb. GUIDs are auto-assigned to anything that needs to be findable: objects with components, lights, cameras, and any object referenced by an entity field, trigger event, or constraint target.
The GUID is also the discriminator for two subtle cases: multi-material submeshes have no GUID (so they're "owned" by their wrapper — see Physics), while real parented children do (so they're excluded from a parent's collider).
Visibility and shadows (export → load)
Optional detail — skip on first read unless you're debugging invisible objects or shadow casters.
- Viewport hidden (Blender eye icon, still render-enabled) — transient
bjs_visible: false(VISIBLE_KEY) in glTF extras;ApplyNodeVisibilitysetsnode.isVisible = falseand disables child lights/cameras. - Collider › Make Invisible —
makeInvisible: trueon the COLLIDER row; the loader's physics handler callsHideEntityNodeso collision-only props can stay visible in Blender while shipping invisible. - Shadow ray off (
visible_shadow = False) —bjs_cast_shadows: 0(CAST_SHADOWS_KEY); meshes stay visible and receive shadows, butSetupShadowsomits them from casters. - Render disabled (
hide_render, camera icon) — omitted from glb and manifest (use_renderable=Trueon the glTF exporter matches the manifest filter).
Monorepo layout
bjs-level-kit/
package.json # npm workspaces root: dev / typecheck / create
packages/engine/ # "@bjs/engine" — the runtime, shared by every app
src/
index.ts # public barrel (apps import "@bjs/engine")
core/ # types.ts (schema) · attachments.ts · Entity.ts · Level.ts · LevelLoader.ts
# loader/ (manifest · nodeResolution · entityBuilder · componentRegistry · scripts · sceneSettings)
scripting/ # Behavior.ts · exposed.ts · BehaviorRegistry.ts
input/ # InputManager · maps/actions/bindings · @inputMap
subsystems/ # physics lights cameras/ shadows constraints audio
# triggers environment fog atmosphere postprocess animation
# materials (NME overrides)
apps/ # each game is an app; engine reaches it via symlink
playground/ # the dev/test app (Vite); template for npm run create
blender_addon/ # the editor half (Python, Blender 4.2+ extension)
scripts/create-app.mjs # scaffolds apps/<name>
docs/ # this documentation, plans, specs
Apps declare "@bjs/engine": "*"; npm install at the root satisfies it with a symlink to packages/engine (no publishing, no copying). The engine ships TS source (exports → src/index.ts); Vite compiles it across the link and hot-reloads engine edits into running apps. Engine and Blender add-on versions move in lockstep.
End-to-end data flow
Blender scene (objects + components + GUIDs)
│ Export Level / Live Link [03-BLENDER-ADDON.html]
▼
level.glb + level.scene.json (+ audio/ files, env textures)
│ Vite dev server serves apps/<app>/public/levels/
▼
LevelLoader.Load(manifestUrl) [04-LOAD-PIPELINE.html]
│ InputManager.LoadAsset → appendSceneAsync (right-handed) → GUID index
│ → per-entity pass → second pass (refs, camera targets) → FinalizeLevel
▼
Level (entities, update loop) ── behaviors run OnStart/OnUpdate
[05-SCRIPTING.html]
Per-frame order (app runRenderLoop → scene.render → RunFrame → Havok step): 02 — Runtime Basics.