From b2653be057a86858a3ce9a892189a89278912a91 Mon Sep 17 00:00:00 2001 From: Christoph Lendenfeld Date: Fri, 26 Sep 2025 13:25:01 +0200 Subject: [PATCH] Anim: Place Pose Bone Gizmo at Custom Transform This PR adds an option "Affect Gizmo" for custom shape transforms to affect the transform gizmos. If enabled, this will place the gizmo at the location and orientation of the "Override Transform" (i.e. in its local space). For Orientations: The gizmo mode *is* respected. I.e. global mode is aligned to the world etc. There is a special case for "Gimbal" where it will not follow the orientation of the "Override Transform" bone. I think it makes sense to keep it that way since this is about the channels of the bone you are actually manipulating. The other option is "Use as Pivot" with which the bone is actually rotated around the override bone. This can be useful for rigs in which shapekeys and armature deformation is combined Taken over from #136468 For design task #135429 Co-authored-by: Wayde Moss Pull Request: https://projects.blender.org/blender/blender/pulls/142847 --- scripts/startup/bl_ui/properties_data_bone.py | 6 +++ source/blender/blenkernel/BKE_action.hh | 16 ++++++++ source/blender/blenkernel/intern/action.cc | 40 +++++++++++++++++++ source/blender/editors/object/object_utils.cc | 3 +- source/blender/editors/transform/transform.hh | 6 +++ .../transform/transform_convert_armature.cc | 19 ++++++--- .../editors/transform/transform_gizmo_3d.cc | 7 +++- .../editors/transform/transform_mode.cc | 5 ++- .../transform/transform_orientations.cc | 15 +++++-- source/blender/makesdna/DNA_action_types.h | 11 ++++- source/blender/makesrna/intern/rna_pose.cc | 20 ++++++++++ 11 files changed, 133 insertions(+), 15 deletions(-) diff --git a/scripts/startup/bl_ui/properties_data_bone.py b/scripts/startup/bl_ui/properties_data_bone.py index 2d5a6223bed..f969a1a4180 100644 --- a/scripts/startup/bl_ui/properties_data_bone.py +++ b/scripts/startup/bl_ui/properties_data_bone.py @@ -418,6 +418,12 @@ class BONE_PT_display_custom_shape(BoneButtonsPanel, Panel): sub.prop(pchan, "custom_shape_scale_xyz", text="Scale") sub.prop_search(pchan, "custom_shape_transform", ob.pose, "bones", text="Override Transform") + subsub = sub.column() + subsub.active = bool(pchan and pchan.custom_shape and pchan.custom_shape_transform) + subsub.prop(pchan, "use_transform_at_custom_shape") + subsubsub = subsub.column() + subsubsub.active = subsub.active and pchan.use_transform_at_custom_shape + subsubsub.prop(pchan, "use_transform_around_custom_shape") sub.prop(pchan, "use_custom_shape_bone_size") sub.separator() diff --git a/source/blender/blenkernel/BKE_action.hh b/source/blender/blenkernel/BKE_action.hh index 98e53dcb49e..7cf9188b0f1 100644 --- a/source/blender/blenkernel/BKE_action.hh +++ b/source/blender/blenkernel/BKE_action.hh @@ -16,6 +16,7 @@ struct BlendDataReader; struct BlendLibReader; struct BlendWriter; struct bArmature; +struct BoneParentTransform; /* The following structures are defined in DNA_action_types.h, and DNA_anim_types.h */ struct AnimationEvalContext; @@ -308,6 +309,21 @@ void BKE_pose_itasc_init(bItasc *itasc); */ bool BKE_pose_channel_in_IK_chain(Object *ob, bPoseChannel *pchan); +/** + * Get the transform location, accounting for POSE_TRANSFORM_AT_CUSTOM_TX. + */ +void BKE_pose_channel_transform_location(const bArmature *arm, + const bPoseChannel *pose_bone, + float r_pose_space_pivot[3]); + +/** + * Get the transform pose orientation, accounting for + * POSE_TRANSFORM_AT_CUSTOM_TX. + */ +void BKE_pose_channel_transform_orientation(const bArmature *arm, + const bPoseChannel *pose_bone, + float r_pose_orientation[3][3]); + /* Bone Groups API --------------------- */ /** diff --git a/source/blender/blenkernel/intern/action.cc b/source/blender/blenkernel/intern/action.cc index 4fe27af5a1b..08f33456219 100644 --- a/source/blender/blenkernel/intern/action.cc +++ b/source/blender/blenkernel/intern/action.cc @@ -1398,6 +1398,46 @@ bool BKE_pose_channel_in_IK_chain(Object *ob, bPoseChannel *pchan) return pose_channel_in_IK_chain(ob, pchan, 0); } +static bool transform_follows_custom_tx(const bArmature *arm, const bPoseChannel *pchan) +{ + if (arm->flag & ARM_NO_CUSTOM) { + return false; + } + + if (!pchan->custom || !pchan->custom_tx) { + return false; + } + + return pchan->flag & POSE_TRANSFORM_AT_CUSTOM_TX; +} + +void BKE_pose_channel_transform_orientation(const bArmature *arm, + const bPoseChannel *pose_bone, + float r_pose_orientation[3][3]) +{ + if (!transform_follows_custom_tx(arm, pose_bone)) { + copy_m3_m4(r_pose_orientation, pose_bone->pose_mat); + return; + } + + BLI_assert(pose_bone->custom_tx); + + const bPoseChannel *custom_tx_bone = pose_bone->custom_tx; + copy_m3_m4(r_pose_orientation, custom_tx_bone->pose_mat); +} + +void BKE_pose_channel_transform_location(const bArmature *arm, + const bPoseChannel *pose_bone, + float r_pose_space_pivot[3]) +{ + if (!transform_follows_custom_tx(arm, pose_bone)) { + copy_v3_v3(r_pose_space_pivot, pose_bone->pose_mat[3]); + return; + } + + copy_v3_v3(r_pose_space_pivot, pose_bone->custom_tx->pose_mat[3]); +} + void BKE_pose_channels_hash_ensure(bPose *pose) { if (!pose->chanhash) { diff --git a/source/blender/editors/object/object_utils.cc b/source/blender/editors/object/object_utils.cc index b16c443cc2e..31ccf00f6f2 100644 --- a/source/blender/editors/object/object_utils.cc +++ b/source/blender/editors/object/object_utils.cc @@ -126,7 +126,8 @@ bool calc_active_center_for_posemode(Object *ob, const bool select_only, float r { bPoseChannel *pchan = BKE_pose_channel_active_if_bonecoll_visible(ob); if (pchan && (!select_only || (pchan->bone->flag & BONE_SELECTED))) { - copy_v3_v3(r_center, pchan->pose_head); + const bArmature *arm = static_cast(ob->data); + BKE_pose_channel_transform_location(arm, pchan, r_center); return true; } return false; diff --git a/source/blender/editors/transform/transform.hh b/source/blender/editors/transform/transform.hh index 9c7011d5149..38a35c4e2b2 100644 --- a/source/blender/editors/transform/transform.hh +++ b/source/blender/editors/transform/transform.hh @@ -471,6 +471,12 @@ struct TransDataExtension { int rotOrder; /** Original object transformation used for rigid bodies. */ float oloc[3], orot[3], oquat[4], orotAxis[3], orotAngle; + + /** + * Use when #TransDataBasic::center has been overridden but the real center is still needed + * for internal calculations. + */ + float center_no_override[3]; }; struct TransData2D { diff --git a/source/blender/editors/transform/transform_convert_armature.cc b/source/blender/editors/transform/transform_convert_armature.cc index c4d5ee9eec4..15f6d36b45c 100644 --- a/source/blender/editors/transform/transform_convert_armature.cc +++ b/source/blender/editors/transform/transform_convert_armature.cc @@ -395,10 +395,15 @@ static void add_pose_transdata( Bone *bone = pchan->bone; float pmat[3][3], omat[3][3]; float cmat[3][3], tmat[3][3]; - float vec[3]; - copy_v3_v3(vec, pchan->pose_mat[3]); - copy_v3_v3(td->center, vec); + const bArmature *arm = static_cast(ob->data); + BKE_pose_channel_transform_location(arm, pchan, td->center); + if (pchan->flag & POSE_TRANSFORM_AROUND_CUSTOM_TX) { + copy_v3_v3(td_ext->center_no_override, pchan->pose_mat[3]); + } + else { + copy_v3_v3(td_ext->center_no_override, td->center); + } td->flag = TD_SELECTED; if (bone->flag & BONE_HINGE_CHILD_TRANSFORM) { @@ -449,11 +454,12 @@ static void add_pose_transdata( /* Proper way to get parent transform + our own transform + constraints transform. */ copy_m3_m4(omat, ob->object_to_world().ptr()); - /* New code, using "generic" BKE_bone_parent_transform_calc_from_pchan(). */ { BoneParentTransform bpt; float rpmat[3][3]; + /* Not using the pchan->custom_tx here because we need the transformation to be + * relative to the actual bone being modified, not it's visual representation. */ BKE_bone_parent_transform_calc_from_pchan(pchan, &bpt); if (t->mode == TFM_TRANSLATION) { copy_m3_m4(pmat, bpt.loc_mat); @@ -498,7 +504,7 @@ static void add_pose_transdata( } /* For `axismtx` we use the bone's own transform. */ - copy_m3_m4(pmat, pchan->pose_mat); + BKE_pose_channel_transform_orientation(arm, pchan, pmat); mul_m3_m3m3(td->axismtx, omat, pmat); normalize_m3(td->axismtx); @@ -703,6 +709,9 @@ static void createTransPose(bContext * /*C*/, TransInfo *t) /* Use pose channels to fill trans data. */ td = tc->data; tdx = tc->data_ext; + tdx->center_no_override[0] = 0; + tdx->center_no_override[1] = 0; + tdx->center_no_override[2] = 0; LISTBASE_FOREACH (bPoseChannel *, pchan, &ob->pose->chanbase) { if (pchan->bone->flag & BONE_TRANSFORM) { add_pose_transdata(t, pchan, ob, td++, tdx++); diff --git a/source/blender/editors/transform/transform_gizmo_3d.cc b/source/blender/editors/transform/transform_gizmo_3d.cc index 1d61149b8ad..531c295b72d 100644 --- a/source/blender/editors/transform/transform_gizmo_3d.cc +++ b/source/blender/editors/transform/transform_gizmo_3d.cc @@ -22,6 +22,7 @@ #include "DNA_meta_types.h" #include "DNA_pointcloud_types.h" +#include "BKE_action.hh" #include "BKE_armature.hh" #include "BKE_context.hh" #include "BKE_crazyspace.hh" @@ -836,12 +837,16 @@ static int gizmo_3d_foreach_selected(const bContext *C, mul_m4_m4m4(mat_local, ob->world_to_object().ptr(), ob_iter->object_to_world().ptr()); } + bArmature *arm = static_cast(ob_iter->data); /* Use channels to get stats. */ LISTBASE_FOREACH (bPoseChannel *, pchan, &ob_iter->pose->chanbase) { if (!(pchan->bone->flag & BONE_TRANSFORM)) { continue; } - run_coord_with_matrix(pchan->pose_head, use_mat_local, mat_local); + + float pchan_pivot[3]; + BKE_pose_channel_transform_location(arm, pchan, pchan_pivot); + run_coord_with_matrix(pchan_pivot, use_mat_local, mat_local); totsel++; if (r_drawflags) { diff --git a/source/blender/editors/transform/transform_mode.cc b/source/blender/editors/transform/transform_mode.cc index 9975990f0dd..b8588283807 100644 --- a/source/blender/editors/transform/transform_mode.cc +++ b/source/blender/editors/transform/transform_mode.cc @@ -651,7 +651,7 @@ void ElementRotation_ex(const TransInfo *t, /* Extract and invert armature object matrix. */ if ((td->flag & TD_NO_LOC) == 0) { - sub_v3_v3v3(vec, td->center, center); + sub_v3_v3v3(vec, td_ext->center_no_override, center); mul_m3_v3(tc->mat3, vec); /* To Global space. */ mul_m3_v3(mat, vec); /* Applying rotation. */ @@ -660,7 +660,8 @@ void ElementRotation_ex(const TransInfo *t, add_v3_v3(vec, center); /* `vec` now is the location where the object has to be. */ - sub_v3_v3v3(vec, vec, td->center); /* Translation needed from the initial location. */ + /* Translation needed from the initial location. */ + sub_v3_v3v3(vec, vec, td_ext->center_no_override); /* Special exception, see TD_PBONE_LOCAL_MTX definition comments. */ if (td->flag & TD_PBONE_LOCAL_MTX_P) { diff --git a/source/blender/editors/transform/transform_orientations.cc b/source/blender/editors/transform/transform_orientations.cc index 4066af2f5a5..be5371218c9 100644 --- a/source/blender/editors/transform/transform_orientations.cc +++ b/source/blender/editors/transform/transform_orientations.cc @@ -665,6 +665,7 @@ short calc_orientation_from_type_ex(const Scene *scene, if (ob) { if (ob->mode & OB_MODE_POSE) { const bPoseChannel *pchan = BKE_pose_channel_active_if_bonecoll_visible(ob); + if (pchan && gimbal_axis_pose(ob, pchan, r_mat)) { break; } @@ -1428,8 +1429,11 @@ int getTransformOrientation_ex(const Scene *scene, bool ok = false; if (activeOnly && (pchan = BKE_pose_channel_active_if_bonecoll_visible(ob))) { - add_v3_v3(r_normal, pchan->pose_mat[2]); - add_v3_v3(r_plane, pchan->pose_mat[1]); + float pose_mat[3][3]; + BKE_pose_channel_transform_orientation(arm, pchan, pose_mat); + + add_v3_v3(r_normal, pose_mat[2]); + add_v3_v3(r_plane, pose_mat[1]); ok = true; } else { @@ -1439,8 +1443,11 @@ int getTransformOrientation_ex(const Scene *scene, /* Use channels to get stats. */ LISTBASE_FOREACH (bPoseChannel *, pchan, &ob->pose->chanbase) { if (pchan->bone && pchan->bone->flag & BONE_TRANSFORM) { - add_v3_v3(r_normal, pchan->pose_mat[2]); - add_v3_v3(r_plane, pchan->pose_mat[1]); + float pose_mat[3][3]; + BKE_pose_channel_transform_orientation(arm, pchan, pose_mat); + + add_v3_v3(r_normal, pose_mat[2]); + add_v3_v3(r_plane, pose_mat[1]); } } ok = true; diff --git a/source/blender/makesdna/DNA_action_types.h b/source/blender/makesdna/DNA_action_types.h index 4e97b7845bb..ec5fd02649c 100644 --- a/source/blender/makesdna/DNA_action_types.h +++ b/source/blender/makesdna/DNA_action_types.h @@ -303,7 +303,7 @@ typedef struct bPoseChannel { ListBase constraints; char name[/*MAXBONENAME*/ 64]; - /** Dynamic, for detecting transform changes. */ + /** Dynamic, for detecting transform changes (ePchan_Flag). */ short flag; /** Settings for IK bones. */ short ikflag; @@ -454,7 +454,14 @@ typedef enum ePchan_Flag { /* has BBone deforms */ POSE_BBONE_SHAPE = (1 << 3), - + /* When set and bPoseChan.custom_tx is not a nullptr, the gizmo will be drawn at the location and + orientation of the custom_tx instead of this bone. */ + POSE_TRANSFORM_AT_CUSTOM_TX = (1 << 4), + /* When set, transformations will modify the bone as if it was a child of the + bPoseChan.custom_tx. The flag only has an effect when `POSE_TRANSFORM_AT_CUSTOM_TX` and + `custom_tx` are set. This can be useful for rigs where the deformation is coming from + blendshapes in addition to the armature. */ + POSE_TRANSFORM_AROUND_CUSTOM_TX = (1 << 5), /* IK/Pose solving */ POSE_CHAIN = (1 << 9), POSE_DONE = (1 << 10), diff --git a/source/blender/makesrna/intern/rna_pose.cc b/source/blender/makesrna/intern/rna_pose.cc index 4af602e375f..31897d8028b 100644 --- a/source/blender/makesrna/intern/rna_pose.cc +++ b/source/blender/makesrna/intern/rna_pose.cc @@ -1170,6 +1170,26 @@ static void rna_def_pose_channel(BlenderRNA *brna) RNA_def_property_ui_range(prop, -FLT_MAX, FLT_MAX, 100, RNA_TRANSLATION_PREC_DEFAULT); RNA_def_property_update(prop, NC_OBJECT | ND_POSE, "rna_Pose_update"); + prop = RNA_def_property(srna, "use_transform_at_custom_shape", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flag", POSE_TRANSFORM_AT_CUSTOM_TX); + RNA_def_property_ui_text( + prop, + "Affect Gizmo", + "The location and orientation of the Custom Shape Transform bone will be used for transform " + "gizmos and for other transform operators in the 3D Viewport. When disabled, the 3D " + "Viewport will still use the actual bone transform for these, even when the custom bone " + "shape transform is overridden."); + RNA_def_property_update(prop, NC_OBJECT | ND_POSE, "rna_Pose_update"); + + prop = RNA_def_property(srna, "use_transform_around_custom_shape", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flag", POSE_TRANSFORM_AROUND_CUSTOM_TX); + RNA_def_property_ui_text( + prop, + "Use As Pivot", + "Transform the bone as if it was a child of the Custom Shape Transform bone. This can be " + "useful when combining shapekey and armature deformations."); + RNA_def_property_update(prop, NC_OBJECT | ND_POSE, "rna_Pose_update"); + prop = RNA_def_property(srna, "use_custom_shape_bone_size", PROP_BOOLEAN, PROP_NONE); RNA_def_property_boolean_negative_sdna( prop, nullptr, "drawflag", PCHAN_DRAW_NO_CUSTOM_BONE_SIZE);