Repository: godotengine/godot (language: cpp)

Issue:
Add SpringBone3D node
Add SpringBone3D node to engine, to simulate physics like hair swinging when moving.

![hair](https://github.com/godotengine/godot/assets/15343372/38178b5c-2ed1-4fd7-abc6-affed8f50da6)

* *Bugsquad edit, closes: https://github.com/godotengine/godot-proposals/issues/8374*


Discussion / hints:
Commits need to be squashed (see https://docs.godotengine.org/en/latest/contributing/workflow/pr_workflow.html#modifying-a-pull-request)
> Commits need to be squashed (see https://docs.godotengine.org/en/latest/contributing/workflow/pr_workflow.html#modifying-a-pull-request)

Done
CI failed: ```doc/classes/SpringBone3D.xml:9: element methods: Schemas validity error : Element 'methods': This element is not expected. Expected is ( tutorials ).```

I would suggest to use a ```godot.exe --doctool ..``` command to fix this.
> CI failed: `doc/classes/SpringBone3D.xml:9: element methods: Schemas validity error : Element 'methods': This element is not expected. Expected is ( tutorials ).`
> 
> I would suggest to use a `godot.exe --doctool ..` command to fix this.

Thanks for reminding! It has been done.
@lyuma we also have the godot-vrm spring bone. Someone needs to attach the few related proposals to this pr description as per pr guidelines and also merge the differences into one pr. We are waiting for the 4.2 feature freeze to end. 
Thanks for opening a pull request :slightly_smiling_face: 

Feature pull requests should be associated to a *feature proposal* to justify the need for implementing the feature in Godot core. Please open a proposal on the [Godot proposals](https://github.com/godotengine/godot-proposals) repository (and link to this pull request in the proposal's body).

**Edit:** Proposal opened: https://github.com/godotengine/godot-proposals/issues/8374
The contribution period for 4.3 is open, maybe we can try to get this ready for 4.3
@fire @lyuma What is the comparison of the features between this PR and Lyuma's VRM spring bone? https://github.com/V-Sekai/godot-vrm/blob/master/addons/vrm/vrm_spring_bone.gd
I noticed processing is performed in physics process, which means it'll look choppy at high FPS as physics ticks are 60 Hz by default. Should there be an option to process SpringBone3D on the process step (formerly "idle"), so that it looks smooth at any framerate?

Alternatively, physics interpolation would need to handle SpringBone3D (or the node itself could take care of interpolation, like GPUParticles3D does).
Superseded by: https://github.com/godotengine/godot/pull/101409

Fix patch:
diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index 7aad56f89c81..fcabcf14600f 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -480,6 +480,15 @@
 		<member name="editors/3d_gizmos/gizmo_colors/skeleton" type="Color" setter="" getter="">
 			The 3D editor gizmo color used for [Skeleton3D] nodes.
 		</member>
+		<member name="editors/3d_gizmos/gizmo_colors/spring_bone_collision" type="Color" setter="" getter="">
+			The 3D editor gizmo color used for [SpringBoneCollision3D] nodes.
+		</member>
+		<member name="editors/3d_gizmos/gizmo_colors/spring_bone_inside_collision" type="Color" setter="" getter="">
+			The 3D editor gizmo color used for [SpringBoneCollision3D] nodes with inside mode.
+		</member>
+		<member name="editors/3d_gizmos/gizmo_colors/spring_bone_joint" type="Color" setter="" getter="">
+			The 3D editor gizmo color used for [SpringBoneSimulator3D] nodes.
+		</member>
 		<member name="editors/3d_gizmos/gizmo_colors/stream_player_3d" type="Color" setter="" getter="">
 			The 3D editor gizmo color used for [AudioStreamPlayer3D]'s emission angle.
 		</member>
diff --git a/doc/classes/SpringBoneCollision3D.xml b/doc/classes/SpringBoneCollision3D.xml
new file mode 100644
index 000000000000..ea4556674cf7
--- /dev/null
+++ b/doc/classes/SpringBoneCollision3D.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="SpringBoneCollision3D" inherits="Node3D" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		A base class of the collision that interacts with [SpringBoneSimulator3D].
+	</brief_description>
+	<description>
+		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.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_skeleton" qualifiers="const">
+			<return type="Skeleton3D" />
+			<description>
+				Get parent [Skeleton3D] node of the parent [SpringBoneSimulator3D] if found.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="bone" type="int" setter="set_bone" getter="get_bone" default="-1">
+			The index of the attached bone.
+		</member>
+		<member name="bone_name" type="String" setter="set_bone_name" getter="get_bone_name" default="&quot;&quot;">
+			The name of the attached bone.
+		</member>
+		<member name="position_offset" type="Vector3" setter="set_position_offset" getter="get_position_offset">
+			The offset of the position from [Skeleton3D]'s [member bone] pose position.
+		</member>
+		<member name="rotation_offset" type="Quaternion" setter="set_rotation_offset" getter="get_rotation_offset">
+			The offset of the rotation from [Skeleton3D]'s [member bone] pose rotation.
+		</member>
+	</members>
+</class>
diff --git a/doc/classes/SpringBoneCollisionCapsule3D.xml b/doc/classes/SpringBoneCollisionCapsule3D.xml
new file mode 100644
index 000000000000..9af9aca331a9
--- /dev/null
+++ b/doc/classes/SpringBoneCollisionCapsule3D.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="SpringBoneCollisionCapsule3D" inherits="SpringBoneCollision3D" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		A capsule shape collision that interacts with [SpringBoneSimulator3D].
+	</brief_description>
+	<description>
+		A capsule shape collision that interacts with [SpringBoneSimulator3D].
+	</description>
+	<tutorials>
+	</tutorials>
+	<members>
+		<member name="height" type="float" setter="set_height" getter="get_height" default="0.5">
+			The capsule's height.
+		</member>
+		<member name="inside" type="bool" setter="set_inside" getter="is_inside" default="false">
+			If [code]true[/code], the collision acts to trap the joint within the collision.
+		</member>
+		<member name="radius" type="float" setter="set_radius" getter="get_radius" default="0.1">
+			The capsule's radius.
+		</member>
+	</members>
+</class>
diff --git a/doc/classes/SpringBoneCollisionPlane3D.xml b/doc/classes/SpringBoneCollisionPlane3D.xml
new file mode 100644
index 000000000000..d9f7199f5d1b
--- /dev/null
+++ b/doc/classes/SpringBoneCollisionPlane3D.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="SpringBoneCollisionPlane3D" inherits="SpringBoneCollision3D" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		A infinite plane collision that interacts with [SpringBoneSimulator3D].
+	</brief_description>
+	<description>
+		A infinite plane collision that interacts with [SpringBoneSimulator3D]. It is an infinite size XZ plane, and the +Y direction is treated as normal.
+	</description>
+	<tutorials>
+	</tutorials>
+</class>
diff --git a/doc/classes/SpringBoneCollisionSphere3D.xml b/doc/classes/SpringBoneCollisionSphere3D.xml
new file mode 100644
index 000000000000..3ccb42a93e9a
--- /dev/null
+++ b/doc/classes/SpringBoneCollisionSphere3D.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="SpringBoneCollisionSphere3D" inherits="SpringBoneCollision3D" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		A sphere shape collision that interacts with [SpringBoneSimulator3D].
+	</brief_description>
+	<description>
+		A sphere shape collision that interacts with [SpringBoneSimulator3D].
+	</description>
+	<tutorials>
+	</tutorials>
+	<members>
+		<member name="inside" type="bool" setter="set_inside" getter="is_inside" default="false">
+			If [code]true[/code], the collision acts to trap the joint within the collision.
+		</member>
+		<member name="radius" type="float" setter="set_radius" getter="get_radius" default="0.1">
+			The sphere's radius.
+		</member>
+	</members>
+</class>
diff --git a/doc/classes/SpringBoneSimulator3D.xml b/doc/classes/SpringBoneSimulator3D.xml
new file mode 100644
index 000000000000..6c639105d9ea
--- /dev/null
+++ b/doc/classes/SpringBoneSimulator3D.xml
@@ -0,0 +1,658 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="SpringBoneSimulator3D" inherits="SkeletonModifier3D" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		A [SkeletonModifier3D] to apply inertial wavering to bone chains.
+	</brief_description>
+	<description>
+		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].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="are_all_child_collisions_enabled" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns [code]true[/code] if the all child [SpringBoneCollision3D]s are contained in the collision list at [param index] in the settings.
+			</description>
+		</method>
+		<method name="clear_collisions">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Clears all collisions from the collision list at [param index] in the settings when [method are_all_child_collisions_enabled] is [code]false[/code].
+			</description>
+		</method>
+		<method name="clear_exclude_collisions">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<description>
+				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].
+			</description>
+		</method>
+		<method name="clear_settings">
+			<return type="void" />
+			<description>
+				Clears all settings.
+			</description>
+		</method>
+		<method name="get_center_bone" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the center bone index of the bone chain.
+			</description>
+		</method>
+		<method name="get_center_bone_name" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the center bone name of the bone chain.
+			</description>
+		</method>
+		<method name="get_center_from" qualifiers="const">
+			<return type="int" enum="SpringBoneSimulator3D.CenterFrom" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns what the center originates from in the bone chain.
+			</description>
+		</method>
+		<method name="get_center_node" qualifiers="const">
+			<return type="NodePath" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the center node path of the bone chain.
+			</description>
+		</method>
+		<method name="get_collision_count" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the collision count of the bone chain's collision list when [method are_all_child_collisions_enabled] is [code]false[/code].
+			</description>
+		</method>
+		<method name="get_collision_path" qualifiers="const">
+			<return type="NodePath" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="collision" type="int" />
+			<description>
+				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].
+			</description>
+		</method>
+		<method name="get_drag" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the drag force damping curve of the bone chain.
+			</description>
+		</method>
+		<method name="get_drag_damping_curve" qualifiers="const">
+			<return type="Curve" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the drag force damping curve of the bone chain.
+			</description>
+		</method>
+		<method name="get_end_bone" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the end bone index of the bone chain.
+			</description>
+		</method>
+		<method name="get_end_bone_direction" qualifiers="const">
+			<return type="int" enum="SpringBoneSimulator3D.BoneDirection" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the end bone's tail direction of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+			</description>
+		</method>
+		<method name="get_end_bone_length" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the end bone's tail length of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+			</description>
+		</method>
+		<method name="get_end_bone_name" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the end bone name of the bone chain.
+			</description>
+		</method>
+		<method name="get_end_bone_tip_radius" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the end bone tip radius of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+			</description>
+		</method>
+		<method name="get_exclude_collision_count" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the exclude collision count of the bone chain's exclude collision list when [method are_all_child_collisions_enabled] is [code]true[/code].
+			</description>
+		</method>
+		<method name="get_exclude_collision_path" qualifiers="const">
+			<return type="NodePath" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="collision" type="int" />
+			<description>
+				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].
+			</description>
+		</method>
+		<method name="get_gravity" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the gravity amount of the bone chain.
+			</description>
+		</method>
+		<method name="get_gravity_damping_curve" qualifiers="const">
+			<return type="Curve" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the gravity amount damping curve of the bone chain.
+			</description>
+		</method>
+		<method name="get_gravity_direction" qualifiers="const">
+			<return type="Vector3" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the gravity direction of the bone chain.
+			</description>
+		</method>
+		<method name="get_joint_bone" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<description>
+				Returns the bone index at [param joint] in the bone chain's joint list.
+			</description>
+		</method>
+		<method name="get_joint_bone_name" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<description>
+				Returns the bone name at [param joint] in the bone chain's joint list.
+			</description>
+		</method>
+		<method name="get_joint_count" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the joint count of the bone chain's joint list.
+			</description>
+		</method>
+		<method name="get_joint_drag" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<description>
+				Returns the drag force at [param joint] in the bone chain's joint list.
+			</description>
+		</method>
+		<method name="get_joint_gravity" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<description>
+				Returns the gravity amount at [param joint] in the bone chain's joint list.
+			</description>
+		</method>
+		<method name="get_joint_gravity_direction" qualifiers="const">
+			<return type="Vector3" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<description>
+				Returns the gravity direction at [param joint] in the bone chain's joint list.
+			</description>
+		</method>
+		<method name="get_joint_radius" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<description>
+				Returns the radius at [param joint] in the bone chain's joint list.
+			</description>
+		</method>
+		<method name="get_joint_rotation_axis" qualifiers="const">
+			<return type="int" enum="SpringBoneSimulator3D.RotationAxis" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<description>
+				Returns the rotation axis at [param joint] in the bone chain's joint list.
+			</description>
+		</method>
+		<method name="get_joint_stiffness" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<description>
+				Returns the stiffness force at [param joint] in the bone chain's joint list.
+			</description>
+		</method>
+		<method name="get_radius" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the joint radius of the bone chain.
+			</description>
+		</method>
+		<method name="get_radius_damping_curve" qualifiers="const">
+			<return type="Curve" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the joint radius damping curve of the bone chain.
+			</description>
+		</method>
+		<method name="get_root_bone" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the root bone index of the bone chain.
+			</description>
+		</method>
+		<method name="get_root_bone_name" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the root bone name of the bone chain.
+			</description>
+		</method>
+		<method name="get_rotation_axis" qualifiers="const">
+			<return type="int" enum="SpringBoneSimulator3D.RotationAxis" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the rotation axis of the bone chain.
+			</description>
+		</method>
+		<method name="get_stiffness" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the stiffness force of the bone chain.
+			</description>
+		</method>
+		<method name="get_stiffness_damping_curve" qualifiers="const">
+			<return type="Curve" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the stiffness force damping curve of the bone chain.
+			</description>
+		</method>
+		<method name="is_config_individual" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns [code]true[/code] if the config can be edited individually for each joint.
+			</description>
+		</method>
+		<method name="is_end_bone_extended" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns [code]true[/code] if the end bone is extended to have the tail.
+			</description>
+		</method>
+		<method name="reset">
+			<return type="void" />
+			<description>
+				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.
+			</description>
+		</method>
+		<method name="set_center_bone">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="bone" type="int" />
+			<description>
+				Sets the center bone index of the bone chain.
+			</description>
+		</method>
+		<method name="set_center_bone_name">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="bone_name" type="String" />
+			<description>
+				Sets the center bone name of the bone chain.
+			</description>
+		</method>
+		<method name="set_center_from">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="center_from" type="int" enum="SpringBoneSimulator3D.CenterFrom" />
+			<description>
+				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.
+			</description>
+		</method>
+		<method name="set_center_node">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="node_path" type="NodePath" />
+			<description>
+				Sets the center node path of the bone chain.
+			</description>
+		</method>
+		<method name="set_collision_count">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="count" type="int" />
+			<description>
+				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].
+			</description>
+		</method>
+		<method name="set_collision_path">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="collision" type="int" />
+			<param index="2" name="node_path" type="NodePath" />
+			<description>
+				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].
+			</description>
+		</method>
+		<method name="set_drag">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="drag" type="float" />
+			<description>
+				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.
+			</description>
+		</method>
+		<method name="set_drag_damping_curve">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="curve" type="Curve" />
+			<description>
+				Sets the drag force damping curve of the bone chain.
+			</description>
+		</method>
+		<method name="set_enable_all_child_collisions">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="enabled" type="bool" />
+			<description>
+				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].
+			</description>
+		</method>
+		<method name="set_end_bone">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="bone" type="int" />
+			<description>
+				Sets the end bone index of the bone chain.
+			</description>
+		</method>
+		<method name="set_end_bone_direction">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="bone_direction" type="int" enum="SpringBoneSimulator3D.BoneDirection" />
+			<description>
+				Sets the end bone tail direction of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+			</description>
+		</method>
+		<method name="set_end_bone_length">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="length" type="float" />
+			<description>
+				Sets the end bone tail length of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+			</description>
+		</method>
+		<method name="set_end_bone_name">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="bone_name" type="String" />
+			<description>
+				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.
+			</description>
+		</method>
+		<method name="set_end_bone_tip_radius">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="radius" type="float" />
+			<description>
+				Sets the end bone tip radius of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+			</description>
+		</method>
+		<method name="set_exclude_collision_count">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="count" type="int" />
+			<description>
+				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].
+			</description>
+		</method>
+		<method name="set_exclude_collision_path">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="collision" type="int" />
+			<param index="2" name="node_path" type="NodePath" />
+			<description>
+				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].
+			</description>
+		</method>
+		<method name="set_extend_end_bone">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="enabled" type="bool" />
+			<description>
+				If [param enabled] is [code]true[/code], the end bone is extended to have the tail.
+			</description>
+		</method>
+		<method name="set_gravity">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="gravity" type="float" />
+			<description>
+				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.
+			</description>
+		</method>
+		<method name="set_gravity_damping_curve">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="curve" type="Curve" />
+			<description>
+				Sets the gravity amount damping curve of the bone chain.
+			</description>
+		</method>
+		<method name="set_gravity_direction">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="gravity_direction" type="Vector3" />
+			<description>
+				Sets the gravity direction of the bone chain.
+				The value is cached in each joint setting in the joint list.
+			</description>
+		</method>
+		<method name="set_individual_config">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="enabled" type="bool" />
+			<description>
+				If [param enabled] is [code]true[/code], the config can be edited individually for each joint.
+			</description>
+		</method>
+		<method name="set_joint_drag">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<param index="2" name="drag" type="float" />
+			<description>
+				Sets the drag force at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+			</description>
+		</method>
+		<method name="set_joint_gravity">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<param index="2" name="gravity" type="float" />
+			<description>
+				Sets the gravity amount at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+			</description>
+		</method>
+		<method name="set_joint_gravity_direction">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<param index="2" name="gravity_direction" type="Vector3" />
+			<description>
+				Sets the gravity direction at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+			</description>
+		</method>
+		<method name="set_joint_radius">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<param index="2" name="radius" type="float" />
+			<description>
+				Sets the joint radius at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+			</description>
+		</method>
+		<method name="set_joint_rotation_axis">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<param index="2" name="axis" type="int" enum="SpringBoneSimulator3D.RotationAxis" />
+			<description>
+				Sets the rotation axis at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+			</description>
+		</method>
+		<method name="set_joint_stiffness">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="joint" type="int" />
+			<param index="2" name="stiffness" type="float" />
+			<description>
+				Sets the stiffness force at [param joint] in the bone chain's joint list when [method is_config_individual] is [code]true[/code].
+			</description>
+		</method>
+		<method name="set_radius">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="radius" type="float" />
+			<description>
+				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.
+			</description>
+		</method>
+		<method name="set_radius_damping_curve">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="curve" type="Curve" />
+			<description>
+				Sets the joint radius damping curve of the bone chain.
+			</description>
+		</method>
+		<method name="set_root_bone">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="bone" type="int" />
+			<description>
+				Sets the root bone index of the bone chain.
+			</description>
+		</method>
+		<method name="set_root_bone_name">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="bone_name" type="String" />
+			<description>
+				Sets the root bone name of the bone chain.
+			</description>
+		</method>
+		<method name="set_rotation_axis">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="axis" type="int" enum="SpringBoneSimulator3D.RotationAxis" />
+			<description>
+				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.
+			</description>
+		</method>
+		<method name="set_stiffness">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="stiffness" type="float" />
+			<description>
+				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.
+			</description>
+		</method>
+		<method name="set_stiffness_damping_curve">
+			<return type="void" />
+			<param index="0" name="index" type="int" />
+			<param index="1" name="curve" type="Curve" />
+			<description>
+				Sets the stiffness force damping curve of the bone chain.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="setting_count" type="int" setter="set_setting_count" getter="get_setting_count" default="0">
+			The number of settings.
+		</member>
+	</members>
+	<constants>
+		<constant name="BONE_DIRECTION_PLUS_X" value="0" enum="BoneDirection">
+			Enumerated value for the +X axis.
+		</constant>
+		<constant name="BONE_DIRECTION_MINUS_X" value="1" enum="BoneDirection">
+			Enumerated value for the -X axis.
+		</constant>
+		<constant name="BONE_DIRECTION_PLUS_Y" value="2" enum="BoneDirection">
+			Enumerated value for the +Y axis.
+		</constant>
+		<constant name="BONE_DIRECTION_MINUS_Y" value="3" enum="BoneDirection">
+			Enumerated value for the -Y axis.
+		</constant>
+		<constant name="BONE_DIRECTION_PLUS_Z" value="4" enum="BoneDirection">
+			Enumerated value for the +Z axis.
+		</constant>
+		<constant name="BONE_DIRECTION_MINUS_Z" value="5" enum="BoneDirection">
+			Enumerated value for the -Z axis.
+		</constant>
+		<constant name="BONE_DIRECTION_FROM_PARENT" value="6" enum="BoneDirection">
+			Enumerated value for the axis from a parent bone to the child bone.
+		</constant>
+		<constant name="CENTER_FROM_WORLD_ORIGIN" value="0" enum="CenterFrom">
+			The world origin is defined as center.
+		</constant>
+		<constant name="CENTER_FROM_NODE" value="1" enum="CenterFrom">
+			The [Node3D] specified by [method set_center_node] is defined as center.
+			If [Node3D] is not found, the parent [Skeleton3D] is treated as center.
+		</constant>
+		<constant name="CENTER_FROM_BONE" value="2" enum="CenterFrom">
+			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.
+		</constant>
+		<constant name="ROTATION_AXIS_X" value="0" enum="RotationAxis">
+			Enumerated value for the rotation of the X axis.
+		</constant>
+		<constant name="ROTATION_AXIS_Y" value="1" enum="RotationAxis">
+			Enumerated value for the rotation of the Y axis.
+		</constant>
+		<constant name="ROTATION_AXIS_Z" value="2" enum="RotationAxis">
+			Enumerated value for the rotation of the Z axis.
+		</constant>
+		<constant name="ROTATION_AXIS_ALL" value="3" enum="RotationAxis">
+			Enumerated value for the unconstrained rotation.
+		</constant>
+	</constants>
+</class>
diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp
index 41992965131c..2a8ad890b615 100644
--- a/editor/editor_inspector.cpp
+++ b/editor/editor_inspector.cpp
@@ -2365,6 +2365,7 @@ void EditorInspectorArray::_setup() {
 
 	Ref<Font> 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 8279c354a462..d703510f71d8 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 @@ class EditorInspectorArray : public EditorInspectorSection {
 	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 79454eab6f05..dabd437f5e55 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -795,6 +795,9 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> 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 000000000000..564c44a41182
--- /dev/null
+++ b/editor/icons/SpringBoneCollision3D.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#b56d6d" d="M7.375 5.053c-.488.733-.289 1.724.445 2.213.2.133.426.219.664.252.122.873.927 1.481 1.8 1.36.874-.121 1.483-.928 1.36-1.801-.033-.238-.119-.464-.25-.664l1.373-1.372c.733.486 1.724.288 2.211-.445.488-.734.291-1.725-.444-2.213-.199-.132-.427-.219-.664-.251-.121-.874-.927-1.483-1.8-1.362s-1.482.928-1.361 1.803c.033.236.119.465.251.664l-1.372 1.37c-.734-.488-1.725-.289-2.213.446zM8.625 10.949c.488-.734.289-1.727-.444-2.215-.2-.131-.427-.217-.665-.25-.122-.873-.927-1.482-1.8-1.361s-1.482.928-1.36 1.802c.033.236.119.464.252.663l-1.373 1.373c-.734-.488-1.724-.289-2.212.445s-.289 1.724.445 2.212c.199.132.426.219.664.252.121.873.927 1.481 1.8 1.36.874-.123 1.482-.926 1.36-1.801-.033-.236-.118-.465-.252-.664l1.374-1.373c.732.49 1.723.292 2.211-.443z"/><circle cx="12" cy="12" r="3" fill="#5c7d8e"/><circle cx="11.143" cy="10.714" r=".856" fill="#728387"/></svg>
\ No newline at end of file
diff --git a/editor/icons/SpringBoneCollisionCapsule3D.svg b/editor/icons/SpringBoneCollisionCapsule3D.svg
new file mode 100644
index 000000000000..3c433562d8a4
--- /dev/null
+++ b/editor/icons/SpringBoneCollisionCapsule3D.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fc7f7f" d="M7.375 5.053c-.488.733-.289 1.724.445 2.213.2.133.426.219.664.252.122.873.927 1.481 1.8 1.36.874-.121 1.483-.928 1.36-1.801-.033-.238-.119-.464-.25-.664l1.373-1.372c.733.486 1.724.288 2.211-.445.488-.734.291-1.725-.444-2.213-.199-.132-.427-.219-.664-.251-.121-.874-.927-1.483-1.8-1.362s-1.482.928-1.361 1.803c.033.236.119.465.251.664l-1.372 1.37c-.734-.488-1.725-.289-2.213.446zM8.625 10.949c.488-.734.289-1.727-.444-2.215-.2-.131-.427-.217-.665-.25-.122-.873-.927-1.482-1.8-1.361s-1.482.928-1.36 1.802c.033.236.119.464.252.663l-1.373 1.373c-.734-.488-1.724-.289-2.212.445s-.289 1.724.445 2.212c.199.132.426.219.664.252.121.873.927 1.481 1.8 1.36.874-.123 1.482-.926 1.36-1.801-.033-.236-.118-.465-.252-.664l1.374-1.373c.732.49 1.723.292 2.211-.443z"/><path fill="#5fb2ff" d="m10.286 10.857v2.285c0 1.578 1.278 2.856 2.856 2.856s2.858-1.277 2.858-2.855v-2.285c0-1.579-1.279-2.858-2.857-2.858s-2.857 1.279-2.857 2.857z"/><circle cx="12.286" cy="10" r=".857" fill="#a2d2ff"/></svg>
\ No newline at end of file
diff --git a/editor/icons/SpringBoneCollisionPlane3D.svg b/editor/icons/SpringBoneCollisionPlane3D.svg
new file mode 100644
index 000000000000..7ccb674ab1a0
--- /dev/null
+++ b/editor/icons/SpringBoneCollisionPlane3D.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-399.5 200.5 16 16"><path fill="#fc7f7f" d="M-392.125 205.553c-.488.733-.289 1.725.445 2.213.2.132.426.218.664.252.121.872.927 1.481 1.8 1.36.874-.121 1.483-.928 1.361-1.801-.033-.238-.119-.464-.251-.664l1.373-1.372c.734.487 1.724.288 2.212-.445.488-.734.29-1.725-.445-2.213-.199-.132-.426-.219-.664-.251-.121-.874-.927-1.483-1.8-1.362s-1.482.928-1.361 1.802c.033.236.119.465.251.664l-1.372 1.371c-.734-.488-1.725-.289-2.213.446zM-390.875 211.449c.488-.734.289-1.726-.444-2.214-.2-.132-.427-.218-.665-.251-.122-.873-.927-1.482-1.8-1.361s-1.482.928-1.36 1.802c.033.236.119.463.252.663l-1.373 1.373c-.734-.488-1.724-.289-2.212.445s-.289 1.724.445 2.212c.199.132.426.219.664.252.121.873.927 1.482 1.8 1.36.874-.122 1.482-.926 1.36-1.8-.033-.237-.118-.465-.252-.664l1.374-1.373c.732.489 1.723.29 2.211-.444z"/><path fill="#5fb2ff" d="m-389.5 216.5 6-.5v-5.5l-6-.5z"/></svg>
\ No newline at end of file
diff --git a/editor/icons/SpringBoneCollisionSphere3D.svg b/editor/icons/SpringBoneCollisionSphere3D.svg
new file mode 100644
index 000000000000..7d39ee952f73
--- /dev/null
+++ b/editor/icons/SpringBoneCollisionSphere3D.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fc7f7f" d="M7.375 5.053c-.488.733-.289 1.724.445 2.213.2.133.426.219.664.252.122.873.927 1.481 1.8 1.36.874-.121 1.483-.928 1.36-1.801-.033-.238-.119-.464-.25-.664l1.373-1.372c.733.486 1.724.288 2.211-.445.488-.734.291-1.725-.444-2.213-.199-.132-.427-.219-.664-.251-.121-.874-.927-1.483-1.8-1.362s-1.482.928-1.361 1.803c.033.236.119.465.251.664l-1.372 1.37c-.734-.488-1.725-.289-2.213.446zM8.625 10.949c.488-.734.289-1.727-.444-2.215-.2-.131-.427-.217-.665-.25-.122-.873-.927-1.482-1.8-1.361s-1.482.928-1.36 1.802c.033.236.119.464.252.663l-1.373 1.373c-.734-.488-1.724-.289-2.212.445s-.289 1.724.445 2.212c.199.132.426.219.664.252.121.873.927 1.481 1.8 1.36.874-.123 1.482-.926 1.36-1.801-.033-.236-.118-.465-.252-.664l1.374-1.373c.732.49 1.723.292 2.211-.443z"/><circle cx="12" cy="12" r="3" fill="#5fb2ff"/><circle cx="11.143" cy="10.714" r=".856" fill="#a2d2ff"/></svg>
\ No newline at end of file
diff --git a/editor/icons/SpringBoneSimulator3D.svg b/editor/icons/SpringBoneSimulator3D.svg
new file mode 100644
index 000000000000..eeb37314c39c
--- /dev/null
+++ b/editor/icons/SpringBoneSimulator3D.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-399.5 200.5 16 16"><g fill="#fc7f7f"><path d="M-392.125 205.553c-.488.733-.289 1.725.445 2.213.2.132.426.218.664.252.121.872.927 1.481 1.8 1.36.874-.121 1.483-.928 1.361-1.801-.033-.238-.119-.464-.251-.664l1.373-1.372c.734.487 1.724.288 2.212-.445.488-.734.29-1.725-.445-2.213-.199-.132-.426-.219-.664-.251-.121-.874-.927-1.483-1.8-1.362s-1.482.928-1.361 1.802c.033.236.119.465.251.664l-1.372 1.371c-.734-.488-1.725-.289-2.213.446zM-390.875 211.449c.488-.734.289-1.726-.444-2.214-.2-.132-.427-.218-.665-.251-.122-.873-.927-1.482-1.8-1.361s-1.482.928-1.36 1.802c.033.236.119.463.252.663l-1.373 1.373c-.734-.488-1.724-.289-2.212.445s-.289 1.724.445 2.212c.199.132.426.219.664.252.121.873.927 1.482 1.8 1.36.874-.122 1.482-.926 1.36-1.8-.033-.237-.118-.465-.252-.664l1.374-1.373c.732.489 1.723.29 2.211-.444z"/><path d="m-397.222 209.5c0 .276.224.5.5.5s.5-.224.5-.5c0-3.263 2.46-5.723 5.722-5.723.276 0 .5-.224.5-.5s-.224-.5-.5-.5c-3.832 0-6.722 2.89-6.722 6.723z"/><path d="M-398 207.723c0-3.263 2.46-5.723 5.722-5.723.276 0 .5-.224.5-.5s-.224-.5-.5-.5c-3.832 0-6.722 2.89-6.722 6.723 0 .276.224.5.5.5s.5-.224.5-.5zM-385.778 207.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5c0 3.263-2.46 5.723-5.722 5.723-.276 0-.5.224-.5.5s.224.5.5.5c3.832 0 6.722-2.89 6.722-6.723z"/><path d="m-384.5 208.777c-.276 0-.5.224-.5.5 0 3.263-2.46 5.723-5.722 5.723-.276 0-.5.224-.5.5s.224.5.5.5c3.832 0 6.722-2.89 6.722-6.723 0-.276-.224-.5-.5-.5z"/></g></svg>
\ 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 000000000000..1cb943b04123
--- /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<Shader> 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<SpringBoneSimulator3D>(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<SpringBoneSimulator3D>(p_gizmo->get_node_3d());
+	p_gizmo->clear();
+
+	if (!simulator->get_setting_count()) {
+		return;
+	}
+
+	Skeleton3D *skeleton = simulator->get_skeleton();
+	if (!skeleton) {
+		return;
+	}
+
+	Ref<ArrayMesh> 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<Material>(), skel_tr, skeleton->register_skin(skeleton->create_skin_from_rest_transforms()));
+}
+
+Ref<ArrayMesh> 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<SurfaceTool> 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<int> bones;
+	LocalVector<float> 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<SurfaceTool> &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<SurfaceTool> &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<Shader> 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<SpringBoneCollision3D>(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<SpringBoneCollision3D>(p_gizmo->get_node_3d());
+	p_gizmo->clear();
+
+	Ref<ArrayMesh> mesh = get_collision_mesh(collision, p_gizmo->is_selected());
+	p_gizmo->add_mesh(mesh);
+}
+
+Ref<ArrayMesh> 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<SurfaceTool> 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<SpringBoneCollisionSphere3D>(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<SpringBoneCollisionCapsule3D>(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<SpringBoneCollisionPlane3D>(p_collision);
+	if (plane) {
+		draw_plane(surface_tool, collision_color);
+		return surface_tool->commit();
+	}
+
+	return surface_tool->commit();
+}
+
+void SpringBoneCollision3DGizmoPlugin::draw_sphere(Ref<SurfaceTool> &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<SurfaceTool> &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<Vector3> 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<SurfaceTool> &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<Vector3> 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 000000000000..8d6be59b5b0a
--- /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<StandardMaterial3D> unselected_mat;
+		Ref<ShaderMaterial> selected_mat;
+	};
+	static SelectionMaterials selection_materials;
+
+public:
+	static Ref<ArrayMesh> get_joints_mesh(Skeleton3D *p_skeleton, SpringBoneSimulator3D *p_simulator, bool p_is_selected);
+	static void draw_sphere(Ref<SurfaceTool> &p_surface_tool, const Basis &p_basis, const Vector3 &p_center, float p_radius, const Color &p_color);
+	static void draw_line(Ref<SurfaceTool> &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<StandardMaterial3D> unselected_mat;
+		Ref<ShaderMaterial> selected_mat;
+	};
+	static SelectionMaterials selection_materials;
+
+public:
+	static Ref<ArrayMesh> get_collision_mesh(SpringBoneCollision3D *p_collision, bool p_is_selected);
+	static void draw_sphere(Ref<SurfaceTool> &p_surface_tool, float p_radius, const Color &p_color);
+	static void draw_capsule(Ref<SurfaceTool> &p_surface_tool, float p_radius, float p_height, const Color &p_color);
+	static void draw_plane(Ref<SurfaceTool> &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 b803d2e5d828..28a2bd89c568 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<RayCast3DGizmoPlugin>(memnew(RayCast3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<ShapeCast3DGizmoPlugin>(memnew(ShapeCast3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<SpringArm3DGizmoPlugin>(memnew(SpringArm3DGizmoPlugin)));
+	add_gizmo_plugin(Ref<SpringBoneCollision3DGizmoPlugin>(memnew(SpringBoneCollision3DGizmoPlugin)));
+	add_gizmo_plugin(Ref<SpringBoneSimulator3DGizmoPlugin>(memnew(SpringBoneSimulator3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<VehicleWheel3DGizmoPlugin>(memnew(VehicleWheel3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<VisibleOnScreenNotifier3DGizmoPlugin>(memnew(VisibleOnScreenNotifier3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<GPUParticles3DGizmoPlugin>(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 000000000000..25ad37fecadd
--- /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<SpringBoneSimulator3D>(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<SpringBoneSimulator3D>(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 000000000000..713891e04d51
--- /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 000000000000..39acc5acebb2
--- /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<Vector3, Vector3> 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<Vector3, Vector3>(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<Vector3, Vector3> 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 000000000000..0e24bed6407c
--- /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<Vector3, Vector3> 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 000000000000..8fd29834622b
--- /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 000000000000..0f3896aa2096
--- /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 000000000000..3560932bc932
--- /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 000000000000..4de0f93700c0
--- /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 000000000000..db2755288bf8
--- /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<BoneDirection>((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<CenterFrom>((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<RotationAxis>((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<RotationAxis>((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<PropertyInfo> *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<BoneAxis>((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<Curve> &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<Curve> SpringBoneSimulator3D::get_radius_damping_curve(int p_index) const {
+	ERR_FAIL_INDEX_V(p_index, settings.size(), Ref<Curve>());
+	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<Curve> &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<Curve> SpringBoneSimulator3D::get_stiffness_damping_curve(int p_index) const {
+	ERR_FAIL_INDEX_V(p_index, settings.size(), Ref<Curve>());
+	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<Curve> &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<Curve> SpringBoneSimulator3D::get_drag_damping_curve(int p_index) const {
+	ERR_FAIL_INDEX_V(p_index, settings.size(), Ref<Curve>());
+	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<Curve> &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<Curve> SpringBoneSimulator3D::get_gravity_damping_curve(int p_index) const {
+	ERR_FAIL_INDEX_V(p_index, settings.size(), Ref<Curve>());
+	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<SpringBone3DJointSetting *> &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<SpringBone3DJointSetting *> 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<SpringBone3DJointSetting *> &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<SpringBone3DJointSetting *> 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<SpringBone3DJointSetting *> &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<SpringBone3DJointSetting *> 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<SpringBone3DJointSetting *> &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<SpringBone3DJointSetting *> 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<SpringBone3DJointSetting *> &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<SpringBone3DJointSetting *> 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<SpringBone3DJointSetting *> &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<SpringBone3DJointSetting *> 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<SpringBone3DJointSetting *> &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<SpringBone3DJointSetting *> 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<SpringBone3DJointSetting *> &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<SpringBone3DJointSetting *> 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<SpringBone3DJointSetting *> &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<SpringBone3DJointSetting *> 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<NodePath> &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<NodePath> 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<NodePath> &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<NodePath> 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<NodePath> &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<NodePath> 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<NodePath> &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<NodePath> 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<ObjectID> SpringBoneSimulator3D::get_valid_collision_instance_ids(int p_index) {
+	ERR_FAIL_INDEX_V(p_index, settings.size(), LocalVector<ObjectID>());
+	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<SpringBoneCollision3D>(p_child)) {
+		_make_collisions_dirty();
+	}
+}
+
+void SpringBoneSimulator3D::move_child_notify(Node *p_child) {
+	if (Object::cast_to<SpringBoneCollision3D>(p_child)) {
+		_make_collisions_dirty();
+	}
+}
+
+void SpringBoneSimulator3D::remove_child_notify(Node *p_child) {
+	if (Object::cast_to<SpringBoneCollision3D>(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<SpringBoneCollision3D>(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<ObjectID> &cache = settings[i]->cached_collisions;
+		cache.clear();
+		if (!settings[i]->enable_all_child_collisions) {
+			// Allow list.
+			Vector<NodePath> &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<uint32_t> masks;
+			Vector<NodePath> &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<SpringBoneCollision3D>(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<int> 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<SpringBone3DJointSetting *> &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<Node3D>(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<SpringBone3DJointSetting *> &p_joints, const LocalVector<ObjectID> &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<SpringBoneCollision3D>(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 000000000000..9c9aa9137364
--- /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<ObjectID> 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<Curve> radius_damping_curve;
+		float stiffness = 1.0;
+		Ref<Curve> stiffness_damping_curve;
+		float drag = 0.4;
+		Ref<Curve> drag_damping_curve;
+		float gravity = 0.0;
+		Ref<Curve> gravity_damping_curve;
+		Vector3 gravity_direction = Vector3(0, -1, 0);
+		RotationAxis rotation_axis = ROTATION_AXIS_ALL;
+		Vector<SpringBone3DJointSetting *> joints;
+
+		// Cache into collisions.
+		bool enable_all_child_collisions = true;
+		Vector<NodePath> collisions;
+		Vector<NodePath> exclude_collisions;
+		LocalVector<ObjectID> cached_collisions;
+
+		// To process.
+		bool simulation_dirty = false;
+		Transform3D cached_center;
+		Transform3D cached_inverted_center;
+	};
+
+protected:
+	Vector<SpringBone3DSetting *> 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<PropertyInfo> *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<SpringBone3DJointSetting *> &p_joints, const LocalVector<ObjectID> &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<Curve> &p_damping_curve);
+	Ref<Curve> 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<Curve> &p_damping_curve);
+	Ref<Curve> 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<Curve> &p_damping_curve);
+	Ref<Curve> 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<Curve> &p_damping_curve);
+	Ref<Curve> 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<ObjectID> 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 53d8043329ca..97a878a96be9 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
 


Test patch: