Integrating an ECS with Three.js¶
Three.js is a rendering engine (scene graph + GPU submission). This ECS is a simulation architecture (data in components, behavior in systems, ordered by a schedule, with safe structural changes via deferred commands + flush points). Integrating them well means letting each do what it’s good at, and defining clean “hand-off” boundaries.
The mental model: ECS drives state, Three.js draws it¶
A practical split that scales:
- ECS World = authoritative game/sim state (position, velocity, health, selection, etc.)
- Three.js Scene = visual representation (Object3D transforms, meshes, materials, lights)
So the goal is not “put Three.js inside ECS”, but:
Systems write simulation state → a render-sync step pushes that state into Three.js objects.
Where ECS fits in the Three.js render loop¶
Three.js typically runs:
- update (your code)
renderer.render(scene, camera)
With ECS, your “update” becomes scheduled phases, e.g.:
input(read DOM/input, write components/resources)sim(gameplay, movement, AI)render(sync ECS → Three.js, then render)
The Schedule already supports this exact idea and flushes commands between phases to make entity/component creation/removal deterministic.
Why flush points matter for Three.js integration¶
Spawning/despawning and add/remove are structural changes in this ECS and are expected to be deferred while iterating queries/systems.
That maps perfectly to Three.js object lifecycle:
- During sim: decide “this entity should appear/disappear” → enqueue ECS commands
- At flush boundary: ECS structure becomes stable
- Render-sync phase: create/remove corresponding
Object3Dsafely, because you’re no longer mid-iteration on archetype tables
This is the same reason this ECS has cmd() / flush() and why Schedule flushes between phases.
A clean integration pattern: “Renderable bridge” components¶
Common approach:
- A
Transformcomponent (position/rotation/scale) is owned by ECS. - A
Renderablecomponent carries a reference/handle to what Three.js should draw (mesh id, model key, material key…). - A render-sync system queries
(Transform, Renderable)and applies changes to the correspondingObject3D.
Key idea: ECS components store “what it is” and “where it is”, while the actual Mesh/Object3D lives in Three.js.
This keeps:
- ECS portable (not tied to Three.js types everywhere)
- Three.js free to manage GPU resources
One-way vs two-way sync (pick a source of truth)¶
Integration gets messy when both ECS and Three.js “own” transforms.
A scalable default:
- ECS is the source of truth for gameplay transforms.
- Three.js
Object3Dis just the projection of that state.
Only do two-way sync when you truly need it (editor gizmos, drag interactions). Even then, treat it as a controlled input step:
- read Object3D change in
inputortoolsphase - write back to ECS components
- let sim proceed from ECS again
Why ECS does not replace Three.js (and shouldn’t try)¶
Even with a “full ECS” architecture, Three.js still owns:
- scene graph concerns (parenting, cameras, lights)
- GPU resource lifetimes (buffers, textures, materials)
- draw submission, sorting, batching, culling strategies
ECS complements that by making simulation state and logic scalable: archetype tables + queries + systems + scheduling.
Scaling tips (when entity counts grow)¶
When you have many similar visuals:
- prefer InstancedMesh in Three.js
- let ECS systems produce instance transforms (dense arrays) from queries
- upload those transforms once per frame
This aligns with why archetype ECS exists: tight iteration over dense component columns.