From 8a0051dd68d24b0f268637f27bf443be225a0bff Mon Sep 17 00:00:00 2001 From: monxa <84077629+monxa@users.noreply.github.com> Date: Wed, 19 Mar 2025 20:26:35 +0100 Subject: [PATCH 1/8] Remove redundant namespace tags (#51) --- src/godot_entry.cpp | 2 +- src/godot_ik.cpp | 14 +++++++------- src/godot_ik_constraint.cpp | 10 +++++----- src/godot_ik_effector.cpp | 6 +++--- src/godot_ik_root.cpp | 6 +++--- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/godot_entry.cpp b/src/godot_entry.cpp index 27c87fa..abd403f 100644 --- a/src/godot_entry.cpp +++ b/src/godot_entry.cpp @@ -27,7 +27,7 @@ void terminate_lib_ikworks(ModuleInitializationLevel p_level) { extern "C" { GDExtensionBool GDE_EXPORT ikworks_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { - godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); + GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); init_obj.register_initializer(initialize_lib_ikworks); init_obj.register_terminator(terminate_lib_ikworks); diff --git a/src/godot_ik.cpp b/src/godot_ik.cpp index cf1bbda..f46a62b 100644 --- a/src/godot_ik.cpp +++ b/src/godot_ik.cpp @@ -531,7 +531,7 @@ void GodotIK::initialize_bone_lengths() { } } -void godot::GodotIK::initialize_effectors() { +void GodotIK::initialize_effectors() { // Collect all nested effectors Vector child_list = get_nested_children_dsf(this); Vector new_effectors; @@ -561,7 +561,7 @@ void godot::GodotIK::initialize_effectors() { effectors = new_effectors; } -void godot::GodotIK::set_effector_properties(GodotIKEffector *effector, GodotIK *ik_controller) { +void GodotIK::set_effector_properties(GodotIKEffector *effector, GodotIK *ik_controller) { effector->set_ik_controller(this); for (int i = 0; i < effector->get_child_count(); i++) { GodotIKConstraint *constraint = Object::cast_to(effector->get_child(i)); @@ -646,7 +646,7 @@ void GodotIK::initialize_connections(Node *root) { } } -void godot::GodotIK::update_all_transforms_from_skeleton() { +void GodotIK::update_all_transforms_from_skeleton() { Skeleton3D *skeleton = get_skeleton(); ERR_FAIL_NULL(skeleton); @@ -752,7 +752,7 @@ bool GodotIK::get_use_global_rotation_poles() const { return use_global_rotation_poles; } -TypedArray godot::GodotIK::get_effectors() { +TypedArray GodotIK::get_effectors() { TypedArray result; result.resize(effectors.size()); for (int i = 0; i < effectors.size(); i++) { @@ -761,11 +761,11 @@ TypedArray godot::GodotIK::get_effectors() { return result; } -int godot::GodotIK::get_current_iteration() { +int GodotIK::get_current_iteration() { return current_iteration; } -void godot::GodotIK::add_external_root(GodotIKRoot *p_root) { +void GodotIK::add_external_root(GodotIKRoot *p_root) { if (this->is_ancestor_of(p_root) || p_root->is_ancestor_of(this)) { print_error("GodotIK can't be ancestor of its external root or vise versa."); return; @@ -776,7 +776,7 @@ void godot::GodotIK::add_external_root(GodotIKRoot *p_root) { } } -void godot::GodotIK::remove_external_root(GodotIKRoot *p_root) { +void GodotIK::remove_external_root(GodotIKRoot *p_root) { external_roots.erase(p_root); if (!p_root) { return; diff --git a/src/godot_ik_constraint.cpp b/src/godot_ik_constraint.cpp index 81f53ab..9dd5c50 100644 --- a/src/godot_ik_constraint.cpp +++ b/src/godot_ik_constraint.cpp @@ -5,7 +5,7 @@ using namespace godot; -void godot::GodotIKConstraint::_bind_methods() { +void GodotIKConstraint::_bind_methods() { ClassDB::bind_method(D_METHOD("get_bone_name"), &GodotIKConstraint::get_bone_name); ClassDB::bind_method(D_METHOD("set_bone_name", "bone_name"), &GodotIKConstraint::set_bone_name); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "bone_name"), "set_bone_name", "get_bone_name"); @@ -36,7 +36,7 @@ PackedVector3Array GodotIKConstraint::apply(Vector3 p_pos_parent_bone, Vector3 p return result; } -void godot::GodotIKConstraint::_validate_property(PropertyInfo &p_property) const { +void GodotIKConstraint::_validate_property(PropertyInfo &p_property) const { if (p_property.name == StringName("bone_name")) { Skeleton3D *skeleton = get_skeleton(); if (skeleton) { @@ -86,15 +86,15 @@ void GodotIKConstraint::set_bone_idx(int p_bone_idx) { } } -void godot::GodotIKConstraint::set_ik_controller(GodotIK *p_ik_controller) { +void GodotIKConstraint::set_ik_controller(GodotIK *p_ik_controller) { ik_controller = p_ik_controller; } -GodotIK *godot::GodotIKConstraint::get_ik_controller() const { +GodotIK *GodotIKConstraint::get_ik_controller() const { return ik_controller; } -Skeleton3D *godot::GodotIKConstraint::get_skeleton() const { +Skeleton3D *GodotIKConstraint::get_skeleton() const { if (!ik_controller) { return nullptr; } diff --git a/src/godot_ik_effector.cpp b/src/godot_ik_effector.cpp index fedff8d..1f04283 100644 --- a/src/godot_ik_effector.cpp +++ b/src/godot_ik_effector.cpp @@ -129,15 +129,15 @@ Skeleton3D *GodotIKEffector::get_skeleton() const { return ik_controller->get_skeleton(); } -void godot::GodotIKEffector::set_active(bool p_active) { +void GodotIKEffector::set_active(bool p_active) { active = p_active; } -bool godot::GodotIKEffector::is_active() const { +bool GodotIKEffector::is_active() const { return active; } -PackedStringArray godot::GodotIKEffector::_get_configuration_warnings() const { +PackedStringArray GodotIKEffector::_get_configuration_warnings() const { PackedStringArray result; if (get_ik_controller() == nullptr) { result.push_back("Needs to be parented by a GodotIK node. Can be nested."); diff --git a/src/godot_ik_root.cpp b/src/godot_ik_root.cpp index 0463506..bc07440 100644 --- a/src/godot_ik_root.cpp +++ b/src/godot_ik_root.cpp @@ -3,7 +3,7 @@ using namespace godot; -void godot::GodotIKRoot::_notification(int p_notification) { +void GodotIKRoot::_notification(int p_notification) { if (p_notification == NOTIFICATION_READY) { if (ik_controller_path.is_empty()) { return; @@ -41,11 +41,11 @@ void GodotIKRoot::set_ik_controller(const NodePath &p_ik_controller) { ik_controller = new_ik_controller; } -NodePath godot::GodotIKRoot::get_ik_controller() const { +NodePath GodotIKRoot::get_ik_controller() const { return ik_controller_path; } -void godot::GodotIKRoot::_bind_methods() { +void GodotIKRoot::_bind_methods() { ClassDB::bind_method(D_METHOD("set_ik_controller", "ik_controller"), &GodotIKRoot::set_ik_controller); ClassDB::bind_method(D_METHOD("get_ik_controller"), &GodotIKRoot::get_ik_controller); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "ik_controller", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "GodotIK"), From 1c536417ad9ac71ea9fd31ea992f36f48dafe4e7 Mon Sep 17 00:00:00 2001 From: Alexander Montag Date: Sat, 17 May 2025 13:47:20 +0200 Subject: [PATCH 2/8] Fix warnings on ZERO basis -> quaternion conversion --- src/godot_ik.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/godot_ik.cpp b/src/godot_ik.cpp index f46a62b..b744c9e 100644 --- a/src/godot_ik.cpp +++ b/src/godot_ik.cpp @@ -110,7 +110,7 @@ void GodotIK::_process_modification() { solve_forward(); } apply_positions(); - + current_iteration = -1; float t2 = Time::get_singleton()->get_ticks_usec(); @@ -274,8 +274,15 @@ void GodotIK::apply_positions() { new_direction = gp_transform.basis.xform_inv(new_direction); additional_rotation = Quaternion(old_direction, new_direction); + Basis additional_rotation_as_basis = gp_transform.basis * additional_rotation * gp_init_transform.basis.inverse(); + // Bring the rotation back into global space. - additional_rotation = gp_transform.basis * additional_rotation * gp_init_transform.basis.inverse(); + if (additional_rotation_as_basis != Basis(Vector3(0, 0, 0), Vector3(0,0,0), Vector3(0,0,0))){ + additional_rotation = additional_rotation_as_basis; + } + else { // Hopefully addresses https://github.com/monxa/GodotIK/issues/56 + additional_rotation = Quaternion(); + } } // Update the parent's transform with the computed rotation. From 921c52304965f7de7cb73874ea81b584ebf03e71 Mon Sep 17 00:00:00 2001 From: Alexander Montag Date: Sun, 18 May 2025 08:23:37 +0200 Subject: [PATCH 3/8] Revert "Fix warnings on ZERO basis -> quaternion conversion" This reverts commit 1c536417ad9ac71ea9fd31ea992f36f48dafe4e7. --- src/godot_ik.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/godot_ik.cpp b/src/godot_ik.cpp index b744c9e..f46a62b 100644 --- a/src/godot_ik.cpp +++ b/src/godot_ik.cpp @@ -110,7 +110,7 @@ void GodotIK::_process_modification() { solve_forward(); } apply_positions(); - + current_iteration = -1; float t2 = Time::get_singleton()->get_ticks_usec(); @@ -274,15 +274,8 @@ void GodotIK::apply_positions() { new_direction = gp_transform.basis.xform_inv(new_direction); additional_rotation = Quaternion(old_direction, new_direction); - Basis additional_rotation_as_basis = gp_transform.basis * additional_rotation * gp_init_transform.basis.inverse(); - // Bring the rotation back into global space. - if (additional_rotation_as_basis != Basis(Vector3(0, 0, 0), Vector3(0,0,0), Vector3(0,0,0))){ - additional_rotation = additional_rotation_as_basis; - } - else { // Hopefully addresses https://github.com/monxa/GodotIK/issues/56 - additional_rotation = Quaternion(); - } + additional_rotation = gp_transform.basis * additional_rotation * gp_init_transform.basis.inverse(); } // Update the parent's transform with the computed rotation. From a3efe43c3d947aa854bbc22cd2c9de7a263c21c5 Mon Sep 17 00:00:00 2001 From: monxa <84077629+monxa@users.noreply.github.com> Date: Sun, 18 May 2025 08:24:56 +0200 Subject: [PATCH 4/8] Add individual influence property for `GodotIKEffector` (#55) * Implement `GodotIKEffector.influence` property, refactor some * Update documentation for `GodotIKEffector` and `GodotIK` * Add another test case to main.tscn --- doc_classes/GodotIK.xml | 2 +- doc_classes/GodotIKEffector.xml | 14 ++--- godot_project/assets/test_arm.glb | Bin 0 -> 3632 bytes godot_project/assets/test_arm.glb.import | 37 +++++++++++++ godot_project/main.tscn | 38 ++++++++----- src/godot_ik.cpp | 66 +++++++++++++++-------- src/godot_ik.h | 12 ++--- src/godot_ik_effector.cpp | 28 +++++++--- src/godot_ik_effector.h | 4 ++ 9 files changed, 143 insertions(+), 58 deletions(-) create mode 100644 godot_project/assets/test_arm.glb create mode 100644 godot_project/assets/test_arm.glb.import diff --git a/doc_classes/GodotIK.xml b/doc_classes/GodotIK.xml index 6d0d651..74ba51a 100644 --- a/doc_classes/GodotIK.xml +++ b/doc_classes/GodotIK.xml @@ -45,7 +45,7 @@ Sets the number of iterations during the IK solving process. Higher values provide more accurate results but may reduce performance, while lower values improve speed with less precision. - + Maintained primarily for backwards compatibility. When enabled, global rotation poles are used as references for rotation adjustments instead of local ones. See: [url]https://github.com/monxa/GodotIK/issues/26#issuecomment-2650330652[/url]. diff --git a/doc_classes/GodotIKEffector.xml b/doc_classes/GodotIKEffector.xml index dff5ffa..0b1032d 100644 --- a/doc_classes/GodotIKEffector.xml +++ b/doc_classes/GodotIKEffector.xml @@ -35,21 +35,17 @@ The number of bones in the IK chain influenced by this effector. + + Determines the influence of a single effector. + Determines how the end bone's transform is applied based on the [enum TransformMode]. Options include updating only the position, preserving rotation, straightening the chain, or applying the full transform. - - - - Emitted when the [member bone_idx] value changes. - - - - + - Emitted when the [member chain_length] value changes. + Emitted when a solver-relevant property changes. diff --git a/godot_project/assets/test_arm.glb b/godot_project/assets/test_arm.glb new file mode 100644 index 0000000000000000000000000000000000000000..b077ba8d88b78106854f336dab345ee6cf814fdb GIT binary patch literal 3632 zcmcImZH!!18NNbn0;Hvcuh_J*Tx_V#o!j&Mv9NQY#7c#Bfi6qvrqSu_-R;`#Omk;S zS+=Wdo2U&5X@pfvk!Tt%SS?5()Y_dp2?TB8&zeXFaLG$usj{H$6p)%^uSB<568LW!kR zD4{HkNKCXani?&+keVnfq5_F2*H%a_xi!LYrWH0?yi#rjA;K`K4eqU#OXF~Bh~h5y zP-+>m%vcqDtR);#&q|fr_&E=G%nY-X5iKRPQn9o^OvGZUsO17)aUr-O@R7!hVM%Iu zXqn(tVXNLW6-|caEo0-uSqeyphy!O1y0eh*`H&D|w9-~-N*Py-8|eYiR^v>oj1ZI% zD*=F(mQ#disWFT*fhENN5*)TbRZ~e@Facox0anx~qNH{he%KOV%KIfVHP^ygp{3BMI9K7qf;8Hoq#zwuInEng zF127N24YcqEonW+<_pyAn&(agSnQaltu>Megx!&gvT!q`X! z#rbFix`5KT)B(vSU*eQ>Kwzk5ia_`&L_{(d8zkF;b>M*#U@K_mR=F`kI?=NrGV0rb zfD+`WYp5J5-nN49v4lQy%GfUTXn-2kX1D~p0t)8K8{7cO1bv92l!)<4f3B-YNwlG? zD+wGk+snYTBY^5gn}8Y{$hd@@B38Gip?OZ=AfZrp&kR%s1cqq_F-03f{3rqgCnd^I zXx>%lTg$*9HuwCWWxb`<(MU$R0|eFq0(i`rbm>MJAep|~362#sR>(cGOhJ3l+aUfN zeFV)9Rl=P>D1uplbFcxe5+#D`K57K$q6Zx`=&IcYmS6!?54?2cNTaTFncwc6L+vD> z?eT-$m>@tlO*Da=yQYaUKto6K*OFq604aT|h5%qJ0$ZRL4DH^vZ)35b7*VJt#T?r0 zoF)+_s`*aYcBe4nwT&pWWk$I*+g7UY$kNaab+xTymAT<>tm1~mboE$ecwMQUjg{T( zhxzcU)v-$5kAWnz5kQLwNFX8}u}DWP714HXsSNvCHJdwo^Nx{`G#eaCclsmC)ot4` zJ67rg6WcM_N!J*{a&<=qqb;R~*Dr_bZXFQb$qVMK$N8f+Pt?;p)5>UlOW3P1vW0G; ze`Lgu$;3rCXMEPMefQL+sh%c=?A2@6W73{Z+#mehGd=ffTRXdce$DN1&$P20xN9(Z z`o;zMsga(dTRS;^rpRwvmh9TQGJ;!&vPbY)VFGy-@ z?@ylk`#_(wzkcT4{MA1P$rTGm+4!k_m5LE(Et6{PsJu2i|)Y=T9eJ z*v*p-H!RQJ-*KSf>}OV~Xdxf9aBonn!?qB2gzZSe#9DHFYd0~{h^MYl+er|U0CA*4^Wjnq7PnQnodk%i1 z`Of20c%M#gc)6Gqf4nj|apng%yZavvSLXj2?@Ols_QM75Avk$(!HF{rzvhdZf6p{BZGi>qor( zR}RI^6Q8=fY1SR~_m>Vmm>hrO(b-oPZ}IlGhkK957gv&vgU7vn@@`iA^jk-Z)2HU_ zPyhRo{I(T`n)Gi!@%DS}cqrL@;K|u1-}s`p-@N=6vj<;h$@pmr|AOGHw{I)HzEUN# zR}}pHzr1&8zUK0uH*fw?c8iOD-F}*Uf8$m8zJL9}+h6^3A-QMKq2jN;xx(!ao_T3! z @@ -12,6 +12,7 @@ #include #include #include +#include #include using namespace godot; @@ -103,6 +104,15 @@ void GodotIK::_process_modification() { return; } chain.effector_position = skeleton->to_local(chain.effector->get_global_position()); + + // see https://github.com/monxa/GodotIK/issues/52 + float influence = chain.effector->get_influence(); + + int bone_idx = chain.effector->get_bone_idx(); + if (chain.effector->is_active() && influence < 1. && bone_idx >= 0 && bone_idx < initial_transforms.size()) { + Vector3 global_pose_pos = initial_transforms[bone_idx].origin; + chain.effector_position = global_pose_pos.lerp(chain.effector_position, influence); + } } for (current_iteration = 0; current_iteration < iteration_count; current_iteration++) { @@ -110,7 +120,7 @@ void GodotIK::_process_modification() { solve_forward(); } apply_positions(); - + current_iteration = -1; float t2 = Time::get_singleton()->get_ticks_usec(); @@ -129,7 +139,7 @@ void GodotIK::set_position_group(int p_idx_bone_in_group, const Vector3 &p_pos_b void GodotIK::solve_backward() { // TODO: Introduce a depth-based chain iteration strategy (look into git history, work had already begun on this) for (IKChain &chain : chains) { - if (chain.bones.size() == 0) { + if (chain.bones.size() == 0 || chain.effector->get_influence() == 0.) { continue; } if (!chain.effector->is_active()) { @@ -160,7 +170,7 @@ void GodotIK::solve_backward() { void GodotIK::solve_forward() { // TODO: Introduce a depth-based chain iteration strategy (look into git history, work had already begun on this) for (IKChain &chain : chains) { - if (!chain.effector->is_active()) { + if (!chain.effector->is_active() || chain.effector->get_influence() == 0.) { continue; } int root_idx = chain.bones.size() - 1; @@ -372,6 +382,9 @@ void GodotIK::apply_constraint(const IKChain &p_chain, int p_idx_in_chain, Godot if (constraint == nullptr) { return; } + + float influence = compute_constraint_step_influence(p_chain.effector->get_influence(), iteration_count); + int idx_bone = p_chain.bones[p_idx_in_chain]; int idx_child = -1; int idx_parent = -1; @@ -397,6 +410,13 @@ void GodotIK::apply_constraint(const IKChain &p_chain, int p_idx_in_chain, Godot PackedVector3Array result = constraint->apply(pos_parent, pos_bone, pos_child, p_dir); + if (influence < 1. && result.size() == 3) { + PackedVector3Array base = { pos_parent, pos_bone, pos_child }; + for (int i = 0; i < 3; i++) { + result[i] = base[i].lerp(result[i], influence); // TODO: Suboptimal since dependent on iteration count. + } + } + if (idx_parent != -1 && pos_parent != result[0]) { set_position_group(idx_parent, result[0]); } @@ -428,9 +448,8 @@ void GodotIK::initialize_if_dirty() { bone_depths = calculate_bone_depths(skeleton); { // Indices by depth creation. Vector indices; - Vector depths = bone_depths; - indices.resize(depths.size()); - for (int i = 0; i < depths.size(); i++) { + indices.resize(bone_depths.size()); + for (int i = 0; i < bone_depths.size(); i++) { indices.write[i] = i; } @@ -445,7 +464,7 @@ void GodotIK::initialize_if_dirty() { } }; - indices.sort_custom(DepthComparator(depths)); + indices.sort_custom(DepthComparator(bone_depths)); indices_by_depth = indices; } initialize_bone_lengths(); @@ -621,21 +640,18 @@ void GodotIK::initialize_chains() { } } -void GodotIK::initialize_connections(Node *root) { - Vector child_list = get_nested_children_dsf(root); // First in, first out through iteration -> BSF - if (!root->is_connected("child_order_changed", callable_deinitialize)) { - root->connect("child_order_changed", callable_deinitialize); +void GodotIK::initialize_connections(Node *p_root) { + Vector child_list = get_nested_children_dsf(p_root); // First in, first out through iteration -> BSF + if (!p_root->is_connected("child_order_changed", callable_deinitialize)) { + p_root->connect("child_order_changed", callable_deinitialize); } for (Node *child : child_list) { if (!child->is_connected("child_order_changed", callable_deinitialize)) { child->connect("child_order_changed", callable_deinitialize); } if (child->is_class("GodotIKEffector")) { - if (!child->is_connected("bone_idx_changed", callable_deinitialize.unbind(1))) { - child->connect("bone_idx_changed", callable_deinitialize.unbind(1)); - } - if (!child->is_connected("chain_length_changed", callable_deinitialize.unbind(1))) { - child->connect("chain_length_changed", callable_deinitialize.unbind(1)); + if (!child->is_connected("ik_property_changed", callable_deinitialize.unbind(1))) { + child->connect("ik_property_changed", callable_deinitialize); } } if (child->is_class("GodotIKConstraint")) { @@ -715,6 +731,15 @@ Vector GodotIK::get_nested_children_dsf(Node *base) const { return child_list; } +// Computes the per-iteration influence step to reach a total influence after N iterations +float GodotIK::compute_constraint_step_influence(float total_influence, int iteration_count) { + if (total_influence == 0 || total_influence == 1. || iteration_count == 0){ + return total_influence; + } + + return 1.0f - powf(1.0f - total_influence, 1.0f / float(iteration_count)); +} + // For editor tooling: void GodotIK::set_effector_transforms_to_bones() { Skeleton3D *skeleton = get_skeleton(); @@ -791,11 +816,8 @@ void GodotIK::remove_external_root(GodotIKRoot *p_root) { child->disconnect("child_order_changed", callable_deinitialize); } if (child->is_class("GodotIKEffector")) { - if (child->is_connected("bone_idx_changed", callable_deinitialize.unbind(1))) { - child->disconnect("bone_idx_changed", callable_deinitialize.unbind(1)); - } - if (child->is_connected("chain_length_changed", callable_deinitialize.unbind(1))) { - child->disconnect("chain_length_changed", callable_deinitialize.unbind(1)); + if (child->is_connected("ik_property_changed", callable_deinitialize)) { + child->disconnect("ik_property_changed", callable_deinitialize); } } if (child->is_class("GodotIKConstraint")) { diff --git a/src/godot_ik.h b/src/godot_ik.h index 30834a9..50dbac8 100644 --- a/src/godot_ik.h +++ b/src/godot_ik.h @@ -5,7 +5,6 @@ #include "godot_ik_effector.h" #include "godot_ik_root.h" -#include #include #include #include @@ -53,7 +52,7 @@ class GDE_EXPORT GodotIK : public SkeletonModifier3D { bool get_use_global_rotation_poles() const; TypedArray get_effectors(); - + int get_current_iteration(); void add_external_root(GodotIKRoot *p_root); @@ -115,10 +114,9 @@ class GDE_EXPORT GodotIK : public SkeletonModifier3D { void initialize_chains(); void update_all_transforms_from_skeleton(); - void initialize_connections(Node *root); + void initialize_connections(Node *p_root); // ! if dirty -------- - void set_effector_transforms_to_bones(); // ! initialization ------------------------------ @@ -130,11 +128,13 @@ class GDE_EXPORT GodotIK : public SkeletonModifier3D { Vector calculate_bone_depths(Skeleton3D *p_skeleton); bool compare_by_depth(int p_a, int p_b, const Vector &p_depths); + Vector get_nested_children_dsf(Node *p_base) const; + float compute_constraint_step_influence(float total_influence, int iteration_count); + void set_effector_transforms_to_bones(); - Vector get_nested_children_dsf(Node *base) const; }; // ! class GodotIK } // namespace godot -#endif // ! GODOT_IK_H \ No newline at end of file +#endif // ! GODOT_IK_H diff --git a/src/godot_ik_effector.cpp b/src/godot_ik_effector.cpp index 1f04283..0af5999 100644 --- a/src/godot_ik_effector.cpp +++ b/src/godot_ik_effector.cpp @@ -2,10 +2,10 @@ #include "godot_ik.h" #include +#include #include #include #include -#include using namespace godot; @@ -36,8 +36,11 @@ void GodotIKEffector::_bind_methods() { ClassDB::bind_method(D_METHOD("is_active"), &GodotIKEffector::is_active); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "active"), "set_active", "is_active"); - ADD_SIGNAL(MethodInfo("bone_idx_changed", PropertyInfo(Variant::Type::INT, "bone_idx"))); - ADD_SIGNAL(MethodInfo("chain_length_changed", PropertyInfo(Variant::Type::INT, "chain_length"))); + ClassDB::bind_method(D_METHOD("set_influence", "influence"), &GodotIKEffector::set_influence); + ClassDB::bind_method(D_METHOD("get_influence"), &GodotIKEffector::get_influence); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "influence", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_influence", "get_influence"); + + ADD_SIGNAL(MethodInfo("ik_property_changed")); ClassDB::bind_method(D_METHOD("get_skeleton"), &GodotIKEffector::get_skeleton); @@ -90,7 +93,7 @@ void GodotIKEffector::set_bone_idx(int p_bone_idx) { } if (prev_bone_idx != bone_idx) { - emit_signal("bone_idx_changed", bone_idx); + emit_signal("ik_property_changed"); } } @@ -99,10 +102,10 @@ int GodotIKEffector::get_chain_length() const { } void GodotIKEffector::set_chain_length(int p_chain_length) { - int prev_chain_length = chain_length; + bool changed = p_chain_length != chain_length; chain_length = p_chain_length; - if (prev_chain_length != chain_length) { - emit_signal("chain_length_changed", chain_length); + if (changed) { + emit_signal("ik_property_changed"); } } @@ -137,10 +140,19 @@ bool GodotIKEffector::is_active() const { return active; } +void GodotIKEffector::set_influence(float p_influence) { + bool changed = influence != p_influence; + influence = p_influence; +} + +float godot::GodotIKEffector::get_influence() const { + return influence; +} + PackedStringArray GodotIKEffector::_get_configuration_warnings() const { PackedStringArray result; if (get_ik_controller() == nullptr) { result.push_back("Needs to be parented by a GodotIK node. Can be nested."); } return result; -} \ No newline at end of file +} diff --git a/src/godot_ik_effector.h b/src/godot_ik_effector.h index 4ed56e3..d2b1701 100644 --- a/src/godot_ik_effector.h +++ b/src/godot_ik_effector.h @@ -40,6 +40,9 @@ class GodotIKEffector : public Node3D { void set_active(bool p_active); bool is_active() const; + void set_influence(float p_influence); + float get_influence() const; + PackedStringArray _get_configuration_warnings() const override; protected: @@ -53,6 +56,7 @@ class GodotIKEffector : public Node3D { int chain_length = 2; TransformMode transform_mode = TransformMode::POSITION_ONLY; GodotIK *ik_controller = nullptr; + float influence = 1.; bool active = true; }; // ! class GodotIKEffector } //namespace godot From 0fbe39a5a30b1d6ac5c666844847499dd16f7f2f Mon Sep 17 00:00:00 2001 From: Alexander Montag Date: Sat, 31 May 2025 03:44:23 +0200 Subject: [PATCH 5/8] Update config warnings explicitly (Godot 4.5+) --- src/godot_ik.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/godot_ik.cpp b/src/godot_ik.cpp index da7a3a9..7fd67db 100644 --- a/src/godot_ik.cpp +++ b/src/godot_ik.cpp @@ -498,6 +498,7 @@ void GodotIK::initialize_if_dirty() { bone_effector_map.write[bone_idx].push_back(chain.effector); } } + update_configuration_warnings(); dirty = false; } From 3dcdf5a81dd594b0fdf81d0bf9dc0a5d469a2a19 Mon Sep 17 00:00:00 2001 From: monxa <84077629+monxa@users.noreply.github.com> Date: Sun, 1 Jun 2025 16:30:21 +0200 Subject: [PATCH 6/8] Add editor tooling to reset Effector transforms (#57) * Refactor * Add editor tooling for positioning Adds an InspectorPlugin with a Button to reset one or all effectors of a given GodotIK to the current pose. * Update documentation * Format * Improve new code slightly * Add UndoRedo and separate concerns. * Improve UX spacing, Improve include structure --- doc_classes/GodotIK.xml | 3 +- doc_classes/GodotIKEffector.xml | 6 ++ godot_project/godot_ik_with_tooling.gd | 11 --- godot_project/godot_ik_with_tooling.gd.uid | 1 - godot_project/main.tscn | 30 +++--- src/editor/godot_ik_editor_plugin.cpp | 110 +++++++++++++++++++++ src/editor/godot_ik_editor_plugin.h | 46 +++++++++ src/godot_entry.cpp | 25 +++-- src/godot_ik.cpp | 24 ++--- src/godot_ik.h | 5 +- src/godot_ik_effector.cpp | 11 +++ src/godot_ik_effector.h | 4 +- 12 files changed, 217 insertions(+), 59 deletions(-) delete mode 100644 godot_project/godot_ik_with_tooling.gd delete mode 100644 godot_project/godot_ik_with_tooling.gd.uid create mode 100644 src/editor/godot_ik_editor_plugin.cpp create mode 100644 src/editor/godot_ik_editor_plugin.h diff --git a/doc_classes/GodotIK.xml b/doc_classes/GodotIK.xml index 74ba51a..647dd11 100644 --- a/doc_classes/GodotIK.xml +++ b/doc_classes/GodotIK.xml @@ -37,7 +37,8 @@ - Synchronizes the transforms of all recognized effector nodes with those of their corresponding bones. This is particularly useful for editor tooling. + Transforms all linked [GodotIKEffector]s to be at the current [Skeleton3D]s pose. Useful for editor tooling - to reset the IK state. + See also [method GodotIKEffector.set_transform_to_bone]. diff --git a/doc_classes/GodotIKEffector.xml b/doc_classes/GodotIKEffector.xml index 0b1032d..a0f6ad2 100644 --- a/doc_classes/GodotIKEffector.xml +++ b/doc_classes/GodotIKEffector.xml @@ -21,6 +21,12 @@ Returns the Skeleton3D instance from the associated IK controller, or null if no IK controller is set. + + + + Sets the [transform] to the [b]current pose[/b] of the bone in the skeleton. Transform is in [Skeleton3D] local space. + + diff --git a/godot_project/godot_ik_with_tooling.gd b/godot_project/godot_ik_with_tooling.gd deleted file mode 100644 index 295257f..0000000 --- a/godot_project/godot_ik_with_tooling.gd +++ /dev/null @@ -1,11 +0,0 @@ -@tool -class_name GodotIKWithTooling extends GodotIK - -## This only works with godot 4.4+ WE NEED SOME EditorPlugin to provide tooling, please <3 -#@export_tool_button("Reset all effectors") var t = func(): - #var skeleton = get_skeleton() - #skeleton.reset_bone_poses() -# - #for effector : GodotIKEffector in get_children(): - #var trans = skeleton.global_transform * skeleton.get_bone_global_pose(effector.bone_idx) - #effector.global_transform = trans diff --git a/godot_project/godot_ik_with_tooling.gd.uid b/godot_project/godot_ik_with_tooling.gd.uid deleted file mode 100644 index f49475d..0000000 --- a/godot_project/godot_ik_with_tooling.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://butv1kt1g08vw diff --git a/godot_project/main.tscn b/godot_project/main.tscn index fab04cb..813d2ba 100644 --- a/godot_project/main.tscn +++ b/godot_project/main.tscn @@ -1,7 +1,6 @@ -[gd_scene load_steps=21 format=4 uid="uid://c8qi1bwxkr7hd"] +[gd_scene load_steps=20 format=4 uid="uid://c8qi1bwxkr7hd"] [ext_resource type="Script" uid="uid://cxopkguwpcegj" path="res://skeleton_3d.gd" id="2_0xm2m"] -[ext_resource type="Script" uid="uid://butv1kt1g08vw" path="res://godot_ik_with_tooling.gd" id="2_1bvp3"] [ext_resource type="Script" uid="uid://df7j0ab7lmhoy" path="res://addons/libik/script/pole_bone_constraint.gd" id="2_h2yge"] [ext_resource type="PackedScene" uid="uid://s820a7rmfvss" path="res://assets/test_arm.glb" id="4_1bvp3"] @@ -372,13 +371,12 @@ skin = SubResource("Skin_m4evq") [node name="GodotIK" type="GodotIK" parent="SkeletonWolf"] iteration_count = 1 -script = ExtResource("2_1bvp3") -metadata/_custom_type_script = "uid://butv1kt1g08vw" [node name="EffectorFL" type="GodotIKEffector" parent="SkeletonWolf/GodotIK"] +bone_name = "ffoot.L" bone_idx = 3 chain_length = 3 -transform = Transform3D(0.996464, 0.0839962, 0.0021044, 0.0755279, -0.88447, -0.460444, -0.0368144, 0.458974, -0.887687, -0.818078, 0.095834, -5.88714) +transform = Transform3D(0.996464, 0.0839959, 0.00210406, 0.0755277, -0.88447, -0.460444, -0.0368144, 0.458975, -0.887686, -0.59416, 0.228332, -6.31295) top_level = true [node name="PoleBoneConstraint" type="GodotIKConstraint" parent="SkeletonWolf/GodotIK/EffectorFL"] @@ -389,9 +387,10 @@ forward = false metadata/_custom_type_script = "uid://df7j0ab7lmhoy" [node name="EffectorFR" type="GodotIKEffector" parent="SkeletonWolf/GodotIK"] +bone_name = "ffoot.R" bone_idx = 8 chain_length = 3 -transform = Transform3D(0.996464, -0.0839962, -0.0021044, -0.0755279, -0.88447, -0.460444, 0.0368144, 0.458974, -0.887687, -0.17745, 0.228332, 0.335999) +transform = Transform3D(0.996464, -0.0839959, -0.00210406, -0.0755277, -0.88447, -0.460444, 0.0368144, 0.458975, -0.887686, -0.17745, 0.228332, 0.336) [node name="PoleBoneConstraint" type="GodotIKConstraint" parent="SkeletonWolf/GodotIK/EffectorFR"] bone_idx = 7 @@ -402,12 +401,14 @@ forward = false metadata/_custom_type_script = "uid://df7j0ab7lmhoy" [node name="EffectorRL" type="GodotIKEffector" parent="SkeletonWolf/GodotIK"] +bone_name = "rfoot.L" bone_idx = 10 -transform = Transform3D(0.980905, -0.009508, 0.194254, -0.0203943, -0.998326, 0.0541186, 0.193414, -0.0570473, -0.979456, 0.191384, 0.268114, -0.742832) +transform = Transform3D(0.980905, -0.00950776, 0.194255, -0.0203941, -0.998326, 0.0541199, 0.193414, -0.0570489, -0.979455, 0.191384, 0.268114, -0.742832) [node name="EffectorRR" type="GodotIKEffector" parent="SkeletonWolf/GodotIK"] +bone_name = "rfoot.R" bone_idx = 12 -transform = Transform3D(0.980905, 0.009508, -0.194254, 0.0203943, -0.998326, 0.0541186, -0.193414, -0.0570473, -0.979456, -0.191384, 0.268114, -0.742832) +transform = Transform3D(0.980905, 0.00950776, -0.194255, 0.0203941, -0.998326, 0.0541199, -0.193414, -0.0570489, -0.979455, -0.191384, 0.268114, -0.742832) [node name="SkeletonSimplest" type="Skeleton3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.398928, 0, -6.55336) @@ -446,10 +447,9 @@ mesh = SubResource("ArrayMesh_xwntw") skin = SubResource("Skin_8nptl") [node name="GodotIK" type="GodotIK" parent="SkeletonSimplest"] -script = ExtResource("2_1bvp3") -metadata/_custom_type_script = "uid://butv1kt1g08vw" [node name="GodotIKEffector" type="GodotIKEffector" parent="SkeletonSimplest/GodotIK"] +bone_name = "Bone.003" bone_idx = 3 chain_length = 4 transform = Transform3D(1, 0, 0, 0, 3.3799e-08, 1, 0, -1, 3.3799e-08, 0, 3.07187e-08, -0.982377) @@ -565,17 +565,16 @@ skin = SubResource("Skin_3juqw") surface_material_override/0 = SubResource("StandardMaterial3D_kcxum") [node name="GodotIK" type="GodotIK" parent="SimpleArmRig"] -script = ExtResource("2_1bvp3") -metadata/_custom_type_script = "uid://butv1kt1g08vw" [node name="GodotIKEffectorL" type="GodotIKEffector" parent="SimpleArmRig/GodotIK"] +bone_name = "Fingers.L" bone_idx = 5 chain_length = 6 transform_mode = 1 -transform = Transform3D(0.0446341, -0.982379, -0.181492, 0.998994, 0.0446905, 0.00378067, 0.00439691, -0.181478, 0.983384, -0.8571, -0.00602645, 0.00448275) +transform = Transform3D(0.0446345, -0.982379, -0.181493, 0.998994, 0.0446909, 0.00378087, 0.00439685, -0.18148, 0.983385, -0.8571, -0.00602621, 0.00448179) [node name="BoneAttachment3D" type="BoneAttachment3D" parent="SimpleArmRig/GodotIK/GodotIKEffectorL"] -transform = Transform3D(0.954568, 0.282226, 0.095643, -0.296989, 0.927323, 0.227747, -0.0244159, -0.245806, 0.969013, 0, 0, 0) +transform = Transform3D(0.954569, 0.282225, 0.0956423, -0.296988, 0.927324, 0.227747, -0.0244156, -0.245805, 0.969011, -5.96046e-08, 0, 0) bone_name = "Fingers.L" bone_idx = 5 use_external_skeleton = true @@ -602,10 +601,11 @@ pole_direction = Vector3(-1, -1, 1) metadata/_custom_type_script = "uid://df7j0ab7lmhoy" [node name="GodotIKEffectorR" type="GodotIKEffector" parent="SimpleArmRig/GodotIK"] +bone_name = "Fingers.R" bone_idx = 11 chain_length = 6 transform_mode = 2 -transform = Transform3D(0.0446341, 0.98238, 0.181492, -0.998994, 0.0446904, 0.00378074, -0.00439683, -0.181478, 0.983384, 0.857101, -0.00602651, 0.00448275) +transform = Transform3D(0.0446337, 0.982379, 0.181492, -0.998994, 0.0446901, 0.00378082, -0.00439668, -0.181478, 0.983385, 0.8571, -0.00602686, 0.00448322) [node name="test_arm" parent="." instance=ExtResource("4_1bvp3")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.37366, 0, -3.05204) diff --git a/src/editor/godot_ik_editor_plugin.cpp b/src/editor/godot_ik_editor_plugin.cpp new file mode 100644 index 0000000..4bd6da3 --- /dev/null +++ b/src/editor/godot_ik_editor_plugin.cpp @@ -0,0 +1,110 @@ +#include "godot_ik_editor_plugin.h" +#include "godot_ik.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace godot; + +GodotIKEditorPlugin::GodotIKEditorPlugin() { + inspector_plugin = Ref(memnew(GodotIKEditorInspectorPlugin)); + add_inspector_plugin(inspector_plugin); + + inspector_plugin->connect("request_reset_effector", callable_mp(this, &GodotIKEditorPlugin::reset_single_effector)); + inspector_plugin->connect("request_reset_all_effectors", callable_mp(this, &GodotIKEditorPlugin::reset_all_effectors)); +} + +void GodotIKEditorPlugin::_bind_methods() { +} + +void GodotIKEditorPlugin::reset_single_effector(Object *p_effector) { + GodotIKEffector *effector = Object::cast_to(p_effector); + ERR_FAIL_NULL_MSG(effector, "[GodotIK] reset_single_effector: Effector is null or of the wrong type."); + + get_undo_redo()->create_action("Reset Single Effector Transform"); + get_undo_redo()->add_do_method(p_effector, "set_transform_to_bone"); + get_undo_redo()->add_undo_property(p_effector, "transform", effector->get_transform()); + get_undo_redo()->commit_action(); +} + +void GodotIKEditorPlugin::reset_all_effectors(Object *p_ik_controller) { + GodotIK *ik_controller = Object::cast_to(p_ik_controller); + ERR_FAIL_NULL_MSG(ik_controller, "[GodotIK] reset_all_effectors: IK controller is null or of the wrong type."); + + get_undo_redo()->create_action("Reset All GodotIK Effector Transforms"); + get_undo_redo()->add_do_method(ik_controller, "set_effector_transforms_to_bones"); + TypedArray all_effectors = ik_controller->get_effectors(); + for (int i = 0; i < all_effectors.size(); i++) { + GodotIKEffector *effector = cast_to(all_effectors[i]); + get_undo_redo()->add_undo_property(effector, "transform", effector->get_transform()); + } + get_undo_redo()->commit_action(); +} + +// -------------------------------------------------- + +GodotIKEditorInspectorPlugin::GodotIKEditorInspectorPlugin() { +} + +void GodotIKEditorInspectorPlugin::_bind_methods() { + ADD_SIGNAL(MethodInfo("request_reset_effector", PropertyInfo(Variant::OBJECT, "effector"))); + ADD_SIGNAL(MethodInfo("request_reset_all_effectors", PropertyInfo(Variant::OBJECT, "ik_controller"))); +} + +bool GodotIKEditorInspectorPlugin::_can_handle(Object *p_object) const { + const StringName obj_name = p_object->get_class(); + if (obj_name == GodotIK::get_class_static()) { + return true; + } + if (obj_name == GodotIKEffector::get_class_static()) { + return true; + } + return false; +} + +void GodotIKEditorInspectorPlugin::_parse_end(Object *p_object) { + const StringName obj_name = p_object->get_class(); + + const float scale = EditorInterface::get_singleton()->get_editor_scale(); + const float margin = EditorInterface::get_singleton()->get_editor_settings()->get_setting("interface/theme/base_margin"); + const int margin_scaled = Math::round(margin * scale); + + MarginContainer *container = memnew(MarginContainer); + container->add_theme_constant_override("margin_top", 6 * EditorInterface::get_singleton()->get_editor_scale()); + container->add_theme_constant_override("margin_bottom", 6 * EditorInterface::get_singleton()->get_editor_scale()); + container->add_theme_constant_override("margin_left", 4 * EditorInterface::get_singleton()->get_editor_scale()); + container->add_theme_constant_override("margin_right", 4 * EditorInterface::get_singleton()->get_editor_scale()); + + Button *reset_button = memnew(Button); + reset_button->set_h_size_flags(Control::SizeFlags::SIZE_EXPAND_FILL); + + if (obj_name == GodotIK::get_class_static()) { + reset_button->set_text("Reset Effectors"); + reset_button->set_tooltip_text("Resets all Effectors to the current [Skeleton3D] pose transform before IK is applied."); + + Callable c_emit_signal = Callable(this, "emit_signal").bind("request_reset_all_effectors", p_object); + reset_button->connect("pressed", c_emit_signal); + } + if (obj_name == GodotIKEffector::get_class_static()) { + reset_button->set_text("Reset Effector"); + reset_button->set_tooltip_text("Resets current IKEffector to the current [Skeleton3D] pose transform before IK is applied."); + + Callable c_emit_signal = Callable(this, "emit_signal").bind("request_reset_effector", p_object); + reset_button->connect("pressed", c_emit_signal); + } + + container->add_child(reset_button); + add_custom_control(container); +} diff --git a/src/editor/godot_ik_editor_plugin.h b/src/editor/godot_ik_editor_plugin.h new file mode 100644 index 0000000..ddcc223 --- /dev/null +++ b/src/editor/godot_ik_editor_plugin.h @@ -0,0 +1,46 @@ +#ifndef GODOT_IK_EDITOR_PLUGIN_H +#define GODOT_IK_EDITOR_PLUGIN_H +#include +#include +#include + +namespace godot { + +class GodotIKEditorInspectorPlugin; + +// --------------------------------------------------------------- + +class GodotIKEditorPlugin : public EditorPlugin { + GDCLASS(GodotIKEditorPlugin, EditorPlugin) +public: + GodotIKEditorPlugin(); + +protected: + static void _bind_methods(); + +private: + void reset_single_effector(Object *p_effector); + void reset_all_effectors(Object *p_ik_controller); + +private: + Ref inspector_plugin; +}; // ! GodotIKEditorPlugin + +// ------------------------------------------------------------------ + +class GodotIKEditorInspectorPlugin : public EditorInspectorPlugin { + GDCLASS(GodotIKEditorInspectorPlugin, EditorInspectorPlugin) +public: + GodotIKEditorInspectorPlugin(); + bool _can_handle(Object *p_object) const override; + void _parse_end(Object *p_object) override; + +protected: + static void _bind_methods(); + +}; //! GodotIKEditorInspectorPlugin + +// --------------------------------------------------------------- + +} //namespace godot +#endif // ! GODOT_IK_EDITOR_PLUGIN diff --git a/src/godot_entry.cpp b/src/godot_entry.cpp index abd403f..dd0dbc5 100644 --- a/src/godot_entry.cpp +++ b/src/godot_entry.cpp @@ -1,3 +1,5 @@ +#include "editor/godot_ik_editor_plugin.h" +#include "godot_cpp/classes/editor_plugin_registration.hpp" #include "godot_ik.h" #include "godot_ik_constraint.h" #include "godot_ik_effector.h" @@ -10,18 +12,23 @@ using namespace godot; void initialize_lib_ikworks(ModuleInitializationLevel p_level) { - if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { - return; + if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { + GDREGISTER_CLASS(GodotIK); + GDREGISTER_CLASS(GodotIKEffector); + GDREGISTER_CLASS(GodotIKConstraint); + GDREGISTER_CLASS(GodotIKRoot); + } + + if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { + GDREGISTER_INTERNAL_CLASS(GodotIKEditorPlugin); + GDREGISTER_INTERNAL_CLASS(GodotIKEditorInspectorPlugin); + EditorPlugins::add_by_type(); } - ClassDB::register_class(); - ClassDB::register_class(); - ClassDB::register_class(); - ClassDB::register_class(); } void terminate_lib_ikworks(ModuleInitializationLevel p_level) { - if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { - return; + if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { + EditorPlugins::remove_by_type(); } } @@ -35,4 +42,4 @@ GDExtensionBool GDE_EXPORT ikworks_init(GDExtensionInterfaceGetProcAddress p_get return init_obj.init(); } -} \ No newline at end of file +} diff --git a/src/godot_ik.cpp b/src/godot_ik.cpp index 7fd67db..64739ba 100644 --- a/src/godot_ik.cpp +++ b/src/godot_ik.cpp @@ -71,7 +71,7 @@ void GodotIK::_notification(int p_notification) { PackedStringArray GodotIK::_get_configuration_warnings() const { PackedStringArray result; - if (chains.size() == 0 && is_active()) { + if (effectors.size() == 0) { result.push_back("At least one child effector is required."); } return result; @@ -498,7 +498,6 @@ void GodotIK::initialize_if_dirty() { bone_effector_map.write[bone_idx].push_back(chain.effector); } } - update_configuration_warnings(); dirty = false; } @@ -589,6 +588,7 @@ void GodotIK::set_effector_properties(GodotIKEffector *effector, GodotIK *ik_con constraint->set_ik_controller(this); } } + update_configuration_warnings(); } void GodotIK::initialize_chains() { @@ -636,7 +636,6 @@ void GodotIK::initialize_chains() { new_chain.constraints.write[placement_in_chain] = constraint; } } - effector->has_one_pole = effector_pole_count == 1; chains.push_back(new_chain); } } @@ -734,25 +733,18 @@ Vector GodotIK::get_nested_children_dsf(Node *base) const { // Computes the per-iteration influence step to reach a total influence after N iterations float GodotIK::compute_constraint_step_influence(float total_influence, int iteration_count) { - if (total_influence == 0 || total_influence == 1. || iteration_count == 0){ - return total_influence; - } + if (total_influence == 0 || total_influence == 1. || iteration_count == 0) { + return total_influence; + } return 1.0f - powf(1.0f - total_influence, 1.0f / float(iteration_count)); } // For editor tooling: void GodotIK::set_effector_transforms_to_bones() { - Skeleton3D *skeleton = get_skeleton(); - if (!skeleton) - return; - initialize_if_dirty(); - for (auto chain : chains) { - if (chain.bones.size() == 0) - continue; - Vector3 bone_position = positions[chain.bones[0]]; - chain.effector->set_global_transform(skeleton->get_global_transform() * initial_transforms[chain.bones[0]]); - } + for (GodotIKEffector * effector : effectors){ + effector->set_transform_to_bone(); + } } // !Helpers diff --git a/src/godot_ik.h b/src/godot_ik.h index 50dbac8..aa5b21f 100644 --- a/src/godot_ik.h +++ b/src/godot_ik.h @@ -58,6 +58,7 @@ class GDE_EXPORT GodotIK : public SkeletonModifier3D { void add_external_root(GodotIKRoot *p_root); void remove_external_root(GodotIKRoot *p_root); + void set_effector_transforms_to_bones(); protected: static void _bind_methods(); @@ -117,7 +118,6 @@ class GDE_EXPORT GodotIK : public SkeletonModifier3D { void initialize_connections(Node *p_root); // ! if dirty -------- - // ! initialization ------------------------------ // helpers ------ @@ -130,9 +130,6 @@ class GDE_EXPORT GodotIK : public SkeletonModifier3D { bool compare_by_depth(int p_a, int p_b, const Vector &p_depths); Vector get_nested_children_dsf(Node *p_base) const; float compute_constraint_step_influence(float total_influence, int iteration_count); - void set_effector_transforms_to_bones(); - - }; // ! class GodotIK } // namespace godot diff --git a/src/godot_ik_effector.cpp b/src/godot_ik_effector.cpp index 0af5999..b4ad657 100644 --- a/src/godot_ik_effector.cpp +++ b/src/godot_ik_effector.cpp @@ -1,4 +1,5 @@ #include "godot_ik_effector.h" +#include "godot_cpp/core/error_macros.hpp" #include "godot_ik.h" #include @@ -44,6 +45,8 @@ void GodotIKEffector::_bind_methods() { ClassDB::bind_method(D_METHOD("get_skeleton"), &GodotIKEffector::get_skeleton); + ClassDB::bind_method(D_METHOD("set_transform_to_bone"), &GodotIKEffector::set_transform_to_bone); + ClassDB::bind_method(D_METHOD("get_ik_controller"), &GodotIKEffector::get_ik_controller); } @@ -132,6 +135,14 @@ Skeleton3D *GodotIKEffector::get_skeleton() const { return ik_controller->get_skeleton(); } +void GodotIKEffector::set_transform_to_bone() { + Skeleton3D *skeleton = get_skeleton(); + ERR_FAIL_NULL_MSG(skeleton, "[GodotIK] set_transform_to_pose failed - Skeleton could not be retrieved."); + + ERR_FAIL_INDEX_MSG(bone_idx, skeleton->get_bone_count(), "[GodotIK] set_transform_to_pose failed - Effectors bone_idx not in skeletons bones."); + set_global_transform(skeleton->get_global_transform() * skeleton->get_bone_global_pose(bone_idx)); +} + void GodotIKEffector::set_active(bool p_active) { active = p_active; } diff --git a/src/godot_ik_effector.h b/src/godot_ik_effector.h index d2b1701..11ab75f 100644 --- a/src/godot_ik_effector.h +++ b/src/godot_ik_effector.h @@ -35,7 +35,7 @@ class GodotIKEffector : public Node3D { Skeleton3D *get_skeleton() const; - bool has_one_pole = false; + void set_transform_to_bone(); void set_active(bool p_active); bool is_active() const; @@ -63,4 +63,4 @@ class GodotIKEffector : public Node3D { VARIANT_ENUM_CAST(GodotIKEffector::TransformMode); -#endif // ! GODOT_IK_EFFECTOR_H \ No newline at end of file +#endif // ! GODOT_IK_EFFECTOR_H From 1198e519dd3d0cd0160d864c3ad74dcd50c1f335 Mon Sep 17 00:00:00 2001 From: monxa <84077629+monxa@users.noreply.github.com> Date: Fri, 6 Jun 2025 22:52:24 +0200 Subject: [PATCH 7/8] Process bones in chains first (#58) --- src/godot_ik.cpp | 41 ++++++++++++++++++++++++++--------------- src/godot_ik.h | 1 - 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/godot_ik.cpp b/src/godot_ik.cpp index 64739ba..613057b 100644 --- a/src/godot_ik.cpp +++ b/src/godot_ik.cpp @@ -1,7 +1,7 @@ #include "godot_ik.h" -#include "godot_cpp/core/error_macros.hpp" #include "godot_ik_effector.h" +#include #include #include #include @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -216,8 +217,9 @@ void GodotIK::apply_positions() { parent_idx = (parent_idx < 0) ? identity_idx : parent_idx; // If neither this bone nor its parent needs processing, skip it. - if (!needs_processing[bone_idx] && !needs_processing[parent_idx]) + if (!needs_processing[bone_idx] && !needs_processing[parent_idx]) { continue; + } // Only apply rotation correction once to each parent_idx. All children will share the same position. // We greedily process the first child->parent relation only. @@ -232,8 +234,9 @@ void GodotIK::apply_positions() { int grandparent_idx = identity_idx; if (parent_idx != identity_idx) { grandparent_idx = skeleton->get_bone_parent(parent_idx); - if (grandparent_idx < 0) + if (grandparent_idx < 0) { grandparent_idx = identity_idx; + } } // Cache the necessary transforms. @@ -445,32 +448,40 @@ void GodotIK::initialize_if_dirty() { } initialize_groups(); - bone_depths = calculate_bone_depths(skeleton); + + initialize_bone_lengths(); + initialize_effectors(); + initialize_chains(); { // Indices by depth creation. + Vector bone_depths = calculate_bone_depths(skeleton); Vector indices; indices.resize(bone_depths.size()); for (int i = 0; i < bone_depths.size(); i++) { indices.write[i] = i; } + HashSet collected_chain_indices; + for (const IKChain &chain : chains) { + for (int idx : chain.bones) { + collected_chain_indices.insert(idx); + } + } struct DepthComparator { const Vector &depths; - DepthComparator(const Vector &p_depths) : - depths(p_depths) { + const HashSet &in_chain; + DepthComparator(const Vector &p_depths, const HashSet &p_in_chain) : + depths(p_depths), in_chain(p_in_chain) { } + // Respect depth AND make bones in chains shallower bool operator()(int a, int b) const { - return depths[a] < depths[b]; + return depths[a] < depths[b] || (depths[a] == depths[b] && in_chain.has(a) && !in_chain.has(b)); } }; - indices.sort_custom(DepthComparator(bone_depths)); + indices.sort_custom(DepthComparator(bone_depths, collected_chain_indices)); indices_by_depth = indices; } - initialize_bone_lengths(); - - initialize_effectors(); - initialize_chains(); // + 1 for identity_idx needs_processing.resize(skeleton->get_bone_count() + 1); needs_processing.fill(false); @@ -742,9 +753,9 @@ float GodotIK::compute_constraint_step_influence(float total_influence, int iter // For editor tooling: void GodotIK::set_effector_transforms_to_bones() { - for (GodotIKEffector * effector : effectors){ - effector->set_transform_to_bone(); - } + for (GodotIKEffector *effector : effectors) { + effector->set_transform_to_bone(); + } } // !Helpers diff --git a/src/godot_ik.h b/src/godot_ik.h index aa5b21f..cbe3b44 100644 --- a/src/godot_ik.h +++ b/src/godot_ik.h @@ -71,7 +71,6 @@ class GDE_EXPORT GodotIK : public SkeletonModifier3D { // Bone length relative to parent. Root has no bone length. Vector bone_lengths; HashMap> grouped_by_position; - Vector bone_depths; Vector indices_by_depth; Vector needs_processing; From 061254e8755878460dbc4b7c67e180477c7b530a Mon Sep 17 00:00:00 2001 From: monxa <84077629+monxa@users.noreply.github.com> Date: Fri, 6 Jun 2025 23:08:54 +0200 Subject: [PATCH 8/8] Propagate Positions Across Chains & Resolve Inter-Chain Dependencies (#60) * Implement inter-chain position propergation * Refactor * Use full transforms for propergate_positions_from_chain_ancestors * Address self-review, improve ERRors * Run clang-format * Add clarification comment in GodotIK::propagate_positions_from_chain_ancestors * Finish addressing self-review --- src/godot_ik.cpp | 161 ++++++++++++++++++++++++++++++++++++++--------- src/godot_ik.h | 4 +- 2 files changed, 136 insertions(+), 29 deletions(-) diff --git a/src/godot_ik.cpp b/src/godot_ik.cpp index 613057b..ebe0c91 100644 --- a/src/godot_ik.cpp +++ b/src/godot_ik.cpp @@ -1,11 +1,14 @@ #include "godot_ik.h" +#include "godot_cpp/variant/quaternion.hpp" +#include "godot_cpp/variant/string.hpp" +#include "godot_cpp/variant/transform2d.hpp" #include "godot_ik_effector.h" -#include #include #include #include #include +#include #include #include #include @@ -117,6 +120,7 @@ void GodotIK::_process_modification() { } for (current_iteration = 0; current_iteration < iteration_count; current_iteration++) { + propagate_positions_from_chain_ancestors(); solve_backward(); solve_forward(); } @@ -129,17 +133,71 @@ void GodotIK::_process_modification() { } void GodotIK::set_position_group(int p_idx_bone_in_group, const Vector3 &p_pos_bone) { - ERR_FAIL_COND_MSG(!grouped_by_position.has(p_idx_bone_in_group), "grouped_by_position should have every index."); + ERR_FAIL_COND_MSG(!grouped_by_position.has(p_idx_bone_in_group), "[GodotIK] grouped_by_position should have every index."); Vector group = grouped_by_position.get(p_idx_bone_in_group); - ERR_FAIL_COND_MSG(group.size() == 0, "grouped_by_position should be reflexive."); + ERR_FAIL_COND_MSG(group.size() == 0, "[GodotIK] grouped_by_position should be reflexive."); for (int i = 0; i < group.size(); i++) { positions.write[group[i]] = p_pos_bone; } } +// Apply ancestor's transform offset to all intermediate bones (from root up to—but not including—ancestor) +void GodotIK::propagate_positions_from_chain_ancestors() { + Skeleton3D *skeleton = get_skeleton(); + if (!skeleton) { + return; + } + for (const IKChain &chain : chains) { + if (chain.closest_parent_in_chain == -1) { + continue; + } + + ERR_FAIL_INDEX(0, chain.bones.size()); + int root_idx = chain.bones[chain.bones.size() - 1]; + int ancestor_idx = root_idx; // include root_idx in updates + + Vector list_to_closest_parent; // TODO: Cache root to ancestor list for performance + while (ancestor_idx != chain.closest_parent_in_chain) { + list_to_closest_parent.push_back(ancestor_idx); + ancestor_idx = skeleton->get_bone_parent(ancestor_idx); + ERR_FAIL_COND_MSG(ancestor_idx == -1, "[GodotIK] Invalid hierarchy: closest_parent_in_chain not actually reachable from root_idx."); + } + + ERR_FAIL_INDEX_MSG(0, list_to_closest_parent.size(), String("[GodotIK] Panic: Dependency propagation from invalid state. Please open an issue!") + (list_to_closest_parent.size())); + + int idx_ancestor_bone = chain.closest_parent_in_chain; + int child_idx_of_ancestor_bone = chain.pivot_child_in_ancestor; + + Transform3D rest_ancestor = initial_transforms[chain.closest_parent_in_chain]; + Transform3D rest_ancestor_child = initial_transforms[child_idx_of_ancestor_bone]; + + Vector3 rest_dir = rest_ancestor.origin.direction_to(rest_ancestor_child.origin); + Vector3 cur_dir = positions[idx_ancestor_bone].direction_to(positions[child_idx_of_ancestor_bone]); + + ERR_FAIL_COND_MSG(rest_dir.length() == 0 || cur_dir.length() == 0, "[GodotIK] Can't propergate transforms with zero length base bone. Please open an issue."); + + // This simulates the actual end transform and propagates it through the chain. + // Same as in final apply_positions post-pass. Note: Might not work if deprecated use_global_poles = true. + Quaternion adjustment = Quaternion(rest_dir, cur_dir); + Basis new_rotation = adjustment * rest_ancestor.basis; + Transform3D new_transform = Transform3D(new_rotation, positions[idx_ancestor_bone]); + Transform3D delta_transform = new_transform * rest_ancestor.inverse(); + + Vector list_from_ancestor = list_to_closest_parent; + list_from_ancestor.reverse(); + for (int index : list_to_closest_parent) { + Transform3D rest_transform = initial_transforms[index]; + Transform3D adjusted_transform = delta_transform * rest_transform; + delta_transform = adjusted_transform * rest_transform.inverse(); + positions.write[index] = (adjusted_transform).origin; + } + } +} + void GodotIK::solve_backward() { - // TODO: Introduce a depth-based chain iteration strategy (look into git history, work had already begun on this) - for (IKChain &chain : chains) { + // Chains are sorted by shallowest bone; iterate in reverse for backward pass + for (int idx_chain = chains.size() - 1; idx_chain >= 0; idx_chain--) { + IKChain &chain = chains.write[idx_chain]; if (chain.bones.size() == 0 || chain.effector->get_influence() == 0.) { continue; } @@ -169,8 +227,9 @@ void GodotIK::solve_backward() { } void GodotIK::solve_forward() { - // TODO: Introduce a depth-based chain iteration strategy (look into git history, work had already begun on this) - for (IKChain &chain : chains) { + // Forward pass uses sorted order (shallowest chains first) + for (int idx_chain = 0; idx_chain < chains.size(); idx_chain++) { + IKChain &chain = chains.write[idx_chain]; if (!chain.effector->is_active() || chain.effector->get_influence() == 0.) { continue; } @@ -452,36 +511,88 @@ void GodotIK::initialize_if_dirty() { initialize_bone_lengths(); initialize_effectors(); initialize_chains(); + { // Indices by depth creation. + HashSet in_chain; + for (const IKChain &chain : chains) { + for (int idx : chain.bones) { + in_chain.insert(idx); + } + } Vector bone_depths = calculate_bone_depths(skeleton); Vector indices; indices.resize(bone_depths.size()); for (int i = 0; i < bone_depths.size(); i++) { indices.write[i] = i; } - HashSet collected_chain_indices; - for (const IKChain &chain : chains) { - for (int idx : chain.bones) { - collected_chain_indices.insert(idx); - } - } struct DepthComparator { - const Vector &depths; - const HashSet &in_chain; + const Vector &comp_depths; + const HashSet &comp_in_chain; DepthComparator(const Vector &p_depths, const HashSet &p_in_chain) : - depths(p_depths), in_chain(p_in_chain) { + comp_depths(p_depths), comp_in_chain(p_in_chain) { } - // Respect depth AND make bones in chains shallower + // Respect depth and make bones that are in chains shallower. Shallower -> Processed first. bool operator()(int a, int b) const { - return depths[a] < depths[b] || (depths[a] == depths[b] && in_chain.has(a) && !in_chain.has(b)); + return comp_depths[a] < comp_depths[b] || (comp_depths[a] == comp_depths[b] && comp_in_chain.has(a) && !comp_in_chain.has(b)); } }; - indices.sort_custom(DepthComparator(bone_depths, collected_chain_indices)); + indices.sort_custom(DepthComparator(bone_depths, in_chain)); indices_by_depth = indices; } + + { // Sort chains by depth of shallowest bone. + struct DepthComparator { + bool operator()(const IKChain &a, const IKChain &b) const { + if (a.bones.size() == 0) { + return true; + } + if (b.bones.size() == 0) { + return false; + } + return a.bones[a.bones.size() - 1] < b.bones[b.bones.size() - 1]; + }; + }; + chains.sort_custom(DepthComparator()); + } + + // Recalculate a mapping on the sorted chains + HashMap> bone_to_chain_map; // bone_idx -> (chain_index, index_in_chain) + for (int i = 0; i < chains.size(); i++) { + bone_to_chain_map.reserve(skeleton->get_bone_count()); + for (int idx_chain = 0; idx_chain < chains.size(); idx_chain++) { + const IKChain &chain = chains[idx_chain]; + for (int idx_in_chain = 0; idx_in_chain < chain.bones.size(); ++idx_in_chain) { + int bone_idx = chain.bones[idx_in_chain]; + if (bone_to_chain_map.has(bone_idx)) { + continue; + } + bone_to_chain_map.insert(bone_idx, Pair(idx_chain, idx_in_chain)); + } + } + } + + // assign closest_parent in chain + for (IKChain &chain : chains) { + if (chain.bones.size() == 0) { + continue; + } + int root_idx = chain.bones[chain.bones.size() - 1]; + int ancestor_idx = skeleton->get_bone_parent(root_idx); + while (ancestor_idx != -1 && !bone_to_chain_map.has(ancestor_idx)) { + ancestor_idx = skeleton->get_bone_parent(ancestor_idx); + } + chain.closest_parent_in_chain = ancestor_idx; + if (ancestor_idx != -1) { + Pair placemnt_pair = bone_to_chain_map.get(ancestor_idx); + ERR_FAIL_COND_MSG(placemnt_pair.second <= 0, "[GodotIK] Effectors inbetween chains not supported. Please open an issue."); + chain.pivot_child_in_ancestor = chains[placemnt_pair.first].bones[placemnt_pair.second - 1]; + } + } + + // fill needs_processing // + 1 for identity_idx needs_processing.resize(skeleton->get_bone_count() + 1); needs_processing.fill(false); @@ -675,7 +786,7 @@ void GodotIK::initialize_connections(Node *p_root) { void GodotIK::update_all_transforms_from_skeleton() { Skeleton3D *skeleton = get_skeleton(); - ERR_FAIL_NULL(skeleton); + ERR_FAIL_NULL_MSG(skeleton, "[GodotIK] No skeleton during _update_all_transforms_from_skeleton."); initial_transforms.resize(identity_idx + 1); // +1 for identity index @@ -698,10 +809,7 @@ void GodotIK::make_dirty() { // ------------ Helpers ---------------- Vector GodotIK::calculate_bone_depths(Skeleton3D *p_skeleton) { - if (!p_skeleton) { - print_error("Skeleton not initialized while calculating bone depths"); - return Vector(); - } + ERR_FAIL_NULL_V_MSG(p_skeleton, Vector(), "[GodotIK] No skeleton during calculate_bone_depths."); int bone_count = p_skeleton->get_bone_count(); if (bone_count == 0) { @@ -795,10 +903,7 @@ int GodotIK::get_current_iteration() { } void GodotIK::add_external_root(GodotIKRoot *p_root) { - if (this->is_ancestor_of(p_root) || p_root->is_ancestor_of(this)) { - print_error("GodotIK can't be ancestor of its external root or vise versa."); - return; - } + ERR_FAIL_COND_MSG(this->is_ancestor_of(p_root) || p_root->is_ancestor_of(this), "[GodotIK] can't be ancestor of its external root or vise versa."); external_roots.push_back(p_root); if (get_skeleton()) { initialize_if_dirty(); diff --git a/src/godot_ik.h b/src/godot_ik.h index cbe3b44..35c1740 100644 --- a/src/godot_ik.h +++ b/src/godot_ik.h @@ -27,6 +27,8 @@ class GDE_EXPORT GodotIK : public SkeletonModifier3D { GodotIKEffector *effector; Vector3 effector_position; Vector constraints; + int closest_parent_in_chain = -1; + int pivot_child_in_ancestor = -1; }; public: @@ -94,7 +96,7 @@ class GDE_EXPORT GodotIK : public SkeletonModifier3D { Callable callable_deinitialize; // update ------ - + void propagate_positions_from_chain_ancestors(); void solve_forward(); void solve_backward(); void apply_positions();