Skip to content

XR Input with IWSDK

This section explains how IWSDK’s XR input stack works and how to use it in your Three.js + WebXR apps. It covers controller/hand visuals, gamepad state, ray/grab pointers, and the XR origin rig (spaces for head, ray, and grip).

When to Use What (Guidance)

A quick decision guide to pick the right layer for input. The rule of thumb is separation of concerns: visuals render input; behaviors respond to events; advanced logic reads the gamepad; attachments live on XR spaces.

  • Input Visuals — presentation only

    • Use to show controllers/hands and animate them from live input.
    • Do not implement game behavior in a visual’s update — keep it strictly render‑oriented (posing meshes, materials, effects). Launching projectiles, grabbing objects, etc. should live elsewhere.
    • If you need a specialized representation (e.g., a tool with custom meshes), create a custom visual implementation and swap it in at runtime.
  • Pointer Events — default for interactions

    • Use for “click, hover, drag” style interactions that work across input modalities (controllers and hand tracking). The same handlers are driven by rays or near‑grabs.
    • Recommended first choice: reactively trigger app behavior from pointer events (onClick, onPointerDown, onPointerMove, …) rather than polling buttons.
    • Benefits: capture/cancel semantics, works with both ray and grab, and integrates cleanly with the scene graph.
  • Stateful Gamepad — advanced/custom input

    • Use when you need logic that can’t be expressed as pointer events: combos, long‑press thresholds, two‑stick gestures, analog values, or non‑ray inputs.
    • Mapping‑aware: built on WebXR Input Profiles, so you can query by component id (e.g., 'xr-standard-squeeze') instead of device‑specific indices.
    • Combine with pointer events: e.g., use axes for locomotion and pointer events for UI.
  • XR Origin Spaces — attaching objects

    • Use xrOrigin.raySpaces.left/right to attach tools aligned with the pointing ray.
    • Use xrOrigin.gripSpaces.left/right to attach held items (flashlights, gadgets).
    • Spaces are persistent; the system handles connect/disconnect and mapping to the current XR input source. You don’t need to listen for source changes.

Typical stack:

Visuals (render only)

Pointers (primary interactions across modalities)

Stateful Gamepad (advanced input & axes)

XR Origin Spaces (where to attach tools)

What the input system provides

  • A rig of XR spaces (head, ray, grip) updated from the XR frame each tick.
  • Visual adapters for controllers and hands that load GLTF models (WebXR Input Profiles) and animate them from live input.
  • A StatefulGamepad helper that exposes edge‑triggered button events, 2D axes magnitudes, and per‑direction transitions.
  • A MultiPointer that aggregates built‑in ray + grab pointers using @pmndrs/pointer-events.

Quick Start

ts
import { PerspectiveCamera, Scene, WebGLRenderer } from 'three';
import { XRInputManager } from '@iwsdk/xr-input';

const scene = new Scene();
const camera = new PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 100);
const renderer = new WebGLRenderer({ antialias: true });
renderer.xr.enabled = true;

// 1) Create input manager
const xrInput = new XRInputManager({ scene, camera });
scene.add(xrInput.xrOrigin); // parent the rig

// 2) Optional: react to the primary adapter (left/right)
xrInput.visualAdapters.left.subscribe((adapter) => {
  console.log('Left primary is now', adapter?.constructor.name);
});

// 3) In your frame loop
renderer.setAnimationLoop((timeMs) => {
  const time = timeMs / 1000;
  const delta = renderer.clock.getDelta();
  xrInput.update(renderer.xr, delta, time);
  renderer.render(scene, camera);
});

Example: cross‑modal button using pointer events

ts
const button = new Mesh(
  new BoxGeometry(0.12, 0.02, 0.12),
  new MeshStandardMaterial(),
);
(button as any).onClick = () => spawnProjectile(); // works with controllers & hands
scene.add(button);

Example: advanced logic with StatefulGamepad

ts
const pad = xrInput.gamepads.right;
if (pad?.getButtonDown('xr-standard-trigger')) spawnProjectile();
if (pad?.getAxesEnteringLeft('xr-standard-thumbstick')) snapTurn(-30);

Example: attaching tools to spaces

ts
const gizmo = new Object3D();
gizmo.position.set(0, -0.02, 0.05);
xrInput.xrOrigin.gripSpaces.left.add(gizmo);

How the pieces fit together

XRSession → XRFrame → update poses

             ├─ XROrigin (head, ray L/R, grip L/R)

             ├─ Visual Adapters
             │    • Controller (GLTF, animated via input profile)
             │    • Hand (skinned, outline, pinch helper)

             └─ MultiPointer (ray + grab via @pmndrs/pointer-events)
                   • select → ray pointer
                   • squeeze → grab pointer
                   • edge events from StatefulGamepad

When to use which piece

  • Use XR Origin to attach your own tools to ray/grip spaces.
  • Use Input Visuals to show hands/controllers and customize assets.
  • Use Stateful Gamepad to read buttons/axes with edge triggers.
  • Use Pointers for ray selections and near grabbing against your Three.js scene.

Requirements

  • Three.js r160+ (peer dependency).
  • A WebXR‑capable browser/runtime; controllers expose inputSource.gamepad.
  • For ray/grab hit‑testing in your scene, your objects should be visible to the pointer‑events intersector (defaults to standard meshes).