diff --git a/core/math/projection.cpp b/core/math/projection.cpp index e0b8dcf815d0..3a53c1d9f6e8 100644 --- a/core/math/projection.cpp +++ b/core/math/projection.cpp @@ -162,6 +162,118 @@ Projection Projection::create_fit_aabb(const AABB &p_aabb) { return proj; } +Projection Projection::create_combined_projection(const Transform3D &p_center, const Projection &p_projection_a, const Transform3D &p_offset_a, const Projection &p_projection_b, const Transform3D &p_offset_b) { + Vector planes[2]; + Projection proj; + + ///////////////////////////////////////////////////////////////////////////// + // Get some basic information + + // 1. obtain our planes + planes[0] = p_projection_a.get_projection_planes(p_offset_a); + planes[1] = p_projection_b.get_projection_planes(p_offset_b); + + // 2. Intersect horizon, left and right to obtain the combined camera origin. + Plane horizon(Vector3(0.0, 1.0, 0.0), Vector3()); + Vector3 origin; + ERR_FAIL_COND_V_MSG( + !horizon.intersect_3(planes[0][Projection::PLANE_LEFT], planes[1][Projection::PLANE_RIGHT], &origin), proj, "Can't determine camera origin"); + + // 3. figure out our near and far plane, this could use some improvement, we may have our far plane too close like this, not sure if this matters + Vector3 near_center = (planes[0][Projection::PLANE_NEAR].get_center() + planes[1][Projection::PLANE_NEAR].get_center()) * 0.5; + Plane near_plane = Plane(Vector3(0.0, 0.0, -1.0), near_center); + Vector3 far_center = (planes[0][Projection::PLANE_FAR].get_center() + planes[1][Projection::PLANE_FAR].get_center()) * 0.5; + Plane far_plane = Plane(Vector3(0.0, 0.0, -1.0), far_center); + + ///////////////////////////////////////////////////////////////////////////// + // Figure out our top/bottom planes + + // 4. Intersect far and left planes with top planes from both eyes, save the point with highest y as top_left. + Vector3 top_left, other; + ERR_FAIL_COND_V_MSG( + !far_plane.intersect_3(planes[0][Projection::PLANE_LEFT], planes[0][Projection::PLANE_TOP], &top_left), proj, "Can't determine left camera far/left/top vector"); + ERR_FAIL_COND_V_MSG( + !far_plane.intersect_3(planes[1][Projection::PLANE_LEFT], planes[1][Projection::PLANE_TOP], &other), proj, "Can't determine right camera far/left/top vector"); + if (top_left.y < other.y) { + top_left = other; + } + + // 5. Intersect far and left planes with bottom planes from both eyes, save the point with lowest y as bottom_left. + Vector3 bottom_left; + ERR_FAIL_COND_V_MSG( + !far_plane.intersect_3(planes[0][Projection::PLANE_LEFT], planes[0][Projection::PLANE_BOTTOM], &bottom_left), proj, "Can't determine left camera far/left/bottom vector"); + ERR_FAIL_COND_V_MSG( + !far_plane.intersect_3(planes[1][Projection::PLANE_LEFT], planes[1][Projection::PLANE_BOTTOM], &other), proj, "Can't determine right camera far/left/bottom vector"); + if (other.y < bottom_left.y) { + bottom_left = other; + } + + // 6. Intersect far and right planes with top planes from both eyes, save the point with highest y as top_right. + Vector3 top_right; + ERR_FAIL_COND_V_MSG( + !far_plane.intersect_3(planes[0][Projection::PLANE_RIGHT], planes[0][Projection::PLANE_TOP], &top_right), proj, "Can't determine left camera far/right/top vector"); + ERR_FAIL_COND_V_MSG( + !far_plane.intersect_3(planes[1][Projection::PLANE_RIGHT], planes[1][Projection::PLANE_TOP], &other), proj, "Can't determine right camera far/right/top vector"); + if (top_right.y < other.y) { + top_right = other; + } + + // 7. Intersect far and right planes with bottom planes from both eyes, save the point with lowest y as bottom_right. + Vector3 bottom_right; + ERR_FAIL_COND_V_MSG( + !far_plane.intersect_3(planes[0][Projection::PLANE_RIGHT], planes[0][Projection::PLANE_BOTTOM], &bottom_right), proj, "Can't determine left camera far/right/bottom vector"); + ERR_FAIL_COND_V_MSG( + !far_plane.intersect_3(planes[1][Projection::PLANE_RIGHT], planes[1][Projection::PLANE_BOTTOM], &other), proj, "Can't determine right camera far/right/bottom vector"); + if (other.y < bottom_right.y) { + bottom_right = other; + } + + // 8. Create top plane with these points: camera origin, top_left, top_right + Plane top(origin, top_left, top_right); + + // 9. Create bottom plane with these points: camera origin, bottom_left, bottom_right + Plane bottom(origin, bottom_left, bottom_right); + + ///////////////////////////////////////////////////////////////////////////// + // Figure out our near plane points + + // 10. Intersect near plane with bottm/left planes, to obtain min_vec then top/right to obtain max_vec + Vector3 min_vec; + ERR_FAIL_COND_V_MSG( + !near_plane.intersect_3(bottom, planes[0][Projection::PLANE_LEFT], &min_vec), proj, "Can't determine left camera near/left/bottom vector"); + ERR_FAIL_COND_V_MSG( + !near_plane.intersect_3(bottom, planes[1][Projection::PLANE_LEFT], &other), proj, "Can't determine right camera near/left/bottom vector"); + if (other.x < min_vec.x) { + min_vec = other; + } + + Vector3 max_vec; + ERR_FAIL_COND_V_MSG( + !near_plane.intersect_3(top, planes[0][Projection::PLANE_RIGHT], &max_vec), proj, "Can't determine left camera near/right/top vector"); + ERR_FAIL_COND_V_MSG( + !near_plane.intersect_3(top, planes[1][Projection::PLANE_RIGHT], &other), proj, "Can't determine right camera near/right/top vector"); + if (max_vec.x < other.x) { + max_vec = other; + } + + // 11. get x and y from these to obtain left, top, right bottom for the frustum. Get the distance from near plane to camera origin to obtain near, and the distance from the far plane to the camera origin to obtain far. + float z_near = -near_plane.distance_to(origin); + float z_far = -far_plane.distance_to(origin); + + // Safeguard our near and far values + z_near = MAX(z_near, origin.z + 0.01); + z_far = MAX(z_far, z_near + 1.0); + + // 12. Use this to build the combined camera matrix. + proj.set_frustum(min_vec.x, max_vec.x, min_vec.y, max_vec.y, z_near, z_far); + + // 13. Add in offset to origin + Transform3D main_offset(Basis(), -origin); + proj = proj * Projection(main_offset); + + return proj; +} + Projection Projection::perspective_znear_adjusted(real_t p_new_znear) const { Projection proj = *this; proj.adjust_perspective_znear(p_new_znear); @@ -876,6 +988,17 @@ bool Projection::is_orthogonal() const { return columns[2][3] == 0.0; } +bool Projection::is_asymmetrical() const { + // NOTE: This assumes that the matrix is a projection across z-axis + // i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0 + return columns[2][0] != 0.0 || columns[2][1] != 0.0; +} + +bool Projection::is_z_axis_projection() const { + // These need to all be zero for this to be a projection along the z-axis. + return columns[0][1] == 0.0 && columns[1][0] == 0.0 && columns[0][3] == 0.0 && columns[1][3] == 0.0; +} + real_t Projection::get_fov() const { // NOTE: This assumes a rectangular projection plane, i.e. that : // - the matrix is a projection across z-axis (i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0) diff --git a/core/math/projection.h b/core/math/projection.h index 87985c2c6f1e..dbb3111ee49d 100644 --- a/core/math/projection.h +++ b/core/math/projection.h @@ -95,6 +95,7 @@ struct [[nodiscard]] Projection { static Projection create_frustum(real_t p_left, real_t p_right, real_t p_bottom, real_t p_top, real_t p_near, real_t p_far); static Projection create_frustum_aspect(real_t p_size, real_t p_aspect, Vector2 p_offset, real_t p_near, real_t p_far, bool p_flip_fov = false); static Projection create_fit_aabb(const AABB &p_aabb); + static Projection create_combined_projection(const Transform3D &p_center, const Projection &p_projection_a, const Transform3D &p_offset_a, const Projection &p_projection_b, const Transform3D &p_offset_b); Projection perspective_znear_adjusted(real_t p_new_znear) const; Plane get_projection_plane(Planes p_plane) const; Projection flipped_y() const; @@ -109,6 +110,8 @@ struct [[nodiscard]] Projection { real_t get_aspect() const; real_t get_fov() const; bool is_orthogonal() const; + bool is_asymmetrical() const; + bool is_z_axis_projection() const; Vector get_projection_planes(const Transform3D &p_transform) const; diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index 7320a3616ede..f2f2d83a5309 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -2625,6 +2625,8 @@ static void _register_variant_builtin_methods_misc() { bind_method(Projection, get_aspect, sarray(), varray()); bind_method(Projection, get_fov, sarray(), varray()); bind_method(Projection, is_orthogonal, sarray(), varray()); + bind_method(Projection, is_asymmetrical, sarray(), varray()); + bind_method(Projection, is_z_axis_projection, sarray(), varray()); bind_method(Projection, get_viewport_half_extents, sarray(), varray()); bind_method(Projection, get_far_plane_half_extents, sarray(), varray()); diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 23b263a23f28..b9fd8e656e39 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -3540,6 +3540,9 @@ If [code]true[/code], enables the D-pad binding modifier if supported by the XR runtime. + + If [code]true[/code], and [member xr/openxr/view_configuration] is set to [code]Stereo with Foveated Inset[/code], and foveated inset is supported, Godot will add and configure an [OpenXRFoveatedInsetViewport] node for rendering the foveated inset. + Action map configuration to load by default. diff --git a/doc/classes/Projection.xml b/doc/classes/Projection.xml index 2dfeedf73372..da25e003a598 100644 --- a/doc/classes/Projection.xml +++ b/doc/classes/Projection.xml @@ -243,12 +243,24 @@ Returns a [Projection] that performs the inverse of this [Projection]'s projective transformation. + + + + Returns [code]true[/code] if this [Projection] is an asymmetrical projection. + + Returns [code]true[/code] if this [Projection] performs an orthogonal projection. + + + + Returns [code]true[/code] if this [Projection] is z-axis aligned. + + diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index 6ae5bfaa3f31..4a333e629a5e 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -182,6 +182,15 @@ Sets camera to use perspective projection. Objects on the screen becomes smaller when they are far away. + + + + + + + Sets camera to use any set of [param projections]. You can specify one projection for each view set on the viewport. If the views are offset (e.g. ocular distance) and/or skewed, this data can be provided through [param offsets]. + + diff --git a/doc/classes/XRCamera3D.xml b/doc/classes/XRCamera3D.xml index 01c68185c5cc..391b8bc6d4d7 100644 --- a/doc/classes/XRCamera3D.xml +++ b/doc/classes/XRCamera3D.xml @@ -6,12 +6,15 @@ A camera node which automatically positions itself based on XR tracking data. In contrast to [XRController3D], the render thread has access to more up-to-date tracking data, and the location of the [XRCamera3D] node can lag a few milliseconds behind what is used for rendering. - [b]Note:[/b] If [member Viewport.use_xr] is [code]true[/code], most of the camera properties are overridden by the active [XRInterface]. The only properties that can be trusted are the near and far planes. $DOCS_URL/tutorials/xr/index.html + + The name of the camera tracker we're bound to. Which trackers are available is not known during design time. + The default tracker refers to the HMD position of the main player. Consult the documentation of the [XRInterface] for any additional trackers. There may be additional tracked headsets for multiplayer systems or a tracker may be available for a physical camera in a mixed reality scenario. + diff --git a/doc/classes/XRInterface.xml b/doc/classes/XRInterface.xml index 3b19e2fe103d..7d147f74eeb2 100644 --- a/doc/classes/XRInterface.xml +++ b/doc/classes/XRInterface.xml @@ -17,6 +17,23 @@ If this is an AR interface that requires displaying a camera feed as the background, this method returns the feed ID in the [CameraServer] for this interface. + + + + + Gets an array of offset transforms for each view for tracker [param tracker_name]. [param tracker_name] must refer to an [XRPositionalTracker] used as a camera. Returns an empty array if this interface is not responsible for this tracker. + + + + + + + + + + Gets an array of projection matrices for each view for tracker [param tracker_name]. [param tracker_name] must refer to an [XRPositionalTracker] used as a camera. Returns an empty array if this interface is not responsible for this tracker. + + @@ -35,7 +52,7 @@ Returns an array of vectors that represent the physical play area mapped to the virtual space around the [XROrigin3D] point. The points form a convex polygon that can be used to react to or visualize the play area. This returns an empty array if this feature is not supported or if the information is not yet available. - + @@ -70,7 +87,7 @@ If supported, returns the status of our tracking. This will allow you to provide feedback to the user whether there are issues with positional tracking. - + diff --git a/doc/classes/XRInterfaceExtension.xml b/doc/classes/XRInterfaceExtension.xml index 3220d70b34b6..94050a0ae79d 100644 --- a/doc/classes/XRInterfaceExtension.xml +++ b/doc/classes/XRInterfaceExtension.xml @@ -28,6 +28,23 @@ Returns the camera feed ID for the [CameraFeed] registered with the [CameraServer] that should be presented as the background on an AR capable device (if applicable). + + + + + Gets an array of offset transforms for each view for tracker [param tracker_name]. [param tracker_name] must refer to an [XRPositionalTracker] used as a camera. Returns an empty array if this interface is not responsible for this tracker. + + + + + + + + + + Gets an array of projection matrices for each view for tracker [param tracker_name]. [param tracker_name] must refer to an [XRPositionalTracker] used as a camera. Returns an empty array if this interface is not responsible for this tracker. + + @@ -70,7 +87,7 @@ Returns the play area mode that sets up our play area. - + @@ -111,7 +128,7 @@ Returns the current status of our tracking. - + diff --git a/doc/classes/XRServer.xml b/doc/classes/XRServer.xml index 9d5c2b1cccad..b750ae7035a7 100644 --- a/doc/classes/XRServer.xml +++ b/doc/classes/XRServer.xml @@ -50,6 +50,25 @@ Finds an interface by its [param name]. For example, if your project uses capabilities of an AR/VR platform, you can find the interface for that platform by name and initialize it. + + + + + Gets an array of offset transforms for each view for tracker [param tracker_name]. [param tracker_name] must refer to an [XRPositionalTracker] used as a camera. + Will check our primary interface first, then check each active interface. Returns empty if no interfaces manage this tracker. + + + + + + + + + + Gets an array of projection matrices for each view for tracker [param tracker_name]. [param tracker_name] must refer to an [XRPositionalTracker] used as a camera. + Will check our primary interface first, then check each active interface. Returns empty if no interfaces manage this tracker. + + @@ -172,8 +191,8 @@ - - The tracker tracks the location of the player's head. This is usually a location centered between the player's eyes. Note that for handheld AR devices this can be the current location of the device. + + The tracker tracks the position of an XR camera (HMD, external camera, phone camera for phone based AR). The tracker tracks the location of a controller. @@ -202,6 +221,9 @@ Used internally to select all trackers. + + The tracker tracks the location of the player's head. This is usually a location centered between the player's eyes. Note that for handheld AR devices this can be the current location of the device. + Fully reset the orientation of the HMD. Regardless of what direction the user is looking to in the real world. The user will look dead ahead in the virtual world. diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp index 597e10206983..045fa1021c95 100644 --- a/drivers/gles3/rasterizer_scene_gles3.cpp +++ b/drivers/gles3/rasterizer_scene_gles3.cpp @@ -2396,6 +2396,9 @@ void RasterizerSceneGLES3::render_scene(const Ref &p_render_ Ref rb = p_render_buffers; ERR_FAIL_COND(rb.is_null()); + // View count of our render target must match our camera data. + ERR_FAIL_COND(rb->get_view_count() != p_camera_data->view_count); + if (rb->get_scaling_3d_mode() != RSE::VIEWPORT_SCALING_3D_MODE_OFF) { // If we're scaling, we apply tonemapping etc. in post, so disable it during rendering apply_environment_effects_in_post = true; @@ -2438,6 +2441,7 @@ void RasterizerSceneGLES3::render_scene(const Ref &p_render_ render_data.inv_cam_transform = render_data.cam_transform.affine_inverse(); render_data.cam_projection = p_camera_data->main_projection; render_data.cam_orthogonal = p_camera_data->is_orthogonal; + render_data.cam_asymmetrical = p_camera_data->is_asymmetrical; render_data.camera_visible_layers = p_camera_data->visible_layers; render_data.main_cam_transform = p_camera_data->main_transform; diff --git a/drivers/gles3/rasterizer_scene_gles3.h b/drivers/gles3/rasterizer_scene_gles3.h index bfe558ddf3de..9b202c8ed9aa 100644 --- a/drivers/gles3/rasterizer_scene_gles3.h +++ b/drivers/gles3/rasterizer_scene_gles3.h @@ -106,6 +106,7 @@ struct RenderDataGLES3 { Transform3D inv_cam_transform; Projection cam_projection; bool cam_orthogonal = false; + bool cam_asymmetrical = false; uint32_t camera_visible_layers = 0xFFFFFFFF; // For billboards to cast correct shadows. diff --git a/main/main.cpp b/main/main.cpp index 68035f50756e..499dbbb216c1 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -2848,13 +2848,14 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph GLOBAL_DEF(PropertyInfo(Variant::STRING, "xr/openxr/target_api_version"), ""); GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "xr/openxr/default_action_map", PROPERTY_HINT_FILE, "*.tres"), "res://openxr_action_map.tres"); GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/form_factor", PROPERTY_HINT_ENUM, "Head Mounted,Handheld"), "0"); - GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/view_configuration", PROPERTY_HINT_ENUM, "Mono,Stereo"), "1"); // "Mono,Stereo,Quad,Observer" + GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/view_configuration", PROPERTY_HINT_ENUM, "Mono,Stereo,Stereo with Foveated Inset"), "1"); GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/reference_space", PROPERTY_HINT_ENUM, "Local,Stage,Local Floor"), "1"); GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/environment_blend_mode", PROPERTY_HINT_ENUM, "Opaque,Additive,Alpha"), "0"); GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/foveation_level", PROPERTY_HINT_ENUM, "Off,Low,Medium,High"), "0"); GLOBAL_DEF_BASIC("xr/openxr/foveation_dynamic", false); GLOBAL_DEF_BASIC("xr/openxr/foveation_eye_tracked", true); GLOBAL_DEF_BASIC("xr/openxr/foveation_with_subsampled_images", true); + GLOBAL_DEF_BASIC("xr/openxr/create_default_foveated_inset_viewport", true); GLOBAL_DEF_BASIC("xr/openxr/submit_depth_buffer", false); GLOBAL_DEF_BASIC("xr/openxr/startup_alert", true); diff --git a/modules/mobile_vr/mobile_vr_interface.cpp b/modules/mobile_vr/mobile_vr_interface.cpp index 39dfae306ab1..c68663200da4 100644 --- a/modules/mobile_vr/mobile_vr_interface.cpp +++ b/modules/mobile_vr/mobile_vr_interface.cpp @@ -279,7 +279,13 @@ Rect2 MobileVRInterface::get_offset_rect() const { } void MobileVRInterface::set_iod(const double p_iod) { - intraocular_dist = p_iod; + if (intraocular_dist != p_iod) { + intraocular_dist = p_iod; + + // Clear our cached values, we'll need to recalculate. + camera_projections.clear(); + camera_offsets.clear(); + } } double MobileVRInterface::get_iod() const { @@ -287,7 +293,12 @@ double MobileVRInterface::get_iod() const { } void MobileVRInterface::set_display_width(const double p_display_width) { - display_width = p_display_width; + if (display_width != p_display_width) { + display_width = p_display_width; + + // Clear our cached view projections, we'll need to recalculate. + camera_projections.clear(); + } } double MobileVRInterface::get_display_width() const { @@ -295,7 +306,12 @@ double MobileVRInterface::get_display_width() const { } void MobileVRInterface::set_display_to_lens(const double p_display_to_lens) { - display_to_lens = p_display_to_lens; + if (display_to_lens != p_display_to_lens) { + display_to_lens = p_display_to_lens; + + // Clear our cached view projections, we'll need to recalculate. + camera_projections.clear(); + } } double MobileVRInterface::get_display_to_lens() const { @@ -303,7 +319,12 @@ double MobileVRInterface::get_display_to_lens() const { } void MobileVRInterface::set_oversample(const double p_oversample) { - oversample = p_oversample; + if (oversample != p_oversample) { + oversample = p_oversample; + + // Clear our cached view projections, we'll need to recalculate. + camera_projections.clear(); + } } double MobileVRInterface::get_oversample() const { @@ -373,8 +394,8 @@ bool MobileVRInterface::initialize() { // we must create a tracker for our head head.instantiate(); - head->set_tracker_type(XRServer::TRACKER_HEAD); - head->set_tracker_name("head"); + head->set_tracker_type(XRServer::TRACKER_CAMERA); + head->set_tracker_name(XR_TRACKER_HEAD); head->set_tracker_desc("Players head"); xr_server->add_tracker(head); @@ -406,6 +427,11 @@ void MobileVRInterface::uninitialize() { } } + // Clear our cached data + camera_projections.clear(); + camera_offsets.clear(); + + // And we're no longer initialized. initialized = false; }; } @@ -465,9 +491,79 @@ Transform3D MobileVRInterface::get_camera_transform() { return transform_for_eye; } +TypedArray MobileVRInterface::get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) { + _THREAD_SAFE_METHOD_ + + if (p_tracker_name != XR_TRACKER_HEAD) { + return TypedArray(); + } + + if (aspect != p_aspect || z_near != p_z_near || z_far != p_z_far) { + camera_projections.clear(); + aspect = p_aspect; + z_near = p_z_near; + z_far = p_z_far; + } + + if (!camera_projections.is_empty()) { + // Return our cached values. + return camera_projections; + } + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL_V(xr_server, camera_projections); + + if (initialized) { + for (int v = 1; v < 3; v++) { + Projection projection; + + projection.set_for_hmd(v, aspect, intraocular_dist, display_width, display_to_lens, oversample, z_near, z_far); + + camera_projections.push_back(projection); + } + } + + return camera_projections; +} + +TypedArray MobileVRInterface::get_camera_offsets(const StringName &p_tracker_name) { + _THREAD_SAFE_METHOD_ + + if (p_tracker_name != XR_TRACKER_HEAD) { + return TypedArray(); + } + + if (!camera_offsets.is_empty()) { + // Return our cached values. + return camera_offsets; + } + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL_V(xr_server, camera_offsets); + + if (initialized) { + float world_scale = xr_server->get_world_scale(); + + for (int v = 0; v < 2; v++) { + Transform3D offset; + + // We don't need to check for the existence of our HMD, doesn't affect our values... + // note * 0.01 to convert cm to m and * 0.5 as we're moving half in each direction... + offset.origin.x = intraocular_dist * 0.01 * world_scale * (v == 0 ? -0.5 : 0.5); + + camera_offsets.push_back(offset); + } + } + + return camera_offsets; +} + +#ifndef DISABLE_DEPRECATED Transform3D MobileVRInterface::get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) { _THREAD_SAFE_METHOD_ + WARN_PRINT_ONCE("MobileVRInterface.get_transform_for_view is deprecated, please use MobileVRInterface.get_camera_offsets"); + Transform3D transform_for_eye; XRServer *xr_server = XRServer::get_singleton(); @@ -476,21 +572,14 @@ Transform3D MobileVRInterface::get_transform_for_view(uint32_t p_view, const Tra if (initialized) { float world_scale = xr_server->get_world_scale(); - // we don't need to check for the existence of our HMD, doesn't affect our values... - // note * 0.01 to convert cm to m and * 0.5 as we're moving half in each direction... - if (p_view == 0) { - transform_for_eye.origin.x = -(intraocular_dist * 0.01 * 0.5 * world_scale); - } else if (p_view == 1) { - transform_for_eye.origin.x = intraocular_dist * 0.01 * 0.5 * world_scale; - } else { - // should not have any other values.. - }; - // just scale our origin point of our transform Transform3D _head_transform = head_transform; _head_transform.origin *= world_scale; - transform_for_eye = p_cam_transform * (xr_server->get_reference_frame()) * _head_transform * transform_for_eye; + Transform3D offset; + offset.origin.x = intraocular_dist * 0.01 * world_scale * (p_view == 0 ? -0.5 : 0.5); + + transform_for_eye = p_cam_transform * (xr_server->get_reference_frame()) * _head_transform * offset; } else { // huh? well just return what we got.... transform_for_eye = p_cam_transform; @@ -502,6 +591,8 @@ Transform3D MobileVRInterface::get_transform_for_view(uint32_t p_view, const Tra Projection MobileVRInterface::get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) { _THREAD_SAFE_METHOD_ + WARN_PRINT_ONCE("MobileVRInterface.get_projection_for_view is deprecated, please use MobileVRInterface.get_camera_projections"); + Projection eye; aspect = p_aspect; @@ -509,6 +600,7 @@ Projection MobileVRInterface::get_projection_for_view(uint32_t p_view, double p_ return eye; } +#endif Vector MobileVRInterface::post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) { _THREAD_SAFE_METHOD_ diff --git a/modules/mobile_vr/mobile_vr_interface.h b/modules/mobile_vr/mobile_vr_interface.h index 10c8f4de2fd6..2b043b90a3a2 100644 --- a/modules/mobile_vr/mobile_vr_interface.h +++ b/modules/mobile_vr/mobile_vr_interface.h @@ -67,10 +67,14 @@ class MobileVRInterface : public XRInterface { double k1 = 0.215; double k2 = 0.215; double aspect = 1.0; + double z_near = 0.01; + double z_far = 4096.0; - // at a minimum we need a tracker for our head + // At a minimum we need a tracker for our head. Ref head; Transform3D head_transform; + TypedArray camera_projections; + TypedArray camera_offsets; XRVRS xr_vrs; @@ -163,8 +167,12 @@ class MobileVRInterface : public XRInterface { virtual Size2 get_render_target_size() override; virtual uint32_t get_view_count() override; virtual Transform3D get_camera_transform() override; + virtual TypedArray get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) override; + virtual TypedArray get_camera_offsets(const StringName &p_tracker_name) override; +#ifndef DISABLE_DEPRECATED virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) override; virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) override; +#endif virtual Vector post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) override; virtual void process() override; diff --git a/modules/openxr/config.py b/modules/openxr/config.py index eac756df36bf..c6f9b8dac6f1 100644 --- a/modules/openxr/config.py +++ b/modules/openxr/config.py @@ -27,6 +27,7 @@ def get_doc_classes(): "OpenXRIPBinding", "OpenXRHand", "OpenXRVisibilityMask", + "OpenXRFoveatedInsetViewport", "OpenXRCompositionLayer", "OpenXRCompositionLayerQuad", "OpenXRCompositionLayerCylinder", diff --git a/modules/openxr/doc_classes/OpenXRAPIExtension.xml b/modules/openxr/doc_classes/OpenXRAPIExtension.xml index 00121d4ececb..8d495d19a9e7 100644 --- a/modules/openxr/doc_classes/OpenXRAPIExtension.xml +++ b/modules/openxr/doc_classes/OpenXRAPIExtension.xml @@ -103,6 +103,12 @@ Returns the predicted display timing for the current frame. + + + + Returns the view count for the primary viewport. + + diff --git a/modules/openxr/doc_classes/OpenXRExtensionWrapper.xml b/modules/openxr/doc_classes/OpenXRExtensionWrapper.xml index ad51c227dbf4..b55e0cdfc6c5 100644 --- a/modules/openxr/doc_classes/OpenXRExtensionWrapper.xml +++ b/modules/openxr/doc_classes/OpenXRExtensionWrapper.xml @@ -14,6 +14,23 @@ + + + + + Returns an array of offsets for each view rendered for the given camera, if this camera is handled by this extension. [param tracker_name] refers to the camera tracker for which we're retrieving this. + + + + + + + + + + Returns an array of projections for each view rendered for the given camera, if this camera is handled by this extension. [param tracker_name] refers to the camera tracker for which we're retrieving this. + + diff --git a/modules/openxr/doc_classes/OpenXRFoveatedInsetViewport.xml b/modules/openxr/doc_classes/OpenXRFoveatedInsetViewport.xml new file mode 100644 index 000000000000..9b86924bf0ff --- /dev/null +++ b/modules/openxr/doc_classes/OpenXRFoveatedInsetViewport.xml @@ -0,0 +1,29 @@ + + + + This viewport is used for rendering our foveated inset. + + + This viewport is used for rendering our foveated inset. When foveated inset rendering is supported OpenXR will create and configure this viewport correctly and use its output. The viewport will be available after OpenXR reaches the "begun" state. You can access it by calling [code]get_tree().get_root().get_node("OpenXRFoveatedInsetViewport")[/code] at this time. + + + + + + + + Returns the [XRCamera3D] node used for rendering the foveated inset. + + + + + + Returns the [XROrigin3D] node used for tracking the foveated inset. Our built-in logic handles positioning this node. It will copy the position and orientation of the [XROrigin3D] node marked as current. + + + + + + + + diff --git a/modules/openxr/doc_classes/OpenXRInterface.xml b/modules/openxr/doc_classes/OpenXRInterface.xml index e9b1938de110..ee7c1194d021 100644 --- a/modules/openxr/doc_classes/OpenXRInterface.xml +++ b/modules/openxr/doc_classes/OpenXRInterface.xml @@ -17,6 +17,13 @@ Returns a list of action sets registered with Godot (loaded from the action map at runtime). + + + + Gets the active view configuration. + [b]Note:[/b] This will only return a valid value after OpenXR is initialized. + + @@ -297,6 +304,21 @@ The OpenXR instance is about to be destroyed and we're exiting. [signal instance_exiting] is emitted when we change to this state. + + Our XR output configuration is monoscopic. + + + Our XR output configuration is stereoscopic. + + + Our XR output configuration is stereoscopic with an additional foveated inset render. + + + Our XR output configuration has not yet been determined. + + + Our XR output configuration is unknown. + Left hand. diff --git a/modules/openxr/extensions/openxr_extension_wrapper.cpp b/modules/openxr/extensions/openxr_extension_wrapper.cpp index 3be637896521..0cebe2d0b6f8 100644 --- a/modules/openxr/extensions/openxr_extension_wrapper.cpp +++ b/modules/openxr/extensions/openxr_extension_wrapper.cpp @@ -82,6 +82,9 @@ void OpenXRExtensionWrapper::_bind_methods() { GDVIRTUAL_BIND(_on_viewport_composition_layer_destroyed, "layer"); GDVIRTUAL_BIND(_set_android_surface_swapchain_create_info_and_get_next_pointer, "property_values", "next_pointer"); + GDVIRTUAL_BIND(_get_camera_projections, "tracker_name", "aspect", "z_near", "z_far"); + GDVIRTUAL_BIND(_get_camera_offsets, "tracker_name"); + #ifndef DISABLE_DEPRECATED GDVIRTUAL_BIND_COMPAT(_get_requested_extensions_bind_compat_109302); GDVIRTUAL_BIND_COMPAT(_set_instance_create_info_and_get_next_pointer_bind_compat_109302, "next_pointer"); @@ -422,6 +425,22 @@ void *OpenXRExtensionWrapper::set_android_surface_swapchain_create_info_and_get_ return p_next_pointer; } +TypedArray OpenXRExtensionWrapper::get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) { + TypedArray camera_projections; + + GDVIRTUAL_CALL(_get_camera_projections, p_tracker_name, p_aspect, p_z_near, p_z_far, camera_projections); + + return camera_projections; +} + +TypedArray OpenXRExtensionWrapper::get_camera_offsets(const StringName &p_tracker_name) { + TypedArray camera_offsets; + + GDVIRTUAL_CALL(_get_camera_offsets, p_tracker_name, camera_offsets); + + return camera_offsets; +} + Ref OpenXRExtensionWrapper::_gdextension_get_openxr_api() { return openxr_api_extension; } diff --git a/modules/openxr/extensions/openxr_extension_wrapper.h b/modules/openxr/extensions/openxr_extension_wrapper.h index 3756934274d2..57d5a1c5627a 100644 --- a/modules/openxr/extensions/openxr_extension_wrapper.h +++ b/modules/openxr/extensions/openxr_extension_wrapper.h @@ -96,6 +96,9 @@ class OpenXRExtensionWrapper : public Object { virtual void *set_view_configuration_and_get_next_pointer(uint32_t p_view, void *p_next_pointer); // Add additional data structures when calling xrEnumerateViewConfiguration virtual void print_view_configuration_info(uint32_t p_view) const; + virtual TypedArray get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far); + virtual TypedArray get_camera_offsets(const StringName &p_tracker_name); + //TODO workaround as GDExtensionPtr return type results in build error in godot-cpp GDVIRTUAL1R(uint64_t, _set_system_properties_and_get_next_pointer, GDExtensionPtr); GDVIRTUAL2R(uint64_t, _set_instance_create_info_and_get_next_pointer, uint64_t, GDExtensionPtr); @@ -115,6 +118,9 @@ class OpenXRExtensionWrapper : public Object { GDVIRTUAL2R(uint64_t, _set_view_configuration_and_get_next_pointer, uint32_t, GDExtensionPtr); GDVIRTUAL1C(_print_view_configuration_info, int); + GDVIRTUAL4R(TypedArray, _get_camera_projections, const StringName &, double, double, double); + GDVIRTUAL1R(TypedArray, _get_camera_offsets, const StringName &); + #ifndef DISABLE_DEPRECATED GDVIRTUAL0R_COMPAT(_get_requested_extensions_bind_compat_109302, Dictionary, _get_requested_extensions); GDVIRTUAL1R_COMPAT(_set_instance_create_info_and_get_next_pointer_bind_compat_109302, uint64_t, _set_instance_create_info_and_get_next_pointer, GDExtensionPtr); @@ -148,6 +154,7 @@ class OpenXRExtensionWrapper : public Object { virtual void on_main_swapchains_created(); // `on_main_swapchains_created` is called right after our main swapchains are (re)created. virtual void on_pre_draw_viewport(RID p_render_target); // `on_pre_draw_viewport` is called right before we start rendering this viewport virtual void on_post_draw_viewport(RID p_render_target); // `on_port_draw_viewport` is called right after we start rendering this viewport (note that on Vulkan draw commands may only be queued) + virtual void on_post_render() {} // `on_post_render` is called right before we submit our XR viewports to the XR compositor. GDVIRTUAL1(_on_register_metadata, OpenXRInteractionProfileMetadata *); GDVIRTUAL0(_on_before_instance_created); diff --git a/modules/openxr/extensions/openxr_foveated_inset_extension.cpp b/modules/openxr/extensions/openxr_foveated_inset_extension.cpp new file mode 100644 index 000000000000..eeb1dd5a9885 --- /dev/null +++ b/modules/openxr/extensions/openxr_foveated_inset_extension.cpp @@ -0,0 +1,344 @@ +/**************************************************************************/ +/* openxr_foveated_inset_extension.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "openxr_foveated_inset_extension.h" + +#include "../scene/openxr_foveated_inset_viewport.h" + +#include "core/config/project_settings.h" +#include "core/object/callable_mp.h" +#include "core/os/os.h" +#include "scene/main/scene_tree.h" +#include "scene/main/window.h" // IWYU pragma: keep. Used via `Node::get_window()`. +#include "servers/rendering/rendering_server.h" +#include "servers/rendering/rendering_server_globals.h" +#include "servers/xr/xr_server.h" + +OpenXRFoveatedInsetExtension *OpenXRFoveatedInsetExtension::singleton = nullptr; + +OpenXRFoveatedInsetExtension *OpenXRFoveatedInsetExtension::get_singleton() { + return singleton; +} + +OpenXRFoveatedInsetExtension::OpenXRFoveatedInsetExtension() { + singleton = this; +} + +OpenXRFoveatedInsetExtension::~OpenXRFoveatedInsetExtension() { + singleton = nullptr; +} + +HashMap OpenXRFoveatedInsetExtension::get_requested_extensions(XrVersion p_version) { + HashMap request_extensions; + + // Only request our extensions if we're attempting to use foveated inset. + int view_configuration_setting = GLOBAL_GET("xr/openxr/view_configuration"); + if (view_configuration_setting == 2) { + // Extension was replaced in OpenXR 1.1, use `XR_VARJO_quad_views` in OpenXR 1.0. + // Note: we currently always include this as there is a dependency with `XR_VARJO_foveated_rendering`. + request_extensions[XR_VARJO_QUAD_VIEWS_EXTENSION_NAME] = &varjo_ext_available; + + // This enables eye tracked foveated inset support. + request_extensions[XR_VARJO_FOVEATED_RENDERING_EXTENSION_NAME] = &varjo_foveated_rendering_ext_available; + } + + return request_extensions; +} + +void OpenXRFoveatedInsetExtension::on_session_created(const XrSession p_session) { + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + + // We must create a tracker for our foveated inset if we use the foveated inset rendering. + if (openxr_api->get_view_configuration() == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET) { + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL(xr_server); + + foveated_inset.instantiate(); + foveated_inset->set_tracker_type(XRServer::TRACKER_CAMERA); + foveated_inset->set_tracker_name(XR_TRACKER_INSET); + foveated_inset->set_tracker_desc("Foveated Inset View"); + xr_server->add_tracker(foveated_inset); + } +} + +void OpenXRFoveatedInsetExtension::on_state_ready() { + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + + // It's too early in `on_session_created` to create our viewport. + // It would be nicer if we could do this earlier... + bool create_inset_viewport = GLOBAL_GET("xr/openxr/create_default_foveated_inset_viewport"); + if (openxr_api->get_view_configuration() == XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET && create_inset_viewport) { + OS *os = OS::get_singleton(); + ERR_FAIL_NULL(os); + + MainLoop *main_loop = os->get_main_loop(); + ERR_FAIL_NULL(main_loop); + + SceneTree *scene_tree = Object::cast_to(main_loop); + ERR_FAIL_NULL(scene_tree); + + Node *root = scene_tree->get_root(); + ERR_FAIL_NULL(root); + + inset_viewport = memnew(OpenXRFoveatedInsetViewport); + inset_viewport->set_name("OpenXRFoveatedInsetViewport"); + root->add_child(inset_viewport); + } +} + +void OpenXRFoveatedInsetExtension::on_session_destroyed() { + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL(xr_server); + + if (inset_viewport) { + Node *parent = inset_viewport->get_parent(); + if (parent) { + parent->remove_child(inset_viewport); + } + inset_viewport->queue_free(); + inset_viewport = nullptr; + } + + _free_swapchains(); + + if (foveated_inset.is_valid()) { + xr_server->remove_tracker(foveated_inset); + foveated_inset.unref(); + } +} + +void OpenXRFoveatedInsetExtension::on_process() { + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + + if (foveated_inset.is_valid()) { + // We use the same center transform here as for our head tracker. + Transform3D t; + Vector3 linear_velocity; + Vector3 angular_velocity; + + head_confidence = openxr_api->get_head_center(t, linear_velocity, angular_velocity); + if (head_confidence != XRPose::XR_TRACKING_CONFIDENCE_NONE) { + // Only update our transform if we have one to update it with + // note that poses are stored without world scale and reference frame applied! + head_transform = t; + head_linear_velocity = linear_velocity; + head_angular_velocity = angular_velocity; + } + + foveated_inset->set_pose("default", head_transform, head_linear_velocity, head_angular_velocity, head_confidence); + } +} + +TypedArray OpenXRFoveatedInsetExtension::get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) { + TypedArray camera_projections; + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL_V(openxr_api, camera_projections); + + if (p_tracker_name == XR_TRACKER_INSET) { + if (openxr_api->get_view_configuration() != XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET) { + // It's possible our application was configured to support foveated inset, + // but the current hardware used does not support it. + return camera_projections; + } + + ERR_FAIL_COND_V(openxr_api->get_view_count() != 4, camera_projections); + + for (uint32_t v = 2; v < 4; v++) { + Projection cm; + openxr_api->get_view_projection(v, p_z_near, p_z_far, cm); + camera_projections.push_back(cm); + } + } + + return camera_projections; +} + +TypedArray OpenXRFoveatedInsetExtension::get_camera_offsets(const StringName &p_tracker_name) { + TypedArray camera_offsets; + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL_V(openxr_api, camera_offsets); + + if (p_tracker_name == XR_TRACKER_INSET) { + if (openxr_api->get_view_configuration() != XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET) { + // It's possible our application was configured to support foveated inset, + // but the current hardware used does not support it. + return camera_offsets; + } + + ERR_FAIL_COND_V(openxr_api->get_view_count() != 4, camera_offsets); + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL_V(xr_server, camera_offsets); + + double world_scale = xr_server->get_world_scale(); + + for (uint32_t v = 2; v < 4; v++) { + Transform3D t; + openxr_api->get_view_offset(v, t); + + // Apply our world scale + t.origin *= world_scale; + + camera_offsets.push_back(t); + } + } + + return camera_offsets; +} + +Size2i OpenXRFoveatedInsetExtension::get_render_size() { + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL_V(openxr_api, Size2i()); + +#ifdef DEBUG_ENABLED + // Views 2 and 3 should have the same size! + ERR_FAIL_COND_V(openxr_api->get_recommended_target_size(2) != openxr_api->get_recommended_target_size(3), Size2i()); +#endif + + return openxr_api->get_recommended_target_size(2); +} + +void OpenXRFoveatedInsetExtension::register_viewport(RID p_viewport) { + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp(this, &OpenXRFoveatedInsetExtension::_register_viewport_rt).bind(p_viewport)); +} + +void OpenXRFoveatedInsetExtension::unregister_viewport(RID p_viewport) { + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp(this, &OpenXRFoveatedInsetExtension::_unregister_viewport_rt).bind(p_viewport)); +} + +void OpenXRFoveatedInsetExtension::_register_viewport_rt(RID p_viewport) { + render_state.viewports.push_back(p_viewport); +} + +void OpenXRFoveatedInsetExtension::_unregister_viewport_rt(RID p_viewport) { + render_state.viewports.erase(p_viewport); +} + +void OpenXRFoveatedInsetExtension::on_pre_render() { + if (render_state.viewports.size() == 0) { + return; + } else if (render_state.viewports.size() > 1) { + WARN_PRINT("Multiple OpenXRFoveatedInsetViewport nodes are active, using the first."); + } + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + + if (openxr_api->get_view_configuration() != XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET) { + return; + } + + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + RID render_target = rendering_server->viewport_get_render_target(render_state.viewports[0]); + if (!render_target.is_valid()) { + return; + } + + // Check if we need to update our swap chains. + Size2i new_size = openxr_api->get_recommended_target_size(2); + if (render_state.size != new_size) { + _free_swapchains(); + + int64_t color_swapchain_format = openxr_api->get_color_swapchain_format(); + if (color_swapchain_format != 0) { + if (!render_state.swapchains[SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, new_size.width, new_size.height, 1, 2)) { + return; + } + + openxr_api->set_object_name(XR_OBJECT_TYPE_SWAPCHAIN, uint64_t(render_state.swapchains[SWAPCHAIN_COLOR].get_swapchain()), "Inset color swapchain"); + } + + // TODO check depth swapchain. + + // Our new size is now applicable. + render_state.size = new_size; + + // Set projections for views 2 and 3. + for (uint32_t i = 2; i < 4; i++) { + openxr_api->set_projection_view_swapchain_rt(i, render_state.swapchains[SWAPCHAIN_COLOR].get_swapchain(), i - 2, new_size); + } + + // TODO Set depth for views 2 and 3. + } + + // Check if we need to acquire our swap chains. + for (int i = 0; i < SWAPCHAIN_MAX; i++) { + if (!render_state.swapchains[i].is_image_acquired() && render_state.swapchains[i].get_swapchain() != XR_NULL_HANDLE) { + bool should_render = true; + if (!render_state.swapchains[i].acquire(should_render)) { + return; + } + } + } + + // Set our swapchains on our render target. + RID color_texture = render_state.swapchains[SWAPCHAIN_COLOR].get_image(); + RID depth_texture = render_state.swapchains[SWAPCHAIN_DEPTH].get_image(); + RSG::texture_storage->render_target_set_override(render_target, color_texture, depth_texture, RID(), RID()); +} + +void OpenXRFoveatedInsetExtension::on_post_render() { + if (render_state.viewports.size() == 0) { + return; + } + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + + if (openxr_api->get_view_configuration() != XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET) { + return; + } + + // Release our swapchain image if we acquired it. + for (int i = 0; i < SWAPCHAIN_MAX; i++) { + if (render_state.swapchains[i].is_image_acquired()) { + render_state.swapchains[i].release(); + } + } +} + +void OpenXRFoveatedInsetExtension::_free_swapchains() { + for (int i = 0; i < SWAPCHAIN_MAX; i++) { + render_state.swapchains[i].queue_free(); + } +} diff --git a/modules/openxr/extensions/openxr_foveated_inset_extension.h b/modules/openxr/extensions/openxr_foveated_inset_extension.h new file mode 100644 index 000000000000..9b0d2c3dc638 --- /dev/null +++ b/modules/openxr/extensions/openxr_foveated_inset_extension.h @@ -0,0 +1,124 @@ +/**************************************************************************/ +/* openxr_foveated_inset_extension.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "../openxr_api.h" +#include "openxr_extension_wrapper.h" + +#include "servers/xr/xr_positional_tracker.h" + +// Foveated inset is a core feature in OpenXR 1.1 but we'll encapsulate +// the functionality in this "extension". +// +// When used we render 4 views instead of 2. +// The first two views are our context view and rendered by our default logic. +// The second two views are our focus view (inset) and handled by the +// implementation here. +// This order is guaranteed by the OpenXR specification. +// +// In OpenXR 1.0 XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET +// was a Varjo only extension implementing this functionality equal to how +// the core support in OpenXR 1.1 defines this. +// We enable it as a fallback if supported. +// +// For more detail on how this works, Varjo has an excellent information page: +// https://varjo.com/blog/make-the-best-out-of-your-varjo-experience-update-on-varjo-quad-view-eye-tracked-foveation-and-varjo-base-settings + +#define XR_TRACKER_INSET SNAME("foveated_inset") + +class OpenXRFoveatedInsetViewport; + +class OpenXRFoveatedInsetExtension : public OpenXRExtensionWrapper { + GDCLASS(OpenXRFoveatedInsetExtension, OpenXRExtensionWrapper); + +protected: + static void _bind_methods() {} + +public: + static OpenXRFoveatedInsetExtension *get_singleton(); + + OpenXRFoveatedInsetExtension(); + virtual ~OpenXRFoveatedInsetExtension() override; + + virtual HashMap get_requested_extensions(XrVersion p_version) override; + + virtual void on_session_created(const XrSession p_session) override; + virtual void on_session_destroyed() override; + virtual void on_state_ready() override; + virtual void on_process() override; + + // Note, call `OpenXR::is_view_configuration_supported(XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET)` + // to check if our foveated inset is supported. + // Even if OpenXR 1.1 is initialized or the Varjo extension is available, + // this does not guarantee the current hardware configuration actually makes foveated inset available. + // Hence we do not expose an `is_available` function here. + + virtual TypedArray get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) override; + virtual TypedArray get_camera_offsets(const StringName &p_tracker_name) override; + + Size2i get_render_size(); + + void register_viewport(RID p_viewport); + void unregister_viewport(RID p_viewport); + virtual void on_pre_render() override; + virtual void on_post_render() override; + +private: + static OpenXRFoveatedInsetExtension *singleton; + OpenXRFoveatedInsetViewport *inset_viewport = nullptr; + + bool varjo_ext_available = false; + bool varjo_foveated_rendering_ext_available = false; + + Ref foveated_inset; + Transform3D head_transform; + Vector3 head_linear_velocity; + Vector3 head_angular_velocity; + XRPose::TrackingConfidence head_confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE; + + enum SwapChainTypes { + SWAPCHAIN_COLOR, + SWAPCHAIN_DEPTH, + SWAPCHAIN_MAX + }; + + // Render state, Only accessible in rendering thread + struct RenderState { + LocalVector viewports; + Size2i size; + OpenXRAPI::OpenXRSwapChainInfo swapchains[SWAPCHAIN_MAX]; + } render_state; + + void _register_viewport_rt(RID p_viewport); + void _unregister_viewport_rt(RID p_viewport); + + void _free_swapchains(); +}; diff --git a/modules/openxr/extensions/openxr_visibility_mask_extension.cpp b/modules/openxr/extensions/openxr_visibility_mask_extension.cpp index ac3e4673f4ef..5f69424d5e9f 100644 --- a/modules/openxr/extensions/openxr_visibility_mask_extension.cpp +++ b/modules/openxr/extensions/openxr_visibility_mask_extension.cpp @@ -37,6 +37,12 @@ #include "core/variant/variant.h" #include "servers/rendering/rendering_server.h" +// Note: if foveated inset rendering is used, our visibility mask should only +// be applied to our context view. +// This code is correct and will only supply data for the primary view. +// But user will need to setup visual layers accordingly. +// Need to document this! + static const char *VISIBILITY_MASK_SHADER_CODE = "shader_type spatial;\n" "render_mode unshaded, shadows_disabled, cull_disabled;\n" @@ -102,7 +108,7 @@ void OpenXRVisibilityMaskExtension::on_session_created(const XrSession p_instanc rendering_server->material_set_render_priority(material, 99); // Get our initial mesh data. - mesh_count = openxr_api->get_view_count(); // We need a mesh for each view. + mesh_count = openxr_api->get_view_count(); // We need a mesh for each primary view. for (uint32_t i = 0; i < mesh_count; i++) { _update_mesh_data(i); } diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index 9f9bf646abae..8a557f538b31 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -77,6 +77,8 @@ #include "extensions/openxr_extension_wrapper_extension.h" #endif // DISABLE_DEPRECATED +#include + #ifdef ANDROID_ENABLED #define OPENXR_LOADER_NAME "libopenxr_loader.so" #endif @@ -805,6 +807,7 @@ bool OpenXRAPI::load_supported_view_configuration_types() { if (!is_view_configuration_supported(view_configuration)) { print_verbose(vformat("OpenXR: %s isn't supported, defaulting to %s.", OpenXRUtil::get_view_configuration_name(view_configuration), OpenXRUtil::get_view_configuration_name(supported_view_configuration_types[0]))); + // Fall back to the first supported. view_configuration = supported_view_configuration_types[0]; } @@ -1315,7 +1318,7 @@ bool OpenXRAPI::create_main_swapchains(const Size2i &p_size) { // We start with our color swapchain... if (color_swapchain_format != 0) { - if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_configuration_views.size())) { + if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, render_state.primary_view_count)) { return false; } @@ -1327,14 +1330,14 @@ bool OpenXRAPI::create_main_swapchains(const Size2i &p_size) { // - we support our depth layer extension // Note: Application Space Warp and Frame Synthesis use a separate lower resolution depth buffer. if (depth_swapchain_format != 0 && submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) { - if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_configuration_views.size())) { + if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, render_state.primary_view_count)) { return false; } set_object_name(XR_OBJECT_TYPE_SWAPCHAIN, uint64_t(render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_swapchain()), "Main depth swapchain"); } - for (uint32_t i = 0; i < render_state.views.size(); i++) { + for (uint32_t i = 0; i < render_state.primary_view_count; i++) { render_state.projection_views[i].subImage.swapchain = render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain(); render_state.projection_views[i].subImage.imageArrayIndex = i; render_state.projection_views[i].subImage.imageRect.offset.x = 0; @@ -1380,7 +1383,8 @@ void OpenXRAPI::destroy_session() { render_state.running = false; } - render_state.views.clear(); + render_state.view_poses.clear(); + render_state.view_fovs.clear(); render_state.projection_views.clear(); render_state.depth_views.clear(); @@ -1570,9 +1574,27 @@ void OpenXRAPI::set_form_factor(XrFormFactor p_form_factor) { } uint32_t OpenXRAPI::get_view_count() const { + // Return the count of all views we have. return view_configuration_views.size(); } +uint32_t OpenXRAPI::get_primary_view_count() const { + // Here we return the view count for our primary view. + // Additional views will be handled separately. + switch (view_configuration) { + case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO: + return 1; + case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO: + return 2; + case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET: + return 2; + case XR_VIEW_CONFIGURATION_TYPE_SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT: + return 2; + default: + return 1; + } +} + void OpenXRAPI::set_view_configuration(XrViewConfigurationType p_view_configuration) { ERR_FAIL_COND(is_initialized()); @@ -1824,7 +1846,7 @@ bool OpenXRAPI::initialize_session() { return false; } - allocate_view_buffers(view_configuration_views.size(), submit_depth_buffer); + allocate_view_buffers(view_configuration_views.size(), get_primary_view_count(), submit_depth_buffer); return true; } @@ -1888,6 +1910,10 @@ XrHandTrackerEXT OpenXRAPI::get_hand_tracker(int p_hand_index) { } Size2 OpenXRAPI::get_recommended_target_size() { + // We return the recommended target size of our primary view here. + // For foveated inset or additional composition layers, + // size is handled in those subsystems! + RenderingServer *rendering_server = RenderingServer::get_singleton(); ERR_FAIL_COND_V(view_configuration_views.is_empty(), Size2()); @@ -1904,19 +1930,26 @@ Size2 OpenXRAPI::get_recommended_target_size() { return target_size; } -XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform, Vector3 &r_linear_velocity, Vector3 &r_angular_velocity) { +Size2i OpenXRAPI::get_recommended_target_size(uint32_t p_view) { + ERR_FAIL_UNSIGNED_INDEX_V(p_view, view_configuration_views.size(), Size2i()); + + return Size2i(view_configuration_views[p_view].recommendedImageRectWidth, view_configuration_views[p_view].recommendedImageRectHeight); +} + +void OpenXRAPI::update_head_tracking() { XrResult result; if (!running) { - return XRPose::XR_TRACKING_CONFIDENCE_NONE; + return; } // Get display time XrTime display_time = get_predicted_display_time(); if (display_time == 0) { - return XRPose::XR_TRACKING_CONFIDENCE_NONE; + return; } + // Get head location first by checking the relationship between our view and play space. XrSpaceVelocity velocity = { XR_TYPE_SPACE_VELOCITY, // type nullptr, // next @@ -1938,11 +1971,22 @@ XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform, result = xrLocateSpace(view_space, play_space, display_time, &location); if (XR_FAILED(result)) { print_line("OpenXR: Failed to locate view space in play space [", get_error_string(result), "]"); - return XRPose::XR_TRACKING_CONFIDENCE_NONE; + return; } - XRPose::TrackingConfidence confidence = transform_from_location(location, r_transform); - parse_velocities(velocity, r_linear_velocity, r_angular_velocity); + XRPose::TrackingConfidence confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE; + if (location.locationFlags != 0) { + // We only update our head pose if we have tracking data, else use what we had. + head_pose = location.pose; + confidence = transform_from_location(location, head_transform); + } + if (velocity.velocityFlags != 0) { + parse_velocities(velocity, head_linear_velocity, head_angular_velocity); + } else { + // No velocity data? then reset these. + head_linear_velocity = Vector3(); + head_angular_velocity = Vector3(); + } if (head_pose_confidence != confidence) { // prevent error spam @@ -1956,9 +2000,167 @@ XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform, } } - return confidence; + // Make sure we can store the data we're keeping for the main thread. + uint32_t view_count = view_configuration_views.size(); + view_offsets.resize(view_count); + view_fovs.resize(view_count); + + // Reserve some local buffers to store intermediate data in. + thread_local PackedVector4Array orientations; + thread_local PackedVector3Array positions; + thread_local PackedVector4Array fovs; + orientations.resize(view_count); + positions.resize(view_count); + fovs.resize(view_count); + + // Now get eye poses in view space... + thread_local LocalVector views; + views.resize(view_count); + + for (uint32_t i = 0; i < view_count; i++) { + views[i] = { + XR_TYPE_VIEW, // type + nullptr, // next + {}, // pose + {}, // fov + }; + } + + void *view_locate_info_next_pointer = nullptr; + for (OpenXRExtensionWrapper *extension : frame_info_extensions) { + void *np = extension->set_view_locate_info_and_get_next_pointer(view_locate_info_next_pointer); + if (np != nullptr) { + view_locate_info_next_pointer = np; + } + } + + XrViewLocateInfo view_locate_info = { + XR_TYPE_VIEW_LOCATE_INFO, // type + view_locate_info_next_pointer, // next + view_configuration, // viewConfigurationType + display_time, // displayTime + view_space // space + }; + + XrViewState view_state = { + XR_TYPE_VIEW_STATE, // type + nullptr, // next + 0 // viewStateFlags + }; + + uint32_t view_count_output; + result = xrLocateViews(session, &view_locate_info, &view_state, view_count, &view_count_output, views.ptr()); + if (XR_FAILED(result)) { + print_line("OpenXR: Couldn't locate views [", get_error_string(result), "]"); + return; + } + + view_pose_valid = (view_state.viewStateFlags != 0); + Vector4 *o = orientations.ptrw(); + Vector3 *p = positions.ptrw(); + Vector4 *f = fovs.ptrw(); + for (uint32_t v = 0; v < view_count; v++) { + view_offsets[v] = transform_from_pose(views[v].pose); + view_fovs[v] = views[v].fov; + + // For rendering we need to combine head and view pose + XrPosef combined_pose; + XrPosef_Multiply(&combined_pose, &head_pose, &views[v].pose); + + // We use Vector3 and Vector4 as a go between as we can't use XrPosef and XrFovf directly. + o[v].x = combined_pose.orientation.x; + o[v].y = combined_pose.orientation.y; + o[v].z = combined_pose.orientation.z; + o[v].w = combined_pose.orientation.w; + + p[v].x = combined_pose.position.x; + p[v].y = combined_pose.position.y; + p[v].z = combined_pose.position.z; + + f[v].x = view_fovs[v].angleLeft; + f[v].y = view_fovs[v].angleRight; + f[v].z = view_fovs[v].angleUp; + f[v].w = view_fovs[v].angleDown; + } + + set_render_state_view_poses(view_pose_valid, orientations, positions, fovs); } +XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform, Vector3 &r_linear_velocity, Vector3 &r_angular_velocity) { + if (!running) { + return XRPose::XR_TRACKING_CONFIDENCE_NONE; + } + + r_transform = head_transform; + r_linear_velocity = head_linear_velocity; + r_angular_velocity = head_angular_velocity; + + return head_pose_confidence; +} + +TypedArray OpenXRAPI::get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) { + TypedArray camera_projections; + + if (p_tracker_name == XR_TRACKER_HEAD) { + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL_V(xr_server, camera_projections); + + for (uint32_t v = 0; v < get_primary_view_count(); v++) { + Projection cm; + get_view_projection(v, p_z_near, p_z_far, cm); + camera_projections.push_back(cm); + } + } else { + for (OpenXRExtensionWrapper *extension : registered_extension_wrappers) { + camera_projections = extension->get_camera_projections(p_tracker_name, p_aspect, p_z_near, p_z_far); + if (!camera_projections.is_empty()) { + return camera_projections; + } + } + } + + return camera_projections; +} + +TypedArray OpenXRAPI::get_camera_offsets(const StringName &p_tracker_name) { + TypedArray camera_offsets; + + if (p_tracker_name == XR_TRACKER_HEAD) { + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL_V(xr_server, camera_offsets); + + double world_scale = xr_server->get_world_scale(); + + for (uint32_t v = 0; v < get_primary_view_count(); v++) { + Transform3D t; + get_view_offset(v, t); + + // Apply our world scale + t.origin *= world_scale; + + camera_offsets.push_back(t); + } + } else { + for (OpenXRExtensionWrapper *extension : registered_extension_wrappers) { + camera_offsets = extension->get_camera_offsets(p_tracker_name); + if (!camera_offsets.is_empty()) { + return camera_offsets; + } + } + } + + return camera_offsets; +} + +bool OpenXRAPI::get_view_offset(uint32_t p_view, Transform3D &r_transform) { + ERR_FAIL_UNSIGNED_INDEX_V(p_view, view_offsets.size(), false); + + r_transform = view_offsets[p_view]; + + return true; +} + +#ifndef DISABLE_DEPRECATED bool OpenXRAPI::get_view_transform(uint32_t p_view, Transform3D &r_transform) { ERR_NOT_ON_RENDER_THREAD_V(false); @@ -1967,64 +2169,55 @@ bool OpenXRAPI::get_view_transform(uint32_t p_view, Transform3D &r_transform) { } // we don't have valid view info - if (render_state.views.is_empty() || !render_state.view_pose_valid) { + if (render_state.view_poses.is_empty() || !render_state.view_pose_valid) { return false; } // Note, the timing of this is set right before rendering, which is what we need here. - r_transform = transform_from_pose(render_state.views[p_view].pose); + r_transform = transform_from_pose(render_state.view_poses[p_view]); return true; } +#endif bool OpenXRAPI::get_view_projection(uint32_t p_view, double p_z_near, double p_z_far, Projection &p_camera_matrix) { - ERR_NOT_ON_RENDER_THREAD_V(false); ERR_FAIL_NULL_V(graphics_extension, false); - if (!render_state.running) { + if (!running) { return false; } // we don't have valid view info - if (render_state.views.is_empty() || !render_state.view_pose_valid) { + if (view_fovs.is_empty() || !view_pose_valid) { return false; } - // if we're using depth views, make sure we update our near and far there... - if (!render_state.depth_views.is_empty()) { - for (XrCompositionLayerDepthInfoKHR &depth_view : render_state.depth_views) { - // As we are using reverse-Z these need to be flipped. - depth_view.nearZ = p_z_far; - depth_view.farZ = p_z_near; - } - } - - render_state.z_near = p_z_near; - render_state.z_far = p_z_far; + // For now we assume these are the same for all views. + set_render_state_near_and_far(p_view, p_z_near, p_z_far); // now update our projection - return graphics_extension->create_projection_fov(render_state.views[p_view].fov, p_z_near, p_z_far, p_camera_matrix); + return graphics_extension->create_projection_fov(view_fovs[p_view], p_z_near, p_z_far, p_camera_matrix); } Vector2 OpenXRAPI::get_eye_focus(uint32_t p_view, float p_aspect) { ERR_FAIL_NULL_V(graphics_extension, Vector2()); - if (!render_state.running) { + if (!running) { return Vector2(); } // xrWaitFrame not run yet - if (render_state.predicted_display_time == 0) { + if (get_predicted_display_time() == 0) { return Vector2(); } // we don't have valid view info - if (render_state.views.is_empty() || !render_state.view_pose_valid) { + if (view_fovs.is_empty() || !view_pose_valid) { return Vector2(); } Projection cm; - if (!graphics_extension->create_projection_fov(render_state.views[p_view].fov, 0.1, 1000.0, cm)) { + if (!graphics_extension->create_projection_fov(view_fovs[p_view], 0.1, 1000.0, cm)) { return Vector2(); } @@ -2036,7 +2229,7 @@ Vector2 OpenXRAPI::get_eye_focus(uint32_t p_view, float p_aspect) { if (eye_gaze_interaction && eye_gaze_interaction->supports_eye_gaze_interaction()) { Vector3 eye_gaze_pose; if (eye_gaze_interaction->get_eye_gaze_pose(1.0, eye_gaze_pose)) { - Transform3D view_transform = transform_from_pose(render_state.views[p_view].pose); + Transform3D view_transform = head_transform * view_offsets[p_view]; eye_gaze_pose = view_transform.xform_inv(eye_gaze_pose); focus = cm.xform(eye_gaze_pose); @@ -2155,7 +2348,7 @@ bool OpenXRAPI::poll_events() { } } -void OpenXRAPI::_allocate_view_buffers_rt(uint32_t p_view_count, bool p_submit_depth_buffer) { +void OpenXRAPI::_allocate_view_buffers_rt(uint32_t p_view_count, uint32_t p_primary_view_count, bool p_submit_depth_buffer) { // Must be called from rendering thread! ERR_NOT_ON_RENDER_THREAD; @@ -2165,16 +2358,13 @@ void OpenXRAPI::_allocate_view_buffers_rt(uint32_t p_view_count, bool p_submit_d openxr_api->render_state.submit_depth_buffer = p_submit_depth_buffer; // Allocate buffers we'll be populating with view information. - openxr_api->render_state.views.resize(p_view_count); + openxr_api->render_state.view_count = p_view_count; + openxr_api->render_state.primary_view_count = p_primary_view_count; + openxr_api->render_state.view_poses.resize(p_view_count); + openxr_api->render_state.view_fovs.resize(p_view_count); openxr_api->render_state.projection_views.resize(p_view_count); for (uint32_t i = 0; i < p_view_count; i++) { - openxr_api->render_state.views[i] = { - XR_TYPE_VIEW, // type - nullptr, // next - {}, // pose - {}, // fov - }; openxr_api->render_state.projection_views[i] = { XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, // type nullptr, // next @@ -2255,6 +2445,63 @@ void OpenXRAPI::_set_render_state_render_region_rt(const Rect2i &p_render_region openxr_api->render_state.render_region = p_render_region; } +void OpenXRAPI::_set_render_state_view_poses(bool p_is_valid, const PackedVector4Array &p_orientations, const PackedVector3Array &p_positions, const PackedVector4Array &p_fovs) { + ERR_NOT_ON_RENDER_THREAD; + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + + if (openxr_api->render_state.view_pose_valid != p_is_valid) { + openxr_api->render_state.view_pose_valid = p_is_valid; + if (!openxr_api->render_state.view_pose_valid) { + print_verbose("OpenXR View pose became invalid"); + } else { + print_verbose("OpenXR View pose became valid"); + } + } + + if (p_is_valid) { + const uint32_t view_count = openxr_api->render_state.view_count; + ERR_FAIL_COND(p_orientations.size() != view_count); + ERR_FAIL_COND(p_positions.size() != view_count); + ERR_FAIL_COND(p_fovs.size() != view_count); + + for (uint32_t view = 0; view < view_count; view++) { + // We use Vector3 and Vector4 as a go between as we can't use XrPosef and XrFovf directly. + + const Vector4 &o = p_orientations[view]; + const Vector3 &p = p_positions[view]; + const Vector4 &f = p_fovs[view]; + + openxr_api->render_state.view_poses[view].orientation = { float(o.x), float(o.y), float(o.z), float(o.w) }; + openxr_api->render_state.view_poses[view].position = { float(p.x), float(p.y), float(p.z) }; + openxr_api->render_state.view_fovs[view] = { float(f.x), float(f.y), float(f.z), float(f.w) }; + } + } +} + +void OpenXRAPI::_set_render_state_near_and_far(uint32_t p_view, double p_z_near, double p_z_far) { + ERR_NOT_ON_RENDER_THREAD; + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + + // Record our near and far for our first view. + if (p_view == 0) { + openxr_api->render_state.z_near = p_z_near; + openxr_api->render_state.z_far = p_z_far; + } + + // if we're using depth views, make sure we update our near and far there... + if (!openxr_api->render_state.depth_views.is_empty()) { + ERR_FAIL_UNSIGNED_INDEX(p_view, openxr_api->render_state.depth_views.size()); + + // As we are using reverse-Z these need to be flipped. + openxr_api->render_state.depth_views[p_view].nearZ = p_z_far; + openxr_api->render_state.depth_views[p_view].farZ = p_z_near; + } +} + void OpenXRAPI::_update_main_swapchain_size_rt() { ERR_NOT_ON_RENDER_THREAD; @@ -2276,12 +2523,12 @@ void OpenXRAPI::_update_main_swapchain_size_rt() { #endif } -void OpenXRAPI::allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer) { +void OpenXRAPI::allocate_view_buffers(uint32_t p_view_count, uint32_t p_primary_view_count, bool p_submit_depth_buffer) { // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... RenderingServer *rendering_server = RenderingServer::get_singleton(); ERR_FAIL_NULL(rendering_server); - rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_allocate_view_buffers_rt).bind(p_view_count, p_submit_depth_buffer)); + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_allocate_view_buffers_rt).bind(p_view_count, p_primary_view_count, p_submit_depth_buffer)); } void OpenXRAPI::set_render_session_running(bool p_is_running) { @@ -2331,6 +2578,32 @@ void OpenXRAPI::set_render_state_render_region(const Rect2i &p_render_region) { rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_state_render_region_rt).bind(p_render_region)); } +void OpenXRAPI::set_render_state_view_poses(bool p_is_valid, const PackedVector4Array &p_orientations, const PackedVector3Array &p_positions, const PackedVector4Array &p_fovs) { + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_state_view_poses).bind(p_is_valid, p_orientations, p_positions, p_fovs)); +} + +void OpenXRAPI::set_render_state_near_and_far(uint32_t p_view, double p_z_near, double p_z_far) { + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_state_near_and_far).bind(p_view, p_z_near, p_z_far)); +} + +void OpenXRAPI::set_projection_view_swapchain_rt(uint32_t p_view, XrSwapchain p_swapchain, uint32_t image_array_index, Size2i p_size) { + ERR_NOT_ON_RENDER_THREAD; + ERR_FAIL_UNSIGNED_INDEX(p_view, render_state.projection_views.size()); + + render_state.projection_views[p_view].subImage.swapchain = p_swapchain; + render_state.projection_views[p_view].subImage.imageArrayIndex = image_array_index; + render_state.projection_views[p_view].subImage.imageRect.offset.x = 0; + render_state.projection_views[p_view].subImage.imageRect.offset.y = 0; + render_state.projection_views[p_view].subImage.imageRect.extent.width = p_size.width; + render_state.projection_views[p_view].subImage.imageRect.extent.height = p_size.height; +} + void OpenXRAPI::update_main_swapchain_size() { RenderingServer *rendering_server = RenderingServer::get_singleton(); ERR_FAIL_NULL(rendering_server); @@ -2408,6 +2681,8 @@ bool OpenXRAPI::process() { play_space_is_dirty = false; } + update_head_tracking(); + GodotProfileZoneGrouped(_profile_zone, "extension wrappers on_process"); for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_process(); @@ -2435,60 +2710,6 @@ void OpenXRAPI::pre_render() { // Process any swapchains that were queued to be freed OpenXRSwapChainInfo::free_queued(); - void *view_locate_info_next_pointer = nullptr; - for (OpenXRExtensionWrapper *extension : frame_info_extensions) { - void *np = extension->set_view_locate_info_and_get_next_pointer(view_locate_info_next_pointer); - if (np != nullptr) { - view_locate_info_next_pointer = np; - } - } - - // Get our view info for the frame we're about to render, note from the OpenXR manual: - // "Repeatedly calling xrLocateViews with the same time may not necessarily return the same result. Instead the prediction gets increasingly accurate as the function is called closer to the given time for which a prediction is made" - - // We're calling this "relatively" early, the positioning we're obtaining here will be used to do our frustum culling, - // occlusion culling, etc. There is however a technique that we can investigate in the future where after our entire - // Vulkan command buffer is build, but right before vkSubmitQueue is called, we call xrLocateViews one more time and - // update the view and projection matrix once more with a slightly more accurate predication and then submit the - // command queues. - - // That is not possible yet but worth investigating in the future. - - XrViewLocateInfo view_locate_info = { - XR_TYPE_VIEW_LOCATE_INFO, // type - view_locate_info_next_pointer, // next - view_configuration, // viewConfigurationType - render_state.predicted_display_time, // displayTime - render_state.play_space // space - }; - XrViewState view_state = { - XR_TYPE_VIEW_STATE, // type - nullptr, // next - 0 // viewStateFlags - }; - uint32_t view_count_output; - XrResult result = xrLocateViews(session, &view_locate_info, &view_state, render_state.views.size(), &view_count_output, render_state.views.ptr()); - if (XR_FAILED(result)) { - print_line("OpenXR: Couldn't locate views [", get_error_string(result), "]"); - return; - } - - bool pose_valid = true; - for (uint64_t i = 0; i < view_count_output; i++) { - if ((view_state.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) == 0 || - (view_state.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) == 0) { - pose_valid = false; - } - } - if (render_state.view_pose_valid != pose_valid) { - render_state.view_pose_valid = pose_valid; - if (!render_state.view_pose_valid) { - print_verbose("OpenXR View pose became invalid"); - } else { - print_verbose("OpenXR View pose became valid"); - } - } - // We should get our frame no from the rendering server, but this will do. begin_debug_label_region(String("Session Frame ") + String::num_uint64(++render_state.frame)); @@ -2497,7 +2718,7 @@ void OpenXRAPI::pre_render() { XR_TYPE_FRAME_BEGIN_INFO, // type nullptr // next }; - result = xrBeginFrame(session, &frame_begin_info); + XrResult result = xrBeginFrame(session, &frame_begin_info); if (XR_FAILED(result)) { print_line("OpenXR: failed to begin frame [", get_error_string(result), "]"); return; @@ -2522,8 +2743,8 @@ bool OpenXRAPI::pre_draw_viewport(RID p_render_target) { return false; } - Size2i swapchain_size = get_recommended_target_size(); - bool should_recreate_swapchain = (swapchain_size != render_state.main_swapchain_size); + Size2i main_swapchain_size = get_recommended_target_size(); + bool should_recreate_swapchain = (main_swapchain_size != render_state.main_swapchain_size); OpenXRFBFoveationExtension *fov_ext = OpenXRFBFoveationExtension::get_singleton(); if (fov_ext) { @@ -2552,7 +2773,7 @@ bool OpenXRAPI::pre_draw_viewport(RID p_render_target) { free_main_swapchains(); // In with the new. - create_main_swapchains(swapchain_size); + create_main_swapchains(main_swapchain_size); } // Acquire our images @@ -2668,23 +2889,28 @@ void OpenXRAPI::end_frame() { } } + // Apply our render region to our primary views. Rect2i new_render_region = (render_state.render_region != Rect2i()) ? render_state.render_region : Rect2i(Point2i(0, 0), render_state.main_swapchain_size); - for (XrCompositionLayerProjectionView &projection_view : render_state.projection_views) { - projection_view.subImage.imageRect.offset.x = new_render_region.position.x; - projection_view.subImage.imageRect.offset.y = new_render_region.position.y; - projection_view.subImage.imageRect.extent.width = new_render_region.size.width; - projection_view.subImage.imageRect.extent.height = new_render_region.size.height; + for (uint32_t v = 0; v < render_state.primary_view_count; v++) { + render_state.projection_views[v].subImage.imageRect.offset.x = new_render_region.position.x; + render_state.projection_views[v].subImage.imageRect.offset.y = new_render_region.position.y; + render_state.projection_views[v].subImage.imageRect.extent.width = new_render_region.size.width; + render_state.projection_views[v].subImage.imageRect.extent.height = new_render_region.size.height; } if (render_state.submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available() && !render_state.depth_views.is_empty()) { - for (XrCompositionLayerDepthInfoKHR &depth_view : render_state.depth_views) { - depth_view.subImage.imageRect.offset.x = new_render_region.position.x; - depth_view.subImage.imageRect.offset.y = new_render_region.position.y; - depth_view.subImage.imageRect.extent.width = new_render_region.size.width; - depth_view.subImage.imageRect.extent.height = new_render_region.size.height; + for (uint32_t v = 0; v < render_state.primary_view_count; v++) { + render_state.depth_views[v].subImage.imageRect.offset.x = new_render_region.position.x; + render_state.depth_views[v].subImage.imageRect.offset.y = new_render_region.position.y; + render_state.depth_views[v].subImage.imageRect.extent.width = new_render_region.size.width; + render_state.depth_views[v].subImage.imageRect.extent.height = new_render_region.size.height; } } + for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { + wrapper->on_post_render(); + } + // must have: // - should_render set to true // - a valid view pose for projection_views[eye].pose to submit layer @@ -2718,9 +2944,9 @@ void OpenXRAPI::end_frame() { } } - for (uint32_t eye = 0; eye < render_state.views.size(); eye++) { - render_state.projection_views[eye].fov = render_state.views[eye].fov; - render_state.projection_views[eye].pose = render_state.views[eye].pose; + for (uint32_t v = 0; v < render_state.view_count; v++) { + render_state.projection_views[v].fov = render_state.view_fovs[v]; + render_state.projection_views[v].pose = render_state.view_poses[v]; } Vector ordered_layers_list; @@ -2974,10 +3200,10 @@ OpenXRAPI::OpenXRAPI() { case 1: { view_configuration = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; } break; - /* we don't support quad and observer configurations (yet) case 2: { - view_configuration = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_QUAD_VARJO; + view_configuration = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET; // This also works for XR_VIEW_CONFIGURATION_TYPE_PRIMARY_QUAD_VARJO. } break; + /* we don't support observer configuration (yet) case 3: { view_configuration = XR_VIEW_CONFIGURATION_TYPE_SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT; } break; diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index 45ab7f66b346..ef6aa7bfe709 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -167,7 +167,18 @@ class OpenXRAPI { XrSpace play_space = XR_NULL_HANDLE; XrSpace custom_play_space = XR_NULL_HANDLE; XrSpace view_space = XR_NULL_HANDLE; + + // Head info XRPose::TrackingConfidence head_pose_confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE; + XrPosef head_pose = { { 0.0, 0.0, 0.0, 1.0 }, { 0.0, 1.6, 0.0 } }; // While we haven't received tracking data, place the camera 1.6 meters above the origin by default. + Transform3D head_transform; + Vector3 head_linear_velocity; + Vector3 head_angular_velocity; + + // View (eye) info + bool view_pose_valid = false; + LocalVector view_offsets; + LocalVector view_fovs; RID velocity_texture; RID velocity_depth_texture; @@ -271,6 +282,7 @@ class OpenXRAPI { bool is_reference_space_supported(XrReferenceSpaceType p_reference_space); bool setup_play_space(); bool setup_view_space(); + void update_head_tracking(); bool load_supported_swapchain_formats(); bool is_swapchain_format_supported(int64_t p_swapchain_format); bool obtain_swapchain_formats(); @@ -360,12 +372,16 @@ class OpenXRAPI { uint64_t frame = 0; Rect2i render_region; - LocalVector views; LocalVector projection_views; LocalVector depth_views; // Only used by Composition Layer Depth Extension if available bool submit_depth_buffer = false; // if set to true we submit depth buffers to OpenXR if a suitable extension is enabled. bool use_subsampled_images = true; // We need to default to true for the warning to be shown if we fallback immediately at startup. + + uint32_t view_count = 0; + uint32_t primary_view_count = 0; bool view_pose_valid = false; + LocalVector view_poses; + LocalVector view_fovs; double z_near = 0.0; double z_far = 0.0; @@ -383,22 +399,26 @@ class OpenXRAPI { OpenXRSwapChainInfo main_swapchains[OPENXR_SWAPCHAIN_MAX]; } render_state; - static void _allocate_view_buffers_rt(uint32_t p_view_count, bool p_submit_depth_buffer); + static void _allocate_view_buffers_rt(uint32_t p_view_count, uint32_t p_primary_view_count, bool p_submit_depth_buffer); static void _set_render_session_running_rt(bool p_is_running); static void _set_render_display_info_rt(XrTime p_predicted_display_time, bool p_should_render); static void _set_render_play_space_rt(uint64_t p_play_space); static void _set_render_environment_blend_mode_rt(int32_t p_environment_blend_mode); static void _set_render_state_multiplier_rt(double p_render_target_size_multiplier); static void _set_render_state_render_region_rt(const Rect2i &p_render_region); + static void _set_render_state_view_poses(bool p_is_valid, const PackedVector4Array &p_orientations, const PackedVector3Array &p_positions, const PackedVector4Array &p_fovs); + static void _set_render_state_near_and_far(uint32_t p_view, double p_z_near, double p_z_far); static void _update_main_swapchain_size_rt(); - void allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer); + void allocate_view_buffers(uint32_t p_view_count, uint32_t p_primary_view_count, bool p_submit_depth_buffer); void set_render_session_running(bool p_is_running); void set_render_display_info(XrTime p_predicted_display_time, bool p_should_render); void set_render_play_space(XrSpace p_play_space); void set_render_environment_blend_mode(XrEnvironmentBlendMode p_mode); void set_render_state_multiplier(double p_render_target_size_multiplier); void set_render_state_render_region(const Rect2i &p_render_region); + void set_render_state_view_poses(bool p_is_valid, const PackedVector4Array &p_orientations, const PackedVector3Array &p_positions, const PackedVector4Array &p_fovs); + void set_render_state_near_and_far(uint32_t p_view, double p_z_near, double p_z_far); public: void update_main_swapchain_size(); @@ -454,6 +474,7 @@ class OpenXRAPI { XrFormFactor get_form_factor() const { return form_factor; } uint32_t get_view_count() const; + uint32_t get_primary_view_count() const; void set_view_configuration(XrViewConfigurationType p_view_configuration); XrViewConfigurationType get_view_configuration() const { return view_configuration; } @@ -482,8 +503,14 @@ class OpenXRAPI { XrHandTrackerEXT get_hand_tracker(int p_hand_index); Size2 get_recommended_target_size(); + Size2i get_recommended_target_size(uint32_t p_view); XRPose::TrackingConfidence get_head_center(Transform3D &r_transform, Vector3 &r_linear_velocity, Vector3 &r_angular_velocity); + TypedArray get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far); + TypedArray get_camera_offsets(const StringName &p_tracker_name); + bool get_view_offset(uint32_t p_view, Transform3D &r_transform); +#ifndef DISABLE_DEPRECATED bool get_view_transform(uint32_t p_view, Transform3D &r_transform); +#endif bool get_view_projection(uint32_t p_view, double p_z_near, double p_z_far, Projection &p_camera_matrix); Vector2 get_eye_focus(uint32_t p_view, float p_aspect); bool process(); @@ -504,6 +531,9 @@ class OpenXRAPI { void post_draw_viewport(RID p_render_target); void end_frame(); + // Rendering + void set_projection_view_swapchain_rt(uint32_t p_view, XrSwapchain p_swapchain, uint32_t image_array_index, Size2i p_size); + // Display refresh rate float get_display_refresh_rate() const; void set_display_refresh_rate(float p_refresh_rate); diff --git a/modules/openxr/openxr_api_extension.cpp b/modules/openxr/openxr_api_extension.cpp index f21bba753dc2..70f7c33adc94 100644 --- a/modules/openxr/openxr_api_extension.cpp +++ b/modules/openxr/openxr_api_extension.cpp @@ -55,6 +55,7 @@ void OpenXRAPIExtension::_bind_methods() { ClassDB::bind_method(D_METHOD("insert_debug_label", "label_name"), &OpenXRAPIExtension::insert_debug_label); ClassDB::bind_method(D_METHOD("get_view_count"), &OpenXRAPIExtension::get_view_count); + ClassDB::bind_method(D_METHOD("get_primary_view_count"), &OpenXRAPIExtension::get_primary_view_count); ClassDB::bind_method(D_METHOD("get_view_configuration"), &OpenXRAPIExtension::get_view_configuration); ClassDB::bind_method(D_METHOD("is_initialized"), &OpenXRAPIExtension::is_initialized); @@ -198,6 +199,11 @@ uint32_t OpenXRAPIExtension::get_view_count() const { return (uint32_t)OpenXRAPI::get_singleton()->get_view_count(); } +uint32_t OpenXRAPIExtension::get_primary_view_count() const { + ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), 0); + return (uint32_t)OpenXRAPI::get_singleton()->get_primary_view_count(); +} + uint64_t OpenXRAPIExtension::get_view_configuration() const { ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), 0); return (uint64_t)OpenXRAPI::get_singleton()->get_view_configuration(); diff --git a/modules/openxr/openxr_api_extension.h b/modules/openxr/openxr_api_extension.h index bd4ee3e35d75..f0f7c96372a4 100644 --- a/modules/openxr/openxr_api_extension.h +++ b/modules/openxr/openxr_api_extension.h @@ -78,6 +78,7 @@ class OpenXRAPIExtension : public RefCounted { void insert_debug_label(const String &p_label_name); uint32_t get_view_count() const; + uint32_t get_primary_view_count() const; uint64_t get_view_configuration() const; bool is_initialized(); diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp index 38e7916a34db..f6ccd3d34b84 100644 --- a/modules/openxr/openxr_interface.cpp +++ b/modules/openxr/openxr_interface.cpp @@ -69,6 +69,9 @@ void OpenXRInterface::_bind_methods() { ClassDB::bind_method(D_METHOD("is_user_presence_supported"), &OpenXRInterface::is_user_presence_supported); ClassDB::bind_method(D_METHOD("is_user_present"), &OpenXRInterface::is_user_present); + // View configuration + ClassDB::bind_method(D_METHOD("get_active_view_configuration"), &OpenXRInterface::get_active_view_configuration); + // Display refresh rate ClassDB::bind_method(D_METHOD("get_display_refresh_rate"), &OpenXRInterface::get_display_refresh_rate); ClassDB::bind_method(D_METHOD("set_display_refresh_rate", "refresh_rate"), &OpenXRInterface::set_display_refresh_rate); @@ -145,6 +148,12 @@ void OpenXRInterface::_bind_methods() { BIND_ENUM_CONSTANT(SESSION_STATE_LOSS_PENDING); BIND_ENUM_CONSTANT(SESSION_STATE_EXITING); + BIND_ENUM_CONSTANT(VIEW_CONFIGURATION_MONO); + BIND_ENUM_CONSTANT(VIEW_CONFIGURATION_STEREO); + BIND_ENUM_CONSTANT(VIEW_CONFIGURATION_STEREO_WITH_INSET); + BIND_ENUM_CONSTANT(VIEW_CONFIGURATION_UNSET); + BIND_ENUM_CONSTANT(VIEW_CONFIGURATION_UNKNOWN); + BIND_ENUM_CONSTANT(HAND_LEFT); BIND_ENUM_CONSTANT(HAND_RIGHT); BIND_ENUM_CONSTANT(HAND_MAX); @@ -702,8 +711,8 @@ bool OpenXRInterface::initialize() { // we must create a tracker for our head head.instantiate(); - head->set_tracker_type(XRServer::TRACKER_HEAD); - head->set_tracker_name("head"); + head->set_tracker_type(XRServer::TRACKER_CAMERA); + head->set_tracker_name(XR_TRACKER_HEAD); head->set_tracker_desc("Players head"); xr_server->add_tracker(head); @@ -1055,8 +1064,13 @@ Size2 OpenXRInterface::get_render_target_size() { } uint32_t OpenXRInterface::get_view_count() { - // TODO set this based on our configuration - return 2; + if (openxr_api == nullptr) { + return 2; + } else { + // We return our primary view count here, + // this controls our primary view! + return openxr_api->get_primary_view_count(); + } } void OpenXRInterface::_set_default_pos(Transform3D &r_transform, double p_world_scale, uint64_t p_eye) { @@ -1085,28 +1099,33 @@ Transform3D OpenXRInterface::get_camera_transform() { hmd_transform.basis = head_transform.basis; hmd_transform.origin = head_transform.origin * world_scale; - return hmd_transform; + return xr_server->get_reference_frame() * hmd_transform; +} + +TypedArray OpenXRInterface::get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) { + ERR_FAIL_NULL_V(openxr_api, TypedArray()); + + return openxr_api->get_camera_projections(p_tracker_name, p_aspect, p_z_near, p_z_far); +} + +TypedArray OpenXRInterface::get_camera_offsets(const StringName &p_tracker_name) { + ERR_FAIL_NULL_V(openxr_api, TypedArray()); + + return openxr_api->get_camera_offsets(p_tracker_name); } +#ifndef DISABLE_DEPRECATED Transform3D OpenXRInterface::get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL_V(xr_server, Transform3D()); + ERR_FAIL_NULL_V(openxr_api, Transform3D()); ERR_FAIL_UNSIGNED_INDEX_V_MSG(p_view, get_view_count(), Transform3D(), "View index outside bounds."); - Transform3D t; - if (openxr_api && openxr_api->get_view_transform(p_view, t)) { - // update our cached value if we have a valid transform - transform_for_view[p_view] = t; - } else { - // reuse cached value - t = transform_for_view[p_view]; - } - - // Apply our world scale - double world_scale = xr_server->get_world_scale(); - t.origin *= world_scale; + Transform3D view_offset; + openxr_api->get_view_offset(p_view, view_offset); + view_offset.origin *= xr_server->get_world_scale(); - return p_cam_transform * xr_server->get_reference_frame() * t; + return p_cam_transform * xr_server->get_reference_frame() * head_transform * view_offset; } Projection OpenXRInterface::get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) { @@ -1124,6 +1143,7 @@ Projection OpenXRInterface::get_projection_for_view(uint32_t p_view, double p_as return cm; } +#endif Rect2i OpenXRInterface::get_render_region() { if (openxr_api) { @@ -1499,6 +1519,24 @@ bool OpenXRInterface::is_user_present() const { } } +/** View configuration */ +OpenXRInterface::ViewConfiguration OpenXRInterface::get_active_view_configuration() const { + if (!openxr_api || !openxr_api->is_initialized()) { + return VIEW_CONFIGURATION_UNSET; + } else { + switch (openxr_api->get_view_configuration()) { + case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO: + return VIEW_CONFIGURATION_MONO; + case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO: + return VIEW_CONFIGURATION_STEREO; + case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET: + return VIEW_CONFIGURATION_STEREO_WITH_INSET; + default: + return VIEW_CONFIGURATION_UNKNOWN; + } + } +} + /** Hand tracking. */ void OpenXRInterface::set_motion_range(const Hand p_hand, const HandMotionRange p_motion_range) { ERR_FAIL_INDEX(p_hand, HAND_MAX); @@ -1643,6 +1681,9 @@ Vector3 OpenXRInterface::get_hand_joint_angular_velocity(Hand p_hand, HandJoints } RID OpenXRInterface::get_vrs_texture() { + // Note, vrs_mode == VRS_XR should not be used with foveated inset. + // Not sure where to best check for this (yet). + if (!openxr_api) { return RID(); } diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h index d79a05f30f58..41e39d6fb3a4 100644 --- a/modules/openxr/openxr_interface.h +++ b/modules/openxr/openxr_interface.h @@ -182,8 +182,12 @@ class OpenXRInterface : public XRInterface { virtual Size2 get_render_target_size() override; virtual uint32_t get_view_count() override; virtual Transform3D get_camera_transform() override; + virtual TypedArray get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) override; + virtual TypedArray get_camera_offsets(const StringName &p_tracker_name) override; +#ifndef DISABLE_DEPRECATED virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) override; virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) override; +#endif virtual Rect2i get_render_region() override; @@ -239,6 +243,17 @@ class OpenXRInterface : public XRInterface { bool is_user_presence_supported() const; bool is_user_present() const; + /** View configuration */ + enum ViewConfiguration { + VIEW_CONFIGURATION_MONO = 0, + VIEW_CONFIGURATION_STEREO = 1, + VIEW_CONFIGURATION_STEREO_WITH_INSET = 2, + VIEW_CONFIGURATION_UNSET = 254, + VIEW_CONFIGURATION_UNKNOWN = 255, + }; + + ViewConfiguration get_active_view_configuration() const; + /** Hand tracking. */ enum Hand { HAND_LEFT, @@ -345,6 +360,7 @@ class OpenXRInterface : public XRInterface { }; VARIANT_ENUM_CAST(OpenXRInterface::SessionState) +VARIANT_ENUM_CAST(OpenXRInterface::ViewConfiguration) VARIANT_ENUM_CAST(OpenXRInterface::Hand) VARIANT_ENUM_CAST(OpenXRInterface::HandMotionRange) VARIANT_ENUM_CAST(OpenXRInterface::HandTrackedSource) diff --git a/modules/openxr/register_types.cpp b/modules/openxr/register_types.cpp index ec3066b292be..9601d831e5be 100644 --- a/modules/openxr/register_types.cpp +++ b/modules/openxr/register_types.cpp @@ -59,6 +59,7 @@ #include "extensions/openxr_dpad_binding_extension.h" #include "extensions/openxr_eye_gaze_interaction.h" #include "extensions/openxr_fb_display_refresh_rate_extension.h" +#include "extensions/openxr_foveated_inset_extension.h" #include "extensions/openxr_frame_synthesis_extension.h" #include "extensions/openxr_future_extension.h" #include "extensions/openxr_hand_interaction_extension.h" @@ -88,6 +89,7 @@ #include "scene/openxr_composition_layer_cylinder.h" #include "scene/openxr_composition_layer_equirect.h" #include "scene/openxr_composition_layer_quad.h" +#include "scene/openxr_foveated_inset_viewport.h" #include "scene/openxr_visibility_mask.h" #ifdef MODULE_GLTF_ENABLED @@ -179,6 +181,7 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) { OpenXRAPI::register_extension_wrapper(memnew(OpenXRPerformanceSettingsExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRValveControllerExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRKHRGenericController)); + OpenXRAPI::register_extension_wrapper(memnew(OpenXRFoveatedInsetExtension)); // Futures extension has to be registered as a singleton so extensions can access it. OpenXRFutureExtension *future_extension = memnew(OpenXRFutureExtension); @@ -305,6 +308,7 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(OpenXRRenderModel); GDREGISTER_CLASS(OpenXRRenderModelManager); #endif + GDREGISTER_CLASS(OpenXRFoveatedInsetViewport); GDREGISTER_CLASS(OpenXRSpatialEntityExtension); GDREGISTER_VIRTUAL_CLASS(OpenXRSpatialEntityTracker); diff --git a/modules/openxr/scene/openxr_foveated_inset_viewport.cpp b/modules/openxr/scene/openxr_foveated_inset_viewport.cpp new file mode 100644 index 000000000000..19f5c5a4d819 --- /dev/null +++ b/modules/openxr/scene/openxr_foveated_inset_viewport.cpp @@ -0,0 +1,128 @@ +/**************************************************************************/ +/* openxr_foveated_inset_viewport.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "openxr_foveated_inset_viewport.h" + +#include "core/config/engine.h" +#include "core/object/class_db.h" +#include "servers/xr/xr_server.h" + +#include "modules/openxr/extensions/openxr_foveated_inset_extension.h" + +OpenXRFoveatedInsetViewport::OpenXRFoveatedInsetViewport() { + set_process_internal(true); + set_process_priority(99999); + set_update_mode(SubViewport::UPDATE_DISABLED); + + OpenXRFoveatedInsetExtension *fi_ext = OpenXRFoveatedInsetExtension::get_singleton(); + ERR_FAIL_NULL(fi_ext); + + fi_ext->register_viewport(get_viewport_rid()); + + xr_origin = memnew(XROrigin3D); + xr_origin->set_current(false); + add_child(xr_origin, false, Node3D::INTERNAL_MODE_BACK); + + xr_camera = memnew(XRCamera3D); + xr_camera->set_tracker(XR_TRACKER_INSET); + xr_origin->add_child(xr_camera, false, Node3D::INTERNAL_MODE_BACK); +} + +OpenXRFoveatedInsetViewport::~OpenXRFoveatedInsetViewport() { + OpenXRFoveatedInsetExtension *fi_ext = OpenXRFoveatedInsetExtension::get_singleton(); + ERR_FAIL_NULL(fi_ext); + + fi_ext->unregister_viewport(get_viewport_rid()); +} + +void OpenXRFoveatedInsetViewport::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_xr_origin3d"), &OpenXRFoveatedInsetViewport::get_xr_origin3d); + ClassDB::bind_method(D_METHOD("get_xr_camera3d"), &OpenXRFoveatedInsetViewport::get_xr_camera3d); +} + +void OpenXRFoveatedInsetViewport::_validate_property(PropertyInfo &p_property) const { + if (!Engine::get_singleton()->is_editor_hint()) { + return; + } + // Hide properties that are managed by us. + if (p_property.name == "size" || p_property.name == "size_2d_override" || p_property.name == "size_2d_override_stretch" || p_property.name == "view_count" || p_property.name == "use_xr" || p_property.name == "render_target_update_mode") { + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } +} + +void OpenXRFoveatedInsetViewport::_notification(int p_what) { + if (Engine::get_singleton()->is_editor_hint()) { + return; + } + + switch (p_what) { + case NOTIFICATION_INTERNAL_PROCESS: { + // Update the size of our viewport + _update_size(); + + // Apply our world origin + XRServer *xr_server = XRServer::get_singleton(); + if (xr_server) { + xr_origin->set_transform(xr_server->get_world_origin()); + } + } break; + } +} + +void OpenXRFoveatedInsetViewport::_update_size() { + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + OpenXRFoveatedInsetExtension *fi_ext = OpenXRFoveatedInsetExtension::get_singleton(); + if (openxr_api == nullptr || fi_ext == nullptr) { + set_update_mode(SubViewport::UPDATE_DISABLED); + return; + } + + if (!openxr_api->is_initialized()) { + set_update_mode(SubViewport::UPDATE_DISABLED); + return; + } + + if (openxr_api->get_view_configuration() != XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO_WITH_FOVEATED_INSET) { + WARN_PRINT_ONCE("View configuration is not set to stereo with foveated inset, or this is not supported."); + set_update_mode(SubViewport::UPDATE_DISABLED); + return; + } + + set_update_mode(SubViewport::UPDATE_ALWAYS); + + Size2i new_size = fi_ext->get_render_size(); + if (get_size() != new_size) { + set_size(new_size); + } + + if (get_view_count() != 2) { + set_view_count(2); + } +} diff --git a/modules/openxr/scene/openxr_foveated_inset_viewport.h b/modules/openxr/scene/openxr_foveated_inset_viewport.h new file mode 100644 index 000000000000..74d95312d5b7 --- /dev/null +++ b/modules/openxr/scene/openxr_foveated_inset_viewport.h @@ -0,0 +1,56 @@ +/**************************************************************************/ +/* openxr_foveated_inset_viewport.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "scene/3d/xr/xr_nodes.h" +#include "scene/main/viewport.h" + +class OpenXRFoveatedInsetViewport : public SubViewport { + GDCLASS(OpenXRFoveatedInsetViewport, SubViewport); + +public: + OpenXRFoveatedInsetViewport(); + ~OpenXRFoveatedInsetViewport(); + + XROrigin3D *get_xr_origin3d() const { return xr_origin; } + XRCamera3D *get_xr_camera3d() const { return xr_camera; } + +protected: + static void _bind_methods(); + void _validate_property(PropertyInfo &p_property) const; + void _notification(int p_what); + +private: + void _update_size(); + + XROrigin3D *xr_origin = nullptr; + XRCamera3D *xr_camera = nullptr; +}; diff --git a/modules/webxr/webxr_interface_js.cpp b/modules/webxr/webxr_interface_js.cpp index 11e89ebc2202..94ea3ce6900f 100644 --- a/modules/webxr/webxr_interface_js.cpp +++ b/modules/webxr/webxr_interface_js.cpp @@ -311,8 +311,8 @@ bool WebXRInterfaceJS::initialize() { head_transform.basis = Basis(); head_transform.origin = Vector3(); head_tracker.instantiate(); - head_tracker->set_tracker_type(XRServer::TRACKER_HEAD); - head_tracker->set_tracker_name("head"); + head_tracker->set_tracker_type(XRServer::TRACKER_CAMERA); + head_tracker->set_tracker_name(XR_TRACKER_HEAD); head_tracker->set_tracker_desc("Players head"); xr_server->add_tracker(head_tracker); @@ -454,6 +454,87 @@ Transform3D WebXRInterfaceJS::get_camera_transform() { return camera_transform; } +TypedArray WebXRInterfaceJS::get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) { + TypedArray camera_projections; + + if (p_tracker_name != XR_TRACKER_HEAD) { + return camera_projections; + } + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL_V(xr_server, camera_projections); + ERR_FAIL_COND_V(!initialized, camera_projections); + + for (uint32_t v = 0; v < get_view_count(); v++) { + Projection view; + + float js_matrix[16]; + bool has_projection = godot_webxr_get_projection_for_view(v, js_matrix); + if (!has_projection) { + return camera_projections; + } + + int k = 0; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + view.columns[i][j] = js_matrix[k++]; + } + } + + // Copied from godot_oculus_mobile's ovr_mobile_session.cpp + view.columns[2][2] = -(p_z_far + p_z_near) / (p_z_far - p_z_near); + view.columns[3][2] = -(2.0f * p_z_far * p_z_near) / (p_z_far - p_z_near); + + camera_projections.push_back(view); + } + + return camera_projections; +} + +TypedArray WebXRInterfaceJS::get_camera_offsets(const StringName &p_tracker_name) { + TypedArray camera_offsets; + + if (p_tracker_name != XR_TRACKER_HEAD) { + return camera_offsets; + } + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL_V(xr_server, camera_offsets); + ERR_FAIL_COND_V(!initialized, camera_offsets); + + // Get our world scale + double world_scale = xr_server->get_world_scale(); + + // Get our head transform + float js_matrix[16]; + bool has_transform = godot_webxr_get_transform_for_view(-1, js_matrix); + if (!has_transform) { + return camera_offsets; + } + + Transform3D inv_head_transform = _js_matrix_to_transform(js_matrix).inverse(); + + for (uint32_t v = 0; v < get_view_count(); v++) { + // Get our view transform + has_transform = godot_webxr_get_transform_for_view(v, js_matrix); + if (!has_transform) { + return camera_offsets; + } + + Transform3D transform_for_view = _js_matrix_to_transform(js_matrix); + + // Calculate the offset + Transform3D offset = inv_head_transform * transform_for_view; + + offset.origin *= world_scale; + + camera_offsets.push_back(offset); + } + + return camera_offsets; +} + +#ifndef DISABLE_DEPRECATED Transform3D WebXRInterfaceJS::get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL_V(xr_server, p_cam_transform); @@ -497,6 +578,7 @@ Projection WebXRInterfaceJS::get_projection_for_view(uint32_t p_view, double p_a return view; } +#endif bool WebXRInterfaceJS::pre_draw_viewport(RID p_render_target) { GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton(); diff --git a/modules/webxr/webxr_interface_js.h b/modules/webxr/webxr_interface_js.h index 3c6bf3c65c85..5395f5cd06ca 100644 --- a/modules/webxr/webxr_interface_js.h +++ b/modules/webxr/webxr_interface_js.h @@ -131,8 +131,12 @@ class WebXRInterfaceJS : public WebXRInterface { virtual Size2 get_render_target_size() override; virtual uint32_t get_view_count() override; virtual Transform3D get_camera_transform() override; + virtual TypedArray get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) override; + virtual TypedArray get_camera_offsets(const StringName &p_tracker_name) override; +#ifndef DISABLE_DEPRECATED virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) override; virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) override; +#endif virtual bool pre_draw_viewport(RID p_render_target) override; virtual Vector post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) override; virtual RID get_color_texture() override; diff --git a/scene/3d/camera_3d.h b/scene/3d/camera_3d.h index 7fd88f412ed1..8667c23fd3c9 100644 --- a/scene/3d/camera_3d.h +++ b/scene/3d/camera_3d.h @@ -117,7 +117,7 @@ class Camera3D : public Node3D { void _update_camera(); virtual void _request_camera_update(); - void _update_camera_mode(); + virtual void _update_camera_mode(); virtual void fti_pump_property() override; virtual void fti_update_servers_property() override; diff --git a/scene/3d/xr/xr_nodes.cpp b/scene/3d/xr/xr_nodes.cpp index 7f58ca9a6da0..64d41e2a6f58 100644 --- a/scene/3d/xr/xr_nodes.cpp +++ b/scene/3d/xr/xr_nodes.cpp @@ -36,10 +36,17 @@ #include "core/object/class_db.h" #include "scene/main/scene_tree.h" #include "scene/main/viewport.h" +#include "servers/rendering/rendering_server.h" #include "servers/xr/xr_interface.h" //////////////////////////////////////////////////////////////////////////////////////////////////// +void XRCamera3D::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_tracker", "tracker_name"), &XRCamera3D::set_tracker); + ClassDB::bind_method(D_METHOD("get_tracker"), &XRCamera3D::get_tracker); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "tracker", PROPERTY_HINT_ENUM_SUGGESTION), "set_tracker", "get_tracker"); +} + void XRCamera3D::_validate_property(PropertyInfo &p_property) const { if (!Engine::get_singleton()->is_editor_hint()) { return; @@ -47,21 +54,99 @@ void XRCamera3D::_validate_property(PropertyInfo &p_property) const { // Hide properties that are managed by XRInterface or otherwise not applicable for XRCamera3D. if (p_property.name == "fov" || p_property.name == "projection" || p_property.name == "size" || p_property.name == "frustum_offset" || p_property.name == "keep_aspect") { p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } else if (p_property.name == "tracker") { + // Set to our default head tracker. + String hint_string = "head"; + p_property.hint_string = hint_string; + } +} + +void XRCamera3D::set_tracker(const StringName &p_tracker_name) { + if (tracker.is_valid() && tracker->get_tracker_name() == p_tracker_name) { + // didn't change + return; + } + + // just in case + _unbind_tracker(); + + // copy the name + tracker_name = p_tracker_name; + pose_name = SceneStringName(default_); + + // see if it's already available + _bind_tracker(); + + update_configuration_warnings(); + notify_property_list_changed(); +} + +StringName XRCamera3D::get_tracker() const { + return tracker_name; +} + +void XRCamera3D::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_INTERNAL_PROCESS: { + if (is_current()) { + _update_projections(); + } + } break; } } +void XRCamera3D::_update_camera_mode() { + // Ignore this here, we are setting our projection matrices later. +} + +void XRCamera3D::fti_update_servers_property() { + // Skip the logic in Camera3D. + Node3D::fti_update_servers_property(); +} + +void XRCamera3D::_update_projections() { + if (tracker.is_null()) { + return; + } + + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL(xr_server); + Viewport *vp = get_viewport(); + ERR_FAIL_NULL(vp); + + Size2 viewport_size = vp->get_visible_rect().size; + + rendering_server->camera_set_projections( + get_camera(), + xr_server->get_camera_projections(tracker_name, viewport_size.aspect(), get_near(), get_far()), + xr_server->get_camera_offsets(tracker_name)); + + return; +} + void XRCamera3D::_bind_tracker() { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); - tracker = xr_server->get_tracker(tracker_name); - if (tracker.is_valid()) { - tracker->connect("pose_changed", callable_mp(this, &XRCamera3D::_pose_changed)); + Ref new_tracker = xr_server->get_tracker(tracker_name); + if (new_tracker.is_null()) { + // Fail silently, tracker does not yet exist. + return; + } else if (tracker == new_tracker) { + // Already bound? + return; + } - Ref pose = tracker->get_pose(pose_name); - if (pose.is_valid()) { - set_transform(pose->get_adjusted_transform()); - } + // Assign our new tracker + tracker = new_tracker; + tracker->connect("pose_changed", callable_mp(this, &XRCamera3D::_pose_changed)); + + // Update our initial pose + Ref pose = tracker->get_pose(pose_name); + if (pose.is_valid()) { + set_transform(pose->get_adjusted_transform()); } } @@ -74,6 +159,8 @@ void XRCamera3D::_unbind_tracker() { void XRCamera3D::_changed_tracker(const StringName &p_tracker_name, int p_tracker_type) { if (p_tracker_name == tracker_name) { + // Rebind it. + _unbind_tracker(); _bind_tracker(); } } @@ -222,6 +309,9 @@ XRCamera3D::XRCamera3D() { // XRCamera3D gets its transform updated every render frame and shouldn't be interpolated. set_physics_interpolation_mode(Node::PHYSICS_INTERPOLATION_MODE_OFF); + // Run internal process at runtime. + set_desired_process_modes(!Engine::get_singleton()->is_editor_hint(), false); + XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); diff --git a/scene/3d/xr/xr_nodes.h b/scene/3d/xr/xr_nodes.h index d213db009090..d81cd29c36a0 100644 --- a/scene/3d/xr/xr_nodes.h +++ b/scene/3d/xr/xr_nodes.h @@ -41,13 +41,17 @@ class XRCamera3D : public Camera3D { GDCLASS(XRCamera3D, Camera3D); protected: - // The name and pose for our HMD tracker is currently the only hardcoded bit. - // If we ever are able to support multiple HMDs we may need to make this settable. - StringName tracker_name = "head"; + StringName tracker_name = XR_TRACKER_HEAD; StringName pose_name = SceneStringName(default_); Ref tracker; + static void _bind_methods(); void _validate_property(PropertyInfo &p_property) const; + void _notification(int p_what); + + virtual void _update_camera_mode() override; + virtual void fti_update_servers_property() override; + void _update_projections(); void _bind_tracker(); void _unbind_tracker(); @@ -64,6 +68,9 @@ class XRCamera3D : public Camera3D { virtual Vector3 project_position(const Point2 &p_point, real_t p_z_depth) const override; virtual Vector get_frustum() const override; + void set_tracker(const StringName &p_tracker_name); + StringName get_tracker() const; + XRCamera3D(); ~XRCamera3D(); }; diff --git a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp index 4c539a5fc199..b07e325eec96 100644 --- a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp @@ -1361,6 +1361,9 @@ void RendererSceneRenderRD::render_scene(const Ref &p_render Ref rb = p_render_buffers; ERR_FAIL_COND(rb.is_null()); + // View count of our render target must match our camera data. + ERR_FAIL_COND(rb->get_view_count() != p_camera_data->view_count); + // setup scene data RenderSceneDataRD scene_data; { @@ -1368,6 +1371,7 @@ void RendererSceneRenderRD::render_scene(const Ref &p_render scene_data.cam_transform = p_camera_data->main_transform; scene_data.cam_projection = p_camera_data->main_projection; scene_data.cam_orthogonal = p_camera_data->is_orthogonal; + scene_data.cam_asymmetrical = p_camera_data->is_asymmetrical; scene_data.camera_visible_layers = p_camera_data->visible_layers; scene_data.taa_jitter = p_camera_data->taa_jitter; scene_data.taa_frame_count = p_camera_data->taa_frame_count; diff --git a/servers/rendering/renderer_rd/storage_rd/render_scene_data_rd.h b/servers/rendering/renderer_rd/storage_rd/render_scene_data_rd.h index 85a9a1e9beb2..f242aad9354a 100644 --- a/servers/rendering/renderer_rd/storage_rd/render_scene_data_rd.h +++ b/servers/rendering/renderer_rd/storage_rd/render_scene_data_rd.h @@ -48,6 +48,7 @@ class RenderSceneDataRD : public RenderSceneData { float taa_frame_count = 0.0f; uint32_t camera_visible_layers; bool cam_orthogonal = false; + bool cam_asymmetrical = false; bool flip_y = false; // For billboards to cast correct shadows. diff --git a/servers/rendering/renderer_scene_cull.cpp b/servers/rendering/renderer_scene_cull.cpp index 4d3b9190b700..e8ddd423040b 100644 --- a/servers/rendering/renderer_scene_cull.cpp +++ b/servers/rendering/renderer_scene_cull.cpp @@ -41,7 +41,6 @@ #ifndef XR_DISABLED #include "servers/xr/xr_interface.h" -#include "servers/xr/xr_server.h" #endif //#define DEBUG_CULL_TIME @@ -94,6 +93,8 @@ void RendererSceneCull::camera_set_perspective(RID p_camera, float p_fovy_degree camera->fov = p_fovy_degrees; camera->znear = p_z_near; camera->zfar = p_z_far; + camera->offsets.clear(); + camera->projections.clear(); } void RendererSceneCull::camera_set_orthogonal(RID p_camera, float p_size, float p_z_near, float p_z_far) { @@ -103,6 +104,8 @@ void RendererSceneCull::camera_set_orthogonal(RID p_camera, float p_size, float camera->size = p_size; camera->znear = p_z_near; camera->zfar = p_z_far; + camera->offsets.clear(); + camera->projections.clear(); } void RendererSceneCull::camera_set_frustum(RID p_camera, float p_size, Vector2 p_offset, float p_z_near, float p_z_far) { @@ -113,6 +116,32 @@ void RendererSceneCull::camera_set_frustum(RID p_camera, float p_size, Vector2 p camera->offset = p_offset; camera->znear = p_z_near; camera->zfar = p_z_far; + camera->offsets.clear(); + camera->projections.clear(); +} + +void RendererSceneCull::camera_set_projections(RID p_camera, TypedArray p_projections, TypedArray p_offsets) { + ERR_FAIL_COND(p_projections.is_empty()); + bool has_offsets = !p_offsets.is_empty(); + ERR_FAIL_COND(has_offsets && p_offsets.size() != p_projections.size()); + + for (const Projection projection : p_projections) { + ERR_FAIL_COND_MSG(!projection.is_z_axis_projection(), "Projections must be z-axis aligned. Use [param p_offsets] to apply any transforms."); + } + + Camera *camera = camera_owner.get_or_null(p_camera); + ERR_FAIL_NULL(camera); + camera->type = Camera::PROJECTION; + camera->offsets.resize(p_projections.size()); + camera->projections.resize(p_projections.size()); + for (int i = 0; i < p_projections.size(); i++) { + if (has_offsets) { + camera->offsets[i] = p_offsets[i]; + } else { + camera->offsets[i] = Transform3D(); + } + camera->projections[i] = p_projections[i]; + } } void RendererSceneCull::camera_set_transform(RID p_camera, const Transform3D &p_transform) { @@ -2663,7 +2692,7 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons return animated_material_found; } -void RendererSceneCull::render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_screen_mesh_lod_threshold, RID p_shadow_atlas, Ref &p_xr_interface, float p_window_output_max_value, RenderingServerTypes::RenderInfo *r_render_info) { +void RendererSceneCull::render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_screen_mesh_lod_threshold, RID p_shadow_atlas, float p_window_output_max_value, RenderingServerTypes::RenderInfo *r_render_info) { #ifndef _3D_DISABLED Camera *camera = camera_owner.get_or_null(p_camera); @@ -2690,89 +2719,79 @@ void RendererSceneCull::render_camera(const Ref &p_render_bu RendererSceneRender::CameraData camera_data; // Setup Camera(s) - if (p_xr_interface.is_null()) { - // Normal camera - Transform3D transform = camera->transform; - Projection projection; - bool vaspect = camera->vaspect; - bool is_orthogonal = false; - - switch (camera->type) { - case Camera::ORTHOGONAL: { - projection.set_orthogonal( - camera->size, - p_viewport_size.width / (float)p_viewport_size.height, - camera->znear, - camera->zfar, - camera->vaspect); - is_orthogonal = true; - } break; - case Camera::PERSPECTIVE: { - projection.set_perspective( - camera->fov, - p_viewport_size.width / (float)p_viewport_size.height, - camera->znear, - camera->zfar, - camera->vaspect); + // Normal camera + Transform3D transform = camera->transform; + bool vaspect = camera->vaspect; + bool is_orthogonal = false; + bool is_asymmetrical = false; + + switch (camera->type) { + case Camera::ORTHOGONAL: { + Projection projection; + projection.set_orthogonal( + camera->size, + p_viewport_size.width / (float)p_viewport_size.height, + camera->znear, + camera->zfar, + camera->vaspect); + is_orthogonal = true; + + camera->offsets.resize(1); + camera->offsets[0] = Transform3D(); + camera->projections.resize(1); + camera->projections[0] = projection; + } break; + case Camera::PERSPECTIVE: { + Projection projection; + projection.set_perspective( + camera->fov, + p_viewport_size.width / (float)p_viewport_size.height, + camera->znear, + camera->zfar, + camera->vaspect); + + camera->offsets.resize(1); + camera->offsets[0] = Transform3D(); + camera->projections.resize(1); + camera->projections[0] = projection; + } break; + case Camera::FRUSTUM: { + Projection projection; + projection.set_frustum( + camera->size, + p_viewport_size.width / (float)p_viewport_size.height, + camera->offset, + camera->znear, + camera->zfar, + camera->vaspect); + + camera->offsets.resize(1); + camera->offsets[0] = Transform3D(); + camera->projections.resize(1); + camera->projections[0] = projection; + } break; + case Camera::PROJECTION: + break; + } - } break; - case Camera::FRUSTUM: { - projection.set_frustum( - camera->size, - p_viewport_size.width / (float)p_viewport_size.height, - camera->offset, - camera->znear, - camera->zfar, - camera->vaspect); - } break; - } + for (const Projection &projection : camera->projections) { + is_orthogonal |= projection.is_orthogonal(); + is_asymmetrical |= projection.is_asymmetrical(); + } - camera_data.set_camera(transform, projection, is_orthogonal, vaspect, jitter, taa_frame_count, camera->visible_layers); -#ifndef XR_DISABLED + if (camera->projections.size() == 1) { + camera_data.set_camera(transform * camera->offsets[0], camera->projections[0], is_orthogonal, is_asymmetrical, vaspect, jitter, taa_frame_count, camera->visible_layers); + } else if (camera->projections.size() == 2) { + camera_data.set_multiview_camera(transform, camera->offsets, camera->projections, is_orthogonal, is_asymmetrical, vaspect, camera->visible_layers); } else { - XRServer *xr_server = XRServer::get_singleton(); - - // Setup our camera for our XR interface. - // We can support multiple views here each with their own camera - Transform3D transforms[RendererSceneRender::MAX_RENDER_VIEWS]; - Projection projections[RendererSceneRender::MAX_RENDER_VIEWS]; - - uint32_t view_count = p_xr_interface->get_view_count(); - ERR_FAIL_COND_MSG(view_count == 0 || view_count > RendererSceneRender::MAX_RENDER_VIEWS, "Requested view count is not supported"); - - float aspect = p_viewport_size.width / (float)p_viewport_size.height; - - Transform3D world_origin = xr_server->get_world_origin(); - - // We ignore our camera position, it will have been positioned with a slightly old tracking position. - // Instead we take our origin point and have our XR interface add fresh tracking data! Whoohoo! - for (uint32_t v = 0; v < view_count; v++) { - transforms[v] = p_xr_interface->get_transform_for_view(v, world_origin); - projections[v] = p_xr_interface->get_projection_for_view(v, aspect, camera->znear, camera->zfar); - } - - // If requested, we move the views to be rendered as if the HMD is at the XROrigin. - if (unlikely(xr_server->is_camera_locked_to_origin())) { - Transform3D camera_reset = p_xr_interface->get_camera_transform().affine_inverse() * xr_server->get_reference_frame().affine_inverse(); - for (uint32_t v = 0; v < view_count; v++) { - transforms[v] *= camera_reset; - } - } - - if (view_count == 1) { - camera_data.set_camera(transforms[0], projections[0], false, camera->vaspect, jitter, p_jitter_phase_count, camera->visible_layers); - } else if (view_count == 2) { - camera_data.set_multiview_camera(view_count, transforms, projections, false, camera->vaspect, camera->visible_layers); - } else { - // this won't be called (see fail check above) but keeping this comment to indicate we may support more then 2 views in the future... - } -#endif // XR_DISABLED + ERR_FAIL_MSG("Unsupported camera setup."); } RID environment = _render_get_environment(p_camera, p_scenario); RID compositor = _render_get_compositor(p_camera, p_scenario); RENDER_TIMESTAMP("Update Occlusion Buffer") + // For now just cull on the first camera RendererSceneOcclusionCull::get_singleton()->buffer_update(p_viewport, camera_data.main_transform, camera_data.main_projection, camera_data.is_orthogonal); @@ -3764,7 +3783,7 @@ void RendererSceneCull::render_empty_scene(const Ref &p_rend RENDER_TIMESTAMP("Render Empty 3D Scene"); RendererSceneRender::CameraData camera_data; - camera_data.set_camera(Transform3D(), Projection(), true, false); + camera_data.set_camera(Transform3D(), Projection(), true, false, false); scene_render->render_scene(p_render_buffers, &camera_data, &camera_data, PagedArray(), PagedArray(), PagedArray(), PagedArray(), PagedArray(), PagedArray(), PagedArray(), environment, RID(), compositor, p_shadow_atlas, RID(), scenario->reflection_atlas, RID(), 0, 0, nullptr, 0, nullptr, 0, p_window_output_max_value, nullptr); #endif @@ -3830,7 +3849,7 @@ bool RendererSceneCull::_render_reflection_probe_step(Instance *p_instance, int RendererSceneRender::CameraData camera_data; Transform3D xform = p_instance->transform * local_view; - camera_data.set_camera(xform, cm, false, false); + camera_data.set_camera(xform, cm, false, false, false); RENDER_TIMESTAMP("Render ReflectionProbe, Face " + itos(face)); _render_scene(&camera_data, render_buffers, environment, RID(), RID(), RSG::light_storage->reflection_probe_get_cull_mask(p_instance->base), p_instance->scenario->self, RID(), shadow_atlas, reflection_probe->instance, face, mesh_lod_threshold, use_shadows); diff --git a/servers/rendering/renderer_scene_cull.h b/servers/rendering/renderer_scene_cull.h index 6d9279889d0c..9379479bd882 100644 --- a/servers/rendering/renderer_scene_cull.h +++ b/servers/rendering/renderer_scene_cull.h @@ -75,7 +75,8 @@ class RendererSceneCull : public RenderingMethod { enum Type { PERSPECTIVE, ORTHOGONAL, - FRUSTUM + FRUSTUM, + PROJECTION, }; Type type; float fov; @@ -88,7 +89,9 @@ class RendererSceneCull : public RenderingMethod { RID attributes; RID compositor; - Transform3D transform; + Transform3D transform; // Our main camera transform + LocalVector offsets; // camera offsets for each view + LocalVector projections; // projection matrix for each view Camera() { visible_layers = 0xFFFFFFFF; @@ -110,6 +113,7 @@ class RendererSceneCull : public RenderingMethod { virtual void camera_set_perspective(RID p_camera, float p_fovy_degrees, float p_z_near, float p_z_far); virtual void camera_set_orthogonal(RID p_camera, float p_size, float p_z_near, float p_z_far); virtual void camera_set_frustum(RID p_camera, float p_size, Vector2 p_offset, float p_z_near, float p_z_far); + virtual void camera_set_projections(RID p_camera, TypedArray p_projections, TypedArray p_offsets = TypedArray()); virtual void camera_set_transform(RID p_camera, const Transform3D &p_transform); virtual void camera_set_cull_mask(RID p_camera, uint32_t p_layers); virtual void camera_set_environment(RID p_camera, RID p_env); @@ -1160,7 +1164,7 @@ class RendererSceneCull : public RenderingMethod { void _render_scene(const RendererSceneRender::CameraData *p_camera_data, const Ref &p_render_buffers, RID p_environment, RID p_force_camera_attributes, RID p_compositor, uint32_t p_visible_layers, RID p_scenario, RID p_viewport, RID p_shadow_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, float p_window_output_max_value, bool p_using_shadows = true, RenderingServerTypes::RenderInfo *r_render_info = nullptr); void render_empty_scene(const Ref &p_render_buffers, RID p_scenario, RID p_shadow_atlas, float p_window_output_max_value); - void render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_screen_mesh_lod_threshold, RID p_shadow_atlas, Ref &p_xr_interface, float p_window_output_max_value, RenderingServerTypes::RenderInfo *r_render_info = nullptr); + void render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_screen_mesh_lod_threshold, RID p_shadow_atlas, float p_window_output_max_value, RenderingServerTypes::RenderInfo *r_render_info = nullptr); void update_dirty_instances() const; void render_particle_colliders(); diff --git a/servers/rendering/renderer_scene_render.cpp b/servers/rendering/renderer_scene_render.cpp index 6223cbaa7978..efa941899c0f 100644 --- a/servers/rendering/renderer_scene_render.cpp +++ b/servers/rendering/renderer_scene_render.cpp @@ -35,9 +35,10 @@ ///////////////////////////////////////////////////////////////////////////// // CameraData -void RendererSceneRender::CameraData::set_camera(const Transform3D p_transform, const Projection p_projection, bool p_is_orthogonal, bool p_vaspect, const Vector2 &p_taa_jitter, float p_taa_frame_count, uint32_t p_visible_layers) { +void RendererSceneRender::CameraData::set_camera(const Transform3D p_transform, const Projection p_projection, bool p_is_orthogonal, bool p_is_asymmetrical, bool p_vaspect, const Vector2 &p_taa_jitter, float p_taa_frame_count, uint32_t p_visible_layers) { view_count = 1; is_orthogonal = p_is_orthogonal; + is_asymmetrical = p_is_asymmetrical; vaspect = p_vaspect; main_transform = p_transform; @@ -50,141 +51,22 @@ void RendererSceneRender::CameraData::set_camera(const Transform3D p_transform, taa_frame_count = p_taa_frame_count; } -void RendererSceneRender::CameraData::set_multiview_camera(uint32_t p_view_count, const Transform3D *p_transforms, const Projection *p_projections, bool p_is_orthogonal, bool p_vaspect, uint32_t p_visible_layers) { - ERR_FAIL_COND_MSG(p_view_count != 2, "Incorrect view count for stereoscopic view"); +void RendererSceneRender::CameraData::set_multiview_camera(const Transform3D &p_transform, const LocalVector &p_offsets, const LocalVector &p_projections, bool p_is_orthogonal, bool p_is_asymmetrical, bool p_vaspect, uint32_t p_visible_layers) { + ERR_FAIL_COND_MSG(p_projections.size() != 2, "Incorrect view count for stereoscopic view"); + ERR_FAIL_COND(p_projections.size() != p_offsets.size()); visible_layers = p_visible_layers; - view_count = p_view_count; + view_count = p_projections.size(); is_orthogonal = p_is_orthogonal; + is_asymmetrical = p_is_asymmetrical; vaspect = p_vaspect; Vector planes[2]; - ///////////////////////////////////////////////////////////////////////////// - // Figure out our center transform - - // 1. obtain our planes - for (uint32_t v = 0; v < view_count; v++) { - planes[v] = p_projections[v].get_projection_planes(p_transforms[v]); - } - - // 2. average and normalize plane normals to obtain z vector, cross them to obtain y vector, and from there the x vector for combined camera basis. - Vector3 n0 = planes[0][Projection::PLANE_LEFT].normal; - Vector3 n1 = planes[1][Projection::PLANE_RIGHT].normal; - Vector3 z = (n0 + n1).normalized(); - Vector3 y = n0.cross(n1).normalized(); - Vector3 x = y.cross(z).normalized(); - y = z.cross(x).normalized(); - main_transform.basis.set_columns(x, y, z); - - // 3. create a horizon plane with one of the eyes and the up vector as normal. - Plane horizon(y, p_transforms[0].origin); - - // 4. Intersect horizon, left and right to obtain the combined camera origin. - ERR_FAIL_COND_MSG( - !horizon.intersect_3(planes[0][Projection::PLANE_LEFT], planes[1][Projection::PLANE_RIGHT], &main_transform.origin), "Can't determine camera origin"); - - // handy to have the inverse of the transform we just build - Transform3D main_transform_inv = main_transform.inverse(); - - // 5. figure out far plane, this could use some improvement, we may have our far plane too close like this, not sure if this matters - Vector3 far_center = (planes[0][Projection::PLANE_FAR].get_center() + planes[1][Projection::PLANE_FAR].get_center()) * 0.5; - Plane far_plane = Plane(-z, far_center); - - ///////////////////////////////////////////////////////////////////////////// - // Figure out our top/bottom planes - - // 6. Intersect far and left planes with top planes from both eyes, save the point with highest y as top_left. - Vector3 top_left, other; - ERR_FAIL_COND_MSG( - !far_plane.intersect_3(planes[0][Projection::PLANE_LEFT], planes[0][Projection::PLANE_TOP], &top_left), "Can't determine left camera far/left/top vector"); - ERR_FAIL_COND_MSG( - !far_plane.intersect_3(planes[1][Projection::PLANE_LEFT], planes[1][Projection::PLANE_TOP], &other), "Can't determine right camera far/left/top vector"); - if (y.dot(top_left) < y.dot(other)) { - top_left = other; - } - - // 7. Intersect far and left planes with bottom planes from both eyes, save the point with lowest y as bottom_left. - Vector3 bottom_left; - ERR_FAIL_COND_MSG( - !far_plane.intersect_3(planes[0][Projection::PLANE_LEFT], planes[0][Projection::PLANE_BOTTOM], &bottom_left), "Can't determine left camera far/left/bottom vector"); - ERR_FAIL_COND_MSG( - !far_plane.intersect_3(planes[1][Projection::PLANE_LEFT], planes[1][Projection::PLANE_BOTTOM], &other), "Can't determine right camera far/left/bottom vector"); - if (y.dot(other) < y.dot(bottom_left)) { - bottom_left = other; - } - - // 8. Intersect far and right planes with top planes from both eyes, save the point with highest y as top_right. - Vector3 top_right; - ERR_FAIL_COND_MSG( - !far_plane.intersect_3(planes[0][Projection::PLANE_RIGHT], planes[0][Projection::PLANE_TOP], &top_right), "Can't determine left camera far/right/top vector"); - ERR_FAIL_COND_MSG( - !far_plane.intersect_3(planes[1][Projection::PLANE_RIGHT], planes[1][Projection::PLANE_TOP], &other), "Can't determine right camera far/right/top vector"); - if (y.dot(top_right) < y.dot(other)) { - top_right = other; - } - - // 9. Intersect far and right planes with bottom planes from both eyes, save the point with lowest y as bottom_right. - Vector3 bottom_right; - ERR_FAIL_COND_MSG( - !far_plane.intersect_3(planes[0][Projection::PLANE_RIGHT], planes[0][Projection::PLANE_BOTTOM], &bottom_right), "Can't determine left camera far/right/bottom vector"); - ERR_FAIL_COND_MSG( - !far_plane.intersect_3(planes[1][Projection::PLANE_RIGHT], planes[1][Projection::PLANE_BOTTOM], &other), "Can't determine right camera far/right/bottom vector"); - if (y.dot(other) < y.dot(bottom_right)) { - bottom_right = other; - } - - // 10. Create top plane with these points: camera origin, top_left, top_right - Plane top(main_transform.origin, top_left, top_right); - - // 11. Create bottom plane with these points: camera origin, bottom_left, bottom_right - Plane bottom(main_transform.origin, bottom_left, bottom_right); - - ///////////////////////////////////////////////////////////////////////////// - // Figure out our near plane points - - // 12. Create a near plane using -camera z and the eye further along in that axis. - Plane near_plane; - Vector3 neg_z = -z; - if (neg_z.dot(p_transforms[1].origin) < neg_z.dot(p_transforms[0].origin)) { - near_plane = Plane(neg_z, p_transforms[0].origin); - } else { - near_plane = Plane(neg_z, p_transforms[1].origin); - } - - // 13. Intersect near plane with bottm/left planes, to obtain min_vec then top/right to obtain max_vec - Vector3 min_vec; - ERR_FAIL_COND_MSG( - !near_plane.intersect_3(bottom, planes[0][Projection::PLANE_LEFT], &min_vec), "Can't determine left camera near/left/bottom vector"); - ERR_FAIL_COND_MSG( - !near_plane.intersect_3(bottom, planes[1][Projection::PLANE_LEFT], &other), "Can't determine right camera near/left/bottom vector"); - if (x.dot(other) < x.dot(min_vec)) { - min_vec = other; - } - - Vector3 max_vec; - ERR_FAIL_COND_MSG( - !near_plane.intersect_3(top, planes[0][Projection::PLANE_RIGHT], &max_vec), "Can't determine left camera near/right/top vector"); - ERR_FAIL_COND_MSG( - !near_plane.intersect_3(top, planes[1][Projection::PLANE_RIGHT], &other), "Can't determine right camera near/right/top vector"); - if (x.dot(max_vec) < x.dot(other)) { - max_vec = other; - } - - // 14. transform these points by the inverse camera to obtain local_min_vec and local_max_vec - Vector3 local_min_vec = main_transform_inv.xform(min_vec); - Vector3 local_max_vec = main_transform_inv.xform(max_vec); - - // 15. get x and y from these to obtain left, top, right bottom for the frustum. Get the distance from near plane to camera origin to obtain near, and the distance from the far plane to the camera origin to obtain far. - float z_near = -near_plane.distance_to(main_transform.origin); - float z_far = -far_plane.distance_to(main_transform.origin); - - // 16. Use this to build the combined camera matrix. - main_projection.set_frustum(local_min_vec.x, local_max_vec.x, local_min_vec.y, local_max_vec.y, z_near, z_far); + main_transform = p_transform; + main_projection = Projection::create_combined_projection(p_transform, p_projections[0], p_offsets[0], p_projections[1], p_offsets[1]); - ///////////////////////////////////////////////////////////////////////////// - // 3. Copy our view data for (uint32_t v = 0; v < view_count; v++) { - view_offset[v] = main_transform_inv * p_transforms[v]; + view_offset[v] = p_offsets[v]; view_projection[v] = p_projections[v] * Projection(view_offset[v].inverse()); } } diff --git a/servers/rendering/renderer_scene_render.h b/servers/rendering/renderer_scene_render.h index 42702492af89..897f0763d5e3 100644 --- a/servers/rendering/renderer_scene_render.h +++ b/servers/rendering/renderer_scene_render.h @@ -306,6 +306,7 @@ class RendererSceneRender { // flags uint32_t view_count; bool is_orthogonal; + bool is_asymmetrical; uint32_t visible_layers; bool vaspect; @@ -318,8 +319,8 @@ class RendererSceneRender { Vector2 taa_jitter; float taa_frame_count = 0.0f; - void set_camera(const Transform3D p_transform, const Projection p_projection, bool p_is_orthogonal, bool p_vaspect, const Vector2 &p_taa_jitter = Vector2(), float p_taa_frame_count = 0.0f, uint32_t p_visible_layers = 0xFFFFFFFF); - void set_multiview_camera(uint32_t p_view_count, const Transform3D *p_transforms, const Projection *p_projections, bool p_is_orthogonal, bool p_vaspect, uint32_t p_visible_layers = 0xFFFFFFFF); + void set_camera(const Transform3D p_transform, const Projection p_projection, bool p_is_orthogonal, bool p_is_asymmetrical, bool p_vaspect, const Vector2 &p_taa_jitter = Vector2(), float p_taa_frame_count = 0.0f, uint32_t p_visible_layers = 0xFFFFFFFF); + void set_multiview_camera(const Transform3D &p_transform, const LocalVector &p_offsets, const LocalVector &p_projection, bool p_is_orthogonal, bool p_is_asymmetrical, bool p_vaspect, uint32_t p_visible_layers = 0xFFFFFFFF); }; virtual void render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, float p_window_output_max_value, const RenderSDFGIUpdateData *p_sdfgi_update_data = nullptr, RenderingServerTypes::RenderInfo *r_render_info = nullptr) = 0; diff --git a/servers/rendering/renderer_viewport.cpp b/servers/rendering/renderer_viewport.cpp index f80a9cc6caf8..5515fae0bb0c 100644 --- a/servers/rendering/renderer_viewport.cpp +++ b/servers/rendering/renderer_viewport.cpp @@ -300,13 +300,6 @@ void RendererViewport::_draw_3d(Viewport *p_viewport) { #ifndef _3D_DISABLED RENDER_TIMESTAMP("> Render 3D Scene"); - Ref xr_interface; -#ifndef XR_DISABLED - if (p_viewport->use_xr && XRServer::get_singleton() != nullptr) { - xr_interface = XRServer::get_singleton()->get_primary_interface(); - } -#endif // XR_DISABLED - if (p_viewport->use_occlusion_culling) { if (p_viewport->occlusion_buffer_dirty) { float aspect = p_viewport->size.aspect(); @@ -323,7 +316,7 @@ void RendererViewport::_draw_3d(Viewport *p_viewport) { } float screen_mesh_lod_threshold = p_viewport->mesh_lod_threshold / float(p_viewport->size.width); - RSG::scene->render_camera(p_viewport->render_buffers, p_viewport->camera, p_viewport->scenario, p_viewport->self, p_viewport->internal_size, p_viewport->jitter_phase_count, screen_mesh_lod_threshold, p_viewport->shadow_atlas, xr_interface, p_viewport->window_output_max_value, &p_viewport->render_info); + RSG::scene->render_camera(p_viewport->render_buffers, p_viewport->camera, p_viewport->scenario, p_viewport->self, p_viewport->internal_size, p_viewport->jitter_phase_count, screen_mesh_lod_threshold, p_viewport->shadow_atlas, p_viewport->window_output_max_value, &p_viewport->render_info); RENDER_TIMESTAMP("< Render 3D Scene"); #endif // _3D_DISABLED diff --git a/servers/rendering/rendering_method.h b/servers/rendering/rendering_method.h index c06adc8b8c8a..6238662289f0 100644 --- a/servers/rendering/rendering_method.h +++ b/servers/rendering/rendering_method.h @@ -30,6 +30,7 @@ #pragma once +#include "core/variant/typed_array.h" #include "core/variant/variant.h" #include "servers/rendering/rendering_server_enums.h" #include "servers/rendering/rendering_server_types.h" @@ -53,6 +54,7 @@ class RenderingMethod { virtual void camera_set_perspective(RID p_camera, float p_fovy_degrees, float p_z_near, float p_z_far) = 0; virtual void camera_set_orthogonal(RID p_camera, float p_size, float p_z_near, float p_z_far) = 0; virtual void camera_set_frustum(RID p_camera, float p_size, Vector2 p_offset, float p_z_near, float p_z_far) = 0; + virtual void camera_set_projections(RID p_camera, TypedArray p_projections, TypedArray p_offsets = TypedArray()) = 0; virtual void camera_set_transform(RID p_camera, const Transform3D &p_transform) = 0; virtual void camera_set_cull_mask(RID p_camera, uint32_t p_layers) = 0; virtual void camera_set_environment(RID p_camera, RID p_env) = 0; @@ -350,7 +352,7 @@ class RenderingMethod { virtual void render_empty_scene(const Ref &p_render_buffers, RID p_scenario, RID p_shadow_atlas, float p_window_output_max_value) = 0; - virtual void render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_mesh_lod_threshold, RID p_shadow_atlas, Ref &p_xr_interface, float p_window_output_max_value, RenderingServerTypes::RenderInfo *r_render_info = nullptr) = 0; + virtual void render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_mesh_lod_threshold, RID p_shadow_atlas, float p_window_output_max_value, RenderingServerTypes::RenderInfo *r_render_info = nullptr) = 0; virtual void update() = 0; virtual void render_probes() = 0; diff --git a/servers/rendering/rendering_server.cpp b/servers/rendering/rendering_server.cpp index 460eeb67d1d2..7e97069cc164 100644 --- a/servers/rendering/rendering_server.cpp +++ b/servers/rendering/rendering_server.cpp @@ -2839,6 +2839,7 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("camera_set_perspective", "camera", "fovy_degrees", "z_near", "z_far"), &RenderingServer::camera_set_perspective); ClassDB::bind_method(D_METHOD("camera_set_orthogonal", "camera", "size", "z_near", "z_far"), &RenderingServer::camera_set_orthogonal); ClassDB::bind_method(D_METHOD("camera_set_frustum", "camera", "size", "offset", "z_near", "z_far"), &RenderingServer::camera_set_frustum); + ClassDB::bind_method(D_METHOD("camera_set_projections", "camera", "projections", "offsets"), &RenderingServer::camera_set_projections, DEFVAL(TypedArray())); ClassDB::bind_method(D_METHOD("camera_set_transform", "camera", "transform"), &RenderingServer::camera_set_transform); ClassDB::bind_method(D_METHOD("camera_set_cull_mask", "camera", "layers"), &RenderingServer::camera_set_cull_mask); ClassDB::bind_method(D_METHOD("camera_set_environment", "camera", "env"), &RenderingServer::camera_set_environment); diff --git a/servers/rendering/rendering_server.h b/servers/rendering/rendering_server.h index 4a5f852bcd19..3cf828a72cf3 100644 --- a/servers/rendering/rendering_server.h +++ b/servers/rendering/rendering_server.h @@ -524,6 +524,7 @@ class RenderingServer : public Object { virtual void camera_set_perspective(RID p_camera, float p_fovy_degrees, float p_z_near, float p_z_far) = 0; virtual void camera_set_orthogonal(RID p_camera, float p_size, float p_z_near, float p_z_far) = 0; virtual void camera_set_frustum(RID p_camera, float p_size, Vector2 p_offset, float p_z_near, float p_z_far) = 0; + virtual void camera_set_projections(RID p_camera, TypedArray p_projections, TypedArray p_offsets = TypedArray()) = 0; virtual void camera_set_transform(RID p_camera, const Transform3D &p_transform) = 0; virtual void camera_set_cull_mask(RID p_camera, uint32_t p_layers) = 0; virtual void camera_set_environment(RID p_camera, RID p_env) = 0; diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h index 9bc71818ed30..985504a0af30 100644 --- a/servers/rendering/rendering_server_default.h +++ b/servers/rendering/rendering_server_default.h @@ -711,6 +711,7 @@ class RenderingServerDefault : public RenderingServer { FUNC4(camera_set_perspective, RID, float, float, float) FUNC4(camera_set_orthogonal, RID, float, float, float) FUNC5(camera_set_frustum, RID, float, Vector2, float, float) + FUNC3(camera_set_projections, RID, TypedArray, TypedArray) FUNC2(camera_set_transform, RID, const Transform3D &) FUNC2(camera_set_cull_mask, RID, uint32_t) FUNC2(camera_set_environment, RID, RID) diff --git a/servers/xr/xr_interface.cpp b/servers/xr/xr_interface.cpp index 1800c91dfe16..6a1c92d30e12 100644 --- a/servers/xr/xr_interface.cpp +++ b/servers/xr/xr_interface.cpp @@ -75,8 +75,14 @@ void XRInterface::_bind_methods() { ClassDB::bind_method(D_METHOD("is_passthrough_enabled"), &XRInterface::is_passthrough_enabled); ClassDB::bind_method(D_METHOD("start_passthrough"), &XRInterface::start_passthrough); ClassDB::bind_method(D_METHOD("stop_passthrough"), &XRInterface::stop_passthrough); + + ClassDB::bind_method(D_METHOD("get_camera_projections", "tracker_name", "aspect", "near", "far"), &XRInterface::get_camera_projections); + ClassDB::bind_method(D_METHOD("get_camera_offsets", "tracker_name"), &XRInterface::get_camera_offsets); + +#ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("get_transform_for_view", "view", "cam_transform"), &XRInterface::get_transform_for_view); ClassDB::bind_method(D_METHOD("get_projection_for_view", "view", "aspect", "near", "far"), &XRInterface::get_projection_for_view); +#endif /** environment blend mode. */ ClassDB::bind_method(D_METHOD("get_supported_environment_blend_modes"), &XRInterface::get_supported_environment_blend_modes); diff --git a/servers/xr/xr_interface.h b/servers/xr/xr_interface.h index 5c8bd894d2c6..aaebf81b54b1 100644 --- a/servers/xr/xr_interface.h +++ b/servers/xr/xr_interface.h @@ -33,6 +33,8 @@ #include "core/object/ref_counted.h" #include "core/os/thread_safe.h" #include "core/variant/type_info.h" +#include "core/variant/typed_array.h" +#include "core/variant/variant.h" /** The XR interface is a template class on top of which we build interface to different AR, VR and tracking SDKs. @@ -132,16 +134,22 @@ class XRInterface : public RefCounted { /** rendering and internal **/ // These methods are called from the main thread. - virtual Transform3D get_camera_transform() = 0; /* returns the position of our camera, only used for updating reference frame. For monoscopic this is equal to the views transform, for stereoscopic this should be an average */ virtual void process() = 0; + // Camera information + virtual Transform3D get_camera_transform() = 0; /* returns the position of our main camera, only used for updating reference frame. For monoscopic this is equal to the views transform, for stereoscopic this should be an average */ + virtual TypedArray get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) = 0; /* Get projections for each eye of the given camera */ + virtual TypedArray get_camera_offsets(const StringName &p_tracker_name) = 0; /* Get offsets for each eye of the given camera */ + // These methods can be called from both main and render thread. virtual Size2 get_render_target_size() = 0; /* returns the recommended render target size per eye for this device */ - virtual uint32_t get_view_count() = 0; /* returns the view count we need (1 is monoscopic, 2 is stereoscopic but can be more) */ + virtual uint32_t get_view_count() = 0; /* returns the primary view count we need (1 is monoscopic, 2 is stereoscopic but can be more) */ // These methods are called from the rendering thread. - virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) = 0; /* get each views transform */ - virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) = 0; /* get each view projection matrix */ +#ifndef DISABLE_DEPRECATED + virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) { return Transform3D(); } /* Deprecated, get each views transform */ + virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) { return Projection(); } /* Deprecated, get each view projection matrix */ +#endif virtual RID get_color_texture(); /* obtain color output texture (if applicable) */ virtual RID get_depth_texture(); /* obtain depth output texture (if applicable, used for reprojection) */ virtual RID get_velocity_texture(); /* obtain velocity output texture (if applicable, used for spacewarp) */ diff --git a/servers/xr/xr_interface_extension.cpp b/servers/xr/xr_interface_extension.cpp index 505570f1b65d..5217b33f9e8e 100644 --- a/servers/xr/xr_interface_extension.cpp +++ b/servers/xr/xr_interface_extension.cpp @@ -50,8 +50,12 @@ void XRInterfaceExtension::_bind_methods() { GDVIRTUAL_BIND(_get_render_target_size); GDVIRTUAL_BIND(_get_view_count); GDVIRTUAL_BIND(_get_camera_transform); + GDVIRTUAL_BIND(_get_camera_projections, "tracker_name", "aspect", "z_near", "z_far"); + GDVIRTUAL_BIND(_get_camera_offsets, "tracker_name"); +#ifndef DISABLE_DEPRECATED GDVIRTUAL_BIND(_get_transform_for_view, "view", "cam_transform"); GDVIRTUAL_BIND(_get_projection_for_view, "view", "aspect", "z_near", "z_far"); +#endif GDVIRTUAL_BIND(_get_vrs_texture); GDVIRTUAL_BIND(_get_vrs_texture_format); @@ -214,6 +218,26 @@ Transform3D XRInterfaceExtension::get_camera_transform() { return transform; } +TypedArray XRInterfaceExtension::get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) { + TypedArray camera_projections; + + if (GDVIRTUAL_CALL(_get_camera_projections, p_tracker_name, p_aspect, p_z_near, p_z_far, camera_projections)) { + return camera_projections; + } + + return camera_projections; +} + +TypedArray XRInterfaceExtension::get_camera_offsets(const StringName &p_tracker_name) { + TypedArray camera_offsets; + + if (GDVIRTUAL_CALL(_get_camera_offsets, p_tracker_name, camera_offsets)) { + return camera_offsets; + } + + return camera_offsets; +} + Transform3D XRInterfaceExtension::get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) { Transform3D transform; GDVIRTUAL_CALL(_get_transform_for_view, p_view, p_cam_transform, transform); diff --git a/servers/xr/xr_interface_extension.h b/servers/xr/xr_interface_extension.h index aac932829616..b1d86984d3f8 100644 --- a/servers/xr/xr_interface_extension.h +++ b/servers/xr/xr_interface_extension.h @@ -101,8 +101,12 @@ class XRInterfaceExtension : public XRInterface { virtual Size2 get_render_target_size() override; virtual uint32_t get_view_count() override; virtual Transform3D get_camera_transform() override; + virtual TypedArray get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) override; + virtual TypedArray get_camera_offsets(const StringName &p_tracker_name) override; +#ifndef DISABLE_DEPRECATED virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) override; virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) override; +#endif virtual RID get_vrs_texture() override; virtual VRSTextureFormat get_vrs_texture_format() override; virtual RID get_color_texture() override; @@ -112,8 +116,12 @@ class XRInterfaceExtension : public XRInterface { GDVIRTUAL0R(Size2, _get_render_target_size); GDVIRTUAL0R(uint32_t, _get_view_count); GDVIRTUAL0R(Transform3D, _get_camera_transform); + GDVIRTUAL4R(TypedArray, _get_camera_projections, const StringName &, double, double, double); + GDVIRTUAL1R(TypedArray, _get_camera_offsets, const StringName &); +#ifndef DISABLE_DEPRECATED GDVIRTUAL2R(Transform3D, _get_transform_for_view, uint32_t, const Transform3D &); GDVIRTUAL4R(PackedFloat64Array, _get_projection_for_view, uint32_t, double, double, double); +#endif GDVIRTUAL0R(RID, _get_vrs_texture); GDVIRTUAL0R(VRSTextureFormat, _get_vrs_texture_format); GDVIRTUAL0R(RID, _get_color_texture); diff --git a/servers/xr/xr_positional_tracker.h b/servers/xr/xr_positional_tracker.h index ea82a42c6a2e..990c1cfd582a 100644 --- a/servers/xr/xr_positional_tracker.h +++ b/servers/xr/xr_positional_tracker.h @@ -41,6 +41,8 @@ This is where potentially additional AR/VR interfaces may be active as there are AR/VR SDKs that solely deal with positional tracking. */ +#define XR_TRACKER_HEAD SNAME("head") + class XRPositionalTracker : public XRTracker { GDCLASS(XRPositionalTracker, XRTracker); _THREAD_SAFE_CLASS_ diff --git a/servers/xr/xr_server.cpp b/servers/xr/xr_server.cpp index 2c16e21b74c3..aa5699219635 100644 --- a/servers/xr/xr_server.cpp +++ b/servers/xr/xr_server.cpp @@ -82,12 +82,15 @@ void XRServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_trackers", "tracker_types"), &XRServer::get_trackers); ClassDB::bind_method(D_METHOD("get_tracker", "tracker_name"), &XRServer::get_tracker); + ClassDB::bind_method(D_METHOD("get_camera_projections", "tracker_name", "aspect", "near", "far"), &XRServer::get_camera_projections); + ClassDB::bind_method(D_METHOD("get_camera_offsets", "tracker_name"), &XRServer::get_camera_offsets); + ClassDB::bind_method(D_METHOD("get_primary_interface"), &XRServer::get_primary_interface); ClassDB::bind_method(D_METHOD("set_primary_interface", "interface"), &XRServer::set_primary_interface); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "primary_interface"), "set_primary_interface", "get_primary_interface"); - BIND_ENUM_CONSTANT(TRACKER_HEAD); + BIND_ENUM_CONSTANT(TRACKER_CAMERA); BIND_ENUM_CONSTANT(TRACKER_CONTROLLER); BIND_ENUM_CONSTANT(TRACKER_BASESTATION); BIND_ENUM_CONSTANT(TRACKER_ANCHOR); @@ -98,6 +101,10 @@ void XRServer::_bind_methods() { BIND_ENUM_CONSTANT(TRACKER_UNKNOWN); BIND_ENUM_CONSTANT(TRACKER_ANY); +#ifndef DISABLE_DEPRECATED + BIND_ENUM_CONSTANT(TRACKER_HEAD); +#endif + BIND_ENUM_CONSTANT(RESET_FULL_ROTATION); BIND_ENUM_CONSTANT(RESET_BUT_KEEP_TILT); BIND_ENUM_CONSTANT(DONT_RESET_ROTATION); @@ -398,6 +405,75 @@ Ref XRServer::get_tracker(const StringName &p_name) const { } } +TypedArray XRServer::get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far) { + TypedArray projections; + + // Try primary interface first. + if (primary_interface.is_valid()) { + projections = primary_interface->get_camera_projections(p_tracker_name, p_aspect, p_z_near, p_z_far); + if (!projections.is_empty()) { + return projections; + } + } + + // Try any other active interfaces. + for (Ref interface : interfaces) { + if (interface != primary_interface && interface->is_initialized()) { + projections = interface->get_camera_projections(p_tracker_name, p_aspect, p_z_near, p_z_far); + if (!projections.is_empty()) { + return projections; + } + } + } + +#ifndef DISABLE_DEPRECATED + // Fallback on old implementation. + if (primary_interface.is_valid() && p_tracker_name == XR_TRACKER_HEAD) { + for (uint32_t v = 0; v < primary_interface->get_view_count(); v++) { + projections.push_back(primary_interface->get_projection_for_view(v, p_aspect, p_z_near, p_z_far)); + } + } +#endif + + return projections; +} + +TypedArray XRServer::get_camera_offsets(const StringName &p_tracker_name) { + TypedArray offsets; + + // Try primary interface first. + if (primary_interface.is_valid()) { + offsets = primary_interface->get_camera_offsets(p_tracker_name); + if (!offsets.is_empty()) { + return offsets; + } + } + + // Try any other active interfaces. + for (Ref interface : interfaces) { + if (interface != primary_interface && interface->is_initialized()) { + offsets = interface->get_camera_offsets(p_tracker_name); + if (!offsets.is_empty()) { + return offsets; + } + } + } + +#ifndef DISABLE_DEPRECATED + // Fallback on old implementation. + if (primary_interface.is_valid() && p_tracker_name == XR_TRACKER_HEAD) { + Transform3D inv_camera_transform = primary_interface->get_camera_transform().inverse(); + + for (uint32_t v = 0; v < primary_interface->get_view_count(); v++) { + Transform3D offset = primary_interface->get_transform_for_view(v, Transform3D()); + offsets.push_back(inv_camera_transform * offset); + } + } +#endif + + return offsets; +} + PackedStringArray XRServer::get_suggested_tracker_names() const { PackedStringArray arr; diff --git a/servers/xr/xr_server.h b/servers/xr/xr_server.h index 31bc4df15ffc..871493e34362 100644 --- a/servers/xr/xr_server.h +++ b/servers/xr/xr_server.h @@ -63,7 +63,7 @@ class XRServer : public Object { }; enum TrackerType { - TRACKER_HEAD = 0x01, /* tracks the position of the players head (or in case of handheld AR, location of the phone) */ + TRACKER_CAMERA = 0x01, /* tracks the position of an XR camera (HMD, external camera, phone camera for phone based AR) */ TRACKER_CONTROLLER = 0x02, /* tracks a controller */ TRACKER_BASESTATION = 0x04, /* tracks location of a base station */ TRACKER_ANCHOR = 0x08, /* tracks an anchor point, used in AR to track a real live location */ @@ -73,7 +73,11 @@ class XRServer : public Object { TRACKER_UNKNOWN = 0x80, /* unknown tracker */ TRACKER_ANY_KNOWN = 0x7f, /* all except unknown */ - TRACKER_ANY = 0xff /* used by get_connected_trackers to return all types */ + TRACKER_ANY = 0xff, /* used by get_connected_trackers to return all types */ + +#ifndef DISABLE_DEPRECATED + TRACKER_HEAD = TRACKER_CAMERA /* Just for backwards compatibility */ +#endif }; enum RotationMode { @@ -205,6 +209,10 @@ class XRServer : public Object { Dictionary get_trackers(int p_tracker_types); Ref get_tracker(const StringName &p_name) const; + // For camera trackers only, we need access to additional information that can't be stored on the tracker itself. + TypedArray get_camera_projections(const StringName &p_tracker_name, double p_aspect, double p_z_near, double p_z_far); + TypedArray get_camera_offsets(const StringName &p_tracker_name); + /* We don't know which trackers and actions will existing during runtime but we can request suggested names from our interfaces to help our IDE UI. */