Browse Source

Implement SpringBoneSimulator3D to wiggle chained bones

Co-authored-by: lyuma <[email protected]>
Co-authored-by: fire <[email protected]>
Co-authored-by: SaracenOne <[email protected]>
Silc Lizard (Tokage) Renew 8 months ago
parent
commit
5472558a98

+ 9 - 0
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>

+ 35 - 0
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>

+ 22 - 0
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>

+ 11 - 0
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>

+ 19 - 0
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>

+ 658 - 0
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>

+ 17 - 9
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));
 				}
 			}

+ 3 - 2
editor/editor_inspector.h

@@ -391,6 +391,7 @@ class EditorInspectorArray : public EditorInspectorSection {
 
 	bool read_only = false;
 	bool movable = true;
+	bool is_const = false;
 	bool numbered = false;
 
 	enum MenuOptions {
@@ -457,8 +458,8 @@ protected:
 	static void _bind_methods();
 
 public:
-	void setup_with_move_element_function(Object *p_object, const String &p_label, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable = true, bool p_numbered = false, int p_page_length = 5, const String &p_add_item_text = "");
-	void setup_with_count_property(Object *p_object, const String &p_label, const StringName &p_count_property, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable = true, bool p_numbered = false, int p_page_length = 5, const String &p_add_item_text = "", const String &p_swap_method = "");
+	void setup_with_move_element_function(Object *p_object, const String &p_label, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable = true, bool p_is_const = false, bool p_numbered = false, int p_page_length = 5, const String &p_add_item_text = "");
+	void setup_with_count_property(Object *p_object, const String &p_label, const StringName &p_count_property, const StringName &p_array_element_prefix, int p_page, const Color &p_bg_color, bool p_foldable, bool p_movable = true, bool p_is_const = false, bool p_numbered = false, int p_page_length = 5, const String &p_add_item_text = "", const String &p_swap_method = "");
 	VBoxContainer *get_vbox(int p_index);
 
 	EditorInspectorArray(bool p_read_only);

+ 3 - 0
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)

+ 1 - 0
editor/icons/SpringBoneCollision3D.svg

@@ -0,0 +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>

+ 1 - 0
editor/icons/SpringBoneCollisionCapsule3D.svg

@@ -0,0 +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>

+ 1 - 0
editor/icons/SpringBoneCollisionPlane3D.svg

@@ -0,0 +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>

+ 1 - 0
editor/icons/SpringBoneCollisionSphere3D.svg

@@ -0,0 +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>

+ 1 - 0
editor/icons/SpringBoneSimulator3D.svg

@@ -0,0 +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>

+ 424 - 0
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));
+		}
+	}
+}

+ 89 - 0
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

+ 3 - 0
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)));

+ 192 - 0
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

+ 75 - 0
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

+ 112 - 0
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);
+}

+ 60 - 0
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

+ 44 - 0
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;
+}

+ 43 - 0
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

+ 78 - 0
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);
+}

+ 59 - 0
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

+ 1601 - 0
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);
+}

+ 281 - 0
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

+ 10 - 0
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