Expose XrSpace selection and eye visibility controls for composition layers

This commit is contained in:
stillmant
2025-11-24 00:01:12 -08:00
parent 235a32ad11
commit 77ecf91008
6 changed files with 176 additions and 11 deletions

View File

@@ -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.
</member>
<member name="eye_visibility" type="int" setter="set_eye_visibility" getter="get_eye_visibility" enum="OpenXRCompositionLayer.EyeVisibility" default="0">
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.
</member>
<member name="layer_viewport" type="SubViewport" setter="set_layer_viewport" getter="get_layer_viewport">
The [SubViewport] to render on the composition layer.
</member>
@@ -161,5 +165,14 @@
<constant name="SWIZZLE_ONE" value="5" enum="Swizzle">
Maps a color channel to the value of one.
</constant>
<constant name="EYE_VISIBILITY_BOTH" value="0" enum="EyeVisibility">
The layer is visible to both the left and right eyes.
</constant>
<constant name="EYE_VISIBILITY_LEFT" value="1" enum="EyeVisibility">
The layer is visible only to the left eye.
</constant>
<constant name="EYE_VISIBILITY_RIGHT" value="2" enum="EyeVisibility">
The layer is visible only to the right eye.
</constant>
</constants>
</class>

View File

@@ -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) {

View File

@@ -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

View File

@@ -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() {

View File

@@ -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<XRCamera3D>(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<JavaObject> 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<XROrigin3D>(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<XROrigin3D>(get_parent());
XRCamera3D *xr_camera = Object::cast_to<XRCamera3D>(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."));
}
}

View File

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