From 77ecf91008af82150ee1eca2ed861fcd54c1bc6d Mon Sep 17 00:00:00 2001 From: stillmant Date: Mon, 24 Nov 2025 00:01:12 -0800 Subject: [PATCH] Expose XrSpace selection and eye visibility controls for composition layers --- .../doc_classes/OpenXRCompositionLayer.xml | 13 +++ .../openxr_composition_layer_extension.cpp | 93 +++++++++++++++++-- .../openxr_composition_layer_extension.h | 21 +++++ modules/openxr/openxr_api.h | 1 + .../openxr/scene/openxr_composition_layer.cpp | 47 +++++++++- .../openxr/scene/openxr_composition_layer.h | 12 +++ 6 files changed, 176 insertions(+), 11 deletions(-) diff --git a/modules/openxr/doc_classes/OpenXRCompositionLayer.xml b/modules/openxr/doc_classes/OpenXRCompositionLayer.xml index 8921f8edcda..dcee4eb358e 100644 --- a/modules/openxr/doc_classes/OpenXRCompositionLayer.xml +++ b/modules/openxr/doc_classes/OpenXRCompositionLayer.xml @@ -46,6 +46,10 @@ Enables a technique called "hole punching", which allows putting the composition layer behind the main projection layer (i.e. setting [member sort_order] to a negative value) while "punching a hole" through everything rendered by Godot so that the layer is still visible. This can be used to create the illusion that the composition layer exists in the same 3D space as everything rendered by Godot, allowing objects to appear to pass both behind or in front of the composition layer. + + The eye(s) the composition layer is visible to. + [b]Note:[/b] Not all composition layer types or runtimes support restricting visibility to a single eye. + The [SubViewport] to render on the composition layer. @@ -161,5 +165,14 @@ Maps a color channel to the value of one. + + The layer is visible to both the left and right eyes. + + + The layer is visible only to the left eye. + + + The layer is visible only to the right eye. + diff --git a/modules/openxr/extensions/openxr_composition_layer_extension.cpp b/modules/openxr/extensions/openxr_composition_layer_extension.cpp index ad9bdf207b9..9851de78938 100644 --- a/modules/openxr/extensions/openxr_composition_layer_extension.cpp +++ b/modules/openxr/extensions/openxr_composition_layer_extension.cpp @@ -280,25 +280,42 @@ void OpenXRCompositionLayerExtension::CompositionLayer::set_alpha_blend(bool p_a } void OpenXRCompositionLayerExtension::CompositionLayer::set_transform(const Transform3D &p_transform) { - Transform3D reference_frame = XRServer::get_singleton()->get_reference_frame(); - Transform3D transform = reference_frame.inverse() * p_transform; - Quaternion quat(transform.basis.orthonormalized()); + Transform3D xf; + + if (pose_space == POSE_HEAD_LOCKED) { + // Local transform relative to the head/camera. + xf = p_transform; + } else { + // Relative to the XROrigin3D, so we need to apply the reference frame. + Transform3D reference_frame = XRServer::get_singleton()->get_reference_frame(); + xf = reference_frame.inverse() * p_transform; + } + + Quaternion quat(xf.basis.orthonormalized()); + + // Prevent invalid quaternion + if (Math::is_zero_approx(quat.length())) { + quat = Quaternion(); // identity quaternion + } XrPosef pose = { { (float)quat.x, (float)quat.y, (float)quat.z, (float)quat.w }, - { (float)transform.origin.x, (float)transform.origin.y, (float)transform.origin.z } + { (float)xf.origin.x, (float)xf.origin.y, (float)xf.origin.z } }; switch (composition_layer.type) { case XR_TYPE_COMPOSITION_LAYER_QUAD: { composition_layer_quad.pose = pose; } break; + case XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR: { composition_layer_cylinder.pose = pose; } break; + case XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR: { composition_layer_equirect.pose = pose; } break; + default: { ERR_PRINT(vformat("Cannot set transform on unsupported composition layer type: %s", composition_layer.type)); } @@ -365,6 +382,50 @@ void OpenXRCompositionLayerExtension::CompositionLayer::set_border_color(const C swapchain_state_is_dirty = true; } +void OpenXRCompositionLayerExtension::CompositionLayer::set_pose_space(PoseSpace p_pose_space) { + pose_space = p_pose_space; +} + +void OpenXRCompositionLayerExtension::CompositionLayer::set_eye_visibility(EyeVisibility p_eye_visibility) { + XrEyeVisibility eye_visibility; + + switch (p_eye_visibility) { + case EYE_VISIBILITY_BOTH: { + eye_visibility = XR_EYE_VISIBILITY_BOTH; + } break; + + case EYE_VISIBILITY_LEFT: { + eye_visibility = XR_EYE_VISIBILITY_LEFT; + } break; + + case EYE_VISIBILITY_RIGHT: { + eye_visibility = XR_EYE_VISIBILITY_RIGHT; + } break; + + default: { + eye_visibility = XR_EYE_VISIBILITY_BOTH; + } + } + + switch (composition_layer.type) { + case XR_TYPE_COMPOSITION_LAYER_QUAD: { + composition_layer_quad.eyeVisibility = eye_visibility; + } break; + + case XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR: { + composition_layer_cylinder.eyeVisibility = eye_visibility; + } break; + + case XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR: { + composition_layer_equirect.eyeVisibility = eye_visibility; + } break; + + default: { + ERR_PRINT(vformat("%s does not support setting eye visibility.", composition_layer.type)); + } + } +} + void OpenXRCompositionLayerExtension::CompositionLayer::set_quad_size(const Size2 &p_size) { ERR_FAIL_COND(composition_layer.type != XR_TYPE_COMPOSITION_LAYER_QUAD); composition_layer_quad.size = { (float)p_size.x, (float)p_size.y }; @@ -476,26 +537,42 @@ XrCompositionLayerBaseHeader *OpenXRCompositionLayerExtension::CompositionLayer: return nullptr; } + // Update the layer's reference space + switch (pose_space) { + case POSE_WORLD_LOCKED: { + layer_reference_space = openxr_api->get_play_space(); + break; + } + + case POSE_HEAD_LOCKED: { + layer_reference_space = openxr_api->get_view_space(); + break; + } + default: { + return nullptr; + } + } + // Update the layer struct for the swapchain. switch (composition_layer.type) { case XR_TYPE_COMPOSITION_LAYER_QUAD: { - composition_layer_quad.space = openxr_api->get_play_space(); + composition_layer_quad.space = layer_reference_space; composition_layer_quad.subImage = subimage; } break; case XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR: { - composition_layer_cylinder.space = openxr_api->get_play_space(); + composition_layer_cylinder.space = layer_reference_space; composition_layer_cylinder.subImage = subimage; } break; case XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR: { - composition_layer_equirect.space = openxr_api->get_play_space(); + composition_layer_equirect.space = layer_reference_space; composition_layer_equirect.subImage = subimage; } break; default: { return nullptr; - } break; + } } if (extension_property_values_changed) { diff --git a/modules/openxr/extensions/openxr_composition_layer_extension.h b/modules/openxr/extensions/openxr_composition_layer_extension.h index 770080ad85c..fd5f54ff1d8 100644 --- a/modules/openxr/extensions/openxr_composition_layer_extension.h +++ b/modules/openxr/extensions/openxr_composition_layer_extension.h @@ -107,6 +107,18 @@ public: SWIZZLE_ONE, }; + // Must be identical to EyeVisibility enum definition in OpenXRCompositionLayer. + enum EyeVisibility { + EYE_VISIBILITY_BOTH, + EYE_VISIBILITY_LEFT, + EYE_VISIBILITY_RIGHT, + }; + + enum PoseSpace { + POSE_WORLD_LOCKED, + POSE_HEAD_LOCKED, + }; + struct SwapchainState { Filter min_filter = Filter::FILTER_LINEAR; Filter mag_filter = Filter::FILTER_LINEAR; @@ -162,6 +174,8 @@ public: OPENXR_LAYER_FUNC1(set_alpha_swizzle, Swizzle); OPENXR_LAYER_FUNC1(set_max_anisotropy, float); OPENXR_LAYER_FUNC1(set_border_color, const Color &); + OPENXR_LAYER_FUNC1(set_pose_space, PoseSpace); + OPENXR_LAYER_FUNC1(set_eye_visibility, EyeVisibility); OPENXR_LAYER_FUNC1(set_quad_size, const Size2 &); @@ -225,6 +239,9 @@ private: } android_surface; #endif + PoseSpace pose_space = POSE_WORLD_LOCKED; + XrSpace layer_reference_space = XR_NULL_HANDLE; + bool use_android_surface = false; bool protected_content = false; Size2i swapchain_size; @@ -252,6 +269,8 @@ private: void set_alpha_swizzle(Swizzle p_mode); void set_max_anisotropy(float p_value); void set_border_color(const Color &p_color); + void set_pose_space(PoseSpace p_pose_space); + void set_eye_visibility(EyeVisibility p_eye_visibility); void set_quad_size(const Size2 &p_size); @@ -290,6 +309,8 @@ VARIANT_ENUM_CAST(OpenXRCompositionLayerExtension::Filter); VARIANT_ENUM_CAST(OpenXRCompositionLayerExtension::MipmapMode); VARIANT_ENUM_CAST(OpenXRCompositionLayerExtension::Wrap); VARIANT_ENUM_CAST(OpenXRCompositionLayerExtension::Swizzle); +VARIANT_ENUM_CAST(OpenXRCompositionLayerExtension::PoseSpace); +VARIANT_ENUM_CAST(OpenXRCompositionLayerExtension::EyeVisibility); #undef OPENXR_LAYER_FUNC1 #undef OPENXR_LAYER_FUNC2 diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index 043abfff423..e57db870fe3 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -504,6 +504,7 @@ public: void finish(); _FORCE_INLINE_ XrSpace get_play_space() const { return play_space; } + _FORCE_INLINE_ XrSpace get_view_space() const { return view_space; } _FORCE_INLINE_ XrTime get_predicted_display_time() { return frame_state.predictedDisplayTime; } _FORCE_INLINE_ XrTime get_next_frame_time() { return frame_state.predictedDisplayTime + frame_state.predictedDisplayPeriod; } _FORCE_INLINE_ bool can_render() { diff --git a/modules/openxr/scene/openxr_composition_layer.cpp b/modules/openxr/scene/openxr_composition_layer.cpp index a6fe5ae845d..1c5266c4936 100644 --- a/modules/openxr/scene/openxr_composition_layer.cpp +++ b/modules/openxr/scene/openxr_composition_layer.cpp @@ -146,6 +146,9 @@ void OpenXRCompositionLayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_border_color", "color"), &OpenXRCompositionLayer::set_border_color); ClassDB::bind_method(D_METHOD("get_border_color"), &OpenXRCompositionLayer::get_border_color); + ClassDB::bind_method(D_METHOD("set_eye_visibility", "eye_visibility"), &OpenXRCompositionLayer::set_eye_visibility); + ClassDB::bind_method(D_METHOD("get_eye_visibility"), &OpenXRCompositionLayer::get_eye_visibility); + ClassDB::bind_method(D_METHOD("intersects_ray", "origin", "direction"), &OpenXRCompositionLayer::intersects_ray); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "layer_viewport", PROPERTY_HINT_NODE_TYPE, "SubViewport"), "set_layer_viewport", "get_layer_viewport"); @@ -155,6 +158,7 @@ void OpenXRCompositionLayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "sort_order", PROPERTY_HINT_NONE, ""), "set_sort_order", "get_sort_order"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "alpha_blend", PROPERTY_HINT_NONE, ""), "set_alpha_blend", "get_alpha_blend"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enable_hole_punch", PROPERTY_HINT_NONE, ""), "set_enable_hole_punch", "get_enable_hole_punch"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "eye_visibility", PROPERTY_HINT_ENUM, "Both,Left,Right"), "set_eye_visibility", "get_eye_visibility"); ADD_GROUP("Swapchain State", "swapchain_state_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "swapchain_state_min_filter", PROPERTY_HINT_ENUM, "Nearest,Linear,Cubic"), "set_min_filter", "get_min_filter"); @@ -190,6 +194,10 @@ void OpenXRCompositionLayer::_bind_methods() { BIND_ENUM_CONSTANT(SWIZZLE_ALPHA); BIND_ENUM_CONSTANT(SWIZZLE_ZERO); BIND_ENUM_CONSTANT(SWIZZLE_ONE); + + BIND_ENUM_CONSTANT(EYE_VISIBILITY_BOTH); + BIND_ENUM_CONSTANT(EYE_VISIBILITY_LEFT); + BIND_ENUM_CONSTANT(EYE_VISIBILITY_RIGHT); } bool OpenXRCompositionLayer::_should_use_fallback_node() { @@ -270,6 +278,18 @@ void OpenXRCompositionLayer::_on_openxr_session_stopping() { void OpenXRCompositionLayer::update_transform() { if (composition_layer_extension) { + bool parent_is_xr_camera = Object::cast_to(get_parent()) != nullptr; + OpenXRCompositionLayerExtension::PoseSpace new_pose_space; + + // Automatically set the PoseSpace to POSE_HEAD_LOCKED if layer is a child of XRCamera3D. + if (parent_is_xr_camera) { + new_pose_space = OpenXRCompositionLayerExtension::PoseSpace::POSE_HEAD_LOCKED; + } else { + new_pose_space = OpenXRCompositionLayerExtension::PoseSpace::POSE_WORLD_LOCKED; + } + + // Pose space must be set first, as composition_layer_set_transform() depends on it. + composition_layer_extension->composition_layer_set_pose_space(composition_layer, new_pose_space); composition_layer_extension->composition_layer_set_transform(composition_layer, get_transform()); } } @@ -606,6 +626,20 @@ Color OpenXRCompositionLayer::get_border_color() const { return border_color; } +void OpenXRCompositionLayer::set_eye_visibility(EyeVisibility p_eye_visibility) { + if (eye_visibility == p_eye_visibility) { + return; + } + eye_visibility = p_eye_visibility; + if (composition_layer_extension) { + composition_layer_extension->composition_layer_set_eye_visibility(composition_layer, (OpenXRCompositionLayerExtension::EyeVisibility)p_eye_visibility); + } +} + +OpenXRCompositionLayer::EyeVisibility OpenXRCompositionLayer::get_eye_visibility() const { + return eye_visibility; +} + Ref OpenXRCompositionLayer::get_android_surface() { if (composition_layer_extension) { return composition_layer_extension->composition_layer_get_android_surface(composition_layer); @@ -678,6 +712,7 @@ void OpenXRCompositionLayer::_notification(int p_what) { if (is_natively_supported() && openxr_session_running && is_inside_tree()) { if (is_visible()) { _setup_composition_layer(); + update_transform(); } else { _clear_composition_layer(); } @@ -694,6 +729,10 @@ void OpenXRCompositionLayer::_notification(int p_what) { } else if (openxr_session_running && is_visible()) { _setup_composition_layer(); } + update_transform(); + } break; + case NOTIFICATION_PARENTED: { + update_transform(); } break; case NOTIFICATION_EXIT_TREE: { // This will clean up existing resources. @@ -760,9 +799,11 @@ PackedStringArray OpenXRCompositionLayer::get_configuration_warnings() const { PackedStringArray warnings = Node3D::get_configuration_warnings(); if (is_visible() && is_inside_tree()) { - XROrigin3D *origin = Object::cast_to(get_parent()); - if (origin == nullptr) { - warnings.push_back(RTR("OpenXR composition layers must have an XROrigin3D node as their parent.")); + XROrigin3D *xr_origin = Object::cast_to(get_parent()); + XRCamera3D *xr_camera = Object::cast_to(get_parent()); + + if (xr_origin == nullptr && xr_camera == nullptr) { + warnings.push_back(RTR("OpenXR composition layers must have have either an XROrigin3D or XRCamera3D node as their parent.")); } } diff --git a/modules/openxr/scene/openxr_composition_layer.h b/modules/openxr/scene/openxr_composition_layer.h index 1af849a9153..aac96fecb8b 100644 --- a/modules/openxr/scene/openxr_composition_layer.h +++ b/modules/openxr/scene/openxr_composition_layer.h @@ -78,6 +78,13 @@ public: SWIZZLE_ONE, }; + // Must be identical to EyeVisibility enum definition in OpenXRCompositionLayerExtension. + enum EyeVisibility { + EYE_VISIBILITY_BOTH, + EYE_VISIBILITY_LEFT, + EYE_VISIBILITY_RIGHT, + }; + protected: RID composition_layer; @@ -106,6 +113,7 @@ private: Swizzle alpha_swizzle = SWIZZLE_ALPHA; float max_anisotropy = 1.0; Color border_color = { 0.0, 0.0, 0.0, 0.0 }; + EyeVisibility eye_visibility = EYE_VISIBILITY_BOTH; bool _should_use_fallback_node(); void _create_fallback_node(); @@ -203,6 +211,9 @@ public: void set_border_color(const Color &p_color); Color get_border_color() const; + void set_eye_visibility(EyeVisibility p_eye_visibility); + EyeVisibility get_eye_visibility() const; + virtual PackedStringArray get_configuration_warnings() const override; virtual Vector2 intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const; @@ -214,3 +225,4 @@ VARIANT_ENUM_CAST(OpenXRCompositionLayer::Filter) VARIANT_ENUM_CAST(OpenXRCompositionLayer::MipmapMode) VARIANT_ENUM_CAST(OpenXRCompositionLayer::Wrap) VARIANT_ENUM_CAST(OpenXRCompositionLayer::Swizzle) +VARIANT_ENUM_CAST(OpenXRCompositionLayer::EyeVisibility)