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):
XRControllerVisualAdapterXRHandVisualAdapter- Responsibilities:
- Connect/disconnect to
XRInputSource. - Keep
visual.modelaligned to the current grip space. - Choose a visual implementation and asset based on input profile.
- Expose
isPrimaryand hook intoXROriginspaces.
- Connect/disconnect to
- Implementations:
AnimatedController— animates buttons/axes using the WebXR Input Profile’s visual responses; usesFlexBatchedMeshfor draw‑call reduction.AnimatedHand— skinned hand with an outline pass (stencil + back‑face); updates joints fromfillPoses.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.
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.
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.
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.profileslist and resolves a WebXR Input Profile JSON. From that it gets:layout(per handedness) including component indices and visual response nodes.assetPathsuggestion for GLTF; the adapter constructshttps://cdn.jsdelivr.net/.../<profileId>/<left|right>.glbunless 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:
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:
xrInput.visualAdapters.controller.left.updateVisualImplementation(
MyControllerVisual,
);Guidelines:
- Cache keys: set a unique
assetKeyPrefixand optionallyassetProfileId/assetPathif you want a custom asset per profile/handedness. The loader caches visuals perassetKeyPrefix-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:
// 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’sassetProfileIdforces a specific one. - Models appear but don’t animate: confirm the layout’s
visualResponsesnode names exist in your GLTF and thatGamepadis present on the input source. - Hands not moving: some runtimes lack the optional
XRFrame.fillPoses. Consider a fallback to per‑jointgetJointPoseif targeting those.