Input & Interaction#
Note
Applies to: Spatial Extensions, Kit 109.0.3+, CloudXR 6
This input and interaction reference covers working with XR input devices including controllers, HMDs, and handling user interactions.
Input Device Fundamentals#
XRCore manages all input devices connected to the XR system. Devices include controllers, head-mounted displays (HMDs), and eye trackers.
Device Names (OpenXR Standard)#
Standard device names follow the OpenXR path convention:
/user/head- HMD/Head tracking/user/hand/left- Left controller/user/hand/right- Right controller/user/eye/left- Left eye tracking/user/eye/right- Right eye tracking
Getting Input Devices#
import omni.kit.xr.core
xr_core = omni.kit.xr.core.XRCore.get_singleton()
# Get specific device
left_hand = xr_core.get_input_device("/user/hand/left")
right_hand = xr_core.get_input_device("/user/hand/right")
head = xr_core.get_input_device("/user/head")
# Get all devices
all_devices = xr_core.get_all_input_devices()
for device in all_devices:
name = str(device.get_name())
device_type = str(device.get_type())
print(f"Device: {name}, Type: {device_type}")
# Check if device exists
if xr_core.has_input_device("/user/hand/left"):
print("Left controller is connected")
Device Properties#
device = xr_core.get_input_device("/user/hand/left")
# Get device name (XRToken)
name_token = device.get_name()
name_str = str(name_token) # Convert to string
# Get device type
type_token = device.get_type()
type_str = str(type_token) # e.g., "controller", "hmd"
# Check if device is active
if device.is_active():
print("Device is active")
Working with Poses#
Poses represent the position and orientation of devices in 3D space. Each device can have multiple poses.
Pose Types#
1. Raw Pose#
Unfiltered data directly from XR runtime.
raw_pose = device.get_raw_pose()
Use Case: When you need unfiltered, lowest latency data
2. Pose (Filtered)#
Filtered real-world coordinates.
pose = device.get_pose() # Default pose (usually "aim" for controllers)
Use Case: General-purpose pose with noise filtering
3. Virtual World Pose#
Pose transformed into USD stage coordinates.
virtual_pose = device.get_virtual_world_pose()
Use Case: Positioning objects in the USD scene
Named Poses#
Controllers support multiple named poses:
grip: Physical grip position (where hand holds controller)
aim: Pointing position (typically extends from controller tip)
# Get grip pose (where hand holds)
grip_pose = device.get_pose("grip")
# Get aim pose (for pointing/raycasting)
aim_pose = device.get_pose("aim")
# List all available poses
pose_names = device.get_pose_names()
for pose_name in pose_names:
print(f"Available pose: {str(pose_name)}")
Extracting Position and Orientation#
Poses are returned as Gf.Matrix4d (4x4 transformation matrices).
from pxr import Gf
# Get pose
pose = device.get_pose("aim")
# Extract position (translation)
position = pose.ExtractTranslation() # Returns Gf.Vec3d
print(f"Position: x={position[0]}, y={position[1]}, z={position[2]}")
# Extract orientation (rotation as quaternion)
rotation = pose.ExtractRotation() # Returns Gf.Quatd
print(f"Rotation: {rotation}")
# Extract as Euler angles (requires conversion)
rotation_mat = rotation.GetRotation()
# Or use pose.ExtractRotationMatrix()
# Get forward direction (for raycasting)
forward = pose.TransformDir(Gf.Vec3d(0, 0, -1)) # -Z is forward
Complete Pose Example#
def print_device_pose(device_name):
"""Print detailed pose information for a device."""
device = xr_core.get_input_device(device_name)
if not device or not device.is_active():
print(f"Device {device_name} not active")
return
# Get different pose types
raw_pose = device.get_raw_pose()
filtered_pose = device.get_pose()
virtual_pose = device.get_virtual_world_pose()
# Extract position from virtual pose
position = virtual_pose.ExtractTranslation()
rotation = virtual_pose.ExtractRotation()
print(f"Device: {device_name}")
print(f" Position: ({position[0]:.3f}, {position[1]:.3f}, {position[2]:.3f})")
print(f" Rotation: {rotation}")
# Get forward direction
forward = virtual_pose.TransformDir(Gf.Vec3d(0, 0, -1))
print(f" Forward: ({forward[0]:.3f}, {forward[1]:.3f}, {forward[2]:.3f})")
# Use it
print_device_pose("/user/hand/left")
Event-Based Input#
Instead of polling button states every frame, use event generators for reactive input handling.
Binding Event Generators#
import carb.events
device = xr_core.get_input_device("/user/hand/left")
# Bind event generator for trigger
event_generator = device.bind_event_generator(
"trigger", # Input name
"my_trigger_event", # Event name prefix
["press", "release", "update"], # Event types to generate
{"press": "Fire weapon"} # Optional tooltips (displayed in VR)
)
# Subscribe to press event
message_bus = xr_core.get_message_bus()
def on_trigger_press(event):
print("Trigger pressed!")
trigger_press_sub = message_bus.create_subscription_to_pop_by_type(
carb.events.type_from_string("my_trigger_event.press"),
on_trigger_press
)
Available Event Types#
press- Button pressed (transition from 0 to 1)release- Button released (transition from 1 to 0)update- Sent every frame while pressed, includes state datatouch- Button touchedlift- Button no longer touchedsuspend- Focus taken by another event generatorresume- Focus returnedstate- Sent every frame with complete button state
Extracting Event Data#
Use XRInputDeviceGeneratorEvent wrapper to extract data from events:
from omni.kit.xr.core import XRInputDeviceGeneratorEvent
def on_trigger_update(event):
# Wrap event for easy data access
input_event = XRInputDeviceGeneratorEvent(event)
# Get gesture values
trigger_value = input_event.get_gesture_value("value")
is_touched = input_event.get_gesture_value("touch")
is_clicked = input_event.get_gesture_value("click")
print(f"Trigger value: {trigger_value:.2f}")
# Get device that generated event
device = input_event.get_device()
device_name = str(device.get_name())
# Get input name
input_name = str(input_event.get_input_name())
# Subscribe to update events
message_bus.create_subscription_to_pop_by_type(
carb.events.type_from_string("my_trigger_event.update"),
on_trigger_update
)
Complete Event Example#
class ControllerInputManager:
def __init__(self, xr_core):
self.xr_core = xr_core
self.subscriptions = []
def setup(self):
left_hand = self.xr_core.get_input_device("/user/hand/left")
if left_hand:
# Bind trigger events
left_hand.bind_event_generator(
"trigger", "left_trigger",
["press", "release"],
{"press": "Select", "release": "Deselect"}
)
# Bind menu events
left_hand.bind_event_generator(
"menu", "left_menu",
["press"],
{"press": "Open Menu"}
)
# Bind thumbstick for continuous updates
left_hand.bind_event_generator(
"thumbstick", "left_thumbstick",
["state"] # Continuous updates
)
# Subscribe to events
message_bus = self.xr_core.get_message_bus()
self.subscriptions.append(
message_bus.create_subscription_to_pop_by_type(
carb.events.type_from_string("left_trigger.press"),
self._on_trigger_press
)
)
self.subscriptions.append(
message_bus.create_subscription_to_pop_by_type(
carb.events.type_from_string("left_thumbstick.state"),
self._on_thumbstick_update
)
)
def _on_trigger_press(self, event):
print("Left trigger pressed - perform selection")
# Implement selection logic here
def _on_thumbstick_update(self, event):
input_event = XRInputDeviceGeneratorEvent(event)
x = input_event.get_gesture_value("x")
y = input_event.get_gesture_value("y")
if abs(x) > 0.1 or abs(y) > 0.1:
print(f"Thumbstick: ({x:.2f}, {y:.2f})")
# Implement navigation logic here
def cleanup(self):
self.subscriptions = []
# Usage
manager = ControllerInputManager(xr_core)
manager.setup()
Action Maps#
Action maps provide controller-agnostic input binding, allowing the same code to work across different controller types.
Getting Action Map#
action_map = xr_core.get_action_map()
if action_map:
# Action map is available
# This provides higher-level abstractions
pass
Using Action Maps#
Action maps abstract device-specific inputs into semantic actions:
# Instead of binding to "trigger" specifically, bind to "select" action
# The action map handles mapping "trigger" on controllers,
# mouse click on desktop, etc.
Note: Action maps are configured through XR system settings and profile configurations. Refer to Settings Reference for details.
Raycasting for Selection#
Cast rays from controllers to select objects in the scene.
Basic Raycasting#
from omni.kit.xr.core import XRRay, XRRayQueryResult
def cast_selection_ray():
left_hand = xr_core.get_input_device("/user/hand/left")
pose = left_hand.get_pose("aim")
# Get ray origin and direction
origin = pose.ExtractTranslation()
forward = pose.TransformDir(Gf.Vec3d(0, 0, -1))
# Create ray
ray = XRRay(
origin, # Ray origin
forward, # Ray direction (normalized)
0.0, # Min distance (meters)
1000.0 # Max distance (meters)
)
# Submit raycast query
xr_core.submit_raycast_query(ray, on_raycast_result)
def on_raycast_result(ray, result):
"""Callback when raycast completes (may be several frames later)."""
if result.valid:
hit_path = result.get_target_usd_path()
hit_pos = result.hit_position
hit_normal = result.normal
hit_distance = result.hit_t
print(f"Hit: {hit_path}")
print(f"Position: {hit_pos}")
print(f"Distance: {hit_distance}m")
# Get model root (useful for selection)
model_path = result.get_target_enclosing_model_usd_path()
print(f"Model: {model_path}")
Async Raycasting#
Use async/await for cleaner code:
import asyncio
from omni.kit.xr.core import XRRay
async def async_raycast_example():
left_hand = xr_core.get_input_device("/user/hand/left")
pose = left_hand.get_pose("aim")
origin = pose.ExtractTranslation()
forward = pose.TransformDir(Gf.Vec3d(0, 0, -1))
ray = XRRay(origin, forward, 0.0, 1000.0)
# Await result
result = await xr_core.execute_raycast_query_async(ray)
if result.valid:
print(f"Hit: {result.get_target_usd_path()}")
return result.get_target_usd_path()
else:
print("No hit")
return None
# Run async
asyncio.ensure_future(async_raycast_example())
Multi-Raycasting#
Cast multiple rays simultaneously:
def multi_raycast_example():
# Create multiple rays
rays = []
for device_name in ["/user/hand/left", "/user/hand/right"]:
device = xr_core.get_input_device(device_name)
pose = device.get_pose("aim")
origin = pose.ExtractTranslation()
forward = pose.TransformDir(Gf.Vec3d(0, 0, -1))
rays.append(XRRay(origin, forward, 0.0, 1000.0))
# Submit all at once
xr_core.submit_multi_raycast_query(rays, on_multi_raycast_result)
def on_multi_raycast_result(rays, results):
"""Callback with all results."""
for i, result in enumerate(results):
if result.valid:
print(f"Ray {i} hit: {result.get_target_usd_path()}")
Controlling Pickable Objects#
Limit raycasting to specific objects:
# Make object pickable
xr_core.set_pickable_path("/World/Objects/MyObject", True)
# Make object non-pickable
xr_core.set_pickable_path("/World/Objects/Background", False)
# Reset to default
xr_core.unset_pickable_path("/World/Objects/MyObject")
Input Smoothing#
Enable input smoothing for steadier poses:
import carb.settings
settings = carb.settings.get_settings()
# Enable smoothing
settings.set("/persistent/xr/profile/vr/inputSmoothing/enabled", True)
# Set smoothing factor (0.0 = no smoothing, 1.0 = max smoothing)
settings.set("/persistent/xr/profile/vr/inputSmoothing/factor", 0.3)
Trade-off: Smoothing reduces jitter but adds latency.
Dominant Hand Configuration#
# Set dominant hand for controller layout
settings.set("/xr/persistent/tools/dominantHand", "right") # or "left"
Next Steps#
Event System – Deep dive into event patterns
Scene Integration – Work with USD objects
Custom Tools – Build interactive tools
Settings Reference – Input-related settings
Key Takeaways
Input devices follow OpenXR naming conventions
Three pose types: raw, filtered, virtual
Event generators provide reactive input handling
Raycasting enables object selection
Action maps provide controller-agnostic input