XrAction
) and the context of actions (XrActionSet
). As an OpenXR Native developer, you do not work directly with the hardware state of the user’s input device. Rather, you must focus on the action state and supply bindings that bind these actions to physical input for every tested device (interaction profile).XrActionSet cubeToolActionSet;
XrAction cubeSpawnAction;
XrActionSetCreateInfo actionSetInfo{XR_TYPE_ACTION_SET_CREATE_INFO};
actionSetInfo.next = nullptr;
actionSetInfo.actionSetName = "cube_tool"; // Machine friendly name
actionSetInfo.localizedActionSetName = "Cube tool"; // Human friendly name
xrCreateActionSet(instance, actionSetInfo, &cubeToolActionSet);
XrActionCreateInfo actionInfo = {XR_TYPE_ACTION_CREATE_INFO};
actionInfo.next = nullptr;
actionInfo.actionName = "spawn_cube"; // Machine friendly name
actionInfo.localizedActionName = "Place cube"; // Human friendly name
actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
xrCreateAction(cubeToolActionSet, actionInfo, &cubeSpawnAction);
XrActionType
is an enum that associates return value types to actions states, defined in the OpenXR specification as:typedef enum XrActionType {
XR_ACTION_TYPE_BOOLEAN_INPUT = 1,
XR_ACTION_TYPE_FLOAT_INPUT = 2,
XR_ACTION_TYPE_VECTOR2F_INPUT = 3,
XR_ACTION_TYPE_POSE_INPUT = 4,
XR_ACTION_TYPE_VIBRATION_OUTPUT = 100,
XR_ACTION_TYPE_MAX_ENUM = 0x7FFFFFFF
} XrActionType;
xrCreateActionSet
and xrCreateAction
. The localizedActionName
and localizedActionSetName
store suitable names for each action and action set respectively. The runtime may wish to display these to the user, for example, in a rebinding menu UI. (“Localized” means text in the natural language used in the user’s system UI.)XrPath touchInteractionProfile{XR_NULL_PATH};
xrStringToPath(instance, "/interaction_profiles/oculus/touch_controller", &touchInteractionProfile));
std::vector<XrActionSuggestedBinding> bindings{};
XrPath inputRightAClick{XR_NULL_PATH};
xrStringToPath(instance, "/user/hand/right/input/input/a/click", &inputRightAClick));
bindings.emplaceBack(XrActionSuggestedBinding{cubeSpawnAction, inputRightAClick});
XrPath inputRightSqueeze{XR_NULL_PATH};
xrStringToPath(instance, "/user/hand/right/input/squeeze/value", &inputRightSqueeze));
bindings.emplaceBack(XrActionSuggestedBinding{cubeRotateAction, inputRightSqueeze});
XrInteractionProfileSuggestedBinding suggestedBindings{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
// These bindings are for the Meta Quest Touch interaction profile
suggestedBindings.interactionProfile = touchInteractionProfile;
suggestedBindings.suggestedBindings = bindings.data();
suggestedBindings.countSuggestedBindings = bindings.size();
// Suggest all the bindings for the Meta Quest Touch interaction profile
xrSuggestInteractionProfileBindings(instance, &suggestedBindings);
// Repeat for other controllers, like vive_controller, xbox controller, and so on
"/interaction_profiles/oculus/touch_controller"
. So, this hypothetical app suggests bindings for the Touch controller. Then it lists a long list of pairs of values, like cubeSpawnAction, XrPath("/user/hand/right/input/input/a/click")
that bind the actions to a path. This path is a special string defined in the spec per interaction profile, referring to a physical button or any part of this input device. In this example the spawn cube action binds to the right-hand controller’s A button on the Meta Quest Touch controller. Then, you must do the same for all other actions.xrSuggestInteractionProfileBindings
and repeat the process for all interaction profiles (that is, every input device the you have tested the app against and aim to support by the app), for example, the Meta Quest Touch controller, the Vive controller, the Xbox controller, and so on.xrAttachSessionActionSets
function which attaches a group of action sets (therefore, their associated actions as well) to the session that is running.xrAttachSessionActionSets(session, &listOfActionSets);
xrAttachSessionActionSets
function can be called only once per session and apps can only use action sets that are part of this call. In this way, it is not possible for apps to add new actions as the user progresses through an experience. That would be problematic, for example, if the user’s system supports a rebinding UI and the user attempts a complete rebinding while in session. The system will have all the binding-related information regardless of the user’s progression to any specific part of an experience that uses them, such as interacting with a menu UI that displays at a certain stage of the app.xrSyncActions
once per frame, for example:std::vector<XrActiveActionSet> activeActionSets = { {cubeToolActionSet}, {navigationActionSet} };
XrActionsSyncInfo syncInfo = {XR_TYPE_ACTIONS_SYNC_INFO};
syncInfo.countActiveActionSets = activeActionSets.size();
syncInfo.activeActionSets = activeActionSets.data();
xrSyncActions(Session, &syncInfo);
xrSynActions
once and then ask about the state of the cubeSpawnAction
action twice, it is guaranteed to get the exact same result if there is no invocation of the xrSyncActions
function in between. This provides you with a robust control mechanism about new input states.xrSyncActions
call, you must define which action sets are active. An action set can be active for the world in general, but when, for example, the user opens a specific menu UI, the app can switch to the action set for this menu interaction and the user will not be able to teleport by using the same Trigger button.xrGetActionStateBoolean
function, so it relates to a boolean action.XrActionStateGetInfo getInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
getInfo.action = spawnCubeAction;
// Can be used to distinguish between hands, but optional:
getInfo.subactionPath = XR_NULL_PATH;
// Output struct
XrActionStateBoolean spawnCubeState = {XR_TYPE_ACTION_STATE_BOOLEAN};
xrGetActionStateBoolean(session, &getInfo, &spawnCubeState);
Value | Notes |
---|---|
A boolean value representing whether it is active or not, depending on whether this state has passed to xrSyncActions | It is also inactive if the device is powered off, or if the app loses focus to a system menu. The input should be ignored if it’s inactive. |
The current state (in this case, true or false ) | |
A boolean value representing whether the state has changed since the last time xrSyncActions was called | This is useful because it allows detecting edges on a signal in a robust way, so for example, the user can trigger an event only once. |
A value representing the estimated last time the physical button state had changed | This can provide some precision since the last time you called xrSyncActions . |
xrCreateActionSpace
for a particular action.XrPath leftHandPath;
xrStringToPath(instance, "/user/hand/left", &leftHandPath));
// Init:
XrActionSpaceCreateInfo createInfo{XR_TYPE_ACTION_SPACE_CREATE_INFO};
createInfo.action = gripAction;
// poseInActionSpace is a fixed offset to the returned pose
createInfo.poseInActionSpace = poseIdentity;
createInfo.subactionPath = leftHandPath;
xrCreateActionSpace(session, &createInfo, &leftGripActionSpace);
// Frame loop:
// Input struct
XrActionStateGetInfo getInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
getInfo.action = gripAction;
getInfo.subactionPath = leftHandPath;
// Output struct
XrActionStatePose state{XR_TYPE_ACTION_STATE_POSE};
// Call getActionStatePose to see if the pose is active
xrGetActionStatePose(Session, &getInfo, &state));
if (state.isActive){
// Output struct
XrSpaceLocation leftGripLocation{XR_TYPE_SPACE_LOCATION};
xrLocateSpace(leftGripActionSpace, localReferenceSpace, predictedDisplayTime, &leftGripLocation);
// Returns pose + valid + tracked bits (Note: not the same as state.Active)
}
gripAction
that was bound to both left and right hands, the subactionPath
allows for selecting only the left hand. Then, create an action space (leftGripActionSpace
). This must happen before the frame loop. During the frame loop, in each frame the app can locate this action when you call the xrLocateSpace
function on this leftGripActionSpace
. This returns the pose (position + rotation) of the controller relative to the provided reference space.predictedisplayTime
returned from the last call to xrWaitFrame
. For details, see xrWaitFrame in the OpenXR specification.xrGetActionStatePose
function to receive an additional state on the pose action, which offers information about whether the pose action is active. For example, this would return false
if the user opens the dash menu on their Quest device. The underlying app, which was focused, will be enforced to turn all its actions to being inactive. To avoid floating controllers rendered on top of the dash controller, you must check whether the state pose is active before using it.valid
and tracked
:valid
bit represents the validity of the pose. It becomes false
if the app completely loses tracking of the controller or if isActive
is false
. There could be a case where the action itself is active but the valid
bit is false if the user is in the dash menu.tracked
bit records whether the pose is tracked. The controller might be in a valid position, but not currently being tracked. This could be the case when the runtime uses dead reckoning to estimate the controller’s position; the app could use the data but be aware that the tracking accuracy have degraded.// For each ActionSet:
xrCreateActionSet(...)
// For each Action
xrCreateAction(...)
// For each ActionSpace
xrCreateActionSpace(...)
// For each supported and tested device:
xrSuggestInteractionProfileBindings(...)
xrAttachSessionActionSets(...)
while (frameLoop) {
// Only once per frame:
xrSyncActions(...)
// As many times as needed, can be multiple times for the same action
xrGetActionStateTYPE(...)
xrLocateSpace(...)
}