From 8a984f4f4e3b569cecc03794618c3c09a829bbba Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Thu, 15 May 2025 14:30:01 +0200 Subject: [PATCH] Anim: Add "replace" mix mode to the Action Constraint The available mix modes on the Action Constraint only allowed *combining* the Action's transforms with the input transforms, but unlike most other constraints lacked a way to completely override/replace those transforms. This PR adds a "Replace" mix mode to the Action Constraint, bringing it in line with most of the other constraints already in Blender. ![action_constraint_replace_mode_screenshot.png](/attachments/51fb09d6-0a87-42dc-a75e-9ae81c856796) ---- Test file: [action_constraint_replace_mode.blend](/attachments/fc3417a8-b60a-4212-9840-5b59191e9ed9) - The small bone at the top is the action constraint target (translating it right-left triggers the action constraint). - Both two-bone chains are set up with action constraints. The base bones of each chain additionally have a copy location constraint to the small sideways bone, placed before the action constraint in their constraint stack. - The chain on the left has the default mix mode, which allows you to manipulate the bones on top of what the action constraint does, and allows the copy location constraint on the base bone to work. - The bones on the right have the new "Replace" mix mode, and therefore manipulating them does not affect the final constrained transformation, and the copy location on the base bone is overridden by the action constraint. Pull Request: https://projects.blender.org/blender/blender/pulls/138316 --- .../blender/blenkernel/intern/constraint.cc | 5 +++ .../blender/makesdna/DNA_constraint_types.h | 2 + .../blender/makesrna/intern/rna_constraint.cc | 6 +++ tests/python/bl_constraints.py | 41 +++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/source/blender/blenkernel/intern/constraint.cc b/source/blender/blenkernel/intern/constraint.cc index e2d4c06a5ae..f1d45e5192e 100644 --- a/source/blender/blenkernel/intern/constraint.cc +++ b/source/blender/blenkernel/intern/constraint.cc @@ -2915,6 +2915,11 @@ static void actcon_evaluate(bConstraint *con, bConstraintOb *cob, ListBase *targ if (VALID_CONS_TARGET(ct) || data->flag & ACTCON_USE_EVAL_TIME) { switch (data->mix_mode) { + /* Replace the input transformation. */ + case ACTCON_MIX_REPLACE: + copy_m4_m4(cob->matrix, ct->matrix); + break; + /* Simple matrix multiplication. */ case ACTCON_MIX_BEFORE_FULL: mul_m4_m4m4(cob->matrix, ct->matrix, cob->matrix); diff --git a/source/blender/makesdna/DNA_constraint_types.h b/source/blender/makesdna/DNA_constraint_types.h index 0bb44d77d97..86a7487d0a3 100644 --- a/source/blender/makesdna/DNA_constraint_types.h +++ b/source/blender/makesdna/DNA_constraint_types.h @@ -842,6 +842,8 @@ typedef enum eActionConstraint_Flags { /** #bActionConstraint.mix_mode */ typedef enum eActionConstraint_MixMode { + /* Replace the input transformation. */ + ACTCON_MIX_REPLACE = 6, /* Multiply the action transformation on the right. */ ACTCON_MIX_AFTER_FULL = 0, /* Multiply the action transformation on the left. */ diff --git a/source/blender/makesrna/intern/rna_constraint.cc b/source/blender/makesrna/intern/rna_constraint.cc index 663bb2b1917..4fc5ab608c8 100644 --- a/source/blender/makesrna/intern/rna_constraint.cc +++ b/source/blender/makesrna/intern/rna_constraint.cc @@ -1848,6 +1848,12 @@ static void rna_def_constraint_action(BlenderRNA *brna) }; static const EnumPropertyItem mix_mode_items[] = { + {ACTCON_MIX_REPLACE, + "REPLACE", + 0, + "Replace", + "Replace the original transformation with the action channels"}, + RNA_ENUM_ITEM_SEPR, {ACTCON_MIX_BEFORE_FULL, "BEFORE_FULL", 0, diff --git a/tests/python/bl_constraints.py b/tests/python/bl_constraints.py index 431f00fe018..4e76d46f422 100644 --- a/tests/python/bl_constraints.py +++ b/tests/python/bl_constraints.py @@ -460,6 +460,47 @@ class ActionConstraintTest(AbstractConstraintTests): con.action_slot, "Assigning an Action with a virgin slot should automatically select that slot") + def test_mix_modes(self): + owner = bpy.context.scene.objects["Action.owner"] + target = bpy.context.scene.objects["Action.target"] + + action = bpy.data.actions.new("Slotted") + slot = action.slots.new('OBJECT', "Slot") + layer = action.layers.new(name="Layer") + strip = layer.strips.new(type='KEYFRAME') + strip.key_insert(slot, "location", 0, 2.0, 0.0) + strip.key_insert(slot, "location", 0, 7.0, 10.0) + + con = owner.constraints["Action"] + con.action = action + con.action_slot = slot + con.transform_channel = 'LOCATION_X' + con.min = -1.0 + con.max = 1.0 + con.frame_start = 0 + con.frame_end = 10 + + # Set the constrained object's location to something other than [0,0,0], + # so we can verify that it's actually replaced/mixed as appropriate to + # the mix mode. + owner.location = (2.0, 3.0, 4.0) + + con.mix_mode = 'REPLACE' + self.matrix_test("Action.owner", Matrix(( + (1.0, 0.0, 0.0, 4.5), + (0.0, 1.0, 0.0, 0.0), + (0.0, 0.0, 1.0, 0.0), + (0.0, 0.0, 0.0, 1.0) + ))) + + con.mix_mode = 'BEFORE_SPLIT' + self.matrix_test("Action.owner", Matrix(( + (1.0, 0.0, 0.0, 6.5), + (0.0, 1.0, 0.0, 3.0), + (0.0, 0.0, 1.0, 4.0), + (0.0, 0.0, 0.0, 1.0) + ))) + def main(): global args