From f24721a4e6f28621c0d5508bd48232af088cd6fd Mon Sep 17 00:00:00 2001 From: Mauricio Narvaez Date: Wed, 6 Dec 2023 20:59:12 +0700 Subject: [PATCH] Add basic Scene API support (SceneModelProvider) --- build.sh | 3 + godotopenxrmeta/CMakeLists.txt | 1 + godotopenxrmeta/src/main/AndroidManifest.xml | 3 + .../src/main/cpp/register_types.cpp | 9 + .../openxr_fb_scene_extension_wrapper.cpp | 411 ++++++++++++++++++ .../scene/openxr_fb_scene_extension_wrapper.h | 106 +++++ .../main/cpp/scene/scene_model_provider.cpp | 68 +++ .../src/main/cpp/scene/scene_model_provider.h | 36 ++ .../src/main/cpp/scene/xr_scene_object.cpp | 83 ++++ .../src/main/cpp/scene/xr_scene_object.h | 58 +++ .../main/cpp/scene/xr_scene_provider_fake.cpp | 51 +++ .../main/cpp/scene/xr_scene_provider_fake.h | 15 + .../src/main/cpp/utils/xr_godot_utils.cpp | 29 ++ .../src/main/cpp/utils/xr_godot_utils.h | 22 + 14 files changed, 895 insertions(+) create mode 100755 build.sh create mode 100644 godotopenxrmeta/src/main/cpp/scene/openxr_fb_scene_extension_wrapper.cpp create mode 100644 godotopenxrmeta/src/main/cpp/scene/openxr_fb_scene_extension_wrapper.h create mode 100644 godotopenxrmeta/src/main/cpp/scene/scene_model_provider.cpp create mode 100644 godotopenxrmeta/src/main/cpp/scene/scene_model_provider.h create mode 100644 godotopenxrmeta/src/main/cpp/scene/xr_scene_object.cpp create mode 100644 godotopenxrmeta/src/main/cpp/scene/xr_scene_object.h create mode 100644 godotopenxrmeta/src/main/cpp/scene/xr_scene_provider_fake.cpp create mode 100644 godotopenxrmeta/src/main/cpp/scene/xr_scene_provider_fake.h create mode 100644 godotopenxrmeta/src/main/cpp/utils/xr_godot_utils.cpp create mode 100644 godotopenxrmeta/src/main/cpp/utils/xr_godot_utils.h diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..2f5c1cb1 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +scons target=template_debug -j4 +scons target=template_release -j4 +./gradlew build diff --git a/godotopenxrmeta/CMakeLists.txt b/godotopenxrmeta/CMakeLists.txt index 7439e540..ae362fef 100644 --- a/godotopenxrmeta/CMakeLists.txt +++ b/godotopenxrmeta/CMakeLists.txt @@ -111,6 +111,7 @@ target_include_directories(${CMAKE_PROJECT_NAME} ${GODOT_CPP_INCLUDE_DIRECTORIES} ${OPENXR_HEADERS_DIR} ${OPENXR_MOBILE_HEADERS_DIR} + ${CMAKE_SOURCE_DIR}/src/main/cpp/ ) target_link_libraries(${CMAKE_PROJECT_NAME} diff --git a/godotopenxrmeta/src/main/AndroidManifest.xml b/godotopenxrmeta/src/main/AndroidManifest.xml index a3dc5ac6..d9613444 100644 --- a/godotopenxrmeta/src/main/AndroidManifest.xml +++ b/godotopenxrmeta/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ android:required="true" android:version="1"/> + + + (); OpenXRFbSceneCaptureExtensionWrapper::get_singleton()->register_extension_wrapper(); + + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); + memnew(OpenXRFbSceneExtensionWrapper)->register_extension_wrapper(); } } diff --git a/godotopenxrmeta/src/main/cpp/scene/openxr_fb_scene_extension_wrapper.cpp b/godotopenxrmeta/src/main/cpp/scene/openxr_fb_scene_extension_wrapper.cpp new file mode 100644 index 00000000..5ecab5aa --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/scene/openxr_fb_scene_extension_wrapper.cpp @@ -0,0 +1,411 @@ +#include "openxr_fb_scene_extension_wrapper.h" + +#include +#include +#include + +#include +#include + +#include "utils/xr_godot_utils.h" + +#define SESSION (XrSession) get_openxr_api()->get_session() + +using namespace godot; + +static const uint32_t MAX_PERSISTENT_SPACES = 100; + +// XR_FB_spatial_entity +PFN_xrCreateSpatialAnchorFB OpenXRFbSceneExtensionWrapper::xrCreateSpatialAnchorFB = nullptr; +PFN_xrGetSpaceUuidFB OpenXRFbSceneExtensionWrapper::xrGetSpaceUuidFB = nullptr; +PFN_xrEnumerateSpaceSupportedComponentsFB OpenXRFbSceneExtensionWrapper::xrEnumerateSpaceSupportedComponentsFB = nullptr; +PFN_xrSetSpaceComponentStatusFB OpenXRFbSceneExtensionWrapper::xrSetSpaceComponentStatusFB = nullptr; +PFN_xrGetSpaceComponentStatusFB OpenXRFbSceneExtensionWrapper::xrGetSpaceComponentStatusFB = nullptr; + +// XR_FB_spatial_entity_query +PFN_xrQuerySpacesFB OpenXRFbSceneExtensionWrapper::xrQuerySpacesFB = nullptr; +PFN_xrRetrieveSpaceQueryResultsFB OpenXRFbSceneExtensionWrapper::xrRetrieveSpaceQueryResultsFB = nullptr; + +// XR_FB_spatial_entity_container +PFN_xrGetSpaceContainerFB OpenXRFbSceneExtensionWrapper::xrGetSpaceContainerFB = nullptr; + +// XR_FB_scene +PFN_xrGetSpaceBoundingBox2DFB OpenXRFbSceneExtensionWrapper::xrGetSpaceBoundingBox2DFB = nullptr; +PFN_xrGetSpaceBoundingBox3DFB OpenXRFbSceneExtensionWrapper::xrGetSpaceBoundingBox3DFB = nullptr; +PFN_xrGetSpaceSemanticLabelsFB OpenXRFbSceneExtensionWrapper::xrGetSpaceSemanticLabelsFB = nullptr; +PFN_xrGetSpaceBoundary2DFB OpenXRFbSceneExtensionWrapper::xrGetSpaceBoundary2DFB = nullptr; +PFN_xrGetSpaceRoomLayoutFB OpenXRFbSceneExtensionWrapper::xrGetSpaceRoomLayoutFB = nullptr; + +// XR_FB_scene_capture +PFN_xrRequestSceneCaptureFB OpenXRFbSceneExtensionWrapper::xrRequestSceneCaptureFB = nullptr; + +// Base OpenXR APIs we still need +PFN_xrLocateSpace OpenXRFbSceneExtensionWrapper::xrLocateSpace = nullptr; + +// Singleton +OpenXRFbSceneExtensionWrapper *OpenXRFbSceneExtensionWrapper::singleton = nullptr; + +OpenXRFbSceneExtensionWrapper* OpenXRFbSceneExtensionWrapper::get_singleton() { + return singleton; +} + +OpenXRFbSceneExtensionWrapper::OpenXRFbSceneExtensionWrapper() { + singleton = this; +} + +OpenXRFbSceneExtensionWrapper::~OpenXRFbSceneExtensionWrapper() { + singleton = nullptr; +} + +void OpenXRFbSceneExtensionWrapper::_on_instance_created(uint64_t instance) { + // XR_FB_spatial_entity + xrCreateSpatialAnchorFB = (PFN_xrCreateSpatialAnchorFB) get_openxr_api()->get_instance_proc_addr("xrCreateSpatialAnchorFB"); + xrGetSpaceUuidFB = (PFN_xrGetSpaceUuidFB) get_openxr_api()->get_instance_proc_addr("xrGetSpaceUuidFB"); + xrEnumerateSpaceSupportedComponentsFB = (PFN_xrEnumerateSpaceSupportedComponentsFB) get_openxr_api()->get_instance_proc_addr("xrEnumerateSpaceSupportedComponentsFB"); + xrSetSpaceComponentStatusFB = (PFN_xrSetSpaceComponentStatusFB) get_openxr_api()->get_instance_proc_addr("xrSetSpaceComponentStatusFB"); + xrGetSpaceComponentStatusFB = (PFN_xrGetSpaceComponentStatusFB) get_openxr_api()->get_instance_proc_addr("xrGetSpaceComponentStatusFB"); + + // XR_FB_spatial_entity_query + xrQuerySpacesFB = (PFN_xrQuerySpacesFB) get_openxr_api()->get_instance_proc_addr("xrQuerySpacesFB"); + xrRetrieveSpaceQueryResultsFB = (PFN_xrRetrieveSpaceQueryResultsFB) get_openxr_api()->get_instance_proc_addr("xrRetrieveSpaceQueryResultsFB"); + + // XR_FB_spatial_entity_container + xrGetSpaceContainerFB = (PFN_xrGetSpaceContainerFB) get_openxr_api()->get_instance_proc_addr("xrGetSpaceContainerFB"); + + // XR_FB_scene + xrGetSpaceBoundingBox2DFB = (PFN_xrGetSpaceBoundingBox2DFB) get_openxr_api()->get_instance_proc_addr("xrGetSpaceBoundingBox2DFB"); + xrGetSpaceBoundingBox3DFB = (PFN_xrGetSpaceBoundingBox3DFB) get_openxr_api()->get_instance_proc_addr("xrGetSpaceBoundingBox3DFB"); + xrGetSpaceSemanticLabelsFB = (PFN_xrGetSpaceSemanticLabelsFB) get_openxr_api()->get_instance_proc_addr("xrGetSpaceSemanticLabelsFB"); + xrGetSpaceBoundary2DFB = (PFN_xrGetSpaceBoundary2DFB) get_openxr_api()->get_instance_proc_addr("xrGetSpaceBoundary2DFB"); + xrGetSpaceRoomLayoutFB = (PFN_xrGetSpaceRoomLayoutFB) get_openxr_api()->get_instance_proc_addr("xrGetSpaceRoomLayoutFB"); + + // XR_FB_scene_capture + xrRequestSceneCaptureFB = (PFN_xrRequestSceneCaptureFB) get_openxr_api()->get_instance_proc_addr("xrRequestSceneCaptureFB"); + + // Base OpenXR + xrLocateSpace = (PFN_xrLocateSpace) get_openxr_api()->get_instance_proc_addr("xrLocateSpace"); +} + +Dictionary OpenXRFbSceneExtensionWrapper::_get_requested_extensions() { + Dictionary requested; + requested[XR_FB_SPATIAL_ENTITY_EXTENSION_NAME] = nullptr; + requested[XR_FB_SPATIAL_ENTITY_QUERY_EXTENSION_NAME] = nullptr; + requested[XR_FB_SPATIAL_ENTITY_CONTAINER_EXTENSION_NAME] = nullptr; + requested[XR_FB_SCENE_EXTENSION_NAME] = nullptr; + requested[XR_FB_SCENE_CAPTURE_EXTENSION_NAME] = nullptr; + return requested; +} + +bool OpenXRFbSceneExtensionWrapper::_on_event_polled(const void *event_ptr) { + const XrEventDataBuffer* event = (XrEventDataBuffer*) event_ptr; + switch(event->type) { + case XR_TYPE_EVENT_DATA_SCENE_CAPTURE_COMPLETE_FB: + on_scene_capture_complete((const XrEventDataSceneCaptureCompleteFB*) event); + return true; + case XR_TYPE_EVENT_DATA_SPACE_QUERY_RESULTS_AVAILABLE_FB: + on_space_query_results((const XrEventDataSpaceQueryResultsAvailableFB*) event); + return true; + case XR_TYPE_EVENT_DATA_SPACE_QUERY_COMPLETE_FB: + on_space_query_complete((const XrEventDataSpaceQueryCompleteFB*) event); + return true; + default: + return false; + } +} + +void OpenXRFbSceneExtensionWrapper::on_scene_capture_complete(const XrEventDataSceneCaptureCompleteFB* event) { + if (sceneCaptureCallbacks_.count(event->requestId) > 0) { + sceneCaptureCallbacks_[event->requestId](); + sceneCaptureCallbacks_.erase(event->requestId); + } else { + WARN_PRINT("Got XR_TYPE_EVENT_DATA_SCENE_CAPTURE_COMPLETE_FB with no callback!"); + } +} + +bool OpenXRFbSceneExtensionWrapper::on_space_query_results(const XrEventDataSpaceQueryResultsAvailableFB* event) { + WARN_PRINT("Got XR_TYPE_EVENT_DATA_SPACE_QUERY_RESULTS_AVAILABLE_FB!"); + // Query the results that are now available using two-call idiom + XrSpaceQueryResultsFB queryResults{ + XR_TYPE_SPACE_QUERY_RESULTS_FB, // type + nullptr, // next + 0, // resultCapacityInput + 0, // resultCapacityOutput + nullptr, // results + }; + XrResult result = xrRetrieveSpaceQueryResultsFB(SESSION, event->requestId, &queryResults); + if (!XR_SUCCEEDED(result)) { + WARN_PRINT("xrRetrieveSpaceQueryResultsFB 1 failed!"); + WARN_PRINT(get_openxr_api()->get_error_string(result)); + return false; + } + + std::vector results(queryResults.resultCountOutput); + queryResults.resultCapacityInput = results.size(); + queryResults.resultCountOutput = 0; + queryResults.results = results.data(); + + result = xrRetrieveSpaceQueryResultsFB(SESSION, event->requestId, &queryResults); + if (!XR_SUCCEEDED(result)) { + WARN_PRINT("xrRetrieveSpaceQueryResultsFB 2 failed!"); + WARN_PRINT(get_openxr_api()->get_error_string(result)); + return false; + } + + // Store the results to send via callback later + for (uint32_t i = 0; i < queryResults.resultCountOutput; ++i) { + results_[event->requestId][results[i].uuid] = results[i]; + } + return true; +} + +void OpenXRFbSceneExtensionWrapper::try_adding_output_for_uuid(XrUuidEXT uuid, XrAsyncRequestIdFB requestId, std::vector& objects) { + if (XrGodotUtils::isUuidValid(uuid) && results_[requestId].count(uuid) > 0) { + auto result = results_[requestId][uuid]; + + // Ensure the anchor we are adding is locatable + if (is_component_supported(result.space, XR_SPACE_COMPONENT_TYPE_LOCATABLE_FB)) { + if (!is_component_enabled(result.space, XR_SPACE_COMPONENT_TYPE_LOCATABLE_FB)) { + if (set_component_enabled(result.space, XR_SPACE_COMPONENT_TYPE_LOCATABLE_FB, true) == std::nullopt) { + std::string err = "Unable to enable XR_SPACE_COMPONENT_TYPE_LOCATABLE_FB for XrSpace " + XrGodotUtils::uuidToString(uuid); + WARN_PRINT(String(err.c_str())); + } + } else { + std::string msg = "XR_SPACE_COMPONENT_TYPE_LOCATABLE_FB already enabled for XrSpace " + XrGodotUtils::uuidToString(uuid); + WARN_PRINT(String(msg.c_str())); + } + } else { + std::string err = "XR_SPACE_COMPONENT_TYPE_LOCATABLE_FB not supported for XrSpace " + XrGodotUtils::uuidToString(uuid); + WARN_PRINT(String(err.c_str())); + } + + // Grab the semantic label if we can + auto labels = get_semantic_labels(result.space); + + XrSceneObjectInternal obj = { + result.uuid, + result.space, + labels, + std::nullopt, + std::nullopt, + std::nullopt, + }; + + // Grab the 2D or 3D shapes + get_shapes(result.space, obj); + + objects.push_back(obj); + } else { + std::string error = "Uuid invalid or unavailable: " + XrGodotUtils::uuidToString(uuid); + WARN_PRINT(String(error.c_str())); + } +} + +void OpenXRFbSceneExtensionWrapper::on_space_query_complete(const XrEventDataSpaceQueryCompleteFB* event) { + if (queryRoomCallbacks_.count(event->requestId) > 0) { + WARN_PRINT("Finished querying spaces!"); + auto& results = results_[event->requestId]; + + // There is exactly 1 room space, find that first + auto roomIt = std::find_if(results.begin(), results.end(), [this](const std::pair & result) { + return is_component_enabled(result.second.space, XR_SPACE_COMPONENT_TYPE_ROOM_LAYOUT_FB); + }); + if (roomIt == results.end()) { + WARN_PRINT("No room available, bailing!"); + queryRoomCallbacks_[event->requestId]({}); + queryRoomCallbacks_.erase(event->requestId); + results_.erase(event->requestId); + return; + } + + std::vector objects; + + // Get the room info: Unused for now because the same info is returned by xrGetSpaceContainerFB + // with semantic labels (keeping as reference in case the exact layout matters layer) + // XrRoomLayoutFB roomLayout = {XR_TYPE_ROOM_LAYOUT_FB}; + // xrGetSpaceRoomLayoutFB(SESSION, roomIt->second.space, &roomLayout); + // std::vector wallUuids(roomLayout.wallUuidCountOutput); + // roomLayout.wallUuidCapacityInput = wallUuids.size(); + // roomLayout.wallUuids = wallUuids.data(); + // xrGetSpaceRoomLayoutFB(SESSION, roomIt->second.space, &roomLayout); + // + // try_adding_output_for_uuid(roomLayout.floorUuid, event->requestId, objects); + // try_adding_output_for_uuid(roomLayout.ceilingUuid, event->requestId, objects); + // for (int i = 0; i < roomLayout.wallUuidCountOutput; i++) { + // try_adding_output_for_uuid(roomLayout.wallUuids[i], event->requestId, objects); + // } + + // Get other contained anchors + XrSpaceContainerFB spaceContainer = {XR_TYPE_SPACE_CONTAINER_FB}; + xrGetSpaceContainerFB(SESSION, roomIt->second.space, &spaceContainer); + std::vector uuids(spaceContainer.uuidCountOutput); + spaceContainer.uuidCapacityInput = uuids.size(); + spaceContainer.uuids = uuids.data(); + xrGetSpaceContainerFB(SESSION, roomIt->second.space, &spaceContainer); + + // Add contained anchors to the output too + for (uint32_t i = 0; i < spaceContainer.uuidCountOutput; i++) { + try_adding_output_for_uuid(spaceContainer.uuids[i], event->requestId, objects); + } + + WARN_PRINT("Finished collecting data, sending to Godot!"); + queryRoomCallbacks_[event->requestId](objects); + queryRoomCallbacks_.erase(event->requestId); + results_.erase(event->requestId); + } else { + WARN_PRINT("Got XR_TYPE_EVENT_DATA_SPACE_QUERY_COMPLETE_FB with no callback!"); + } +} + +bool OpenXRFbSceneExtensionWrapper::request_scene_capture(SceneCaptureCallback_t callback) { + if (!xrRequestSceneCaptureFB) { + WARN_PRINT("Failed to load xrRequestSceneCaptureFB hook"); + return false; + } + + XrAsyncRequestIdFB requestId; + XrSceneCaptureRequestInfoFB request = {XR_TYPE_SCENE_CAPTURE_REQUEST_INFO_FB, nullptr}; + std::string requestString = "TABLE"; + request.requestByteCount = requestString.size(); + request.request = requestString.c_str(); + const XrResult result = xrRequestSceneCaptureFB(SESSION, &request, &requestId); + + if (!XR_SUCCEEDED(result)) { + WARN_PRINT("xrRequestSceneCaptureFB failed!"); + WARN_PRINT(get_openxr_api()->get_error_string(result)); + return false; + } + + sceneCaptureCallbacks_[requestId] = callback; + return true; +} + +bool OpenXRFbSceneExtensionWrapper::query_room(QueryRoomCallback_t callback) { + if (!xrQuerySpacesFB) { + WARN_PRINT("Failed to load xrQuerySpacesFB hook"); + return false; + } + + XrSpaceQueryInfoFB queryInfo = { + XR_TYPE_SPACE_QUERY_INFO_FB, + nullptr, + XR_SPACE_QUERY_ACTION_LOAD_FB, + MAX_PERSISTENT_SPACES, + 0, + nullptr, + nullptr}; + XrAsyncRequestIdFB requestId; + const XrResult result = + xrQuerySpacesFB(SESSION, (XrSpaceQueryInfoBaseHeaderFB*) &queryInfo, &requestId); + + if (!XR_SUCCEEDED(result)) { + WARN_PRINT("xrQuerySpacesFB failed!"); + WARN_PRINT(get_openxr_api()->get_error_string(result)); + return false; + } + + queryRoomCallbacks_[requestId] = callback; + return true; +} + +bool OpenXRFbSceneExtensionWrapper::is_component_supported(const XrSpace space, XrSpaceComponentTypeFB type) { + uint32_t numComponents = 0; + xrEnumerateSpaceSupportedComponentsFB(space, 0, &numComponents, nullptr); + std::vector components(numComponents); + xrEnumerateSpaceSupportedComponentsFB(space, numComponents, &numComponents, components.data()); + + bool supported = false; + for (uint32_t c = 0; c < numComponents; ++c) { + if (components[c] == type) { + supported = true; + break; + } + } + return supported; +} + +bool OpenXRFbSceneExtensionWrapper::is_component_enabled(const XrSpace space, XrSpaceComponentTypeFB type) { + XrSpaceComponentStatusFB status = {XR_TYPE_SPACE_COMPONENT_STATUS_FB, nullptr}; + xrGetSpaceComponentStatusFB(space, type, &status); + return (status.enabled && !status.changePending); +} + +std::optional OpenXRFbSceneExtensionWrapper::set_component_enabled(const XrSpace space, XrSpaceComponentTypeFB type, bool status) { + XrSpaceComponentStatusSetInfoFB request = { + XR_TYPE_SPACE_COMPONENT_STATUS_SET_INFO_FB, + nullptr, + type, + status, + 0}; + XrAsyncRequestIdFB requestId; + if (XR_SUCCEEDED(xrSetSpaceComponentStatusFB(space, &request, &requestId))) { + return requestId; + } + return std::nullopt; +} + +void OpenXRFbSceneExtensionWrapper::locate_space(const XrSpace space, XrSpaceLocation* location) { + XrTime display_time = (XrTime) get_openxr_api()->get_next_frame_time(); + XrSpace play_space = (XrSpace) get_openxr_api()->get_play_space(); + xrLocateSpace(space, play_space, display_time, location); +} + +std::optional OpenXRFbSceneExtensionWrapper::get_semantic_labels(const XrSpace space) { + if (!is_component_enabled(space, XR_SPACE_COMPONENT_TYPE_SEMANTIC_LABELS_FB)) { + return std::nullopt; + } + + static const std::string recognizedLabels = + "TABLE,COUCH,FLOOR,CEILING,WALL_FACE,WINDOW_FRAME,DOOR_FRAME,STORAGE,BED,SCREEN,LAMP,PLANT,WALL_ART,INVISIBLE_WALL_FACE,OTHER"; + const XrSemanticLabelsSupportInfoFB semanticLabelsSupportInfo = { + XR_TYPE_SEMANTIC_LABELS_SUPPORT_INFO_FB, + nullptr, + XR_SEMANTIC_LABELS_SUPPORT_MULTIPLE_SEMANTIC_LABELS_BIT_FB | XR_SEMANTIC_LABELS_SUPPORT_ACCEPT_DESK_TO_TABLE_MIGRATION_BIT_FB | + XR_SEMANTIC_LABELS_SUPPORT_ACCEPT_INVISIBLE_WALL_FACE_BIT_FB, + recognizedLabels.c_str()}; + + XrSemanticLabelsFB labels = {XR_TYPE_SEMANTIC_LABELS_FB, &semanticLabelsSupportInfo, 0}; + + // First call. + xrGetSpaceSemanticLabelsFB(SESSION, space, &labels); + // Second call + std::vector labelData(labels.bufferCountOutput); + labels.bufferCapacityInput = labelData.size(); + labels.buffer = labelData.data(); + xrGetSpaceSemanticLabelsFB(SESSION, space, &labels); + + return std::string(labels.buffer, labels.bufferCountOutput); +} + +void OpenXRFbSceneExtensionWrapper::get_shapes(const XrSpace space, XrSceneObjectInternal& object) { + if (is_component_enabled(space, XR_SPACE_COMPONENT_TYPE_BOUNDED_2D_FB)) { + // Grab both the bounding box 2D and the boundary + XrRect2Df boundingBox2D; + if (XR_SUCCEEDED(xrGetSpaceBoundingBox2DFB(SESSION, space, &boundingBox2D))) { + object.boundingBox2D = boundingBox2D; + } + + XrBoundary2DFB boundary2D = {XR_TYPE_BOUNDARY_2D_FB, nullptr, 0}; + if (XR_SUCCEEDED(xrGetSpaceBoundary2DFB(SESSION, space, &boundary2D))) { + std::vector vertices(boundary2D.vertexCountOutput); + boundary2D.vertexCapacityInput = vertices.size(); + boundary2D.vertices = vertices.data(); + if (XR_SUCCEEDED(xrGetSpaceBoundary2DFB(SESSION, space, &boundary2D))) { + object.boundary2D = vertices; + } + } + } + + if (is_component_enabled(space, XR_SPACE_COMPONENT_TYPE_BOUNDED_3D_FB)) { + XrRect3DfFB boundingBox3D; + if (XR_SUCCEEDED(xrGetSpaceBoundingBox3DFB(SESSION, space, &boundingBox3D))) { + object.boundingBox3D = boundingBox3D; + } + } + + // TODO: Need to enable the extension for this + // if (is_component_enabled(space, XR_SPACE_COMPONENT_TYPE_TRIANGLE_MESH_META)) { + // WARN_PRINT("Found component with XR_SPACE_COMPONENT_TYPE_TRIANGLE_MESH_META"); + // } +} + +void OpenXRFbSceneExtensionWrapper::_bind_methods() {} diff --git a/godotopenxrmeta/src/main/cpp/scene/openxr_fb_scene_extension_wrapper.h b/godotopenxrmeta/src/main/cpp/scene/openxr_fb_scene_extension_wrapper.h new file mode 100644 index 00000000..88436dc6 --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/scene/openxr_fb_scene_extension_wrapper.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include + +#include "openxr/fb_spatial_entity.h" +#include "openxr/fb_spatial_entity_query.h" +#include "openxr/fb_spatial_entity_container.h" +#include "openxr/fb_scene.h" +#include "openxr/fb_scene_capture.h" + +#include "scene/xr_scene_object.h" +#include "utils/xr_godot_utils.h" + +namespace godot { + +typedef std::function SceneCaptureCallback_t; +typedef std::function)> QueryRoomCallback_t; + +/** + * A generic scene model provider, which can use OpenXR or fake / injected data + */ +class IXrSceneProvider { +public: + // Requests that a scene capture session begins. Returns true on success, false on error + virtual bool request_scene_capture(SceneCaptureCallback_t callback) = 0; + // Queries available XrSpaces that represent a room + virtual bool query_room(QueryRoomCallback_t callback) = 0; + // Populates an XrSpaceLocation for the XrSpace, based on play space + virtual void locate_space(XrSpace space, XrSpaceLocation* location) = 0; +}; + +// An OpenXR extension wrapper for the collection of scene related OpenXR extensions from Meta +class OpenXRFbSceneExtensionWrapper : public OpenXRExtensionWrapperExtension, public IXrSceneProvider { + GDCLASS(OpenXRFbSceneExtensionWrapper, OpenXRExtensionWrapperExtension) +public: + static OpenXRFbSceneExtensionWrapper *get_singleton(); + OpenXRFbSceneExtensionWrapper(); + virtual ~OpenXRFbSceneExtensionWrapper() override; + + // OpenXRExtensionWrapper + void _on_instance_created(uint64_t instance) override; + Dictionary _get_requested_extensions() override; + bool _on_event_polled(const void *event) override; + + // IXrSceneProvider + bool request_scene_capture(SceneCaptureCallback_t callback) override; + bool query_room(QueryRoomCallback_t callback) override; + void locate_space(XrSpace space, XrSpaceLocation* location) override; + +protected: + static void _bind_methods(); + +private: + static OpenXRFbSceneExtensionWrapper *singleton; + + void on_scene_capture_complete(const XrEventDataSceneCaptureCompleteFB* event); + bool on_space_query_results(const XrEventDataSpaceQueryResultsAvailableFB* event); + void on_space_query_complete(const XrEventDataSpaceQueryCompleteFB* event); + + // Helper for on_space_query_complete + void try_adding_output_for_uuid(XrUuidEXT uuid, XrAsyncRequestIdFB requestId, std::vector& objects); + + bool is_component_supported(const XrSpace space, XrSpaceComponentTypeFB type); + bool is_component_enabled(const XrSpace space, XrSpaceComponentTypeFB type); + std::optional set_component_enabled(const XrSpace space, XrSpaceComponentTypeFB type, bool status); + std::optional get_semantic_labels(const XrSpace space); + void get_shapes(const XrSpace space, XrSceneObjectInternal& object); + + std::map sceneCaptureCallbacks_; + std::map queryRoomCallbacks_; + std::map> results_; + + // XR_FB_spatial_entity + static PFN_xrCreateSpatialAnchorFB xrCreateSpatialAnchorFB; + static PFN_xrGetSpaceUuidFB xrGetSpaceUuidFB; + static PFN_xrEnumerateSpaceSupportedComponentsFB xrEnumerateSpaceSupportedComponentsFB; + static PFN_xrSetSpaceComponentStatusFB xrSetSpaceComponentStatusFB; + static PFN_xrGetSpaceComponentStatusFB xrGetSpaceComponentStatusFB; + static PFN_xrLocateSpace xrLocateSpace; + + // XR_FB_spatial_entity_query + static PFN_xrQuerySpacesFB xrQuerySpacesFB; + static PFN_xrRetrieveSpaceQueryResultsFB xrRetrieveSpaceQueryResultsFB; + + // XR_FB_spatial_entity_container + static PFN_xrGetSpaceContainerFB xrGetSpaceContainerFB; + + // XR_FB_scene + static PFN_xrGetSpaceBoundingBox2DFB xrGetSpaceBoundingBox2DFB; + static PFN_xrGetSpaceBoundingBox3DFB xrGetSpaceBoundingBox3DFB; + static PFN_xrGetSpaceSemanticLabelsFB xrGetSpaceSemanticLabelsFB; + static PFN_xrGetSpaceBoundary2DFB xrGetSpaceBoundary2DFB; + static PFN_xrGetSpaceRoomLayoutFB xrGetSpaceRoomLayoutFB; + + // XR_FB_scene_capture + static PFN_xrRequestSceneCaptureFB xrRequestSceneCaptureFB; +}; + +} // namespace godot diff --git a/godotopenxrmeta/src/main/cpp/scene/scene_model_provider.cpp b/godotopenxrmeta/src/main/cpp/scene/scene_model_provider.cpp new file mode 100644 index 00000000..b3688b03 --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/scene/scene_model_provider.cpp @@ -0,0 +1,68 @@ +#include "scene_model_provider.h" + +#include +#include +#include "xr_scene_provider_fake.h" + +using namespace godot; + +SceneModelProvider::SceneModelProvider() { + scene_provider = OpenXRFbSceneExtensionWrapper::get_singleton(); +} + +void SceneModelProvider::set_simulated(bool simulated) { + if (simulated) { + scene_provider = memnew(XrSceneProviderFake); + } else { + scene_provider = OpenXRFbSceneExtensionWrapper::get_singleton(); + } +} + +bool SceneModelProvider::request_scene_capture(const Callable &callback) { + if (sceneCaptureCallback_ != std::nullopt) { + WARN_PRINT("Already capturing!"); + return false; + } + sceneCaptureCallback_ = Callable(callback); + + return scene_provider->request_scene_capture([=] { + sceneCaptureCallback_->call(); + sceneCaptureCallback_.reset(); + }); +} + +bool SceneModelProvider::query_room(const Callable &callback) { + if (queryRoomCallback_ != std::nullopt) { + WARN_PRINT("Already querying!"); + return false; + } + queryRoomCallback_ = Callable(callback); + + return scene_provider->query_room([=](std::vector results) { + std::string val = "query_room complete with result count: " + std::to_string(results.size()); + WARN_PRINT(String(val.c_str())); + sceneObjects_ = results; + WorkerThreadPool::get_singleton()->add_task(Callable(*queryRoomCallback_)); + queryRoomCallback_.reset(); + }); +} + +void SceneModelProvider::load_xr_scene_object(int index, Ref object) { + if (index >= 0 && index < sceneObjects_.size()) { + object->init(sceneObjects_[index], scene_provider); + } else { + WARN_PRINT("Index out of bounds"); + } +}; + +int SceneModelProvider::get_xr_scene_object_count() { + return sceneObjects_.size(); +}; + +void SceneModelProvider::_bind_methods() { + ClassDB::bind_method(D_METHOD("request_scene_capture"), &SceneModelProvider::request_scene_capture); + ClassDB::bind_method(D_METHOD("query_room"), &SceneModelProvider::query_room); + ClassDB::bind_method(D_METHOD("load_xr_scene_object"), &SceneModelProvider::load_xr_scene_object); + ClassDB::bind_method(D_METHOD("get_xr_scene_object_count"), &SceneModelProvider::get_xr_scene_object_count); + ClassDB::bind_method(D_METHOD("set_simulated"), &SceneModelProvider::set_simulated); +} diff --git a/godotopenxrmeta/src/main/cpp/scene/scene_model_provider.h b/godotopenxrmeta/src/main/cpp/scene/scene_model_provider.h new file mode 100644 index 00000000..1e7e8b5d --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/scene/scene_model_provider.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include +#include + +#include "openxr_fb_scene_extension_wrapper.h" + +namespace godot { + +/** + * A real scene model provider, backed by OpenXR. Exposed to GDScript + */ +class SceneModelProvider : public RefCounted { + GDCLASS(SceneModelProvider, RefCounted) + +public: + SceneModelProvider(); + bool request_scene_capture(const Callable &callback); + bool query_room(const Callable &callback); + void set_simulated(bool simulated); + void load_xr_scene_object(int index, Ref); + int get_xr_scene_object_count(); + +protected: + static void _bind_methods(); + +private: + IXrSceneProvider* scene_provider; + std::vector sceneObjects_; + std::optional sceneCaptureCallback_; + std::optional queryRoomCallback_; +}; + +} diff --git a/godotopenxrmeta/src/main/cpp/scene/xr_scene_object.cpp b/godotopenxrmeta/src/main/cpp/scene/xr_scene_object.cpp new file mode 100644 index 00000000..50a4f204 --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/scene/xr_scene_object.cpp @@ -0,0 +1,83 @@ +#include "xr_scene_object.h" + +#include + +#include "utils/xr_godot_utils.h" +#include "openxr_fb_scene_extension_wrapper.h" + +using namespace godot; + +void XrSceneObject::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_id"), &XrSceneObject::get_id); + ClassDB::bind_method(D_METHOD("get_label"), &XrSceneObject::get_label); + ClassDB::bind_method(D_METHOD("update_transform"), &XrSceneObject::update_transform); + ClassDB::bind_method(D_METHOD("transform_valid"), &XrSceneObject::transform_valid); + ClassDB::bind_method(D_METHOD("get_transform"), &XrSceneObject::get_transform); + ClassDB::bind_method(D_METHOD("get_bounding_box_2d"), &XrSceneObject::get_bounding_box_2d); +} + +XrSceneObject::XrSceneObject() {} +XrSceneObject::~XrSceneObject() {} + +void XrSceneObject::init(XrSceneObjectInternal state, IXrSceneProvider* provider) { + state_ = state; + provider_ = provider; +} + +String XrSceneObject::get_id() { + return String(XrGodotUtils::uuidToString(state_.uuid).c_str()); +} + +String XrSceneObject::get_label() { + if (state_.label != std::nullopt) { + return String(state_.label->c_str()); + } + return String(); +} + +void XrSceneObject::update_transform() { + // Reset the input structs + velocity_ = { + XR_TYPE_SPACE_VELOCITY, // type + nullptr, // next + 0, // velocityFlags + { 0.0, 0.0, 0.0 }, // linearVelocity + { 0.0, 0.0, 0.0 } // angularVelocity + }; + + location_ = { + XR_TYPE_SPACE_LOCATION, // type + &velocity_, // next + 0, // locationFlags + { + { 0.0, 0.0, 0.0, 0.0 }, // orientation + { 0.0, 0.0, 0.0 } // position + } // pose + }; + provider_->locate_space(state_.space, &location_); +} + +bool XrSceneObject::transform_valid() { + return location_.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT; +} + +Transform3D XrSceneObject::get_transform() { + // Compute a Transform3D from the space + const XrPosef &p_pose = location_.pose; + Quaternion q(p_pose.orientation.x, p_pose.orientation.y, p_pose.orientation.z, p_pose.orientation.w); + Basis basis(q); + Vector3 origin(p_pose.position.x, p_pose.position.y, p_pose.position.z); + + return Transform3D(basis, origin); +} + +Rect2 XrSceneObject::get_bounding_box_2d() { + if (state_.boundingBox2D == std::nullopt) { + return Rect2(); + } + + return Rect2( + {state_.boundingBox2D->offset.x, state_.boundingBox2D->offset.y}, + {state_.boundingBox2D->extent.width, state_.boundingBox2D->extent.height} + ); +} diff --git a/godotopenxrmeta/src/main/cpp/scene/xr_scene_object.h b/godotopenxrmeta/src/main/cpp/scene/xr_scene_object.h new file mode 100644 index 00000000..b7a6f937 --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/scene/xr_scene_object.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include + +#include +#include "openxr/fb_spatial_entity.h" +#include "openxr/fb_scene.h" + +namespace godot { + +class IXrSceneProvider; + +struct XrSceneObjectInternal { + XrUuidEXT uuid; + XrSpace space; + std::optional label; + + // Vertices and lines on a plane, probably use this for a floor / ceiling as they are irregularly shaped + // We store a std::vector instead of XrBoundary2DFB to own the vertex memory + std::optional> boundary2D; + // A rectangle containing the whole thing, perfect for desks / tables / play surfaces + std::optional boundingBox2D; + // 3D box for the whole thing, better for obstacles and other objects not used as a surface + std::optional boundingBox3D; +}; + +class XrSceneObject : public RefCounted { + GDCLASS(XrSceneObject, RefCounted) + +public: + XrSceneObject(); + ~XrSceneObject(); + + void init(XrSceneObjectInternal state, IXrSceneProvider* provider); + void update_transform(); + + String get_id(); + String get_label(); + + bool transform_valid(); + Transform3D get_transform(); + + Rect2 get_bounding_box_2d(); + +protected: + static void _bind_methods(); + +private: + IXrSceneProvider* provider_; + XrSceneObjectInternal state_; + + XrSpaceVelocity velocity_; + XrSpaceLocation location_; +}; + +} // namespace godot diff --git a/godotopenxrmeta/src/main/cpp/scene/xr_scene_provider_fake.cpp b/godotopenxrmeta/src/main/cpp/scene/xr_scene_provider_fake.cpp new file mode 100644 index 00000000..f5e7de10 --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/scene/xr_scene_provider_fake.cpp @@ -0,0 +1,51 @@ +#include "xr_scene_provider_fake.h" + +using namespace godot; + +namespace { + // struct XrSceneObjectInternal { + // XrUuidEXT uuid; + // XrSpace space; + // }; + + static const std::vector objects = { + XrSceneObjectInternal { // Fake 0 + {{0xFF,0xEE,0xDD,0xCC,0xBB,0xAA,0xAA,0xBB,0xCC,0xDD,0xEE,0xFF,0xFF,0xEE,0xDD,0x01}}, // UUID + (XrSpace) 0, // XrSpace (Index into poses) + "CEILING", // label + }, + XrSceneObjectInternal { // Fake 1 + {{0xFF,0xEE,0xDD,0xCC,0xBB,0xAA,0xAA,0xBB,0xCC,0xDD,0xEE,0xFF,0xFF,0xEE,0xDD,0x02}}, // UUID + (XrSpace) 1, // XrSpace (Index into poses) + "FLOOR", // label + }, + }; + + static const std::vector poses = { + { // Fake 0 + {0.0, 0.0, 0.0, 1.0}, + {1.0, 1.5, -2.0}, + }, + { // Fake 1 + {0.0, 0.0, 0.0, 1.0}, + {-1.0, 1.5, -2.0}, + }, + }; +} // anonymous namespace + +bool XrSceneProviderFake::request_scene_capture(SceneCaptureCallback_t callback) { + // Immediately run the callback. It runs the callable async using Godot's thread pool + callback(); + return true; +} + +bool XrSceneProviderFake::query_room(QueryRoomCallback_t callback) { + // Immediately run the callback. It runs the callable async using Godot's thread pool + callback(objects); + return true; +} + +void XrSceneProviderFake::locate_space(XrSpace space, XrSpaceLocation* location) { + location->locationFlags = XR_SPACE_LOCATION_POSITION_VALID_BIT; + location->pose = poses[(int64_t) space]; +} diff --git a/godotopenxrmeta/src/main/cpp/scene/xr_scene_provider_fake.h b/godotopenxrmeta/src/main/cpp/scene/xr_scene_provider_fake.h new file mode 100644 index 00000000..69a4043d --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/scene/xr_scene_provider_fake.h @@ -0,0 +1,15 @@ +#include "openxr_fb_scene_extension_wrapper.h" + +namespace godot { + +/** + * A fake scene model provider, to use when OpenXR is not available + */ +class XrSceneProviderFake : public IXrSceneProvider { +public: + bool request_scene_capture(SceneCaptureCallback_t callback) override; + bool query_room(QueryRoomCallback_t callback) override; + void locate_space(XrSpace space, XrSpaceLocation* location) override; +}; + +} // namespace godot \ No newline at end of file diff --git a/godotopenxrmeta/src/main/cpp/utils/xr_godot_utils.cpp b/godotopenxrmeta/src/main/cpp/utils/xr_godot_utils.cpp new file mode 100644 index 00000000..74e2e0d7 --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/utils/xr_godot_utils.cpp @@ -0,0 +1,29 @@ +#include "xr_godot_utils.h" + +#include +#include +#include + +using namespace godot; + +bool XrGodotUtils::isUuidValid(const XrUuidEXT& uuid) { + for (int i = 0; i < XR_UUID_SIZE_EXT; ++i) { + if (uuid.data[i] > 0) { + return true; + } + } + return false; +} + +std::string XrGodotUtils::hexStr(const uint8_t *data, int len) { + std::stringstream ss; + ss << std::hex; + for(int i = 0; i < len; i++) { + ss << std::setw(2) << std::setfill('0') << (int)data[i]; + } + return ss.str(); +} + +std::string XrGodotUtils::uuidToString(XrUuidEXT uuid) { + return hexStr(uuid.data, XR_UUID_SIZE_EXT); +} diff --git a/godotopenxrmeta/src/main/cpp/utils/xr_godot_utils.h b/godotopenxrmeta/src/main/cpp/utils/xr_godot_utils.h new file mode 100644 index 00000000..d5588f41 --- /dev/null +++ b/godotopenxrmeta/src/main/cpp/utils/xr_godot_utils.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include + +namespace godot { + +class XrGodotUtils { + public: + static bool isUuidValid(const XrUuidEXT& uuid); + static std::string hexStr(const uint8_t *data, int len); + static std::string uuidToString(XrUuidEXT uuid); +}; + +struct XrUuidExtCmp { + bool operator()(const XrUuidEXT& a, const XrUuidEXT& b) const { + return XrGodotUtils::uuidToString(a) < XrGodotUtils::uuidToString(b); + } +}; + +} // namespace godot