ECS Three.js Interop
This is the most important concept to understand in IWSDK. Many developers get confused about when to use ECS vs Three.js APIs. This guide clarifies the relationship and shows you exactly how to work with both.
The core problem: Two different APIs
The confusion:
// Which way should I move an object?
// Three.js way:
entity.object3D.position.x += 1;
// ECS way:
const pos = entity.getVectorView(Transform, 'position');
pos[0] += 1;
The answer: Both work, but ECS Transform data is the source of truth. IWSDK automatically syncs to Three.js visuals.
Mental model: Data-driven visuals
Think of it this way:
ECS Components (Data) ←sync→ Three.js Objects (Visuals)
───────────────────── ──────────────────────────
Transform { pos: [2,1,0] } ────────→ object3D.position: Vector3(2,1,0)
Transform { rot: [0,0,0,1] } ────────→ object3D.quaternion: Quaternion(0,0,0,1)
Transform { scale: [1,1,1] } ────────→ object3D.scale: Vector3(1,1,1)
// ECS components store the authoritative state
// Three.js objects provide the visual representation
Key Insight: ECS components hold the authoritative data. Three.js objects are synchronized views of that data.
How IWSDK bridges the two worlds
1. Entity creation links ECS + Three.js
import { World } from '@iwsdk/core';
const world = await World.create(container);
// This creates BOTH an ECS entity AND a Three.js Object3D
const entity = world.createTransformEntity();
console.log(entity.index); // ECS entity ID: 42
console.log(entity.object3D); // Three.js Object3D instance
console.log(entity.hasComponent(Transform)); // true - ECS Transform component
What happened:
createTransformEntity() creates an ECS entity- Creates a Three.js
Object3D and attaches it as entity.object3D - Adds a
Transform component with position/rotation/scale data - Registers the entity for automatic sync via TransformSystem
IWSDK runs a built-in TransformSystem that automatically synchronizes:
// Every frame, TransformSystem does this internally:
for (const entity of this.queries.transforms.entities) {
const transform = entity.getComponent(Transform);
const object3D = entity.object3D;
// Sync ECS data → Three.js visuals
object3D.position.fromArray(transform.position);
object3D.quaternion.fromArray(transform.orientation);
object3D.scale.fromArray(transform.scale);
}
You never write this code. IWSDK handles it automatically.
When to use ECS vs Three.js APIs
Transform updates:
// Update position through ECS - gets synced to Three.js automatically
const pos = entity.getVectorView(Transform, 'position');
pos[0] += deltaX;
pos[1] += deltaY;
pos[2] += deltaZ;
Data-driven logic:
// ECS Transform is the single source of truth
entity.setValue(Transform, 'position', [2, 1, -5]);
// Three.js object3D.position gets updated automatically
Creating meshes and materials:
import { BoxGeometry, MeshStandardMaterial, Mesh } from '@iwsdk/core';
const geometry = new BoxGeometry(1, 1, 1);
const material = new MeshStandardMaterial({ color: 0xff0000 });
const mesh = new Mesh(geometry, material);
const entity = world.createTransformEntity(mesh);
Complex 3D hierarchies:
// Build a complex object with multiple parts
const parent = world.createTransformEntity();
const body = new Mesh(bodyGeometry, bodyMaterial);
const wheel1 = new Mesh(wheelGeometry, wheelMaterial);
const wheel2 = new Mesh(wheelGeometry, wheelMaterial);
parent.object3D.add(body);
parent.object3D.add(wheel1);
parent.object3D.add(wheel2);
// Position wheels relative to parent
wheel1.position.set(-1, -0.5, 1.2);
wheel2.position.set(1, -0.5, 1.2);
Visual properties:
// Set Three.js-specific properties
entity.object3D.name = 'MyObject';
entity.object3D.layers.set(1); // Render layer
entity.object3D.castShadow = true;
entity.object3D.receiveShadow = true;
Pattern 1: Direct Three.js updates (Recommended)
// Update Three.js object directly - immediate and familiar
entity.object3D.position.x += deltaX; // Move right
entity.object3D.position.y += deltaY; // Move up
entity.object3D.position.z += deltaZ; // Move forward
// Animate rotation over time
const time = performance.now() * 0.001;
entity.object3D.rotation.y = time; // Rotate around Y-axis
// Update position through ECS - syncs to Three.js automatically
const pos = entity.getVectorView(Transform, 'position');
pos[0] += deltaX; // Move right
pos[1] += deltaY; // Move up
pos[2] += deltaZ; // Move forward
// IWSDK's TransformSystem automatically updates:
// entity.object3D.position.set(pos[0], pos[1], pos[2])
Parenting and hierarchies
IWSDK provides two root contexts:
// Persistent objects (survive level changes)
const ui = world.createTransformEntity(undefined, { persistent: true });
// Attached to: world.getPersistentRoot() (the Scene)
// Level objects (cleaned up on level change)
const prop = world.createTransformEntity(); // default
// Attached to: world.getActiveRoot() (current level)
const parent = world.createTransformEntity();
const child = world.createTransformEntity(undefined, { parent });
// Both ECS Transform and Three.js hierarchy are set up:
console.log(child.getValue(Transform, 'parent') === parent.index); // true
console.log(child.object3D.parent === parent.object3D); // true
Understanding when things happen each frame:
1. Input Systems (-4 priority) ─── Update controller/hand data
2. Game Logic Systems (0 priority) ─ Your gameplay code here
3. TransformSystem (1 priority) ── Sync ECS → Three.js
4. renderer.render(scene, camera) ─ Three.js draws the frame
Key rules:
- Write game logic in your systems (priority 0 or negative)
- Never write code that runs after
TransformSystem - Three.js objects are automatically synced before rendering
Common pitfalls and solutions
Pitfall: fighting the sync system
// DON'T: Manually update Three.js and expect ECS to follow
entity.object3D!.position.x = 5; // This gets overwritten!
// DO: Update ECS and let TransformSystem sync
const pos = entity.getVectorView(Transform, 'position');
pos[0] = 5; // Three.js automatically updates
Pitfall: component vs Object3D confusion
// DON'T: Store Three.js objects directly in components
// Keep ECS data separate from Three.js objects
// DO: Use Transform component for position/rotation/scale
// IWSDK handles the Object3D sync automatically
The golden rules:
- ECS owns the data - Transform components are the source of truth
- Three.js shows the data - Object3D properties are synchronized views
- IWSDK handles sync - TransformSystem runs automatically every frame
- Use ECS for logic - Game systems should manipulate components
- Use Three.js for setup - Materials, geometries, and visual properties
This interop system lets you leverage the best of both worlds: ECS’s data-driven architecture for game logic, and Three.js’s powerful rendering capabilities for visuals.