From 302512de4e86b9ceaebedfd56baac05d0fc936d3 Mon Sep 17 00:00:00 2001 From: Tianzi Hou Date: Thu, 1 May 2025 18:34:41 +0100 Subject: [PATCH] Add Xbox Controller Mapper node and update gamepad node documentation --- __init__.py | 2 + docs/gamepad_nodes.md | 61 ++++++++ nodes/gamepad_nodes.py | 335 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 392 insertions(+), 6 deletions(-) diff --git a/__init__.py b/__init__.py index 8dd2424..b6694ef 100644 --- a/__init__.py +++ b/__init__.py @@ -55,6 +55,7 @@ NODE_CLASS_MAPPINGS = { "VrchVideoWebViewerNode": VrchVideoWebViewerNode, "VrchWebSocketServerNode": VrchWebSocketServerNode, "VrchWebViewerNode": VrchWebViewerNode, + "VrchXboxControllerNode": VrchXboxControllerNode, "VrchXYOSCControlNode": VrchXYOSCControlNode, "VrchXYZOSCControlNode": VrchXYZOSCControlNode, } @@ -103,6 +104,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "VrchVideoWebViewerNode": "VIDEO Web Viewer @ vrch.ai", "VrchWebSocketServerNode": "WebSocket Server @ vrch.ai", "VrchWebViewerNode": "Web Viewer @ vrch.ai", + "VrchXboxControllerNode": "Xbox Controller Mapper @ vrch.ai", "VrchXYOSCControlNode": "XY OSC Control @ vrch.ai", "VrchXYZOSCControlNode": "XYZ OSC Control @ vrch.ai", } diff --git a/docs/gamepad_nodes.md b/docs/gamepad_nodes.md index da94151..aa14291 100644 --- a/docs/gamepad_nodes.md +++ b/docs/gamepad_nodes.md @@ -42,3 +42,64 @@ - Buttons 12-15: D-pad (Up/Down/Left/Right) - Axes 0-1: Left analog stick (X, Y) - Axes 2-3: Right analog stick (X, Y) + +### Node: `Xbox Controller Mapper @ vrch.ai` (vrch.ai/control/gamepad) + +1. **Overview:** + - The `Xbox Controller Mapper @ vrch.ai` node provides optimized mapping for Xbox controllers. + - It takes raw gamepad data from the `Gamepad Loader @ vrch.ai` node and maps it to standard Xbox controller inputs. + - This makes it easier to work with Xbox controllers by providing named outputs for each button and control. + +2. **Setup:** + - First add a `Gamepad Loader @ vrch.ai` node to your workflow. + - Then add the `Xbox Controller Mapper @ vrch.ai` node. + - Connect the `RAW_DATA` output from the Gamepad Loader to the `raw_data` input of the Xbox Controller Mapper. + +3. **Inputs:** + - **`raw_data`**: JSON data from the `Gamepad Loader @ vrch.ai` node. + - **`debug`**: Enable to view detailed debug information in the console. Default is `False`. + +4. **Outputs:** + - **Analog Controls:** + - **`LEFT_STICK`**: Array containing X and Y values for the left analog stick `[x, y]` (values range from -1.0 to 1.0). + - **`RIGHT_STICK`**: Array containing X and Y values for the right analog stick `[x, y]` (values range from -1.0 to 1.0). + - **`LEFT_TRIGGER`**: Value for the left trigger (LT) (values range from 0.0 to 1.0). + - **`RIGHT_TRIGGER`**: Value for the right trigger (RT) (values range from 0.0 to 1.0). + + - **Face Buttons:** + - **`A_BUTTON`**: Boolean state of the A button (bottom face button). + - **`B_BUTTON`**: Boolean state of the B button (right face button). + - **`X_BUTTON`**: Boolean state of the X button (left face button). + - **`Y_BUTTON`**: Boolean state of the Y button (top face button). + + - **Shoulder Buttons:** + - **`LB_BUTTON`**: Boolean state of the left bumper (LB). + - **`RB_BUTTON`**: Boolean state of the right bumper (RB). + + - **Center Buttons:** + - **`VIEW_BUTTON`**: Boolean state of the View button (formerly Back). + - **`MENU_BUTTON`**: Boolean state of the Menu button (formerly Start). + - **`XBOX_BUTTON`**: Boolean state of the Xbox logo button. + + - **Stick Presses:** + - **`LEFT_STICK_PRESS`**: Boolean state of the left stick press (L3). + - **`RIGHT_STICK_PRESS`**: Boolean state of the right stick press (R3). + + - **D-Pad:** + - **`DPAD_UP`**: Boolean state of the D-pad up direction. + - **`DPAD_DOWN`**: Boolean state of the D-pad down direction. + - **`DPAD_LEFT`**: Boolean state of the D-pad left direction. + - **`DPAD_RIGHT`**: Boolean state of the D-pad right direction. + + - **Complete Data:** + - **`FULL_MAPPING`**: Complete mapping of all controller inputs as a JSON object. + +5. **Usage Examples:** + - Use `A_BUTTON` to trigger actions in your workflow. + - Use `LEFT_STICK` to control position or movement parameters. + - Use `RIGHT_TRIGGER` to control intensity or pressure-sensitive parameters. + +**Note:** +- This node is specifically optimized for Xbox controllers and follows the standard Xbox controller button layout. +- The button mapping is based on the standard Web Gamepad API implementation for Xbox controllers. +- If your controller has a different layout, you may need to adjust your workflow accordingly. \ No newline at end of file diff --git a/nodes/gamepad_nodes.py b/nodes/gamepad_nodes.py index decbe57..fd59135 100644 --- a/nodes/gamepad_nodes.py +++ b/nodes/gamepad_nodes.py @@ -21,12 +21,12 @@ class VrchGamepadLoaderNode: } RETURN_TYPES = ( - "JSON", # RAW_DATA - "FLOAT", # LEFT_STICK - "FLOAT", # RIGHT_STICK - "BOOL", # BUTTONS_BOOLEAN - "INT", # BUTTONS_INT - "FLOAT", # BUTTONS_FLOAT + "JSON", # RAW_DATA + "FLOAT", # LEFT_STICK + "FLOAT", # RIGHT_STICK + "BOOLEAN", # BUTTONS_BOOLEAN + "INT", # BUTTONS_INT + "FLOAT", # BUTTONS_FLOAT ) RETURN_NAMES = ( @@ -170,3 +170,326 @@ class VrchGamepadLoaderNode: m = hashlib.sha256() m.update(raw_data.encode("utf-8")) return m.hexdigest() + + +class VrchXboxControllerNode: + """ + A specialized node for mapping Xbox controller inputs from raw gamepad data. + This node takes raw gamepad data from VrchGamepadLoaderNode and maps it to + standard Xbox controller buttons and controls for easier use in workflows. + """ + + @classmethod + def INPUT_TYPES(s): + """ + Define the input parameters for the node. + + Returns: + dict: Dictionary containing input parameter specifications + """ + return { + "required": { + "raw_data": ("JSON", {"default": {}}), # Raw gamepad data from VrchGamepadLoaderNode + "debug": ("BOOLEAN", {"default": False}), # Enable debug output + }, + } + + # Define the types of outputs this node will provide + RETURN_TYPES = ( + "JSON", # FULL_MAPPING - Complete mapping as JSON + "FLOAT", # LEFT_STICK - Array with [x, y] values + "FLOAT", # RIGHT_STICK - Array with [x, y] values + "FLOAT", # LEFT_TRIGGER - Value from 0.0 to 1.0 + "FLOAT", # RIGHT_TRIGGER - Value from 0.0 to 1.0 + "BOOLEAN", # A_BUTTON - Bottom face button + "BOOLEAN", # B_BUTTON - Right face button + "BOOLEAN", # X_BUTTON - Left face button + "BOOLEAN", # Y_BUTTON - Top face button + "BOOLEAN", # LB_BUTTON - Left bumper + "BOOLEAN", # RB_BUTTON - Right bumper + "BOOLEAN", # VIEW_BUTTON - Left center button (formerly Back) + "BOOLEAN", # MENU_BUTTON - Right center button (formerly Start) + "BOOLEAN", # LEFT_STICK_PRESS - Left stick click + "BOOLEAN", # RIGHT_STICK_PRESS - Right stick click + "BOOLEAN", # DPAD_UP - D-pad up direction + "BOOLEAN", # DPAD_DOWN - D-pad down direction + "BOOLEAN", # DPAD_LEFT - D-pad left direction + "BOOLEAN", # DPAD_RIGHT - D-pad right direction + "BOOLEAN", # XBOX_BUTTON - Xbox logo button + ) + + # Names for the outputs, matching the types above + RETURN_NAMES = ( + "FULL_MAPPING", + "LEFT_STICK", + "RIGHT_STICK", + "LEFT_TRIGGER", + "RIGHT_TRIGGER", + "A_BUTTON", + "B_BUTTON", + "X_BUTTON", + "Y_BUTTON", + "LB_BUTTON", + "RB_BUTTON", + "VIEW_BUTTON", + "MENU_BUTTON", + "LEFT_STICK_PRESS", + "RIGHT_STICK_PRESS", + "DPAD_UP", + "DPAD_DOWN", + "DPAD_LEFT", + "DPAD_RIGHT", + "XBOX_BUTTON", + ) + + CATEGORY = CATEGORY + FUNCTION = "process_xbox_controller" + + def process_xbox_controller(self, raw_data, debug=False): + """ + Process raw gamepad data and map it to Xbox controller inputs. + + Args: + raw_data (dict): Raw gamepad data from VrchGamepadLoaderNode + debug (bool): Whether to print debug information + + Returns: + tuple: All mapped Xbox controller inputs as specified in RETURN_TYPES + """ + # Initialize default values for all outputs + left_stick = [0.0, 0.0] # [x, y] values for left analog stick + right_stick = [0.0, 0.0] # [x, y] values for right analog stick + left_trigger = 0.0 # Value for left trigger (LT) + right_trigger = 0.0 # Value for right trigger (RT) + + # Initialize button states (all False by default) + a_button = False # A button (bottom face button) + b_button = False # B button (right face button) + x_button = False # X button (left face button) + y_button = False # Y button (top face button) + lb_button = False # Left bumper (LB) + rb_button = False # Right bumper (RB) + view_button = False # View button (formerly Back) + menu_button = False # Menu button (formerly Start) + left_stick_press = False # Left stick press (L3) + right_stick_press = False # Right stick press (R3) + dpad_up = False # D-pad up + dpad_down = False # D-pad down + dpad_left = False # D-pad left + dpad_right = False # D-pad right + xbox_button = False # Xbox logo button + + # Create a complete mapping object for the JSON output + full_mapping = { + "left_stick": left_stick, + "right_stick": right_stick, + "left_trigger": left_trigger, + "right_trigger": right_trigger, + "a_button": a_button, + "b_button": b_button, + "x_button": x_button, + "y_button": y_button, + "lb_button": lb_button, + "rb_button": rb_button, + "view_button": view_button, + "menu_button": menu_button, + "left_stick_press": left_stick_press, + "right_stick_press": right_stick_press, + "dpad_up": dpad_up, + "dpad_down": dpad_down, + "dpad_left": dpad_left, + "dpad_right": dpad_right, + "xbox_button": xbox_button, + } + + # If no data is provided, return default values + if not raw_data: + if debug: + print("[VrchXboxControllerNode] No raw data provided.") + return ( + left_stick, right_stick, left_trigger, right_trigger, + a_button, b_button, x_button, y_button, + lb_button, rb_button, view_button, menu_button, + left_stick_press, right_stick_press, + dpad_up, dpad_down, dpad_left, dpad_right, + xbox_button, full_mapping + ) + + # Parse and process the raw_data + try: + # Extract data from raw_data + if "raw_data" in raw_data and raw_data["raw_data"]: + # Handle cases where raw_data might be a JSON string + if isinstance(raw_data["raw_data"], str): + try: + parsed_data = json.loads(raw_data["raw_data"]) + except json.JSONDecodeError: + # If JSON parsing fails, use raw_data as is + parsed_data = raw_data + else: + # If raw_data is already a dict, use it directly + parsed_data = raw_data + + # Process axes data (analog sticks) + if "axes" in parsed_data and isinstance(parsed_data["axes"], list): + axes = parsed_data["axes"] + # Map axes to stick positions + if len(axes) > 0: + left_stick[0] = float(axes[0]) # Left stick X-axis + if len(axes) > 1: + left_stick[1] = float(axes[1]) # Left stick Y-axis + if len(axes) > 2: + right_stick[0] = float(axes[2]) # Right stick X-axis + if len(axes) > 3: + right_stick[1] = float(axes[3]) # Right stick Y-axis + + # Process buttons data + if "buttons" in parsed_data and isinstance(parsed_data["buttons"], list): + buttons = parsed_data["buttons"] + + # Map buttons to Xbox controller layout + # Standard Xbox controller button mapping + if len(buttons) > 0: + a_button = self._get_button_state(buttons, 0) # A button + if len(buttons) > 1: + b_button = self._get_button_state(buttons, 1) # B button + if len(buttons) > 2: + x_button = self._get_button_state(buttons, 2) # X button + if len(buttons) > 3: + y_button = self._get_button_state(buttons, 3) # Y button + if len(buttons) > 4: + lb_button = self._get_button_state(buttons, 4) # Left bumper + if len(buttons) > 5: + rb_button = self._get_button_state(buttons, 5) # Right bumper + if len(buttons) > 6: + left_trigger = self._get_button_value(buttons, 6) # Left trigger + if len(buttons) > 7: + right_trigger = self._get_button_value(buttons, 7) # Right trigger + if len(buttons) > 8: + view_button = self._get_button_state(buttons, 8) # View button + if len(buttons) > 9: + menu_button = self._get_button_state(buttons, 9) # Menu button + if len(buttons) > 10: + left_stick_press = self._get_button_state(buttons, 10) # Left stick press + if len(buttons) > 11: + right_stick_press = self._get_button_state(buttons, 11) # Right stick press + if len(buttons) > 12: + dpad_up = self._get_button_state(buttons, 12) # D-pad up + if len(buttons) > 13: + dpad_down = self._get_button_state(buttons, 13) # D-pad down + if len(buttons) > 14: + dpad_left = self._get_button_state(buttons, 14) # D-pad left + if len(buttons) > 15: + dpad_right = self._get_button_state(buttons, 15) # D-pad right + if len(buttons) > 16: + xbox_button = self._get_button_state(buttons, 16) # Xbox button + + # Update the full mapping object with current values + full_mapping = { + "left_stick": left_stick, + "right_stick": right_stick, + "left_trigger": left_trigger, + "right_trigger": right_trigger, + "a_button": a_button, + "b_button": b_button, + "x_button": x_button, + "y_button": y_button, + "lb_button": lb_button, + "rb_button": rb_button, + "view_button": view_button, + "menu_button": menu_button, + "left_stick_press": left_stick_press, + "right_stick_press": right_stick_press, + "dpad_up": dpad_up, + "dpad_down": dpad_down, + "dpad_left": dpad_left, + "dpad_right": dpad_right, + "xbox_button": xbox_button, + } + + # Print debug information if enabled + if debug: + print(f"[VrchXboxControllerNode] Xbox controller mapping: {full_mapping}") + + # Return all mapped values + return ( + full_mapping, + left_stick, right_stick, left_trigger, right_trigger, + a_button, b_button, x_button, y_button, + lb_button, rb_button, view_button, menu_button, + left_stick_press, right_stick_press, + dpad_up, dpad_down, dpad_left, dpad_right, + xbox_button, + ) + + except Exception as e: + # Handle any errors during processing + if debug: + print(f"[VrchXboxControllerNode] Error processing Xbox controller data: {str(e)}") + + # Return default values in case of error + return ( + full_mapping, + left_stick, right_stick, left_trigger, right_trigger, + a_button, b_button, x_button, y_button, + lb_button, rb_button, view_button, menu_button, + left_stick_press, right_stick_press, + dpad_up, dpad_down, dpad_left, dpad_right, + xbox_button, + ) + + def _get_button_state(self, buttons, index): + """ + Get the pressed state of a button at the specified index. + + Args: + buttons (list): List of button objects + index (int): Index of the button to check + + Returns: + bool: True if the button is pressed, False otherwise + """ + try: + btn = buttons[index] + if isinstance(btn, dict): + return bool(btn.get("pressed", False)) + elif hasattr(btn, "pressed"): + return bool(btn.pressed) + return False + except (IndexError, TypeError): + return False + + def _get_button_value(self, buttons, index): + """ + Get the analog value of a button at the specified index. + Used for triggers which can have values between 0.0 and 1.0. + + Args: + buttons (list): List of button objects + index (int): Index of the button to check + + Returns: + float: Value of the button (0.0 to 1.0) + """ + try: + btn = buttons[index] + if isinstance(btn, dict): + return float(btn.get("value", 0.0)) + elif hasattr(btn, "value"): + return float(btn.value) + return 0.0 + except (IndexError, TypeError): + return 0.0 + + @classmethod + def IS_CHANGED(cls, **kwargs): + raw_data = kwargs.get("raw_data", "") + debug = kwargs.get("debug", False) + if not raw_data: + if debug: + print("[VrchXboxControllerNode] No raw_data provided to IS_CHANGED.") + return False + + m = hashlib.sha256() + m.update(raw_data.encode("utf-8")) + return m.hexdigest()