Skip to content

Commit 498705f

Browse files
committed
Gamepad-based button/axis state (#499)
This commit describes how to access controller button and axis state for `XRInputSource`s via a `gamepad` attribute, which is an instance of the `Gamepad` interface described in the Gamepad API. It details the additional restrictions that apply to gamepads that are exposed in this way, as well as how they interact with the existing `select/selectstart/selectend` events.
1 parent df0844d commit 498705f

File tree

1 file changed

+131
-2
lines changed

1 file changed

+131
-2
lines changed

input-explainer.md

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,29 @@ function updatePointingRay(inputSourcePose, virtualHitTestResult) {
220220
```
221221

222222
### Renderable models
223-
For `tracked-controller` input sources, it is often appropriate for the application to render a contextually appropriate model (such as a racket in a tennis game), However, there are times when it's best to not render anything at all, such as when the XR device uses a transparent display and the user can see their hands and/or any tracked devices without app assistance. See [Handling non-opaque displays](explainer.md#Handling-non-opaque-displays) in the main explainer for more details.
223+
For `tracked-pointer` input sources, it is often appropriate for the application to render a contextually appropriate model (such as a racket in a tennis game). Other times it's desirable for the application to render a device that matches what the user is holding, especially when relaying instructions about it's use. Finally, there are times when it's best to not render anything at all, such as when the XR device uses a transparent display and the user can see their hands and/or any tracked devices without app assistance. See [Handling non-opaque displays](explainer.md#Handling-non-opaque-displays) in the main explainer for more details.
224+
225+
226+
#### Choosing renderable models
227+
The majority of `tracked-pointer` input sources will have a non-null `gamepad` attribute on the `XRInputSource` object. The `Gamepad`'s `id` is used to determine what should be rendered if the app intends to visualize the input source itself, rather than an alternative virtual object. (See the section on [Button and Axis State](#button-and-axis-state) for more details.)
228+
229+
The WebXR Device API currently does not offer any way to retrieve renderable resources that represent the input devices from the API itself, and as such the `Gamepad`'s `id` must be used as a key to load an appropriate resources from the application's server or a CDN. The example below presumes that the `getInputSourceRenderableModel` call would do the required lookup and caching.
230+
231+
```js
232+
function loadRenderableInputModels(xrInputSource) {
233+
// Don't load renderable models if the input source doesn't have a gamepad.
234+
if (!inputSource.gamepad)
235+
return;
236+
237+
// Retrieve a mesh to render based on the gamepad object's id.
238+
let renderableModel = getInputSourceRenderableModel(inputSource.gamepad.id);
239+
if (!renderableModel)
240+
return;
241+
242+
// Add the model to the imaginary 3D engine's scene.
243+
scene.inputObjects.add(renderableModel, xrInputSource);
244+
}
245+
```
224246

225247
#### Placing renderable models
226248
The `targetRaySpace` should not be used to place the renderable model of a 'tracked-pointer'. Instead, 'tracked-pointer' input sources will have a non-null `gripSpace` which should be used instead. The `gripSpace` is an `XRSpace` where, if the user was holding a straight rod in their hand, it would be aligned with the negative Z axis (forward) and the origin rests at their palm. In many cases this will be different from the `targetRaySpace` such as on a motion controller with a tip angled slightly downward for comfort.
@@ -229,11 +251,17 @@ Using the `gripSpace` developers can properly render a virtual object held in th
229251

230252
Similar to the description in the [Targeting ray pose](#targeting-ray-pose) section, developers should pass their `gripSpace` to `XRFrame.getPose()` each frame for updated location information. Developers should take care to check the result from `getPose()` as it may return `null` in cases where tracking has been lost or the `XRSpace`'s `XRInputSource` instance is no longer connected or available.
231253

254+
Finally, rendered models of input sources should generally only be shown if the `XRSession`'s `environmentBlendMode` is `'opaque'`, as any other mode implies that the user can see any physical device they may be holding. Rendering cursors, highlights, and pointing rays may still be desirable, depending on the application's needs.
255+
232256
```js
233257
function updateRenderableInputModels(xrFrame) {
258+
// Don't display input models if the blend mode is not 'opaque'
259+
if (xrFrame.session.environmentBlendMode != 'opaque')
260+
return;
261+
234262
foreach(inputObject of scene.inputObjects) {
235263
let xrInputSource = inputObject.xrInputSource;
236-
if(xrInputSource.gripSource) {
264+
if(xrInputSource.gripSpace) {
237265
let pose = xrFrame.getPose(xrInputSource.gripSpace, xrReferenceFrame);
238266
if (pose) {
239267
inputObject.setTransform(pose.transform);
@@ -246,6 +274,104 @@ function updateRenderableInputModels(xrFrame) {
246274
}
247275
```
248276

277+
## Button and Axis State
278+
279+
Some applications need more than point-and-click style interaction provided by the `select` events. For input sources with buttons and axes, more complete information about the state of those inputs can be observed via the `XRInputSource`'s `gamepad` attribute. `gamepad` is an instance of the [`Gamepad`](https://w3c.github.io/gamepad/#gamepad-interface) interface if the input source has buttons and axes to report, and `null` otherwise.
280+
281+
Examples of input sources that may expose their state this way include Oculus Touch, Vive wands, Oculus Go and Daydream controllers, or other similar devices. Input devices not directly associated with the XR device, such as the majority of traditional gamepads, and tracked devices without discreet inputs, such as optical hand tracking, must not be exposed using this interface.
282+
283+
`Gamepad` instances reported in this way have several notable behavioral changes vs. the ones reported by `navigator.getGamepads()`:
284+
285+
- `Gamepad` instances connected to an `XRInputSource` must not be included in the array returned by `navigator.getGamepads()`.
286+
- The `Gamepad`'s `index` attribute must be `-1`.
287+
- The `Gamepad`'s `connected` attribute must be `true` unless the related `XRInputSource` is removed from the `inputSources` array or the related `XRSession` is ended.
288+
289+
Finally, the `id` attribute for `Gamepad`s surfaced by the WebXR API are more strictly formatted than those of traditional gamepads in order to make them a more appropriate key for determining rendering assets.
290+
291+
- The `id` MAY be `'unknown'` if the type of input source cannot be reliably identified or the UA determines that the input source type must be masked for any reason. Applications should render a generic input device in this case.
292+
- Inline sessions MUST only expose `id`s of `'unknown'`.
293+
- Otherwise the `id` must be a lower-case string that describes the physical input source.
294+
- The exact format is [still under discussion](https://github.com/immersive-web/webxr/issues/550), but will probably need to include at least an indication of the input device's manufacturer and model. (Such as `oculus-touch`)
295+
- It must not include an indication of the handedness of the input source (such as `oculus-touch-left`), as that can be determined from the `handedness` attribute.
296+
- UAs SHOULD make an effort to align on the strings that are returned for any given device.
297+
298+
All other attributes behave as described in the [Gamepad](https://w3c.github.io/gamepad/) specification.
299+
300+
```js
301+
function onXRFrame(timestamp, frame) {
302+
let inputSource = primaryInputSource;
303+
304+
// Check to see if the input source has gamepad data.
305+
if (inputSource && inputSource.gamepad) {
306+
let gamepad = inputSource.gamepad;
307+
308+
// Use joystick or touchpad values for movement.
309+
if (gamepad.axes.length >= 2) {
310+
MoveUser(gamepad.axes[0], gamepad.axes[1]);
311+
}
312+
313+
// If the first gamepad button is pressed, perform an action.
314+
if (gamepad.buttons.length >= 1 && gamepad.buttons[0].pressed) {
315+
EmitPaint();
316+
}
317+
318+
// etc.
319+
}
320+
321+
// Do the rest of typical frame processing...
322+
}
323+
```
324+
325+
If the application includes interactions that require user activation (such as starting media playback), the application can listen to the `XRInputSource`s `select` events, which fire for every pressed and released button on the controller. When triggered by a controller input, the `XRInputSourceEvent` will include a `buttonIndex` other than `-1` to indicate which button on the gamepad triggered the event.
326+
327+
The UA may update the `gamepad` state at any point, but it must remain constant while running a batch of `XRSession` `requestAnimationFrame` callbacks or event callbacks which provide an `XRFrame`.
328+
329+
### XR gamepad mapping
330+
331+
The WebXR Device API also introduces a new standard controller layout indicated by the `mapping` value of `xr-standard`. (Additional mapping variants may be added in the future if necessary.) This defines a specific layout for the inputs most commonly found on XR controller devices today. The following table describes the buttons/axes and their physical locations:
332+
333+
| Button | `xr-standard` Location |
334+
| ---------- | --------------------------------- |
335+
| buttons[0] | Primary trigger |
336+
| buttons[1] | Primary Touchpad/Joystick click |
337+
| buttons[2] | Grip/Secondary trigger |
338+
| buttons[3] | Secondary Touchpad/Joystick click |
339+
340+
| Axis | `xr-standard` Location |
341+
| ------- | ----------------------------- |
342+
| axes[0] | Primary Touchpad/Joystick X |
343+
| axes[1] | Primary Touchpad/Joystick Y |
344+
| axes[2] | Secondary Touchpad/Joystick X |
345+
| axes[3] | Secondary Touchpad/Joystick Y |
346+
347+
Additional device-specific inputs may be exposed after these reserved indices, but devices that lack one of the canonical inputs must still preserve their place in the array. If a device has both a touchpad and a joystick the UA should designate one of them to be the primary axis-based input and expose the other at axes[2] and axes[3] with an associated button at button[3].
348+
349+
In order to make use of the `xr-standard` mapping, a device must meet **at least** the following criteria:
350+
351+
- Is a `tracked-pointer` device.
352+
- Has a trigger or similarly accessed button
353+
- Has at least one touchpad or joystick
354+
355+
devices that lack one of those elements may still expose `gamepad` data, but must not claim the `xr-standard` mapping. For example: The controls on the side of a Gear VR would not qualify for the `xr-standard` mapping because they represent a `gaze`-style input. Similarly, a Daydream controller would not qualify for the `xr-standard` mapping since it lacks a trigger.
356+
357+
### Exposing button/axis values with action maps
358+
359+
Some native APIs rely on what's commonly referred to as an "action mapping" system to handle controller input. In action map systems the developer creates a list of application-specific actions (such as "undo" or "jump") and suggested input bindings (like "left hand touchpad") that should trigger the related action. Such systems may allow users to re-bind the inputs associated with each action, and may not provide a mechanism for enumerating or monitoring the inputs outside of the action map.
360+
361+
When using an API that limits reading controller input to use of an action map, it is suggested that a mapping be created with one action per possible input, given the same name as the target input. For example, an similar mapping to the following may be used for each device:
362+
363+
| Button/Axis | Action name | Sample binding |
364+
|-------------|------------------|---------------------------|
365+
| button[0] | "trigger" | "[device]/trigger" |
366+
| button[1] | "touchpad-click" | "[device]/touchpad/click" |
367+
| button[2] | "grip" | "[device]/grip" |
368+
| axis[0] | "touchpad-x" | "[device]/touchpad/x" |
369+
| axis[1] | "touchpad-y" | "[device]/touchpad/y" |
370+
371+
If the API does not provided a way to enumerate the available input devices, the UA should provide bindings for the left and right hand instead of a specific device and expose a `Gamepad` for any hand that has at least one non-`null` input.
372+
373+
The UA must not make any attempt to circumvent user remapping of the inputs.
374+
249375
## Appendix A: Proposed partial IDL
250376
This is a partial IDL and is considered additive to the core IDL found in the main [explainer](explainer.md).
251377
```webidl
@@ -284,6 +410,7 @@ interface XRInputSource {
284410
readonly attribute XRTargetRayMode targetRayMode;
285411
readonly attribute XRSpace targetRaySpace;
286412
readonly attribute XRSpace? gripSpace;
413+
readonly attribute Gamepad? gamepad;
287414
};
288415
289416
//
@@ -317,10 +444,12 @@ dictionary XRInputSourceChangeEventInit : EventInit {
317444
interface XRInputSourceEvent : Event {
318445
readonly attribute XRFrame frame;
319446
readonly attribute XRInputSource inputSource;
447+
readonly attribute long buttonIndex;
320448
};
321449
322450
dictionary XRInputSourceEventInit : EventInit {
323451
required XRFrame frame;
324452
required XRInputSource inputSource;
453+
long buttonIndex = -1;
325454
};
326455
```

0 commit comments

Comments
 (0)