Skip to content

Conversation

@stillmant
Copy link

@stillmant stillmant commented Nov 24, 2025

Automatically set pose space for OpenXR composition layers and improve transform handling

This PR improves OpenXRCompositionLayer by automatically selecting pose space based on the parent and ensuring robust transform updates.

✔ Automatic Pose Space Selection

  • Parent is XRCamera3D → layer becomes head-locked (POSE_HEAD_LOCKED)
  • Parent is XROrigin3D or other → layer is world-locked (POSE_WORLD_LOCKED)

Removes the previous requirement for layers to be parented under XROrigin3D while maintaining correct OpenXR behavior.

✔ Robust Transform Handling

  • Converts Godot transforms into OpenXR XrPosef with quaternion normalization.
  • HEAD_LOCKED layers: local transform relative to the camera.
  • WORLD_LOCKED layers: global transform converted to play space, correctly handling moved XR origins.
  • _update_pose_space() updates transforms on node movement, re-parenting, or visibility changes.

✔ Eye Visibility

Allows controlling which eye(s) the layer renders in via eye_visibility:

  • Both eyes (default)
  • Left eye only
  • Right eye only

OpenXR refs: XrCompositionLayerBaseHeader, XrSpace, XrEyeVisibility

✅ Summary

  • Automatic head-locking based on parent type
  • Safe transform → OpenXR pose conversion, avoiding invalid pose errors
  • World-locked layers respect moved XR origins
  • Optional eye visibility for finer control

@stillmant stillmant requested review from a team as code owners November 24, 2025 08:08
@BastiaanOlij
Copy link
Contributor

I wonder if we should auto detect the view pose based on the composition layer being a child of an XRCamera3D node, else positioning in the editor would be a little unintuitive.

Other than that, worth supporting I guess.

cc @dsnopek

@stillmant stillmant force-pushed the enable-headlocking-and-individual-eye-rendering-for-composition-layers branch from f73f367 to f9eb5aa Compare November 24, 2025 10:19
@AThousandShips
Copy link
Member

Please also fix the style issues before we go ahead and run CI again

@stillmant stillmant force-pushed the enable-headlocking-and-individual-eye-rendering-for-composition-layers branch from f9eb5aa to a8fe0a5 Compare November 24, 2025 10:42
@brycehutchings
Copy link
Contributor

I wonder if we should auto detect the view pose based on the composition layer being a child of an XRCamera3D node, else positioning in the editor would be a little unintuitive.

Other than that, worth supporting I guess.

cc @dsnopek

Just curious to see if I understand, but if a composition layer is a child of the XR camera, it makes sense to default (or force) to using the XR_REFERENCE_SPACE_TYPE_VIEW space which would cause the pose to be relative to the camera and properly reprojected as head locked. This would mean the pose used by the composition layer should be relative to the camera. I don't think it makes sense to use world-locked with something that is a child of a camera. This might remove the need for explicitly specifying world or head locked entirely.

@m4gr3d
Copy link
Contributor

m4gr3d commented Nov 24, 2025

I wonder if we should auto detect the view pose based on the composition layer being a child of an XRCamera3D node, else positioning in the editor would be a little unintuitive.
Other than that, worth supporting I guess.
cc @dsnopek

Just curious to see if I understand, but if a composition layer is a child of the XR camera, it makes sense to default (or force) to using the XR_REFERENCE_SPACE_TYPE_VIEW space which would cause the pose to be relative to the camera and properly reprojected as head locked. This would mean the pose used by the composition layer should be relative to the camera. I don't think it makes sense to use world-locked with something that is a child of a camera. This might remove the need for explicitly specifying world or head locked entirely.

At the moment composition layers can only be children of the XrOrigin3D node hence why the current approach in the PR. If we do relax that restriction, then yes I agree this would be a good default behavior to have.

@dsnopek
Copy link
Contributor

dsnopek commented Nov 24, 2025

I'm curious about your use case is for eye visibility?

I wonder if we should auto detect the view pose based on the composition layer being a child of an XRCamera3D node

I agree that this would be a better "API" for this, than exposing it as a property - it would be much more intuitive to Godot developers. As it is now, I think it'll be very confusing, because the position in Godot will be relative to the XROrigin3D, but then that will be applied relative to the camera.

@stillmant
Copy link
Author

@BastiaanOlij @dsnopek I like the suggestion to automatically switch to View space when a composition layer is a child of the XR camera — I’ll update the PR accordingly!

Regarding the use case for eye visibility: I’m currently working on a project that streams stereoscopic video from external cameras into head-locked composition layers to provide depth perception to the user. I needed a way to selectively render to each eye's panel or else the panels would cross over into the view of the other eye. I could also see this being useful for asymmetric UI/HUD elements where each eye needs different information. In the past, I’ve worked on XR projects for vision-science research as well, and I believe this feature could support similar experimental setups.

Before implementing this, I found this repo and wanted to build upon that idea so other developers could achieve the same effect more easily, especially since the OpenXR layer structs already support configuring eye visibility.

@stillmant
Copy link
Author

@dsnopek As @m4gr3d points out, to support automatically switching between pose spaces based on the layer’s parent, we would need to remove the following configuration warning:

	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."));
		}
	}

I’m not completely sure what the broader implications of removing this check would be, but I assume it was added for a good reason. Before making any changes here, I wanted to get your thoughts on whether this warning is still necessary or if it could be relaxed to allow the automatic behavior.

@BastiaanOlij
Copy link
Contributor

Just curious to see if I understand, but if a composition layer is a child of the XR camera, it makes sense to default (or force) to using the XR_REFERENCE_SPACE_TYPE_VIEW space which would cause the pose to be relative to the camera and properly reprojected as head locked. This would mean the pose used by the composition layer should be relative to the camera. I don't think it makes sense to use world-locked with something that is a child of a camera. This might remove the need for explicitly specifying world or head locked entirely.

We never use world locked poses with composition layers, they've always been local to the XROrigin3D node, hence that node being the parent.
Making XRCamera3D an alternative parent, the pose will automatically be correct as again the local pose will be set on the composition layer.

@BastiaanOlij
Copy link
Contributor

@dsnopek As @m4gr3d points out, to support automatically switching between pose spaces based on the layer’s parent, we would need to remove the following configuration warning:

	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."));
		}
	}

I’m not completely sure what the broader implications of removing this check would be, but I assume it was added for a good reason. Before making any changes here, I wanted to get your thoughts on whether this warning is still necessary or if it could be relaxed to allow the automatic behavior.

You wouldn't remove the check, just allow the parent to be either XROrigin3D or XRCamera3D node.
Then check in your enter tree notification whether the parent is XROrigin3D (and use get_play_space), or XRCamera3D (and use get_view_space). I guess for this you could keep OpenXRCompositionLayer::set_pose_space but just set the value based on the parent.

@dsnopek
Copy link
Contributor

dsnopek commented Nov 25, 2025

Regarding the use case for eye visibility: I’m currently working on a project that streams stereoscopic video from external cameras into head-locked composition layers to provide depth perception to the user.

Ah, very cool!

You wouldn't remove the check, just allow the parent to be either XROrigin3D or XRCamera3D node.
Then check in your enter tree notification whether the parent is XROrigin3D (and use get_play_space), or XRCamera3D (and use get_view_space).

👍

I guess for this you could keep OpenXRCompositionLayer::set_pose_space but just set the value based on the parent.

Keeping the method is fine, but it shouldn't be exposed to users

@BastiaanOlij
Copy link
Contributor

Keeping the method is fine, but it shouldn't be exposed to users

Oops, no I meant keeping it on the layer struct. I scrolled down too far :) I would remove it from the user facing node all together.
Then in the node you check the parent and set it correctly on the struct.

@stillmant stillmant force-pushed the enable-headlocking-and-individual-eye-rendering-for-composition-layers branch from a8fe0a5 to 49ab436 Compare December 10, 2025 05:33
@stillmant stillmant changed the title Expose XrSpace selection and eye visibility controls for composition layers Expose eye visibility controls for composition layers and set its XrSpace based on parent type Dec 10, 2025
@stillmant stillmant changed the title Expose eye visibility controls for composition layers and set its XrSpace based on parent type Expose eye visibility controls for composition layers and set XrSpace based on parent type Dec 10, 2025
Transform3D reference_frame = XRServer::get_singleton()->get_reference_frame();
Transform3D transform = reference_frame.inverse() * p_transform;
Quaternion quat(transform.basis.orthonormalized());
Transform3D xf;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing to do with your PR but I just realised, the fact that composition layers are positioned with quaternion and position means we're loosing scale.

There was a bug report of that some time ago around world_scale not being applied, and this explains why :)

cc @dsnopek

Copy link
Contributor

@dsnopek dsnopek Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it makes sense to apply scale to composition layers. Or, at least, it's not what I would want in like 99% of circumstances, so I don't think it should happen automatically. Like, in the Alice demo you wouldn't want your settings panel to change size depending on whether your character is big or small - it should be sized based on real world sizes in either case.

To really scale a composition layer you'd need to scale the size of the shape (quad, equirect, cylinder). I suppose we could have a checkbox that, if enabled, would lead to multiplying the shape sizes by the world scale? But I really don't think this will be very commonly used

Copy link
Contributor

@BastiaanOlij BastiaanOlij left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Purely based on code review and earlier discussions, this looks good to me. Probably too late to merge into 4.6 but IMHO suitable for an early merge in 4.7

Copy link
Contributor

@dsnopek dsnopek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

I haven't had a chance to test this yet, but the code looks pretty good to me! I just have a couple nitpicks.

I agree that this should be saved for Godot 4.7

@stillmant stillmant force-pushed the enable-headlocking-and-individual-eye-rendering-for-composition-layers branch from 49ab436 to e9a7446 Compare December 11, 2025 05:18
@m4gr3d m4gr3d modified the milestones: 4.x, 4.7 Dec 11, 2025
@stillmant stillmant force-pushed the enable-headlocking-and-individual-eye-rendering-for-composition-layers branch from e9a7446 to e742d9d Compare December 14, 2025 12:43
Copy link
Contributor

@dsnopek dsnopek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Looks good to me :-)

@@ -686,6 +729,7 @@ void OpenXRCompositionLayer::_notification(int p_what) {
} break;
case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
update_transform();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need the call to update_transform() given that _update_pose_space() also calls composition_layer_extension->composition_layer_set_transform(...).

Following on that thought, do we need to deprecate and remove update_transform() and just use _update_pose_space() instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants