Skip to content

Input Visuals ​

Input visuals render the user’s controllers or hands and keep them animated from live input. IWSDK separates “adapters” (WebXR + scene wiring) from “implementations” (how the model looks and animates).

Architecture ​

  • Adapters (per side):
    • XRControllerVisualAdapter
    • XRHandVisualAdapter
    • Responsibilities:
      • Connect/disconnect to XRInputSource.
      • Keep visual.model aligned to the current grip space.
      • Choose a visual implementation and asset based on input profile.
      • Expose isPrimary and hook into XROrigin spaces.
  • Implementations:
    • AnimatedController — animates buttons/axes using the WebXR Input Profile’s visual responses; uses FlexBatchedMesh for draw‑call reduction.
    • AnimatedHand — skinned hand with an outline pass (stencil + back‑face); updates joints from fillPoses.
    • AnimatedControllerHand — a controller‑holding hand that blends toward a “pressed” pose from button values.

Using visuals ​

Visuals are created automatically by XRInputManager when an input source appears. Add the origin to your scene and call update each frame.

ts
import { XRInputManager } from '@iwsdk/xr-input';

const xrInput = new XRInputManager({ scene, camera });
scene.add(xrInput.xrOrigin);

renderer.setAnimationLoop((t) => {
  xrInput.update(renderer.xr, clock.getDelta(), t / 1000);
  renderer.render(scene, camera);
});

By default, controllers use AnimatedController and hands use AnimatedHand. Only the “primary” source per side is visible; secondary sources are tracked but hidden.

Swapping visual implementations ​

You can switch an adapter to a different implementation class at runtime. This keeps the WebXR wiring and swaps the rendering logic.

ts
import { AnimatedControllerHand } from '@iwsdk/xr-input';

// Replace the controller visual with a controller-hand hybrid on the right hand
const rightController = xrInput.visualAdapters.controller.right;
rightController.updateVisualImplementation(AnimatedControllerHand);

Implementation classes must follow the VisualImplementation interface and usually extend BaseControllerVisual or BaseHandVisual.

Customizing asset loading (CDN, cache, etc.) ​

Adapters fetch assets via an XRAssetLoader (default uses GLTFLoader). Provide your own to change source or add caching.

ts
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';

const assetLoader = {
  async loadGLTF(assetPath: string): Promise<GLTF> {
    // Implement your fetch policy here: local cache, versioned CDN, etc.
    return await new GLTFLoader().loadAsync(assetPath);
  },
};

const xrInput = new XRInputManager({ scene, camera, assetLoader });

How profiles pick visuals ​

  • Controllers: the adapter reads the XRInputSource.profiles list and resolves a WebXR Input Profile JSON. From that it gets:
    • layout (per handedness) including component indices and visual response nodes.
    • assetPath suggestion for GLTF; the adapter constructs https://cdn.jsdelivr.net/.../<profileId>/<left|right>.glb unless overridden.
  • Hands: a default “generic hand” layout is used; hand joints are updated via XRFrame.fillPoses.

Note: The repo prebuild script generates generated-profiles.ts so profiles are available at runtime without network requests for JSON.

Build your own visual ​

Start from one of the base classes:

ts
import { Group, MeshStandardMaterial } from 'three';
import { BaseControllerVisual } from '@iwsdk/xr-input';

export class MyControllerVisual extends BaseControllerVisual {
  init() {
    // Called once after GLTF is loaded
    this.model.traverse((n) => {
      // material tweaks, batching, etc.
      if ((n as any).isMesh) (n as any).material = new MeshStandardMaterial();
    });
  }
  update(dt: number) {
    if (!this.gamepad || !this.model.visible) return;
    // Read button/axis values from this.gamepad and animate nodes
  }
}
MyControllerVisual.assetKeyPrefix = 'my-controller';

Then instruct an adapter to use it:

ts
xrInput.visualAdapters.controller.left.updateVisualImplementation(
  MyControllerVisual,
);

Guidelines:

  • Cache keys: set a unique assetKeyPrefix and optionally assetProfileId/assetPath if you want a custom asset per profile/handedness. The loader caches visuals per assetKeyPrefix-profileId-handedness.
  • Keep your GLTF skeleton/joint names consistent with layout or your update code.
  • For hands, ensure your skinned mesh disables frustum culling and consider an outline pass for legibility.

Toggling visibility ​

You can enable/disable visuals without disconnecting the adapter:

ts
// Controller visuals off, logic continues to run
xrInput.visualAdapters.controller.left.toggleVisual(false);

// Hand visuals on
xrInput.visualAdapters.hand.left.toggleVisual(true);

Troubleshooting ​

  • Seeing the wrong controller model: check the runtime‑reported profile list in devtools (inputSource.profiles) and whether your adapter’s assetProfileId forces a specific one.
  • Models appear but don’t animate: confirm the layout’s visualResponses node names exist in your GLTF and that Gamepad is present on the input source.
  • Hands not moving: some runtimes lack the optional XRFrame.fillPoses. Consider a fallback to per‑joint getJointPose if targeting those.

Privacy | Terms