1/**************************************************************************/
2/* xr_nodes.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "xr_nodes.h"
32
33#include "core/config/project_settings.h"
34#include "scene/main/viewport.h"
35#include "servers/xr/xr_interface.h"
36
37////////////////////////////////////////////////////////////////////////////////////////////////////
38
39void XRCamera3D::_bind_tracker() {
40 XRServer *xr_server = XRServer::get_singleton();
41 ERR_FAIL_NULL(xr_server);
42
43 tracker = xr_server->get_tracker(tracker_name);
44 if (tracker.is_valid()) {
45 tracker->connect("pose_changed", callable_mp(this, &XRCamera3D::_pose_changed));
46
47 Ref<XRPose> pose = tracker->get_pose(pose_name);
48 if (pose.is_valid()) {
49 set_transform(pose->get_adjusted_transform());
50 }
51 }
52}
53
54void XRCamera3D::_unbind_tracker() {
55 if (tracker.is_valid()) {
56 tracker->disconnect("pose_changed", callable_mp(this, &XRCamera3D::_pose_changed));
57 }
58 tracker.unref();
59}
60
61void XRCamera3D::_changed_tracker(const StringName p_tracker_name, int p_tracker_type) {
62 if (p_tracker_name == tracker_name) {
63 _bind_tracker();
64 }
65}
66
67void XRCamera3D::_removed_tracker(const StringName p_tracker_name, int p_tracker_type) {
68 if (p_tracker_name == tracker_name) {
69 _unbind_tracker();
70 }
71}
72
73void XRCamera3D::_pose_changed(const Ref<XRPose> &p_pose) {
74 if (p_pose->get_name() == pose_name) {
75 set_transform(p_pose->get_adjusted_transform());
76 }
77}
78
79PackedStringArray XRCamera3D::get_configuration_warnings() const {
80 PackedStringArray warnings = Node::get_configuration_warnings();
81
82 if (is_visible() && is_inside_tree()) {
83 // must be child node of XROrigin3D!
84 XROrigin3D *origin = Object::cast_to<XROrigin3D>(get_parent());
85 if (origin == nullptr) {
86 warnings.push_back(RTR("XRCamera3D must have an XROrigin3D node as its parent."));
87 };
88 }
89
90 return warnings;
91};
92
93Vector3 XRCamera3D::project_local_ray_normal(const Point2 &p_pos) const {
94 // get our XRServer
95 XRServer *xr_server = XRServer::get_singleton();
96 ERR_FAIL_NULL_V(xr_server, Vector3());
97
98 Ref<XRInterface> xr_interface = xr_server->get_primary_interface();
99 if (xr_interface.is_null()) {
100 // we might be in the editor or have VR turned off, just call superclass
101 return Camera3D::project_local_ray_normal(p_pos);
102 }
103
104 ERR_FAIL_COND_V_MSG(!is_inside_tree(), Vector3(), "Camera is not inside scene.");
105
106 Size2 viewport_size = get_viewport()->get_camera_rect_size();
107 Vector2 cpos = get_viewport()->get_camera_coords(p_pos);
108 Vector3 ray;
109
110 // Just use the first view, if multiple views are supported this function has no good result
111 Projection cm = xr_interface->get_projection_for_view(0, viewport_size.aspect(), get_near(), get_far());
112 Vector2 screen_he = cm.get_viewport_half_extents();
113 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();
114
115 return ray;
116};
117
118Point2 XRCamera3D::unproject_position(const Vector3 &p_pos) const {
119 // get our XRServer
120 XRServer *xr_server = XRServer::get_singleton();
121 ERR_FAIL_NULL_V(xr_server, Vector2());
122
123 Ref<XRInterface> xr_interface = xr_server->get_primary_interface();
124 if (xr_interface.is_null()) {
125 // we might be in the editor or have VR turned off, just call superclass
126 return Camera3D::unproject_position(p_pos);
127 }
128
129 ERR_FAIL_COND_V_MSG(!is_inside_tree(), Vector2(), "Camera is not inside scene.");
130
131 Size2 viewport_size = get_viewport()->get_visible_rect().size;
132
133 // Just use the first view, if multiple views are supported this function has no good result
134 Projection cm = xr_interface->get_projection_for_view(0, viewport_size.aspect(), get_near(), get_far());
135
136 Plane p(get_camera_transform().xform_inv(p_pos), 1.0);
137
138 p = cm.xform4(p);
139 p.normal /= p.d;
140
141 Point2 res;
142 res.x = (p.normal.x * 0.5 + 0.5) * viewport_size.x;
143 res.y = (-p.normal.y * 0.5 + 0.5) * viewport_size.y;
144
145 return res;
146};
147
148Vector3 XRCamera3D::project_position(const Point2 &p_point, real_t p_z_depth) const {
149 // get our XRServer
150 XRServer *xr_server = XRServer::get_singleton();
151 ERR_FAIL_NULL_V(xr_server, Vector3());
152
153 Ref<XRInterface> xr_interface = xr_server->get_primary_interface();
154 if (xr_interface.is_null()) {
155 // we might be in the editor or have VR turned off, just call superclass
156 return Camera3D::project_position(p_point, p_z_depth);
157 }
158
159 ERR_FAIL_COND_V_MSG(!is_inside_tree(), Vector3(), "Camera is not inside scene.");
160
161 Size2 viewport_size = get_viewport()->get_visible_rect().size;
162
163 // Just use the first view, if multiple views are supported this function has no good result
164 Projection cm = xr_interface->get_projection_for_view(0, viewport_size.aspect(), get_near(), get_far());
165
166 Vector2 vp_he = cm.get_viewport_half_extents();
167
168 Vector2 point;
169 point.x = (p_point.x / viewport_size.x) * 2.0 - 1.0;
170 point.y = (1.0 - (p_point.y / viewport_size.y)) * 2.0 - 1.0;
171 point *= vp_he;
172
173 Vector3 p(point.x, point.y, -p_z_depth);
174
175 return get_camera_transform().xform(p);
176};
177
178Vector<Plane> XRCamera3D::get_frustum() const {
179 // get our XRServer
180 XRServer *xr_server = XRServer::get_singleton();
181 ERR_FAIL_NULL_V(xr_server, Vector<Plane>());
182
183 Ref<XRInterface> xr_interface = xr_server->get_primary_interface();
184 if (xr_interface.is_null()) {
185 // we might be in the editor or have VR turned off, just call superclass
186 return Camera3D::get_frustum();
187 }
188
189 ERR_FAIL_COND_V(!is_inside_world(), Vector<Plane>());
190
191 Size2 viewport_size = get_viewport()->get_visible_rect().size;
192 // TODO Just use the first view for now, this is mostly for debugging so we may look into using our combined projection here.
193 Projection cm = xr_interface->get_projection_for_view(0, viewport_size.aspect(), get_near(), get_far());
194 return cm.get_projection_planes(get_camera_transform());
195};
196
197XRCamera3D::XRCamera3D() {
198 XRServer *xr_server = XRServer::get_singleton();
199 ERR_FAIL_NULL(xr_server);
200
201 xr_server->connect("tracker_added", callable_mp(this, &XRCamera3D::_changed_tracker));
202 xr_server->connect("tracker_updated", callable_mp(this, &XRCamera3D::_changed_tracker));
203 xr_server->connect("tracker_removed", callable_mp(this, &XRCamera3D::_removed_tracker));
204
205 // check if our tracker already exists and if so, bind it...
206 _bind_tracker();
207}
208
209XRCamera3D::~XRCamera3D() {
210 XRServer *xr_server = XRServer::get_singleton();
211 ERR_FAIL_NULL(xr_server);
212
213 xr_server->disconnect("tracker_added", callable_mp(this, &XRCamera3D::_changed_tracker));
214 xr_server->disconnect("tracker_updated", callable_mp(this, &XRCamera3D::_changed_tracker));
215 xr_server->disconnect("tracker_removed", callable_mp(this, &XRCamera3D::_removed_tracker));
216}
217
218////////////////////////////////////////////////////////////////////////////////////////////////////
219// XRNode3D is a node that has it's transform updated by an XRPositionalTracker.
220// Note that trackers are only available in runtime and only after an XRInterface registers one.
221// So we bind by name and as long as a tracker isn't available, our node remains inactive.
222
223void XRNode3D::_bind_methods() {
224 ClassDB::bind_method(D_METHOD("set_tracker", "tracker_name"), &XRNode3D::set_tracker);
225 ClassDB::bind_method(D_METHOD("get_tracker"), &XRNode3D::get_tracker);
226 ADD_PROPERTY(PropertyInfo(Variant::STRING, "tracker", PROPERTY_HINT_ENUM_SUGGESTION), "set_tracker", "get_tracker");
227
228 ClassDB::bind_method(D_METHOD("set_pose_name", "pose"), &XRNode3D::set_pose_name);
229 ClassDB::bind_method(D_METHOD("get_pose_name"), &XRNode3D::get_pose_name);
230 ADD_PROPERTY(PropertyInfo(Variant::STRING, "pose", PROPERTY_HINT_ENUM_SUGGESTION), "set_pose_name", "get_pose_name");
231
232 ClassDB::bind_method(D_METHOD("get_is_active"), &XRNode3D::get_is_active);
233 ClassDB::bind_method(D_METHOD("get_has_tracking_data"), &XRNode3D::get_has_tracking_data);
234 ClassDB::bind_method(D_METHOD("get_pose"), &XRNode3D::get_pose);
235 ClassDB::bind_method(D_METHOD("trigger_haptic_pulse", "action_name", "frequency", "amplitude", "duration_sec", "delay_sec"), &XRNode3D::trigger_haptic_pulse);
236
237 ADD_SIGNAL(MethodInfo("tracking_changed", PropertyInfo(Variant::BOOL, "tracking")));
238};
239
240void XRNode3D::_validate_property(PropertyInfo &p_property) const {
241 XRServer *xr_server = XRServer::get_singleton();
242 ERR_FAIL_NULL(xr_server);
243
244 if (p_property.name == "tracker") {
245 PackedStringArray names = xr_server->get_suggested_tracker_names();
246 String hint_string;
247 for (const String &name : names) {
248 hint_string += name + ",";
249 }
250 p_property.hint_string = hint_string;
251 } else if (p_property.name == "pose") {
252 PackedStringArray names = xr_server->get_suggested_pose_names(tracker_name);
253 String hint_string;
254 for (const String &name : names) {
255 hint_string += name + ",";
256 }
257 p_property.hint_string = hint_string;
258 }
259}
260
261void XRNode3D::set_tracker(const StringName p_tracker_name) {
262 if (tracker.is_valid() && tracker->get_tracker_name() == p_tracker_name) {
263 // didn't change
264 return;
265 }
266
267 // just in case
268 _unbind_tracker();
269
270 // copy the name
271 tracker_name = p_tracker_name;
272 pose_name = "default";
273
274 // see if it's already available
275 _bind_tracker();
276
277 update_configuration_warnings();
278 notify_property_list_changed();
279}
280
281StringName XRNode3D::get_tracker() const {
282 return tracker_name;
283}
284
285void XRNode3D::set_pose_name(const StringName p_pose_name) {
286 pose_name = p_pose_name;
287
288 // Update pose if we are bound to a tracker with a valid pose
289 Ref<XRPose> pose = get_pose();
290 if (pose.is_valid()) {
291 set_transform(pose->get_adjusted_transform());
292 }
293}
294
295StringName XRNode3D::get_pose_name() const {
296 return pose_name;
297}
298
299bool XRNode3D::get_is_active() const {
300 if (tracker.is_null()) {
301 return false;
302 } else if (!tracker->has_pose(pose_name)) {
303 return false;
304 } else {
305 return true;
306 }
307}
308
309bool XRNode3D::get_has_tracking_data() const {
310 return has_tracking_data;
311}
312
313void XRNode3D::trigger_haptic_pulse(const String &p_action_name, double p_frequency, double p_amplitude, double p_duration_sec, double p_delay_sec) {
314 // TODO need to link trackers to the interface that registered them so we can call this on the correct interface.
315 // For now this works fine as in 99% of the cases we only have our primary interface active
316 XRServer *xr_server = XRServer::get_singleton();
317 if (xr_server != nullptr) {
318 Ref<XRInterface> xr_interface = xr_server->get_primary_interface();
319 if (xr_interface.is_valid()) {
320 xr_interface->trigger_haptic_pulse(p_action_name, tracker_name, p_frequency, p_amplitude, p_duration_sec, p_delay_sec);
321 }
322 }
323}
324
325Ref<XRPose> XRNode3D::get_pose() {
326 if (tracker.is_valid()) {
327 return tracker->get_pose(pose_name);
328 } else {
329 return Ref<XRPose>();
330 }
331}
332
333void XRNode3D::_bind_tracker() {
334 ERR_FAIL_COND_MSG(tracker.is_valid(), "Unbind the current tracker first");
335
336 XRServer *xr_server = XRServer::get_singleton();
337 if (xr_server != nullptr) {
338 tracker = xr_server->get_tracker(tracker_name);
339 if (tracker.is_null()) {
340 // It is possible and valid if the tracker isn't available (yet), in this case we just exit
341 return;
342 }
343
344 tracker->connect("pose_changed", callable_mp(this, &XRNode3D::_pose_changed));
345 tracker->connect("pose_lost_tracking", callable_mp(this, &XRNode3D::_pose_lost_tracking));
346
347 Ref<XRPose> pose = get_pose();
348 if (pose.is_valid()) {
349 set_transform(pose->get_adjusted_transform());
350 _set_has_tracking_data(pose->get_has_tracking_data());
351 }
352 }
353}
354
355void XRNode3D::_unbind_tracker() {
356 if (tracker.is_valid()) {
357 tracker->disconnect("pose_changed", callable_mp(this, &XRNode3D::_pose_changed));
358 tracker->disconnect("pose_lost_tracking", callable_mp(this, &XRNode3D::_pose_lost_tracking));
359
360 tracker.unref();
361
362 _set_has_tracking_data(false);
363 }
364}
365
366void XRNode3D::_changed_tracker(const StringName p_tracker_name, int p_tracker_type) {
367 if (tracker_name == p_tracker_name) {
368 // just in case unref our current tracker
369 _unbind_tracker();
370
371 // get our new tracker
372 _bind_tracker();
373 }
374}
375
376void XRNode3D::_removed_tracker(const StringName p_tracker_name, int p_tracker_type) {
377 if (tracker_name == p_tracker_name) {
378 // unref our tracker, it's no longer available
379 _unbind_tracker();
380 }
381}
382
383void XRNode3D::_pose_changed(const Ref<XRPose> &p_pose) {
384 if (p_pose.is_valid() && p_pose->get_name() == pose_name) {
385 set_transform(p_pose->get_adjusted_transform());
386 _set_has_tracking_data(p_pose->get_has_tracking_data());
387 }
388}
389
390void XRNode3D::_pose_lost_tracking(const Ref<XRPose> &p_pose) {
391 if (p_pose.is_valid() && p_pose->get_name() == pose_name) {
392 _set_has_tracking_data(false);
393 }
394}
395
396void XRNode3D::_set_has_tracking_data(bool p_has_tracking_data) {
397 // Ignore if the has_tracking_data state isn't changing.
398 if (p_has_tracking_data == has_tracking_data) {
399 return;
400 }
401
402 // Handle change of has_tracking_data.
403 has_tracking_data = p_has_tracking_data;
404 emit_signal(SNAME("tracking_changed"), has_tracking_data);
405}
406
407XRNode3D::XRNode3D() {
408 XRServer *xr_server = XRServer::get_singleton();
409 ERR_FAIL_NULL(xr_server);
410
411 xr_server->connect("tracker_added", callable_mp(this, &XRNode3D::_changed_tracker));
412 xr_server->connect("tracker_updated", callable_mp(this, &XRNode3D::_changed_tracker));
413 xr_server->connect("tracker_removed", callable_mp(this, &XRNode3D::_removed_tracker));
414}
415
416XRNode3D::~XRNode3D() {
417 _unbind_tracker();
418
419 XRServer *xr_server = XRServer::get_singleton();
420 ERR_FAIL_NULL(xr_server);
421
422 xr_server->disconnect("tracker_added", callable_mp(this, &XRNode3D::_changed_tracker));
423 xr_server->disconnect("tracker_updated", callable_mp(this, &XRNode3D::_changed_tracker));
424 xr_server->disconnect("tracker_removed", callable_mp(this, &XRNode3D::_removed_tracker));
425}
426
427PackedStringArray XRNode3D::get_configuration_warnings() const {
428 PackedStringArray warnings = Node::get_configuration_warnings();
429
430 if (is_visible() && is_inside_tree()) {
431 // must be child node of XROrigin!
432 XROrigin3D *origin = Object::cast_to<XROrigin3D>(get_parent());
433 if (origin == nullptr) {
434 warnings.push_back(RTR("XRController3D must have an XROrigin3D node as its parent."));
435 }
436
437 if (tracker_name == "") {
438 warnings.push_back(RTR("No tracker name is set."));
439 }
440
441 if (pose_name == "") {
442 warnings.push_back(RTR("No pose is set."));
443 }
444 }
445
446 return warnings;
447}
448
449////////////////////////////////////////////////////////////////////////////////////////////////////
450
451void XRController3D::_bind_methods() {
452 // passthroughs to information about our related joystick
453 ClassDB::bind_method(D_METHOD("is_button_pressed", "name"), &XRController3D::is_button_pressed);
454 ClassDB::bind_method(D_METHOD("get_input", "name"), &XRController3D::get_input);
455 ClassDB::bind_method(D_METHOD("get_float", "name"), &XRController3D::get_float);
456 ClassDB::bind_method(D_METHOD("get_vector2", "name"), &XRController3D::get_vector2);
457
458 ClassDB::bind_method(D_METHOD("get_tracker_hand"), &XRController3D::get_tracker_hand);
459
460 ADD_SIGNAL(MethodInfo("button_pressed", PropertyInfo(Variant::STRING, "name")));
461 ADD_SIGNAL(MethodInfo("button_released", PropertyInfo(Variant::STRING, "name")));
462 ADD_SIGNAL(MethodInfo("input_float_changed", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::FLOAT, "value")));
463 ADD_SIGNAL(MethodInfo("input_vector2_changed", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::VECTOR2, "value")));
464};
465
466void XRController3D::_bind_tracker() {
467 XRNode3D::_bind_tracker();
468 if (tracker.is_valid()) {
469 // bind to input signals
470 tracker->connect("button_pressed", callable_mp(this, &XRController3D::_button_pressed));
471 tracker->connect("button_released", callable_mp(this, &XRController3D::_button_released));
472 tracker->connect("input_float_changed", callable_mp(this, &XRController3D::_input_float_changed));
473 tracker->connect("input_vector2_changed", callable_mp(this, &XRController3D::_input_vector2_changed));
474 }
475}
476
477void XRController3D::_unbind_tracker() {
478 if (tracker.is_valid()) {
479 // unbind input signals
480 tracker->disconnect("button_pressed", callable_mp(this, &XRController3D::_button_pressed));
481 tracker->disconnect("button_released", callable_mp(this, &XRController3D::_button_released));
482 tracker->disconnect("input_float_changed", callable_mp(this, &XRController3D::_input_float_changed));
483 tracker->disconnect("input_vector2_changed", callable_mp(this, &XRController3D::_input_vector2_changed));
484 }
485
486 XRNode3D::_unbind_tracker();
487}
488
489void XRController3D::_button_pressed(const String &p_name) {
490 // just pass it on...
491 emit_signal(SNAME("button_pressed"), p_name);
492}
493
494void XRController3D::_button_released(const String &p_name) {
495 // just pass it on...
496 emit_signal(SNAME("button_released"), p_name);
497}
498
499void XRController3D::_input_float_changed(const String &p_name, float p_value) {
500 // just pass it on...
501 emit_signal(SNAME("input_float_changed"), p_name, p_value);
502}
503
504void XRController3D::_input_vector2_changed(const String &p_name, Vector2 p_value) {
505 // just pass it on...
506 emit_signal(SNAME("input_vector2_changed"), p_name, p_value);
507}
508
509bool XRController3D::is_button_pressed(const StringName &p_name) const {
510 if (tracker.is_valid()) {
511 // Inputs should already be of the correct type, our XR runtime handles conversions between raw input and the desired type
512 bool pressed = tracker->get_input(p_name);
513 return pressed;
514 } else {
515 return false;
516 }
517}
518
519Variant XRController3D::get_input(const StringName &p_name) const {
520 if (tracker.is_valid()) {
521 return tracker->get_input(p_name);
522 } else {
523 return Variant();
524 }
525}
526
527float XRController3D::get_float(const StringName &p_name) const {
528 if (tracker.is_valid()) {
529 // 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
530 Variant input = tracker->get_input(p_name);
531 switch (input.get_type()) {
532 case Variant::BOOL: {
533 bool value = input;
534 return value ? 1.0 : 0.0;
535 } break;
536 case Variant::FLOAT: {
537 float value = input;
538 return value;
539 } break;
540 default:
541 return 0.0;
542 };
543 } else {
544 return 0.0;
545 }
546}
547
548Vector2 XRController3D::get_vector2(const StringName &p_name) const {
549 if (tracker.is_valid()) {
550 // 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
551 Variant input = tracker->get_input(p_name);
552 switch (input.get_type()) {
553 case Variant::BOOL: {
554 bool value = input;
555 return Vector2(value ? 1.0 : 0.0, 0.0);
556 } break;
557 case Variant::FLOAT: {
558 float value = input;
559 return Vector2(value, 0.0);
560 } break;
561 case Variant::VECTOR2: {
562 Vector2 axis = input;
563 return axis;
564 }
565 default:
566 return Vector2();
567 }
568 } else {
569 return Vector2();
570 }
571}
572
573XRPositionalTracker::TrackerHand XRController3D::get_tracker_hand() const {
574 // get our XRServer
575 if (!tracker.is_valid()) {
576 return XRPositionalTracker::TRACKER_HAND_UNKNOWN;
577 }
578
579 return tracker->get_tracker_hand();
580}
581
582////////////////////////////////////////////////////////////////////////////////////////////////////
583
584void XRAnchor3D::_bind_methods() {
585 ClassDB::bind_method(D_METHOD("get_size"), &XRAnchor3D::get_size);
586 ClassDB::bind_method(D_METHOD("get_plane"), &XRAnchor3D::get_plane);
587}
588
589Vector3 XRAnchor3D::get_size() const {
590 return size;
591}
592
593Plane XRAnchor3D::get_plane() const {
594 Vector3 location = get_position();
595 Basis orientation = get_transform().basis;
596
597 Plane plane(orientation.get_column(1).normalized(), location);
598
599 return plane;
600}
601
602////////////////////////////////////////////////////////////////////////////////////////////////////
603
604Vector<XROrigin3D *> XROrigin3D::origin_nodes;
605
606PackedStringArray XROrigin3D::get_configuration_warnings() const {
607 PackedStringArray warnings = Node::get_configuration_warnings();
608
609 if (is_visible() && is_inside_tree()) {
610 bool has_camera = false;
611 for (int i = 0; !has_camera && i < get_child_count(); i++) {
612 XRCamera3D *camera = Object::cast_to<XRCamera3D>(get_child(i));
613 if (camera) {
614 // found it!
615 has_camera = true;
616 }
617 }
618
619 if (!has_camera) {
620 warnings.push_back(RTR("XROrigin3D requires an XRCamera3D child node."));
621 }
622 }
623
624 bool xr_enabled = GLOBAL_GET("xr/shaders/enabled");
625 if (!xr_enabled) {
626 warnings.push_back(RTR("XR is not enabled in rendering project settings. Stereoscopic output is not supported unless this is enabled."));
627 }
628
629 return warnings;
630}
631
632void XROrigin3D::_bind_methods() {
633 ClassDB::bind_method(D_METHOD("set_world_scale", "world_scale"), &XROrigin3D::set_world_scale);
634 ClassDB::bind_method(D_METHOD("get_world_scale"), &XROrigin3D::get_world_scale);
635 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "world_scale"), "set_world_scale", "get_world_scale");
636
637 ClassDB::bind_method(D_METHOD("set_current", "enabled"), &XROrigin3D::set_current);
638 ClassDB::bind_method(D_METHOD("is_current"), &XROrigin3D::is_current);
639 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "current"), "set_current", "is_current");
640}
641
642real_t XROrigin3D::get_world_scale() const {
643 // get our XRServer
644 XRServer *xr_server = XRServer::get_singleton();
645 ERR_FAIL_NULL_V(xr_server, 1.0);
646
647 return xr_server->get_world_scale();
648}
649
650void XROrigin3D::set_world_scale(real_t p_world_scale) {
651 // get our XRServer
652 XRServer *xr_server = XRServer::get_singleton();
653 ERR_FAIL_NULL(xr_server);
654
655 xr_server->set_world_scale(p_world_scale);
656}
657
658void XROrigin3D::_set_current(bool p_enabled, bool p_update_others) {
659 // We run this logic even if current already equals p_enabled as we may have set this previously before we entered our tree.
660 // This is then called a second time on NOTIFICATION_ENTER_TREE where we actually process activating this origin node.
661 current = p_enabled;
662
663 if (!is_inside_tree() || Engine::get_singleton()->is_editor_hint()) {
664 return;
665 }
666
667 // Notify us of any transform changes
668 set_notify_local_transform(current);
669 set_notify_transform(current);
670
671 // update XRServer with our current position
672 if (current) {
673 XRServer *xr_server = XRServer::get_singleton();
674 ERR_FAIL_NULL(xr_server);
675
676 xr_server->set_world_origin(get_global_transform());
677 }
678
679 // Check if we need to update our other origin nodes accordingly
680 if (p_update_others) {
681 if (current) {
682 for (int i = 0; i < origin_nodes.size(); i++) {
683 if (origin_nodes[i] != this && origin_nodes[i]->current) {
684 origin_nodes[i]->_set_current(false, false);
685 }
686 }
687 } else {
688 // We no longer have a current origin so find the first one we can make current
689 for (int i = 0; i < origin_nodes.size(); i++) {
690 if (origin_nodes[i] != this) {
691 origin_nodes[i]->_set_current(true, false);
692 return; // we are done.
693 }
694 }
695 }
696 }
697}
698
699void XROrigin3D::set_current(bool p_enabled) {
700 _set_current(p_enabled, true);
701}
702
703bool XROrigin3D::is_current() const {
704 if (Engine::get_singleton()->is_editor_hint()) {
705 // return as is
706 return current;
707 } else {
708 return current && is_inside_tree();
709 }
710}
711
712void XROrigin3D::_notification(int p_what) {
713 // get our XRServer
714 XRServer *xr_server = XRServer::get_singleton();
715 ERR_FAIL_NULL(xr_server);
716
717 switch (p_what) {
718 case NOTIFICATION_ENTER_TREE: {
719 if (!Engine::get_singleton()->is_editor_hint()) {
720 if (origin_nodes.is_empty()) {
721 // first entry always becomes current
722 current = true;
723 }
724
725 origin_nodes.push_back(this);
726
727 if (current) {
728 // set this again so we do whatever setup is needed.
729 set_current(true);
730 }
731 }
732 } break;
733
734 case NOTIFICATION_EXIT_TREE: {
735 if (!Engine::get_singleton()->is_editor_hint()) {
736 origin_nodes.erase(this);
737
738 if (current) {
739 // We are no longer current
740 set_current(false);
741 }
742 }
743 } break;
744
745 case NOTIFICATION_LOCAL_TRANSFORM_CHANGED:
746 case NOTIFICATION_TRANSFORM_CHANGED: {
747 if (current && !Engine::get_singleton()->is_editor_hint()) {
748 xr_server->set_world_origin(get_global_transform());
749 }
750 } break;
751 }
752
753 if (current) {
754 // send our notification to all active XE interfaces, they may need to react to it also
755 for (int i = 0; i < xr_server->get_interface_count(); i++) {
756 Ref<XRInterface> interface = xr_server->get_interface(i);
757 if (interface.is_valid() && interface->is_initialized()) {
758 interface->notification(p_what);
759 }
760 }
761 }
762}
763