/*************************************************************************/ /* xr_nodes.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* 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 "xr_nodes.h" #include "core/config/project_settings.h" #include "scene/main/viewport.h" #include "servers/xr/xr_interface.h" //////////////////////////////////////////////////////////////////////////////////////////////////// void XRCamera3D::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { // need to find our XROrigin3D parent and let it know we're its camera! XROrigin3D *origin = Object::cast_to(get_parent()); if (origin != nullptr) { origin->set_tracked_camera(this); } }; break; case NOTIFICATION_EXIT_TREE: { // need to find our XROrigin3D parent and let it know we're no longer its camera! XROrigin3D *origin = Object::cast_to(get_parent()); if (origin != nullptr && origin->get_tracked_camera() == this) { origin->set_tracked_camera(nullptr); } }; break; }; }; void XRCamera3D::_changed_tracker(const StringName p_tracker_name, int p_tracker_type) { if (p_tracker_name == tracker_name) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); tracker = xr_server->get_tracker(p_tracker_name); if (tracker.is_valid()) { tracker->connect("pose_changed", callable_mp(this, &XRCamera3D::_pose_changed)); Ref pose = tracker->get_pose(pose_name); if (pose.is_valid()) { set_transform(pose->get_adjusted_transform()); } } } } void XRCamera3D::_removed_tracker(const StringName p_tracker_name, int p_tracker_type) { if (p_tracker_name == tracker_name) { if (tracker.is_valid()) { tracker->disconnect("pose_changed", callable_mp(this, &XRCamera3D::_pose_changed)); } tracker.unref(); } } void XRCamera3D::_pose_changed(const Ref &p_pose) { if (p_pose->get_name() == pose_name) { set_transform(p_pose->get_adjusted_transform()); } } TypedArray XRCamera3D::get_configuration_warnings() const { TypedArray warnings = Node::get_configuration_warnings(); if (is_visible() && is_inside_tree()) { // must be child node of XROrigin3D! XROrigin3D *origin = Object::cast_to(get_parent()); if (origin == nullptr) { warnings.push_back(TTR("XRCamera3D must have an XROrigin3D node as its parent.")); }; } return warnings; }; Vector3 XRCamera3D::project_local_ray_normal(const Point2 &p_pos) const { // get our XRServer XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL_V(xr_server, Vector3()); Ref xr_interface = xr_server->get_primary_interface(); if (xr_interface.is_null()) { // we might be in the editor or have VR turned off, just call superclass return Camera3D::project_local_ray_normal(p_pos); } ERR_FAIL_COND_V_MSG(!is_inside_tree(), Vector3(), "Camera is not inside scene."); Size2 viewport_size = get_viewport()->get_camera_rect_size(); Vector2 cpos = get_viewport()->get_camera_coords(p_pos); Vector3 ray; // Just use the first view, if multiple views are supported this function has no good result CameraMatrix cm = xr_interface->get_projection_for_view(0, viewport_size.aspect(), get_near(), get_far()); Vector2 screen_he = cm.get_viewport_half_extents(); ray = Vector3(((cpos.x / viewport_size.width) * 2.0 - 1.0) * screen_he.x, ((1.0 - (cpos.y / viewport_size.height)) * 2.0 - 1.0) * screen_he.y, -get_near()).normalized(); return ray; }; Point2 XRCamera3D::unproject_position(const Vector3 &p_pos) const { // get our XRServer XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL_V(xr_server, Vector2()); Ref xr_interface = xr_server->get_primary_interface(); if (xr_interface.is_null()) { // we might be in the editor or have VR turned off, just call superclass return Camera3D::unproject_position(p_pos); } ERR_FAIL_COND_V_MSG(!is_inside_tree(), Vector2(), "Camera is not inside scene."); Size2 viewport_size = get_viewport()->get_visible_rect().size; // Just use the first view, if multiple views are supported this function has no good result CameraMatrix cm = xr_interface->get_projection_for_view(0, viewport_size.aspect(), get_near(), get_far()); Plane p(get_camera_transform().xform_inv(p_pos), 1.0); p = cm.xform4(p); p.normal /= p.d; Point2 res; res.x = (p.normal.x * 0.5 + 0.5) * viewport_size.x; res.y = (-p.normal.y * 0.5 + 0.5) * viewport_size.y; return res; }; Vector3 XRCamera3D::project_position(const Point2 &p_point, real_t p_z_depth) const { // get our XRServer XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL_V(xr_server, Vector3()); Ref xr_interface = xr_server->get_primary_interface(); if (xr_interface.is_null()) { // we might be in the editor or have VR turned off, just call superclass return Camera3D::project_position(p_point, p_z_depth); } ERR_FAIL_COND_V_MSG(!is_inside_tree(), Vector3(), "Camera is not inside scene."); Size2 viewport_size = get_viewport()->get_visible_rect().size; // Just use the first view, if multiple views are supported this function has no good result CameraMatrix cm = xr_interface->get_projection_for_view(0, viewport_size.aspect(), get_near(), get_far()); Vector2 vp_he = cm.get_viewport_half_extents(); Vector2 point; point.x = (p_point.x / viewport_size.x) * 2.0 - 1.0; point.y = (1.0 - (p_point.y / viewport_size.y)) * 2.0 - 1.0; point *= vp_he; Vector3 p(point.x, point.y, -p_z_depth); return get_camera_transform().xform(p); }; Vector XRCamera3D::get_frustum() const { // get our XRServer XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL_V(xr_server, Vector()); Ref xr_interface = xr_server->get_primary_interface(); if (xr_interface.is_null()) { // we might be in the editor or have VR turned off, just call superclass return Camera3D::get_frustum(); } ERR_FAIL_COND_V(!is_inside_world(), Vector()); Size2 viewport_size = get_viewport()->get_visible_rect().size; // TODO Just use the first view for now, this is mostly for debugging so we may look into using our combined projection here. CameraMatrix cm = xr_interface->get_projection_for_view(0, viewport_size.aspect(), get_near(), get_far()); return cm.get_projection_planes(get_camera_transform()); }; XRCamera3D::XRCamera3D() { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); xr_server->connect("tracker_added", callable_mp(this, &XRCamera3D::_changed_tracker)); xr_server->connect("tracker_updated", callable_mp(this, &XRCamera3D::_changed_tracker)); xr_server->connect("tracker_removed", callable_mp(this, &XRCamera3D::_removed_tracker)); } XRCamera3D::~XRCamera3D() { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); xr_server->disconnect("tracker_added", callable_mp(this, &XRCamera3D::_changed_tracker)); xr_server->disconnect("tracker_updated", callable_mp(this, &XRCamera3D::_changed_tracker)); xr_server->disconnect("tracker_removed", callable_mp(this, &XRCamera3D::_removed_tracker)); } //////////////////////////////////////////////////////////////////////////////////////////////////// // XRNode3D is a node that has it's transform updated by an XRPositionalTracker. // Note that trackers are only available in runtime and only after an XRInterface registers one. // So we bind by name and as long as a tracker isn't available, our node remains inactive. void XRNode3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_tracker", "tracker_name"), &XRNode3D::set_tracker); ClassDB::bind_method(D_METHOD("get_tracker"), &XRNode3D::get_tracker); ADD_PROPERTY(PropertyInfo(Variant::STRING, "tracker", PROPERTY_HINT_ENUM_SUGGESTION), "set_tracker", "get_tracker"); ClassDB::bind_method(D_METHOD("set_pose_name", "pose"), &XRNode3D::set_pose_name); ClassDB::bind_method(D_METHOD("get_pose_name"), &XRNode3D::get_pose_name); ADD_PROPERTY(PropertyInfo(Variant::STRING, "pose", PROPERTY_HINT_ENUM_SUGGESTION), "set_pose_name", "get_pose_name"); ClassDB::bind_method(D_METHOD("get_is_active"), &XRNode3D::get_is_active); ClassDB::bind_method(D_METHOD("get_has_tracking_data"), &XRNode3D::get_has_tracking_data); ClassDB::bind_method(D_METHOD("get_pose"), &XRNode3D::get_pose); ClassDB::bind_method(D_METHOD("trigger_haptic_pulse", "action_name", "frequency", "amplitude", "duration_sec", "delay_sec"), &XRNode3D::trigger_haptic_pulse); }; void XRNode3D::_validate_property(PropertyInfo &property) const { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); if (property.name == "tracker") { PackedStringArray names = xr_server->get_suggested_tracker_names(); String hint_string; for (const String &name : names) { hint_string += name + ","; } property.hint_string = hint_string; } else if (property.name == "pose") { PackedStringArray names = xr_server->get_suggested_pose_names(tracker_name); String hint_string; for (const String &name : names) { hint_string += name + ","; } property.hint_string = hint_string; } Node3D::_validate_property(property); } void XRNode3D::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 = "default"; // see if it's already available _bind_tracker(); update_configuration_warnings(); notify_property_list_changed(); } StringName XRNode3D::get_tracker() const { return tracker_name; } void XRNode3D::set_pose_name(const StringName p_pose_name) { pose_name = p_pose_name; // Update pose if we are bound to a tracker with a valid pose Ref pose = get_pose(); if (pose.is_valid()) { set_transform(pose->get_adjusted_transform()); } } StringName XRNode3D::get_pose_name() const { return pose_name; } bool XRNode3D::get_is_active() const { if (tracker.is_null()) { return false; } else if (!tracker->has_pose(pose_name)) { return false; } else { return true; } } bool XRNode3D::get_has_tracking_data() const { if (tracker.is_null()) { return false; } else if (!tracker->has_pose(pose_name)) { return false; } else { return tracker->get_pose(pose_name)->get_has_tracking_data(); } } void XRNode3D::trigger_haptic_pulse(const String &p_action_name, double p_frequency, double p_amplitude, double p_duration_sec, double p_delay_sec) { // TODO need to link trackers to the interface that registered them so we can call this on the correct interface. // For now this works fine as in 99% of the cases we only have our primary interface active XRServer *xr_server = XRServer::get_singleton(); if (xr_server != nullptr) { Ref xr_interface = xr_server->get_primary_interface(); if (xr_interface.is_valid()) { xr_interface->trigger_haptic_pulse(p_action_name, tracker_name, p_frequency, p_amplitude, p_duration_sec, p_delay_sec); } } } Ref XRNode3D::get_pose() { if (tracker.is_valid()) { return tracker->get_pose(pose_name); } else { return Ref(); } } void XRNode3D::_bind_tracker() { ERR_FAIL_COND_MSG(tracker.is_valid(), "Unbind the current tracker first"); XRServer *xr_server = XRServer::get_singleton(); if (xr_server != nullptr) { tracker = xr_server->get_tracker(tracker_name); if (tracker.is_null()) { // It is possible and valid if the tracker isn't available (yet), in this case we just exit return; } tracker->connect("pose_changed", callable_mp(this, &XRNode3D::_pose_changed)); Ref pose = get_pose(); if (pose.is_valid()) { set_transform(pose->get_adjusted_transform()); } } } void XRNode3D::_unbind_tracker() { if (tracker.is_valid()) { tracker->disconnect("pose_changed", callable_mp(this, &XRNode3D::_pose_changed)); tracker.unref(); } } void XRNode3D::_changed_tracker(const StringName p_tracker_name, int p_tracker_type) { if (p_tracker_name == p_tracker_name) { // just in case unref our current tracker _unbind_tracker(); // get our new tracker _bind_tracker(); } } void XRNode3D::_removed_tracker(const StringName p_tracker_name, int p_tracker_type) { if (p_tracker_name == p_tracker_name) { // unref our tracker, it's no longer available _unbind_tracker(); } } void XRNode3D::_pose_changed(const Ref &p_pose) { if (p_pose.is_valid() && p_pose->get_name() == pose_name) { set_transform(p_pose->get_adjusted_transform()); } } XRNode3D::XRNode3D() { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); xr_server->connect("tracker_added", callable_mp(this, &XRNode3D::_changed_tracker)); xr_server->connect("tracker_updated", callable_mp(this, &XRNode3D::_changed_tracker)); xr_server->connect("tracker_removed", callable_mp(this, &XRNode3D::_removed_tracker)); } XRNode3D::~XRNode3D() { _unbind_tracker(); XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); xr_server->disconnect("tracker_added", callable_mp(this, &XRNode3D::_changed_tracker)); xr_server->disconnect("tracker_updated", callable_mp(this, &XRNode3D::_changed_tracker)); xr_server->disconnect("tracker_removed", callable_mp(this, &XRNode3D::_removed_tracker)); } TypedArray XRNode3D::get_configuration_warnings() const { TypedArray warnings = Node::get_configuration_warnings(); if (is_visible() && is_inside_tree()) { // must be child node of XROrigin! XROrigin3D *origin = Object::cast_to(get_parent()); if (origin == nullptr) { warnings.push_back(TTR("XRController3D must have an XROrigin3D node as its parent.")); } if (tracker_name == "") { warnings.push_back(TTR("No tracker name is set.")); } if (pose_name == "") { warnings.push_back(TTR("No pose is set.")); } } return warnings; } //////////////////////////////////////////////////////////////////////////////////////////////////// void XRController3D::_bind_methods() { // passthroughs to information about our related joystick ClassDB::bind_method(D_METHOD("is_button_pressed", "name"), &XRController3D::is_button_pressed); ClassDB::bind_method(D_METHOD("get_value", "name"), &XRController3D::get_value); ClassDB::bind_method(D_METHOD("get_axis", "name"), &XRController3D::get_axis); ClassDB::bind_method(D_METHOD("get_tracker_hand"), &XRController3D::get_tracker_hand); ClassDB::bind_method(D_METHOD("get_rumble"), &XRController3D::get_rumble); ClassDB::bind_method(D_METHOD("set_rumble", "rumble"), &XRController3D::set_rumble); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rumble", PROPERTY_HINT_RANGE, "0.0,1.0,0.01"), "set_rumble", "get_rumble"); ADD_PROPERTY_DEFAULT("rumble", 0.0); ADD_SIGNAL(MethodInfo("button_pressed", PropertyInfo(Variant::STRING, "name"))); ADD_SIGNAL(MethodInfo("button_released", PropertyInfo(Variant::STRING, "name"))); ADD_SIGNAL(MethodInfo("input_value_changed", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::FLOAT, "value"))); ADD_SIGNAL(MethodInfo("input_axis_changed", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::VECTOR2, "value"))); }; void XRController3D::_bind_tracker() { XRNode3D::_bind_tracker(); if (tracker.is_valid()) { // bind to input signals tracker->connect("button_pressed", callable_mp(this, &XRController3D::_button_pressed)); tracker->connect("button_released", callable_mp(this, &XRController3D::_button_released)); tracker->connect("input_value_changed", callable_mp(this, &XRController3D::_input_value_changed)); tracker->connect("input_axis_changed", callable_mp(this, &XRController3D::_input_axis_changed)); } } void XRController3D::_unbind_tracker() { if (tracker.is_valid()) { // unbind input signals tracker->disconnect("button_pressed", callable_mp(this, &XRController3D::_button_pressed)); tracker->disconnect("button_released", callable_mp(this, &XRController3D::_button_released)); tracker->disconnect("input_value_changed", callable_mp(this, &XRController3D::_input_value_changed)); tracker->disconnect("input_axis_changed", callable_mp(this, &XRController3D::_input_axis_changed)); } XRNode3D::_unbind_tracker(); } void XRController3D::_button_pressed(const String &p_name) { // just pass it on... emit_signal(SNAME("button_pressed"), p_name); } void XRController3D::_button_released(const String &p_name) { // just pass it on... emit_signal(SNAME("button_released"), p_name); } void XRController3D::_input_value_changed(const String &p_name, float p_value) { // just pass it on... emit_signal(SNAME("input_value_changed"), p_name, p_value); } void XRController3D::_input_axis_changed(const String &p_name, Vector2 p_value) { // just pass it on... emit_signal(SNAME("input_axis_changed"), p_name, p_value); } bool XRController3D::is_button_pressed(const StringName &p_name) const { if (tracker.is_valid()) { // Inputs should already be of the correct type, our XR runtime handles conversions between raw input and the desired type bool pressed = tracker->get_input(p_name); return pressed; } else { return false; } } float XRController3D::get_value(const StringName &p_name) const { if (tracker.is_valid()) { // Inputs should already be of the correct type, our XR runtime handles conversions between raw input and the desired type, but just in case we convert Variant input = tracker->get_input(p_name); switch (input.get_type()) { case Variant::BOOL: { bool value = input; return value ? 1.0 : 0.0; } break; case Variant::FLOAT: { float value = input; return value; } break; default: return 0.0; }; } else { return 0.0; } } Vector2 XRController3D::get_axis(const StringName &p_name) const { if (tracker.is_valid()) { // Inputs should already be of the correct type, our XR runtime handles conversions between raw input and the desired type, but just in case we convert Variant input = tracker->get_input(p_name); switch (input.get_type()) { case Variant::BOOL: { bool value = input; return Vector2(value ? 1.0 : 0.0, 0.0); } break; case Variant::FLOAT: { float value = input; return Vector2(value, 0.0); } break; case Variant::VECTOR2: { Vector2 axis = input; return axis; } default: return Vector2(); } } else { return Vector2(); } } real_t XRController3D::get_rumble() const { if (!tracker.is_valid()) { return 0.0; } return tracker->get_rumble(); } void XRController3D::set_rumble(real_t p_rumble) { if (tracker.is_valid()) { tracker->set_rumble(p_rumble); } } XRPositionalTracker::TrackerHand XRController3D::get_tracker_hand() const { // get our XRServer if (!tracker.is_valid()) { return XRPositionalTracker::TRACKER_HAND_UNKNOWN; } return tracker->get_tracker_hand(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void XRAnchor3D::_bind_methods() { ClassDB::bind_method(D_METHOD("get_size"), &XRAnchor3D::get_size); ClassDB::bind_method(D_METHOD("get_plane"), &XRAnchor3D::get_plane); } Vector3 XRAnchor3D::get_size() const { return size; } Plane XRAnchor3D::get_plane() const { Vector3 location = get_position(); Basis orientation = get_transform().basis; Plane plane(orientation.get_axis(1).normalized(), location); return plane; } //////////////////////////////////////////////////////////////////////////////////////////////////// TypedArray XROrigin3D::get_configuration_warnings() const { TypedArray warnings = Node::get_configuration_warnings(); if (is_visible() && is_inside_tree()) { if (tracked_camera == nullptr) { warnings.push_back(TTR("XROrigin3D requires an XRCamera3D child node.")); } } bool xr_enabled = GLOBAL_GET("rendering/xr/enabled"); if (!xr_enabled) { warnings.push_back(TTR("XR is not enabled in rendering project settings. Stereoscopic output is not supported unless this is enabled.")); } return warnings; } void XROrigin3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_world_scale", "world_scale"), &XROrigin3D::set_world_scale); ClassDB::bind_method(D_METHOD("get_world_scale"), &XROrigin3D::get_world_scale); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "world_scale"), "set_world_scale", "get_world_scale"); } void XROrigin3D::set_tracked_camera(XRCamera3D *p_tracked_camera) { tracked_camera = p_tracked_camera; } XRCamera3D *XROrigin3D::get_tracked_camera() const { return tracked_camera; } real_t XROrigin3D::get_world_scale() const { // get our XRServer XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL_V(xr_server, 1.0); return xr_server->get_world_scale(); } void XROrigin3D::set_world_scale(real_t p_world_scale) { // get our XRServer XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); xr_server->set_world_scale(p_world_scale); } void XROrigin3D::_notification(int p_what) { // get our XRServer XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); switch (p_what) { case NOTIFICATION_ENTER_TREE: { set_process_internal(true); }; break; case NOTIFICATION_EXIT_TREE: { set_process_internal(false); }; break; case NOTIFICATION_INTERNAL_PROCESS: { // set our world origin to our node transform xr_server->set_world_origin(get_global_transform()); // check if we have a primary interface Ref xr_interface = xr_server->get_primary_interface(); if (xr_interface.is_valid() && tracked_camera != nullptr) { // get our positioning transform for our headset Transform3D t = xr_interface->get_camera_transform(); // now apply this to our camera tracked_camera->set_transform(t); }; }; break; default: break; }; // send our notification to all active XE interfaces, they may need to react to it also for (int i = 0; i < xr_server->get_interface_count(); i++) { Ref interface = xr_server->get_interface(i); if (interface.is_valid() && interface->is_initialized()) { interface->notification(p_what); } } }