Trion Engine is a TypeScript ECS-based browser engine built on Three.js. It provides a small runtime for composing entities, components and systems in the browser, with a clear separation between gameplay data and the Three.js rendering boundary.
Trion is under active development. The current runtime provides an ECS core, a Three.js-backed rendering boundary, input handling, prefabs, scene queries, JSON-compatible scene serialization, GLTF/GLB loading with animation support, audio playback and a CPU particle system, alongside a browser-based editor for composing scenes. It is not a complete game engine: there is no networking, no WebGPU backend and no file-backed project format yet.
Current runtime: Web
Editor: Browser-based editor (nested hierarchy with rename/reparenting, folders, multi-selection, visibility/lock, Transform gizmos/inspection, undo/redo, Play Mode, WASD camera, asset browser, prefab workflow, scene save/load, audio and particle preview, preferences, console)
Rendering backend: Three.js / WebGL
- Engine lifecycle driven by a single
requestAnimationFrameloop - SceneManager ownership of the active Scene reference
- Scene, Entity and Component ECS runtime
- Transform, Hierarchy, Camera, MeshRenderer, Script, Animation, Audio and Particle components, plus directional/point/spot lights and RigidBody/BoxCollider/SphereCollider physics components
- Perspective and orthographic camera synchronization
- Three.js-backed mesh rendering and explicit resource ownership
- Keyboard, mouse, scroll and single-frame input states
- Immutable prefabs with component overrides
- Linear scene queries by ID, name, tag, component or predicate
- JSON-compatible scene serialization with stable entity IDs
- Asynchronous GLTF/GLB loading into
AssetManagerwith geometry, material, animation clip and animation-root registration - Texture loading and texture-backed standard material creation by asset ID
- Animation support via
AnimationComponent,AnimationSystemandAnimationMixer - Audio playback via
AudioComponent,AudioSystemandAssetManager.loadAudio(), with 2D/3D spatial playback and editor preview - CPU particle effects via
ParticleComponentandParticleSystem(emission rate, bursts, lifetime, gravity, color/size over life, local/world space), with editor preview transport - Directional, point and spot lights via
LightSystemwith editor viewport helpers - Backend-agnostic physics architecture with an initial Rapier implementation
- DOM-backed UI subsystem with
UIComponent,UITextComponent,UIButtonComponentandUISystem - Browser editor with nested hierarchy, folders, multi-selection, visibility/lock, entity selection and picking, inline rename and drag-and-drop reparenting, a Modify Selected menu (rename/duplicate/group/delete), Transform gizmos (J/K/L) and inspection, undo/redo history, play mode with snapshot restore, a WASD editor camera over the existing renderer viewport, an asset browser (models, audio, materials, prefabs, scenes), a prefab create/instantiate/edit workflow, scene save/save-as/open/new with dirty tracking, audio and particle preview, editor preferences and a console panel
Game code
|
v
Engine -> SceneManager -> Scene -> Entity / Component data
|
v
Systems (e.g. PhysicsSystem, ScriptSystem)
|
v
Renderer / AssetManager / Three.js / PhysicsBackend
SceneManager holds the active Scene reference. Scene owns entities. Systems read ECS data and synchronize Three.js objects behind the graphics boundary. AssetManager owns registered geometries, materials, textures, animation clips and animation roots; MeshRendererComponent and AnimationComponent refer to them by string ID.
Engine runs the frame loop. It calls onPreUpdate, updates the active Scene through SceneManager, then calls onPostUpdate. Input and systems are wired by the application in src/main.ts:
requestAnimationFrame
-> Engine.tick(deltaTime)
-> onPreUpdate(deltaTime) // Input.beginFrame()
-> SceneManager.getActiveScene().update(deltaTime)
-> onPostUpdate(deltaTime) // systems + render + Input.endFrame()
The demo post-update callback runs physics and script/UI updates only in Play Mode, while AnimationSystem, ParticleSystem, MeshRendererSystem, LightSystem and AudioSystem (runtime sources in Play Mode, editor previews in Edit Mode), editor updates, camera synchronization and Renderer.render() run in both modes. The editor camera renders the viewport in edit mode; the runtime camera takes over in Play Mode. See Editor for the editor-side behavior.
import {
Engine,
createCamera,
createMeshRenderer,
createTransform,
} from './engine/index.ts'
const engine = new Engine()
// engine.scene is the active Scene held by engine.sceneManager
const camera = engine.scene.createEntity({ name: 'Main Camera' })
camera.addComponent(createTransform({ x: 0, y: 1.2, z: 4 }))
camera.addComponent(createCamera())
const cube = engine.scene.createEntity({ name: 'Cube', tag: 'Renderable' })
cube.addComponent(createTransform())
cube.addComponent(createMeshRenderer({
geometryId: 'cube',
materialId: 'normal',
}))Geometry and materials must be registered with AssetManager before a MeshRendererComponent can render them.
Prefabs are immutable component templates, not entities. Instantiation creates an independent entity and fresh component instances.
import { createPrefab, createTransform, createMeshRenderer } from './engine/index.ts'
const cubePrefab = createPrefab([
createTransform(),
createMeshRenderer({ geometryId: 'cube', materialId: 'normal' }),
])
const cube = engine.scene.instantiate(cubePrefab, {
transform: { position: { x: 2, y: 0, z: 0 } },
})SceneManager is a runtime owner for the currently active Scene. It stores, retrieves and replaces that reference. It does not own entities or replace Scene.
const engine = new Engine()
const next = new Scene()
engine.sceneManager.setActiveScene(next)
engine.sceneManager.getActiveScene() // === next
engine.scene // same Scene; convenience getter
engine.sceneManager.dispose() // drops the reference; does not clear the SceneSystems constructed with a Scene keep that instance. Replacing the active Scene does not retarget them.
Scene queries perform simple O(n) scans over the Scene's entities.
const player = engine.scene.findByName('Player')
const enemies = engine.scene.findByTag('Enemy')
const camera = engine.scene.findFirstByComponent('camera')
const renderables = engine.scene.findByComponent('meshRenderer')const save = engine.scene.serialize()
const json = JSON.stringify(save, null, 2)
engine.scene.deserialize(JSON.parse(json))Serialization stores entity IDs, optional names/tags and JSON-compatible component data. JavaScript functions are excluded, so Script callbacks are not persisted.
AssetManager.loadGLTF(id, url) loads meshes, animation clips and an animation root from a GLTF or GLB asset. The returned IDs can be passed directly to createMeshRenderer and createAnimation.
const imported = await assets.loadGLTF('rubiks-cube', '/assets/rubiks-cube.glb')
const selectedClip = imported.animations[0]
const animatedEntity = engine.scene.createEntity({ name: 'Animated Mesh' })
animatedEntity.addComponent(createTransform({ x: 0, y: 0, z: -3 }))
animatedEntity.addComponent(createMeshRenderer({
geometryId: imported.meshes[0].geometryId,
materialId: imported.meshes[0].materialId,
}))
animatedEntity.addComponent(createAnimation({
assetId: imported.id,
clips: imported.animations,
activeClip: selectedClip,
playing: Boolean(selectedClip),
loop: true,
}))For mesh index 0, geometry and material IDs are rubiks-cube/mesh/0 and rubiks-cube/material/0. Animation clip IDs are emitted as rubiks-cube/animation/<index>. Multi-material meshes currently use their first material because MeshRendererComponent supports one material ID. The bundled demo loads /assets/rubiks-cube.glb this way for its Rubik's cube entity.
Animated entities are created with a Transform component plus a MeshRenderer and an Animation component. AnimationSystem clones the GLTF scene root into a runtime target object, creates an AnimationMixer, and drives the selected clip each frame. The system preserves the GLTF hierarchy and can attach a SkinnedMesh when the imported geometry contains skinning data.
Particle effects are created with a Transform component plus a Particle component. ParticleSystem runs a CPU simulation (emission rate, scheduled bursts, lifetime, gravity, color/size over life) and renders each emitter as one preallocated THREE.Points object with camera-facing, transparent billboards.
const sparks = engine.scene.createEntity({ name: 'Sparks' })
sparks.addComponent(createTransform({ x: 0, y: 1, z: 0 }))
sparks.addComponent(createParticle({
emissionRate: 40,
lifetime: 1.2,
startSpeed: 3,
startColor: '#ffd27a',
}))In the editor, particle effects preview directly in Edit Mode (Play/Pause/Stop/Restart/Burst transport) and run from playOnStart emitters in Play Mode. Simulation state lives in the system, so previewing never mutates saved scene data.
await assets.loadTexture('player/albedo', '/assets/player-albedo.png')
assets.createStandardMaterial('player/material', { map: 'player/albedo' })Textures are owned by AssetManager and are disposed through removeTexture() or dispose(). Standard materials are created from registered textures and then registered as material IDs for MeshRendererComponent.
Trion Engine includes a backend-agnostic physics architecture.
The engine provides three pure ECS physics components:
RigidBodyComponent(createRigidBody): Marks an entity as a physical body (dynamicorfixed).BoxColliderComponent(createBoxCollider): Attaches a box collision shape.SphereColliderComponent(createSphereCollider): Attaches a spherical collision shape.
The PhysicsSystem bridges these pure data components to an underlying PhysicsBackend implementation.
import { PhysicsSystem, RapierPhysicsBackend } from './engine/index.ts'
const physicsSystem = new PhysicsSystem(engine.scene)
const backend = new RapierPhysicsBackend()
await backend.initialize({ x: 0, y: -9.81, z: 0 })
physicsSystem.setBackend(backend)The engine-facing API contains absolutely zero backend-specific types (e.g., no Rapier objects). All Rapier interactions are isolated entirely within RapierPhysicsBackend.ts. Adding another backend (like Ammo.js or Jolt) would simply involve creating a new class that implements the PhysicsBackend interface, with no changes needed in PhysicsSystem or the ECS components.
Trion includes a minimal DOM-backed UI subsystem implemented as an ECS system.
The engine provides three pure ECS UI components:
UIComponent(createUI): Position, size, visibility and optional background color.UITextComponent(createUIText): Text content, color and font size.UIButtonComponent(createUIButton): Interaction state (interactable,isHovered,isPressed).
The UISystem bridges these pure data components to the browser DOM.
import { UISystem } from './engine/index.ts'
const uiSystem = new UISystem(engine.scene)
// Call uiSystem.update(deltaTime) in your engine.onPostUpdate- UI components contain only engine-facing data — no callbacks, no DOM references.
UISystemowns the DOM lifecycle: it creates a root container, manages child elements per entity, and cleans up on entity/component removal.UIButtonComponentinteraction state (isHovered,isPressed) is written byUISystemfrom DOM pointer events and read by game scripts.- UI does not depend on Three.js rendering; it overlays the canvas via a full-viewport DOM container.
The browser editor (src/editor/, wired in src/main.ts) edits the live Scene through the public ECS API:
- Hierarchy panel (nested tree, viewport click-to-select picking, selection highlight box), inline entity rename (double-click or
F2,Enterto confirm,Escapeto cancel), drag-and-drop reparenting with world-transform preservation, a Modify Selected menu for rename/duplicate/delete, and a Transform inspector. - Move/Rotate/Scale gizmos (
J/K/L) driven by Three.jsTransformControls; theTransformComponentstays authoritative and gizmo drags are undoable. - Animated entities are gizmoed, picked and highlighted via their
AnimationSystemtarget (AnimationSystem.getTarget()), which carries the world transform; the renderer mesh underneath it is never driven directly. - Light entities are gizmoed via their runtime light object (directional starts in rotate, point/spot in translate) and picked by clicking their viewport helper; drags edit the
TransformComponentand are undoable like any Transform edit. - Audio Sources preview clips in Edit Mode without touching Play Mode state; Particle Systems preview in Edit Mode with Play/Pause/Stop/Restart/Burst transport plus emitter shape/direction helpers. Both run from their components in Play Mode and reset fully on stop.
- Undo/redo (
Ctrl+Z/Ctrl+Shift+Z/Ctrl+Y, 50 entries) covers Transform edits, renames, reparents, duplication and entity create/delete. History is editor-only and disabled in Play Mode; undo/redo push state to the viewport in the same tick. - Play Mode (
F5/▶ Play,F8/⏹ Stop) snapshots the scene, runs physics/scripts/UI against the runtime camera, then restores the exact pre-play state on stop and discards runtime changes. - Editor camera: right-drag orbits, middle-drag pans, wheel zooms,
WASDmoves while hovering the viewport,Alt+left-drag orbits. Camera input suspends during gizmo drags and Play Mode. - Asset Browser (toggleable
Assetspanel) discoverspublic/assetsplus stored prefabs and scenes: models instantiate into the scene via double-click or drag into the viewport; prefab and scene entries work the same way through their own flows. - Prefab workflow: save a selected entity as a prefab from the Inspector, instantiate prefabs from the Asset Browser, and edit prefabs in an isolated session (same viewport/hierarchy/inspector/gizmo tooling) with Save/Cancel returning to the untouched scene.
- Scene File workflow (
Save Scenemenu,Ctrl+S/Ctrl+Shift+S): save, save-as, open and new scene through the existing serializer, with dirty tracking, a Save/Don't Save/Cancel prompt on unsaved switches, and history that resets per scene. Saving is disabled in Play Mode so runtime state never leaks into scene assets. - Entity hierarchy uses the optional
hierarchycomponent parent link; renderers compose world transforms from ECS locals while physics bodies and the runtime camera use local transforms.
npm install
npm run devCreate a production build with:
npm run buildPreview the production build with npm run preview.
src/
engine/
components/ ECS component data and factories (including ui/)
core/ Engine, SceneManager, Scene, Entity, Prefab, serialization
graphics/ Renderer, AssetManager, camera/mesh systems, GLTF loading
input/ DOM input service
physics/ Physics backend interfaces, Rapier implementation, PhysicsSystem
systems/ Runtime systems (Script, Animation, Audio, Particle, UI)
editor/ Browser editor UI kept separate from the runtime engine
(panels, asset browser, prefab/scene stores, dialogs, history)
main.ts Browser demo and engine wiring
- Keep ECS data separate from rendering implementation details.
- Make ownership explicit: SceneManager owns the active Scene reference; Scene owns entities; AssetManager owns registered GPU resources; Renderer owns the scene graph and WebGL context.
- Prefer small APIs and direct iteration over premature abstractions.
- Keep Three.js-specific code inside the graphics boundary.
- No networking or WebGPU backend.
- Prefabs are single-entity snapshots with no override/reconciliation system yet.
- Prefab and scene assets persist in the browser's local storage rather than project files.
- Scene instances referencing unloaded GLTF asset IDs render only once the source model is loaded.
- Animation support is currently focused on GLTF animation clips and hierarchy-preserving runtime targets; it is not a full animation editor.
- Particle simulation is CPU-based with a per-emitter particle cap; there is no GPU particle pipeline yet.
- Multi-material GLTF meshes use their first material.
- Scene serialization excludes functions and does not restore Script callbacks.
- Physics bodies and the runtime camera use local transforms and ignore hierarchy parenting.
- Querying currently uses linear scans rather than indexes.
See ROADMAP.md for implemented work and planned future directions.
See CONTRIBUTING.md. Contributors should understand the existing ownership and graphics-boundary rules before adding systems or asset features.
Trion Engine is licensed under the MIT License. See LICENSE for the full license text.