| name | iwsdk-planner |
| description | IWSDK project planning and best practices guide. Use when planning new IWSDK features, designing systems/components, reviewing IWSDK code architecture, or when the user asks about IWSDK patterns, ECS design, signals, or reactive programming in this codebase. |
IWSDK Project Planner
You are an expert IWSDK (Immersive Web SDK) architect. Apply these patterns and best practices when planning, implementing, or reviewing IWSDK code.
Core Architecture
IWSDK is built on three pillars:
- ECS (Entity Component System) via
elics library
- Reactive Signals via
@preact/signals-core
- Three.js Integration with zero-copy transform binding (super-three v0.181.0)
IWSDK is a 3D web framework with first-class XR support. World.create() always creates a persistent world.player origin and keeps world.camera under it, even when xr: false is used for browser-only apps. For first-person browser movement, move world.player; for orbit, editor, product, cinematic, or third-person cameras, it is fine to keep world.player at the origin and drive world.camera. Remember that world.camera.position is local to world.player, so use camera.getWorldPosition(...) when world-space viewer position matters.
Use input.canvasPointerEvents for browser mouse/touch canvas events. Browser canvas pointers and XR rays both feed Three/Object3D pointer events and ECS Hovered/Pressed tags on Interactable/RayInteractable entities. XR-specific input is available at world.input.xr; keyboard and standard browser gamepads live at world.input.keyboard and world.input.browserGamepads. Reusable systems should prefer world.input.actions for intent such as locomotion.move or locomotion.jump; opt into browser locomotion bindings with features.locomotion.browserControls.
Critical Best Practices
1. Keep Systems Stateless
Systems should NOT store arrays of entities or maintain entity references. Use queries instead.
export class BadSystem extends createSystem({
items: { required: [MyComponent] },
}) {
private myEntities: Entity[] = [];
init() {
this.queries.items.subscribe('qualify', (entity) => {
this.myEntities.push(entity);
});
}
}
export class GoodSystem extends createSystem({
items: { required: [MyComponent] },
}) {
update() {
for (const entity of this.queries.items.entities) {
}
}
}
Exception: Scratch variables for temporary per-frame calculations are OK.
2. Use Query Subscribe/Unsubscribe for Reactive Programming
Instead of polling or storing state, react to entity lifecycle events:
export class ReactiveSystem extends createSystem({
interactables: { required: [Interactable, Transform] },
}) {
init() {
this.queries.interactables.subscribe('qualify', (entity) => {
this.setupEventListeners(entity);
});
this.queries.interactables.subscribe('disqualify', (entity) => {
this.cleanupEventListeners(entity);
});
}
}
3. Use Signals for Reactive State
IWSDK uses @preact/signals-core. Prefer signals over manual state tracking:
export class MySystem extends createSystem(
{},
{
speed: { type: Types.Float32, default: 5.0 },
jumpHeight: { type: Types.Float32, default: 2.0 },
},
) {
init() {
this.cleanupFuncs.push(
this.config.speed.subscribe((newSpeed) => {
console.log('Speed changed:', newSpeed);
}),
);
}
update(delta) {
const currentSpeed = this.config.speed.peek();
}
}
World Globals Signals for Cross-System Communication
Store signals in world.globals for state that multiple systems need to read/write:
import { signal } from '@preact/signals-core';
(world.globals as Record<string, unknown>).gamePaused = signal(true);
(world.globals as Record<string, unknown>).audioMasterVolume = signal(1.0);
const gamePaused = (this.globals.gamePaused as Signal<boolean>).peek();
const gamePausedSignal = this.globals.gamePaused as Signal<boolean>;
gamePausedSignal.value = !gamePausedSignal.value;
4. Component Types (Complete List)
import { Types } from '@iwsdk/core';
Types.Float32;
Types.Float64;
Types.Int8;
Types.Int16;
Types.Int32;
Types.Uint32;
Types.Boolean;
Types.String;
Types.Vec3;
Types.Vec4;
Types.Color;
Types.Entity;
Types.Enum;
Types.Object;
5. Component Design Patterns
import { createComponent, Types } from '@iwsdk/core';
export const Interactable = createComponent('Interactable', {}, '');
export const Health = createComponent('Health', {
current: { type: Types.Float32, default: 100 },
max: { type: Types.Float32, default: 100 },
});
export const State = createComponent('State', {
mode: {
type: Types.Enum,
enum: { Idle: 'idle', Moving: 'moving', Attacking: 'attacking' },
default: 'idle',
},
});
export const Velocity = createComponent('Velocity', {
linear: { type: Types.Vec3, default: [0, 0, 0] },
angular: { type: Types.Vec3, default: [0, 0, 0] },
});
export const Tint = createComponent('Tint', {
color: { type: Types.Color, default: [1, 1, 1, 1] },
});
6. Query Patterns with Filters
export class DamageSystem extends createSystem({
enemies: { required: [Enemy] },
vulnerableEnemies: {
required: [Enemy, Health],
excluded: [Invulnerable, Shield],
},
lowHealth: {
required: [Health],
where: [lt(Health, 'current', 20)],
},
activeBosses: {
required: [Boss, Health],
where: [gt(Health, 'current', 0), eq(State, 'mode', 'attacking')],
},
}) {}
7. System Interface (Full)
interface System {
world: World;
queries: Record<string, Query>;
config: { [key]: Signal };
cleanupFuncs: Array<() => void>;
player: XROrigin;
input: XRInputManager;
scene: Scene;
camera: PerspectiveCamera;
renderer: WebGLRenderer;
visibilityState: Signal<VisibilityState>;
xrManager: WebXRManager;
xrFrame: XRFrame;
globals: Record<string, any>;
createEntity(): Entity;
createTransformEntity(object?: Object3D, parent?: Entity): Entity;
init(): void;
update(delta: number, time: number): void;
play(): void;
stop(): void;
}
8. XR Input System (Critical)
update() {
const leftGamepad = this.input.xr.gamepads.left;
const rightGamepad = this.input.xr.gamepads.right;
leftGamepad?.getButtonPressed(InputComponent.Trigger);
leftGamepad?.getButtonDown(InputComponent.Trigger);
leftGamepad?.getButtonUp(InputComponent.Trigger);
leftGamepad?.getButtonValue(InputComponent.Trigger);
leftGamepad?.getButtonTouched(InputComponent.Trigger);
leftGamepad?.getSelectStart();
leftGamepad?.getSelectEnd();
leftGamepad?.getSelecting();
const axes = leftGamepad?.getAxesValues(InputComponent.Thumbstick);
console.log(axes?.x, axes?.y);
leftGamepad?.getAxesEnteringUp(InputComponent.Thumbstick);
leftGamepad?.getAxesEnteringDown(InputComponent.Thumbstick);
leftGamepad?.getAxesEnteringLeft(InputComponent.Thumbstick);
leftGamepad?.getAxesEnteringRight(InputComponent.Thumbstick);
}
InputComponent Enum:
import { InputComponent } from '@iwsdk/core';
InputComponent.Trigger;
InputComponent.Squeeze;
InputComponent.Touchpad;
InputComponent.Thumbstick;
InputComponent.A_Button;
InputComponent.B_Button;
InputComponent.X_Button;
InputComponent.Y_Button;
InputComponent.Thumbrest;
InputComponent.Menu;
9. Player/XROrigin Access (Critical)
this.player;
this.player.head;
this.player.raySpaces.left;
this.player.raySpaces.right;
this.player.gripSpaces.left;
this.player.gripSpaces.right;
this.player.secondaryRaySpaces.left;
this.player.secondaryRaySpaces.right;
this.player.secondaryGripSpaces.left;
this.player.secondaryGripSpaces.right;
const headPos = new Vector3();
this.player.head.getWorldPosition(headPos);
const rightHandPos = new Vector3();
this.player.gripSpaces.right.getWorldPosition(rightHandPos);
10. Audio System
import {
AudioSource,
PlaybackMode,
AudioUtils,
InstanceStealPolicy,
DistanceModel,
} from '@iwsdk/core';
entity.addComponent(AudioSource, {
src: 'audio/click.mp3',
positional: true,
loop: false,
autoplay: false,
volume: 0.5,
refDistance: 1,
rolloffFactor: 1,
maxDistance: 10000,
maxInstances: 1,
playbackMode: PlaybackMode.Restart,
});
AudioUtils.play(entity);
PlaybackMode:
PlaybackMode.Restart;
PlaybackMode.Overlap;
PlaybackMode.Ignore;
PlaybackMode.FadeRestart;
InstanceStealPolicy:
InstanceStealPolicy.Oldest;
InstanceStealPolicy.Quietest;
InstanceStealPolicy.Furthest;
DistanceModel:
DistanceModel.Linear;
DistanceModel.Inverse;
DistanceModel.Exponential;
11. Physics System
PhysicsBody - motion properties:
import { PhysicsBody, PhysicsState } from '@iwsdk/core';
entity.addComponent(PhysicsBody, {
state: PhysicsState.Dynamic,
linearDamping: 0.0,
angularDamping: 0.0,
gravityFactor: 1.0,
centerOfMass: [Infinity, Infinity, Infinity],
});
PhysicsState:
PhysicsState.Static;
PhysicsState.Dynamic;
PhysicsState.Kinematic;
PhysicsShape - collision shape AND material properties:
import { PhysicsShape, PhysicsShapeType } from '@iwsdk/core';
entity.addComponent(PhysicsShape, {
shape: PhysicsShapeType.Auto,
dimensions: [0, 0, 0],
density: 1.0,
restitution: 0.0,
friction: 0.5,
});
PhysicsShapeType:
PhysicsShapeType.Sphere;
PhysicsShapeType.Box;
PhysicsShapeType.Cylinder;
PhysicsShapeType.Capsules;
PhysicsShapeType.ConvexHull;
PhysicsShapeType.TriMesh;
PhysicsShapeType.Auto;
12. Grabbable Components
import {
OneHandGrabbable,
TwoHandsGrabbable,
DistanceGrabbable,
MovementMode,
} from '@iwsdk/core';
entity.addComponent(OneHandGrabbable, {});
entity.addComponent(OneHandGrabbable, {
rotate: true,
rotateMin: [0, -Math.PI, 0],
rotateMax: [0, Math.PI, 0],
translate: true,
translateMin: [-2, 0, -2],
translateMax: [2, 3, 2],
});
entity.addComponent(TwoHandsGrabbable, {
rotate: true,
translate: true,
scale: true,
scaleMin: [0.5, 0.5, 0.5],
scaleMax: [2, 2, 2],
});
entity.addComponent(DistanceGrabbable, {
rotate: true,
translate: true,
scale: true,
movementMode: MovementMode.MoveTowardsTarget,
returnToOrigin: false,
moveSpeed: 0.1,
});
import { GrabSystem } from '@iwsdk/core';
const grab = world.getSystem(GrabSystem);
grab.forceRelease(entity);
const hand = grab.getHolderHand(entity);
13. Environment/Lighting
import {
DomeGradient,
DomeTexture,
IBLGradient,
IBLTexture,
} from '@iwsdk/core';
entity.addComponent(DomeGradient, {
sky: [0.2423, 0.6172, 0.8308, 1.0],
equator: [0.6584, 0.7084, 0.7913, 1.0],
ground: [0.807, 0.7758, 0.7454, 1.0],
intensity: 1.0,
});
entity.addComponent(DomeTexture, {
url: '/textures/sky.hdr',
});
entity.addComponent(IBLGradient, {
sky: [0.6902, 0.749, 0.7843, 1.0],
equator: [0.6584, 0.7084, 0.7913, 1.0],
ground: [0.807, 0.7758, 0.7454, 1.0],
intensity: 1.0,
});
entity.addComponent(IBLTexture, {
src: 'room',
intensity: 1.0,
rotation: [0, 0, 0],
});
Critical Environment Usage Notes:
-
Environment components MUST be added to the level root entity, not arbitrary entities. The EnvironmentSystem queries require LevelRoot:
someEntity.addComponent(DomeGradient, { ... });
const root = world.activeLevel.value;
root.addComponent(DomeGradient, { sky: [0.24, 0.62, 0.83, 1.0], ... });
-
After changing environment properties, MUST set _needsUpdate: true — changes are silently ignored without it:
root.setValue(DomeGradient, 'sky', [0.1, 0.2, 0.8, 1.0]);
root.setValue(DomeGradient, '_needsUpdate', true);
-
Background vs IBL are separate: DomeTexture/DomeGradient controls the visible sky. IBLTexture/IBLGradient controls scene lighting (reflections, ambient). You can mix them:
root.addComponent(DomeTexture, { src: '/envs/sky.hdr', intensity: 0.9 });
root.addComponent(IBLTexture, { src: 'room', intensity: 1.2 });
-
In AR sessions, backgrounds (dome) are automatically hidden but IBL remains active for realistic lighting on virtual objects.
14. Asset Loading
import { AssetManager, AssetType } from '@iwsdk/core';
const world = await World.create(container, {
assets: {
myModel: {
url: '/models/scene.glb',
type: AssetType.GLTF,
priority: 'critical',
},
mySound: {
url: '/audio/click.mp3',
type: AssetType.Audio,
priority: 'background',
},
myTexture: { url: '/textures/wood.jpg', type: AssetType.Texture },
myHDR: { url: '/textures/env.hdr', type: AssetType.HDRTexture },
},
});
const model = AssetManager.getGLTF('myModel');
const texture = AssetManager.getTexture('myTexture');
const audio = AssetManager.getAudio('mySound');
AssetType Enum:
AssetType.GLTF;
AssetType.Audio;
AssetType.Texture;
AssetType.HDRTexture;
15. VisibilityState
import { VisibilityState } from '@iwsdk/core';
VisibilityState.NonImmersive;
VisibilityState.Hidden;
VisibilityState.Visible;
VisibilityState.VisibleBlurred;
this.world.visibilityState.subscribe((state) => {
switch (state) {
case VisibilityState.NonImmersive:
break;
case VisibilityState.Visible:
break;
case VisibilityState.VisibleBlurred:
break;
}
});
XR Session Optimization
Configure frame rate and foveation when the XR session becomes visible:
this.world.visibilityState.subscribe((state) => {
if (state === VisibilityState.Visible) {
this.world.session?.updateTargetFrameRate(72);
this.world.renderer.xr.setFoveation(1);
}
});
16. Locomotion Configuration
import { LocomotionSystem, TurningMethod } from '@iwsdk/core';
const locomotion = world.getSystem(LocomotionSystem);
locomotion.config.slidingSpeed.value = 3.0;
locomotion.config.turningMethod.value = TurningMethod.SmoothTurn;
locomotion.config.turningAngle.value = 45;
locomotion.config.turningSpeed.value = 180;
locomotion.config.comfortAssist.value = 0.5;
locomotion.config.rayGravity.value = -0.4;
locomotion.config.jumpHeight.value = 1.5;
locomotion.config.jumpCooldown.value = 0.1;
locomotion.config.maxDropDistance.value = 5.0;
locomotion.config.useWorker.value = true;
locomotion.config.enableJumping.value = true;
locomotion.config.initialPlayerPosition.value = [0, 0, 0];
EnvironmentType (used with LocomotionEnvironment component):
import { EnvironmentType } from '@iwsdk/core';
EnvironmentType.STATIC;
EnvironmentType.KINEMATIC;
17. Scene Understanding (AR)
import { XRPlane, XRMesh, XRAnchor } from '@iwsdk/core';
World.create(container, {
xr: {
sessionMode: SessionMode.ImmersiveAR,
features: { planeDetection: true, meshDetection: true },
},
features: {
sceneUnderstanding: true,
},
});
export class MyARSystem extends createSystem({
planes: { required: [XRPlane] },
meshes: { required: [XRMesh] },
anchors: { required: [XRAnchor] },
}) {
init() {
this.queries.planes.subscribe('qualify', (entity) => {
const plane = entity.object3D;
});
}
}
18. Feature Configuration (Critical!)
Only enable features that your experience actually uses. Features have prerequisites - enabling them without proper setup causes problems.
Feature Decision Matrix
| Feature | Enable When | Prerequisites | If Missing |
|---|
locomotion | Player needs to move (teleport/slide) | Collision geometry in scene (floor, walls) | Player falls through world |
physics | Objects need dynamic simulation | PhysicsShape + PhysicsBody components | Wasted overhead |
grabbing | Objects are grabbable | Grabbable components on entities | Wasted overhead |
sceneUnderstanding | AR with real-world surfaces | AR session mode | Feature won't work |
environmentRaycast | AR object placement | AR session + hit-test support | Feature won't work |
spatialUI | Using PanelUI components | UI config files | No UI renders |
Locomotion Requires Environment Setup
const world = await World.create(container, {
features: {
locomotion: true,
},
});
const world = await World.create(container, {
level: '/glxf/SceneWithFloor.glxf',
features: {
locomotion: true,
physics: true,
},
});
const world = await World.create(container, {
features: {
locomotion: false,
grabbing: true,
},
});
VR vs AR Feature Sets
const vrWorld = await World.create(container, {
xr: { sessionMode: SessionMode.ImmersiveVR },
features: {
locomotion: true,
grabbing: true,
physics: true,
},
});
const arWorld = await World.create(container, {
xr: {
sessionMode: SessionMode.ImmersiveAR,
features: { planeDetection: true, hitTest: true },
},
features: {
locomotion: false,
sceneUnderstanding: true,
environmentRaycast: true,
grabbing: true,
},
});
19. World Initialization Pattern
import { World, SessionMode } from '@iwsdk/core';
const world = await World.create(container, {
render: {
fov: 50,
near: 0.1,
far: 200,
defaultLighting: true,
stencil: false,
},
assets: {
myModel: { url: '/models/scene.glb', type: AssetType.GLTF },
},
level: '/glxf/MyScene.glxf',
xr: {
sessionMode: SessionMode.ImmersiveVR,
referenceSpaceType: 'local-floor',
requiredFeatures: ['hand-tracking'],
optionalFeatures: ['plane-detection'],
offer: 'once',
},
features: {
locomotion: true,
grabbing: true,
physics: true,
sceneUnderstanding: true,
environmentRaycast: true,
camera: true,
spatialUI: {
forwardHtmlEvents: true,
preferredColorScheme: 'dark',
},
},
});
world.registerSystem(MySystem);
world.launchXR();
world.exitXR();
Post-Creation Initialization Sequence
After World.create(), follow this order for proper initialization:
const simulator = await RaceSimulator.create({ useWorker: true });
world.globals.raceSimulator = simulator;
(world.globals as Record<string, unknown>).carProxies = new Map();
(world.globals as Record<string, unknown>).gamePaused = signal(true);
world
.registerComponent(VehiclePhysicsLink)
.registerComponent(VehiclePhysicsState);
await setupScene(world, simulator, { aiCount: 5 });
world
.registerSystem(PlayerInputSystem, { priority: 0 })
.registerSystem(PhysicsStepSystem, { priority: 10 });
20. Transform Entity Creation
const entity = world.createTransformEntity(mesh, {
parent: parentEntity,
persistent: false,
});
entity.object3D.position.set(0, 1, 0);
entity.setValue(Transform, 'position', [0, 1, 0]);
const posView = entity.getVectorView(Transform, 'position');
posView[0] += delta;
21. Panel UI Pattern
export class SettingsSystem extends createSystem({
settingsPanel: {
required: [PanelUI, PanelDocument],
where: [eq(PanelUI, 'config', './ui/settings.json')],
},
}) {
init() {
this.queries.settingsPanel.subscribe('qualify', (entity) => {
const doc = PanelDocument.data.document[entity.index];
const button = doc.getElementById('my-button');
button.addEventListener('click', () => {
AudioUtils.play(entity);
});
});
this.world.visibilityState.subscribe((state) => {
const is2D = state === VisibilityState.NonImmersive;
xrButton.setProperties({ display: is2D ? 'flex' : 'none' });
});
}
}
22. Environment Raycasting (AR Hit-Test)
EnvironmentRaycastTarget makes an entity automatically follow XR hit-test results (raycast against real-world surfaces). The entity is positioned at the hit point and oriented to match the surface normal.
import { EnvironmentRaycastTarget, RaycastSpace } from '@iwsdk/core';
const reticle = world.createTransformEntity(reticleMesh);
reticle.addComponent(EnvironmentRaycastTarget, {
space: RaycastSpace.Right,
maxDistance: 100,
});
const marker = world.createTransformEntity(markerMesh);
marker.addComponent(EnvironmentRaycastTarget, {
space: RaycastSpace.Screen,
});
const xrResult = entity.getValue(EnvironmentRaycastTarget, 'xrHitTestResult');
if (xrResult && gamepad?.getSelectStart()) {
spawnObject(entity.object3D.position.clone());
}
RaycastSpace:
RaycastSpace.Left;
RaycastSpace.Right;
RaycastSpace.Viewer;
RaycastSpace.Screen;
Prerequisites: features: { environmentRaycast: true } and xr.features: { hitTest: true } in AR session mode.
23. Follower Component
Makes an entity follow another Object3D (typically the player's head for head-locked UI).
import { Follower, FollowBehavior } from '@iwsdk/core';
entity.addComponent(Follower, {
target: world.player.head,
offsetPosition: [0, -0.2, -0.8],
behavior: FollowBehavior.PivotY,
maxAngle: 30,
tolerance: 0.4,
speed: 1,
});
FollowBehavior:
FollowBehavior.FaceTarget;
FollowBehavior.PivotY;
FollowBehavior.NoRotation;
Note: target must be an Object3D instance (e.g., world.player.head), not an entity.
24. CameraSource Component
Access device camera video as a texture. Useful for mixed reality effects or photo capture.
import {
CameraSource,
CameraFacing,
CameraState,
CameraUtils,
} from '@iwsdk/core';
entity.addComponent(CameraSource, {
deviceId: '',
facing: CameraFacing.Back,
width: 1920,
height: 1080,
frameRate: 30,
});
CameraFacing:
CameraFacing.Back;
CameraFacing.Front;
CameraFacing.Unknown;
CameraState:
CameraState.Inactive;
CameraState.Starting;
CameraState.Active;
CameraState.Error;
CameraUtils static class:
const devices = await CameraUtils.getDevices();
const backCam = CameraUtils.findByFacing(devices, CameraFacing.Back);
const canvas = CameraUtils.captureFrame(cameraEntity);
if (canvas) {
const texture = new CanvasTexture(canvas);
}
Requires: features: { camera: true }. Camera stream only activates during XR sessions.
25. PhysicsManipulation Component
Applies forces or velocity changes to physics bodies. One-shot component — PhysicsSystem applies the values and auto-removes it each frame.
import { PhysicsManipulation } from '@iwsdk/core';
entity.addComponent(PhysicsManipulation, {
force: [0, 10, 0],
linearVelocity: [0, 0, 0],
angularVelocity: [0, 0, 0],
});
update() {
if (!entity.hasComponent(PhysicsManipulation)) {
entity.addComponent(PhysicsManipulation, { force: [0, 5, 0] });
}
}
26. ScreenSpace Usage Notes
ScreenSpace positions a PanelUI entity relative to the screen in non-XR (browser) mode.
All position/size values are CSS strings, not numbers:
import { ScreenSpace } from '@iwsdk/core';
entity.addComponent(ScreenSpace, {
width: '400px',
height: '300px',
top: '20px',
left: '20px',
bottom: 'auto',
right: 'auto',
zOffset: 0.2,
});
How it works: The system creates hidden DOM elements and uses getComputedStyle() to convert CSS values → pixels → meters. This means any valid CSS expression works (calc(), vw, vh, %, etc.).
XR behavior: When entering XR, ScreenSpaceUISystem automatically moves the panel back to world space. When exiting XR, it re-positions to screen space.
27. Entity API
entity.destroy();
entity.dispose();
entity.getComponents();
dispose() vs destroy(): Use dispose() when the entity has meshes, materials, or textures that should be freed from GPU memory. Use destroy() when GPU resources are shared or managed elsewhere. Use dispose() with caution when resources may be shared across multiple entities.
28. Utility Functions
import { setWorldPosition, setWorldQuaternion } from '@iwsdk/core';
setWorldPosition(object3D, worldPosition);
setWorldQuaternion(object3D, worldQuaternion);
These are useful when you need to position an object in world space but it's nested under transformed parents. They compute the correct local transform to achieve the desired world transform.
world.getActiveRoot();
world.getPersistentRoot();
Core Components Reference (30 Total)
| Component | Purpose |
|---|
| Transform | Position, rotation, scale |
| Visibility | Show/hide objects |
| LevelTag | Marks level membership |
| LevelRoot | Level root marker |
| Interactable | Marks interactive objects |
| Hovered | Currently hovered |
| Pressed | Currently pressed/grabbed |
| OneHandGrabbable | Single-hand manipulation |
| TwoHandsGrabbable | Two-hand manipulation |
| DistanceGrabbable | Grab from distance |
| Handle | Manipulation handle |
| PhysicsBody | Physics motion properties |
| PhysicsShape | Collision shape + material |
| PhysicsManipulation | Force/velocity application |
| DomeGradient | Gradient sky |
| DomeTexture | Textured sky |
| IBLGradient | Gradient IBL lighting |
| IBLTexture | Texture IBL lighting |
| PanelUI | UI panel configuration |
| PanelDocument | Loaded UI document |
| ScreenSpace | Screen-attached UI |
| Follower | Object following |
| XRPlane | Detected AR planes |
| XRMesh | Detected AR meshes |
| XRAnchor | Spatial anchors |
| AudioSource | Audio configuration |
| CameraSource | Camera device |
| DepthOccludable | Depth-based occlusion for AR |
| LocomotionEnvironment | Locomotion settings |
| EnvironmentRaycastTarget | AR environment hit-test target |
Core Systems Reference (19 Total)
| System | Priority | Purpose |
|---|
| LocomotionSystem | -5 | Movement (teleport/slide/turn) |
| InputSystem | -4 | Interactable state management |
| GrabSystem | -3 | Grab handling |
| PhysicsSystem | -2 | Physics simulation |
| SceneUnderstandingSystem | -1 | AR plane/mesh detection |
| EnvironmentRaycastSystem | -1 | AR environment raycasting |
| CameraSystem | default | Camera access |
| LevelSystem | default | Level loading |
| EnvironmentSystem | default | Lighting/sky |
| AudioSystem | default | Spatial audio |
| TransformSystem | default | Transform sync |
| VisibilitySystem | default | Visibility sync |
| PanelUISystem | default | UI panels |
| ScreenSpaceUISystem | default | Screen UI |
| FollowSystem | default | Object following |
| TurnSystem | default | Rotation |
| TeleportSystem | default | Teleportation |
| SlideSystem | default | Smooth movement |
| DepthSensingSystem | default | Depth occlusion for AR |
Custom System Priority Guidelines
Register custom systems with priorities following input→simulation→rendering pipeline:
Priority 0-9: Input capture (player input, AI decisions)
Priority 10-19: Simulation (physics step, game logic)
Priority 20-29: Visual sync (sync Three.js objects to physics)
Priority 30+: Low-priority updates (UI, HUD, ambient effects)
Example:
world
.registerSystem(PlayerInputSystem, { priority: 0 })
.registerSystem(AIDriverSystem, { priority: 1 })
.registerSystem(PhysicsStepSystem, { priority: 10 })
.registerSystem(VehicleSyncSystem, { priority: 20 })
.registerSystem(DashboardSystem, { priority: 35 });
Project Structure
my-iwsdk-project/
├── src/
│ ├── index.ts # Entry point with World.create()
│ ├── systems/
│ │ ├── ui-system.ts # Panel/UI management
│ │ └── game-system.ts # Game logic
│ └── components/
│ └── custom.ts # Custom component definitions
├── public/
│ ├── gltf/ # 3D models
│ ├── audio/ # Audio files
│ ├── glxf/ # Generated scene files
│ └── ui/ # Compiled UI configs
├── ui/
│ └── *.uikitml # UI markup source
├── metaspatial/ # Meta Spatial Editor project
├── vite.config.ts
└── package.json
What IWSDK Provides (Don't Rebuild These)
Before writing custom code, check if IWSDK already provides the functionality. Rebuilding built-in features wastes time and produces inferior results (missing BVH acceleration, XR compatibility, comfort features, etc.).
Reinvention Risk Table
| What you might build from scratch | What IWSDK already provides |
|---|
| GLTF loading with GLTFLoader | AssetManager.loadGLTF() or AssetManifest in World.create() |
| Ray/line mesh for controller pointing | RayPointer — cylinder + gradient shader + cursor circle (auto) |
| Hover/click detection with Raycaster | Interactable + Hovered + Pressed components |
| Custom skybox sphere | DomeGradient or DomeTexture component |
| PBR environment lighting | IBLGradient or IBLTexture component |
| Teleport arc + landing marker | LocomotionSystem — full visuals included |
| Comfort vignette for motion | LocomotionSystem — comfortAssist config |
| Controller 3D models | Auto-loaded from WebXR Input Profiles |
| Hand tracking meshes | AnimatedHand with skeletal mesh + outline |
| Object grab + manipulation | OneHandGrabbable / TwoHandsGrabbable / DistanceGrabbable |
| Hit-test against real world | EnvironmentRaycastTarget component |
| Spatial audio | AudioSource component with pooling |
| Camera feed texture | CameraSource component |
| Depth occlusion shader | DepthOccludable component |
| HUD / screen-space UI | ScreenSpace component with CSS units |
| Follow-head billboard | Follower component |
| Scene cleanup on level change | LevelSystem + LevelTag (automatic) |
| Gamepad button debouncing | StatefulGamepad — getButtonDown() / getButtonUp() |
| Manual GPU cleanup with traverse | entity.dispose() — destroys entity + cleans up geometry/materials/textures |
| Manual world-space positioning | setWorldPosition() / setWorldQuaternion() utilities |
| Manual XR hit-test setup | EnvironmentRaycastTarget component + EnvironmentRaycastSystem |
| Manual camera video feed | CameraSource component + CameraUtils static class |
Asset Loading (AssetManager)
Always use AssetManager — never use raw GLTFLoader, TextureLoader, etc. AssetManager handles DRACO/KTX2 decoder setup, caching, and de-duplication automatically.
Manifest pattern (preload at startup):
const world = await World.create(container, {
assets: {
myModel: {
url: '/models/scene.glb',
type: AssetType.GLTF,
priority: 'critical',
},
mySound: {
url: '/audio/click.mp3',
type: AssetType.Audio,
priority: 'background',
},
myTexture: { url: '/textures/wood.jpg', type: AssetType.Texture },
myHDR: { url: '/textures/env.hdr', type: AssetType.HDRTexture },
},
});
const gltf = AssetManager.getGLTF('myModel');
const texture = AssetManager.getTexture('myTexture');
const audio = AssetManager.getAudio('mySound');
Runtime loading (on-demand):
const gltf = await AssetManager.loadGLTF('/models/dynamic.glb', 'dynamicModel');
const texture = await AssetManager.loadTexture(
'/textures/new.jpg',
'newTexture',
);
Supported AssetTypes: GLTF, Audio, Texture, HDRTexture
Entity Parenting & Level Lifecycle
Always use createTransformEntity — never scene.add(). Entities created with createTransformEntity get a Transform component, participate in ECS queries, and are automatically managed by the level system.
const entity = world.createTransformEntity(mesh, parentEntity);
const entity = world.createTransformEntity(mesh, {
parent: parentEntity,
persistent: false,
});
const hud = world.createTransformEntity(hudMesh, {
parent: world.sceneEntity,
persistent: true,
});
Level lifecycle:
- Entities with
LevelTag are automatically destroyed when world.loadLevel() is called
- Non-persistent entities created via
createTransformEntity automatically get LevelTag
- Use
persistent: true for entities that should survive level transitions (HUDs, audio managers, etc.)
world.loadLevel(url) destroys all level-tagged entities, then loads the new GLXF scene
Warning: Parenting an Object3D under another Object3D that is NOT an entity's object3D will silently reparent it to the scene root and log a warning.
Input & Interaction
Interactable → Hovered/Pressed flow:
- Add
Interactable component to any entity that should respond to pointer input
InputSystem automatically performs BVH-accelerated raycasting each frame
- When a ray hits an Interactable entity,
Hovered tag is added
- When the user presses the select button while hovering,
Pressed tag is added
- Query for
Hovered/Pressed in your system to react to interactions
entity.addComponent(Interactable);
export class MyInteractionSystem extends createSystem({
hovered: { required: [MyComponent, Hovered] },
pressed: { required: [MyComponent, Pressed] },
}) {
init() {
this.queries.pressed.subscribe('qualify', (entity) => {
});
}
}
StatefulGamepad API (accessed via this.input.xr.gamepads.left / .right):
getButtonDown(InputComponent.Trigger) — true on the frame the button was pressed
getButtonUp(InputComponent.Trigger) — true on the frame the button was released
getButtonPressed(InputComponent.Trigger) — true while held
getButtonValue(InputComponent.Trigger) — analog 0-1
getAxesValues(InputComponent.Thumbstick) — { x, y } in -1 to 1 range
getAxesEnteringUp/Down/Left/Right(InputComponent.Thumbstick) — directional flick detection
Built-in Visuals (Don't Recreate)
These visuals are automatically created and managed by IWSDK systems:
- RayPointer — Cylinder mesh with gradient shader + circular cursor at hit point. Created automatically by
InputSystem for each connected controller.
- AnimatedController — GLTF controller models auto-loaded from the WebXR Input Profiles registry. Matches the user's actual hardware.
- AnimatedHand — Skeletal hand mesh with outline shader for hand tracking mode. Auto-managed by the input system.
- Teleport visuals — Parabolic arc + landing circle indicator. Rendered by
LocomotionSystem when teleport mode is active.
- Comfort vignette — Screen-edge darkening during smooth locomotion. Controlled by
locomotion.config.comfortAssist (0 = off, 1 = maximum).
- DomeGradient / DomeTexture — Sky dome rendering. Add the component to an entity;
EnvironmentSystem handles the rest.
Anti-Patterns to Avoid
- DON'T store entity arrays in systems - use queries
- DON'T poll for state changes - use signal subscriptions
- DON'T manually track component additions/removals - use query subscribe
- DON'T create entities in update() without proper lifecycle management
- DON'T use
Types.Object for data that could be typed (use Vec3, Float32, etc.)
- DON'T forget cleanup functions for subscriptions and resources
- DON'T modify entities during query iteration without careful consideration
- DON'T enable locomotion without collision geometry - player falls through world
- DON'T enable features you don't use - adds overhead and can cause bugs
- DON'T confuse PhysicsBody (motion) with PhysicsShape (collision + material)
- DON'T use raw
GLTFLoader/TextureLoader — use AssetManager for caching, DRACO/KTX2 setup
- DON'T use
scene.add() — use createTransformEntity() for proper ECS integration
- DON'T use
new Raycaster() — use Interactable component for BVH-accelerated XR interaction
- DON'T add environment components (
DomeGradient/IBLTexture/etc.) to arbitrary entities — must go on the level root (world.activeLevel.value)
- DON'T forget
_needsUpdate after changing environment properties — changes are silently ignored without entity.setValue(DomeGradient, '_needsUpdate', true)
- DON'T use
entity.destroy() for objects with GPU resources — use entity.dispose() which also cleans up geometry/materials/textures
- DON'T pass numbers to
ScreenSpace — all position/size values are CSS strings like '400px' or '50vw'
Performance Tips
- Use
getVectorView() for direct TypedArray access (zero-copy)
- Batch query enumeration (don't create intermediate arrays)
- Use tag components for cheap boolean queries
- Leverage query filters (
where) to reduce iteration scope
- System config signals auto-deduplicate (no callback if value unchanged)
- Physics and locomotion can run in Web Workers for heavy scenes
- Use
PhysicsShapeType.Auto to let IWSDK pick optimal collision shape
When Planning a New Feature
- Determine feature flags - What built-in features does this need? (locomotion, physics, grabbing, etc.)
- Check prerequisites - If using locomotion, is collision geometry set up?
- Identify components needed - What data does this feature require?
- Design queries - How will systems find relevant entities?
- Plan reactivity - What changes should trigger updates?
- Consider lifecycle - When are entities created/destroyed?
- Map to existing systems - Can built-in systems (grab, physics, etc.) help?
- VR vs AR - Does this work differently in each mode?
- Input handling - What controller/hand inputs are needed?
- Audio feedback - What sounds should play on interactions?
- Select 3D assets - What models are needed? (see Asset Selection below)
Asset Selection with Kenney Prototype Kit
This project includes the Kenney Prototype Kit at public/kenney_prototype-kit/ with 143 prototyping models.
During Planning: Choose Assets
When planning a feature that needs 3D models, consult the asset catalog:
-
Read the catalog index to find relevant models:
Read: public/kenney_prototype-kit/catalog/README.md
-
Browse category files for detailed descriptions:
walls.md - 25 wall pieces for room construction
floors.md - 8 ground/platform surfaces
doors.md - 6 animated doorways
shapes.md - 18 geometric primitives
indicators.md - 17 waypoints/markers
misc-props.md - Coins, crates, flags
buttons-levers.md - Interactive controls
- And more...
-
Preview models visually using the /preview-model skill:
/preview-model wall-corner
/preview-model door-rotate b
-
Document asset choices in your plan with:
- Model name and category
- Suggested position/scale
- Texture variation (a, b, or c)
Asset Selection Checklist
When your plan involves 3D objects, answer these:
| Question | Example Answer |
|---|
| What models are needed? | wall-corner, door-rotate, indicator-arrow |
| What texture variation? | Variation A (purple/lavender with orange accents) |
| What scale factor? | 0.5x for desk-scale, 1.0x for room-scale |
| Where positioned? | (0, 0.85, -1.5) on desk, (0, 0, -3) on floor |
| Any interactions? | Grabbable, physics-enabled, trigger zones |
Example: Planning a Simple Puzzle Room
Feature: Player solves a button puzzle to open a door
Asset Selection:
- Read
catalog/walls.md → select wall-corner, wall-doorway
- Read
catalog/doors.md → select door-rotate (animated swing door)
- Read
catalog/buttons-levers.md → select button-round (pressable)
- Read
catalog/indicators.md → select indicator-arrow (shows where to go)
Preview with /preview-model:
/preview-model door-rotate a
/preview-model button-round a
Document in plan:
Assets:
- door-rotate.glb at (0, 0, -3), scale 1.0, variation-a texture
- button-round.glb at (1, 1, -2), scale 0.5, variation-a texture
- indicator-arrow.glb at (0, 0.1, -2.5), scale 0.3, variation-a texture