diff --git a/.gitignore b/.gitignore index 6f289cc..8f84fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,31 @@ # Godot .godot/ *.import -android/build/ -build/ *.apk +# Android build (regenerated each export) - keep flavor manifest +android/build/* +!android/build/src/ +android/build/src/* +!android/build/src/standard/ + +# Build output and temp files +build/*.apk +build/*.zip +build/*.txt +build/android_backup/ + +# Local tools (not committed) +Godot4_3/ +Godot4_6_1/ +android-sdk/ +platform-tools/ +export_templates/ +jdk17/ +zulu17*/ +debug.keystore +.claude/ + # OS .DS_Store Thumbs.db diff --git a/Main.gd b/Main.gd index 63394a1..a7ba7d5 100644 --- a/Main.gd +++ b/Main.gd @@ -1,18 +1,32 @@ extends Node3D ## Main entry point for G1 Teleop Quest 3 app. -## Initializes XR session with passthrough, wires body tracker to WebSocket client. +## Two-phase startup: +## CONFIG phase: Dark VR environment with start screen UI panel +## AR phase: Passthrough mixed reality with body tracking + +enum Phase { CONFIG, AR } @onready var body_tracker: Node = $BodyTracker @onready var teleop_client: Node = $TeleopClient @onready var xr_origin: XROrigin3D = $XROrigin3D @onready var xr_camera: XRCamera3D = $XROrigin3D/XRCamera3D @onready var webcam_quad: MeshInstance3D = $XROrigin3D/XRCamera3D/WebcamQuad +@onready var start_screen: Node3D = $XROrigin3D/StartScreen +@onready var left_controller: XRController3D = $XROrigin3D/LeftController +@onready var right_controller: XRController3D = $XROrigin3D/RightController +@onready var vr_pointer: Node3D = $VRUIPointer var xr_interface: XRInterface var xr_is_focused: bool = false +var current_phase: Phase = Phase.CONFIG +var _panel_positioned: bool = false func _ready() -> void: + # Hide webcam quad and start screen until positioned + webcam_quad.visible = false + start_screen.visible = false + # Initialize OpenXR interface xr_interface = XRServer.find_interface("OpenXR") if xr_interface and xr_interface.is_initialized(): @@ -35,12 +49,53 @@ func _ready() -> void: # Enable XR on the viewport get_viewport().use_xr = true - # Wire body tracker output to teleop client - body_tracker.tracking_data_ready.connect(teleop_client._on_tracking_data) + # Connect start screen signals + start_screen.connect_requested.connect(_on_connect_requested) + start_screen.launch_ar_requested.connect(_on_launch_ar_requested) + + # Connect teleop client connection state to start screen + teleop_client.connection_state_changed.connect(_on_connection_state_changed) - # Wire webcam frames from teleop client to webcam display + # Wire webcam frames (can happen anytime we're connected) teleop_client.webcam_frame_received.connect(webcam_quad._on_webcam_frame) + # Setup VR pointer with references to controllers and XR origin + vr_pointer.setup(xr_origin, xr_camera, left_controller, right_controller) + + # Setup body tracker visualization + body_tracker.setup(xr_origin) + + print("[Main] Starting in CONFIG phase") + + +func _process(_delta: float) -> void: + if not _panel_positioned and current_phase == Phase.CONFIG: + # Wait until we have valid head tracking data to position the panel + var hmd := XRServer.get_hmd_transform() + if hmd.origin != Vector3.ZERO and hmd.origin.y > 0.3: + _position_panel_in_front_of_user(hmd) + _panel_positioned = true + + +func _position_panel_in_front_of_user(hmd: Transform3D) -> void: + # Place the panel 1.2m in front of the user at their eye height + var forward := -hmd.basis.z + forward.y = 0 # Project onto horizontal plane + if forward.length() < 0.01: + forward = Vector3(0, 0, -1) + forward = forward.normalized() + + var panel_pos := hmd.origin + forward * 1.2 + panel_pos.y = hmd.origin.y # Same height as eyes + + start_screen.global_position = panel_pos + # Face the panel toward the user + # look_at() points -Z at target, but QuadMesh front face is +Z, so rotate 180 + start_screen.look_at(hmd.origin, Vector3.UP) + start_screen.rotate_y(PI) + start_screen.visible = true + print("[Main] Panel positioned at %s (user at %s)" % [panel_pos, hmd.origin]) + func _on_openxr_session_begun() -> void: print("[Main] OpenXR session begun") @@ -48,8 +103,10 @@ func _on_openxr_session_begun() -> void: func _on_openxr_ready() -> void: - # Enable passthrough (Quest 3 mixed reality) - _enable_passthrough() + # In CONFIG phase, we stay in opaque/dark VR mode + # Passthrough is only enabled when user clicks "Launch AR" + if current_phase == Phase.AR: + _enable_passthrough() func _on_openxr_focused() -> void: @@ -62,19 +119,48 @@ func _on_openxr_stopping() -> void: print("[Main] OpenXR session stopping") +func _on_connect_requested(host: String, port: int) -> void: + print("[Main] Connect requested: %s:%d" % [host, port]) + # If already connected, disconnect first + if teleop_client.is_connected: + teleop_client.disconnect_from_server() + return + + teleop_client.server_host = host + teleop_client.server_port = port + teleop_client.connect_to_server() + + +func _on_connection_state_changed(connected: bool) -> void: + start_screen.set_connected(connected) + + +func _on_launch_ar_requested() -> void: + if current_phase == Phase.AR: + return + + print("[Main] Launching AR mode") + current_phase = Phase.AR + + # Enable passthrough + _enable_passthrough() + + # Wire body tracker to teleop client + body_tracker.tracking_data_ready.connect(teleop_client._on_tracking_data) + + # Show webcam quad, hide start screen + webcam_quad.visible = true + start_screen.hide_screen() + + func _enable_passthrough() -> void: - # Request passthrough blend mode for mixed reality - # Environment blend mode 3 = alpha blend (passthrough) var openxr = xr_interface as OpenXRInterface if openxr: - # Try to start passthrough var modes = openxr.get_supported_environment_blend_modes() print("[Main] Supported blend modes: ", modes) - # XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND = 2 in Godot's enum if XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND in modes: openxr.set_environment_blend_mode(XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND) print("[Main] Passthrough enabled (alpha blend)") - # Clear background to transparent get_viewport().transparent_bg = true RenderingServer.set_default_clear_color(Color(0, 0, 0, 0)) else: diff --git a/Main.gd.uid b/Main.gd.uid new file mode 100644 index 0000000..d1eaec0 --- /dev/null +++ b/Main.gd.uid @@ -0,0 +1 @@ +uid://c4co4sxvbrqxn diff --git a/Main.tscn b/Main.tscn index 4d3e127..451ff7b 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1,10 +1,11 @@ -[gd_scene load_steps=6 format=3 uid="uid://main_scene"] +[gd_scene load_steps=7 format=3 uid="uid://ttx3xgp56hlv"] [ext_resource type="Script" path="res://Main.gd" id="1"] [ext_resource type="Script" path="res://scripts/body_tracker.gd" id="2"] [ext_resource type="Script" path="res://scripts/teleop_client.gd" id="3"] -[ext_resource type="Script" path="res://scripts/webcam_display.gd" id="4"] [ext_resource type="PackedScene" path="res://scenes/webcam_quad.tscn" id="5"] +[ext_resource type="PackedScene" path="res://scenes/start_screen.tscn" id="6"] +[ext_resource type="Script" path="res://scripts/vr_ui_pointer.gd" id="7"] [node name="Main" type="Node3D"] script = ExtResource("1") @@ -13,17 +14,34 @@ script = ExtResource("1") [node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"] +[node name="WebcamQuad" parent="XROrigin3D/XRCamera3D" instance=ExtResource("5")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.3, -1.5) + +[node name="StartScreen" parent="XROrigin3D" instance=ExtResource("6")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -1.5) + [node name="LeftController" type="XRController3D" parent="XROrigin3D"] tracker = &"left_hand" +pose = &"aim_pose" [node name="RightController" type="XRController3D" parent="XROrigin3D"] tracker = &"right_hand" +pose = &"aim_pose" + +[node name="LeftHandTracker" type="XRNode3D" parent="XROrigin3D"] +tracker = &"/user/hand_tracker/left" +show_when_tracked = true -[node name="BodyTracker" type="Node" parent="."] +[node name="RightHandTracker" type="XRNode3D" parent="XROrigin3D"] +tracker = &"/user/hand_tracker/right" +show_when_tracked = true + +[node name="VRUIPointer" type="Node3D" parent="."] +script = ExtResource("7") + +[node name="BodyTracker" type="Node3D" parent="."] script = ExtResource("2") [node name="TeleopClient" type="Node" parent="."] script = ExtResource("3") - -[node name="WebcamQuad" parent="XROrigin3D/XRCamera3D" instance=ExtResource("5")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.3, -1.5) +auto_connect = false diff --git a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-androidxr-debug.aar b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-androidxr-debug.aar new file mode 100644 index 0000000..ab19935 Binary files /dev/null and b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-androidxr-debug.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-khronos-debug.aar b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-khronos-debug.aar index 2b7bece..24e596b 100644 Binary files a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-khronos-debug.aar and b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-khronos-debug.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-lynx-debug.aar b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-lynx-debug.aar index 25ec8bc..2e0ae0a 100644 Binary files a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-lynx-debug.aar and b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-lynx-debug.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-magicleap-debug.aar b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-magicleap-debug.aar index 8af39a1..04c4c58 100644 Binary files a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-magicleap-debug.aar and b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-magicleap-debug.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-meta-debug.aar b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-meta-debug.aar index 23f588a..158fe5d 100644 Binary files a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-meta-debug.aar and b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-meta-debug.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-pico-debug.aar b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-pico-debug.aar index 3ff9ad6..3328df9 100644 Binary files a/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-pico-debug.aar and b/addons/godotopenxrvendors/.bin/android/debug/godotopenxr-pico-debug.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-androidxr-release.aar b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-androidxr-release.aar new file mode 100644 index 0000000..2f13707 Binary files /dev/null and b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-androidxr-release.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-khronos-release.aar b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-khronos-release.aar index 60394ea..fef0ca2 100644 Binary files a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-khronos-release.aar and b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-khronos-release.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-lynx-release.aar b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-lynx-release.aar index 6445235..c05c85c 100644 Binary files a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-lynx-release.aar and b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-lynx-release.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-magicleap-release.aar b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-magicleap-release.aar index e5451ea..53e010c 100644 Binary files a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-magicleap-release.aar and b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-magicleap-release.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-meta-release.aar b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-meta-release.aar index bac336d..4b85c73 100644 Binary files a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-meta-release.aar and b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-meta-release.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-pico-release.aar b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-pico-release.aar index 15d2757..8cf1920 100644 Binary files a/addons/godotopenxrvendors/.bin/android/release/godotopenxr-pico-release.aar and b/addons/godotopenxrvendors/.bin/android/release/godotopenxr-pico-release.aar differ diff --git a/addons/godotopenxrvendors/.bin/android/template_debug/arm64/libgodotopenxrvendors.so b/addons/godotopenxrvendors/.bin/android/template_debug/arm64/libgodotopenxrvendors.so index 0097984..dccbffd 100644 Binary files a/addons/godotopenxrvendors/.bin/android/template_debug/arm64/libgodotopenxrvendors.so and b/addons/godotopenxrvendors/.bin/android/template_debug/arm64/libgodotopenxrvendors.so differ diff --git a/addons/godotopenxrvendors/.bin/android/template_debug/x86_64/libgodotopenxrvendors.so b/addons/godotopenxrvendors/.bin/android/template_debug/x86_64/libgodotopenxrvendors.so new file mode 100644 index 0000000..7dbb733 Binary files /dev/null and b/addons/godotopenxrvendors/.bin/android/template_debug/x86_64/libgodotopenxrvendors.so differ diff --git a/addons/godotopenxrvendors/.bin/android/template_release/arm64/libgodotopenxrvendors.so b/addons/godotopenxrvendors/.bin/android/template_release/arm64/libgodotopenxrvendors.so index cc0a2c4..003373e 100644 Binary files a/addons/godotopenxrvendors/.bin/android/template_release/arm64/libgodotopenxrvendors.so and b/addons/godotopenxrvendors/.bin/android/template_release/arm64/libgodotopenxrvendors.so differ diff --git a/addons/godotopenxrvendors/.bin/android/template_release/x86_64/libgodotopenxrvendors.so b/addons/godotopenxrvendors/.bin/android/template_release/x86_64/libgodotopenxrvendors.so new file mode 100644 index 0000000..7b4c3bd Binary files /dev/null and b/addons/godotopenxrvendors/.bin/android/template_release/x86_64/libgodotopenxrvendors.so differ diff --git a/addons/godotopenxrvendors/.bin/linux/template_debug/arm64/libgodotopenxrvendors.so b/addons/godotopenxrvendors/.bin/linux/template_debug/arm64/libgodotopenxrvendors.so new file mode 100644 index 0000000..d1b61ba Binary files /dev/null and b/addons/godotopenxrvendors/.bin/linux/template_debug/arm64/libgodotopenxrvendors.so differ diff --git a/addons/godotopenxrvendors/.bin/linux/template_debug/x86_64/libgodotopenxrvendors.so b/addons/godotopenxrvendors/.bin/linux/template_debug/x86_64/libgodotopenxrvendors.so index 36fe04a..a770502 100644 Binary files a/addons/godotopenxrvendors/.bin/linux/template_debug/x86_64/libgodotopenxrvendors.so and b/addons/godotopenxrvendors/.bin/linux/template_debug/x86_64/libgodotopenxrvendors.so differ diff --git a/addons/godotopenxrvendors/.bin/linux/template_release/arm64/libgodotopenxrvendors.so b/addons/godotopenxrvendors/.bin/linux/template_release/arm64/libgodotopenxrvendors.so new file mode 100644 index 0000000..e11e6f1 Binary files /dev/null and b/addons/godotopenxrvendors/.bin/linux/template_release/arm64/libgodotopenxrvendors.so differ diff --git a/addons/godotopenxrvendors/.bin/linux/template_release/x86_64/libgodotopenxrvendors.so b/addons/godotopenxrvendors/.bin/linux/template_release/x86_64/libgodotopenxrvendors.so index 7fd0d3f..8bc0d8e 100644 Binary files a/addons/godotopenxrvendors/.bin/linux/template_release/x86_64/libgodotopenxrvendors.so and b/addons/godotopenxrvendors/.bin/linux/template_release/x86_64/libgodotopenxrvendors.so differ diff --git a/addons/godotopenxrvendors/.bin/macos/template_debug/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos b/addons/godotopenxrvendors/.bin/macos/template_debug/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos index 9899300..bc4d9e8 100644 Binary files a/addons/godotopenxrvendors/.bin/macos/template_debug/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos and b/addons/godotopenxrvendors/.bin/macos/template_debug/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos differ diff --git a/addons/godotopenxrvendors/.bin/macos/template_release/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos b/addons/godotopenxrvendors/.bin/macos/template_release/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos index 2e1fde7..ebbc941 100644 Binary files a/addons/godotopenxrvendors/.bin/macos/template_release/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos and b/addons/godotopenxrvendors/.bin/macos/template_release/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos differ diff --git a/addons/godotopenxrvendors/.bin/windows/template_debug/x86_64/libgodotopenxrvendors.dll b/addons/godotopenxrvendors/.bin/windows/template_debug/x86_64/libgodotopenxrvendors.dll index 986af51..bdf3201 100644 Binary files a/addons/godotopenxrvendors/.bin/windows/template_debug/x86_64/libgodotopenxrvendors.dll and b/addons/godotopenxrvendors/.bin/windows/template_debug/x86_64/libgodotopenxrvendors.dll differ diff --git a/addons/godotopenxrvendors/.bin/windows/template_release/x86_64/libgodotopenxrvendors.dll b/addons/godotopenxrvendors/.bin/windows/template_release/x86_64/libgodotopenxrvendors.dll index 42cf549..379fb3b 100644 Binary files a/addons/godotopenxrvendors/.bin/windows/template_release/x86_64/libgodotopenxrvendors.dll and b/addons/godotopenxrvendors/.bin/windows/template_release/x86_64/libgodotopenxrvendors.dll differ diff --git a/addons/godotopenxrvendors/GodotOpenXRVendors_CHANGES.md b/addons/godotopenxrvendors/GodotOpenXRVendors_CHANGES.md index 93863bf..ab72611 100644 --- a/addons/godotopenxrvendors/GodotOpenXRVendors_CHANGES.md +++ b/addons/godotopenxrvendors/GodotOpenXRVendors_CHANGES.md @@ -1,5 +1,105 @@ # Change history for the Godot OpenXR loaders asset +## 4.3.0 + +- Fix issue with instant splash screen +- Explicitly assign hand tracking mesh skeleton property to parent Skeleton3D +- Add Linux arm64 builds +- Minor tweaks to `XR_FB_space_warp` delta pose calculation and the sample +- Add support and manual page for `XR_ANDROID_scene_meshing` extension +- Add manual page for Android XR Passthrough Camera State +- Add documentation about Meta Boundary Visibility extension +- Add manual page for vendor performance metrics +- Add support and manual page for `XR_ANDROID_light_estimation` extension +- Add support and documentation for `XR_ANDROID_eye_tracking` extension +- Add support for `XR_META_colocation_discovery` extension +- Add the necessary permissions to enable EXT spatial entities on Meta headsets +- Fix errors in Meta Environment Depth documentation +- Add manual page for Body and Face Tracking +- Add manual page for Meta Color Space +- Only add shader globals when the environment depth extensions are enabled +- Add support and documentation for `XR_ANDROID_face_tracking` extension +- Fix issues with Meta hand-tracking extensions on Android XR +- Add support and manual page for `XR_ANDROID_depth_texture` extension +- Add XR Project Setup Wizard + +## 4.2.2 + +- Fix crashes when using the plugin on Godot 4.4.x +- Fix background opacity on Android XR when using Compatibility renderer + +## 4.2.1 + +- Fix generation of 16kb page compatible binaries +- Fix logic to detect when running on Android XR devices +- Fix the logic used to transfer data between hybrid apps' modes +- Fix generation of the release asset + +## 4.2.0 + +- Add support for the **Android XR** OpenXR vendor +- Implement `XR_ANDROID_passthrough_camera_state` extension +- Implement `XR_ANDROID_performance_metrics` extension +- Document the settings required to make an app have a passthrough loading screen +- Add experimental support for the `XR_ML_marker_understanding` extension +- Fix the missing `com.oculus.permission.USE_SCENE` permission for the environment depth feature +- Add the required permission for enabling spatial entities on Pico devices +- Fix missing sample links for the passthrough and composition layer documentation pages +- Only add the global shader uniforms for Meta environment depth when it's enabled +- Fix missing shader uniform and Godot version in `project.godot` for meta-scene-sample +- Use `gdformat` to ensure consistent GDScript coding standards in CI +- Fix `OpenXRFbHandTrackingMesh` changes after startup and detecting when it's not supported +- Generate native debug symbols for Android +- Fix the manifest configuration for boundaryless apps: set `android.hardware.vr.headtracking` to `required="true"` +- Fix running the meta-scene-sample on Godot 4.4 +- Add `quest3s` to the list of supported devices +- Fix errors reported by `XrApiLayer_core_validation` +- Allow using bilinear filtering in reprojection and provide example for smoothing in a shader +- Implement `XR_META_performance_metrics` extension +- Add documentation page about Meta Environment Depth +- Add method for getting the Meta environment depth map on the CPU side +- Clear `XRServer#remove_tracker` errors when closing the Godot editor +- Fix Meta passthrough when rendering with "Separate" thread model +- Fix `XR_META_environment_depth` when rendering with "Separate" thread model +- Fix issues with invalid data returned by `xrGetHandMeshFB` +- Implement `XR_META_headset_id` extension +- Implement `XR_META_simultaneous_hands_and_controllers` extension + +## 4.1.1 + +- Update the export plugin version to match the maven central release + +## 4.1.0 + +- Implement `XR_META_boundary_visibility` extension +- Add HorizonOS camera permissions when the Android CAMERA permission is enabled +- Implement `XR_FB_space_warp` extension (only with Godot 4.5+) +- Implement `XR_META_environment_depth` extension (only with Godot 4.5+) +- Implement `XR_FB_color_space` extension +- Update OpenXR to 1.1.49 release +- Implement `XR_META_body_tracking_full_body`, `XR_META_body_tracking_fidelity` and `XR_META_body_tracking_callibration` +- Clean-up editor plugins and class registration +- `OpenXRFbSceneManager`: Clarify how to check if scene capture is possible + +## 4.0.0 + +- Support making hybrid apps for Meta headsets +- Add support for `XR_FB_android_surface_swapchain_create` +- Implement `XR_META_recommended_layer_resolution` +- Remove CMake from the build process +- Implement instant splash screen for Meta headsets +- Avoid casting errors when building with `precision=double` +- Add missing Pico store manifest +- Add support for `XR_FB_composition_layer_image_layout` +- Update demo and samples for Godot 4.4 +- Switch Meta and Lynx to the Khronos loader +- Fix `OpenXRFbSpatialEntityStorageExtensionWrapper` typos +- Add support for `XR_FB_composition_layer_depth_test` +- Use project settings to avoid enabling unneeded OpenXR extensions +- Passthrough extensions should override real alpha blend mode, if enabled +- Update the main manifest with the latest from the Khronos OpenXR loader AAR +- Improve hand tracking related code in demo project + ## 3.1.2 - Fix passthrough sample color map display bug - Fix the issue preventing overridden vendor options from being updated @@ -43,6 +143,10 @@ - Add manifest entries to Pico and switch Pico to using the Khronos Loader - Add Meta Passthrough tutorial doc +## 2.0.4 +- Fix misc crash when reloading project on Godot 4.3 +- Fix issue with only the first permission being requested + ## 2.0.3 - Migrate the export scripts from gdscript to C++ via gdextension - Manually request eye tracking permission if it's included in the app manifest diff --git a/addons/godotopenxrvendors/androidxr/LICENSE b/addons/godotopenxrvendors/androidxr/LICENSE new file mode 100644 index 0000000..6b0b127 --- /dev/null +++ b/addons/godotopenxrvendors/androidxr/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/addons/godotopenxrvendors/meta/LICENSE-LOADER b/addons/godotopenxrvendors/meta/LICENSE-LOADER new file mode 100644 index 0000000..6b0b127 --- /dev/null +++ b/addons/godotopenxrvendors/meta/LICENSE-LOADER @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/addons/godotopenxrvendors/meta/LICENSE.txt b/addons/godotopenxrvendors/meta/LICENSE-SDK similarity index 95% rename from addons/godotopenxrvendors/meta/LICENSE.txt rename to addons/godotopenxrvendors/meta/LICENSE-SDK index 6516fa3..c2c569e 100644 --- a/addons/godotopenxrvendors/meta/LICENSE.txt +++ b/addons/godotopenxrvendors/meta/LICENSE-SDK @@ -1,3 +1,3 @@ Copyright © Facebook Technologies, LLC and its affiliates. All rights reserved. -Your use of this SDK or tool is subject to the Oculus SDK License Agreement, available at https://developer.oculus.com/licenses/oculussdk/ \ No newline at end of file +Your use of this SDK or tool is subject to the Oculus SDK License Agreement, available at https://developer.oculus.com/licenses/oculussdk/ diff --git a/addons/godotopenxrvendors/plugin.gdextension b/addons/godotopenxrvendors/plugin.gdextension index c215b51..921978e 100644 --- a/addons/godotopenxrvendors/plugin.gdextension +++ b/addons/godotopenxrvendors/plugin.gdextension @@ -1,7 +1,7 @@ [configuration] entry_symbol = "plugin_library_init" -compatibility_minimum = "4.3" +compatibility_minimum = "4.4" android_aar_plugin = true [libraries] @@ -16,3 +16,5 @@ windows.debug.x86_64 = "res://addons/godotopenxrvendors/.bin/windows/template_de windows.release.x86_64 = "res://addons/godotopenxrvendors/.bin/windows/template_release/x86_64/libgodotopenxrvendors.dll" linux.debug.x86_64 = "res://addons/godotopenxrvendors/.bin/linux/template_debug/x86_64/libgodotopenxrvendors.so" linux.release.x86_64 = "res://addons/godotopenxrvendors/.bin/linux/template_release/x86_64/libgodotopenxrvendors.so" +linux.debug.arm64 = "res://addons/godotopenxrvendors/.bin/linux/template_debug/arm64/libgodotopenxrvendors.so" +linux.release.arm64 = "res://addons/godotopenxrvendors/.bin/linux/template_release/arm64/libgodotopenxrvendors.so" diff --git a/addons/godotopenxrvendors/plugin.gdextension.uid b/addons/godotopenxrvendors/plugin.gdextension.uid new file mode 100644 index 0000000..bf67758 --- /dev/null +++ b/addons/godotopenxrvendors/plugin.gdextension.uid @@ -0,0 +1 @@ +uid://u32t56vkch04 diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 8d39e82..1ac4a47 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -1,35 +1,38 @@ - - - - + + + + - - - + + + + + + + + + + - - - + + + - + - - diff --git a/android/build/src/standard/AndroidManifest.xml b/android/build/src/standard/AndroidManifest.xml new file mode 100644 index 0000000..ef40e78 --- /dev/null +++ b/android/build/src/standard/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build_461.bat b/build/build_461.bat new file mode 100644 index 0000000..38f696f --- /dev/null +++ b/build/build_461.bat @@ -0,0 +1,2 @@ +@echo off +"C:\git\g1-teleop\Godot4_6_1\Godot_v4.6.1-stable_win64_console.exe" --headless --quit --path "C:\git\g1-teleop" --export-debug "Quest 3" "C:\git\g1-teleop\build\g1-teleop.apk" diff --git a/build/install_461.py b/build/install_461.py new file mode 100644 index 0000000..e341081 --- /dev/null +++ b/build/install_461.py @@ -0,0 +1,57 @@ +"""Install Godot 4.6.1 export templates and OpenXR vendors plugin v4.3.0.""" +import zipfile +import os +import shutil + +# 1. Install OpenXR vendors plugin v4.3.0 +plugin_zip = r"C:\git\g1-teleop\build\godotopenxrvendors_v4.3.0.zip" +addons_dir = r"C:\git\g1-teleop\addons" +old_plugin = os.path.join(addons_dir, "godotopenxrvendors") + +if os.path.exists(plugin_zip): + print(f"[1/2] Installing OpenXR vendors plugin v4.3.0...") + # Remove old plugin + if os.path.exists(old_plugin): + print(f" Removing old plugin at {old_plugin}") + shutil.rmtree(old_plugin) + # Extract new plugin - the zip contains addons/godotopenxrvendors/ + with zipfile.ZipFile(plugin_zip, 'r') as z: + # List top-level to understand structure + names = z.namelist() + print(f" Zip contains {len(names)} files") + if names[0].startswith("addons/"): + # Extract directly to project root + z.extractall(r"C:\git\g1-teleop") + print(" Extracted to project root (addons/ prefix)") + else: + # Extract to addons dir + z.extractall(addons_dir) + print(f" Extracted to {addons_dir}") + print(" Done!") +else: + print(f"[1/2] Plugin zip not found: {plugin_zip}") + +# 2. Install export templates +tpz_file = r"C:\Users\John\AppData\Roaming\Godot\export_templates\godot461_templates.tpz" +templates_dir = r"C:\Users\John\AppData\Roaming\Godot\export_templates\4.6.1.stable" + +if os.path.exists(tpz_file): + print(f"[2/2] Installing export templates...") + os.makedirs(templates_dir, exist_ok=True) + with zipfile.ZipFile(tpz_file, 'r') as z: + names = z.namelist() + print(f" TPZ contains {len(names)} files") + # TPZ typically has templates/ prefix + for name in names: + if name.startswith("templates/") and not name.endswith("/"): + # Strip the templates/ prefix + dest_name = name[len("templates/"):] + dest_path = os.path.join(templates_dir, dest_name) + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + with z.open(name) as src, open(dest_path, 'wb') as dst: + shutil.copyfileobj(src, dst) + print(f" Extracted to {templates_dir}") + print(" Done!") +else: + print(f"[2/2] Export templates not yet downloaded: {tpz_file}") + print(" Run this script again after download completes.") diff --git a/build/setup_android_template.py b/build/setup_android_template.py new file mode 100644 index 0000000..e4a1126 --- /dev/null +++ b/build/setup_android_template.py @@ -0,0 +1,34 @@ +"""Manually install android build template from export templates.""" +import zipfile +import os + +source_zip = r"C:\Users\John\AppData\Roaming\Godot\export_templates\4.6.1.stable\android_source.zip" +dest_dir = r"C:\git\g1-teleop\android\build" + +if not os.path.exists(source_zip): + print(f"ERROR: android_source.zip not found at {source_zip}") + exit(1) + +os.makedirs(dest_dir, exist_ok=True) + +with zipfile.ZipFile(source_zip, 'r') as z: + names = z.namelist() + print(f"android_source.zip contains {len(names)} files") + # Show top-level structure + top = set() + for n in names: + parts = n.split('/') + if parts[0]: + top.add(parts[0]) + print(f"Top-level entries: {sorted(top)}") + + z.extractall(dest_dir) + print(f"Extracted to {dest_dir}") + +# Update build version +version_file = r"C:\git\g1-teleop\android\.build_version" +with open(version_file, 'w') as f: + f.write("4.6.1.stable\n") +print(f"Updated {version_file} to 4.6.1.stable") + +print("\nDone!") diff --git a/build/setup_vendor_plugin.py b/build/setup_vendor_plugin.py new file mode 100644 index 0000000..5272aad --- /dev/null +++ b/build/setup_vendor_plugin.py @@ -0,0 +1,45 @@ +"""Copy OpenXR vendors plugin files to the gradle build directory.""" +import shutil +import os + +project = r"C:\git\g1-teleop" +build_dir = os.path.join(project, "android", "build") +addons = os.path.join(project, "addons", "godotopenxrvendors") + +# 1. Copy Meta AAR to libs/debug and libs/plugins/debug +for d in ["libs/debug", "libs/plugins/debug"]: + dest = os.path.join(build_dir, d) + os.makedirs(dest, exist_ok=True) + src = os.path.join(addons, ".bin", "android", "debug", "godotopenxr-meta-debug.aar") + if os.path.exists(src): + shutil.copy2(src, dest) + print(f"Copied meta debug AAR to {dest}") + +for d in ["libs/release", "libs/plugins/release"]: + dest = os.path.join(build_dir, d) + os.makedirs(dest, exist_ok=True) + src = os.path.join(addons, ".bin", "android", "release", "godotopenxr-meta-release.aar") + if os.path.exists(src): + shutil.copy2(src, dest) + print(f"Copied meta release AAR to {dest}") + +# 2. Copy libgodotopenxrvendors.so to libs/debug/arm64-v8a +for build_type in ["debug", "release"]: + template = "template_debug" if build_type == "debug" else "template_release" + so_dir = os.path.join(build_dir, "libs", build_type, "arm64-v8a") + os.makedirs(so_dir, exist_ok=True) + src = os.path.join(addons, ".bin", "android", template, "arm64", "libgodotopenxrvendors.so") + if os.path.exists(src): + shutil.copy2(src, so_dir) + print(f"Copied libgodotopenxrvendors.so to {so_dir}") + +# 3. Copy the gdextension file to assets so it's included in the APK +assets_addons = os.path.join(build_dir, "src", "main", "assets", "addons", "godotopenxrvendors") +os.makedirs(assets_addons, exist_ok=True) +for f in ["plugin.gdextension", "plugin.gdextension.uid"]: + src = os.path.join(addons, f) + if os.path.exists(src): + shutil.copy2(src, assets_addons) + print(f"Copied {f} to assets") + +print("\nDone! Now rebuild the APK.") diff --git a/export_presets.cfg b/export_presets.cfg index bd56fb3..a263598 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -39,9 +39,17 @@ launcher_icons/adaptive_foreground_432x432="" launcher_icons/adaptive_background_432x432="" graphics/opengl_debug=false xr_features/xr_mode=1 -xr_features/hand_tracking=2 -xr_features/hand_tracking_frequency=1 -xr_features/passthrough=2 +xr_features/enable_meta_plugin=true +meta_xr_features/hand_tracking=2 +meta_xr_features/hand_tracking_frequency=1 +meta_xr_features/passthrough=2 +meta_xr_features/body_tracking=2 +meta_xr_features/quest_1_support=false +meta_xr_features/quest_2_support=true +meta_xr_features/quest_3_support=true +meta_xr_features/quest_pro_support=true +meta_xr_features/use_experimental_features=false +meta_xr_features/boundary_mode=0 permissions/internet=true permissions/access_network_state=true permissions/access_wifi_state=true diff --git a/openxr_action_map.tres b/openxr_action_map.tres index 6c7a5c5..67bd0a3 100644 --- a/openxr_action_map.tres +++ b/openxr_action_map.tres @@ -1,3 +1,307 @@ [gd_resource type="OpenXRActionMap" format=3 uid="uid://openxr_actions"] +[sub_resource type="OpenXRAction" id="OpenXRAction_aim"] +resource_name = "aim_pose" +localized_name = "Aim Pose" +action_type = 3 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_grip"] +resource_name = "grip_pose" +localized_name = "Grip Pose" +action_type = 3 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_palm"] +resource_name = "palm_pose" +localized_name = "Palm Pose" +action_type = 3 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_trigger"] +resource_name = "trigger" +localized_name = "Trigger" +action_type = 0 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_trigger_click"] +resource_name = "trigger_click" +localized_name = "Trigger Click" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_trigger_touch"] +resource_name = "trigger_touch" +localized_name = "Trigger Touch" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_grip_val"] +resource_name = "grip" +localized_name = "Grip" +action_type = 0 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_grip_click"] +resource_name = "grip_click" +localized_name = "Grip Click" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_menu"] +resource_name = "menu_button" +localized_name = "Menu Button" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_select"] +resource_name = "select_button" +localized_name = "Select Button" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_ax"] +resource_name = "ax_button" +localized_name = "A/X Button" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_by"] +resource_name = "by_button" +localized_name = "B/Y Button" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_ax_touch"] +resource_name = "ax_touch" +localized_name = "A/X Touch" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_by_touch"] +resource_name = "by_touch" +localized_name = "B/Y Touch" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_thumbstick"] +resource_name = "thumbstick" +localized_name = "Thumbstick" +action_type = 2 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_thumbstick_click"] +resource_name = "thumbstick_click" +localized_name = "Thumbstick Click" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_thumbstick_touch"] +resource_name = "thumbstick_touch" +localized_name = "Thumbstick Touch" +action_type = 1 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_haptic"] +resource_name = "haptic" +localized_name = "Haptic" +action_type = 4 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRActionSet" id="OpenXRActionSet_godot"] +resource_name = "godot" +localized_name = "Godot Action Set" +priority = 0 +actions = [SubResource("OpenXRAction_aim"), SubResource("OpenXRAction_grip"), SubResource("OpenXRAction_palm"), SubResource("OpenXRAction_trigger"), SubResource("OpenXRAction_trigger_click"), SubResource("OpenXRAction_trigger_touch"), SubResource("OpenXRAction_grip_val"), SubResource("OpenXRAction_grip_click"), SubResource("OpenXRAction_menu"), SubResource("OpenXRAction_select"), SubResource("OpenXRAction_ax"), SubResource("OpenXRAction_by"), SubResource("OpenXRAction_ax_touch"), SubResource("OpenXRAction_by_touch"), SubResource("OpenXRAction_thumbstick"), SubResource("OpenXRAction_thumbstick_click"), SubResource("OpenXRAction_thumbstick_touch"), SubResource("OpenXRAction_haptic")] + +[sub_resource type="OpenXRIPBinding" id="simple_aim"] +action = SubResource("OpenXRAction_aim") +paths = PackedStringArray("/user/hand/left/input/aim/pose", "/user/hand/right/input/aim/pose") + +[sub_resource type="OpenXRIPBinding" id="simple_grip"] +action = SubResource("OpenXRAction_grip") +paths = PackedStringArray("/user/hand/left/input/grip/pose", "/user/hand/right/input/grip/pose") + +[sub_resource type="OpenXRIPBinding" id="simple_select"] +action = SubResource("OpenXRAction_select") +paths = PackedStringArray("/user/hand/left/input/select/click", "/user/hand/right/input/select/click") + +[sub_resource type="OpenXRIPBinding" id="simple_menu"] +action = SubResource("OpenXRAction_menu") +paths = PackedStringArray("/user/hand/left/input/menu/click", "/user/hand/right/input/menu/click") + +[sub_resource type="OpenXRIPBinding" id="simple_haptic"] +action = SubResource("OpenXRAction_haptic") +paths = PackedStringArray("/user/hand/left/output/haptic", "/user/hand/right/output/haptic") + +[sub_resource type="OpenXRInteractionProfile" id="profile_simple"] +interaction_profile_path = "/interaction_profiles/khr/simple_controller" +bindings = [SubResource("simple_aim"), SubResource("simple_grip"), SubResource("simple_select"), SubResource("simple_menu"), SubResource("simple_haptic")] + +[sub_resource type="OpenXRIPBinding" id="touch_aim"] +action = SubResource("OpenXRAction_aim") +paths = PackedStringArray("/user/hand/left/input/aim/pose", "/user/hand/right/input/aim/pose") + +[sub_resource type="OpenXRIPBinding" id="touch_grip"] +action = SubResource("OpenXRAction_grip") +paths = PackedStringArray("/user/hand/left/input/grip/pose", "/user/hand/right/input/grip/pose") + +[sub_resource type="OpenXRIPBinding" id="touch_trigger"] +action = SubResource("OpenXRAction_trigger") +paths = PackedStringArray("/user/hand/left/input/trigger/value", "/user/hand/right/input/trigger/value") + +[sub_resource type="OpenXRIPBinding" id="touch_trigger_click"] +action = SubResource("OpenXRAction_trigger_click") +paths = PackedStringArray("/user/hand/left/input/trigger/value", "/user/hand/right/input/trigger/value") + +[sub_resource type="OpenXRIPBinding" id="touch_trigger_touch"] +action = SubResource("OpenXRAction_trigger_touch") +paths = PackedStringArray("/user/hand/left/input/trigger/touch", "/user/hand/right/input/trigger/touch") + +[sub_resource type="OpenXRIPBinding" id="touch_grip_val"] +action = SubResource("OpenXRAction_grip_val") +paths = PackedStringArray("/user/hand/left/input/squeeze/value", "/user/hand/right/input/squeeze/value") + +[sub_resource type="OpenXRIPBinding" id="touch_grip_click"] +action = SubResource("OpenXRAction_grip_click") +paths = PackedStringArray("/user/hand/left/input/squeeze/value", "/user/hand/right/input/squeeze/value") + +[sub_resource type="OpenXRIPBinding" id="touch_menu"] +action = SubResource("OpenXRAction_menu") +paths = PackedStringArray("/user/hand/left/input/menu/click") + +[sub_resource type="OpenXRIPBinding" id="touch_ax"] +action = SubResource("OpenXRAction_ax") +paths = PackedStringArray("/user/hand/left/input/x/click", "/user/hand/right/input/a/click") + +[sub_resource type="OpenXRIPBinding" id="touch_by"] +action = SubResource("OpenXRAction_by") +paths = PackedStringArray("/user/hand/left/input/y/click", "/user/hand/right/input/b/click") + +[sub_resource type="OpenXRIPBinding" id="touch_ax_touch"] +action = SubResource("OpenXRAction_ax_touch") +paths = PackedStringArray("/user/hand/left/input/x/touch", "/user/hand/right/input/a/touch") + +[sub_resource type="OpenXRIPBinding" id="touch_by_touch"] +action = SubResource("OpenXRAction_by_touch") +paths = PackedStringArray("/user/hand/left/input/y/touch", "/user/hand/right/input/b/touch") + +[sub_resource type="OpenXRIPBinding" id="touch_thumbstick"] +action = SubResource("OpenXRAction_thumbstick") +paths = PackedStringArray("/user/hand/left/input/thumbstick", "/user/hand/right/input/thumbstick") + +[sub_resource type="OpenXRIPBinding" id="touch_thumbstick_click"] +action = SubResource("OpenXRAction_thumbstick_click") +paths = PackedStringArray("/user/hand/left/input/thumbstick/click", "/user/hand/right/input/thumbstick/click") + +[sub_resource type="OpenXRIPBinding" id="touch_thumbstick_touch"] +action = SubResource("OpenXRAction_thumbstick_touch") +paths = PackedStringArray("/user/hand/left/input/thumbstick/touch", "/user/hand/right/input/thumbstick/touch") + +[sub_resource type="OpenXRIPBinding" id="touch_haptic"] +action = SubResource("OpenXRAction_haptic") +paths = PackedStringArray("/user/hand/left/output/haptic", "/user/hand/right/output/haptic") + +[sub_resource type="OpenXRInteractionProfile" id="profile_touch"] +interaction_profile_path = "/interaction_profiles/oculus/touch_controller" +bindings = [SubResource("touch_aim"), SubResource("touch_grip"), SubResource("touch_trigger"), SubResource("touch_trigger_click"), SubResource("touch_trigger_touch"), SubResource("touch_grip_val"), SubResource("touch_grip_click"), SubResource("touch_menu"), SubResource("touch_ax"), SubResource("touch_by"), SubResource("touch_ax_touch"), SubResource("touch_by_touch"), SubResource("touch_thumbstick"), SubResource("touch_thumbstick_click"), SubResource("touch_thumbstick_touch"), SubResource("touch_haptic")] + +[sub_resource type="OpenXRIPBinding" id="touch_plus_aim"] +action = SubResource("OpenXRAction_aim") +paths = PackedStringArray("/user/hand/left/input/aim/pose", "/user/hand/right/input/aim/pose") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_grip"] +action = SubResource("OpenXRAction_grip") +paths = PackedStringArray("/user/hand/left/input/grip/pose", "/user/hand/right/input/grip/pose") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_trigger"] +action = SubResource("OpenXRAction_trigger") +paths = PackedStringArray("/user/hand/left/input/trigger/value", "/user/hand/right/input/trigger/value") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_trigger_click"] +action = SubResource("OpenXRAction_trigger_click") +paths = PackedStringArray("/user/hand/left/input/trigger/value", "/user/hand/right/input/trigger/value") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_trigger_touch"] +action = SubResource("OpenXRAction_trigger_touch") +paths = PackedStringArray("/user/hand/left/input/trigger/touch", "/user/hand/right/input/trigger/touch") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_grip_val"] +action = SubResource("OpenXRAction_grip_val") +paths = PackedStringArray("/user/hand/left/input/squeeze/value", "/user/hand/right/input/squeeze/value") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_grip_click"] +action = SubResource("OpenXRAction_grip_click") +paths = PackedStringArray("/user/hand/left/input/squeeze/value", "/user/hand/right/input/squeeze/value") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_menu"] +action = SubResource("OpenXRAction_menu") +paths = PackedStringArray("/user/hand/left/input/menu/click") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_ax"] +action = SubResource("OpenXRAction_ax") +paths = PackedStringArray("/user/hand/left/input/x/click", "/user/hand/right/input/a/click") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_by"] +action = SubResource("OpenXRAction_by") +paths = PackedStringArray("/user/hand/left/input/y/click", "/user/hand/right/input/b/click") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_ax_touch"] +action = SubResource("OpenXRAction_ax_touch") +paths = PackedStringArray("/user/hand/left/input/x/touch", "/user/hand/right/input/a/touch") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_by_touch"] +action = SubResource("OpenXRAction_by_touch") +paths = PackedStringArray("/user/hand/left/input/y/touch", "/user/hand/right/input/b/touch") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_thumbstick"] +action = SubResource("OpenXRAction_thumbstick") +paths = PackedStringArray("/user/hand/left/input/thumbstick", "/user/hand/right/input/thumbstick") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_thumbstick_click"] +action = SubResource("OpenXRAction_thumbstick_click") +paths = PackedStringArray("/user/hand/left/input/thumbstick/click", "/user/hand/right/input/thumbstick/click") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_thumbstick_touch"] +action = SubResource("OpenXRAction_thumbstick_touch") +paths = PackedStringArray("/user/hand/left/input/thumbstick/touch", "/user/hand/right/input/thumbstick/touch") + +[sub_resource type="OpenXRIPBinding" id="touch_plus_haptic"] +action = SubResource("OpenXRAction_haptic") +paths = PackedStringArray("/user/hand/left/output/haptic", "/user/hand/right/output/haptic") + +[sub_resource type="OpenXRInteractionProfile" id="profile_touch_plus"] +interaction_profile_path = "/interaction_profiles/meta/touch_controller_plus" +bindings = [SubResource("touch_plus_aim"), SubResource("touch_plus_grip"), SubResource("touch_plus_trigger"), SubResource("touch_plus_trigger_click"), SubResource("touch_plus_trigger_touch"), SubResource("touch_plus_grip_val"), SubResource("touch_plus_grip_click"), SubResource("touch_plus_menu"), SubResource("touch_plus_ax"), SubResource("touch_plus_by"), SubResource("touch_plus_ax_touch"), SubResource("touch_plus_by_touch"), SubResource("touch_plus_thumbstick"), SubResource("touch_plus_thumbstick_click"), SubResource("touch_plus_thumbstick_touch"), SubResource("touch_plus_haptic")] + +[sub_resource type="OpenXRIPBinding" id="hand_aim"] +action = SubResource("OpenXRAction_aim") +paths = PackedStringArray("/user/hand/left/input/aim/pose", "/user/hand/right/input/aim/pose") + +[sub_resource type="OpenXRIPBinding" id="hand_grip"] +action = SubResource("OpenXRAction_grip") +paths = PackedStringArray("/user/hand/left/input/grip/pose", "/user/hand/right/input/grip/pose") + +[sub_resource type="OpenXRIPBinding" id="hand_trigger"] +action = SubResource("OpenXRAction_trigger") +paths = PackedStringArray("/user/hand/left/input/pinch_ext/value", "/user/hand/right/input/pinch_ext/value") + +[sub_resource type="OpenXRIPBinding" id="hand_trigger_click"] +action = SubResource("OpenXRAction_trigger_click") +paths = PackedStringArray("/user/hand/left/input/pinch_ext/ready_ext", "/user/hand/right/input/pinch_ext/ready_ext") + +[sub_resource type="OpenXRIPBinding" id="hand_grip_val"] +action = SubResource("OpenXRAction_grip_val") +paths = PackedStringArray("/user/hand/left/input/grasp_ext/value", "/user/hand/right/input/grasp_ext/value") + +[sub_resource type="OpenXRIPBinding" id="hand_grip_click"] +action = SubResource("OpenXRAction_grip_click") +paths = PackedStringArray("/user/hand/left/input/grasp_ext/ready_ext", "/user/hand/right/input/grasp_ext/ready_ext") + +[sub_resource type="OpenXRInteractionProfile" id="profile_hand_interaction"] +interaction_profile_path = "/interaction_profiles/ext/hand_interaction_ext" +bindings = [SubResource("hand_aim"), SubResource("hand_grip"), SubResource("hand_trigger"), SubResource("hand_trigger_click"), SubResource("hand_grip_val"), SubResource("hand_grip_click")] + [resource] +action_sets = [SubResource("OpenXRActionSet_godot")] +interaction_profiles = [SubResource("profile_simple"), SubResource("profile_touch"), SubResource("profile_touch_plus"), SubResource("profile_hand_interaction")] diff --git a/project.godot b/project.godot index fd7d9b5..f7b27f0 100644 --- a/project.godot +++ b/project.godot @@ -3,9 +3,8 @@ ; since the parameters that go here are not all obvious. ; ; Format: -; [section] is a section -; param=value ; assigned a value -; param=value ; assigned a value +; [section] ; section goes between [] +; param=value ; assign values to parameters config_version=5 @@ -14,11 +13,9 @@ config_version=5 config/name="G1 Teleop" config/description="Native Quest 3 teleoperation app for Unitree G1 humanoid robot with body tracking" run/main_scene="res://Main.tscn" -config/features=PackedStringArray("4.3", "Mobile") +config/features=PackedStringArray("4.6", "Mobile") config/icon="res://icon.svg" -[autoload] - [display] window/size/viewport_width=1920 @@ -27,18 +24,20 @@ window/size/viewport_height=1920 [rendering] renderer/rendering_method="mobile" -renderer/rendering_method.mobile="mobile" textures/vram_compression/import_etc2_astc=true [xr] openxr/enabled=true -openxr/default_action_map="res://openxr_action_map.tres" openxr/form_factor=0 openxr/view_configuration=1 openxr/reference_space=1 openxr/environment_blend_mode=0 openxr/foveation_level=3 openxr/foveation_dynamic=true +openxr/extensions/hand_tracking=true +openxr/extensions/hand_tracking_unobstructed_data_source=true +openxr/extensions/hand_tracking_controller_data_source=true +openxr/extensions/hand_interaction_profile=true +openxr/extensions/meta/body_tracking=true shaders/enabled=true - diff --git a/scenes/start_screen.tscn b/scenes/start_screen.tscn new file mode 100644 index 0000000..8ea5c03 --- /dev/null +++ b/scenes/start_screen.tscn @@ -0,0 +1,112 @@ +[gd_scene load_steps=6 format=3 uid="uid://start_screen_01"] + +[ext_resource type="Script" path="res://scripts/start_screen.gd" id="1"] + +[sub_resource type="QuadMesh" id="QuadMesh_1"] +size = Vector2(0.8, 0.6) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"] +shading_mode = 0 +albedo_color = Color(0.15, 0.15, 0.2, 1) + +[sub_resource type="Theme" id="Theme_1"] +default_font_size = 28 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1"] +bg_color = Color(0.12, 0.12, 0.18, 1) +corner_radius_top_left = 12 +corner_radius_top_right = 12 +corner_radius_bottom_right = 12 +corner_radius_bottom_left = 12 + +[node name="StartScreen" type="Node3D"] +script = ExtResource("1") + +[node name="UIMesh" type="MeshInstance3D" parent="."] +mesh = SubResource("QuadMesh_1") +material_override = SubResource("StandardMaterial3D_1") + +[node name="SubViewport" type="SubViewport" parent="UIMesh"] +transparent_bg = false +handle_input_locally = true +size = Vector2i(1024, 768) +render_target_update_mode = 3 + +[node name="PanelContainer" type="PanelContainer" parent="UIMesh/SubViewport"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +theme = SubResource("Theme_1") +theme_override_styles/panel = SubResource("StyleBoxFlat_1") + +[node name="MarginContainer" type="MarginContainer" parent="UIMesh/SubViewport/PanelContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 60 +theme_override_constants/margin_top = 40 +theme_override_constants/margin_right = 60 +theme_override_constants/margin_bottom = 40 + +[node name="VBox" type="VBoxContainer" parent="UIMesh/SubViewport/PanelContainer/MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="Title" type="Label" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox"] +layout_mode = 2 +theme_override_font_sizes/font_size = 48 +text = "G1 Teleop" +horizontal_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox"] +layout_mode = 2 + +[node name="ServerRow" type="HBoxContainer" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Label" type="Label" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/ServerRow"] +layout_mode = 2 +custom_minimum_size = Vector2(180, 0) +text = "Server:" + +[node name="HostInput" type="LineEdit" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/ServerRow"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "10.0.0.64" +placeholder_text = "IP address or hostname" +virtual_keyboard_enabled = true + +[node name="PortRow" type="HBoxContainer" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Label" type="Label" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/PortRow"] +layout_mode = 2 +custom_minimum_size = Vector2(180, 0) +text = "Port:" + +[node name="PortInput" type="LineEdit" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/PortRow"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "8765" +placeholder_text = "Port number" +virtual_keyboard_enabled = true + +[node name="ConnectButton" type="Button" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 60) +text = "Connect to Server" + +[node name="StatusLabel" type="Label" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1) +text = "Not connected" +horizontal_alignment = 1 + +[node name="HSeparator2" type="HSeparator" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox"] +layout_mode = 2 + +[node name="LaunchARButton" type="Button" parent="UIMesh/SubViewport/PanelContainer/MarginContainer/VBox"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 70) +theme_override_font_sizes/font_size = 36 +text = "Launch AR" diff --git a/scripts/body_tracker.gd b/scripts/body_tracker.gd index 240eeb9..a702e3d 100644 --- a/scripts/body_tracker.gd +++ b/scripts/body_tracker.gd @@ -1,6 +1,7 @@ -extends Node +extends Node3D ## Reads XR_FB_body_tracking joints each frame via Godot's XRBodyTracker. ## Computes chest-relative wrist positions and emits tracking data. +## Visualizes all body joints as colored spheres. ## ## Meta body tracking provides 70 joints. We use: ## CHEST (5) - torso orientation (solves body rotation problem) @@ -17,6 +18,7 @@ signal tracking_data_ready(data: Dictionary) ## Joint indices from XR_FB_body_tracking ## Reference: Meta OpenXR body tracking extension +const JOINT_ROOT := 0 const JOINT_HIPS := 1 const JOINT_SPINE_LOWER := 2 const JOINT_SPINE_MIDDLE := 3 @@ -46,6 +48,14 @@ const HAND_JOINT_COUNT := 25 ## Total body joint count const BODY_JOINT_COUNT := 70 +## Joint visualization colors +const COLOR_BODY := Color(1.0, 0.7, 0.2, 1.0) # Orange - spine/torso +const COLOR_HEAD := Color(1.0, 1.0, 1.0, 1.0) # White +const COLOR_LEFT_ARM := Color(0.3, 0.5, 1.0, 1.0) # Blue +const COLOR_RIGHT_ARM := Color(0.3, 1.0, 0.5, 1.0) # Green +const COLOR_LEFT_HAND := Color(0.5, 0.7, 1.0, 1.0) # Light blue +const COLOR_RIGHT_HAND := Color(0.5, 1.0, 0.7, 1.0) # Light green + ## Tracking state var body_tracker_name: StringName = &"/user/body_tracker" var is_tracking: bool = false @@ -58,11 +68,59 @@ var frames_since_last_send: int = 0 @export var debug_log: bool = false var _log_counter: int = 0 +## Joint visualization +var _xr_origin: XROrigin3D +var _joint_spheres: Array = [] # Array of MeshInstance3D, indexed by joint + + +func setup(xr_origin: XROrigin3D) -> void: + _xr_origin = xr_origin + _create_joint_spheres() + func _ready() -> void: print("[BodyTracker] Initialized, waiting for body tracking data...") +func _create_joint_spheres() -> void: + var body_mesh := SphereMesh.new() + body_mesh.radius = 0.025 + body_mesh.height = 0.05 + + # Only create spheres for body joints (0-17), skip hand joints (18-69) + # Hand joints are already visualized by vr_ui_pointer.gd + _joint_spheres.resize(BODY_JOINT_COUNT) + for i in range(BODY_JOINT_COUNT): + if i >= JOINT_LEFT_HAND_START: + _joint_spheres[i] = null + continue + var s := MeshInstance3D.new() + s.mesh = body_mesh + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.albedo_color = _get_joint_color(i) + s.material_override = mat + s.visible = false + add_child(s) + _joint_spheres[i] = s + + +func _get_joint_color(joint_idx: int) -> Color: + if joint_idx == JOINT_HEAD: + return COLOR_HEAD + elif joint_idx <= JOINT_NECK: + return COLOR_BODY + elif joint_idx >= JOINT_LEFT_SHOULDER and joint_idx <= JOINT_LEFT_HAND_WRIST: + return COLOR_LEFT_ARM + elif joint_idx >= JOINT_RIGHT_SHOULDER and joint_idx <= JOINT_RIGHT_HAND_WRIST: + return COLOR_RIGHT_ARM + elif joint_idx >= JOINT_LEFT_HAND_START and joint_idx < JOINT_RIGHT_HAND_START: + return COLOR_LEFT_HAND + elif joint_idx >= JOINT_RIGHT_HAND_START: + return COLOR_RIGHT_HAND + return COLOR_BODY + + func _process(_delta: float) -> void: frames_since_last_send += 1 if frames_since_last_send < send_every_n_frames: @@ -74,18 +132,23 @@ func _process(_delta: float) -> void: if is_tracking: print("[BodyTracker] Lost body tracking") is_tracking = false + _hide_all_spheres() return if not tracker.get_has_tracking_data(): if is_tracking: print("[BodyTracker] Body tracking data unavailable") is_tracking = false + _hide_all_spheres() return if not is_tracking: print("[BodyTracker] Body tracking active!") is_tracking = true + # Update joint sphere positions + _update_joint_spheres(tracker) + # Read key joint poses var chest_xform := tracker.get_joint_transform(JOINT_CHEST) var head_xform := tracker.get_joint_transform(JOINT_HEAD) @@ -180,6 +243,27 @@ func _get_hand_positions(tracker: XRBodyTracker, start_idx: int, wrist_xform: Tr return positions +func _update_joint_spheres(tracker: XRBodyTracker) -> void: + if _joint_spheres.is_empty() or _xr_origin == null: + return + var origin_xform := _xr_origin.global_transform + for i in range(BODY_JOINT_COUNT): + if _joint_spheres[i] == null: + continue + var xform := tracker.get_joint_transform(i) + if xform.origin == Vector3.ZERO: + _joint_spheres[i].visible = false + continue + _joint_spheres[i].global_position = origin_xform * xform.origin + _joint_spheres[i].visible = true + + +func _hide_all_spheres() -> void: + for s in _joint_spheres: + if s != null: + s.visible = false + + ## Get 25 hand joint rotations relative to wrist, as flat array (225 floats) ## Each joint: 9 floats (3x3 rotation matrix, column-major) func _get_hand_rotations(tracker: XRBodyTracker, start_idx: int, wrist_xform: Transform3D) -> Array: diff --git a/scripts/body_tracker.gd.uid b/scripts/body_tracker.gd.uid new file mode 100644 index 0000000..c29e48c --- /dev/null +++ b/scripts/body_tracker.gd.uid @@ -0,0 +1 @@ +uid://1xnuuli2itfk diff --git a/scripts/start_screen.gd b/scripts/start_screen.gd new file mode 100644 index 0000000..46444e5 --- /dev/null +++ b/scripts/start_screen.gd @@ -0,0 +1,74 @@ +extends Node3D +## VR start screen UI panel. +## Renders a 2D UI in a SubViewport on a QuadMesh in VR space. +## Allows user to enter server URL/port, connect, and launch AR mode. + +signal connect_requested(host: String, port: int) +signal launch_ar_requested() + +@onready var ui_mesh: MeshInstance3D = $UIMesh +@onready var viewport: SubViewport = $UIMesh/SubViewport +@onready var host_input: LineEdit = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/ServerRow/HostInput +@onready var port_input: LineEdit = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/PortRow/PortInput +@onready var connect_button: Button = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/ConnectButton +@onready var status_label: Label = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/StatusLabel +@onready var launch_ar_button: Button = $UIMesh/SubViewport/PanelContainer/MarginContainer/VBox/LaunchARButton + +var _is_connected: bool = false + + +func _ready() -> void: + add_to_group("start_screen") + connect_button.pressed.connect(_on_connect_pressed) + launch_ar_button.pressed.connect(_on_launch_ar_pressed) + + # Set up the mesh material to display the SubViewport + var material := StandardMaterial3D.new() + material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + material.albedo_texture = viewport.get_texture() + material.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED + ui_mesh.material_override = material + + print("[StartScreen] Ready") + + +func _on_connect_pressed() -> void: + var host := host_input.text.strip_edges() + if host.is_empty(): + update_status("Please enter a server address") + return + + var port := int(port_input.text.strip_edges()) + if port <= 0 or port > 65535: + update_status("Invalid port number") + return + + update_status("Connecting to %s:%d..." % [host, port]) + connect_requested.emit(host, port) + + +func _on_launch_ar_pressed() -> void: + launch_ar_requested.emit() + + +func update_status(text: String) -> void: + status_label.text = text + + +func set_connected(connected: bool) -> void: + _is_connected = connected + if connected: + update_status("Connected!") + connect_button.text = "Disconnect" + else: + if connect_button.text == "Disconnect": + update_status("Disconnected") + connect_button.text = "Connect to Server" + + +func show_screen() -> void: + visible = true + + +func hide_screen() -> void: + visible = false diff --git a/scripts/start_screen.gd.uid b/scripts/start_screen.gd.uid new file mode 100644 index 0000000..23d1a44 --- /dev/null +++ b/scripts/start_screen.gd.uid @@ -0,0 +1 @@ +uid://cd50pdphkb1do diff --git a/scripts/teleop_client.gd.uid b/scripts/teleop_client.gd.uid new file mode 100644 index 0000000..c24230a --- /dev/null +++ b/scripts/teleop_client.gd.uid @@ -0,0 +1 @@ +uid://cejwgl45w03x7 diff --git a/scripts/vr_ui_pointer.gd b/scripts/vr_ui_pointer.gd new file mode 100644 index 0000000..b1b7ffc --- /dev/null +++ b/scripts/vr_ui_pointer.gd @@ -0,0 +1,469 @@ +extends Node3D +## VR UI pointer with hand tracking visualization, controller ray-pointing, +## and finger poke interaction. Renders hand joints as spheres and shows +## controller placeholder meshes. + +@export var ray_length: float = 5.0 +@export var poke_threshold: float = 0.03 +@export var hover_distance: float = 0.15 +@export var laser_color: Color = Color(0.3, 0.6, 1.0, 0.6) + +var _xr_origin: XROrigin3D +var _camera: XRCamera3D +var _left_ctrl: XRController3D +var _right_ctrl: XRController3D + +var _laser_right: MeshInstance3D +var _laser_left: MeshInstance3D +var _reticle: MeshInstance3D + +# Hand joint visualization: _hand_joints[hand_idx][joint_idx] = MeshInstance3D +var _hand_joints: Array = [[], []] +var _ctrl_mesh_left: MeshInstance3D +var _ctrl_mesh_right: MeshInstance3D + +var _current_viewport: SubViewport = null +var _last_viewport_pos: Vector2 = Vector2.ZERO +var _is_pressing: bool = false +var _active_method: String = "" +var _log_timer: float = 0.0 + +const JOINT_COUNT := 26 +# Fingertip joint indices for XRHandTracker (thumb=5, index=10, middle=15, ring=20, pinky=25) +const TIP_JOINTS := [5, 10, 15, 20, 25] +const HAND_COLORS := [Color(0.3, 0.6, 1.0, 1.0), Color(0.3, 1.0, 0.6, 1.0)] + +# XRBodyTracker fallback: hand joint start indices and count +const BODY_LEFT_HAND_START := 18 +const BODY_RIGHT_HAND_START := 43 +const BODY_HAND_JOINT_COUNT := 25 +# Fingertip indices within 25-joint body tracker hand block +const BODY_TIP_JOINTS := [4, 9, 14, 19, 24] +# Index finger tip within body tracker hand block (for poke interaction) +const BODY_INDEX_TIP := 9 + + +func setup(xr_origin: XROrigin3D, camera: XRCamera3D, left_ctrl: XRController3D, right_ctrl: XRController3D) -> void: + _xr_origin = xr_origin + _camera = camera + _left_ctrl = left_ctrl + _right_ctrl = right_ctrl + + _left_ctrl.button_pressed.connect(_on_left_button_pressed) + _left_ctrl.button_released.connect(_on_left_button_released) + _right_ctrl.button_pressed.connect(_on_right_button_pressed) + _right_ctrl.button_released.connect(_on_right_button_released) + + # Create lasers (start hidden until controllers are active) + _laser_right = _create_laser() + _laser_right.visible = false + _right_ctrl.add_child(_laser_right) + _laser_left = _create_laser() + _laser_left.visible = false + _left_ctrl.add_child(_laser_left) + + # Reticle dot where pointer intersects UI + _reticle = MeshInstance3D.new() + var sphere := SphereMesh.new() + sphere.radius = 0.01 + sphere.height = 0.02 + _reticle.mesh = sphere + var rmat := StandardMaterial3D.new() + rmat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + rmat.albedo_color = Color(1, 1, 1, 0.9) + rmat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + rmat.no_depth_test = true + rmat.render_priority = 10 + _reticle.material_override = rmat + _reticle.visible = false + add_child(_reticle) + + # Hand joint spheres + _create_hand_visuals() + + # Controller placeholder meshes + _ctrl_mesh_left = _create_controller_mesh(_left_ctrl) + _ctrl_mesh_right = _create_controller_mesh(_right_ctrl) + + print("[VRUIPointer] Setup complete") + + +func _create_laser() -> MeshInstance3D: + var laser := MeshInstance3D.new() + var cyl := CylinderMesh.new() + cyl.top_radius = 0.002 + cyl.bottom_radius = 0.002 + cyl.height = ray_length + laser.mesh = cyl + laser.position = Vector3(0, 0, -ray_length / 2.0) + laser.rotation.x = deg_to_rad(90) + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.albedo_color = laser_color + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + laser.material_override = mat + return laser + + +func _create_hand_visuals() -> void: + # Shared meshes - larger for visibility in VR + var joint_mesh := SphereMesh.new() + joint_mesh.radius = 0.01 + joint_mesh.height = 0.02 + var tip_mesh := SphereMesh.new() + tip_mesh.radius = 0.013 + tip_mesh.height = 0.026 + + for hand_idx in [0, 1]: + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.albedo_color = HAND_COLORS[hand_idx] + + for joint_idx in range(JOINT_COUNT): + var s := MeshInstance3D.new() + s.mesh = tip_mesh if joint_idx in TIP_JOINTS else joint_mesh + s.material_override = mat + s.visible = false + add_child(s) + _hand_joints[hand_idx].append(s) + + +func _create_controller_mesh(ctrl: XRController3D) -> MeshInstance3D: + var mesh_inst := MeshInstance3D.new() + var box := BoxMesh.new() + box.size = Vector3(0.05, 0.03, 0.12) + mesh_inst.mesh = box + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.albedo_color = Color(0.5, 0.5, 0.6, 0.7) + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mesh_inst.material_override = mat + mesh_inst.visible = false + ctrl.add_child(mesh_inst) + return mesh_inst + + +func _process(delta: float) -> void: + if _xr_origin == null: + return + + # Debug logging every 5 seconds + _log_timer += delta + if _log_timer > 5.0: + _log_timer = 0.0 + var l := _left_ctrl.get_is_active() if _left_ctrl else false + var r := _right_ctrl.get_is_active() if _right_ctrl else false + var panels := get_tree().get_nodes_in_group("start_screen").size() + print("[VRUIPointer] ctrl=L:%s/R:%s panels=%d" % [l, r, panels]) + # List all XR trackers + var trackers := XRServer.get_trackers(0xFF) + var tracker_names := [] + for key in trackers: + tracker_names.append(str(key)) + print("[VRUIPointer] all_trackers=%s" % [", ".join(tracker_names)]) + # Hand tracker diagnostics + for hand_idx in [0, 1]: + var side := "L" if hand_idx == 0 else "R" + var tn := &"/user/hand_tracker/left" if hand_idx == 0 else &"/user/hand_tracker/right" + var tr = XRServer.get_tracker(tn) + if tr == null: + print("[VRUIPointer] hand_%s: tracker=NULL" % side) + else: + var ht = tr as XRHandTracker + if ht: + var src = ht.hand_tracking_source + print("[VRUIPointer] hand_%s: has_data=%s source=%d type=%s" % [side, ht.has_tracking_data, src, ht.get_class()]) + # Sample joint data even if has_data=false + var wrist := ht.get_hand_joint_transform(0) + var index_tip := ht.get_hand_joint_transform(10) + print("[VRUIPointer] hand_%s: wrist=%s idx_tip=%s" % [side, wrist.origin, index_tip.origin]) + else: + print("[VRUIPointer] hand_%s: tracker exists but NOT XRHandTracker, class=%s" % [side, tr.get_class()]) + # Body tracker diagnostics (fallback for hand data) + var bt = XRServer.get_tracker(&"/user/body_tracker") as XRBodyTracker + if bt == null: + print("[VRUIPointer] body_tracker=NULL") + else: + var bt_data := bt.get_has_tracking_data() + var vis_l := 0 + var vis_r := 0 + for s in _hand_joints[0]: + if s.visible: + vis_l += 1 + for s in _hand_joints[1]: + if s.visible: + vis_r += 1 + if bt_data: + var lw := bt.get_joint_transform(BODY_LEFT_HAND_START) + var rw := bt.get_joint_transform(BODY_RIGHT_HAND_START) + print("[VRUIPointer] body: has_data=true L_wrist=%s R_wrist=%s vis=L:%d/R:%d" % [lw.origin, rw.origin, vis_l, vis_r]) + else: + print("[VRUIPointer] body: has_data=false vis=L:%d/R:%d" % [vis_l, vis_r]) + + # Update visuals every frame + _update_hand_visuals() + + # Controller visibility and laser defaults + var l_active := _left_ctrl.get_is_active() + var r_active := _right_ctrl.get_is_active() + _ctrl_mesh_left.visible = l_active + _ctrl_mesh_right.visible = r_active + _laser_left.visible = l_active + _laser_right.visible = r_active + if l_active: + _reset_laser_length(_laser_left) + if r_active: + _reset_laser_length(_laser_right) + + # UI interaction + var panels := _get_ui_panels() + if panels.is_empty(): + _reticle.visible = false + return + + var hit := false + + # 1. Hand tracking poke (priority over controller ray) + for hand_idx in [0, 1]: + var tip_pos := _get_fingertip_world_position(hand_idx) + if tip_pos == Vector3.ZERO: + continue + for panel in panels: + var result := _check_point_against_panel(tip_pos, panel[0], panel[1]) + if result.size() > 0 and abs(result[0]) < hover_distance: + hit = true + _reticle.global_position = result[2] + _reticle.visible = true + _current_viewport = panel[1] + _last_viewport_pos = result[1] + _send_mouse_motion(panel[1], result[1]) + + var method := "poke_%d" % hand_idx + if result[0] < poke_threshold and not _is_pressing: + _is_pressing = true + _active_method = method + _send_mouse_button(panel[1], result[1], true) + elif result[0] >= poke_threshold and _is_pressing and _active_method == method: + _is_pressing = false + _send_mouse_button(panel[1], result[1], false) + break + if hit: + break + + # 2. Controller ray pointing + if not hit: + for ctrl_data in [[_right_ctrl, _laser_right, "ray_right"], [_left_ctrl, _laser_left, "ray_left"]]: + var ctrl: XRController3D = ctrl_data[0] + var laser: MeshInstance3D = ctrl_data[1] + var method: String = ctrl_data[2] + if not ctrl.get_is_active(): + continue + var ray_origin := ctrl.global_position + var ray_dir := -ctrl.global_transform.basis.z.normalized() + + for panel in panels: + var result := _ray_intersect_panel(ray_origin, ray_dir, panel[0], panel[1]) + if result.size() > 0: + hit = true + _reticle.global_position = result[2] + _reticle.visible = true + _current_viewport = panel[1] + _last_viewport_pos = result[1] + _send_mouse_motion(panel[1], result[1]) + var dist := ray_origin.distance_to(result[2]) + _update_laser_length(laser, dist) + break + if hit: + break + + if not hit: + _reticle.visible = false + if _is_pressing: + _is_pressing = false + if _current_viewport: + _send_mouse_button(_current_viewport, _last_viewport_pos, false) + _current_viewport = null + + +func _has_hand_tracking(hand: int) -> bool: + var tracker_name := &"/user/hand_tracker/left" if hand == 0 else &"/user/hand_tracker/right" + var tracker = XRServer.get_tracker(tracker_name) as XRHandTracker + return tracker != null and tracker.has_tracking_data + + +func _update_hand_visuals() -> void: + for hand_idx in [0, 1]: + # Try XRHandTracker first (works with controllers) + var tracker_name := &"/user/hand_tracker/left" if hand_idx == 0 else &"/user/hand_tracker/right" + var tracker = XRServer.get_tracker(tracker_name) + var hand_tracker: XRHandTracker = tracker as XRHandTracker if tracker else null + + if hand_tracker and hand_tracker.has_tracking_data: + for joint_idx in range(JOINT_COUNT): + var xform := hand_tracker.get_hand_joint_transform(joint_idx) + if xform.origin == Vector3.ZERO: + _hand_joints[hand_idx][joint_idx].visible = false + continue + var world_pos := _xr_origin.global_transform * xform.origin + _hand_joints[hand_idx][joint_idx].global_position = world_pos + _hand_joints[hand_idx][joint_idx].visible = true + continue + + # Fallback: XRBodyTracker (Meta FB body tracking, works without controllers) + var body_tracker = XRServer.get_tracker(&"/user/body_tracker") as XRBodyTracker + if body_tracker and body_tracker.get_has_tracking_data(): + var start_idx := BODY_LEFT_HAND_START if hand_idx == 0 else BODY_RIGHT_HAND_START + for i in range(BODY_HAND_JOINT_COUNT): + var xform := body_tracker.get_joint_transform(start_idx + i) + if xform.origin == Vector3.ZERO: + if i < JOINT_COUNT: + _hand_joints[hand_idx][i].visible = false + continue + var world_pos := _xr_origin.global_transform * xform.origin + if i < JOINT_COUNT: + _hand_joints[hand_idx][i].global_position = world_pos + _hand_joints[hand_idx][i].visible = true + # Hide the 26th sphere (body tracker has 25 joints, not 26) + _hand_joints[hand_idx][25].visible = false + continue + + # No tracking data from either source + for s in _hand_joints[hand_idx]: + s.visible = false + + +func _update_laser_length(laser: MeshInstance3D, length: float) -> void: + var cyl := laser.mesh as CylinderMesh + if cyl: + cyl.height = length + laser.position = Vector3(0, 0, -length / 2.0) + + +func _reset_laser_length(laser: MeshInstance3D) -> void: + var cyl := laser.mesh as CylinderMesh + if cyl: + cyl.height = ray_length + laser.position = Vector3(0, 0, -ray_length / 2.0) + + +func _get_fingertip_world_position(hand: int) -> Vector3: + # Try XRHandTracker first + var tracker_name := &"/user/hand_tracker/left" if hand == 0 else &"/user/hand_tracker/right" + var tracker = XRServer.get_tracker(tracker_name) + if tracker: + var hand_tracker := tracker as XRHandTracker + if hand_tracker and hand_tracker.has_tracking_data: + var tip_xform := hand_tracker.get_hand_joint_transform(XRHandTracker.HAND_JOINT_INDEX_FINGER_TIP) + if tip_xform.origin != Vector3.ZERO: + return _xr_origin.global_transform * tip_xform.origin + + # Fallback: XRBodyTracker + var body_tracker = XRServer.get_tracker(&"/user/body_tracker") as XRBodyTracker + if body_tracker and body_tracker.get_has_tracking_data(): + var start_idx := BODY_LEFT_HAND_START if hand == 0 else BODY_RIGHT_HAND_START + var tip_xform := body_tracker.get_joint_transform(start_idx + BODY_INDEX_TIP) + if tip_xform.origin != Vector3.ZERO: + return _xr_origin.global_transform * tip_xform.origin + + return Vector3.ZERO + + +func _get_ui_panels() -> Array: + var results := [] + for screen in get_tree().get_nodes_in_group("start_screen"): + if not screen.visible: + continue + var ui_mesh: MeshInstance3D = screen.get_node_or_null("UIMesh") + var vp: SubViewport = screen.get_node_or_null("UIMesh/SubViewport") + if ui_mesh and vp: + results.append([ui_mesh, vp]) + return results + + +func _check_point_against_panel(point: Vector3, mesh: MeshInstance3D, vp: SubViewport) -> Array: + var mx := mesh.global_transform + var normal := mx.basis.z.normalized() + var signed_dist := normal.dot(point - mx.origin) + var projected := point - normal * signed_dist + var local_hit := mx.affine_inverse() * projected + var qm := mesh.mesh as QuadMesh + if qm == null: + return [] + var qs := qm.size + if abs(local_hit.x) > qs.x / 2.0 or abs(local_hit.y) > qs.y / 2.0: + return [] + var u := (local_hit.x / qs.x) + 0.5 + var v := 0.5 - (local_hit.y / qs.y) + return [signed_dist, Vector2(u * vp.size.x, v * vp.size.y), projected] + + +func _ray_intersect_panel(ray_origin: Vector3, ray_dir: Vector3, mesh: MeshInstance3D, vp: SubViewport) -> Array: + var mx := mesh.global_transform + var normal := mx.basis.z.normalized() + var denom := normal.dot(ray_dir) + if abs(denom) < 0.0001: + return [] + var t := normal.dot(mx.origin - ray_origin) / denom + if t < 0 or t > ray_length: + return [] + var hit := ray_origin + ray_dir * t + var local_hit := mx.affine_inverse() * hit + var qm := mesh.mesh as QuadMesh + if qm == null: + return [] + var qs := qm.size + if abs(local_hit.x) > qs.x / 2.0 or abs(local_hit.y) > qs.y / 2.0: + return [] + var u := (local_hit.x / qs.x) + 0.5 + var v := 0.5 - (local_hit.y / qs.y) + return [0.0, Vector2(u * vp.size.x, v * vp.size.y), hit] + + +func _on_right_button_pressed(button_name: String) -> void: + if button_name in ["trigger_click", "ax_button", "primary_click"]: + if _current_viewport and not _is_pressing: + _is_pressing = true + _active_method = "ray_right" + _send_mouse_button(_current_viewport, _last_viewport_pos, true) + + +func _on_right_button_released(button_name: String) -> void: + if button_name in ["trigger_click", "ax_button", "primary_click"]: + if _is_pressing and _active_method == "ray_right": + _is_pressing = false + if _current_viewport: + _send_mouse_button(_current_viewport, _last_viewport_pos, false) + + +func _on_left_button_pressed(button_name: String) -> void: + if button_name in ["trigger_click", "ax_button", "primary_click"]: + if _current_viewport and not _is_pressing: + _is_pressing = true + _active_method = "ray_left" + _send_mouse_button(_current_viewport, _last_viewport_pos, true) + + +func _on_left_button_released(button_name: String) -> void: + if button_name in ["trigger_click", "ax_button", "primary_click"]: + if _is_pressing and _active_method == "ray_left": + _is_pressing = false + if _current_viewport: + _send_mouse_button(_current_viewport, _last_viewport_pos, false) + + +func _send_mouse_motion(vp: SubViewport, pos: Vector2) -> void: + var event := InputEventMouseMotion.new() + event.position = pos + event.global_position = pos + vp.push_input(event) + + +func _send_mouse_button(vp: SubViewport, pos: Vector2, pressed: bool) -> void: + var event := InputEventMouseButton.new() + event.position = pos + event.global_position = pos + event.button_index = MOUSE_BUTTON_LEFT + event.pressed = pressed + if pressed: + event.button_mask = MOUSE_BUTTON_MASK_LEFT + vp.push_input(event) diff --git a/scripts/vr_ui_pointer.gd.uid b/scripts/vr_ui_pointer.gd.uid new file mode 100644 index 0000000..84e80f0 --- /dev/null +++ b/scripts/vr_ui_pointer.gd.uid @@ -0,0 +1 @@ +uid://c7w0y2lapybff diff --git a/scripts/webcam_display.gd.uid b/scripts/webcam_display.gd.uid new file mode 100644 index 0000000..5006fc8 --- /dev/null +++ b/scripts/webcam_display.gd.uid @@ -0,0 +1 @@ +uid://df6sw3ko66dyi