Browse Source

Add start screen UI, hand/body tracking, upgrade to Godot 4.6.1

- Two-phase startup: CONFIG phase (VR start screen) -> AR phase (passthrough)
- Start screen with server host/port input, connect button, and launch AR button
- VR UI pointer with controller ray-pointing and hand poke interaction
- Body tracking visualization with colored spheres (spine, head, arms)
- Hand tracking with camera-based tracking (no controllers required)
- Android flavor manifest for Quest hand/body tracking permissions
- Upgrade OpenXR Vendors plugin from v3.1.2 to v4.3.0-stable
- Export presets updated for Godot 4.6.1 Meta plugin format
- Build scripts for Godot 4.6.1 with vendor plugin setup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
master
melancholytron 1 month ago
parent
commit
8afe9ac201
  1. 25
      .gitignore
  2. 108
      Main.gd
  3. 1
      Main.gd.uid
  4. 30
      Main.tscn
  5. BIN
      addons/godotopenxrvendors/.bin/android/debug/godotopenxr-androidxr-debug.aar
  6. BIN
      addons/godotopenxrvendors/.bin/android/debug/godotopenxr-khronos-debug.aar
  7. BIN
      addons/godotopenxrvendors/.bin/android/debug/godotopenxr-lynx-debug.aar
  8. BIN
      addons/godotopenxrvendors/.bin/android/debug/godotopenxr-magicleap-debug.aar
  9. BIN
      addons/godotopenxrvendors/.bin/android/debug/godotopenxr-meta-debug.aar
  10. BIN
      addons/godotopenxrvendors/.bin/android/debug/godotopenxr-pico-debug.aar
  11. BIN
      addons/godotopenxrvendors/.bin/android/release/godotopenxr-androidxr-release.aar
  12. BIN
      addons/godotopenxrvendors/.bin/android/release/godotopenxr-khronos-release.aar
  13. BIN
      addons/godotopenxrvendors/.bin/android/release/godotopenxr-lynx-release.aar
  14. BIN
      addons/godotopenxrvendors/.bin/android/release/godotopenxr-magicleap-release.aar
  15. BIN
      addons/godotopenxrvendors/.bin/android/release/godotopenxr-meta-release.aar
  16. BIN
      addons/godotopenxrvendors/.bin/android/release/godotopenxr-pico-release.aar
  17. BIN
      addons/godotopenxrvendors/.bin/android/template_debug/arm64/libgodotopenxrvendors.so
  18. BIN
      addons/godotopenxrvendors/.bin/android/template_debug/x86_64/libgodotopenxrvendors.so
  19. BIN
      addons/godotopenxrvendors/.bin/android/template_release/arm64/libgodotopenxrvendors.so
  20. BIN
      addons/godotopenxrvendors/.bin/android/template_release/x86_64/libgodotopenxrvendors.so
  21. BIN
      addons/godotopenxrvendors/.bin/linux/template_debug/arm64/libgodotopenxrvendors.so
  22. BIN
      addons/godotopenxrvendors/.bin/linux/template_debug/x86_64/libgodotopenxrvendors.so
  23. BIN
      addons/godotopenxrvendors/.bin/linux/template_release/arm64/libgodotopenxrvendors.so
  24. BIN
      addons/godotopenxrvendors/.bin/linux/template_release/x86_64/libgodotopenxrvendors.so
  25. BIN
      addons/godotopenxrvendors/.bin/macos/template_debug/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos
  26. BIN
      addons/godotopenxrvendors/.bin/macos/template_release/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos
  27. BIN
      addons/godotopenxrvendors/.bin/windows/template_debug/x86_64/libgodotopenxrvendors.dll
  28. BIN
      addons/godotopenxrvendors/.bin/windows/template_release/x86_64/libgodotopenxrvendors.dll
  29. 104
      addons/godotopenxrvendors/GodotOpenXRVendors_CHANGES.md
  30. 203
      addons/godotopenxrvendors/androidxr/LICENSE
  31. 203
      addons/godotopenxrvendors/meta/LICENSE-LOADER
  32. 0
      addons/godotopenxrvendors/meta/LICENSE-SDK
  33. 4
      addons/godotopenxrvendors/plugin.gdextension
  34. 1
      addons/godotopenxrvendors/plugin.gdextension.uid
  35. 47
      android/AndroidManifest.xml
  36. 32
      android/build/src/standard/AndroidManifest.xml
  37. 2
      build/build_461.bat
  38. 57
      build/install_461.py
  39. 34
      build/setup_android_template.py
  40. 45
      build/setup_vendor_plugin.py
  41. 14
      export_presets.cfg
  42. 304
      openxr_action_map.tres
  43. 17
      project.godot
  44. 112
      scenes/start_screen.tscn
  45. 86
      scripts/body_tracker.gd
  46. 1
      scripts/body_tracker.gd.uid
  47. 74
      scripts/start_screen.gd
  48. 1
      scripts/start_screen.gd.uid
  49. 1
      scripts/teleop_client.gd.uid
  50. 469
      scripts/vr_ui_pointer.gd
  51. 1
      scripts/vr_ui_pointer.gd.uid
  52. 1
      scripts/webcam_display.gd.uid

25
.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

108
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:

1
Main.gd.uid

@ -0,0 +1 @@
uid://c4co4sxvbrqxn

30
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

BIN
addons/godotopenxrvendors/.bin/android/debug/godotopenxr-androidxr-debug.aar

BIN
addons/godotopenxrvendors/.bin/android/debug/godotopenxr-khronos-debug.aar

BIN
addons/godotopenxrvendors/.bin/android/debug/godotopenxr-lynx-debug.aar

BIN
addons/godotopenxrvendors/.bin/android/debug/godotopenxr-magicleap-debug.aar

BIN
addons/godotopenxrvendors/.bin/android/debug/godotopenxr-meta-debug.aar

BIN
addons/godotopenxrvendors/.bin/android/debug/godotopenxr-pico-debug.aar

BIN
addons/godotopenxrvendors/.bin/android/release/godotopenxr-androidxr-release.aar

BIN
addons/godotopenxrvendors/.bin/android/release/godotopenxr-khronos-release.aar

BIN
addons/godotopenxrvendors/.bin/android/release/godotopenxr-lynx-release.aar

BIN
addons/godotopenxrvendors/.bin/android/release/godotopenxr-magicleap-release.aar

BIN
addons/godotopenxrvendors/.bin/android/release/godotopenxr-meta-release.aar

BIN
addons/godotopenxrvendors/.bin/android/release/godotopenxr-pico-release.aar

BIN
addons/godotopenxrvendors/.bin/android/template_debug/arm64/libgodotopenxrvendors.so

BIN
addons/godotopenxrvendors/.bin/android/template_debug/x86_64/libgodotopenxrvendors.so

BIN
addons/godotopenxrvendors/.bin/android/template_release/arm64/libgodotopenxrvendors.so

BIN
addons/godotopenxrvendors/.bin/android/template_release/x86_64/libgodotopenxrvendors.so

BIN
addons/godotopenxrvendors/.bin/linux/template_debug/arm64/libgodotopenxrvendors.so

BIN
addons/godotopenxrvendors/.bin/linux/template_debug/x86_64/libgodotopenxrvendors.so

BIN
addons/godotopenxrvendors/.bin/linux/template_release/arm64/libgodotopenxrvendors.so

BIN
addons/godotopenxrvendors/.bin/linux/template_release/x86_64/libgodotopenxrvendors.so

BIN
addons/godotopenxrvendors/.bin/macos/template_debug/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos

BIN
addons/godotopenxrvendors/.bin/macos/template_release/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos

BIN
addons/godotopenxrvendors/.bin/windows/template_debug/x86_64/libgodotopenxrvendors.dll

BIN
addons/godotopenxrvendors/.bin/windows/template_release/x86_64/libgodotopenxrvendors.dll

104
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

203
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.

203
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.

0
addons/godotopenxrvendors/meta/LICENSE.txt → addons/godotopenxrvendors/meta/LICENSE-SDK

4
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"

1
addons/godotopenxrvendors/plugin.gdextension.uid

@ -0,0 +1 @@
uid://u32t56vkch04

47
android/AndroidManifest.xml

@ -1,35 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Additional manifest entries for Quest 3 body tracking.
These are merged into the final AndroidManifest.xml by Godot's build system
when using the Meta OpenXR Vendors plugin.
Key extensions enabled:
- XR_FB_body_tracking (70 joints including chest)
- XR_META_body_tracking_full_body (v4.1.1+)
- XR_EXT_hand_tracking (fallback, if body tracking unavailable)
- Passthrough (mixed reality)
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required permissions -->
<!-- User manifest overlay - merged with the build template manifest.
Adds Quest-specific features that the Godot 4.6.1 export plugin
does not inject automatically. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Network permissions for WebSocket connection to robot server -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Meta Quest features -->
<uses-feature android:name="com.oculus.feature.PASSTHROUGH" android:required="true" />
<uses-feature android:name="oculus.software.body_tracking" android:required="true" />
<!-- Hand tracking support - prevents "need controllers" dialog -->
<uses-feature android:name="oculus.software.handtracking" android:required="false" />
<!-- Passthrough AR mode -->
<uses-feature android:name="com.oculus.feature.PASSTHROUGH" android:required="true" />
<!-- Body tracking -->
<uses-feature android:name="oculus.software.body_tracking" android:required="false" />
<!-- VR head tracking - override template's required=false -->
<uses-feature
android:name="android.hardware.vr.headtracking"
android:required="true"
android:version="1"
tools:replace="android:required" />
<application>
<!-- Body tracking metadata -->
<meta-data android:name="com.oculus.supportedDevices" android:value="quest3|quest3s|questpro" />
<meta-data android:name="com.oculus.intent.category.VR" android:value="vr_only" />
<!-- Hand tracking configuration -->
<meta-data android:name="com.oculus.handtracking.frequency" android:value="HIGH" />
<meta-data android:name="com.oculus.handtracking.version" android:value="V2.0" />
<!-- Enable body tracking API -->
<!-- Body tracking configuration -->
<meta-data android:name="com.oculus.bodytracking.enabled" android:value="true" />
<!-- Request full body tracking (includes upper body by default) -->
<meta-data android:name="com.oculus.bodytracking.full_body" android:value="true" />
</application>

32
android/build/src/standard/AndroidManifest.xml

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Quest-specific manifest entries merged via Android Gradle Plugin's
manifest merger. Placed in 'standard' product flavor so Godot's export
(which only overwrites src/main/AndroidManifest.xml) won't touch it. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Hand tracking permission - required for camera-based hand tracking access -->
<uses-permission android:name="com.oculus.permission.HAND_TRACKING" />
<!-- Body tracking permission -->
<uses-permission android:name="com.oculus.permission.BODY_TRACKING" />
<!-- Hand tracking support - prevents Quest "need controllers" dialog -->
<uses-feature android:name="oculus.software.handtracking" android:required="false" />
<!-- Passthrough AR mode -->
<uses-feature android:name="com.oculus.feature.PASSTHROUGH" android:required="true" />
<!-- Body tracking -->
<uses-feature android:name="oculus.software.body_tracking" android:required="false" />
<application>
<!-- Hand tracking configuration -->
<meta-data android:name="com.oculus.handtracking.frequency" android:value="HIGH" />
<meta-data android:name="com.oculus.handtracking.version" android:value="V2.0" />
<!-- Body tracking configuration -->
<meta-data android:name="com.oculus.bodytracking.enabled" android:value="true" />
<meta-data android:name="com.oculus.bodytracking.full_body" android:value="true" />
</application>
</manifest>

2
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"

57
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.")

34
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!")

45
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.")

14
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

304
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")]

17
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

112
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"

86
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:

1
scripts/body_tracker.gd.uid

@ -0,0 +1 @@
uid://1xnuuli2itfk

74
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

1
scripts/start_screen.gd.uid

@ -0,0 +1 @@
uid://cd50pdphkb1do

1
scripts/teleop_client.gd.uid

@ -0,0 +1 @@
uid://cejwgl45w03x7

469
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)

1
scripts/vr_ui_pointer.gd.uid

@ -0,0 +1 @@
uid://c7w0y2lapybff

1
scripts/webcam_display.gd.uid

@ -0,0 +1 @@
uid://df6sw3ko66dyi
Loading…
Cancel
Save