2023-01-05 13:25:55 +01:00
/**************************************************************************/
/* xr_nodes.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. */
/**************************************************************************/
2017-04-23 14:10:41 +02:00
2020-04-08 16:47:36 +02:00
# include "xr_nodes.h"
2020-05-19 11:24:58 +02:00
2022-02-12 02:46:22 +01:00
# include "core/config/project_settings.h"
2021-08-13 01:05:59 +02:00
# include "scene/main/viewport.h"
2020-04-08 16:47:36 +02:00
# include "servers/xr/xr_interface.h"
2017-04-23 14:10:41 +02:00
////////////////////////////////////////////////////////////////////////////////////////////////////
2022-07-29 06:18:05 +02:00
void XRCamera3D : : _bind_tracker ( ) {
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL ( xr_server ) ;
2022-02-15 18:06:48 +01:00
2022-07-29 06:18:05 +02:00
tracker = xr_server - > get_tracker ( tracker_name ) ;
if ( tracker . is_valid ( ) ) {
tracker - > connect ( " pose_changed " , callable_mp ( this , & XRCamera3D : : _pose_changed ) ) ;
Ref < XRPose > pose = tracker - > get_pose ( pose_name ) ;
if ( pose . is_valid ( ) ) {
set_transform ( pose - > get_adjusted_transform ( ) ) ;
}
2022-02-15 18:06:48 +01:00
}
}
2017-04-23 14:10:41 +02:00
2022-07-29 06:18:05 +02:00
void XRCamera3D : : _unbind_tracker ( ) {
if ( tracker . is_valid ( ) ) {
tracker - > disconnect ( " pose_changed " , callable_mp ( this , & XRCamera3D : : _pose_changed ) ) ;
}
tracker . unref ( ) ;
}
2021-08-29 08:05:11 +02:00
void XRCamera3D : : _changed_tracker ( const StringName p_tracker_name , int p_tracker_type ) {
if ( p_tracker_name = = tracker_name ) {
2022-07-29 06:18:05 +02:00
_bind_tracker ( ) ;
2021-08-29 08:05:11 +02:00
}
}
void XRCamera3D : : _removed_tracker ( const StringName p_tracker_name , int p_tracker_type ) {
if ( p_tracker_name = = tracker_name ) {
2022-07-29 06:18:05 +02:00
_unbind_tracker ( ) ;
2021-08-29 08:05:11 +02:00
}
}
void XRCamera3D : : _pose_changed ( const Ref < XRPose > & p_pose ) {
if ( p_pose - > get_name ( ) = = pose_name ) {
set_transform ( p_pose - > get_adjusted_transform ( ) ) ;
}
}
2022-09-19 17:43:15 +02:00
PackedStringArray XRCamera3D : : get_configuration_warnings ( ) const {
PackedStringArray warnings = Node : : get_configuration_warnings ( ) ;
2020-10-29 11:01:28 +01:00
if ( is_visible ( ) & & is_inside_tree ( ) ) {
// must be child node of XROrigin3D!
XROrigin3D * origin = Object : : cast_to < XROrigin3D > ( get_parent ( ) ) ;
if ( origin = = nullptr ) {
2022-03-28 15:24:14 +02:00
warnings . push_back ( RTR ( " XRCamera3D must have an XROrigin3D node as its parent. " ) ) ;
2020-10-29 11:01:28 +01:00
} ;
2020-05-14 16:41:43 +02:00
}
2017-04-23 14:10:41 +02:00
2020-10-29 11:01:28 +01:00
return warnings ;
2017-04-23 14:10:41 +02:00
} ;
2020-04-08 16:47:36 +02:00
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 ( ) ) ;
2017-09-29 13:36:27 +02:00
2020-04-08 16:47:36 +02:00
Ref < XRInterface > xr_interface = xr_server - > get_primary_interface ( ) ;
if ( xr_interface . is_null ( ) ) {
2018-06-24 03:54:08 +02:00
// we might be in the editor or have VR turned off, just call superclass
2020-03-26 22:49:16 +01:00
return Camera3D : : project_local_ray_normal ( p_pos ) ;
2018-06-24 03:54:08 +02:00
}
2017-09-29 13:36:27 +02:00
2019-08-08 22:11:48 +02:00
ERR_FAIL_COND_V_MSG ( ! is_inside_tree ( ) , Vector3 ( ) , " Camera is not inside scene. " ) ;
2017-09-29 13:36:27 +02:00
Size2 viewport_size = get_viewport ( ) - > get_camera_rect_size ( ) ;
Vector2 cpos = get_viewport ( ) - > get_camera_coords ( p_pos ) ;
Vector3 ray ;
2021-05-07 15:19:04 +02:00
// Just use the first view, if multiple views are supported this function has no good result
Implement Vector4, Vector4i, Projection
Implement built-in classes Vector4, Vector4i and Projection.
* Two versions of Vector4 (float and integer).
* A Projection class, which is a 4x4 matrix specialized in projection types.
These types have been requested for a long time, but given they were very corner case they were not added before.
Because in Godot 4, reimplementing parts of the rendering engine is now possible, access to these types (heavily used by the rendering code) becomes a necessity.
**Q**: Why Projection and not Matrix4?
**A**: Godot does not use Matrix2, Matrix3, Matrix4x3, etc. naming convention because, within the engine, these types always have a *purpose*. As such, Godot names them: Transform2D, Transform3D or Basis. In this case, this 4x4 matrix is _always_ used as a _Projection_, hence the naming.
2022-07-20 01:11:13 +02:00
Projection cm = xr_interface - > get_projection_for_view ( 0 , viewport_size . aspect ( ) , get_near ( ) , get_far ( ) ) ;
2020-01-21 19:39:16 +01:00
Vector2 screen_he = cm . get_viewport_half_extents ( ) ;
2020-12-16 13:40:42 +01:00
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 ( ) ;
2017-09-29 13:36:27 +02:00
return ray ;
} ;
2020-04-08 16:47:36 +02:00
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 ( ) ) ;
2017-09-29 13:36:27 +02:00
2020-04-08 16:47:36 +02:00
Ref < XRInterface > xr_interface = xr_server - > get_primary_interface ( ) ;
if ( xr_interface . is_null ( ) ) {
2018-06-24 03:54:08 +02:00
// we might be in the editor or have VR turned off, just call superclass
2020-03-26 22:49:16 +01:00
return Camera3D : : unproject_position ( p_pos ) ;
2018-06-24 03:54:08 +02:00
}
2017-09-29 13:36:27 +02:00
2019-08-08 22:11:48 +02:00
ERR_FAIL_COND_V_MSG ( ! is_inside_tree ( ) , Vector2 ( ) , " Camera is not inside scene. " ) ;
2017-09-29 13:36:27 +02:00
Size2 viewport_size = get_viewport ( ) - > get_visible_rect ( ) . size ;
2021-05-07 15:19:04 +02:00
// Just use the first view, if multiple views are supported this function has no good result
Implement Vector4, Vector4i, Projection
Implement built-in classes Vector4, Vector4i and Projection.
* Two versions of Vector4 (float and integer).
* A Projection class, which is a 4x4 matrix specialized in projection types.
These types have been requested for a long time, but given they were very corner case they were not added before.
Because in Godot 4, reimplementing parts of the rendering engine is now possible, access to these types (heavily used by the rendering code) becomes a necessity.
**Q**: Why Projection and not Matrix4?
**A**: Godot does not use Matrix2, Matrix3, Matrix4x3, etc. naming convention because, within the engine, these types always have a *purpose*. As such, Godot names them: Transform2D, Transform3D or Basis. In this case, this 4x4 matrix is _always_ used as a _Projection_, hence the naming.
2022-07-20 01:11:13 +02:00
Projection cm = xr_interface - > get_projection_for_view ( 0 , viewport_size . aspect ( ) , get_near ( ) , get_far ( ) ) ;
2017-09-29 13:36:27 +02:00
Plane p ( get_camera_transform ( ) . xform_inv ( p_pos ) , 1.0 ) ;
p = cm . xform4 ( p ) ;
2020-05-10 16:47:11 +02:00
p . normal / = p . d ;
2017-09-29 13:36:27 +02:00
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 ;
} ;
2021-01-30 01:55:54 +01:00
Vector3 XRCamera3D : : project_position ( const Point2 & p_point , real_t p_z_depth ) const {
2020-04-08 16:47:36 +02:00
// get our XRServer
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL_V ( xr_server , Vector3 ( ) ) ;
2017-09-29 13:36:27 +02:00
2020-04-08 16:47:36 +02:00
Ref < XRInterface > xr_interface = xr_server - > get_primary_interface ( ) ;
if ( xr_interface . is_null ( ) ) {
2018-06-24 03:54:08 +02:00
// we might be in the editor or have VR turned off, just call superclass
2020-03-26 22:49:16 +01:00
return Camera3D : : project_position ( p_point , p_z_depth ) ;
2018-06-24 03:54:08 +02:00
}
2017-09-29 13:36:27 +02:00
2019-08-08 22:11:48 +02:00
ERR_FAIL_COND_V_MSG ( ! is_inside_tree ( ) , Vector3 ( ) , " Camera is not inside scene. " ) ;
2017-09-29 13:36:27 +02:00
Size2 viewport_size = get_viewport ( ) - > get_visible_rect ( ) . size ;
2021-05-07 15:19:04 +02:00
// Just use the first view, if multiple views are supported this function has no good result
Implement Vector4, Vector4i, Projection
Implement built-in classes Vector4, Vector4i and Projection.
* Two versions of Vector4 (float and integer).
* A Projection class, which is a 4x4 matrix specialized in projection types.
These types have been requested for a long time, but given they were very corner case they were not added before.
Because in Godot 4, reimplementing parts of the rendering engine is now possible, access to these types (heavily used by the rendering code) becomes a necessity.
**Q**: Why Projection and not Matrix4?
**A**: Godot does not use Matrix2, Matrix3, Matrix4x3, etc. naming convention because, within the engine, these types always have a *purpose*. As such, Godot names them: Transform2D, Transform3D or Basis. In this case, this 4x4 matrix is _always_ used as a _Projection_, hence the naming.
2022-07-20 01:11:13 +02:00
Projection cm = xr_interface - > get_projection_for_view ( 0 , viewport_size . aspect ( ) , get_near ( ) , get_far ( ) ) ;
2017-09-29 13:36:27 +02:00
2020-01-21 19:39:16 +01:00
Vector2 vp_he = cm . get_viewport_half_extents ( ) ;
2017-09-29 13:36:27 +02:00
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 ;
2020-01-21 19:39:16 +01:00
point * = vp_he ;
2017-09-29 13:36:27 +02:00
2019-05-28 15:14:13 +02:00
Vector3 p ( point . x , point . y , - p_z_depth ) ;
2017-09-29 13:36:27 +02:00
return get_camera_transform ( ) . xform ( p ) ;
} ;
2020-04-08 16:47:36 +02:00
Vector < Plane > XRCamera3D : : get_frustum ( ) const {
// get our XRServer
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL_V ( xr_server , Vector < Plane > ( ) ) ;
2017-09-29 13:36:27 +02:00
2020-04-08 16:47:36 +02:00
Ref < XRInterface > xr_interface = xr_server - > get_primary_interface ( ) ;
if ( xr_interface . is_null ( ) ) {
2018-06-24 03:54:08 +02:00
// we might be in the editor or have VR turned off, just call superclass
2020-03-26 22:49:16 +01:00
return Camera3D : : get_frustum ( ) ;
2018-06-24 03:54:08 +02:00
}
2017-09-29 13:36:27 +02:00
ERR_FAIL_COND_V ( ! is_inside_world ( ) , Vector < Plane > ( ) ) ;
Size2 viewport_size = get_viewport ( ) - > get_visible_rect ( ) . size ;
2021-05-07 15:19:04 +02:00
// TODO Just use the first view for now, this is mostly for debugging so we may look into using our combined projection here.
Implement Vector4, Vector4i, Projection
Implement built-in classes Vector4, Vector4i and Projection.
* Two versions of Vector4 (float and integer).
* A Projection class, which is a 4x4 matrix specialized in projection types.
These types have been requested for a long time, but given they were very corner case they were not added before.
Because in Godot 4, reimplementing parts of the rendering engine is now possible, access to these types (heavily used by the rendering code) becomes a necessity.
**Q**: Why Projection and not Matrix4?
**A**: Godot does not use Matrix2, Matrix3, Matrix4x3, etc. naming convention because, within the engine, these types always have a *purpose*. As such, Godot names them: Transform2D, Transform3D or Basis. In this case, this 4x4 matrix is _always_ used as a _Projection_, hence the naming.
2022-07-20 01:11:13 +02:00
Projection cm = xr_interface - > get_projection_for_view ( 0 , viewport_size . aspect ( ) , get_near ( ) , get_far ( ) ) ;
2017-09-29 13:36:27 +02:00
return cm . get_projection_planes ( get_camera_transform ( ) ) ;
} ;
2021-08-29 08:05:11 +02:00
XRCamera3D : : XRCamera3D ( ) {
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL ( xr_server ) ;
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
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 ) ) ;
2022-07-29 06:18:05 +02:00
// check if our tracker already exists and if so, bind it...
_bind_tracker ( ) ;
2021-08-29 08:05:11 +02:00
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
XRCamera3D : : ~ XRCamera3D ( ) {
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL ( xr_server ) ;
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
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 ) ) ;
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
////////////////////////////////////////////////////////////////////////////////////////////////////
// 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.
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
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 " ) ;
2017-11-01 11:46:37 +01:00
2021-08-29 08:05:11 +02:00
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 " ) ;
2019-02-05 11:02:13 +01:00
2021-08-29 08:05:11 +02:00
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 ) ;
2023-09-02 01:13:54 +02:00
ADD_SIGNAL ( MethodInfo ( " tracking_changed " , PropertyInfo ( Variant : : BOOL , " tracking " ) ) ) ;
2017-04-23 14:10:41 +02:00
} ;
2022-08-12 22:57:11 +02:00
void XRNode3D : : _validate_property ( PropertyInfo & p_property ) const {
2021-08-29 08:05:11 +02:00
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL ( xr_server ) ;
2017-04-23 14:10:41 +02:00
2022-08-12 22:57:11 +02:00
if ( p_property . name = = " tracker " ) {
2021-08-29 08:05:11 +02:00
PackedStringArray names = xr_server - > get_suggested_tracker_names ( ) ;
String hint_string ;
for ( const String & name : names ) {
hint_string + = name + " , " ;
}
2022-08-12 22:57:11 +02:00
p_property . hint_string = hint_string ;
} else if ( p_property . name = = " pose " ) {
2021-08-29 08:05:11 +02:00
PackedStringArray names = xr_server - > get_suggested_pose_names ( tracker_name ) ;
String hint_string ;
for ( const String & name : names ) {
hint_string + = name + " , " ;
}
2022-08-12 22:57:11 +02:00
p_property . hint_string = hint_string ;
2021-08-29 08:05:11 +02:00
}
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
void XRNode3D : : set_tracker ( const StringName p_tracker_name ) {
if ( tracker . is_valid ( ) & & tracker - > get_tracker_name ( ) = = p_tracker_name ) {
// didn't change
return ;
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
// just in case
_unbind_tracker ( ) ;
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
// copy the name
tracker_name = p_tracker_name ;
pose_name = " default " ;
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
// see if it's already available
_bind_tracker ( ) ;
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
update_configuration_warnings ( ) ;
notify_property_list_changed ( ) ;
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
StringName XRNode3D : : get_tracker ( ) const {
return tracker_name ;
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
void XRNode3D : : set_pose_name ( const StringName p_pose_name ) {
pose_name = p_pose_name ;
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
// Update pose if we are bound to a tracker with a valid pose
Ref < XRPose > pose = get_pose ( ) ;
if ( pose . is_valid ( ) ) {
set_transform ( pose - > get_adjusted_transform ( ) ) ;
}
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
StringName XRNode3D : : get_pose_name ( ) const {
return pose_name ;
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
bool XRNode3D : : get_is_active ( ) const {
if ( tracker . is_null ( ) ) {
return false ;
} else if ( ! tracker - > has_pose ( pose_name ) ) {
return false ;
} else {
return true ;
}
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
bool XRNode3D : : get_has_tracking_data ( ) const {
2023-09-02 01:13:54 +02:00
return has_tracking_data ;
2021-08-29 08:05:11 +02:00
}
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
2020-04-08 16:47:36 +02:00
XRServer * xr_server = XRServer : : get_singleton ( ) ;
2021-08-29 08:05:11 +02:00
if ( xr_server ! = nullptr ) {
Ref < XRInterface > 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 ) ;
}
}
}
2017-11-01 11:46:37 +01:00
2021-08-29 08:05:11 +02:00
Ref < XRPose > XRNode3D : : get_pose ( ) {
if ( tracker . is_valid ( ) ) {
return tracker - > get_pose ( pose_name ) ;
} else {
return Ref < XRPose > ( ) ;
}
}
2017-11-01 11:46:37 +01:00
2021-08-29 08:05:11 +02:00
void XRNode3D : : _bind_tracker ( ) {
ERR_FAIL_COND_MSG ( tracker . is_valid ( ) , " Unbind the current tracker first " ) ;
2017-11-01 11:46:37 +01:00
2020-04-08 16:47:36 +02:00
XRServer * xr_server = XRServer : : get_singleton ( ) ;
2021-08-29 08:05:11 +02:00
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 ) ) ;
2023-09-02 01:13:54 +02:00
tracker - > connect ( " pose_lost_tracking " , callable_mp ( this , & XRNode3D : : _pose_lost_tracking ) ) ;
2021-08-29 08:05:11 +02:00
Ref < XRPose > pose = get_pose ( ) ;
if ( pose . is_valid ( ) ) {
set_transform ( pose - > get_adjusted_transform ( ) ) ;
2023-09-02 01:13:54 +02:00
_set_has_tracking_data ( pose - > get_has_tracking_data ( ) ) ;
2021-08-29 08:05:11 +02:00
}
}
}
2017-11-01 11:46:37 +01:00
2021-08-29 08:05:11 +02:00
void XRNode3D : : _unbind_tracker ( ) {
2021-03-29 12:15:53 +02:00
if ( tracker . is_valid ( ) ) {
2021-08-29 08:05:11 +02:00
tracker - > disconnect ( " pose_changed " , callable_mp ( this , & XRNode3D : : _pose_changed ) ) ;
2023-09-02 01:13:54 +02:00
tracker - > disconnect ( " pose_lost_tracking " , callable_mp ( this , & XRNode3D : : _pose_lost_tracking ) ) ;
2017-11-01 11:46:37 +01:00
2021-08-29 08:05:11 +02:00
tracker . unref ( ) ;
2023-09-02 01:13:54 +02:00
_set_has_tracking_data ( false ) ;
2021-08-29 08:05:11 +02:00
}
2019-02-05 11:02:13 +01:00
}
2021-08-29 08:05:11 +02:00
void XRNode3D : : _changed_tracker ( const StringName p_tracker_name , int p_tracker_type ) {
2022-10-17 11:18:04 +02:00
if ( tracker_name = = p_tracker_name ) {
2021-08-29 08:05:11 +02:00
// just in case unref our current tracker
_unbind_tracker ( ) ;
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
// get our new tracker
_bind_tracker ( ) ;
}
}
void XRNode3D : : _removed_tracker ( const StringName p_tracker_name , int p_tracker_type ) {
2022-10-17 11:18:04 +02:00
if ( tracker_name = = p_tracker_name ) {
2021-08-29 08:05:11 +02:00
// unref our tracker, it's no longer available
_unbind_tracker ( ) ;
}
}
void XRNode3D : : _pose_changed ( const Ref < XRPose > & p_pose ) {
if ( p_pose . is_valid ( ) & & p_pose - > get_name ( ) = = pose_name ) {
set_transform ( p_pose - > get_adjusted_transform ( ) ) ;
2023-09-02 01:13:54 +02:00
_set_has_tracking_data ( p_pose - > get_has_tracking_data ( ) ) ;
2021-08-29 08:05:11 +02:00
}
}
2023-09-02 01:13:54 +02:00
void XRNode3D : : _pose_lost_tracking ( const Ref < XRPose > & p_pose ) {
if ( p_pose . is_valid ( ) & & p_pose - > get_name ( ) = = pose_name ) {
_set_has_tracking_data ( false ) ;
}
}
void XRNode3D : : _set_has_tracking_data ( bool p_has_tracking_data ) {
// Ignore if the has_tracking_data state isn't changing.
if ( p_has_tracking_data = = has_tracking_data ) {
return ;
}
// Handle change of has_tracking_data.
has_tracking_data = p_has_tracking_data ;
emit_signal ( SNAME ( " tracking_changed " ) , has_tracking_data ) ;
}
2021-08-29 08:05:11 +02:00
XRNode3D : : XRNode3D ( ) {
2020-04-08 16:47:36 +02:00
XRServer * xr_server = XRServer : : get_singleton ( ) ;
2021-08-29 08:05:11 +02:00
ERR_FAIL_NULL ( xr_server ) ;
2017-09-10 08:15:11 +02:00
2021-08-29 08:05:11 +02:00
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 ) ) ;
}
2017-09-10 08:15:11 +02:00
2021-08-29 08:05:11 +02:00
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 ) ) ;
}
2017-09-10 08:15:11 +02:00
2022-09-19 17:43:15 +02:00
PackedStringArray XRNode3D : : get_configuration_warnings ( ) const {
PackedStringArray warnings = Node : : get_configuration_warnings ( ) ;
2020-05-14 22:59:27 +02:00
2020-10-29 11:01:28 +01:00
if ( is_visible ( ) & & is_inside_tree ( ) ) {
// must be child node of XROrigin!
XROrigin3D * origin = Object : : cast_to < XROrigin3D > ( get_parent ( ) ) ;
if ( origin = = nullptr ) {
2022-03-28 15:24:14 +02:00
warnings . push_back ( RTR ( " XRController3D must have an XROrigin3D node as its parent. " ) ) ;
2020-05-14 22:59:27 +02:00
}
2017-04-23 14:10:41 +02:00
2021-08-29 08:05:11 +02:00
if ( tracker_name = = " " ) {
2022-03-28 15:24:14 +02:00
warnings . push_back ( RTR ( " No tracker name is set. " ) ) ;
2021-08-29 08:05:11 +02:00
}
if ( pose_name = = " " ) {
2022-03-28 15:24:14 +02:00
warnings . push_back ( RTR ( " No pose is set. " ) ) ;
2020-05-14 22:59:27 +02:00
}
2020-10-29 11:01:28 +01:00
}
2017-04-23 14:10:41 +02:00
2020-10-29 11:01:28 +01:00
return warnings ;
2021-08-29 08:05:11 +02:00
}
2017-04-23 14:10:41 +02:00
////////////////////////////////////////////////////////////////////////////////////////////////////
2021-08-29 08:05:11 +02:00
void XRController3D : : _bind_methods ( ) {
// passthroughs to information about our related joystick
ClassDB : : bind_method ( D_METHOD ( " is_button_pressed " , " name " ) , & XRController3D : : is_button_pressed ) ;
2023-01-22 03:17:20 +01:00
ClassDB : : bind_method ( D_METHOD ( " get_input " , " name " ) , & XRController3D : : get_input ) ;
ClassDB : : bind_method ( D_METHOD ( " get_float " , " name " ) , & XRController3D : : get_float ) ;
ClassDB : : bind_method ( D_METHOD ( " get_vector2 " , " name " ) , & XRController3D : : get_vector2 ) ;
2017-08-03 10:58:05 +02:00
2021-08-29 08:05:11 +02:00
ClassDB : : bind_method ( D_METHOD ( " get_tracker_hand " ) , & XRController3D : : get_tracker_hand ) ;
2017-09-29 13:36:27 +02:00
2021-08-29 08:05:11 +02:00
ADD_SIGNAL ( MethodInfo ( " button_pressed " , PropertyInfo ( Variant : : STRING , " name " ) ) ) ;
ADD_SIGNAL ( MethodInfo ( " button_released " , PropertyInfo ( Variant : : STRING , " name " ) ) ) ;
2023-01-22 03:17:20 +01:00
ADD_SIGNAL ( MethodInfo ( " input_float_changed " , PropertyInfo ( Variant : : STRING , " name " ) , PropertyInfo ( Variant : : FLOAT , " value " ) ) ) ;
ADD_SIGNAL ( MethodInfo ( " input_vector2_changed " , PropertyInfo ( Variant : : STRING , " name " ) , PropertyInfo ( Variant : : VECTOR2 , " value " ) ) ) ;
2017-08-03 10:58:05 +02:00
} ;
2021-08-29 08:05:11 +02:00
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 ) ) ;
2023-01-22 03:17:20 +01:00
tracker - > connect ( " input_float_changed " , callable_mp ( this , & XRController3D : : _input_float_changed ) ) ;
tracker - > connect ( " input_vector2_changed " , callable_mp ( this , & XRController3D : : _input_vector2_changed ) ) ;
2021-08-29 08:05:11 +02:00
}
}
2017-08-03 10:58:05 +02:00
2021-08-29 08:05:11 +02:00
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 ) ) ;
2023-01-22 03:17:20 +01:00
tracker - > disconnect ( " input_float_changed " , callable_mp ( this , & XRController3D : : _input_float_changed ) ) ;
tracker - > disconnect ( " input_vector2_changed " , callable_mp ( this , & XRController3D : : _input_vector2_changed ) ) ;
2021-08-29 08:05:11 +02:00
}
2017-08-03 10:58:05 +02:00
2021-08-29 08:05:11 +02:00
XRNode3D : : _unbind_tracker ( ) ;
}
2017-08-03 10:58:05 +02:00
2021-08-29 08:05:11 +02:00
void XRController3D : : _button_pressed ( const String & p_name ) {
// just pass it on...
2022-02-06 15:53:53 +01:00
emit_signal ( SNAME ( " button_pressed " ) , p_name ) ;
2021-08-29 08:05:11 +02:00
}
2017-08-03 10:58:05 +02:00
2021-08-29 08:05:11 +02:00
void XRController3D : : _button_released ( const String & p_name ) {
// just pass it on...
2022-02-06 15:53:53 +01:00
emit_signal ( SNAME ( " button_released " ) , p_name ) ;
2021-08-29 08:05:11 +02:00
}
2017-08-03 10:58:05 +02:00
2023-01-22 03:17:20 +01:00
void XRController3D : : _input_float_changed ( const String & p_name , float p_value ) {
2021-08-29 08:05:11 +02:00
// just pass it on...
2023-01-22 03:17:20 +01:00
emit_signal ( SNAME ( " input_float_changed " ) , p_name , p_value ) ;
2021-08-29 08:05:11 +02:00
}
2017-08-03 10:58:05 +02:00
2023-01-22 03:17:20 +01:00
void XRController3D : : _input_vector2_changed ( const String & p_name , Vector2 p_value ) {
2021-08-29 08:05:11 +02:00
// just pass it on...
2023-01-22 03:17:20 +01:00
emit_signal ( SNAME ( " input_vector2_changed " ) , p_name , p_value ) ;
2021-08-29 08:05:11 +02:00
}
2017-08-03 10:58:05 +02:00
2021-08-29 08:05:11 +02:00
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 ;
}
}
2020-05-14 22:59:27 +02:00
2023-01-22 03:17:20 +01:00
Variant XRController3D : : get_input ( const StringName & p_name ) const {
if ( tracker . is_valid ( ) ) {
return tracker - > get_input ( p_name ) ;
} else {
return Variant ( ) ;
}
}
float XRController3D : : get_float ( const StringName & p_name ) const {
2021-08-29 08:05:11 +02:00
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 ;
}
}
2017-08-03 10:58:05 +02:00
2023-01-22 03:17:20 +01:00
Vector2 XRController3D : : get_vector2 ( const StringName & p_name ) const {
2021-08-29 08:05:11 +02:00
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 ( ) ;
2020-05-14 22:59:27 +02:00
}
2021-08-29 08:05:11 +02:00
} else {
return Vector2 ( ) ;
2020-10-29 11:01:28 +01:00
}
2021-08-29 08:05:11 +02:00
}
2017-08-03 10:58:05 +02:00
2021-08-29 08:05:11 +02:00
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 ;
}
2017-08-03 10:58:05 +02:00
2020-04-08 16:47:36 +02:00
Plane XRAnchor3D : : get_plane ( ) const {
2020-12-08 18:35:30 +01:00
Vector3 location = get_position ( ) ;
2017-09-29 13:36:27 +02:00
Basis orientation = get_transform ( ) . basis ;
2022-05-03 14:50:35 +02:00
Plane plane ( orientation . get_column ( 1 ) . normalized ( ) , location ) ;
2017-09-29 13:36:27 +02:00
return plane ;
2019-02-05 11:02:13 +01:00
}
2017-08-03 10:58:05 +02:00
////////////////////////////////////////////////////////////////////////////////////////////////////
2022-07-29 06:18:05 +02:00
Vector < XROrigin3D * > XROrigin3D : : origin_nodes ;
2022-09-19 17:43:15 +02:00
PackedStringArray XROrigin3D : : get_configuration_warnings ( ) const {
PackedStringArray warnings = Node : : get_configuration_warnings ( ) ;
2020-05-14 22:59:27 +02:00
2020-10-29 11:01:28 +01:00
if ( is_visible ( ) & & is_inside_tree ( ) ) {
2022-07-29 06:18:05 +02:00
bool has_camera = false ;
for ( int i = 0 ; ! has_camera & & i < get_child_count ( ) ; i + + ) {
XRCamera3D * camera = Object : : cast_to < XRCamera3D > ( get_child ( i ) ) ;
if ( camera ) {
// found it!
has_camera = true ;
}
}
if ( ! has_camera ) {
2022-03-28 15:24:14 +02:00
warnings . push_back ( RTR ( " XROrigin3D requires an XRCamera3D child node. " ) ) ;
2020-05-14 22:59:27 +02:00
}
2020-05-14 16:41:43 +02:00
}
2017-04-23 14:10:41 +02:00
2021-12-14 02:44:12 +01:00
bool xr_enabled = GLOBAL_GET ( " xr/shaders/enabled " ) ;
2021-06-14 03:05:16 +02:00
if ( ! xr_enabled ) {
2022-03-28 15:24:14 +02:00
warnings . push_back ( RTR ( " XR is not enabled in rendering project settings. Stereoscopic output is not supported unless this is enabled. " ) ) ;
2021-05-07 15:19:04 +02:00
}
2020-10-29 11:01:28 +01:00
return warnings ;
2021-08-29 08:05:11 +02:00
}
2017-04-23 14:10:41 +02:00
2020-04-08 16:47:36 +02:00
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 ) ;
Variant: Added 64-bit packed arrays, renamed Variant::REAL to FLOAT.
- Renames PackedIntArray to PackedInt32Array.
- Renames PackedFloatArray to PackedFloat32Array.
- Adds PackedInt64Array and PackedFloat64Array.
- Renames Variant::REAL to Variant::FLOAT for consistency.
Packed arrays are for storing large amount of data and creating stuff like
meshes, buffers. textures, etc. Forcing them to be 64 is a huge waste of
memory. That said, many users requested the ability to have 64 bits packed
arrays for their games, so this is just an optional added type.
For Variant, the float datatype is always 64 bits, and exposed as `float`.
We still have `real_t` which is the datatype that can change from 32 to 64
bits depending on a compile flag (not entirely working right now, but that's
the idea). It affects math related datatypes and code only.
Neither Variant nor PackedArray make use of real_t, which is only intended
for math precision, so the term is removed from there to keep only float.
2020-02-24 19:20:53 +01:00
ADD_PROPERTY ( PropertyInfo ( Variant : : FLOAT , " world_scale " ) , " set_world_scale " , " get_world_scale " ) ;
2017-04-23 14:10:41 +02:00
2022-07-29 06:18:05 +02:00
ClassDB : : bind_method ( D_METHOD ( " set_current " , " enabled " ) , & XROrigin3D : : set_current ) ;
ClassDB : : bind_method ( D_METHOD ( " is_current " ) , & XROrigin3D : : is_current ) ;
ADD_PROPERTY ( PropertyInfo ( Variant : : BOOL , " current " ) , " set_current " , " is_current " ) ;
2021-08-29 08:05:11 +02:00
}
2017-04-23 14:10:41 +02:00
2021-01-30 01:55:54 +01:00
real_t XROrigin3D : : get_world_scale ( ) const {
2020-04-08 16:47:36 +02:00
// get our XRServer
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL_V ( xr_server , 1.0 ) ;
2017-04-23 14:10:41 +02:00
2020-04-08 16:47:36 +02:00
return xr_server - > get_world_scale ( ) ;
2021-08-29 08:05:11 +02:00
}
2017-04-23 14:10:41 +02:00
2021-01-30 01:55:54 +01:00
void XROrigin3D : : set_world_scale ( real_t p_world_scale ) {
2020-04-08 16:47:36 +02:00
// get our XRServer
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL ( xr_server ) ;
2017-04-23 14:10:41 +02:00
2020-04-08 16:47:36 +02:00
xr_server - > set_world_scale ( p_world_scale ) ;
2021-08-29 08:05:11 +02:00
}
2017-04-23 14:10:41 +02:00
2022-12-03 11:42:05 +01:00
void XROrigin3D : : _set_current ( bool p_enabled , bool p_update_others ) {
// We run this logic even if current already equals p_enabled as we may have set this previously before we entered our tree.
// This is then called a second time on NOTIFICATION_ENTER_TREE where we actually process activating this origin node.
2022-07-29 06:18:05 +02:00
current = p_enabled ;
if ( ! is_inside_tree ( ) | | Engine : : get_singleton ( ) - > is_editor_hint ( ) ) {
return ;
}
// Notify us of any transform changes
set_notify_local_transform ( current ) ;
set_notify_transform ( current ) ;
2022-12-03 11:42:05 +01:00
// update XRServer with our current position
2022-07-29 06:18:05 +02:00
if ( current ) {
2022-11-06 12:16:25 +01:00
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL ( xr_server ) ;
xr_server - > set_world_origin ( get_global_transform ( ) ) ;
2022-12-03 11:42:05 +01:00
}
// Check if we need to update our other origin nodes accordingly
if ( p_update_others ) {
if ( current ) {
for ( int i = 0 ; i < origin_nodes . size ( ) ; i + + ) {
if ( origin_nodes [ i ] ! = this & & origin_nodes [ i ] - > current ) {
origin_nodes [ i ] - > _set_current ( false , false ) ;
}
}
} else {
// We no longer have a current origin so find the first one we can make current
for ( int i = 0 ; i < origin_nodes . size ( ) ; i + + ) {
if ( origin_nodes [ i ] ! = this ) {
origin_nodes [ i ] - > _set_current ( true , false ) ;
return ; // we are done.
}
2022-07-29 06:18:05 +02:00
}
}
}
}
2022-12-03 11:42:05 +01:00
void XROrigin3D : : set_current ( bool p_enabled ) {
_set_current ( p_enabled , true ) ;
}
2022-07-29 06:18:05 +02:00
bool XROrigin3D : : is_current ( ) const {
if ( Engine : : get_singleton ( ) - > is_editor_hint ( ) ) {
// return as is
return current ;
} else {
return current & & is_inside_tree ( ) ;
}
}
2020-04-08 16:47:36 +02:00
void XROrigin3D : : _notification ( int p_what ) {
// get our XRServer
XRServer * xr_server = XRServer : : get_singleton ( ) ;
ERR_FAIL_NULL ( xr_server ) ;
2019-03-13 10:43:21 +01:00
2017-04-23 14:10:41 +02:00
switch ( p_what ) {
case NOTIFICATION_ENTER_TREE : {
2022-07-29 06:18:05 +02:00
if ( ! Engine : : get_singleton ( ) - > is_editor_hint ( ) ) {
if ( origin_nodes . is_empty ( ) ) {
// first entry always becomes current
current = true ;
}
origin_nodes . push_back ( this ) ;
if ( current ) {
// set this again so we do whatever setup is needed.
set_current ( true ) ;
}
}
2022-02-15 18:06:48 +01:00
} break ;
2017-04-23 14:10:41 +02:00
case NOTIFICATION_EXIT_TREE : {
2022-07-29 06:18:05 +02:00
if ( ! Engine : : get_singleton ( ) - > is_editor_hint ( ) ) {
origin_nodes . erase ( this ) ;
2017-04-23 14:10:41 +02:00
2022-07-29 06:18:05 +02:00
if ( current ) {
// We are no longer current
set_current ( false ) ;
}
}
} break ;
2017-04-23 14:10:41 +02:00
2022-07-29 06:18:05 +02:00
case NOTIFICATION_LOCAL_TRANSFORM_CHANGED :
case NOTIFICATION_TRANSFORM_CHANGED : {
if ( current & & ! Engine : : get_singleton ( ) - > is_editor_hint ( ) ) {
xr_server - > set_world_origin ( get_global_transform ( ) ) ;
2022-02-15 18:06:48 +01:00
}
} break ;
}
2019-03-13 10:43:21 +01:00
2022-07-29 06:18:05 +02:00
if ( current ) {
// 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 < XRInterface > interface = xr_server - > get_interface ( i ) ;
if ( interface . is_valid ( ) & & interface - > is_initialized ( ) ) {
interface - > notification ( p_what ) ;
}
2019-03-13 10:43:21 +01:00
}
}
2021-08-29 08:05:11 +02:00
}