diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index 7aad56f89c8..fcabcf14600 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -480,6 +480,15 @@
The 3D editor gizmo color used for [Skeleton3D] nodes.
+
+ The 3D editor gizmo color used for [SpringBoneCollision3D] nodes.
+
+
+ The 3D editor gizmo color used for [SpringBoneCollision3D] nodes with inside mode.
+
+
+ The 3D editor gizmo color used for [SpringBoneSimulator3D] nodes.
+
The 3D editor gizmo color used for [AudioStreamPlayer3D]'s emission angle.
diff --git a/doc/classes/SpringBoneCollision3D.xml b/doc/classes/SpringBoneCollision3D.xml
new file mode 100644
index 00000000000..ea4556674cf
--- /dev/null
+++ b/doc/classes/SpringBoneCollision3D.xml
@@ -0,0 +1,35 @@
+
+
+
+ A base class of the collision that interacts with [SpringBoneSimulator3D].
+
+
+ A collision can be a child of [SpringBoneSimulator3D]. If it is not a child of [SpringBoneSimulator3D], it has no effect.
+ The colliding and sliding are done in the [SpringBoneSimulator3D]'s modification process in order of its collision list which is set by [method SpringBoneSimulator3D.set_collision_path]. If [method SpringBoneSimulator3D.are_all_child_collisions_enabled] is [code]true[/code], the order matches [SceneTree].
+ If [member bone] is set, it synchronizes with the bone pose of the ancestor [Skeleton3D], which is done in before the [SpringBoneSimulator3D]'s modification process as the pre-process.
+
+
+
+
+
+
+
+ Get parent [Skeleton3D] node of the parent [SpringBoneSimulator3D] if found.
+
+
+
+
+
+ The index of the attached bone.
+
+
+ The name of the attached bone.
+
+
+ The offset of the position from [Skeleton3D]'s [member bone] pose position.
+
+
+ The offset of the rotation from [Skeleton3D]'s [member bone] pose rotation.
+
+
+
diff --git a/doc/classes/SpringBoneCollisionCapsule3D.xml b/doc/classes/SpringBoneCollisionCapsule3D.xml
new file mode 100644
index 00000000000..9af9aca331a
--- /dev/null
+++ b/doc/classes/SpringBoneCollisionCapsule3D.xml
@@ -0,0 +1,22 @@
+
+
+
+ A capsule shape collision that interacts with [SpringBoneSimulator3D].
+
+
+ A capsule shape collision that interacts with [SpringBoneSimulator3D].
+
+
+
+
+
+ The capsule's height.
+
+
+ If [code]true[/code], the collision acts to trap the joint within the collision.
+
+
+ The capsule's radius.
+
+
+
diff --git a/doc/classes/SpringBoneCollisionPlane3D.xml b/doc/classes/SpringBoneCollisionPlane3D.xml
new file mode 100644
index 00000000000..d9f7199f5d1
--- /dev/null
+++ b/doc/classes/SpringBoneCollisionPlane3D.xml
@@ -0,0 +1,11 @@
+
+
+
+ A infinite plane collision that interacts with [SpringBoneSimulator3D].
+
+
+ A infinite plane collision that interacts with [SpringBoneSimulator3D]. It is an infinite size XZ plane, and the +Y direction is treated as normal.
+
+
+
+
diff --git a/doc/classes/SpringBoneCollisionSphere3D.xml b/doc/classes/SpringBoneCollisionSphere3D.xml
new file mode 100644
index 00000000000..3ccb42a93e9
--- /dev/null
+++ b/doc/classes/SpringBoneCollisionSphere3D.xml
@@ -0,0 +1,19 @@
+
+
+
+ A sphere shape collision that interacts with [SpringBoneSimulator3D].
+
+
+ A sphere shape collision that interacts with [SpringBoneSimulator3D].
+
+
+
+
+
+ If [code]true[/code], the collision acts to trap the joint within the collision.
+
+
+ The sphere's radius.
+
+
+
diff --git a/doc/classes/SpringBoneSimulator3D.xml b/doc/classes/SpringBoneSimulator3D.xml
new file mode 100644
index 00000000000..6c639105d9e
--- /dev/null
+++ b/doc/classes/SpringBoneSimulator3D.xml
@@ -0,0 +1,658 @@
+
+
+
+ A [SkeletonModifier3D] to apply inertial wavering to bone chains.
+
+
+ This [SkeletonModifier3D] can be used to wiggle hair, cloth, and tails. This modifier behaves differently from [PhysicalBoneSimulator3D] as it attempts to return the original pose after modification.
+ If you setup [method set_root_bone] and [method set_end_bone], it is treated as one bone chain. Note that it does not support a branched chain like Y-shaped chains.
+ When a bone chain is created, an array is generated from the bones that exist in between and listed in the joint list.
+ Several properties can be applied to each joint, such as [method set_joint_stiffness], [method set_joint_drag], and [method set_joint_gravity].
+ For simplicity, you can set values to all joints at the same time by using a [Curve]. If you want to specify detailed values individually, set [method set_individual_config] to [code]true[/code].
+ For physical simulation, [SpringBoneSimulator3D] can have children as self-standing collisions that are not related to [PhysicsServer3D], see also [SpringBoneCollision3D].
+
+
+
+
+
+
+
+
+ Returns [code]true[/code] if the all child [SpringBoneCollision3D]s are contained in the collision list at [param index] in the settings.
+
+
+
+
+
+
+ Clears all collisions from the collision list at [param index] in the settings when [method are_all_child_collisions_enabled] is [code]false[/code].
+
+
+
+
+
+
+ Clears all exclude collisions from the collision list at [param index] in the settings when [method are_all_child_collisions_enabled] is [code]true[/code].
+
+
+
+
+
+ Clears all settings.
+
+
+
+
+
+
+ Returns the center bone index of the bone chain.
+
+
+
+
+
+
+ Returns the center bone name of the bone chain.
+
+
+
+
+
+
+ Returns what the center originates from in the bone chain.
+
+
+
+
+
+
+ Returns the center node path of the bone chain.
+
+
+
+
+
+
+ Returns the collision count of the bone chain's collision list when [method are_all_child_collisions_enabled] is [code]false[/code].
+
+
+
+
+
+
+
+ Returns the node path of the [SpringBoneCollision3D] at [param collision] in the bone chain's collision list when [method are_all_child_collisions_enabled] is [code]false[/code].
+
+
+
+
+
+
+ Returns the drag force damping curve of the bone chain.
+
+
+
+
+
+
+ Returns the drag force damping curve of the bone chain.
+
+
+
+
+
+
+ Returns the end bone index of the bone chain.
+
+
+
+
+
+
+ Returns the end bone's tail direction of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+
+
+
+
+
+
+ Returns the end bone's tail length of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+
+
+
+
+
+
+ Returns the end bone name of the bone chain.
+
+
+
+
+
+
+ Returns the end bone tip radius of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+
+
+
+
+
+
+ Returns the exclude collision count of the bone chain's exclude collision list when [method are_all_child_collisions_enabled] is [code]true[/code].
+
+
+
+
+
+
+
+ Returns the node path of the [SpringBoneCollision3D] at [param collision] in the bone chain's exclude collision list when [method are_all_child_collisions_enabled] is [code]true[/code].
+
+
+
+
+
+
+ Returns the gravity amount of the bone chain.
+
+
+
+
+
+
+ Returns the gravity amount damping curve of the bone chain.
+
+
+
+
+
+
+ Returns the gravity direction of the bone chain.
+
+
+
+
+
+
+
+ Returns the bone index at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+
+ Returns the bone name at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+ Returns the joint count of the bone chain's joint list.
+
+
+
+
+
+
+
+ Returns the drag force at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+
+ Returns the gravity amount at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+
+ Returns the gravity direction at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+
+ Returns the radius at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+
+ Returns the rotation axis at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+
+ Returns the stiffness force at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+ Returns the joint radius of the bone chain.
+
+
+
+
+
+
+ Returns the joint radius damping curve of the bone chain.
+
+
+
+
+
+
+ Returns the root bone index of the bone chain.
+
+
+
+
+
+
+ Returns the root bone name of the bone chain.
+
+
+
+
+
+
+ Returns the rotation axis of the bone chain.
+
+
+
+
+
+
+ Returns the stiffness force of the bone chain.
+
+
+
+
+
+
+ Returns the stiffness force damping curve of the bone chain.
+
+
+
+
+
+
+ Returns [code]true[/code] if the config can be edited individually for each joint.
+
+
+
+
+
+
+ Returns [code]true[/code] if the end bone is extended to have the tail.
+
+
+
+
+
+ Resets a simulating state with respect to the current bone pose.
+ It is useful to prevent the simulation result getting violent. For example, calling this immediately after a call to [method AnimationPlayer.play] without a fading, or within the previous [signal SkeletonModifier3D.modification_processed] signal if it's condition changes significantly.
+
+
+
+
+
+
+
+ Sets the center bone index of the bone chain.
+
+
+
+
+
+
+
+ Sets the center bone name of the bone chain.
+
+
+
+
+
+
+
+ Sets what the center originates from in the bone chain.
+ Bone movement is calculated based on the difference in relative distance between center and bone in the previous and next frames.
+ For example, if the parent [Skeleton3D] is used as the center, the bones are considered to have not moved if the [Skeleton3D] moves in the world.
+ In this case, only a change in the bone pose is considered to be a bone movement.
+
+
+
+
+
+
+
+ Sets the center node path of the bone chain.
+
+
+
+
+
+
+
+ Sets the number of collisions in the collision list at [param index] in the settings when [method are_all_child_collisions_enabled] is [code]false[/code].
+
+
+
+
+
+
+
+
+ Sets the node path of the [SpringBoneCollision3D] at [param collision] in the bone chain's collision list when [method are_all_child_collisions_enabled] is [code]false[/code].
+
+
+
+
+
+
+
+ Sets the drag force of the bone chain. The greater the value, the more suppressed the wiggling.
+ The value is scaled by [method set_drag_damping_curve] and cached in each joint setting in the joint list.
+
+
+
+
+
+
+
+ Sets the drag force damping curve of the bone chain.
+
+
+
+
+
+
+
+ If sets [param enabled] to [code]true[/code], the all child [SpringBoneCollision3D]s are collided and [method set_exclude_collision_path] is enabled as an exclusion list at [param index] in the settings.
+ If sets [param enabled] to [code]false[/code], you need to manually register all valid collisions with [method set_collision_path].
+
+
+
+
+
+
+
+ Sets the end bone index of the bone chain.
+
+
+
+
+
+
+
+ Sets the end bone tail direction of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+
+
+
+
+
+
+
+ Sets the end bone tail length of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+
+
+
+
+
+
+
+ Sets the end bone name of the bone chain.
+ [b]Note:[/b] End bone must be the root bone or a child of the root bone. If they are the same, the tail must be extended by [method set_extend_end_bone] to jiggle the bone.
+
+
+
+
+
+
+
+ Sets the end bone tip radius of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+
+
+
+
+
+
+
+ Sets the number of exclude collisions in the exclude collision list at [param index] in the settings when [method are_all_child_collisions_enabled] is [code]true[/code].
+
+
+
+
+
+
+
+
+ Sets the node path of the [SpringBoneCollision3D] at [param collision] in the bone chain's exclude collision list when [method are_all_child_collisions_enabled] is [code]true[/code].
+
+
+
+
+
+
+
+ If [param enabled] is [code]true[/code], the end bone is extended to have the tail.
+
+
+
+
+
+
+
+ Sets the gravity amount of the bone chain.
+ If [param gravity] is not [code]0[/code], the modified pose will not return to the original pose since it is always affected by gravity.
+ The value is scaled by [method set_gravity_damping_curve] and cached in each joint setting in the joint list.
+
+
+
+
+
+
+
+ Sets the gravity amount damping curve of the bone chain.
+
+
+
+
+
+
+
+ Sets the gravity direction of the bone chain.
+ The value is cached in each joint setting in the joint list.
+
+
+
+
+
+
+
+ If [param enabled] is [code]true[/code], the config can be edited individually for each joint.
+
+
+
+
+
+
+
+
+ Sets the drag force at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+
+
+
+
+
+
+
+
+ Sets the gravity amount at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+
+
+
+
+
+
+
+
+ Sets the gravity direction at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+
+
+
+
+
+
+
+
+ Sets the joint radius at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+
+
+
+
+
+
+
+
+ Sets the rotation axis at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+
+
+
+
+
+
+
+
+ Sets the stiffness force at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+
+
+
+
+
+
+
+ Sets the joint radius of the bone chain. It is used to move and slide with the [SpringBoneCollision3D] in the collision list.
+ The value is scaled by [method set_radius_damping_curve] and cached in each joint setting in the joint list.
+
+
+
+
+
+
+
+ Sets the joint radius damping curve of the bone chain.
+
+
+
+
+
+
+
+ Sets the root bone index of the bone chain.
+
+
+
+
+
+
+
+ Sets the root bone name of the bone chain.
+
+
+
+
+
+
+
+ Sets the rotation axis of the bone chain. If sets a specific axis, it acts like a hinge joint.
+ The value is cached in each joint setting in the joint list.
+
+
+
+
+
+
+
+ Sets the stiffness force of the bone chain. The greater the value, the faster it recovers to its initial pose.
+ If [param stiffness] is [code]0[/code], the modified pose will not return to the original pose.
+ The value is scaled by [method set_stiffness_damping_curve] and cached in each joint setting in the joint list.
+
+
+
+
+
+
+
+ Sets the stiffness force damping curve of the bone chain.
+
+
+
+
+
+ The number of settings.
+
+
+
+
+ Enumerated value for the +X axis.
+
+
+ Enumerated value for the -X axis.
+
+
+ Enumerated value for the +Y axis.
+
+
+ Enumerated value for the -Y axis.
+
+
+ Enumerated value for the +Z axis.
+
+
+ Enumerated value for the -Z axis.
+
+
+ Enumerated value for the axis from a parent bone to the child bone.
+
+
+ The world origin is defined as center.
+
+
+ The [Node3D] specified by [method set_center_node] is defined as center.
+ If [Node3D] is not found, the parent [Skeleton3D] is treated as center.
+
+
+ The bone pose origin of the parent [Skeleton3D] specified by [method set_center_bone] is defined as center.
+ If [Node3D] is not found, the parent [Skeleton3D] is treated as center.
+
+
+ Enumerated value for the rotation of the X axis.
+
+
+ Enumerated value for the rotation of the Y axis.
+
+
+ Enumerated value for the rotation of the Z axis.
+
+
+ Enumerated value for the unconstrained rotation.
+
+
+
diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp
index 41992965131..2a8ad890b61 100644
--- a/editor/editor_inspector.cpp
+++ b/editor/editor_inspector.cpp
@@ -2365,6 +2365,7 @@ void EditorInspectorArray::_setup() {
Ref numbers_font;
int numbers_min_w = 0;
+ bool unresizable = is_const || read_only;
if (numbered) {
numbers_font = get_theme_font(SNAME("bold"), EditorStringName(EditorFonts));
@@ -2461,15 +2462,17 @@ void EditorInspectorArray::_setup() {
ae.vbox->set_v_size_flags(SIZE_EXPAND_FILL);
ae.hbox->add_child(ae.vbox);
- ae.erase = memnew(Button);
- ae.erase->set_button_icon(get_editor_theme_icon(SNAME("Remove")));
- ae.erase->set_v_size_flags(SIZE_SHRINK_CENTER);
- ae.erase->connect(SceneStringName(pressed), callable_mp(this, &EditorInspectorArray::_remove_item).bind(element_position));
- ae.hbox->add_child(ae.erase);
+ if (!unresizable) {
+ ae.erase = memnew(Button);
+ ae.erase->set_button_icon(get_editor_theme_icon(SNAME("Remove")));
+ ae.erase->set_v_size_flags(SIZE_SHRINK_CENTER);
+ ae.erase->connect(SceneStringName(pressed), callable_mp(this, &EditorInspectorArray::_remove_item).bind(element_position));
+ ae.hbox->add_child(ae.erase);
+ }
}
// Hide/show the add button.
- add_button->set_visible(page == max_page);
+ add_button->set_visible(page == max_page && !unresizable);
// Add paginator if there's more than 1 page.
if (max_page > 0) {
@@ -2587,12 +2590,13 @@ void EditorInspectorArray::_bind_methods() {
ADD_SIGNAL(MethodInfo("page_change_request"));
}
-void EditorInspectorArray::setup_with_move_element_function(Object *p_object, const String &p_label, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable, bool p_numbered, int p_page_length, const String &p_add_item_text) {
+void EditorInspectorArray::setup_with_move_element_function(Object *p_object, const String &p_label, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable, bool p_is_const, bool p_numbered, int p_page_length, const String &p_add_item_text) {
count_property = "";
mode = MODE_USE_MOVE_ARRAY_ELEMENT_FUNCTION;
array_element_prefix = p_array_element_prefix;
page = p_page;
movable = p_movable;
+ is_const = p_is_const;
page_length = p_page_length;
numbered = p_numbered;
@@ -2601,12 +2605,13 @@ void EditorInspectorArray::setup_with_move_element_function(Object *p_object, co
_setup();
}
-void EditorInspectorArray::setup_with_count_property(Object *p_object, const String &p_label, const StringName &p_count_property, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable, bool p_numbered, int p_page_length, const String &p_add_item_text, const String &p_swap_method) {
+void EditorInspectorArray::setup_with_count_property(Object *p_object, const String &p_label, const StringName &p_count_property, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable, bool p_is_const, bool p_numbered, int p_page_length, const String &p_add_item_text, const String &p_swap_method) {
count_property = p_count_property;
mode = MODE_USE_COUNT_PROPERTY;
array_element_prefix = p_array_element_prefix;
page = p_page;
movable = p_movable;
+ is_const = p_is_const;
page_length = p_page_length;
numbered = p_numbered;
swap_method = p_swap_method;
@@ -3442,6 +3447,7 @@ void EditorInspector::update_tree() {
int page_size = 5;
bool movable = true;
+ bool is_const = false;
bool numbered = false;
bool foldable = use_folding;
String add_button_text = TTR("Add Element");
@@ -3453,6 +3459,8 @@ void EditorInspector::update_tree() {
add_button_text = class_name_components[i].get_slice("=", 1).strip_edges();
} else if (class_name_components[i] == "static") {
movable = false;
+ } else if (class_name_components[i] == "const") {
+ is_const = true;
} else if (class_name_components[i] == "numbered") {
numbered = true;
} else if (class_name_components[i] == "unfoldable") {
@@ -3479,7 +3487,7 @@ void EditorInspector::update_tree() {
editor_inspector_array = memnew(EditorInspectorArray(all_read_only));
int page = per_array_page.has(array_element_prefix) ? per_array_page[array_element_prefix] : 0;
- editor_inspector_array->setup_with_count_property(object, class_name_components[0], p.name, array_element_prefix, page, c, foldable, movable, numbered, page_size, add_button_text, swap_method);
+ editor_inspector_array->setup_with_count_property(object, class_name_components[0], p.name, array_element_prefix, page, c, foldable, movable, is_const, numbered, page_size, add_button_text, swap_method);
editor_inspector_array->connect("page_change_request", callable_mp(this, &EditorInspector::_page_change_request).bind(array_element_prefix));
}
}
diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h
index 8279c354a46..d703510f71d 100644
--- a/editor/editor_inspector.h
+++ b/editor/editor_inspector.h
@@ -391,6 +391,7 @@ class EditorInspectorArray : public EditorInspectorSection {
bool read_only = false;
bool movable = true;
+ bool is_const = false;
bool numbered = false;
enum MenuOptions {
@@ -457,8 +458,8 @@ protected:
static void _bind_methods();
public:
- void setup_with_move_element_function(Object *p_object, const String &p_label, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable = true, bool p_numbered = false, int p_page_length = 5, const String &p_add_item_text = "");
- void setup_with_count_property(Object *p_object, const String &p_label, const StringName &p_count_property, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable = true, bool p_numbered = false, int p_page_length = 5, const String &p_add_item_text = "", const String &p_swap_method = "");
+ void setup_with_move_element_function(Object *p_object, const String &p_label, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable = true, bool p_is_const = false, bool p_numbered = false, int p_page_length = 5, const String &p_add_item_text = "");
+ void setup_with_count_property(Object *p_object, const String &p_label, const StringName &p_count_property, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable = true, bool p_is_const = false, bool p_numbered = false, int p_page_length = 5, const String &p_add_item_text = "", const String &p_swap_method = "");
VBoxContainer *get_vbox(int p_index);
EditorInspectorArray(bool p_read_only);
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index 79454eab6f0..dabd437f5e5 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -795,6 +795,9 @@ void EditorSettings::_load_defaults(Ref p_extra_config) {
EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/selected_bone", Color(0.8, 0.3, 0.0), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/csg", Color(0.0, 0.4, 1, 0.15), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
EDITOR_SETTING(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/gridmap_grid", Color(0.8, 0.5, 0.1), "")
+ EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/spring_bone_joint", Color(0.8, 0.9, 0.6), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
+ EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/spring_bone_collision", Color(0.6, 0.8, 0.9), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
+ EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/spring_bone_inside_collision", Color(0.9, 0.6, 0.8), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
_initial_set("editors/3d_gizmos/gizmo_settings/bone_axis_length", (float)0.1);
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d_gizmos/gizmo_settings/bone_shape", 1, "Wire,Octahedron");
EDITOR_SETTING_USAGE(Variant::FLOAT, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_settings/path3d_tilt_disk_size", 0.8, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED)
diff --git a/editor/icons/SpringBoneCollision3D.svg b/editor/icons/SpringBoneCollision3D.svg
new file mode 100644
index 00000000000..564c44a4118
--- /dev/null
+++ b/editor/icons/SpringBoneCollision3D.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/SpringBoneCollisionCapsule3D.svg b/editor/icons/SpringBoneCollisionCapsule3D.svg
new file mode 100644
index 00000000000..3c433562d8a
--- /dev/null
+++ b/editor/icons/SpringBoneCollisionCapsule3D.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/SpringBoneCollisionPlane3D.svg b/editor/icons/SpringBoneCollisionPlane3D.svg
new file mode 100644
index 00000000000..7ccb674ab1a
--- /dev/null
+++ b/editor/icons/SpringBoneCollisionPlane3D.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/SpringBoneCollisionSphere3D.svg b/editor/icons/SpringBoneCollisionSphere3D.svg
new file mode 100644
index 00000000000..7d39ee952f7
--- /dev/null
+++ b/editor/icons/SpringBoneCollisionSphere3D.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/SpringBoneSimulator3D.svg b/editor/icons/SpringBoneSimulator3D.svg
new file mode 100644
index 00000000000..eeb37314c39
--- /dev/null
+++ b/editor/icons/SpringBoneSimulator3D.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/plugins/gizmos/spring_bone_3d_gizmo_plugin.cpp b/editor/plugins/gizmos/spring_bone_3d_gizmo_plugin.cpp
new file mode 100644
index 00000000000..1cb943b0412
--- /dev/null
+++ b/editor/plugins/gizmos/spring_bone_3d_gizmo_plugin.cpp
@@ -0,0 +1,424 @@
+/**************************************************************************/
+/* spring_bone_3d_gizmo_plugin.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 "spring_bone_3d_gizmo_plugin.h"
+
+#include "editor/editor_settings.h"
+#include "scene/3d/spring_bone_collision_capsule_3d.h"
+#include "scene/3d/spring_bone_collision_plane_3d.h"
+#include "scene/3d/spring_bone_collision_sphere_3d.h"
+
+// SpringBoneSimulator3D
+
+SpringBoneSimulator3DGizmoPlugin::SelectionMaterials SpringBoneSimulator3DGizmoPlugin::selection_materials;
+
+SpringBoneSimulator3DGizmoPlugin::SpringBoneSimulator3DGizmoPlugin() {
+ selection_materials.unselected_mat.instantiate();
+ selection_materials.unselected_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+ selection_materials.unselected_mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
+ selection_materials.unselected_mat->set_flag(StandardMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true);
+ selection_materials.unselected_mat->set_flag(StandardMaterial3D::FLAG_SRGB_VERTEX_COLOR, true);
+ selection_materials.unselected_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
+
+ selection_materials.selected_mat.instantiate();
+ Ref sh;
+ sh.instantiate();
+ sh->set_code(R"(
+// Skeleton 3D gizmo bones shader.
+
+shader_type spatial;
+render_mode unshaded, shadows_disabled;
+void vertex() {
+ if (!OUTPUT_IS_SRGB) {
+ COLOR.rgb = mix( pow((COLOR.rgb + vec3(0.055)) * (1.0 / (1.0 + 0.055)), vec3(2.4)), COLOR.rgb* (1.0 / 12.92), lessThan(COLOR.rgb,vec3(0.04045)) );
+ }
+ VERTEX = VERTEX;
+ POSITION = PROJECTION_MATRIX * VIEW_MATRIX * MODEL_MATRIX * vec4(VERTEX.xyz, 1.0);
+ POSITION.z = mix(POSITION.z, POSITION.w, 0.998);
+}
+void fragment() {
+ ALBEDO = COLOR.rgb;
+ ALPHA = COLOR.a;
+}
+)");
+ selection_materials.selected_mat->set_shader(sh);
+}
+
+SpringBoneSimulator3DGizmoPlugin::~SpringBoneSimulator3DGizmoPlugin() {
+ selection_materials.unselected_mat.unref();
+ selection_materials.selected_mat.unref();
+}
+
+bool SpringBoneSimulator3DGizmoPlugin::has_gizmo(Node3D *p_spatial) {
+ return Object::cast_to(p_spatial) != nullptr;
+}
+
+String SpringBoneSimulator3DGizmoPlugin::get_gizmo_name() const {
+ return "SpringBoneSimulator3D";
+}
+
+int SpringBoneSimulator3DGizmoPlugin::get_priority() const {
+ return -1;
+}
+
+void SpringBoneSimulator3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) {
+ SpringBoneSimulator3D *simulator = Object::cast_to(p_gizmo->get_node_3d());
+ p_gizmo->clear();
+
+ if (!simulator->get_setting_count()) {
+ return;
+ }
+
+ Skeleton3D *skeleton = simulator->get_skeleton();
+ if (!skeleton) {
+ return;
+ }
+
+ Ref mesh = get_joints_mesh(skeleton, simulator, p_gizmo->is_selected());
+ Transform3D skel_tr = simulator->get_global_transform().inverse() * skeleton->get_global_transform();
+ p_gizmo->add_mesh(mesh, Ref(), skel_tr, skeleton->register_skin(skeleton->create_skin_from_rest_transforms()));
+}
+
+Ref SpringBoneSimulator3DGizmoPlugin::get_joints_mesh(Skeleton3D *p_skeleton, SpringBoneSimulator3D *p_simulator, bool p_is_selected) {
+ Color bone_color = EDITOR_GET("editors/3d_gizmos/gizmo_colors/spring_bone_joint");
+
+ Ref surface_tool;
+ surface_tool.instantiate();
+ surface_tool->begin(Mesh::PRIMITIVE_LINES);
+
+ if (p_is_selected) {
+ surface_tool->set_material(selection_materials.selected_mat);
+ } else {
+ selection_materials.unselected_mat->set_albedo(bone_color);
+ surface_tool->set_material(selection_materials.unselected_mat);
+ }
+
+ LocalVector bones;
+ LocalVector weights;
+ bones.resize(4);
+ weights.resize(4);
+ for (int i = 0; i < 4; i++) {
+ bones[i] = 0;
+ weights[i] = 0;
+ }
+ weights[0] = 1;
+
+ for (int i = 0; i < p_simulator->get_setting_count(); i++) {
+ int current_bone = -1;
+ int prev_bone = -1;
+ int joint_end = p_simulator->get_joint_count(i) - 1;
+ for (int j = 0; j <= joint_end; j++) {
+ current_bone = p_simulator->get_joint_bone(i, j);
+ if (j > 0) {
+ Transform3D parent_global_pose = p_skeleton->get_bone_global_rest(prev_bone);
+ Transform3D global_pose = p_skeleton->get_bone_global_rest(current_bone);
+ draw_line(surface_tool, parent_global_pose.origin, global_pose.origin, bone_color);
+ draw_sphere(surface_tool, global_pose.basis, global_pose.origin, p_simulator->get_joint_radius(i, j), bone_color);
+ }
+ if (j == joint_end && p_simulator->is_end_bone_extended(i) && p_simulator->get_end_bone_length(i) > 0) {
+ Vector3 axis = p_simulator->get_end_bone_axis(current_bone, p_simulator->get_end_bone_direction(i));
+ if (axis.is_zero_approx()) {
+ continue;
+ }
+ bones[0] = current_bone;
+ surface_tool->set_bones(bones);
+ surface_tool->set_weights(weights);
+ Transform3D global_pose = p_skeleton->get_bone_global_rest(current_bone);
+ axis = global_pose.xform(axis * p_simulator->get_end_bone_length(i));
+ draw_line(surface_tool, global_pose.origin, axis, bone_color);
+ draw_sphere(surface_tool, global_pose.basis, axis, p_simulator->get_end_bone_tip_radius(i), bone_color);
+ } else {
+ bones[0] = current_bone;
+ surface_tool->set_bones(bones);
+ surface_tool->set_weights(weights);
+ }
+ prev_bone = current_bone;
+ }
+ }
+
+ return surface_tool->commit();
+}
+
+void SpringBoneSimulator3DGizmoPlugin::draw_sphere(Ref &p_surface_tool, const Basis &p_basis, const Vector3 &p_center, float p_radius, const Color &p_color) {
+ static const Vector3 VECTOR3_RIGHT = Vector3(1, 0, 0);
+ static const Vector3 VECTOR3_UP = Vector3(0, 1, 0);
+ static const Vector3 VECTOR3_FORWARD = Vector3(0, 0, 1);
+ static const int STEP = 16;
+ static const float SPPI = Math_TAU / (float)STEP;
+
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(p_center + ((p_basis.xform(VECTOR3_UP * p_radius)).rotated(p_basis.xform(VECTOR3_RIGHT), SPPI * ((i - 1) % STEP))));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(p_center + ((p_basis.xform(VECTOR3_UP * p_radius)).rotated(p_basis.xform(VECTOR3_RIGHT), SPPI * (i % STEP))));
+ }
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(p_center + ((p_basis.xform(VECTOR3_RIGHT * p_radius)).rotated(p_basis.xform(VECTOR3_FORWARD), SPPI * ((i - 1) % STEP))));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(p_center + ((p_basis.xform(VECTOR3_RIGHT * p_radius)).rotated(p_basis.xform(VECTOR3_FORWARD), SPPI * (i % STEP))));
+ }
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(p_center + ((p_basis.xform(VECTOR3_FORWARD * p_radius)).rotated(p_basis.xform(VECTOR3_UP), SPPI * ((i - 1) % STEP))));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(p_center + ((p_basis.xform(VECTOR3_FORWARD * p_radius)).rotated(p_basis.xform(VECTOR3_UP), SPPI * (i % STEP))));
+ }
+}
+
+void SpringBoneSimulator3DGizmoPlugin::draw_line(Ref &p_surface_tool, const Vector3 &p_begin_pos, const Vector3 &p_end_pos, const Color &p_color) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(p_begin_pos);
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(p_end_pos);
+}
+
+// SpringBoneCollision3D
+
+SpringBoneCollision3DGizmoPlugin::SelectionMaterials SpringBoneCollision3DGizmoPlugin::selection_materials;
+
+SpringBoneCollision3DGizmoPlugin::SpringBoneCollision3DGizmoPlugin() {
+ selection_materials.unselected_mat.instantiate();
+ selection_materials.unselected_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+ selection_materials.unselected_mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
+ selection_materials.unselected_mat->set_flag(StandardMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true);
+ selection_materials.unselected_mat->set_flag(StandardMaterial3D::FLAG_SRGB_VERTEX_COLOR, true);
+ selection_materials.unselected_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
+
+ selection_materials.selected_mat.instantiate();
+ Ref sh;
+ sh.instantiate();
+ sh->set_code(R"(
+// Skeleton 3D gizmo bones shader.
+
+shader_type spatial;
+render_mode unshaded, shadows_disabled;
+void vertex() {
+ if (!OUTPUT_IS_SRGB) {
+ COLOR.rgb = mix( pow((COLOR.rgb + vec3(0.055)) * (1.0 / (1.0 + 0.055)), vec3(2.4)), COLOR.rgb* (1.0 / 12.92), lessThan(COLOR.rgb,vec3(0.04045)) );
+ }
+ VERTEX = VERTEX;
+ POSITION = PROJECTION_MATRIX * VIEW_MATRIX * MODEL_MATRIX * vec4(VERTEX.xyz, 1.0);
+ POSITION.z = mix(POSITION.z, POSITION.w, 0.998);
+}
+void fragment() {
+ ALBEDO = COLOR.rgb;
+ ALPHA = COLOR.a;
+}
+)");
+ selection_materials.selected_mat->set_shader(sh);
+}
+
+SpringBoneCollision3DGizmoPlugin::~SpringBoneCollision3DGizmoPlugin() {
+ selection_materials.unselected_mat.unref();
+ selection_materials.selected_mat.unref();
+}
+
+bool SpringBoneCollision3DGizmoPlugin::has_gizmo(Node3D *p_spatial) {
+ return Object::cast_to(p_spatial) != nullptr;
+}
+
+String SpringBoneCollision3DGizmoPlugin::get_gizmo_name() const {
+ return "SpringBoneCollision3D";
+}
+
+int SpringBoneCollision3DGizmoPlugin::get_priority() const {
+ return -1;
+}
+
+void SpringBoneCollision3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) {
+ SpringBoneCollision3D *collision = Object::cast_to(p_gizmo->get_node_3d());
+ p_gizmo->clear();
+
+ Ref mesh = get_collision_mesh(collision, p_gizmo->is_selected());
+ p_gizmo->add_mesh(mesh);
+}
+
+Ref SpringBoneCollision3DGizmoPlugin::get_collision_mesh(SpringBoneCollision3D *p_collision, bool p_is_selected) {
+ Color collision_color = EDITOR_GET("editors/3d_gizmos/gizmo_colors/spring_bone_collision");
+ Color inside_collision_color = EDITOR_GET("editors/3d_gizmos/gizmo_colors/spring_bone_inside_collision");
+
+ Ref surface_tool;
+ surface_tool.instantiate();
+ surface_tool->begin(Mesh::PRIMITIVE_LINES);
+
+ if (p_is_selected) {
+ surface_tool->set_material(selection_materials.selected_mat);
+ } else {
+ selection_materials.unselected_mat->set_albedo(collision_color);
+ surface_tool->set_material(selection_materials.unselected_mat);
+ }
+
+ SpringBoneCollisionSphere3D *sphere = Object::cast_to(p_collision);
+ if (sphere) {
+ draw_sphere(surface_tool, sphere->get_radius(), sphere->is_inside() ? inside_collision_color : collision_color);
+ return surface_tool->commit();
+ }
+
+ SpringBoneCollisionCapsule3D *capsule = Object::cast_to(p_collision);
+ if (capsule) {
+ draw_capsule(surface_tool, capsule->get_radius(), capsule->get_height(), capsule->is_inside() ? inside_collision_color : collision_color);
+ return surface_tool->commit();
+ }
+
+ SpringBoneCollisionPlane3D *plane = Object::cast_to(p_collision);
+ if (plane) {
+ draw_plane(surface_tool, collision_color);
+ return surface_tool->commit();
+ }
+
+ return surface_tool->commit();
+}
+
+void SpringBoneCollision3DGizmoPlugin::draw_sphere(Ref &p_surface_tool, float p_radius, const Color &p_color) {
+ static const Vector3 VECTOR3_RIGHT = Vector3(1, 0, 0);
+ static const Vector3 VECTOR3_UP = Vector3(0, 1, 0);
+ static const Vector3 VECTOR3_FORWARD = Vector3(0, 0, 1);
+ static const int STEP = 16;
+ static const float SPPI = Math_TAU / (float)STEP;
+
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((VECTOR3_UP * p_radius).rotated(VECTOR3_RIGHT, SPPI * ((i - 1) % STEP)));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((VECTOR3_UP * p_radius).rotated(VECTOR3_RIGHT, SPPI * (i % STEP)));
+ }
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((VECTOR3_RIGHT * p_radius).rotated(VECTOR3_FORWARD, SPPI * ((i - 1) % STEP)));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((VECTOR3_RIGHT * p_radius).rotated(VECTOR3_FORWARD, SPPI * (i % STEP)));
+ }
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((VECTOR3_FORWARD * p_radius).rotated(VECTOR3_UP, SPPI * ((i - 1) % STEP)));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((VECTOR3_FORWARD * p_radius).rotated(VECTOR3_UP, SPPI * (i % STEP)));
+ }
+}
+
+void SpringBoneCollision3DGizmoPlugin::draw_capsule(Ref &p_surface_tool, float p_radius, float p_height, const Color &p_color) {
+ static const Vector3 VECTOR3_RIGHT = Vector3(1, 0, 0);
+ static const Vector3 VECTOR3_UP = Vector3(0, 1, 0);
+ static const Vector3 VECTOR3_FORWARD = Vector3(0, 0, 1);
+ static const int STEP = 16;
+ static const int HALF_STEP = 8;
+ static const float SPPI = Math_TAU / (float)STEP;
+ static const float HALF_PI = Math_PI * 0.5;
+
+ Vector3 top = VECTOR3_UP * (p_height * 0.5 - p_radius);
+ Vector3 bottom = -top;
+
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((i - 1 < HALF_STEP ? top : bottom) + (VECTOR3_UP * p_radius).rotated(VECTOR3_RIGHT, -HALF_PI + SPPI * ((i - 1) % STEP)));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((i - 1 < HALF_STEP ? top : bottom) + (VECTOR3_UP * p_radius).rotated(VECTOR3_RIGHT, -HALF_PI + SPPI * (i % STEP)));
+ }
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((i - 1 < HALF_STEP ? top : bottom) + (VECTOR3_RIGHT * p_radius).rotated(VECTOR3_FORWARD, SPPI * ((i - 1) % STEP)));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex((i - 1 < HALF_STEP ? top : bottom) + (VECTOR3_RIGHT * p_radius).rotated(VECTOR3_FORWARD, SPPI * (i % STEP)));
+ }
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(top + (VECTOR3_FORWARD * p_radius).rotated(VECTOR3_UP, SPPI * ((i - 1) % STEP)));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(top + (VECTOR3_FORWARD * p_radius).rotated(VECTOR3_UP, SPPI * (i % STEP)));
+ }
+ for (int i = 1; i <= STEP; i++) {
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(bottom + (VECTOR3_FORWARD * p_radius).rotated(VECTOR3_UP, SPPI * ((i - 1) % STEP)));
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(bottom + (VECTOR3_FORWARD * p_radius).rotated(VECTOR3_UP, SPPI * (i % STEP)));
+ }
+ LocalVector directions;
+ directions.resize(4);
+ directions[0] = VECTOR3_RIGHT;
+ directions[1] = -VECTOR3_RIGHT;
+ directions[2] = VECTOR3_FORWARD;
+ directions[3] = -VECTOR3_FORWARD;
+ for (int i = 0; i < 4; i++) {
+ Vector3 dir = directions[i] * p_radius;
+ p_surface_tool->set_color(p_color);
+ p_surface_tool->add_vertex(top + dir);
+ p_surface_tool->add_vertex(bottom + dir);
+ }
+}
+
+void SpringBoneCollision3DGizmoPlugin::draw_plane(Ref &p_surface_tool, const Color &p_color) {
+ static const Vector3 VECTOR3_UP = Vector3(0, 1, 0);
+ static const float HALF_PI = Math_PI * 0.5;
+ static const float ARROW_LENGTH = 0.3;
+ static const float ARROW_HALF_WIDTH = 0.05;
+ static const float ARROW_TOP_HALF_WIDTH = 0.1;
+ static const float ARROW_TOP = 0.5;
+ static const float RECT_SIZE = 1.0;
+ static const int RECT_STEP_COUNT = 9;
+ static const float RECT_HALF_SIZE = RECT_SIZE * 0.5;
+ static const float RECT_STEP = RECT_SIZE / (float)RECT_STEP_COUNT;
+
+ p_surface_tool->set_color(p_color);
+
+ // Draw arrow of the normal.
+ LocalVector arrow;
+ arrow.resize(7);
+ arrow[0] = Vector3(0, ARROW_TOP, 0);
+ arrow[1] = Vector3(-ARROW_TOP_HALF_WIDTH, ARROW_LENGTH, 0);
+ arrow[2] = Vector3(-ARROW_HALF_WIDTH, ARROW_LENGTH, 0);
+ arrow[3] = Vector3(-ARROW_HALF_WIDTH, 0, 0);
+ arrow[4] = Vector3(ARROW_HALF_WIDTH, 0, 0);
+ arrow[5] = Vector3(ARROW_HALF_WIDTH, ARROW_LENGTH, 0);
+ arrow[6] = Vector3(ARROW_TOP_HALF_WIDTH, ARROW_LENGTH, 0);
+ for (int i = 0; i < 2; i++) {
+ Basis ma(VECTOR3_UP, HALF_PI * i);
+ for (uint32_t j = 0; j < arrow.size(); j++) {
+ Vector3 v1 = arrow[j];
+ Vector3 v2 = arrow[(j + 1) % arrow.size()];
+ p_surface_tool->add_vertex(ma.xform(v1));
+ p_surface_tool->add_vertex(ma.xform(v2));
+ }
+ }
+
+ // Draw dashed line of the rect.
+ for (int i = 0; i < 4; i++) {
+ Basis ma(VECTOR3_UP, HALF_PI * i);
+ for (int j = 0; j < RECT_STEP_COUNT; j++) {
+ if (j % 2 == 1) {
+ continue;
+ }
+ Vector3 v1 = Vector3(RECT_HALF_SIZE, 0, RECT_HALF_SIZE - RECT_STEP * j);
+ Vector3 v2 = Vector3(RECT_HALF_SIZE, 0, RECT_HALF_SIZE - RECT_STEP * (j + 1));
+ p_surface_tool->add_vertex(ma.xform(v1));
+ p_surface_tool->add_vertex(ma.xform(v2));
+ }
+ }
+}
diff --git a/editor/plugins/gizmos/spring_bone_3d_gizmo_plugin.h b/editor/plugins/gizmos/spring_bone_3d_gizmo_plugin.h
new file mode 100644
index 00000000000..8d6be59b5b0
--- /dev/null
+++ b/editor/plugins/gizmos/spring_bone_3d_gizmo_plugin.h
@@ -0,0 +1,89 @@
+/**************************************************************************/
+/* spring_bone_3d_gizmo_plugin.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. */
+/**************************************************************************/
+
+#ifndef SPRING_BONE_3D_GIZMO_PLUGIN_H
+#define SPRING_BONE_3D_GIZMO_PLUGIN_H
+
+#include "editor/plugins/editor_plugin.h"
+#include "editor/plugins/node_3d_editor_plugin.h"
+#include "scene/3d/spring_bone_collision_3d.h"
+#include "scene/3d/spring_bone_simulator_3d.h"
+#include "scene/resources/surface_tool.h"
+
+class SpringBoneSimulator3DGizmoPlugin : public EditorNode3DGizmoPlugin {
+ GDCLASS(SpringBoneSimulator3DGizmoPlugin, EditorNode3DGizmoPlugin);
+
+ struct SelectionMaterials {
+ Ref unselected_mat;
+ Ref selected_mat;
+ };
+ static SelectionMaterials selection_materials;
+
+public:
+ static Ref get_joints_mesh(Skeleton3D *p_skeleton, SpringBoneSimulator3D *p_simulator, bool p_is_selected);
+ static void draw_sphere(Ref &p_surface_tool, const Basis &p_basis, const Vector3 &p_center, float p_radius, const Color &p_color);
+ static void draw_line(Ref &p_surface_tool, const Vector3 &p_begin_pos, const Vector3 &p_end_pos, const Color &p_color);
+
+ bool has_gizmo(Node3D *p_spatial) override;
+ String get_gizmo_name() const override;
+ int get_priority() const override;
+
+ void redraw(EditorNode3DGizmo *p_gizmo) override;
+
+ SpringBoneSimulator3DGizmoPlugin();
+ ~SpringBoneSimulator3DGizmoPlugin();
+};
+
+class SpringBoneCollision3DGizmoPlugin : public EditorNode3DGizmoPlugin {
+ GDCLASS(SpringBoneCollision3DGizmoPlugin, EditorNode3DGizmoPlugin);
+
+ struct SelectionMaterials {
+ Ref unselected_mat;
+ Ref selected_mat;
+ };
+ static SelectionMaterials selection_materials;
+
+public:
+ static Ref get_collision_mesh(SpringBoneCollision3D *p_collision, bool p_is_selected);
+ static void draw_sphere(Ref &p_surface_tool, float p_radius, const Color &p_color);
+ static void draw_capsule(Ref &p_surface_tool, float p_radius, float p_height, const Color &p_color);
+ static void draw_plane(Ref &p_surface_tool, const Color &p_color);
+
+ bool has_gizmo(Node3D *p_spatial) override;
+ String get_gizmo_name() const override;
+ int get_priority() const override;
+
+ void redraw(EditorNode3DGizmo *p_gizmo) override;
+
+ SpringBoneCollision3DGizmoPlugin();
+ ~SpringBoneCollision3DGizmoPlugin();
+};
+
+#endif // SPRING_BONE_3D_GIZMO_PLUGIN_H
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index b803d2e5d82..28a2bd89c56 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -74,6 +74,7 @@
#include "editor/plugins/gizmos/shape_cast_3d_gizmo_plugin.h"
#include "editor/plugins/gizmos/soft_body_3d_gizmo_plugin.h"
#include "editor/plugins/gizmos/spring_arm_3d_gizmo_plugin.h"
+#include "editor/plugins/gizmos/spring_bone_3d_gizmo_plugin.h"
#include "editor/plugins/gizmos/sprite_base_3d_gizmo_plugin.h"
#include "editor/plugins/gizmos/vehicle_body_3d_gizmo_plugin.h"
#include "editor/plugins/gizmos/visible_on_screen_notifier_3d_gizmo_plugin.h"
@@ -8555,6 +8556,8 @@ void Node3DEditor::_register_all_gizmos() {
add_gizmo_plugin(Ref(memnew(RayCast3DGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(ShapeCast3DGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(SpringArm3DGizmoPlugin)));
+ add_gizmo_plugin(Ref(memnew(SpringBoneCollision3DGizmoPlugin)));
+ add_gizmo_plugin(Ref(memnew(SpringBoneSimulator3DGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(VehicleWheel3DGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(VisibleOnScreenNotifier3DGizmoPlugin)));
add_gizmo_plugin(Ref(memnew(GPUParticles3DGizmoPlugin)));
diff --git a/scene/3d/spring_bone_collision_3d.cpp b/scene/3d/spring_bone_collision_3d.cpp
new file mode 100644
index 00000000000..25ad37fecad
--- /dev/null
+++ b/scene/3d/spring_bone_collision_3d.cpp
@@ -0,0 +1,192 @@
+/**************************************************************************/
+/* spring_bone_collision_3d.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 "spring_bone_collision_3d.h"
+
+#include "scene/3d/spring_bone_simulator_3d.h"
+
+PackedStringArray SpringBoneCollision3D::get_configuration_warnings() const {
+ PackedStringArray warnings = Node3D::get_configuration_warnings();
+
+ SpringBoneSimulator3D *parent = Object::cast_to(get_parent());
+ if (!parent) {
+ warnings.push_back(RTR("Parent node should be a SpringBoneSimulator3D node."));
+ }
+
+ return warnings;
+}
+
+void SpringBoneCollision3D::_validate_property(PropertyInfo &p_property) const {
+ if (p_property.name == "bone_name") {
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ p_property.hint = PROPERTY_HINT_ENUM_SUGGESTION;
+ p_property.hint_string = sk->get_concatenated_bone_names();
+ } else {
+ p_property.hint = PROPERTY_HINT_NONE;
+ p_property.hint_string = "";
+ }
+ } else if (bone < 0 && (p_property.name == "position_offset" || p_property.name == "rotation_offset")) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+}
+
+Skeleton3D *SpringBoneCollision3D::get_skeleton() const {
+ SpringBoneSimulator3D *parent = Object::cast_to(get_parent());
+ if (!parent) {
+ return nullptr;
+ }
+ return parent->get_skeleton();
+}
+
+void SpringBoneCollision3D::set_bone_name(const String &p_name) {
+ bone_name = p_name;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ set_bone(sk->find_bone(bone_name));
+ }
+}
+
+String SpringBoneCollision3D::get_bone_name() const {
+ return bone_name;
+}
+
+void SpringBoneCollision3D::set_bone(int p_bone) {
+ bone = p_bone;
+
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ if (bone <= -1 || bone >= sk->get_bone_count()) {
+ WARN_PRINT("Bone index out of range! Cannot connect BoneAttachment to node!");
+ bone = -1;
+ } else {
+ bone_name = sk->get_bone_name(bone);
+ }
+ }
+
+ notify_property_list_changed();
+}
+
+int SpringBoneCollision3D::get_bone() const {
+ return bone;
+}
+
+void SpringBoneCollision3D::set_position_offset(const Vector3 &p_offset) {
+ if (position_offset == p_offset) {
+ return;
+ }
+ position_offset = p_offset;
+ sync_pose();
+#ifdef TOOLS_ENABLED
+ update_gizmos();
+#endif // TOOLS_ENABLED
+}
+
+Vector3 SpringBoneCollision3D::get_position_offset() const {
+ return position_offset;
+}
+
+void SpringBoneCollision3D::set_rotation_offset(const Quaternion &p_offset) {
+ if (rotation_offset == p_offset) {
+ return;
+ }
+ rotation_offset = p_offset;
+ sync_pose();
+#ifdef TOOLS_ENABLED
+ update_gizmos();
+#endif // TOOLS_ENABLED
+}
+
+Quaternion SpringBoneCollision3D::get_rotation_offset() const {
+ return rotation_offset;
+}
+
+void SpringBoneCollision3D::sync_pose() {
+ if (bone >= 0) {
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ Transform3D tr = sk->get_global_transform() * sk->get_bone_global_pose(bone);
+ tr.origin += tr.basis.get_rotation_quaternion().xform(position_offset);
+ tr.basis *= Basis(rotation_offset);
+ set_global_transform(tr);
+ }
+ }
+}
+
+Transform3D SpringBoneCollision3D::get_transform_from_skeleton(const Transform3D &p_center) const {
+ Transform3D gtr = get_global_transform();
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ Transform3D tr = sk->get_global_transform();
+ gtr = tr.affine_inverse() * p_center * gtr;
+ }
+ return gtr;
+}
+
+void SpringBoneCollision3D::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("get_skeleton"), &SpringBoneCollision3D::get_skeleton);
+
+ ClassDB::bind_method(D_METHOD("set_bone_name", "bone_name"), &SpringBoneCollision3D::set_bone_name);
+ ClassDB::bind_method(D_METHOD("get_bone_name"), &SpringBoneCollision3D::get_bone_name);
+
+ ClassDB::bind_method(D_METHOD("set_bone", "bone"), &SpringBoneCollision3D::set_bone);
+ ClassDB::bind_method(D_METHOD("get_bone"), &SpringBoneCollision3D::get_bone);
+
+ ClassDB::bind_method(D_METHOD("set_position_offset", "offset"), &SpringBoneCollision3D::set_position_offset);
+ ClassDB::bind_method(D_METHOD("get_position_offset"), &SpringBoneCollision3D::get_position_offset);
+
+ ClassDB::bind_method(D_METHOD("set_rotation_offset", "offset"), &SpringBoneCollision3D::set_rotation_offset);
+ ClassDB::bind_method(D_METHOD("get_rotation_offset"), &SpringBoneCollision3D::get_rotation_offset);
+
+ ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "bone_name"), "set_bone_name", "get_bone_name");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_bone", "get_bone");
+
+ ADD_GROUP("Offset", "");
+ ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "position_offset"), "set_position_offset", "get_position_offset");
+ ADD_PROPERTY(PropertyInfo(Variant::QUATERNION, "rotation_offset"), "set_rotation_offset", "get_rotation_offset");
+}
+
+Vector3 SpringBoneCollision3D::collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const {
+ return _collide(p_center, p_bone_radius, p_bone_length, p_current);
+}
+
+Vector3 SpringBoneCollision3D::_collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const {
+ return Vector3(0, 0, 0);
+}
+
+#ifdef TOOLS_ENABLED
+void SpringBoneCollision3D::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_EDITOR_PRE_SAVE: {
+ sync_pose();
+ } break;
+ }
+}
+#endif // TOOLS_ENABLED
diff --git a/scene/3d/spring_bone_collision_3d.h b/scene/3d/spring_bone_collision_3d.h
new file mode 100644
index 00000000000..713891e04d5
--- /dev/null
+++ b/scene/3d/spring_bone_collision_3d.h
@@ -0,0 +1,75 @@
+/**************************************************************************/
+/* spring_bone_collision_3d.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. */
+/**************************************************************************/
+
+#ifndef SPRING_BONE_COLLISION_3D_H
+#define SPRING_BONE_COLLISION_3D_H
+
+#include "scene/3d/skeleton_3d.h"
+
+class SpringBoneCollision3D : public Node3D {
+ GDCLASS(SpringBoneCollision3D, Node3D);
+
+ String bone_name;
+ int bone = -1;
+
+ Vector3 position_offset;
+ Quaternion rotation_offset;
+
+protected:
+ PackedStringArray get_configuration_warnings() const override;
+
+ void _validate_property(PropertyInfo &p_property) const;
+ static void _bind_methods();
+#ifdef TOOLS_ENABLED
+ virtual void _notification(int p_what);
+#endif // TOOLS_ENABLED
+
+ virtual Vector3 _collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const;
+
+public:
+ Skeleton3D *get_skeleton() const;
+
+ void set_bone_name(const String &p_name);
+ String get_bone_name() const;
+ void set_bone(int p_bone);
+ int get_bone() const;
+
+ void set_position_offset(const Vector3 &p_offset);
+ Vector3 get_position_offset() const;
+ void set_rotation_offset(const Quaternion &p_offset);
+ Quaternion get_rotation_offset() const;
+
+ void sync_pose();
+ Transform3D get_transform_from_skeleton(const Transform3D &p_center) const;
+
+ Vector3 collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const;
+};
+
+#endif // SPRING_BONE_COLLISION_3D_H
diff --git a/scene/3d/spring_bone_collision_capsule_3d.cpp b/scene/3d/spring_bone_collision_capsule_3d.cpp
new file mode 100644
index 00000000000..39acc5acebb
--- /dev/null
+++ b/scene/3d/spring_bone_collision_capsule_3d.cpp
@@ -0,0 +1,112 @@
+/**************************************************************************/
+/* spring_bone_collision_capsule_3d.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 "spring_bone_collision_capsule_3d.h"
+
+#include "scene/3d/spring_bone_collision_sphere_3d.h"
+
+void SpringBoneCollisionCapsule3D::set_radius(float p_radius) {
+ radius = p_radius;
+ if (radius > height * 0.5) {
+ height = radius * 2.0;
+ }
+#ifdef TOOLS_ENABLED
+ update_gizmos();
+#endif // TOOLS_ENABLED
+}
+
+float SpringBoneCollisionCapsule3D::get_radius() const {
+ return radius;
+}
+
+void SpringBoneCollisionCapsule3D::set_height(float p_height) {
+ height = p_height;
+ if (radius > height * 0.5) {
+ radius = height * 0.5;
+ }
+#ifdef TOOLS_ENABLED
+ update_gizmos();
+#endif // TOOLS_ENABLED
+}
+
+float SpringBoneCollisionCapsule3D::get_height() const {
+ return height;
+}
+
+void SpringBoneCollisionCapsule3D::set_inside(bool p_enabled) {
+ inside = p_enabled;
+#ifdef TOOLS_ENABLED
+ update_gizmos();
+#endif // TOOLS_ENABLED
+}
+
+bool SpringBoneCollisionCapsule3D::is_inside() const {
+ return inside;
+}
+
+Pair SpringBoneCollisionCapsule3D::get_head_and_tail(const Transform3D &p_center) const {
+ static const Vector3 VECTOR3_UP = Vector3(0, 1, 0);
+ static const Vector3 VECTOR3_DOWN = Vector3(0, -1, 0);
+ Transform3D tr = get_transform_from_skeleton(p_center);
+ return Pair(tr.origin + tr.basis.xform(VECTOR3_UP * (height * 0.5 - radius)), tr.origin + tr.basis.xform(VECTOR3_DOWN * (height * 0.5 - radius)));
+}
+
+void SpringBoneCollisionCapsule3D::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_radius", "radius"), &SpringBoneCollisionCapsule3D::set_radius);
+ ClassDB::bind_method(D_METHOD("get_radius"), &SpringBoneCollisionCapsule3D::get_radius);
+ ClassDB::bind_method(D_METHOD("set_height", "height"), &SpringBoneCollisionCapsule3D::set_height);
+ ClassDB::bind_method(D_METHOD("get_height"), &SpringBoneCollisionCapsule3D::get_height);
+ ClassDB::bind_method(D_METHOD("set_inside", "enabled"), &SpringBoneCollisionCapsule3D::set_inside);
+ ClassDB::bind_method(D_METHOD("is_inside"), &SpringBoneCollisionCapsule3D::is_inside);
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radius", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,suffix:m"), "set_radius", "get_radius");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "height", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,suffix:m"), "set_height", "get_height");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "inside"), "set_inside", "is_inside");
+}
+
+Vector3 SpringBoneCollisionCapsule3D::_collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const {
+ Pair head_tail = get_head_and_tail(p_center);
+ Vector3 head = head_tail.first;
+ Vector3 tail = head_tail.second;
+ Vector3 p = tail - head;
+ Vector3 q = p_current - head;
+ float dot = p.dot(q);
+ if (dot <= 0) {
+ return SpringBoneCollisionSphere3D::_collide_sphere(head, radius, inside, p_bone_radius, p_bone_length, p_current);
+ }
+ float pls = p.length_squared();
+ if (Math::is_zero_approx(pls)) {
+ return p_current;
+ }
+ if (pls <= dot) {
+ return SpringBoneCollisionSphere3D::_collide_sphere(head + p, radius, inside, p_bone_radius, p_bone_length, p_current);
+ }
+ return SpringBoneCollisionSphere3D::_collide_sphere(head + p * (dot / pls), radius, inside, p_bone_radius, p_bone_length, p_current);
+}
diff --git a/scene/3d/spring_bone_collision_capsule_3d.h b/scene/3d/spring_bone_collision_capsule_3d.h
new file mode 100644
index 00000000000..0e24bed6407
--- /dev/null
+++ b/scene/3d/spring_bone_collision_capsule_3d.h
@@ -0,0 +1,60 @@
+/**************************************************************************/
+/* spring_bone_collision_capsule_3d.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. */
+/**************************************************************************/
+
+#ifndef SPRING_BONE_COLLISION_CAPSULE_3D_H
+#define SPRING_BONE_COLLISION_CAPSULE_3D_H
+
+#include "scene/3d/spring_bone_collision_3d.h"
+
+class SpringBoneCollisionCapsule3D : public SpringBoneCollision3D {
+ GDCLASS(SpringBoneCollisionCapsule3D, SpringBoneCollision3D);
+
+ float radius = 0.1;
+ float height = 0.5;
+ bool inside = false;
+
+protected:
+ static void _bind_methods();
+
+ virtual Vector3 _collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const override;
+
+public:
+ void set_radius(float p_radius);
+ float get_radius() const;
+ void set_height(float p_height);
+ float get_height() const;
+ void set_inside(bool p_enabled);
+ bool is_inside() const;
+
+ // Helper.
+ Pair get_head_and_tail(const Transform3D &p_center) const;
+};
+
+#endif // SPRING_BONE_COLLISION_CAPSULE_3D_H
diff --git a/scene/3d/spring_bone_collision_plane_3d.cpp b/scene/3d/spring_bone_collision_plane_3d.cpp
new file mode 100644
index 00000000000..8fd29834622
--- /dev/null
+++ b/scene/3d/spring_bone_collision_plane_3d.cpp
@@ -0,0 +1,44 @@
+/**************************************************************************/
+/* spring_bone_collision_plane_3d.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 "spring_bone_collision_plane_3d.h"
+
+Vector3 SpringBoneCollisionPlane3D::_collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const {
+ static const Vector3 VECTOR3_UP = Vector3(0, 1, 0);
+ Transform3D tr = get_transform_from_skeleton(p_center);
+ Vector3 pos = tr.origin;
+ Vector3 normal = tr.basis.get_rotation_quaternion().xform(VECTOR3_UP);
+ Vector3 to_vec = p_current - pos;
+ float distance = to_vec.dot(normal) - p_bone_radius;
+ if (distance > 0) {
+ return p_current;
+ }
+ return p_current + normal * -distance;
+}
diff --git a/scene/3d/spring_bone_collision_plane_3d.h b/scene/3d/spring_bone_collision_plane_3d.h
new file mode 100644
index 00000000000..0f3896aa209
--- /dev/null
+++ b/scene/3d/spring_bone_collision_plane_3d.h
@@ -0,0 +1,43 @@
+/**************************************************************************/
+/* spring_bone_collision_plane_3d.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. */
+/**************************************************************************/
+
+#ifndef SPRING_BONE_COLLISION_PLANE_3D_H
+#define SPRING_BONE_COLLISION_PLANE_3D_H
+
+#include "scene/3d/spring_bone_collision_3d.h"
+
+class SpringBoneCollisionPlane3D : public SpringBoneCollision3D {
+ GDCLASS(SpringBoneCollisionPlane3D, SpringBoneCollision3D);
+
+protected:
+ virtual Vector3 _collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const override;
+};
+
+#endif // SPRING_BONE_COLLISION_PLANE_3D_H
diff --git a/scene/3d/spring_bone_collision_sphere_3d.cpp b/scene/3d/spring_bone_collision_sphere_3d.cpp
new file mode 100644
index 00000000000..3560932bc93
--- /dev/null
+++ b/scene/3d/spring_bone_collision_sphere_3d.cpp
@@ -0,0 +1,78 @@
+/**************************************************************************/
+/* spring_bone_collision_sphere_3d.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 "spring_bone_collision_sphere_3d.h"
+
+void SpringBoneCollisionSphere3D::set_radius(float p_radius) {
+ radius = p_radius;
+#ifdef TOOLS_ENABLED
+ update_gizmos();
+#endif // TOOLS_ENABLED
+}
+
+float SpringBoneCollisionSphere3D::get_radius() const {
+ return radius;
+}
+
+void SpringBoneCollisionSphere3D::set_inside(bool p_enabled) {
+ inside = p_enabled;
+#ifdef TOOLS_ENABLED
+ update_gizmos();
+#endif // TOOLS_ENABLED
+}
+
+bool SpringBoneCollisionSphere3D::is_inside() const {
+ return inside;
+}
+
+void SpringBoneCollisionSphere3D::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_radius", "radius"), &SpringBoneCollisionSphere3D::set_radius);
+ ClassDB::bind_method(D_METHOD("get_radius"), &SpringBoneCollisionSphere3D::get_radius);
+ ClassDB::bind_method(D_METHOD("set_inside", "enabled"), &SpringBoneCollisionSphere3D::set_inside);
+ ClassDB::bind_method(D_METHOD("is_inside"), &SpringBoneCollisionSphere3D::is_inside);
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radius", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,suffix:m"), "set_radius", "get_radius");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "inside"), "set_inside", "is_inside");
+}
+
+Vector3 SpringBoneCollisionSphere3D::_collide_sphere(const Vector3 &p_origin, float p_radius, bool p_inside, float p_bone_radius, float p_bone_length, const Vector3 &p_current) {
+ Vector3 diff = p_current - p_origin;
+ float length = diff.length();
+ float r = p_inside ? p_radius - p_bone_radius : p_bone_radius + p_radius;
+ float distance = p_inside ? r - length : length - r;
+ if (distance > 0) {
+ return p_current;
+ }
+ return p_origin + diff.normalized() * r;
+}
+
+Vector3 SpringBoneCollisionSphere3D::_collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const {
+ return _collide_sphere(get_transform_from_skeleton(p_center).origin, radius, inside, p_bone_radius, p_bone_length, p_current);
+}
diff --git a/scene/3d/spring_bone_collision_sphere_3d.h b/scene/3d/spring_bone_collision_sphere_3d.h
new file mode 100644
index 00000000000..4de0f93700c
--- /dev/null
+++ b/scene/3d/spring_bone_collision_sphere_3d.h
@@ -0,0 +1,59 @@
+/**************************************************************************/
+/* spring_bone_collision_sphere_3d.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. */
+/**************************************************************************/
+
+#ifndef SPRING_BONE_COLLISION_SPHERE_3D_H
+#define SPRING_BONE_COLLISION_SPHERE_3D_H
+
+#include "scene/3d/spring_bone_collision_3d.h"
+
+class SpringBoneCollisionCapsule3D;
+
+class SpringBoneCollisionSphere3D : public SpringBoneCollision3D {
+ GDCLASS(SpringBoneCollisionSphere3D, SpringBoneCollision3D);
+
+ friend class SpringBoneCollisionCapsule3D;
+
+ float radius = 0.1;
+ bool inside = false;
+
+protected:
+ static void _bind_methods();
+
+ static Vector3 _collide_sphere(const Vector3 &p_origin, float p_radius, bool p_inside, float p_bone_radius, float p_bone_length, const Vector3 &p_current);
+ virtual Vector3 _collide(const Transform3D &p_center, float p_bone_radius, float p_bone_length, const Vector3 &p_current) const override;
+
+public:
+ void set_radius(float p_radius);
+ float get_radius() const;
+ void set_inside(bool p_enabled);
+ bool is_inside() const;
+};
+
+#endif // SPRING_BONE_COLLISION_SPHERE_3D_H
diff --git a/scene/3d/spring_bone_simulator_3d.cpp b/scene/3d/spring_bone_simulator_3d.cpp
new file mode 100644
index 00000000000..db2755288bf
--- /dev/null
+++ b/scene/3d/spring_bone_simulator_3d.cpp
@@ -0,0 +1,1601 @@
+/**************************************************************************/
+/* spring_bone_simulator_3d.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 "spring_bone_simulator_3d.h"
+
+#include "scene/3d/spring_bone_collision_3d.h"
+
+// Original VRM Spring Bone movement logic was distributed by (c) VRM Consortium. Licensed under the MIT license.
+
+bool SpringBoneSimulator3D::_set(const StringName &p_path, const Variant &p_value) {
+ String path = p_path;
+
+ if (path.begins_with("settings/")) {
+ int which = path.get_slicec('/', 1).to_int();
+ String what = path.get_slicec('/', 2);
+ ERR_FAIL_INDEX_V(which, settings.size(), false);
+
+ if (what == "root_bone_name") {
+ set_root_bone_name(which, p_value);
+ } else if (what == "root_bone") {
+ set_root_bone(which, p_value);
+ } else if (what == "end_bone_name") {
+ set_end_bone_name(which, p_value);
+ } else if (what == "end_bone") {
+ String opt = path.get_slicec('/', 3);
+ if (opt.is_empty()) {
+ set_end_bone(which, p_value);
+ } else if (opt == "direction") {
+ set_end_bone_direction(which, static_cast((int)p_value));
+ } else if (opt == "length") {
+ set_end_bone_length(which, p_value);
+ } else if (opt == "tip_radius") {
+ set_end_bone_tip_radius(which, p_value);
+ } else {
+ return false;
+ }
+ } else if (what == "extend_end_bone") {
+ set_extend_end_bone(which, p_value);
+ } else if (what == "center_from") {
+ set_center_from(which, static_cast((int)p_value));
+ } else if (what == "center_node") {
+ set_center_node(which, p_value);
+ } else if (what == "center_bone") {
+ set_center_bone(which, p_value);
+ } else if (what == "center_bone_name") {
+ set_center_bone_name(which, p_value);
+ } else if (what == "individual_config") {
+ set_individual_config(which, p_value);
+ } else if (what == "rotation_axis") {
+ set_rotation_axis(which, static_cast((int)p_value));
+ } else if (what == "radius") {
+ String opt = path.get_slicec('/', 3);
+ if (opt == "value") {
+ set_radius(which, p_value);
+ } else if (opt == "damping_curve") {
+ set_radius_damping_curve(which, p_value);
+ } else {
+ return false;
+ }
+ } else if (what == "stiffness") {
+ String opt = path.get_slicec('/', 3);
+ if (opt == "value") {
+ set_stiffness(which, p_value);
+ } else if (opt == "damping_curve") {
+ set_stiffness_damping_curve(which, p_value);
+ } else {
+ return false;
+ }
+ } else if (what == "drag") {
+ String opt = path.get_slicec('/', 3);
+ if (opt == "value") {
+ set_drag(which, p_value);
+ } else if (opt == "damping_curve") {
+ set_drag_damping_curve(which, p_value);
+ } else {
+ return false;
+ }
+ } else if (what == "gravity") {
+ String opt = path.get_slicec('/', 3);
+ if (opt == "value") {
+ set_gravity(which, p_value);
+ } else if (opt == "damping_curve") {
+ set_gravity_damping_curve(which, p_value);
+ } else if (opt == "direction") {
+ set_gravity_direction(which, p_value);
+ } else {
+ return false;
+ }
+ } else if (what == "enable_all_child_collisions") {
+ set_enable_all_child_collisions(which, p_value);
+ } else if (what == "joint_count") {
+ set_joint_count(which, p_value);
+ } else if (what == "joints") {
+ int idx = path.get_slicec('/', 3).to_int();
+ String prop = path.get_slicec('/', 4);
+ if (prop == "bone_name") {
+ set_joint_bone_name(which, idx, p_value);
+ } else if (prop == "bone") {
+ set_joint_bone(which, idx, p_value);
+ } else if (prop == "rotation_axis") {
+ set_joint_rotation_axis(which, idx, static_cast((int)p_value));
+ } else if (prop == "radius") {
+ set_joint_radius(which, idx, p_value);
+ } else if (prop == "stiffness") {
+ set_joint_stiffness(which, idx, p_value);
+ } else if (prop == "drag") {
+ set_joint_drag(which, idx, p_value);
+ } else if (prop == "gravity") {
+ set_joint_gravity(which, idx, p_value);
+ } else if (prop == "gravity_direction") {
+ set_joint_gravity_direction(which, idx, p_value);
+ } else {
+ return false;
+ }
+ } else if (what == "exclude_collision_count") {
+ set_exclude_collision_count(which, p_value);
+ } else if (what == "exclude_collisions") {
+ int idx = path.get_slicec('/', 3).to_int();
+ set_exclude_collision_path(which, idx, p_value);
+ } else if (what == "collision_count") {
+ set_collision_count(which, p_value);
+ } else if (what == "collisions") {
+ int idx = path.get_slicec('/', 3).to_int();
+ set_collision_path(which, idx, p_value);
+ } else {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool SpringBoneSimulator3D::_get(const StringName &p_path, Variant &r_ret) const {
+ String path = p_path;
+
+ if (path.begins_with("settings/")) {
+ int which = path.get_slicec('/', 1).to_int();
+ String what = path.get_slicec('/', 2);
+ ERR_FAIL_INDEX_V(which, settings.size(), false);
+
+ if (what == "root_bone_name") {
+ r_ret = get_root_bone_name(which);
+ } else if (what == "root_bone") {
+ r_ret = get_root_bone(which);
+ } else if (what == "end_bone_name") {
+ r_ret = get_end_bone_name(which);
+ } else if (what == "end_bone") {
+ String opt = path.get_slicec('/', 3);
+ if (opt.is_empty()) {
+ r_ret = get_end_bone(which);
+ } else if (opt == "direction") {
+ r_ret = (int)get_end_bone_direction(which);
+ } else if (opt == "length") {
+ r_ret = get_end_bone_length(which);
+ } else if (opt == "tip_radius") {
+ r_ret = get_end_bone_tip_radius(which);
+ } else {
+ return false;
+ }
+ } else if (what == "extend_end_bone") {
+ r_ret = is_end_bone_extended(which);
+ } else if (what == "center_from") {
+ r_ret = (int)get_center_from(which);
+ } else if (what == "center_node") {
+ r_ret = get_center_node(which);
+ } else if (what == "center_bone") {
+ r_ret = get_center_bone(which);
+ } else if (what == "center_bone_name") {
+ r_ret = get_center_bone_name(which);
+ } else if (what == "individual_config") {
+ r_ret = is_config_individual(which);
+ } else if (what == "rotation_axis") {
+ r_ret = (int)get_rotation_axis(which);
+ } else if (what == "radius") {
+ String opt = path.get_slicec('/', 3);
+ if (opt == "value") {
+ r_ret = get_radius(which);
+ } else if (opt == "damping_curve") {
+ r_ret = get_radius_damping_curve(which);
+ } else {
+ return false;
+ }
+ } else if (what == "stiffness") {
+ String opt = path.get_slicec('/', 3);
+ if (opt == "value") {
+ r_ret = get_stiffness(which);
+ } else if (opt == "damping_curve") {
+ r_ret = get_stiffness_damping_curve(which);
+ } else {
+ return false;
+ }
+ } else if (what == "drag") {
+ String opt = path.get_slicec('/', 3);
+ if (opt == "value") {
+ r_ret = get_drag(which);
+ } else if (opt == "damping_curve") {
+ r_ret = get_drag_damping_curve(which);
+ } else {
+ return false;
+ }
+ } else if (what == "gravity") {
+ String opt = path.get_slicec('/', 3);
+ if (opt == "value") {
+ r_ret = get_gravity(which);
+ } else if (opt == "damping_curve") {
+ r_ret = get_gravity_damping_curve(which);
+ } else if (opt == "direction") {
+ r_ret = get_gravity_direction(which);
+ } else {
+ return false;
+ }
+ } else if (what == "enable_all_child_collisions") {
+ r_ret = are_all_child_collisions_enabled(which);
+ } else if (what == "joint_count") {
+ r_ret = get_joint_count(which);
+ } else if (what == "joints") {
+ int idx = path.get_slicec('/', 3).to_int();
+ String prop = path.get_slicec('/', 4);
+ if (prop == "bone_name") {
+ r_ret = get_joint_bone_name(which, idx);
+ } else if (prop == "bone") {
+ r_ret = get_joint_bone(which, idx);
+ } else if (prop == "rotation_axis") {
+ r_ret = (int)get_joint_rotation_axis(which, idx);
+ } else if (prop == "radius") {
+ r_ret = get_joint_radius(which, idx);
+ } else if (prop == "stiffness") {
+ r_ret = get_joint_stiffness(which, idx);
+ } else if (prop == "drag") {
+ r_ret = get_joint_drag(which, idx);
+ } else if (prop == "gravity") {
+ r_ret = get_joint_gravity(which, idx);
+ } else if (prop == "gravity_direction") {
+ r_ret = get_joint_gravity_direction(which, idx);
+ } else {
+ return false;
+ }
+ } else if (what == "exclude_collision_count") {
+ r_ret = get_exclude_collision_count(which);
+ } else if (what == "exclude_collisions") {
+ int idx = path.get_slicec('/', 3).to_int();
+ r_ret = get_exclude_collision_path(which, idx);
+ } else if (what == "collision_count") {
+ r_ret = get_collision_count(which);
+ } else if (what == "collisions") {
+ int idx = path.get_slicec('/', 3).to_int();
+ r_ret = get_collision_path(which, idx);
+ } else {
+ return false;
+ }
+ }
+ return true;
+}
+
+void SpringBoneSimulator3D::_get_property_list(List *p_list) const {
+ String enum_hint;
+ Skeleton3D *skeleton = get_skeleton();
+ if (skeleton) {
+ enum_hint = skeleton->get_concatenated_bone_names();
+ }
+
+ for (int i = 0; i < settings.size(); i++) {
+ String path = "settings/" + itos(i) + "/";
+ p_list->push_back(PropertyInfo(Variant::STRING, path + "root_bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint));
+ p_list->push_back(PropertyInfo(Variant::INT, path + "root_bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
+ p_list->push_back(PropertyInfo(Variant::STRING, path + "end_bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint));
+ p_list->push_back(PropertyInfo(Variant::INT, path + "end_bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
+ p_list->push_back(PropertyInfo(Variant::BOOL, path + "extend_end_bone"));
+ p_list->push_back(PropertyInfo(Variant::INT, path + "end_bone/direction", PROPERTY_HINT_ENUM, "+X,-X,+Y,-Y,+Z,-Z,FromParent"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, path + "end_bone/length", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,suffix:m"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, path + "end_bone/tip_radius", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,suffix:m"));
+ p_list->push_back(PropertyInfo(Variant::INT, path + "center_from", PROPERTY_HINT_ENUM, "WorldOrigin,Node,Bone"));
+ p_list->push_back(PropertyInfo(Variant::NODE_PATH, path + "center_node"));
+ p_list->push_back(PropertyInfo(Variant::STRING, path + "center_bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint));
+ p_list->push_back(PropertyInfo(Variant::INT, path + "center_bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
+ p_list->push_back(PropertyInfo(Variant::BOOL, path + "individual_config"));
+ p_list->push_back(PropertyInfo(Variant::INT, path + "rotation_axis", PROPERTY_HINT_ENUM, "X,Y,Z,All"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, path + "radius/value", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,suffix:m"));
+ p_list->push_back(PropertyInfo(Variant::OBJECT, path + "radius/damping_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, path + "stiffness/value", PROPERTY_HINT_RANGE, "0,4,0.01,or_greater"));
+ p_list->push_back(PropertyInfo(Variant::OBJECT, path + "stiffness/damping_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, path + "drag/value", PROPERTY_HINT_RANGE, "0,1,0.01,or_greater"));
+ p_list->push_back(PropertyInfo(Variant::OBJECT, path + "drag/damping_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, path + "gravity/value", PROPERTY_HINT_RANGE, "0,1,0.01,or_greater"));
+ p_list->push_back(PropertyInfo(Variant::OBJECT, path + "gravity/damping_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"));
+ p_list->push_back(PropertyInfo(Variant::VECTOR3, path + "gravity/direction"));
+ p_list->push_back(PropertyInfo(Variant::INT, path + "joint_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, "Joints," + path + "joints/,static,const"));
+ for (int j = 0; j < settings[i]->joints.size(); j++) {
+ String joint_path = path + "joints/" + itos(j) + "/";
+ p_list->push_back(PropertyInfo(Variant::STRING, joint_path + "bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint, PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_STORAGE));
+ p_list->push_back(PropertyInfo(Variant::INT, joint_path + "bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_READ_ONLY));
+ p_list->push_back(PropertyInfo(Variant::INT, joint_path + "rotation_axis", PROPERTY_HINT_ENUM, "X,Y,Z,All"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, joint_path + "radius", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,suffix:m"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, joint_path + "stiffness", PROPERTY_HINT_RANGE, "0,4,0.01,or_greater"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, joint_path + "drag", PROPERTY_HINT_RANGE, "0,1,0.01,or_greater"));
+ p_list->push_back(PropertyInfo(Variant::FLOAT, joint_path + "gravity", PROPERTY_HINT_RANGE, "0,1,0.01,or_greater"));
+ p_list->push_back(PropertyInfo(Variant::VECTOR3, joint_path + "gravity_direction"));
+ }
+ p_list->push_back(PropertyInfo(Variant::BOOL, path + "enable_all_child_collisions"));
+ p_list->push_back(PropertyInfo(Variant::INT, path + "exclude_collision_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, "Exclude Collisions," + path + "exclude_collisions/"));
+ for (int j = 0; j < settings[i]->exclude_collisions.size(); j++) {
+ String collision_path = path + "exclude_collisions/" + itos(j);
+ p_list->push_back(PropertyInfo(Variant::NODE_PATH, collision_path, PROPERTY_HINT_NODE_PATH_VALID_TYPES, "SpringBoneCollision3D"));
+ }
+ p_list->push_back(PropertyInfo(Variant::INT, path + "collision_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, "Collisions," + path + "collisions/"));
+ for (int j = 0; j < settings[i]->collisions.size(); j++) {
+ String collision_path = path + "collisions/" + itos(j);
+ p_list->push_back(PropertyInfo(Variant::NODE_PATH, collision_path, PROPERTY_HINT_NODE_PATH_VALID_TYPES, "SpringBoneCollision3D"));
+ }
+ }
+
+ for (PropertyInfo &E : *p_list) {
+ _validate_property(E);
+ }
+}
+
+void SpringBoneSimulator3D::_validate_property(PropertyInfo &p_property) const {
+ PackedStringArray split = p_property.name.split("/");
+ if (split.size() > 2 && split[0] == "settings") {
+ int which = split[1].to_int();
+
+ // Extended end bone option.
+ if (split[2] == "end_bone" && !is_end_bone_extended(which) && split.size() > 3) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+
+ // Center option.
+ if (get_center_from(which) != CENTER_FROM_BONE && (split[2] == "center_bone" || split[2] == "center_bone_name")) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+ if (get_center_from(which) != CENTER_FROM_NODE && split[2] == "center_node") {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+
+ // Joints option.
+ if (is_config_individual(which)) {
+ if (split[2] == "rotation_axis" || split[2] == "radius" || split[2] == "radius_damping_curve" ||
+ split[2] == "stiffness" || split[2] == "stiffness_damping_curve" ||
+ split[2] == "drag" || split[2] == "drag_damping_curve" ||
+ split[2] == "gravity" || split[2] == "gravity_damping_curve" || split[2] == "gravity_direction") {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+ } else {
+ if (split[2] == "joints" || split[2] == "joint_count") {
+ // Don't storage them since they are overridden by _update_joints().
+ p_property.usage ^= PROPERTY_USAGE_STORAGE;
+ p_property.usage |= PROPERTY_USAGE_READ_ONLY;
+ }
+ }
+
+ // Collisions option.
+ if (are_all_child_collisions_enabled(which)) {
+ if (split[2] == "collisions" || split[2] == "collision_count") {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+ } else {
+ if (split[2] == "exclude_collisions" || split[2] == "exclude_collision_count") {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+ }
+ }
+}
+
+void SpringBoneSimulator3D::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE: {
+#ifdef TOOLS_ENABLED
+ if (Engine::get_singleton()->is_editor_hint()) {
+ set_notify_local_transform(true); // Used for updating gizmo in editor.
+ }
+#endif // TOOLS_ENABLED
+ _make_collisions_dirty();
+ _make_all_joints_dirty();
+ } break;
+#ifdef TOOLS_ENABLED
+ case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
+ update_gizmos();
+ } break;
+#endif // TOOLS_ENABLED
+ }
+}
+
+// Setting.
+
+void SpringBoneSimulator3D::set_root_bone_name(int p_index, const String &p_bone_name) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->root_bone_name = p_bone_name;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ set_root_bone(p_index, sk->find_bone(settings[p_index]->root_bone_name));
+ }
+}
+
+String SpringBoneSimulator3D::get_root_bone_name(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), String());
+ return settings[p_index]->root_bone_name;
+}
+
+void SpringBoneSimulator3D::set_root_bone(int p_index, int p_bone) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ bool changed = settings[p_index]->root_bone != p_bone;
+ settings[p_index]->root_bone = p_bone;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ if (settings[p_index]->root_bone <= -1 || settings[p_index]->root_bone >= sk->get_bone_count()) {
+ WARN_PRINT("Root bone index out of range!");
+ settings[p_index]->root_bone = -1;
+ } else {
+ settings[p_index]->root_bone_name = sk->get_bone_name(settings[p_index]->root_bone);
+ }
+ }
+ if (changed) {
+ _update_joint_array(p_index);
+ }
+}
+
+int SpringBoneSimulator3D::get_root_bone(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), -1);
+ return settings[p_index]->root_bone;
+}
+
+void SpringBoneSimulator3D::set_end_bone_name(int p_index, const String &p_bone_name) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->end_bone_name = p_bone_name;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ set_end_bone(p_index, sk->find_bone(settings[p_index]->end_bone_name));
+ }
+}
+
+String SpringBoneSimulator3D::get_end_bone_name(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), String());
+ return settings[p_index]->end_bone_name;
+}
+
+void SpringBoneSimulator3D::set_end_bone(int p_index, int p_bone) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ bool changed = settings[p_index]->end_bone != p_bone;
+ settings[p_index]->end_bone = p_bone;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ if (settings[p_index]->end_bone <= -1 || settings[p_index]->end_bone >= sk->get_bone_count()) {
+ WARN_PRINT("End bone index out of range!");
+ settings[p_index]->end_bone = -1;
+ } else {
+ settings[p_index]->end_bone_name = sk->get_bone_name(settings[p_index]->end_bone);
+ }
+ }
+ if (changed) {
+ _update_joint_array(p_index);
+ }
+}
+
+int SpringBoneSimulator3D::get_end_bone(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), -1);
+ return settings[p_index]->end_bone;
+}
+
+void SpringBoneSimulator3D::set_extend_end_bone(int p_index, bool p_enabled) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->extend_end_bone = p_enabled;
+ _make_joints_dirty(p_index);
+ notify_property_list_changed();
+}
+
+bool SpringBoneSimulator3D::is_end_bone_extended(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), false);
+ return settings[p_index]->extend_end_bone;
+}
+
+void SpringBoneSimulator3D::set_end_bone_direction(int p_index, BoneDirection p_bone_direction) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->end_bone_direction = p_bone_direction;
+ _make_joints_dirty(p_index);
+}
+
+SpringBoneSimulator3D::BoneDirection SpringBoneSimulator3D::get_end_bone_direction(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), BONE_DIRECTION_FROM_PARENT);
+ return settings[p_index]->end_bone_direction;
+}
+
+void SpringBoneSimulator3D::set_end_bone_length(int p_index, float p_length) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->end_bone_length = p_length;
+ _make_joints_dirty(p_index);
+}
+
+float SpringBoneSimulator3D::get_end_bone_length(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ return settings[p_index]->end_bone_length;
+}
+
+void SpringBoneSimulator3D::set_end_bone_tip_radius(int p_index, float p_radius) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->end_bone_tip_radius = p_radius;
+ _make_joints_dirty(p_index);
+}
+
+float SpringBoneSimulator3D::get_end_bone_tip_radius(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ return settings[p_index]->end_bone_tip_radius;
+}
+
+Vector3 SpringBoneSimulator3D::get_end_bone_axis(int p_end_bone, BoneDirection p_direction) const {
+ Vector3 axis;
+ if (p_direction == BONE_DIRECTION_FROM_PARENT) {
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ axis = sk->get_bone_rest(p_end_bone).basis.xform_inv(sk->get_bone_rest(p_end_bone).origin);
+ axis.normalize();
+ }
+ } else {
+ axis = get_vector_from_bone_axis(static_cast((int)p_direction));
+ }
+ return axis;
+}
+
+void SpringBoneSimulator3D::set_center_from(int p_index, CenterFrom p_center_from) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ bool center_changed = settings[p_index]->center_from != p_center_from;
+ settings[p_index]->center_from = p_center_from;
+ if (center_changed) {
+ reset();
+ }
+ notify_property_list_changed();
+}
+
+SpringBoneSimulator3D::CenterFrom SpringBoneSimulator3D::get_center_from(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), CENTER_FROM_WORLD_ORIGIN);
+ return settings[p_index]->center_from;
+}
+
+void SpringBoneSimulator3D::set_center_node(int p_index, const NodePath &p_node_path) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ bool center_changed = settings[p_index]->center_node != p_node_path;
+ settings[p_index]->center_node = p_node_path;
+ if (center_changed) {
+ reset();
+ }
+}
+
+NodePath SpringBoneSimulator3D::get_center_node(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), NodePath());
+ return settings[p_index]->center_node;
+}
+
+void SpringBoneSimulator3D::set_center_bone_name(int p_index, const String &p_bone_name) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->center_bone_name = p_bone_name;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ set_center_bone(p_index, sk->find_bone(settings[p_index]->center_bone_name));
+ }
+}
+
+String SpringBoneSimulator3D::get_center_bone_name(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), String());
+ return settings[p_index]->center_bone_name;
+}
+
+void SpringBoneSimulator3D::set_center_bone(int p_index, int p_bone) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ bool center_changed = settings[p_index]->center_bone != p_bone;
+ settings[p_index]->center_bone = p_bone;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ if (settings[p_index]->center_bone <= -1 || settings[p_index]->center_bone >= sk->get_bone_count()) {
+ WARN_PRINT("Center bone index out of range!");
+ settings[p_index]->center_bone = -1;
+ } else {
+ settings[p_index]->center_bone_name = sk->get_bone_name(settings[p_index]->center_bone);
+ }
+ }
+ if (center_changed) {
+ reset();
+ }
+}
+
+int SpringBoneSimulator3D::get_center_bone(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), -1);
+ return settings[p_index]->center_bone;
+}
+
+void SpringBoneSimulator3D::set_radius(int p_index, float p_radius) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ settings[p_index]->radius = p_radius;
+ _make_joints_dirty(p_index);
+}
+
+float SpringBoneSimulator3D::get_radius(int p_index) const {
+ return settings[p_index]->radius;
+}
+
+void SpringBoneSimulator3D::set_radius_damping_curve(int p_index, const Ref &p_damping_curve) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ if (settings[p_index]->radius_damping_curve.is_valid()) {
+ settings[p_index]->radius_damping_curve->disconnect_changed(callable_mp(this, &SpringBoneSimulator3D::_make_joints_dirty));
+ }
+ settings[p_index]->radius_damping_curve = p_damping_curve;
+ if (settings[p_index]->radius_damping_curve.is_valid()) {
+ settings[p_index]->radius_damping_curve->connect_changed(callable_mp(this, &SpringBoneSimulator3D::_make_joints_dirty).bind(p_index));
+ }
+ _make_joints_dirty(p_index);
+}
+
+Ref SpringBoneSimulator3D::get_radius_damping_curve(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), Ref());
+ return settings[p_index]->radius_damping_curve;
+}
+
+void SpringBoneSimulator3D::set_stiffness(int p_index, float p_stiffness) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ settings[p_index]->stiffness = p_stiffness;
+ _make_joints_dirty(p_index);
+}
+
+float SpringBoneSimulator3D::get_stiffness(int p_index) const {
+ return settings[p_index]->stiffness;
+}
+
+void SpringBoneSimulator3D::set_stiffness_damping_curve(int p_index, const Ref &p_damping_curve) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ if (settings[p_index]->stiffness_damping_curve.is_valid()) {
+ settings[p_index]->stiffness_damping_curve->disconnect_changed(callable_mp(this, &SpringBoneSimulator3D::_make_joints_dirty));
+ }
+ settings[p_index]->stiffness_damping_curve = p_damping_curve;
+ if (settings[p_index]->stiffness_damping_curve.is_valid()) {
+ settings[p_index]->stiffness_damping_curve->connect_changed(callable_mp(this, &SpringBoneSimulator3D::_make_joints_dirty).bind(p_index));
+ }
+ _make_joints_dirty(p_index);
+}
+
+Ref SpringBoneSimulator3D::get_stiffness_damping_curve(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), Ref());
+ return settings[p_index]->stiffness_damping_curve;
+}
+
+void SpringBoneSimulator3D::set_drag(int p_index, float p_drag) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ settings[p_index]->drag = p_drag;
+ _make_joints_dirty(p_index);
+}
+
+float SpringBoneSimulator3D::get_drag(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ return settings[p_index]->drag;
+}
+
+void SpringBoneSimulator3D::set_drag_damping_curve(int p_index, const Ref &p_damping_curve) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ if (settings[p_index]->drag_damping_curve.is_valid()) {
+ settings[p_index]->drag_damping_curve->disconnect_changed(callable_mp(this, &SpringBoneSimulator3D::_make_joints_dirty));
+ }
+ settings[p_index]->drag_damping_curve = p_damping_curve;
+ if (settings[p_index]->drag_damping_curve.is_valid()) {
+ settings[p_index]->drag_damping_curve->connect_changed(callable_mp(this, &SpringBoneSimulator3D::_make_joints_dirty).bind(p_index));
+ }
+ _make_joints_dirty(p_index);
+}
+
+Ref SpringBoneSimulator3D::get_drag_damping_curve(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), Ref());
+ return settings[p_index]->drag_damping_curve;
+}
+
+void SpringBoneSimulator3D::set_gravity(int p_index, float p_gravity) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ settings[p_index]->gravity = p_gravity;
+ _make_joints_dirty(p_index);
+}
+
+float SpringBoneSimulator3D::get_gravity(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ return settings[p_index]->gravity;
+}
+
+void SpringBoneSimulator3D::set_gravity_damping_curve(int p_index, const Ref &p_damping_curve) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ if (settings[p_index]->gravity_damping_curve.is_valid()) {
+ settings[p_index]->gravity_damping_curve->disconnect_changed(callable_mp(this, &SpringBoneSimulator3D::_make_joints_dirty));
+ }
+ settings[p_index]->gravity_damping_curve = p_damping_curve;
+ if (settings[p_index]->gravity_damping_curve.is_valid()) {
+ settings[p_index]->gravity_damping_curve->connect_changed(callable_mp(this, &SpringBoneSimulator3D::_make_joints_dirty).bind(p_index));
+ }
+ _make_joints_dirty(p_index);
+}
+
+Ref SpringBoneSimulator3D::get_gravity_damping_curve(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), Ref());
+ return settings[p_index]->gravity_damping_curve;
+}
+
+void SpringBoneSimulator3D::set_gravity_direction(int p_index, const Vector3 &p_gravity_direction) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ ERR_FAIL_COND(p_gravity_direction.is_zero_approx());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ settings[p_index]->gravity_direction = p_gravity_direction;
+ _make_joints_dirty(p_index);
+}
+
+Vector3 SpringBoneSimulator3D::get_gravity_direction(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), Vector3(0, -1, 0));
+ return settings[p_index]->gravity_direction;
+}
+
+void SpringBoneSimulator3D::set_rotation_axis(int p_index, RotationAxis p_axis) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (is_config_individual(p_index)) {
+ return; // Joint config is individual mode.
+ }
+ settings[p_index]->rotation_axis = p_axis;
+ _make_joints_dirty(p_index);
+}
+
+SpringBoneSimulator3D::RotationAxis SpringBoneSimulator3D::get_rotation_axis(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), ROTATION_AXIS_ALL);
+ return settings[p_index]->rotation_axis;
+}
+
+void SpringBoneSimulator3D::set_setting_count(int p_count) {
+ ERR_FAIL_COND(p_count < 0);
+ int delta = p_count - settings.size() + 1;
+ settings.resize(p_count);
+ if (delta > 1) {
+ for (int i = 1; i < delta; i++) {
+ settings.write[p_count - i] = memnew(SpringBone3DSetting);
+ }
+ }
+ notify_property_list_changed();
+}
+
+int SpringBoneSimulator3D::get_setting_count() const {
+ return settings.size();
+}
+
+void SpringBoneSimulator3D::clear_settings() {
+ set_setting_count(0);
+}
+
+// Individual joints.
+
+void SpringBoneSimulator3D::set_individual_config(int p_index, bool p_enabled) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->individual_config = p_enabled;
+ _make_joints_dirty(p_index);
+ notify_property_list_changed();
+}
+
+bool SpringBoneSimulator3D::is_config_individual(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), false);
+ return settings[p_index]->individual_config;
+}
+
+void SpringBoneSimulator3D::set_joint_bone_name(int p_index, int p_joint, const String &p_bone_name) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ Vector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, joints.size());
+ joints[p_joint]->bone_name = p_bone_name;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ set_joint_bone(p_index, p_joint, sk->find_bone(joints[p_joint]->bone_name));
+ }
+}
+
+String SpringBoneSimulator3D::get_joint_bone_name(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), String());
+ Vector joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, joints.size(), String());
+ return joints[p_joint]->bone_name;
+}
+
+void SpringBoneSimulator3D::set_joint_bone(int p_index, int p_joint, int p_bone) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ Vector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, joints.size());
+ joints[p_joint]->bone = p_bone;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ if (joints[p_joint]->bone <= -1 || joints[p_joint]->bone >= sk->get_bone_count()) {
+ WARN_PRINT("Joint bone index out of range!");
+ joints[p_joint]->bone = -1;
+ } else {
+ joints[p_joint]->bone_name = sk->get_bone_name(joints[p_joint]->bone);
+ }
+ }
+}
+
+int SpringBoneSimulator3D::get_joint_bone(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), -1);
+ Vector joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, joints.size(), -1);
+ return joints[p_joint]->bone;
+}
+
+void SpringBoneSimulator3D::set_joint_radius(int p_index, int p_joint, float p_radius) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (!is_config_individual(p_index)) {
+ return; // Joints are read-only.
+ }
+ Vector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, joints.size());
+ joints[p_joint]->radius = p_radius;
+}
+
+float SpringBoneSimulator3D::get_joint_radius(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ Vector joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, joints.size(), 0);
+ return joints[p_joint]->radius;
+}
+
+void SpringBoneSimulator3D::set_joint_stiffness(int p_index, int p_joint, float p_stiffness) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (!is_config_individual(p_index)) {
+ return; // Joints are read-only.
+ }
+ Vector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, joints.size());
+ joints[p_joint]->stiffness = p_stiffness;
+}
+
+float SpringBoneSimulator3D::get_joint_stiffness(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ Vector joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, joints.size(), 0);
+ return joints[p_joint]->stiffness;
+}
+
+void SpringBoneSimulator3D::set_joint_drag(int p_index, int p_joint, float p_drag) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (!is_config_individual(p_index)) {
+ return; // Joints are read-only.
+ }
+ Vector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, joints.size());
+ joints[p_joint]->drag = p_drag;
+}
+
+float SpringBoneSimulator3D::get_joint_drag(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ Vector joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, joints.size(), 0);
+ return joints[p_joint]->drag;
+}
+
+void SpringBoneSimulator3D::set_joint_gravity(int p_index, int p_joint, float p_gravity) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (!is_config_individual(p_index)) {
+ return; // Joints are read-only.
+ }
+ Vector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, joints.size());
+ joints[p_joint]->gravity = p_gravity;
+}
+
+float SpringBoneSimulator3D::get_joint_gravity(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ Vector joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, joints.size(), 0);
+ return joints[p_joint]->gravity;
+}
+
+void SpringBoneSimulator3D::set_joint_gravity_direction(int p_index, int p_joint, const Vector3 &p_gravity_direction) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ ERR_FAIL_COND(p_gravity_direction.is_zero_approx());
+ if (!is_config_individual(p_index)) {
+ return; // Joints are read-only.
+ }
+ Vector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, joints.size());
+ joints[p_joint]->gravity_direction = p_gravity_direction;
+}
+
+Vector3 SpringBoneSimulator3D::get_joint_gravity_direction(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), Vector3(0, -1, 0));
+ Vector joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, joints.size(), Vector3(0, -1, 0));
+ return joints[p_joint]->gravity_direction;
+}
+
+void SpringBoneSimulator3D::set_joint_rotation_axis(int p_index, int p_joint, RotationAxis p_axis) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (!is_config_individual(p_index)) {
+ return; // Joints are read-only.
+ }
+ Vector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, joints.size());
+ joints[p_joint]->rotation_axis = p_axis;
+}
+
+SpringBoneSimulator3D::RotationAxis SpringBoneSimulator3D::get_joint_rotation_axis(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), ROTATION_AXIS_ALL);
+ Vector joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, joints.size(), ROTATION_AXIS_ALL);
+ return joints[p_joint]->rotation_axis;
+}
+
+void SpringBoneSimulator3D::set_joint_count(int p_index, int p_count) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ ERR_FAIL_COND(p_count < 0);
+ Vector &joints = settings[p_index]->joints;
+ int delta = p_count - joints.size() + 1;
+ joints.resize(p_count);
+ if (delta > 1) {
+ for (int i = 1; i < delta; i++) {
+ joints.write[p_count - i] = memnew(SpringBone3DJointSetting);
+ }
+ }
+ notify_property_list_changed();
+}
+
+int SpringBoneSimulator3D::get_joint_count(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ Vector joints = settings[p_index]->joints;
+ return joints.size();
+}
+
+// Individual collisions.
+
+void SpringBoneSimulator3D::set_enable_all_child_collisions(int p_index, bool p_enabled) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->enable_all_child_collisions = p_enabled;
+ notify_property_list_changed();
+}
+
+bool SpringBoneSimulator3D::are_all_child_collisions_enabled(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), false);
+ return settings[p_index]->enable_all_child_collisions;
+}
+
+void SpringBoneSimulator3D::set_exclude_collision_path(int p_index, int p_collision, const NodePath &p_node_path) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (!are_all_child_collisions_enabled(p_index)) {
+ return; // Exclude collision list is disabled.
+ }
+ Vector &setting_exclude_collisions = settings[p_index]->exclude_collisions;
+ ERR_FAIL_INDEX(p_collision, setting_exclude_collisions.size());
+ setting_exclude_collisions.write[p_collision] = NodePath(); // Reset first.
+ if (is_inside_tree()) {
+ Node *node = get_node_or_null(p_node_path);
+ if (!node) {
+ _make_collisions_dirty();
+ return;
+ }
+ node = node->get_parent();
+ if (!node || node != this) {
+ _make_collisions_dirty();
+ ERR_FAIL_EDMSG("Collision must be child of current SpringBoneSimulator3D.");
+ }
+ }
+ setting_exclude_collisions.write[p_collision] = p_node_path;
+ _make_collisions_dirty();
+}
+
+NodePath SpringBoneSimulator3D::get_exclude_collision_path(int p_index, int p_collision) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), NodePath());
+ Vector setting_exclude_collisions = settings[p_index]->exclude_collisions;
+ ERR_FAIL_INDEX_V(p_collision, setting_exclude_collisions.size(), NodePath());
+ return setting_exclude_collisions[p_collision];
+}
+
+void SpringBoneSimulator3D::set_exclude_collision_count(int p_index, int p_count) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (!are_all_child_collisions_enabled(p_index)) {
+ return; // Exclude collision list is disabled.
+ }
+ Vector &setting_exclude_collisions = settings[p_index]->exclude_collisions;
+ setting_exclude_collisions.resize(p_count);
+ _make_collisions_dirty();
+ notify_property_list_changed();
+}
+
+int SpringBoneSimulator3D::get_exclude_collision_count(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ Vector setting_exclude_collisions = settings[p_index]->exclude_collisions;
+ return setting_exclude_collisions.size();
+}
+
+void SpringBoneSimulator3D::clear_exclude_collisions(int p_index) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (!are_all_child_collisions_enabled(p_index)) {
+ return; // Exclude collision list is disabled.
+ }
+ set_exclude_collision_count(p_index, 0);
+}
+
+void SpringBoneSimulator3D::set_collision_path(int p_index, int p_collision, const NodePath &p_node_path) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (are_all_child_collisions_enabled(p_index)) {
+ return; // Collision list is disabled.
+ }
+ Vector &setting_collisions = settings[p_index]->collisions;
+ ERR_FAIL_INDEX(p_collision, setting_collisions.size());
+ setting_collisions.write[p_collision] = NodePath(); // Reset first.
+ if (is_inside_tree()) {
+ Node *node = get_node_or_null(p_node_path);
+ if (!node) {
+ _make_collisions_dirty();
+ return;
+ }
+ node = node->get_parent();
+ if (!node || node != this) {
+ _make_collisions_dirty();
+ ERR_FAIL_EDMSG("Collision must be child of current SpringBoneSimulator3D.");
+ }
+ }
+ setting_collisions.write[p_collision] = p_node_path;
+ _make_collisions_dirty();
+}
+
+NodePath SpringBoneSimulator3D::get_collision_path(int p_index, int p_collision) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), NodePath());
+ Vector setting_collisions = settings[p_index]->collisions;
+ ERR_FAIL_INDEX_V(p_collision, setting_collisions.size(), NodePath());
+ return setting_collisions[p_collision];
+}
+
+void SpringBoneSimulator3D::set_collision_count(int p_index, int p_count) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (are_all_child_collisions_enabled(p_index)) {
+ return; // Collision list is disabled.
+ }
+ Vector &setting_collisions = settings[p_index]->collisions;
+ setting_collisions.resize(p_count);
+ _make_collisions_dirty();
+ notify_property_list_changed();
+}
+
+int SpringBoneSimulator3D::get_collision_count(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), 0);
+ Vector setting_collisions = settings[p_index]->collisions;
+ return setting_collisions.size();
+}
+
+void SpringBoneSimulator3D::clear_collisions(int p_index) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ if (are_all_child_collisions_enabled(p_index)) {
+ return; // Collision list is disabled.
+ }
+ set_collision_count(p_index, 0);
+}
+
+LocalVector SpringBoneSimulator3D::get_valid_collision_instance_ids(int p_index) {
+ ERR_FAIL_INDEX_V(p_index, settings.size(), LocalVector());
+ if (collisions_dirty) {
+ _find_collisions();
+ }
+ return settings[p_index]->cached_collisions;
+}
+
+void SpringBoneSimulator3D::_bind_methods() {
+ // Setting.
+ ClassDB::bind_method(D_METHOD("set_root_bone_name", "index", "bone_name"), &SpringBoneSimulator3D::set_root_bone_name);
+ ClassDB::bind_method(D_METHOD("get_root_bone_name", "index"), &SpringBoneSimulator3D::get_root_bone_name);
+ ClassDB::bind_method(D_METHOD("set_root_bone", "index", "bone"), &SpringBoneSimulator3D::set_root_bone);
+ ClassDB::bind_method(D_METHOD("get_root_bone", "index"), &SpringBoneSimulator3D::get_root_bone);
+
+ ClassDB::bind_method(D_METHOD("set_end_bone_name", "index", "bone_name"), &SpringBoneSimulator3D::set_end_bone_name);
+ ClassDB::bind_method(D_METHOD("get_end_bone_name", "index"), &SpringBoneSimulator3D::get_end_bone_name);
+ ClassDB::bind_method(D_METHOD("set_end_bone", "index", "bone"), &SpringBoneSimulator3D::set_end_bone);
+ ClassDB::bind_method(D_METHOD("get_end_bone", "index"), &SpringBoneSimulator3D::get_end_bone);
+
+ ClassDB::bind_method(D_METHOD("set_extend_end_bone", "index", "enabled"), &SpringBoneSimulator3D::set_extend_end_bone);
+ ClassDB::bind_method(D_METHOD("is_end_bone_extended", "index"), &SpringBoneSimulator3D::is_end_bone_extended);
+ ClassDB::bind_method(D_METHOD("set_end_bone_direction", "index", "bone_direction"), &SpringBoneSimulator3D::set_end_bone_direction);
+ ClassDB::bind_method(D_METHOD("get_end_bone_direction", "index"), &SpringBoneSimulator3D::get_end_bone_direction);
+ ClassDB::bind_method(D_METHOD("set_end_bone_length", "index", "length"), &SpringBoneSimulator3D::set_end_bone_length);
+ ClassDB::bind_method(D_METHOD("get_end_bone_length", "index"), &SpringBoneSimulator3D::get_end_bone_length);
+ ClassDB::bind_method(D_METHOD("set_end_bone_tip_radius", "index", "radius"), &SpringBoneSimulator3D::set_end_bone_tip_radius);
+ ClassDB::bind_method(D_METHOD("get_end_bone_tip_radius", "index"), &SpringBoneSimulator3D::get_end_bone_tip_radius);
+
+ ClassDB::bind_method(D_METHOD("set_center_from", "index", "center_from"), &SpringBoneSimulator3D::set_center_from);
+ ClassDB::bind_method(D_METHOD("get_center_from", "index"), &SpringBoneSimulator3D::get_center_from);
+ ClassDB::bind_method(D_METHOD("set_center_node", "index", "node_path"), &SpringBoneSimulator3D::set_center_node);
+ ClassDB::bind_method(D_METHOD("get_center_node", "index"), &SpringBoneSimulator3D::get_center_node);
+ ClassDB::bind_method(D_METHOD("set_center_bone_name", "index", "bone_name"), &SpringBoneSimulator3D::set_center_bone_name);
+ ClassDB::bind_method(D_METHOD("get_center_bone_name", "index"), &SpringBoneSimulator3D::get_center_bone_name);
+ ClassDB::bind_method(D_METHOD("set_center_bone", "index", "bone"), &SpringBoneSimulator3D::set_center_bone);
+ ClassDB::bind_method(D_METHOD("get_center_bone", "index"), &SpringBoneSimulator3D::get_center_bone);
+
+ ClassDB::bind_method(D_METHOD("set_radius", "index", "radius"), &SpringBoneSimulator3D::set_radius);
+ ClassDB::bind_method(D_METHOD("get_radius", "index"), &SpringBoneSimulator3D::get_radius);
+ ClassDB::bind_method(D_METHOD("set_rotation_axis", "index", "axis"), &SpringBoneSimulator3D::set_rotation_axis);
+ ClassDB::bind_method(D_METHOD("get_rotation_axis", "index"), &SpringBoneSimulator3D::get_rotation_axis);
+ ClassDB::bind_method(D_METHOD("set_radius_damping_curve", "index", "curve"), &SpringBoneSimulator3D::set_radius_damping_curve);
+ ClassDB::bind_method(D_METHOD("get_radius_damping_curve", "index"), &SpringBoneSimulator3D::get_radius_damping_curve);
+ ClassDB::bind_method(D_METHOD("set_stiffness", "index", "stiffness"), &SpringBoneSimulator3D::set_stiffness);
+ ClassDB::bind_method(D_METHOD("get_stiffness", "index"), &SpringBoneSimulator3D::get_stiffness);
+ ClassDB::bind_method(D_METHOD("set_stiffness_damping_curve", "index", "curve"), &SpringBoneSimulator3D::set_stiffness_damping_curve);
+ ClassDB::bind_method(D_METHOD("get_stiffness_damping_curve", "index"), &SpringBoneSimulator3D::get_stiffness_damping_curve);
+ ClassDB::bind_method(D_METHOD("set_drag", "index", "drag"), &SpringBoneSimulator3D::set_drag);
+ ClassDB::bind_method(D_METHOD("get_drag", "index"), &SpringBoneSimulator3D::get_drag);
+ ClassDB::bind_method(D_METHOD("set_drag_damping_curve", "index", "curve"), &SpringBoneSimulator3D::set_drag_damping_curve);
+ ClassDB::bind_method(D_METHOD("get_drag_damping_curve", "index"), &SpringBoneSimulator3D::get_drag_damping_curve);
+ ClassDB::bind_method(D_METHOD("set_gravity", "index", "gravity"), &SpringBoneSimulator3D::set_gravity);
+ ClassDB::bind_method(D_METHOD("get_gravity", "index"), &SpringBoneSimulator3D::get_gravity);
+ ClassDB::bind_method(D_METHOD("set_gravity_damping_curve", "index", "curve"), &SpringBoneSimulator3D::set_gravity_damping_curve);
+ ClassDB::bind_method(D_METHOD("get_gravity_damping_curve", "index"), &SpringBoneSimulator3D::get_gravity_damping_curve);
+ ClassDB::bind_method(D_METHOD("set_gravity_direction", "index", "gravity_direction"), &SpringBoneSimulator3D::set_gravity_direction);
+ ClassDB::bind_method(D_METHOD("get_gravity_direction", "index"), &SpringBoneSimulator3D::get_gravity_direction);
+
+ ClassDB::bind_method(D_METHOD("set_setting_count", "count"), &SpringBoneSimulator3D::set_setting_count);
+ ClassDB::bind_method(D_METHOD("get_setting_count"), &SpringBoneSimulator3D::get_setting_count);
+ ClassDB::bind_method(D_METHOD("clear_settings"), &SpringBoneSimulator3D::clear_settings);
+
+ // Individual joints.
+ ClassDB::bind_method(D_METHOD("set_individual_config", "index", "enabled"), &SpringBoneSimulator3D::set_individual_config);
+ ClassDB::bind_method(D_METHOD("is_config_individual", "index"), &SpringBoneSimulator3D::is_config_individual);
+
+ ClassDB::bind_method(D_METHOD("get_joint_bone_name", "index", "joint"), &SpringBoneSimulator3D::get_joint_bone_name);
+ ClassDB::bind_method(D_METHOD("get_joint_bone", "index", "joint"), &SpringBoneSimulator3D::get_joint_bone);
+ ClassDB::bind_method(D_METHOD("set_joint_rotation_axis", "index", "joint", "axis"), &SpringBoneSimulator3D::set_joint_rotation_axis);
+ ClassDB::bind_method(D_METHOD("get_joint_rotation_axis", "index", "joint"), &SpringBoneSimulator3D::get_joint_rotation_axis);
+ ClassDB::bind_method(D_METHOD("set_joint_radius", "index", "joint", "radius"), &SpringBoneSimulator3D::set_joint_radius);
+ ClassDB::bind_method(D_METHOD("get_joint_radius", "index", "joint"), &SpringBoneSimulator3D::get_joint_radius);
+ ClassDB::bind_method(D_METHOD("set_joint_stiffness", "index", "joint", "stiffness"), &SpringBoneSimulator3D::set_joint_stiffness);
+ ClassDB::bind_method(D_METHOD("get_joint_stiffness", "index", "joint"), &SpringBoneSimulator3D::get_joint_stiffness);
+ ClassDB::bind_method(D_METHOD("set_joint_drag", "index", "joint", "drag"), &SpringBoneSimulator3D::set_joint_drag);
+ ClassDB::bind_method(D_METHOD("get_joint_drag", "index", "joint"), &SpringBoneSimulator3D::get_joint_drag);
+ ClassDB::bind_method(D_METHOD("set_joint_gravity", "index", "joint", "gravity"), &SpringBoneSimulator3D::set_joint_gravity);
+ ClassDB::bind_method(D_METHOD("get_joint_gravity", "index", "joint"), &SpringBoneSimulator3D::get_joint_gravity);
+ ClassDB::bind_method(D_METHOD("set_joint_gravity_direction", "index", "joint", "gravity_direction"), &SpringBoneSimulator3D::set_joint_gravity_direction);
+ ClassDB::bind_method(D_METHOD("get_joint_gravity_direction", "index", "joint"), &SpringBoneSimulator3D::get_joint_gravity_direction);
+
+ ClassDB::bind_method(D_METHOD("get_joint_count", "index"), &SpringBoneSimulator3D::get_joint_count);
+
+ // Individual collisions.
+ ClassDB::bind_method(D_METHOD("set_enable_all_child_collisions", "index", "enabled"), &SpringBoneSimulator3D::set_enable_all_child_collisions);
+ ClassDB::bind_method(D_METHOD("are_all_child_collisions_enabled", "index"), &SpringBoneSimulator3D::are_all_child_collisions_enabled);
+
+ ClassDB::bind_method(D_METHOD("set_exclude_collision_path", "index", "collision", "node_path"), &SpringBoneSimulator3D::set_exclude_collision_path);
+ ClassDB::bind_method(D_METHOD("get_exclude_collision_path", "index", "collision"), &SpringBoneSimulator3D::get_exclude_collision_path);
+
+ ClassDB::bind_method(D_METHOD("set_exclude_collision_count", "index", "count"), &SpringBoneSimulator3D::set_exclude_collision_count);
+ ClassDB::bind_method(D_METHOD("get_exclude_collision_count", "index"), &SpringBoneSimulator3D::get_exclude_collision_count);
+ ClassDB::bind_method(D_METHOD("clear_exclude_collisions", "index"), &SpringBoneSimulator3D::clear_exclude_collisions);
+
+ ClassDB::bind_method(D_METHOD("set_collision_path", "index", "collision", "node_path"), &SpringBoneSimulator3D::set_collision_path);
+ ClassDB::bind_method(D_METHOD("get_collision_path", "index", "collision"), &SpringBoneSimulator3D::get_collision_path);
+
+ ClassDB::bind_method(D_METHOD("set_collision_count", "index", "count"), &SpringBoneSimulator3D::set_collision_count);
+ ClassDB::bind_method(D_METHOD("get_collision_count", "index"), &SpringBoneSimulator3D::get_collision_count);
+ ClassDB::bind_method(D_METHOD("clear_collisions", "index"), &SpringBoneSimulator3D::clear_collisions);
+
+ // To process manually.
+ ClassDB::bind_method(D_METHOD("reset"), &SpringBoneSimulator3D::reset);
+
+ ADD_ARRAY_COUNT("Settings", "setting_count", "set_setting_count", "get_setting_count", "settings/");
+
+ BIND_ENUM_CONSTANT(BONE_DIRECTION_PLUS_X);
+ BIND_ENUM_CONSTANT(BONE_DIRECTION_MINUS_X);
+ BIND_ENUM_CONSTANT(BONE_DIRECTION_PLUS_Y);
+ BIND_ENUM_CONSTANT(BONE_DIRECTION_MINUS_Y);
+ BIND_ENUM_CONSTANT(BONE_DIRECTION_PLUS_Z);
+ BIND_ENUM_CONSTANT(BONE_DIRECTION_MINUS_Z);
+ BIND_ENUM_CONSTANT(BONE_DIRECTION_FROM_PARENT);
+
+ BIND_ENUM_CONSTANT(CENTER_FROM_WORLD_ORIGIN);
+ BIND_ENUM_CONSTANT(CENTER_FROM_NODE);
+ BIND_ENUM_CONSTANT(CENTER_FROM_BONE);
+
+ BIND_ENUM_CONSTANT(ROTATION_AXIS_X);
+ BIND_ENUM_CONSTANT(ROTATION_AXIS_Y);
+ BIND_ENUM_CONSTANT(ROTATION_AXIS_Z);
+ BIND_ENUM_CONSTANT(ROTATION_AXIS_ALL);
+}
+
+void SpringBoneSimulator3D::_make_joints_dirty(int p_index) {
+ ERR_FAIL_INDEX(p_index, settings.size());
+ settings[p_index]->joints_dirty = true;
+ if (joints_dirty) {
+ return;
+ }
+ joints_dirty = true;
+ callable_mp(this, &SpringBoneSimulator3D::_update_joints).call_deferred();
+}
+
+void SpringBoneSimulator3D::_make_all_joints_dirty() {
+ for (int i = 0; i < settings.size(); i++) {
+ _update_joint_array(i);
+ }
+}
+
+void SpringBoneSimulator3D::add_child_notify(Node *p_child) {
+ if (Object::cast_to(p_child)) {
+ _make_collisions_dirty();
+ }
+}
+
+void SpringBoneSimulator3D::move_child_notify(Node *p_child) {
+ if (Object::cast_to(p_child)) {
+ _make_collisions_dirty();
+ }
+}
+
+void SpringBoneSimulator3D::remove_child_notify(Node *p_child) {
+ if (Object::cast_to(p_child)) {
+ _make_collisions_dirty();
+ }
+}
+
+void SpringBoneSimulator3D::_find_collisions() {
+ if (!collisions_dirty) {
+ return;
+ }
+ collisions.clear();
+ for (int i = 0; i < get_child_count(); i++) {
+ SpringBoneCollision3D *c = Object::cast_to(get_child(i));
+ if (c) {
+ collisions.push_back(c->get_instance_id());
+ }
+ }
+
+ bool setting_updated = false;
+
+ for (int i = 0; i < settings.size(); i++) {
+ LocalVector &cache = settings[i]->cached_collisions;
+ cache.clear();
+ if (!settings[i]->enable_all_child_collisions) {
+ // Allow list.
+ Vector &setting_collisions = settings[i]->collisions;
+ for (int j = 0; j < setting_collisions.size(); j++) {
+ Node *n = get_node_or_null(setting_collisions[j]);
+ if (!n) {
+ continue;
+ }
+ ObjectID id = n->get_instance_id();
+ if (!collisions.has(id)) {
+ setting_collisions.write[j] = NodePath(); // Clear path if not found.
+ } else {
+ cache.push_back(id);
+ }
+ }
+ } else {
+ // Deny list.
+ LocalVector masks;
+ Vector &setting_exclude_collisions = settings[i]->exclude_collisions;
+ for (int j = 0; j < setting_exclude_collisions.size(); j++) {
+ Node *n = get_node_or_null(setting_exclude_collisions[j]);
+ if (!n) {
+ continue;
+ }
+ ObjectID id = n->get_instance_id();
+ int find = collisions.find(id);
+ if (find < 0) {
+ setting_exclude_collisions.write[j] = NodePath(); // Clear path if not found.
+ } else {
+ masks.push_back((uint32_t)find);
+ }
+ }
+ uint32_t mask_index = 0;
+ for (uint32_t j = 0; j < collisions.size(); j++) {
+ if (mask_index < masks.size() && j == masks[mask_index]) {
+ mask_index++;
+ continue;
+ }
+ cache.push_back(collisions[j]);
+ }
+ }
+ }
+
+ collisions_dirty = false;
+
+ if (setting_updated) {
+ notify_property_list_changed();
+ }
+}
+
+void SpringBoneSimulator3D::_process_collisions() {
+ for (const ObjectID &oid : collisions) {
+ Object *t_obj = ObjectDB::get_instance(oid);
+ if (!t_obj) {
+ continue;
+ }
+ SpringBoneCollision3D *col = Object::cast_to(t_obj);
+ if (!col) {
+ continue;
+ }
+ col->sync_pose();
+ }
+}
+
+void SpringBoneSimulator3D::_make_collisions_dirty() {
+ collisions_dirty = true;
+}
+
+void SpringBoneSimulator3D::_update_joint_array(int p_index) {
+ _make_joints_dirty(p_index);
+ set_joint_count(p_index, 0);
+
+ Skeleton3D *sk = get_skeleton();
+ int current_bone = settings[p_index]->end_bone;
+ int root_bone = settings[p_index]->root_bone;
+ if (!sk || current_bone < 0 || root_bone < 0) {
+ return;
+ }
+
+ // Validation.
+ bool valid = false;
+ while (current_bone >= 0) {
+ if (current_bone == root_bone) {
+ valid = true;
+ break;
+ }
+ current_bone = sk->get_bone_parent(current_bone);
+ }
+ ERR_FAIL_COND_EDMSG(!valid, "End bone must be the same as or a child of root bone.");
+
+ Vector new_joints;
+ current_bone = settings[p_index]->end_bone;
+ while (current_bone != root_bone) {
+ new_joints.push_back(current_bone);
+ current_bone = sk->get_bone_parent(current_bone);
+ }
+ new_joints.push_back(current_bone);
+ new_joints.reverse();
+
+ set_joint_count(p_index, new_joints.size());
+ for (int i = 0; i < new_joints.size(); i++) {
+ set_joint_bone(p_index, i, new_joints[i]);
+ }
+}
+
+void SpringBoneSimulator3D::_update_joints() {
+ if (!joints_dirty) {
+ return;
+ }
+ for (int i = 0; i < settings.size(); i++) {
+ if (!settings[i]->joints_dirty) {
+ continue;
+ }
+ if (settings[i]->individual_config) {
+ settings[i]->joints_dirty = false;
+ continue; // Abort.
+ }
+ Vector &joints = settings[i]->joints;
+ float unit = joints.size() > 0 ? (1.0 / float(joints.size() - 1)) : 0.0;
+ for (int j = 0; j < joints.size(); j++) {
+ float offset = j * unit;
+
+ if (settings[i]->radius_damping_curve.is_valid()) {
+ joints[j]->radius = settings[i]->radius * settings[i]->radius_damping_curve->sample_baked(offset);
+ } else {
+ joints[j]->radius = settings[i]->radius;
+ }
+
+ if (settings[i]->stiffness_damping_curve.is_valid()) {
+ joints[j]->stiffness = settings[i]->stiffness * settings[i]->stiffness_damping_curve->sample_baked(offset);
+ } else {
+ joints[j]->stiffness = settings[i]->stiffness;
+ }
+
+ if (settings[i]->drag_damping_curve.is_valid()) {
+ joints[j]->drag = settings[i]->drag * settings[i]->drag_damping_curve->sample_baked(offset);
+ } else {
+ joints[j]->drag = settings[i]->drag;
+ }
+
+ if (settings[i]->gravity_damping_curve.is_valid()) {
+ joints[j]->gravity = settings[i]->gravity * settings[i]->gravity_damping_curve->sample_baked(offset);
+ } else {
+ joints[j]->gravity = settings[i]->gravity;
+ }
+
+ joints[j]->gravity_direction = settings[i]->gravity_direction;
+ joints[j]->rotation_axis = settings[i]->rotation_axis;
+ }
+ settings[i]->simulation_dirty = true;
+ settings[i]->joints_dirty = false;
+ }
+ joints_dirty = false;
+#ifdef TOOLS_ENABLED
+ update_gizmos();
+#endif // TOOLS_ENABLED
+}
+
+void SpringBoneSimulator3D::_set_active(bool p_active) {
+ if (p_active) {
+ reset();
+ }
+}
+
+void SpringBoneSimulator3D::_process_modification() {
+ Skeleton3D *skeleton = get_skeleton();
+ if (!skeleton) {
+ return;
+ }
+ _find_collisions();
+ _process_collisions();
+ double delta = skeleton->get_modifier_callback_mode_process() == Skeleton3D::MODIFIER_CALLBACK_MODE_PROCESS_IDLE ? skeleton->get_process_delta_time() : skeleton->get_physics_process_delta_time();
+ for (int i = 0; i < settings.size(); i++) {
+ _init_joints(skeleton, settings[i]);
+ _process_joints(delta, skeleton, settings[i]->joints, get_valid_collision_instance_ids(i), settings[i]->cached_center, settings[i]->cached_inverted_center, settings[i]->cached_inverted_center.basis.get_rotation_quaternion());
+ }
+}
+
+void SpringBoneSimulator3D::reset() {
+ Skeleton3D *skeleton = get_skeleton();
+ if (!skeleton) {
+ return;
+ }
+ _find_collisions();
+ _process_collisions();
+ for (int i = 0; i < settings.size(); i++) {
+ settings[i]->simulation_dirty = true;
+ _init_joints(skeleton, settings[i]);
+ }
+}
+
+void SpringBoneSimulator3D::_init_joints(Skeleton3D *p_skeleton, SpringBone3DSetting *setting) {
+ if (setting->center_from == CENTER_FROM_WORLD_ORIGIN) {
+ setting->cached_center = p_skeleton->get_global_transform();
+ } else if (setting->center_from == CENTER_FROM_NODE) {
+ if (setting->center_node == NodePath()) {
+ setting->cached_center = Transform3D();
+ } else {
+ Node3D *nd = Object::cast_to(get_node_or_null(setting->center_node));
+ if (!nd) {
+ setting->cached_center = Transform3D();
+ } else {
+ setting->cached_center = nd->get_global_transform().affine_inverse() * p_skeleton->get_global_transform();
+ }
+ }
+ } else {
+ if (setting->center_bone >= 0) {
+ setting->cached_center = p_skeleton->get_bone_global_pose(setting->center_bone);
+ } else {
+ setting->cached_center = Transform3D();
+ }
+ }
+ setting->cached_inverted_center = setting->cached_center.affine_inverse();
+
+ if (!setting->simulation_dirty) {
+ return;
+ }
+ for (int i = 0; i < setting->joints.size(); i++) {
+ if (setting->joints[i]->verlet) {
+ memdelete(setting->joints[i]->verlet);
+ setting->joints[i]->verlet = nullptr;
+ }
+ if (i < setting->joints.size() - 1) {
+ setting->joints[i]->verlet = memnew(SpringBone3DVerletInfo);
+ Vector3 axis = p_skeleton->get_bone_rest(setting->joints[i + 1]->bone).origin;
+ setting->joints[i]->verlet->current_tail = setting->cached_center.xform(p_skeleton->get_bone_global_pose(setting->joints[i]->bone).xform(axis));
+ setting->joints[i]->verlet->prev_tail = setting->joints[i]->verlet->current_tail;
+ setting->joints[i]->verlet->forward_vector = axis.normalized();
+ setting->joints[i]->verlet->length = axis.length();
+ } else if (setting->extend_end_bone && setting->end_bone_length > 0) {
+ Vector3 axis = get_end_bone_axis(setting->end_bone, setting->end_bone_direction);
+ if (axis.is_zero_approx()) {
+ continue;
+ }
+ setting->joints[i]->verlet = memnew(SpringBone3DVerletInfo);
+ setting->joints[i]->verlet->forward_vector = axis;
+ setting->joints[i]->verlet->length = setting->end_bone_length;
+ setting->joints[i]->verlet->current_tail = setting->cached_center.xform(p_skeleton->get_bone_global_pose(setting->joints[i]->bone).xform(axis * setting->end_bone_length));
+ setting->joints[i]->verlet->prev_tail = setting->joints[i]->verlet->current_tail;
+ }
+ }
+ setting->simulation_dirty = false;
+}
+
+Vector3 SpringBoneSimulator3D::snap_position_to_plane(const Transform3D &p_rest, RotationAxis p_axis, const Vector3 &p_position) {
+ if (p_axis == ROTATION_AXIS_ALL) {
+ return p_position;
+ }
+ Vector3 result = p_position;
+ result = p_rest.affine_inverse().xform(result);
+ result[(int)p_axis] = 0;
+ result = p_rest.xform(result);
+ return result;
+}
+
+Vector3 SpringBoneSimulator3D::limit_length(const Vector3 &p_origin, const Vector3 &p_destination, float p_length) {
+ return p_origin + (p_destination - p_origin).normalized() * p_length;
+}
+
+void SpringBoneSimulator3D::_process_joints(double p_delta, Skeleton3D *p_skeleton, Vector &p_joints, const LocalVector &p_collisions, const Transform3D &p_center_transform, const Transform3D &p_inverted_center_transform, const Quaternion &p_inverted_center_rotation) {
+ for (int i = 0; i < p_joints.size(); i++) {
+ SpringBone3DVerletInfo *verlet = p_joints[i]->verlet;
+ if (!verlet) {
+ continue; // Means not extended end bone.
+ }
+ Transform3D current_global_pose = p_skeleton->get_bone_global_pose(p_joints[i]->bone);
+ Transform3D current_world_pose = p_center_transform * current_global_pose;
+ Quaternion current_rot = current_global_pose.basis.get_rotation_quaternion();
+ Vector3 current_origin = p_center_transform.xform(current_global_pose.origin);
+ Vector3 external = p_inverted_center_rotation.xform(p_joints[i]->gravity_direction * p_joints[i]->gravity * p_delta);
+
+ // Integration of velocity by verlet.
+ Vector3 next_tail = verlet->current_tail +
+ (verlet->current_tail - verlet->prev_tail) * (1.0 - p_joints[i]->drag) +
+ p_center_transform.basis.get_rotation_quaternion().xform(current_rot.xform(verlet->forward_vector * (p_joints[i]->stiffness * p_delta)) + external);
+ // Snap to plane if axis locked.
+ next_tail = snap_position_to_plane(current_world_pose, p_joints[i]->rotation_axis, next_tail);
+ // Limit bone length.
+ next_tail = limit_length(current_origin, next_tail, verlet->length);
+
+ // Collision movement.
+ for (uint32_t j = 0; j < p_collisions.size(); j++) {
+ Object *obj = ObjectDB::get_instance(p_collisions[j]);
+ if (!obj) {
+ continue;
+ }
+ SpringBoneCollision3D *col = Object::cast_to(obj);
+ if (col) {
+ // Collider movement should separate from the effect of the center.
+ next_tail = col->collide(p_center_transform, p_joints[i]->radius, verlet->length, next_tail);
+ // Snap to plane if axis locked.
+ next_tail = snap_position_to_plane(current_world_pose, p_joints[i]->rotation_axis, next_tail);
+ // Limit bone length.
+ next_tail = limit_length(current_origin, next_tail, verlet->length);
+ }
+ }
+
+ // Store current tails for next process.
+ verlet->prev_tail = verlet->current_tail;
+ verlet->current_tail = next_tail;
+
+ // Apply rotation.
+ Vector3 from = current_rot.xform(verlet->forward_vector);
+ Vector3 to = p_inverted_center_transform.basis.xform(next_tail - current_origin).normalized();
+ Quaternion from_to = get_from_to_rotation(from, to);
+ from_to *= current_rot;
+ from_to = get_local_pose_rotation(p_skeleton, p_joints[i]->bone, from_to);
+ p_skeleton->set_bone_pose_rotation(p_joints[i]->bone, from_to);
+ }
+}
+
+Quaternion SpringBoneSimulator3D::get_local_pose_rotation(Skeleton3D *p_skeleton, int p_bone, const Quaternion &p_global_pose_rotation) {
+ int parent = p_skeleton->get_bone_parent(p_bone);
+ if (parent < 0) {
+ return p_global_pose_rotation;
+ }
+ return p_skeleton->get_bone_global_pose(parent).basis.orthonormalized().inverse() * p_global_pose_rotation;
+}
+
+Quaternion SpringBoneSimulator3D::get_from_to_rotation(const Vector3 &p_from, const Vector3 &p_to) {
+ Vector3 axis = p_from.cross(p_to);
+ if (axis.is_zero_approx()) {
+ return Quaternion(0, 0, 0, 1);
+ }
+ float angle = p_from.angle_to(p_to);
+ if (Math::is_zero_approx(angle)) {
+ angle = 0.0;
+ }
+ return Quaternion(axis.normalized(), angle);
+}
diff --git a/scene/3d/spring_bone_simulator_3d.h b/scene/3d/spring_bone_simulator_3d.h
new file mode 100644
index 00000000000..9c9aa913736
--- /dev/null
+++ b/scene/3d/spring_bone_simulator_3d.h
@@ -0,0 +1,281 @@
+/**************************************************************************/
+/* spring_bone_simulator_3d.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. */
+/**************************************************************************/
+
+#ifndef SPRING_BONE_SIMULATOR_3D_H
+#define SPRING_BONE_SIMULATOR_3D_H
+
+#include "scene/3d/skeleton_modifier_3d.h"
+
+class SpringBoneSimulator3D : public SkeletonModifier3D {
+ GDCLASS(SpringBoneSimulator3D, SkeletonModifier3D);
+
+ bool joints_dirty = false;
+
+ LocalVector collisions; // To process collisions for sync position with skeleton.
+ bool collisions_dirty = false;
+ void _find_collisions();
+ void _process_collisions();
+ void _make_collisions_dirty();
+
+public:
+ enum BoneDirection {
+ BONE_DIRECTION_PLUS_X,
+ BONE_DIRECTION_MINUS_X,
+ BONE_DIRECTION_PLUS_Y,
+ BONE_DIRECTION_MINUS_Y,
+ BONE_DIRECTION_PLUS_Z,
+ BONE_DIRECTION_MINUS_Z,
+ BONE_DIRECTION_FROM_PARENT,
+ };
+
+ enum CenterFrom {
+ CENTER_FROM_WORLD_ORIGIN,
+ CENTER_FROM_NODE,
+ CENTER_FROM_BONE,
+ };
+
+ enum RotationAxis {
+ ROTATION_AXIS_X,
+ ROTATION_AXIS_Y,
+ ROTATION_AXIS_Z,
+ ROTATION_AXIS_ALL,
+ };
+
+ struct SpringBone3DVerletInfo {
+ Vector3 prev_tail;
+ Vector3 current_tail;
+ Vector3 forward_vector;
+ float length = 0.0;
+ };
+
+ struct SpringBone3DJointSetting {
+ String bone_name;
+ int bone = -1;
+
+ RotationAxis rotation_axis = ROTATION_AXIS_ALL;
+ float radius = 0.1;
+ float stiffness = 1.0;
+ float drag = 0.0;
+ float gravity = 0.0;
+ Vector3 gravity_direction = Vector3(0, -1, 0);
+
+ // To process.
+ SpringBone3DVerletInfo *verlet = nullptr;
+ };
+
+ struct SpringBone3DSetting {
+ bool joints_dirty = false;
+
+ String root_bone_name;
+ int root_bone = -1;
+
+ String end_bone_name;
+ int end_bone = -1;
+
+ // To make virtual end joint.
+ bool extend_end_bone = false;
+ BoneDirection end_bone_direction = BONE_DIRECTION_FROM_PARENT;
+ float end_bone_length = 0.0;
+ float end_bone_tip_radius = 0.02;
+
+ CenterFrom center_from = CENTER_FROM_WORLD_ORIGIN;
+ NodePath center_node;
+ String center_bone_name;
+ int center_bone = -1;
+
+ // Cache into joints.
+ bool individual_config = false;
+ float radius = 0.02;
+ Ref radius_damping_curve;
+ float stiffness = 1.0;
+ Ref stiffness_damping_curve;
+ float drag = 0.4;
+ Ref drag_damping_curve;
+ float gravity = 0.0;
+ Ref gravity_damping_curve;
+ Vector3 gravity_direction = Vector3(0, -1, 0);
+ RotationAxis rotation_axis = ROTATION_AXIS_ALL;
+ Vector joints;
+
+ // Cache into collisions.
+ bool enable_all_child_collisions = true;
+ Vector collisions;
+ Vector exclude_collisions;
+ LocalVector cached_collisions;
+
+ // To process.
+ bool simulation_dirty = false;
+ Transform3D cached_center;
+ Transform3D cached_inverted_center;
+ };
+
+protected:
+ Vector settings;
+
+ bool _get(const StringName &p_path, Variant &r_ret) const;
+ bool _set(const StringName &p_path, const Variant &p_value);
+ void _get_property_list(List *p_list) const;
+ void _validate_property(PropertyInfo &p_property) const;
+
+ void _notification(int p_what);
+
+ static void _bind_methods();
+
+ virtual void _set_active(bool p_active) override;
+ virtual void _process_modification() override;
+ void _init_joints(Skeleton3D *p_skeleton, SpringBone3DSetting *p_setting);
+ void _process_joints(double p_delta, Skeleton3D *p_skeleton, Vector &p_joints, const LocalVector &p_collisions, const Transform3D &p_center_transform, const Transform3D &p_inverted_center_transform, const Quaternion &p_inverted_center_rotation);
+
+ void _make_joints_dirty(int p_index);
+ void _make_all_joints_dirty();
+
+ void _update_joint_array(int p_index);
+ void _update_joints();
+
+ virtual void add_child_notify(Node *p_child) override;
+ virtual void move_child_notify(Node *p_child) override;
+ virtual void remove_child_notify(Node *p_child) override;
+
+public:
+ // Setting.
+ void set_root_bone_name(int p_index, const String &p_bone_name);
+ String get_root_bone_name(int p_index) const;
+ void set_root_bone(int p_index, int p_bone);
+ int get_root_bone(int p_index) const;
+
+ void set_end_bone_name(int p_index, const String &p_bone_name);
+ String get_end_bone_name(int p_index) const;
+ void set_end_bone(int p_index, int p_bone);
+ int get_end_bone(int p_index) const;
+
+ void set_extend_end_bone(int p_index, bool p_enabled);
+ bool is_end_bone_extended(int p_index) const;
+ void set_end_bone_direction(int p_index, BoneDirection p_bone_direction);
+ BoneDirection get_end_bone_direction(int p_index) const;
+ void set_end_bone_length(int p_index, float p_length);
+ float get_end_bone_length(int p_index) const;
+ void set_end_bone_tip_radius(int p_index, float p_radius);
+ float get_end_bone_tip_radius(int p_index) const;
+ Vector3 get_end_bone_axis(int p_end_bone, BoneDirection p_direction) const; // Helper.
+
+ void set_center_from(int p_index, CenterFrom p_center_from);
+ CenterFrom get_center_from(int p_index) const;
+ void set_center_node(int p_index, const NodePath &p_node_path);
+ NodePath get_center_node(int p_index) const;
+ void set_center_bone_name(int p_index, const String &p_bone_name);
+ String get_center_bone_name(int p_index) const;
+ void set_center_bone(int p_index, int p_bone);
+ int get_center_bone(int p_index) const;
+
+ void set_rotation_axis(int p_index, RotationAxis p_axis);
+ RotationAxis get_rotation_axis(int p_index) const;
+ void set_radius(int p_index, float p_radius);
+ float get_radius(int p_index) const;
+ void set_radius_damping_curve(int p_index, const Ref &p_damping_curve);
+ Ref get_radius_damping_curve(int p_index) const;
+ void set_stiffness(int p_index, float p_stiffness);
+ float get_stiffness(int p_index) const;
+ void set_stiffness_damping_curve(int p_index, const Ref &p_damping_curve);
+ Ref get_stiffness_damping_curve(int p_index) const;
+ void set_drag(int p_index, float p_drag);
+ float get_drag(int p_index) const;
+ void set_drag_damping_curve(int p_index, const Ref &p_damping_curve);
+ Ref get_drag_damping_curve(int p_index) const;
+ void set_gravity(int p_index, float p_gravity);
+ float get_gravity(int p_index) const;
+ void set_gravity_damping_curve(int p_index, const Ref &p_damping_curve);
+ Ref get_gravity_damping_curve(int p_index) const;
+ void set_gravity_direction(int p_index, const Vector3 &p_gravity_direction);
+ Vector3 get_gravity_direction(int p_index) const;
+
+ void set_setting_count(int p_count);
+ int get_setting_count() const;
+ void clear_settings();
+
+ // Individual joints.
+ void set_individual_config(int p_index, bool p_enabled);
+ bool is_config_individual(int p_index) const;
+
+ void set_joint_bone_name(int p_index, int p_joint, const String &p_bone_name);
+ String get_joint_bone_name(int p_index, int p_joint) const;
+ void set_joint_bone(int p_index, int p_joint, int p_bone);
+ int get_joint_bone(int p_index, int p_joint) const;
+
+ void set_joint_rotation_axis(int p_index, int p_joint, RotationAxis p_axis);
+ RotationAxis get_joint_rotation_axis(int p_index, int p_joint) const;
+ void set_joint_radius(int p_index, int p_joint, float p_radius);
+ float get_joint_radius(int p_index, int p_joint) const;
+ void set_joint_stiffness(int p_index, int p_joint, float p_stiffness);
+ float get_joint_stiffness(int p_index, int p_joint) const;
+ void set_joint_drag(int p_index, int p_joint, float p_drag);
+ float get_joint_drag(int p_index, int p_joint) const;
+ void set_joint_gravity(int p_index, int p_joint, float p_gravity);
+ float get_joint_gravity(int p_index, int p_joint) const;
+ void set_joint_gravity_direction(int p_index, int p_joint, const Vector3 &p_gravity_direction);
+ Vector3 get_joint_gravity_direction(int p_index, int p_joint) const;
+
+ void set_joint_count(int p_index, int p_count);
+ int get_joint_count(int p_index) const;
+
+ // Individual collisions.
+ void set_enable_all_child_collisions(int p_index, bool p_enabled);
+ bool are_all_child_collisions_enabled(int p_index) const;
+
+ void set_exclude_collision_path(int p_index, int p_collision, const NodePath &p_node_path);
+ NodePath get_exclude_collision_path(int p_index, int p_collision) const;
+
+ void set_exclude_collision_count(int p_index, int p_count);
+ int get_exclude_collision_count(int p_index) const;
+ void clear_exclude_collisions(int p_index);
+
+ void set_collision_path(int p_index, int p_collision, const NodePath &p_node_path);
+ NodePath get_collision_path(int p_index, int p_collision) const;
+
+ void set_collision_count(int p_index, int p_count);
+ int get_collision_count(int p_index) const;
+ void clear_collisions(int p_index);
+
+ LocalVector get_valid_collision_instance_ids(int p_index);
+
+ // Helper.
+ static Quaternion get_local_pose_rotation(Skeleton3D *p_skeleton, int p_bone, const Quaternion &p_global_pose_rotation);
+ static Quaternion get_from_to_rotation(const Vector3 &p_from, const Vector3 &p_to);
+ static Vector3 snap_position_to_plane(const Transform3D &p_rest, RotationAxis p_axis, const Vector3 &p_position);
+ static Vector3 limit_length(const Vector3 &p_origin, const Vector3 &p_destination, float p_length);
+
+ // To process manually.
+ void reset();
+};
+
+VARIANT_ENUM_CAST(SpringBoneSimulator3D::BoneDirection);
+VARIANT_ENUM_CAST(SpringBoneSimulator3D::CenterFrom);
+VARIANT_ENUM_CAST(SpringBoneSimulator3D::RotationAxis);
+
+#endif // SPRING_BONE_SIMULATOR_3D_H
diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp
index 53d8043329c..97a878a96be 100644
--- a/scene/register_scene_types.cpp
+++ b/scene/register_scene_types.cpp
@@ -284,6 +284,11 @@
#include "scene/3d/skeleton_ik_3d.h"
#include "scene/3d/skeleton_modifier_3d.h"
#include "scene/3d/soft_body_3d.h"
+#include "scene/3d/spring_bone_collision_3d.h"
+#include "scene/3d/spring_bone_collision_capsule_3d.h"
+#include "scene/3d/spring_bone_collision_plane_3d.h"
+#include "scene/3d/spring_bone_collision_sphere_3d.h"
+#include "scene/3d/spring_bone_simulator_3d.h"
#include "scene/3d/sprite_3d.h"
#include "scene/3d/visible_on_screen_notifier_3d.h"
#include "scene/3d/voxel_gi.h"
@@ -600,6 +605,11 @@ void register_scene_types() {
GDREGISTER_CLASS(RootMotionView);
GDREGISTER_VIRTUAL_CLASS(SkeletonModifier3D);
GDREGISTER_CLASS(RetargetModifier3D);
+ GDREGISTER_CLASS(SpringBoneSimulator3D);
+ GDREGISTER_VIRTUAL_CLASS(SpringBoneCollision3D);
+ GDREGISTER_CLASS(SpringBoneCollisionSphere3D);
+ GDREGISTER_CLASS(SpringBoneCollisionCapsule3D);
+ GDREGISTER_CLASS(SpringBoneCollisionPlane3D);
OS::get_singleton()->yield(); // may take time to init