Map Gamepad Buttons to WebXR Animations in Three.js
In WebXR applications built with Three.js, leveraging VR controllers can significantly enhance user immersion. This article provides a step-by-step guide on how to detect gamepad inputs, identify specific button presses, and map them to trigger custom 3D character or object animations using the Three.js AnimationMixer.
1. Access the WebXR Session and Gamepad API
To read inputs from VR controllers, you must access the active WebXR
session’s inputSources within your Three.js render loop.
Unlike standard 2D browser games, WebXR gamepads are accessed through
the XR input sources.
function animate() {
renderer.setAnimationLoop(render);
}
function render() {
const session = renderer.xr.getSession();
if (session) {
processGamepadInput(session);
}
// Update your Three.js animation mixer
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
renderer.render(scene, camera);
}2. Identify and Read Controller Buttons
Each controller in WebXR exposes a gamepad object
containing an array of buttons. While button mapping can
vary slightly between hardware manufacturers, the standard WebXR gamepad
mapping for modern controllers (like Meta Quest or Valve Index)
generally follows this layout:
- Button 0: Trigger
- Button 1: Grip/Squeeze
- Button 2: Touchpad/Joystick Click
- Button 3: Primary Button (A on right controller, X on left)
- Button 4: Secondary Button (B on right controller, Y on left)
Here is how to extract and monitor these inputs:
// Track previous frame states to detect single-click triggers
const prevButtonStates = new Map();
function processGamepadInput(session) {
for (const source of session.inputSources) {
if (source.gamepad) {
const gamepad = source.gamepad;
const handedness = source.handedness; // 'left' or 'right'
// We will map Button 3 (A/X button)
const actionButton = gamepad.buttons[3];
if (actionButton) {
const buttonKey = `${handedness}_button3`;
const wasPressed = prevButtonStates.get(buttonKey) || false;
if (actionButton.pressed && !wasPressed) {
// Trigger the animation only on the initial press event
triggerCustomAnimation(handedness);
}
// Save current state for the next frame comparison
prevButtonStates.set(buttonKey, actionButton.pressed);
}
}
}
}3. Set Up and Trigger the Three.js Animation
To play a custom animation when the button is pressed, load your GLTF
model with its animations and initialize a
THREE.AnimationMixer. Store the desired animation clip as
an AnimationAction.
let mixer;
let jumpAction;
let waveAction;
const loader = new GLTFLoader();
loader.load('path/to/model.gltf', (gltf) => {
const model = gltf.scene;
scene.add(model);
mixer = new THREE.AnimationMixer(model);
// Find animations by name
const jumpClip = THREE.AnimationClip.findByName(gltf.animations, 'Jump');
const waveClip = THREE.AnimationClip.findByName(gltf.animations, 'Wave');
if (jumpClip) jumpAction = mixer.clipAction(jumpClip);
if (waveClip) waveAction = mixer.clipAction(waveClip);
});Finally, define the triggerCustomAnimation function to
play the corresponding action based on which hand pressed the
button.
function triggerCustomAnimation(hand) {
let action;
if (hand === 'right' && jumpAction) {
action = jumpAction;
} else if (hand === 'left' && waveAction) {
action = waveAction;
}
if (action) {
action.reset();
action.setLoop(THREE.LoopOnce);
action.clampWhenFinished = true; // Stops at the final frame
action.play();
}
}