Browse Source
Add start screen UI, hand/body tracking, upgrade to Godot 4.6.1
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
52 changed files with 1923 additions and 56 deletions
-
25.gitignore
-
108Main.gd
-
1Main.gd.uid
-
30Main.tscn
-
BINaddons/godotopenxrvendors/.bin/android/debug/godotopenxr-androidxr-debug.aar
-
BINaddons/godotopenxrvendors/.bin/android/debug/godotopenxr-khronos-debug.aar
-
BINaddons/godotopenxrvendors/.bin/android/debug/godotopenxr-lynx-debug.aar
-
BINaddons/godotopenxrvendors/.bin/android/debug/godotopenxr-magicleap-debug.aar
-
BINaddons/godotopenxrvendors/.bin/android/debug/godotopenxr-meta-debug.aar
-
BINaddons/godotopenxrvendors/.bin/android/debug/godotopenxr-pico-debug.aar
-
BINaddons/godotopenxrvendors/.bin/android/release/godotopenxr-androidxr-release.aar
-
BINaddons/godotopenxrvendors/.bin/android/release/godotopenxr-khronos-release.aar
-
BINaddons/godotopenxrvendors/.bin/android/release/godotopenxr-lynx-release.aar
-
BINaddons/godotopenxrvendors/.bin/android/release/godotopenxr-magicleap-release.aar
-
BINaddons/godotopenxrvendors/.bin/android/release/godotopenxr-meta-release.aar
-
BINaddons/godotopenxrvendors/.bin/android/release/godotopenxr-pico-release.aar
-
BINaddons/godotopenxrvendors/.bin/android/template_debug/arm64/libgodotopenxrvendors.so
-
BINaddons/godotopenxrvendors/.bin/android/template_debug/x86_64/libgodotopenxrvendors.so
-
BINaddons/godotopenxrvendors/.bin/android/template_release/arm64/libgodotopenxrvendors.so
-
BINaddons/godotopenxrvendors/.bin/android/template_release/x86_64/libgodotopenxrvendors.so
-
BINaddons/godotopenxrvendors/.bin/linux/template_debug/arm64/libgodotopenxrvendors.so
-
BINaddons/godotopenxrvendors/.bin/linux/template_debug/x86_64/libgodotopenxrvendors.so
-
BINaddons/godotopenxrvendors/.bin/linux/template_release/arm64/libgodotopenxrvendors.so
-
BINaddons/godotopenxrvendors/.bin/linux/template_release/x86_64/libgodotopenxrvendors.so
-
BINaddons/godotopenxrvendors/.bin/macos/template_debug/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos
-
BINaddons/godotopenxrvendors/.bin/macos/template_release/libgodotopenxrvendors.macos.framework/libgodotopenxrvendors.macos
-
BINaddons/godotopenxrvendors/.bin/windows/template_debug/x86_64/libgodotopenxrvendors.dll
-
BINaddons/godotopenxrvendors/.bin/windows/template_release/x86_64/libgodotopenxrvendors.dll
-
104addons/godotopenxrvendors/GodotOpenXRVendors_CHANGES.md
-
203addons/godotopenxrvendors/androidxr/LICENSE
-
203addons/godotopenxrvendors/meta/LICENSE-LOADER
-
2addons/godotopenxrvendors/meta/LICENSE-SDK
-
4addons/godotopenxrvendors/plugin.gdextension
-
1addons/godotopenxrvendors/plugin.gdextension.uid
-
47android/AndroidManifest.xml
-
32android/build/src/standard/AndroidManifest.xml
-
2build/build_461.bat
-
57build/install_461.py
-
34build/setup_android_template.py
-
45build/setup_vendor_plugin.py
-
14export_presets.cfg
-
304openxr_action_map.tres
-
17project.godot
-
112scenes/start_screen.tscn
-
86scripts/body_tracker.gd
-
1scripts/body_tracker.gd.uid
-
74scripts/start_screen.gd
-
1scripts/start_screen.gd.uid
-
1scripts/teleop_client.gd.uid
-
469scripts/vr_ui_pointer.gd
-
1scripts/vr_ui_pointer.gd.uid
-
1scripts/webcam_display.gd.uid
@ -0,0 +1 @@ |
|||
uid://c4co4sxvbrqxn |
|||
@ -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,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. |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
Copyright © Facebook Technologies, LLC and its affiliates. All rights reserved. |
|||
|
|||
Your use of this SDK or tool is subject to the Oculus SDK License Agreement, available at https://developer.oculus.com/licenses/oculussdk/ |
|||
Your use of this SDK or tool is subject to the Oculus SDK License Agreement, available at https://developer.oculus.com/licenses/oculussdk/ |
|||
@ -0,0 +1 @@ |
|||
uid://u32t56vkch04 |
|||
@ -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> |
|||
|
|||
|
|||
@ -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> |
|||
@ -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" |
|||
@ -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.") |
|||
@ -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!") |
|||
@ -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.") |
|||
@ -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")] |
|||
@ -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" |
|||
@ -0,0 +1 @@ |
|||
uid://1xnuuli2itfk |
|||
@ -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 |
|||
@ -0,0 +1 @@ |
|||
uid://cd50pdphkb1do |
|||
@ -0,0 +1 @@ |
|||
uid://cejwgl45w03x7 |
|||
@ -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) |
|||
@ -0,0 +1 @@ |
|||
uid://c7w0y2lapybff |
|||
@ -0,0 +1 @@ |
|||
uid://df6sw3ko66dyi |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue