diff --git a/doc/classes/BoneTwistDisperser3D.xml b/doc/classes/BoneTwistDisperser3D.xml
new file mode 100644
index 00000000000..15319c06040
--- /dev/null
+++ b/doc/classes/BoneTwistDisperser3D.xml
@@ -0,0 +1,268 @@
+
+
+
+ A node that propagates and disperses the child bone's twist to the parent bones.
+
+
+ This [BoneTwistDisperser3D] allows for smooth twist interpolation between multiple bones by dispersing the end bone's twist to the parents. This only changes the twist without changing the global position of each joint.
+ This is useful for smoothly twisting bones in combination with [CopyTransformModifier3D] and IK.
+ [b]Note:[/b] If an extracted twist is greater than 180 degrees, flipping occurs. This is similar to [ConvertTransformModifier3D].
+
+
+
+
+
+
+
+ Clears all settings.
+
+
+
+
+
+
+ Returns the damping curve when [method get_disperse_mode] is [constant DISPERSE_MODE_CUSTOM].
+
+
+
+
+
+
+ Returns whether to use automatic amount assignment or to allow manual assignment.
+
+
+
+
+
+
+ Returns the end bone index of the bone chain.
+
+
+
+
+
+
+ Returns the tail direction of the end bone of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+
+
+
+
+
+
+ Returns the end bone name of the bone chain.
+
+
+
+
+
+
+
+ Returns the bone index at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+
+ Returns the bone name at [param joint] in the bone chain's joint list.
+
+
+
+
+
+
+ Returns the joint count of the bone chain's joint list.
+
+
+
+
+
+
+
+ Returns the twist amount at [param joint] in the bone chain's joint list when [method get_disperse_mode] is [constant DISPERSE_MODE_CUSTOM].
+
+
+
+
+
+
+ Returns the reference bone to extract twist of the setting at [param index].
+ This bone is either the end of the chain or its parent, depending on [method is_end_bone_extended].
+
+
+
+
+
+
+ Returns the reference bone name to extract twist of the setting at [param index].
+ This bone is either the end of the chain or its parent, depending on [method is_end_bone_extended].
+
+
+
+
+
+
+ Returns the root bone index of the bone chain.
+
+
+
+
+
+
+ Returns the root bone name of the bone chain.
+
+
+
+
+
+
+ Returns the rotation to an arbitrary state before twisting for the current bone pose to extract the twist when [method is_twist_from_rest] is [code]false[/code].
+
+
+
+
+
+
+ Returns the position at which to divide the segment between joints for weight assignment when [method get_disperse_mode] is [constant DISPERSE_MODE_WEIGHTED].
+
+
+
+
+
+
+ Returns [code]true[/code] if the end bone is extended to have a tail.
+
+
+
+
+
+
+ Returns [code]true[/code] if extracting the twist amount from the difference between the bone rest and the current bone pose.
+
+
+
+
+
+
+
+ Sets the damping curve when [method get_disperse_mode] is [constant DISPERSE_MODE_CUSTOM].
+
+
+
+
+
+
+
+ Sets whether to use automatic amount assignment or to allow manual assignment.
+
+
+
+
+
+
+
+ Sets the end bone index of the bone chain.
+
+
+
+
+
+
+
+ Sets the end bone tail direction of the bone chain when [method is_end_bone_extended] is [code]true[/code].
+
+
+
+
+
+
+
+ Sets the end bone name of the bone chain.
+ [b]Note:[/b] The end bone must be a child of the root bone.
+
+
+
+
+
+
+
+ If [param enabled] is [code]true[/code], the end bone is extended to have a tail.
+ If [param enabled] is [code]false[/code], [method get_reference_bone] becomes a parent of the end bone and it uses the vector to the end bone as a twist axis.
+
+
+
+
+
+
+
+
+ Sets the twist amount at [param joint] in the bone chain's joint list when [method get_disperse_mode] is [constant DISPERSE_MODE_CUSTOM].
+
+
+
+
+
+
+
+ Sets the root bone index of the bone chain.
+
+
+
+
+
+
+
+ Sets the root bone name of the bone chain.
+
+
+
+
+
+
+
+ Sets the rotation to an arbitrary state before twisting for the current bone pose to extract the twist when [method is_twist_from_rest] is [code]false[/code].
+ In other words, by calling [method set_twist_from] by [signal SkeletonModifier3D.modification_processed] of a specific [SkeletonModifier3D], you can extract only the twists generated by modifiers processed after that but before this [BoneTwistDisperser3D].
+
+
+
+
+
+
+
+ If [param enabled] is [code]true[/code], it extracts the twist amount from the difference between the bone rest and the current bone pose.
+ If [param enabled] is [code]false[/code], it extracts the twist amount from the difference between [method get_twist_from] and the current bone pose. See also [method set_twist_from].
+
+
+
+
+
+
+
+ Sets the position at which to divide the segment between joints for weight assignment when [method get_disperse_mode] is [constant DISPERSE_MODE_WEIGHTED].
+ For example, when [param weight_position] is [code]0.5[/code], if two bone segments with a length of [code]1.0[/code] exist between three joints, weights are assigned to each joint from root to end at ratios of [code]0.5[/code], [code]1.0[/code], and [code]0.5[/code]. Then amounts become [code]0.25[/code], [code]0.75[/code], and [code]1.0[/code] respectively.
+
+
+
+
+
+ If [code]true[/code], the solver retrieves the bone axis from the bone pose every frame.
+ If [code]false[/code], the solver retrieves the bone axis from the bone rest and caches it.
+
+
+ The number of settings.
+
+
+
+
+ Assign amounts so that they monotonically increase from [code]0.0[/code] to [code]1.0[/code], ensuring all weights are equal. For example, with five joints, the amounts would be [code]0.2[/code], [code]0.4[/code], [code]0.6[/code], [code]0.8[/code], and [code]1.0[/code] starting from the root bone.
+
+
+ Assign amounts so that they monotonically increase from [code]0.0[/code] to [code]1.0[/code], based on the length of the bones between joint segments. See also [method set_weight_position].
+
+
+ You can assign arbitrary amounts to the joint list. See also [method set_joint_twist_amount].
+ When [method is_end_bone_extended] is [code]false[/code], a child of the reference bone exists solely to determine the twist axis, so its custom amount has absolutely no effect at all.
+
+
+
diff --git a/doc/classes/ConvertTransformModifier3D.xml b/doc/classes/ConvertTransformModifier3D.xml
index e6845eff41f..0c8b28588df 100644
--- a/doc/classes/ConvertTransformModifier3D.xml
+++ b/doc/classes/ConvertTransformModifier3D.xml
@@ -15,6 +15,7 @@
[b]Not Relative + Not Additive:[/b]
- Extract reference pose absolutely and the apply bone's pose is replaced with it.
[b]Note:[/b] Relative option is available only in the case [method BoneConstraint3D.get_reference_type] is [constant BoneConstraint3D.REFERENCE_TYPE_BONE]. See also [enum BoneConstraint3D.ReferenceType].
+ [b]Note:[/b] If there is a rotation greater than [code]180[/code] degrees with constrained axes, flipping may occur.
diff --git a/editor/icons/BoneTwistDisperser3D.svg b/editor/icons/BoneTwistDisperser3D.svg
new file mode 100644
index 00000000000..e8d2307e521
--- /dev/null
+++ b/editor/icons/BoneTwistDisperser3D.svg
@@ -0,0 +1 @@
+
diff --git a/scene/3d/bone_twist_disperser_3d.cpp b/scene/3d/bone_twist_disperser_3d.cpp
new file mode 100644
index 00000000000..d1da52215ab
--- /dev/null
+++ b/scene/3d/bone_twist_disperser_3d.cpp
@@ -0,0 +1,814 @@
+/**************************************************************************/
+/* bone_twist_disperser_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 "bone_twist_disperser_3d.h"
+
+bool BoneTwistDisperser3D::_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, (int)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") {
+ set_end_bone(which, p_value);
+ } else if (what == "end_bone_direction") {
+ set_end_bone_direction(which, static_cast((int)p_value));
+ } else if (what == "extend_end_bone") {
+ set_extend_end_bone(which, p_value);
+ } else if (what == "twist_from_rest") {
+ set_twist_from_rest(which, p_value);
+ } else if (what == "twist_from") {
+ set_twist_from(which, p_value);
+ } else if (what == "disperse_mode") {
+ set_disperse_mode(which, static_cast((int)p_value));
+ } else if (what == "weight_position") {
+ set_weight_position(which, p_value);
+ } else if (what == "damping_curve") {
+ set_damping_curve(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 == "twist_amount") {
+ set_joint_twist_amount(which, idx, p_value);
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool BoneTwistDisperser3D::_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, (int)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") {
+ r_ret = get_end_bone(which);
+ } else if (what == "end_bone_direction") {
+ r_ret = (int)get_end_bone_direction(which);
+ } else if (what == "reference_bone_name") {
+ r_ret = get_reference_bone_name(which);
+ } else if (what == "extend_end_bone") {
+ r_ret = is_end_bone_extended(which);
+ } else if (what == "twist_from_rest") {
+ r_ret = is_twist_from_rest(which);
+ } else if (what == "twist_from") {
+ r_ret = get_twist_from(which);
+ } else if (what == "disperse_mode") {
+ r_ret = (int)get_disperse_mode(which);
+ } else if (what == "weight_position") {
+ r_ret = get_weight_position(which);
+ } else if (what == "damping_curve") {
+ r_ret = get_damping_curve(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 == "twist_amount") {
+ r_ret = get_joint_twist_amount(which, idx);
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+}
+
+void BoneTwistDisperser3D::_get_property_list(List *p_list) const {
+ String enum_hint;
+ Skeleton3D *skeleton = get_skeleton();
+ if (skeleton) {
+ enum_hint = skeleton->get_concatenated_bone_names();
+ }
+
+ LocalVector props;
+
+ for (uint32_t i = 0; i < settings.size(); i++) {
+ String path = "settings/" + itos(i) + "/";
+ props.push_back(PropertyInfo(Variant::STRING, path + "root_bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint));
+ props.push_back(PropertyInfo(Variant::INT, path + "root_bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
+ props.push_back(PropertyInfo(Variant::STRING, path + "end_bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint));
+ props.push_back(PropertyInfo(Variant::INT, path + "end_bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
+ props.push_back(PropertyInfo(Variant::BOOL, path + "extend_end_bone"));
+ props.push_back(PropertyInfo(Variant::INT, path + "end_bone_direction", PROPERTY_HINT_ENUM, SkeletonModifier3D::get_hint_bone_direction()));
+
+ props.push_back(PropertyInfo(Variant::STRING, path + "reference_bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint, PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY));
+ props.push_back(PropertyInfo(Variant::BOOL, path + "twist_from_rest"));
+ props.push_back(PropertyInfo(Variant::QUATERNION, path + "twist_from"));
+ props.push_back(PropertyInfo(Variant::INT, path + "disperse_mode", PROPERTY_HINT_ENUM, "Even,Weighted,Custom"));
+ props.push_back(PropertyInfo(Variant::FLOAT, path + "weight_position", PROPERTY_HINT_RANGE, "0,1,0.001"));
+ props.push_back(PropertyInfo(Variant::OBJECT, path + "damping_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"));
+
+ props.push_back(PropertyInfo(Variant::INT, path + "joint_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, "Joints," + path + "joints/,static,const"));
+ for (uint32_t j = 0; j < settings[i]->joints.size(); j++) {
+ String joint_path = path + "joints/" + itos(j) + "/";
+ props.push_back(PropertyInfo(Variant::STRING, joint_path + "bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint, PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY));
+ props.push_back(PropertyInfo(Variant::INT, joint_path + "bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_READ_ONLY));
+ props.push_back(PropertyInfo(Variant::FLOAT, joint_path + "twist_amount", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,or_less"));
+ }
+ }
+
+ for (PropertyInfo &p : props) {
+ _validate_dynamic_prop(p);
+ p_list->push_back(p);
+ }
+}
+
+void BoneTwistDisperser3D::_validate_dynamic_prop(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.
+ bool force_hide = false;
+ if (split[2] == "extend_end_bone" && get_end_bone(which) == -1) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ force_hide = true;
+ }
+ if (force_hide || (split[2] == "end_bone_direction" && !is_end_bone_extended(which))) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+
+ if (split[2] == "twist_from" && is_twist_from_rest(which)) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+
+ if (split[2] == "weight_position" && get_disperse_mode(which) != DISPERSE_MODE_WEIGHTED) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+
+ if (split[2] == "damping_curve" && get_disperse_mode(which) != DISPERSE_MODE_CUSTOM) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+
+ if (split[2] == "joints" && split[4] == "twist_amount") {
+ bool mutable_amount = true;
+ if (get_disperse_mode(which) != DISPERSE_MODE_CUSTOM) {
+ mutable_amount = false;
+ } else if (!is_end_bone_extended(which)) {
+ int joint = split[3].to_int();
+ mutable_amount = joint < get_joint_count(which) - 1; // Hide child of reference bone.
+ }
+ if (get_damping_curve(which).is_valid()) {
+ p_property.usage |= PROPERTY_USAGE_READ_ONLY;
+ }
+ if (!mutable_amount) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+ }
+ }
+}
+
+void BoneTwistDisperser3D::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE: {
+ _make_all_joints_dirty();
+ } break;
+ }
+}
+
+void BoneTwistDisperser3D::_set_active(bool p_active) {
+ if (p_active) {
+ _make_all_joints_dirty();
+ }
+}
+
+void BoneTwistDisperser3D::_skeleton_changed(Skeleton3D *p_old, Skeleton3D *p_new) {
+ _make_all_joints_dirty();
+}
+
+// Setting.
+
+void BoneTwistDisperser3D::set_mutable_bone_axes(bool p_enabled) {
+ mutable_bone_axes = p_enabled;
+}
+
+bool BoneTwistDisperser3D::are_bone_axes_mutable() const {
+ return mutable_bone_axes;
+}
+
+void BoneTwistDisperser3D::set_root_bone_name(int p_index, const String &p_bone_name) {
+ ERR_FAIL_INDEX(p_index, (int)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 BoneTwistDisperser3D::get_root_bone_name(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), String());
+ return settings[p_index]->root_bone.name;
+}
+
+void BoneTwistDisperser3D::set_root_bone(int p_index, int p_bone) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ bool changed = settings[p_index]->root_bone.bone != p_bone;
+ settings[p_index]->root_bone.bone = p_bone;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ if (settings[p_index]->root_bone.bone <= -1 || settings[p_index]->root_bone.bone >= sk->get_bone_count()) {
+ WARN_PRINT("Root bone index out of range!");
+ settings[p_index]->root_bone.bone = -1;
+ } else {
+ settings[p_index]->root_bone.name = sk->get_bone_name(settings[p_index]->root_bone.bone);
+ }
+ }
+ if (changed) {
+ _make_joints_dirty(p_index);
+ }
+}
+
+int BoneTwistDisperser3D::get_root_bone(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), -1);
+ return settings[p_index]->root_bone.bone;
+}
+
+void BoneTwistDisperser3D::set_end_bone_name(int p_index, const String &p_bone_name) {
+ ERR_FAIL_INDEX(p_index, (int)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 BoneTwistDisperser3D::get_end_bone_name(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), String());
+ return settings[p_index]->end_bone.name;
+}
+
+void BoneTwistDisperser3D::set_end_bone(int p_index, int p_bone) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ bool changed = settings[p_index]->end_bone.bone != p_bone;
+ settings[p_index]->end_bone.bone = p_bone;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ if (settings[p_index]->end_bone.bone <= -1 || settings[p_index]->end_bone.bone >= sk->get_bone_count()) {
+ WARN_PRINT("End bone index out of range!");
+ settings[p_index]->end_bone.bone = -1;
+ } else {
+ settings[p_index]->end_bone.name = sk->get_bone_name(settings[p_index]->end_bone.bone);
+ }
+ }
+ if (changed) {
+ _make_joints_dirty(p_index);
+ }
+ notify_property_list_changed();
+}
+
+int BoneTwistDisperser3D::get_end_bone(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), -1);
+ return settings[p_index]->end_bone.bone;
+}
+
+void BoneTwistDisperser3D::set_extend_end_bone(int p_index, bool p_enabled) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ settings[p_index]->extend_end_bone = p_enabled;
+ _update_reference_bone(p_index);
+ notify_property_list_changed();
+}
+
+bool BoneTwistDisperser3D::is_end_bone_extended(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), false);
+ return settings[p_index]->extend_end_bone;
+}
+
+void BoneTwistDisperser3D::set_end_bone_direction(int p_index, BoneDirection p_bone_direction) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ settings[p_index]->end_bone_direction = p_bone_direction;
+}
+
+SkeletonModifier3D::BoneDirection BoneTwistDisperser3D::get_end_bone_direction(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), BONE_DIRECTION_FROM_PARENT);
+ return settings[p_index]->end_bone_direction;
+}
+
+void BoneTwistDisperser3D::set_twist_from_rest(int p_index, bool p_enabled) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ settings[p_index]->twist_from_rest = p_enabled;
+ notify_property_list_changed();
+}
+
+bool BoneTwistDisperser3D::is_twist_from_rest(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), true);
+ return settings[p_index]->twist_from_rest;
+}
+
+void BoneTwistDisperser3D::set_twist_from(int p_index, const Quaternion &p_from) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ settings[p_index]->twist_from = p_from;
+}
+
+Quaternion BoneTwistDisperser3D::get_twist_from(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), Quaternion());
+ return settings[p_index]->twist_from;
+}
+
+void BoneTwistDisperser3D::_update_reference_bone(int p_index) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ LocalVector &joints = settings[p_index]->joints;
+ if (joints.size() >= 2) {
+ if (settings[p_index]->extend_end_bone) {
+ settings[p_index]->reference_bone = settings[p_index]->end_bone;
+ _update_curve(p_index);
+ return;
+ } else {
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ int parent = sk->get_bone_parent(settings[p_index]->end_bone.bone);
+ if (parent >= 0) {
+ settings[p_index]->reference_bone.bone = parent;
+ settings[p_index]->reference_bone.name = sk->get_bone_name(parent);
+ _update_curve(p_index);
+ return;
+ }
+ }
+ }
+ }
+ settings[p_index]->reference_bone.bone = -1;
+ settings[p_index]->reference_bone.name = String();
+}
+
+void BoneTwistDisperser3D::_update_curve(int p_index) {
+ Ref curve = settings[p_index]->damping_curve;
+ if (curve.is_null()) {
+ return;
+ }
+ LocalVector &joints = settings[p_index]->joints;
+ float unit = (int)joints.size() > 0 ? (1.0 / float((int)joints.size() - 1)) : 0.0;
+ for (uint32_t i = 0; i < joints.size(); i++) {
+ joints[i].custom_amount = curve->sample_baked(i * unit);
+ }
+}
+
+String BoneTwistDisperser3D::get_reference_bone_name(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), String());
+ return settings[p_index]->reference_bone.name;
+}
+
+int BoneTwistDisperser3D::get_reference_bone(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), -1);
+ return settings[p_index]->reference_bone.bone;
+}
+
+void BoneTwistDisperser3D::set_disperse_mode(int p_index, DisperseMode p_disperse_mode) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ settings[p_index]->disperse_mode = p_disperse_mode;
+ notify_property_list_changed();
+}
+
+BoneTwistDisperser3D::DisperseMode BoneTwistDisperser3D::get_disperse_mode(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), DISPERSE_MODE_EVEN);
+ return settings[p_index]->disperse_mode;
+}
+
+void BoneTwistDisperser3D::set_weight_position(int p_index, float p_position) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ settings[p_index]->weight_position = p_position;
+}
+
+float BoneTwistDisperser3D::get_weight_position(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), 0.0);
+ return settings[p_index]->weight_position;
+}
+
+void BoneTwistDisperser3D::set_damping_curve(int p_index, const Ref &p_damping_curve) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ bool changed = settings[p_index]->damping_curve != p_damping_curve;
+ if (settings[p_index]->damping_curve.is_valid()) {
+ settings[p_index]->damping_curve->disconnect_changed(callable_mp(this, &BoneTwistDisperser3D::_update_curve));
+ }
+ settings[p_index]->damping_curve = p_damping_curve;
+ if (settings[p_index]->damping_curve.is_valid()) {
+ settings[p_index]->damping_curve->connect_changed(callable_mp(this, &BoneTwistDisperser3D::_update_curve).bind(p_index));
+ }
+ if (changed) {
+ _make_joints_dirty(p_index);
+ }
+ notify_property_list_changed();
+}
+
+Ref BoneTwistDisperser3D::get_damping_curve(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), Ref());
+ return settings[p_index]->damping_curve;
+}
+
+// Individual joints.
+
+void BoneTwistDisperser3D::set_joint_bone_name(int p_index, int p_joint, const String &p_bone_name) {
+ // Exists only for indicate bone name on the inspector, no needs to make dirty joint array.
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ LocalVector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, (int)joints.size());
+ joints[p_joint].joint.name = p_bone_name;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ set_joint_bone(p_index, p_joint, sk->find_bone(joints[p_joint].joint.name));
+ }
+}
+
+String BoneTwistDisperser3D::get_joint_bone_name(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), String());
+ const LocalVector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, (int)joints.size(), String());
+ return joints[p_joint].joint.name;
+}
+
+void BoneTwistDisperser3D::set_joint_bone(int p_index, int p_joint, int p_bone) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ LocalVector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, (int)joints.size());
+ joints[p_joint].joint.bone = p_bone;
+ Skeleton3D *sk = get_skeleton();
+ if (sk) {
+ if (joints[p_joint].joint.bone <= -1 || joints[p_joint].joint.bone >= sk->get_bone_count()) {
+ WARN_PRINT("Joint bone index out of range!");
+ joints[p_joint].joint.bone = -1;
+ } else {
+ joints[p_joint].joint.name = sk->get_bone_name(joints[p_joint].joint.bone);
+ }
+ }
+}
+
+int BoneTwistDisperser3D::get_joint_bone(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), -1);
+ const LocalVector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, (int)joints.size(), -1);
+ return joints[p_joint].joint.bone;
+}
+
+void BoneTwistDisperser3D::set_joint_count(int p_index, int p_count) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ ERR_FAIL_COND(p_count < 0);
+ LocalVector &joints = settings[p_index]->joints;
+ joints.resize(p_count);
+ notify_property_list_changed();
+}
+
+int BoneTwistDisperser3D::get_joint_count(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), 0);
+ const LocalVector &joints = settings[p_index]->joints;
+ return joints.size();
+}
+
+void BoneTwistDisperser3D::set_joint_twist_amount(int p_index, int p_joint, float p_amount) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ LocalVector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX(p_joint, (int)joints.size());
+ joints[p_joint].custom_amount = p_amount;
+}
+
+float BoneTwistDisperser3D::get_joint_twist_amount(int p_index, int p_joint) const {
+ ERR_FAIL_INDEX_V(p_index, (int)settings.size(), 0);
+ const LocalVector &joints = settings[p_index]->joints;
+ ERR_FAIL_INDEX_V(p_joint, (int)joints.size(), 0);
+ return joints[p_joint].custom_amount;
+}
+
+void BoneTwistDisperser3D::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_setting_count", "count"), &BoneTwistDisperser3D::set_setting_count);
+ ClassDB::bind_method(D_METHOD("get_setting_count"), &BoneTwistDisperser3D::get_setting_count);
+ ClassDB::bind_method(D_METHOD("clear_settings"), &BoneTwistDisperser3D::clear_settings);
+
+ ClassDB::bind_method(D_METHOD("set_mutable_bone_axes", "enabled"), &BoneTwistDisperser3D::set_mutable_bone_axes);
+ ClassDB::bind_method(D_METHOD("are_bone_axes_mutable"), &BoneTwistDisperser3D::are_bone_axes_mutable);
+
+ // Setting.
+ ClassDB::bind_method(D_METHOD("set_root_bone_name", "index", "bone_name"), &BoneTwistDisperser3D::set_root_bone_name);
+ ClassDB::bind_method(D_METHOD("get_root_bone_name", "index"), &BoneTwistDisperser3D::get_root_bone_name);
+ ClassDB::bind_method(D_METHOD("set_root_bone", "index", "bone"), &BoneTwistDisperser3D::set_root_bone);
+ ClassDB::bind_method(D_METHOD("get_root_bone", "index"), &BoneTwistDisperser3D::get_root_bone);
+
+ ClassDB::bind_method(D_METHOD("set_end_bone_name", "index", "bone_name"), &BoneTwistDisperser3D::set_end_bone_name);
+ ClassDB::bind_method(D_METHOD("get_end_bone_name", "index"), &BoneTwistDisperser3D::get_end_bone_name);
+ ClassDB::bind_method(D_METHOD("set_end_bone", "index", "bone"), &BoneTwistDisperser3D::set_end_bone);
+ ClassDB::bind_method(D_METHOD("get_end_bone", "index"), &BoneTwistDisperser3D::get_end_bone);
+
+ ClassDB::bind_method(D_METHOD("get_reference_bone_name", "index"), &BoneTwistDisperser3D::get_reference_bone_name);
+ ClassDB::bind_method(D_METHOD("get_reference_bone", "index"), &BoneTwistDisperser3D::get_reference_bone);
+
+ ClassDB::bind_method(D_METHOD("set_extend_end_bone", "index", "enabled"), &BoneTwistDisperser3D::set_extend_end_bone);
+ ClassDB::bind_method(D_METHOD("is_end_bone_extended", "index"), &BoneTwistDisperser3D::is_end_bone_extended);
+ ClassDB::bind_method(D_METHOD("set_end_bone_direction", "index", "bone_direction"), &BoneTwistDisperser3D::set_end_bone_direction);
+ ClassDB::bind_method(D_METHOD("get_end_bone_direction", "index"), &BoneTwistDisperser3D::get_end_bone_direction);
+
+ ClassDB::bind_method(D_METHOD("set_twist_from_rest", "index", "enabled"), &BoneTwistDisperser3D::set_twist_from_rest);
+ ClassDB::bind_method(D_METHOD("is_twist_from_rest", "index"), &BoneTwistDisperser3D::is_twist_from_rest);
+ ClassDB::bind_method(D_METHOD("set_twist_from", "index", "from"), &BoneTwistDisperser3D::set_twist_from);
+ ClassDB::bind_method(D_METHOD("get_twist_from", "index"), &BoneTwistDisperser3D::get_twist_from);
+
+ ClassDB::bind_method(D_METHOD("set_disperse_mode", "index", "disperse_mode"), &BoneTwistDisperser3D::set_disperse_mode);
+ ClassDB::bind_method(D_METHOD("get_disperse_mode", "index"), &BoneTwistDisperser3D::get_disperse_mode);
+ ClassDB::bind_method(D_METHOD("set_weight_position", "index", "weight_position"), &BoneTwistDisperser3D::set_weight_position);
+ ClassDB::bind_method(D_METHOD("get_weight_position", "index"), &BoneTwistDisperser3D::get_weight_position);
+ ClassDB::bind_method(D_METHOD("set_damping_curve", "index", "curve"), &BoneTwistDisperser3D::set_damping_curve);
+ ClassDB::bind_method(D_METHOD("get_damping_curve", "index"), &BoneTwistDisperser3D::get_damping_curve);
+
+ // Individual joints.
+ ClassDB::bind_method(D_METHOD("get_joint_bone_name", "index", "joint"), &BoneTwistDisperser3D::get_joint_bone_name);
+ ClassDB::bind_method(D_METHOD("get_joint_bone", "index", "joint"), &BoneTwistDisperser3D::get_joint_bone);
+ ClassDB::bind_method(D_METHOD("get_joint_twist_amount", "index", "joint"), &BoneTwistDisperser3D::get_joint_twist_amount);
+ ClassDB::bind_method(D_METHOD("set_joint_twist_amount", "index", "joint", "twist_amount"), &BoneTwistDisperser3D::set_joint_twist_amount);
+
+ ClassDB::bind_method(D_METHOD("get_joint_count", "index"), &BoneTwistDisperser3D::get_joint_count);
+
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "mutable_bone_axes"), "set_mutable_bone_axes", "are_bone_axes_mutable");
+ ADD_ARRAY_COUNT("Settings", "setting_count", "set_setting_count", "get_setting_count", "settings/");
+
+ BIND_ENUM_CONSTANT(DISPERSE_MODE_EVEN);
+ BIND_ENUM_CONSTANT(DISPERSE_MODE_WEIGHTED);
+ BIND_ENUM_CONSTANT(DISPERSE_MODE_CUSTOM);
+}
+
+void BoneTwistDisperser3D::_validate_bone_names() {
+ for (uint32_t i = 0; i < settings.size(); i++) {
+ // Prior bone name.
+ if (!settings[i]->root_bone.name.is_empty()) {
+ set_root_bone_name(i, settings[i]->root_bone.name);
+ } else if (settings[i]->root_bone.bone != -1) {
+ set_root_bone(i, settings[i]->root_bone.bone);
+ }
+ // Prior bone name.
+ if (!settings[i]->end_bone.name.is_empty()) {
+ set_end_bone_name(i, settings[i]->end_bone.name);
+ } else if (settings[i]->end_bone.bone != -1) {
+ set_end_bone(i, settings[i]->end_bone.bone);
+ }
+ }
+}
+
+void BoneTwistDisperser3D::_make_all_joints_dirty() {
+ for (uint32_t i = 0; i < settings.size(); i++) {
+ _make_joints_dirty(i);
+ }
+}
+
+void BoneTwistDisperser3D::_make_joints_dirty(int p_index) {
+ ERR_FAIL_INDEX(p_index, (int)settings.size());
+ if (settings[p_index]->joints_dirty) {
+ return;
+ }
+ settings[p_index]->joints_dirty = true;
+ callable_mp(this, &BoneTwistDisperser3D::_update_joints).call_deferred(p_index);
+}
+
+void BoneTwistDisperser3D::_update_joints(int p_index) {
+ Skeleton3D *sk = get_skeleton();
+ int current_bone = settings[p_index]->end_bone.bone;
+ int root_bone = settings[p_index]->root_bone.bone;
+ if (!sk || current_bone < 0 || root_bone < 0) {
+ set_joint_count(p_index, 0);
+ settings[p_index]->joints_dirty = false;
+ return;
+ }
+
+ // Validation.
+ bool valid = false;
+ while (current_bone >= 0) {
+ current_bone = sk->get_bone_parent(current_bone);
+ if (current_bone == root_bone) {
+ valid = true;
+ break;
+ }
+ }
+
+ if (!valid) {
+ set_joint_count(p_index, 0);
+ _update_reference_bone(p_index);
+ settings[p_index]->joints_dirty = false;
+ ERR_FAIL_EDMSG("End bone must be a child of the root bone.");
+ }
+
+ Vector new_joints;
+ current_bone = settings[p_index]->end_bone.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 (uint32_t i = 0; i < new_joints.size(); i++) {
+ set_joint_bone(p_index, i, new_joints[i]);
+ }
+
+ _update_reference_bone(p_index);
+ settings[p_index]->joints_dirty = false;
+}
+
+int BoneTwistDisperser3D::get_setting_count() const {
+ return (int)settings.size();
+}
+
+void BoneTwistDisperser3D::set_setting_count(int p_count) {
+ ERR_FAIL_COND(p_count < 0);
+ int delta = p_count - settings.size();
+ if (delta < 0) {
+ for (int i = delta; i < 0; i++) {
+ memdelete(settings[settings.size() + i]);
+ settings[settings.size() + i] = nullptr;
+ }
+ }
+ settings.resize(p_count);
+ delta++;
+ if (delta > 1) {
+ for (int i = 1; i < delta; i++) {
+ settings[p_count - i] = memnew(BoneTwistDisperser3DSetting);
+ }
+ }
+ notify_property_list_changed();
+}
+
+void BoneTwistDisperser3D::clear_settings() {
+ set_setting_count(0);
+}
+
+void BoneTwistDisperser3D::_process_modification(double p_delta) {
+ Skeleton3D *skeleton = get_skeleton();
+ if (!skeleton) {
+ return;
+ }
+ for (BoneTwistDisperser3DSetting *setting : settings) {
+ if (!setting || setting->reference_bone.bone < 0) {
+ continue;
+ }
+ LocalVector &joints = setting->joints;
+ // Calc amount.
+ int actual_joint_size = setting->extend_end_bone ? (int)joints.size() : (int)joints.size() - 1;
+ if (actual_joint_size <= 1) {
+ continue;
+ }
+ if (setting->disperse_mode == DISPERSE_MODE_EVEN) {
+ double div = 1.0 / actual_joint_size;
+ for (int i = 0; i < actual_joint_size; i++) {
+ joints[i].amount = ((double)i + 1.0) * div;
+ }
+ } else if (setting->disperse_mode == DISPERSE_MODE_WEIGHTED) {
+ // Assign length for each bone.
+ double total_length = 0.0;
+ double weight_sub = 1.0 - setting->weight_position;
+ if (mutable_bone_axes) {
+ for (int i = 0; i < actual_joint_size; i++) {
+ double length = 0.0;
+ if (i == 0) {
+ length = skeleton->get_bone_pose_position(joints[i + 1].joint.bone).length() * setting->weight_position;
+ } else if (i == actual_joint_size - 1) {
+ length = skeleton->get_bone_pose_position(joints[i].joint.bone).length() * weight_sub;
+ } else {
+ length = skeleton->get_bone_pose_position(joints[i].joint.bone).length() * setting->weight_position + skeleton->get_bone_pose_position(joints[i + 1].joint.bone).length() * weight_sub;
+ }
+ total_length += length;
+ joints[i].amount = total_length;
+ }
+ } else {
+ for (int i = 0; i < actual_joint_size; i++) {
+ double length = 0.0;
+ if (i == 0) {
+ length = skeleton->get_bone_rest(joints[i + 1].joint.bone).origin.length() * setting->weight_position;
+ } else if (i == actual_joint_size - 1) {
+ length = skeleton->get_bone_rest(joints[i].joint.bone).origin.length() * weight_sub;
+ } else {
+ length = skeleton->get_bone_rest(joints[i].joint.bone).origin.length() * setting->weight_position + skeleton->get_bone_rest(joints[i + 1].joint.bone).origin.length() * weight_sub;
+ }
+ total_length += length;
+ joints[i].amount = total_length;
+ }
+ }
+ if (Math::is_zero_approx(total_length)) {
+ continue;
+ }
+ // Normalize.
+ double div = 1.0 / total_length;
+ for (int i = 0; i < actual_joint_size; i++) {
+ joints[i].amount *= div;
+ }
+ } else {
+ for (int i = 0; i < actual_joint_size; i++) {
+ joints[i].amount = joints[i].custom_amount;
+ }
+ }
+ int end = actual_joint_size - 1;
+ joints[end].amount -= 1.0; // Remove twist from current pose.
+
+ // Retrieve axes.
+ if (mutable_bone_axes) {
+ for (int i = 0; i < end; i++) {
+ joints[i].axis = skeleton->get_bone_pose_position(joints[i + 1].joint.bone).normalized();
+ if (joints[i].axis.is_zero_approx() && i > 0) {
+ joints[i].axis = joints[i - 1].axis;
+ }
+ }
+ } else {
+ for (int i = 0; i < end; i++) {
+ joints[i].axis = skeleton->get_bone_rest(joints[i + 1].joint.bone).origin.normalized();
+ if (joints[i].axis.is_zero_approx() && i > 0) {
+ joints[i].axis = joints[i - 1].axis;
+ }
+ }
+ }
+
+ if (!setting->extend_end_bone) {
+ joints[end].axis = mutable_bone_axes ? skeleton->get_bone_pose_position(setting->end_bone.bone) : skeleton->get_bone_rest(setting->end_bone.bone).origin;
+ joints[end].axis.normalize();
+ } else if (setting->end_bone_direction == BONE_DIRECTION_FROM_PARENT) {
+ joints[end].axis = skeleton->get_bone_rest(setting->end_bone.bone).basis.xform_inv(mutable_bone_axes ? skeleton->get_bone_pose_position(setting->end_bone.bone) : skeleton->get_bone_rest(setting->end_bone.bone).origin);
+ joints[end].axis.normalize();
+ } else {
+ joints[end].axis = get_vector_from_bone_axis(static_cast((int)setting->end_bone_direction));
+ }
+ if (joints[end].axis.is_zero_approx() && end > 0) {
+ joints[end].axis = joints[end - 1].axis;
+ }
+
+ // Extract twist.
+ Quaternion twist_rest = setting->twist_from_rest ? skeleton->get_bone_rest(setting->reference_bone.bone).basis.get_rotation_quaternion() : setting->twist_from.normalized();
+ Quaternion ref_rot = twist_rest.inverse() * skeleton->get_bone_pose_rotation(setting->reference_bone.bone);
+ ref_rot.normalize();
+ double twist = get_roll_angle(ref_rot, joints[end].axis);
+
+ // Apply twist for each bone by their amount.
+ // Twist parent, then cancel all twists caused by this modifier in child, and re-apply accumulated twist.
+ Quaternion prev_rot;
+ if (mutable_bone_axes) {
+ for (int i = 0; i < actual_joint_size; i++) {
+ int bn = joints[i].joint.bone;
+ Quaternion cur_rot = Quaternion(joints[i].axis, twist * joints[i].amount);
+ skeleton->set_bone_pose_rotation(bn, prev_rot.inverse() * skeleton->get_bone_pose_rotation(bn) * cur_rot);
+ prev_rot = cur_rot;
+ }
+ } else {
+ for (int i = 0; i < actual_joint_size; i++) {
+ int bn = joints[i].joint.bone;
+ Quaternion cur_rot = Quaternion(joints[i].axis, twist * joints[i].amount);
+ skeleton->set_bone_pose_rotation(bn, prev_rot.inverse() * skeleton->get_bone_pose_rotation(bn) * cur_rot);
+ prev_rot = cur_rot;
+ }
+ }
+ }
+}
+
+BoneTwistDisperser3D::~BoneTwistDisperser3D() {
+ clear_settings();
+}
diff --git a/scene/3d/bone_twist_disperser_3d.h b/scene/3d/bone_twist_disperser_3d.h
new file mode 100644
index 00000000000..d9bad0f7bba
--- /dev/null
+++ b/scene/3d/bone_twist_disperser_3d.h
@@ -0,0 +1,163 @@
+/**************************************************************************/
+/* bone_twist_disperser_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. */
+/**************************************************************************/
+
+#pragma once
+
+#include "scene/3d/skeleton_modifier_3d.h"
+
+class BoneTwistDisperser3D : public SkeletonModifier3D {
+ GDCLASS(BoneTwistDisperser3D, SkeletonModifier3D);
+
+ bool mutable_bone_axes = true;
+
+public:
+ enum DisperseMode {
+ DISPERSE_MODE_EVEN,
+ DISPERSE_MODE_WEIGHTED,
+ DISPERSE_MODE_CUSTOM,
+ };
+
+ struct BoneJoint {
+ StringName name;
+ int bone = -1;
+ };
+
+ struct DisperseJointSetting {
+ BoneJoint joint;
+ double custom_amount = 1.0;
+ // For processing.
+ double amount = 1.0;
+ Vector3 axis;
+ };
+
+ struct BoneTwistDisperser3DSetting {
+ bool joints_dirty = false;
+
+ DisperseMode disperse_mode = DISPERSE_MODE_EVEN;
+ bool twist_from_rest = true;
+ Quaternion twist_from;
+
+ BoneJoint root_bone;
+ BoneJoint end_bone;
+ LocalVector joints;
+
+ bool extend_end_bone = false;
+ BoneDirection end_bone_direction = BONE_DIRECTION_FROM_PARENT;
+
+ float weight_position = 0.5;
+ Ref damping_curve;
+
+ BoneJoint reference_bone; // To cache.
+
+ ~BoneTwistDisperser3DSetting() {
+ joints.clear();
+ damping_curve.unref();
+ }
+ };
+
+protected:
+ LocalVector settings;
+
+ bool _get(const StringName &p_path, Variant &r_ret) const;
+ bool _set(const StringName &p_path, const Variant &p_value);
+ void _get_property_list(List *p_list) const;
+ void _validate_dynamic_prop(PropertyInfo &p_property) const;
+
+ void _notification(int p_what);
+ static void _bind_methods();
+
+ virtual void _set_active(bool p_active) override;
+ virtual void _skeleton_changed(Skeleton3D *p_old, Skeleton3D *p_new) override;
+ virtual void _validate_bone_names() override;
+
+ void _make_all_joints_dirty();
+
+ void _make_joints_dirty(int p_index);
+ void _update_joints(int p_index);
+ void _update_reference_bone(int p_index);
+ void _update_curve(int p_index);
+
+ virtual void _process_modification(double p_delta) override;
+
+public:
+ void set_mutable_bone_axes(bool p_enabled);
+ bool are_bone_axes_mutable() const;
+
+ int get_setting_count() const;
+ void set_setting_count(int p_count);
+ void clear_settings();
+
+ // 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_twist_from_rest(int p_index, bool p_enabled);
+ bool is_twist_from_rest(int p_index) const;
+ void set_twist_from(int p_index, const Quaternion &p_from);
+ Quaternion get_twist_from(int p_index) const;
+
+ String get_reference_bone_name(int p_index) const;
+ int get_reference_bone(int p_index) const;
+
+ void set_disperse_mode(int p_index, DisperseMode p_disperse_mode);
+ DisperseMode get_disperse_mode(int p_index) const;
+ void set_weight_position(int p_index, float p_position);
+ float get_weight_position(int p_index) const;
+ void set_damping_curve(int p_index, const Ref &p_damping_curve);
+ Ref get_damping_curve(int p_index) const;
+
+ // Individual joints.
+ 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_twist_amount(int p_index, int p_joint, float p_amount);
+ float get_joint_twist_amount(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;
+
+ ~BoneTwistDisperser3D();
+};
+
+VARIANT_ENUM_CAST(BoneTwistDisperser3D::DisperseMode);
diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp
index f6efd44e59e..da9d0fbc949 100644
--- a/scene/register_scene_types.cpp
+++ b/scene/register_scene_types.cpp
@@ -220,6 +220,7 @@
#include "scene/3d/audio_stream_player_3d.h"
#include "scene/3d/bone_attachment_3d.h"
#include "scene/3d/bone_constraint_3d.h"
+#include "scene/3d/bone_twist_disperser_3d.h"
#include "scene/3d/camera_3d.h"
#include "scene/3d/ccd_ik_3d.h"
#include "scene/3d/chain_ik_3d.h"
@@ -684,6 +685,7 @@ void register_scene_types() {
GDREGISTER_CLASS(CCDIK3D);
GDREGISTER_CLASS(JacobianIK3D);
GDREGISTER_CLASS(LimitAngularVelocityModifier3D);
+ GDREGISTER_CLASS(BoneTwistDisperser3D);
#ifndef XR_DISABLED
GDREGISTER_CLASS(XRCamera3D);