Constraints: replace 'Set Inverse' operator with an eval-time update

This fixes {T70269}.

Before this commit there was complicated code to try and compute the
correct parent inverse matrix for the 'Child Of' and 'Object Solver'
constraints outside the constraint evaluation. This was done mostly
correctly, but did have some issues. The Set Inverse operator now defers
this computation to be performed during constraint evaluation by just
setting a flag. If the constraint is disabled, and thus tagging it for
update in the depsgraph is not enough to trigger immediate evaluation,
evaluation is forced by temporarily enabling it.

This fix changes the way how the inverse matrix works when some of the
channels of the constraint are disabled. Before this commit, the channel
flags were used to filter both the parent and the inverse matrix. This
meant that it was impossible to make an inverse matrix that would
actually fully neutralize the effect of the constraint. Now only the
parent matrix is filtered, while inverse is applied fully. As a result,
pressing the 'Set Inverse' matrix produces the same transformation as
disabling the constraint. This is also reflected in the changed values
in the 'Child Of' unit test.

This change is not backward compatible, but it should be OK because the
old way was effectively unusable, so it is unlikely anybody relied on
it.

The change in matrix for the Object Solver constraint is due to a
different method of computing it, which caused a slightly different
floating point error that was slightly bigger than allowed by the test,
so I updated the matrix values there as well.

This patch was original written by @angavrilov and subsequently updated
by me.

Differential Revision: https://developer.blender.org/D6091
This commit is contained in:
Sybren A. Stüvel 2020-02-27 10:24:11 +01:00
parent 4952fb1669
commit 10162d68e3
5 changed files with 135 additions and 181 deletions

@ -860,93 +860,87 @@ static void childof_evaluate(bConstraint *con, bConstraintOb *cob, ListBase *tar
}
float parmat[4][4];
/* simple matrix parenting */
if (data->flag == CHILDOF_ALL) {
/* multiply target (parent matrix) by offset (parent inverse) to get
* the effect of the parent that will be exerted on the owner
*/
mul_m4_m4m4(parmat, ct->matrix, data->invmat);
/* now multiply the parent matrix by the owner matrix to get the
* the effect of this constraint (i.e. owner is 'parented' to parent)
*/
mul_m4_m4m4(cob->matrix, parmat, cob->matrix);
/* Simple matrix parenting. */
if ((data->flag & CHILDOF_ALL) == CHILDOF_ALL) {
copy_m4_m4(parmat, ct->matrix);
}
/* Filter the parent matrix by channel. */
else {
float invmat[4][4], tempmat[4][4];
float loc[3], eul[3], size[3];
float loco[3], eulo[3], sizo[3];
/* get offset (parent-inverse) matrix */
copy_m4_m4(invmat, data->invmat);
/* extract components of both matrices */
copy_v3_v3(loc, ct->matrix[3]);
mat4_to_eulO(eul, ct->rotOrder, ct->matrix);
mat4_to_size(size, ct->matrix);
copy_v3_v3(loco, invmat[3]);
mat4_to_eulO(eulo, cob->rotOrder, invmat);
mat4_to_size(sizo, invmat);
/* disable channels not enabled */
if (!(data->flag & CHILDOF_LOCX)) {
loc[0] = loco[0] = 0.0f;
loc[0] = 0.0f;
}
if (!(data->flag & CHILDOF_LOCY)) {
loc[1] = loco[1] = 0.0f;
loc[1] = 0.0f;
}
if (!(data->flag & CHILDOF_LOCZ)) {
loc[2] = loco[2] = 0.0f;
loc[2] = 0.0f;
}
if (!(data->flag & CHILDOF_ROTX)) {
eul[0] = eulo[0] = 0.0f;
eul[0] = 0.0f;
}
if (!(data->flag & CHILDOF_ROTY)) {
eul[1] = eulo[1] = 0.0f;
eul[1] = 0.0f;
}
if (!(data->flag & CHILDOF_ROTZ)) {
eul[2] = eulo[2] = 0.0f;
eul[2] = 0.0f;
}
if (!(data->flag & CHILDOF_SIZEX)) {
size[0] = sizo[0] = 1.0f;
size[0] = 1.0f;
}
if (!(data->flag & CHILDOF_SIZEY)) {
size[1] = sizo[1] = 1.0f;
size[1] = 1.0f;
}
if (!(data->flag & CHILDOF_SIZEZ)) {
size[2] = sizo[2] = 1.0f;
size[2] = 1.0f;
}
/* make new target mat and offset mat */
loc_eulO_size_to_mat4(ct->matrix, loc, eul, size, ct->rotOrder);
loc_eulO_size_to_mat4(invmat, loco, eulo, sizo, cob->rotOrder);
loc_eulO_size_to_mat4(parmat, loc, eul, size, ct->rotOrder);
}
/* multiply target (parent matrix) by offset (parent inverse) to get
* the effect of the parent that will be exerted on the owner
*/
mul_m4_m4m4(parmat, ct->matrix, invmat);
/* Compute the inverse matrix if requested. */
if (data->flag & CHILDOF_SET_INVERSE) {
invert_m4_m4(data->invmat, parmat);
/* now multiply the parent matrix by the owner matrix to get the
* the effect of this constraint (i.e. owner is 'parented' to parent)
*/
copy_m4_m4(tempmat, cob->matrix);
mul_m4_m4m4(cob->matrix, parmat, tempmat);
data->flag &= ~CHILDOF_SET_INVERSE;
/* without this, changes to scale and rotation can change location
/* Write the computed matrix back to the master copy if in COW evaluation. */
bConstraint *orig_con = constraint_find_original_for_update(cob, con);
if (orig_con != NULL) {
bChildOfConstraint *orig_data = orig_con->data;
copy_m4_m4(orig_data->invmat, data->invmat);
orig_data->flag &= ~CHILDOF_SET_INVERSE;
}
}
/* Multiply together the target (parent) matrix, parent inverse,
* and the owner transform matrixto get the effect of this constraint
* (i.e. owner is 'parented' to parent). */
float orig_cob_matrix[4][4];
copy_m4_m4(orig_cob_matrix, cob->matrix);
mul_m4_series(cob->matrix, parmat, data->invmat, orig_cob_matrix);
/* Without this, changes to scale and rotation can change location
* of a parentless bone or a disconnected bone. Even though its set
* to zero above. */
if (!(data->flag & CHILDOF_LOCX)) {
cob->matrix[3][0] = tempmat[3][0];
cob->matrix[3][0] = orig_cob_matrix[3][0];
}
if (!(data->flag & CHILDOF_LOCY)) {
cob->matrix[3][1] = tempmat[3][1];
cob->matrix[3][1] = orig_cob_matrix[3][1];
}
if (!(data->flag & CHILDOF_LOCZ)) {
cob->matrix[3][2] = tempmat[3][2];
}
cob->matrix[3][2] = orig_cob_matrix[3][2];
}
}
@ -4891,23 +4885,35 @@ static void objectsolver_evaluate(bConstraint *con, bConstraintOb *cob, ListBase
return;
}
float mat[4][4], obmat[4][4], imat[4][4], cammat[4][4], camimat[4][4], parmat[4][4];
float mat[4][4], obmat[4][4], imat[4][4], parmat[4][4];
float ctime = DEG_get_ctime(depsgraph);
float framenr = BKE_movieclip_remap_scene_to_clip_frame(clip, ctime);
BKE_object_where_is_calc_mat4(camob, cammat);
BKE_tracking_camera_get_reconstructed_interpolate(tracking, object, framenr, mat);
invert_m4_m4(camimat, cammat);
mul_m4_m4m4(parmat, cammat, data->invmat);
invert_m4_m4(imat, mat);
mul_m4_m4m4(parmat, camob->obmat, imat);
copy_m4_m4(cammat, camob->obmat);
copy_m4_m4(obmat, cob->matrix);
invert_m4_m4(imat, mat);
/* Recalculate the inverse matrix if requested. */
if (data->flag & OBJECTSOLVER_SET_INVERSE) {
invert_m4_m4(data->invmat, parmat);
mul_m4_series(cob->matrix, cammat, imat, camimat, parmat, obmat);
data->flag &= ~OBJECTSOLVER_SET_INVERSE;
/* Write the computed matrix back to the master copy if in COW evaluation. */
bConstraint *orig_con = constraint_find_original_for_update(cob, con);
if (orig_con != NULL) {
bObjectSolverConstraint *orig_data = orig_con->data;
copy_m4_m4(orig_data->invmat, data->invmat);
orig_data->flag &= ~OBJECTSOLVER_SET_INVERSE;
}
}
mul_m4_series(cob->matrix, parmat, data->invmat, obmat);
}
static bConstraintTypeInfo CTI_OBJECTSOLVER = {

@ -871,118 +871,31 @@ void CONSTRAINT_OT_limitdistance_reset(wmOperatorType *ot)
/* ------------- Child-Of Constraint ------------------ */
static void child_get_inverse_matrix_owner_bone(
Depsgraph *depsgraph, wmOperator *op, Scene *scene, Object *ob, float invmat[4][4])
/* Force evaluation so that the 'set inverse' flag is handled.
* No-op when the constraint is enabled, as in such cases the evaluation will happen anyway.
*/
static void force_evaluation_if_constraint_disabled(bContext *C, Object *ob, bConstraint *con)
{
/* For bone owner we want to do this in evaluated domain.
* BKE_pose_where_is / BKE_pose_where_is_bone relies on (re)evaluating parts of the scene
* and copying new evaluated stuff back to original.
*/
Object *ob_eval = DEG_get_evaluated_object(depsgraph, ob);
bConstraint *con_eval = edit_constraint_property_get(op, ob_eval, CONSTRAINT_TYPE_CHILDOF);
/* nullify inverse matrix first */
unit_m4(invmat);
bPoseChannel *pchan_eval = BKE_pose_channel_active(ob_eval);
/* try to find a pose channel - assume that this is the constraint owner */
/* TODO: get from context instead? */
if (ob_eval && ob_eval->pose && pchan_eval) {
bConstraint *con_last;
/* calculate/set inverse matrix:
* We just calculate all transform-stack eval up to but not including this constraint.
* This is because inverse should just inverse correct for just the constraint's influence
* when it gets applied; that is, at the time of application, we don't know anything about
* what follows.
*/
float imat[4][4], tmat[4][4];
float pmat[4][4];
/* make sure we passed the correct constraint */
BLI_assert(BLI_findindex(&pchan_eval->constraints, con_eval) != -1);
/* 1. calculate posemat where inverse doesn't exist yet (inverse was cleared above),
* to use as baseline ("pmat") to derive delta from. This extra calc saves users
* from having pressing "Clear Inverse" first
*/
BKE_pose_where_is(depsgraph, scene, ob_eval);
copy_m4_m4(pmat, pchan_eval->pose_mat);
/* 2. knock out constraints starting from this one */
con_last = pchan_eval->constraints.last;
pchan_eval->constraints.last = con_eval->prev;
if (con_eval->prev) {
/* new end must not point to this one, else this chain cutting is useless */
con_eval->prev->next = NULL;
}
else {
/* constraint was first */
pchan_eval->constraints.first = NULL;
if ((con->flag & (CONSTRAINT_DISABLE | CONSTRAINT_OFF)) == 0) {
return;
}
/* 3. solve pose without disabled constraints */
BKE_pose_where_is(depsgraph, scene, ob_eval);
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
Scene *scene = DEG_get_evaluated_scene(depsgraph);
/* 4. determine effect of constraint by removing the newly calculated
* pchan->pose_mat from the original pchan->pose_mat, thus determining
* the effect of the constraint
*/
invert_m4_m4(imat, pchan_eval->pose_mat);
mul_m4_m4m4(tmat, pmat, imat);
invert_m4_m4(invmat, tmat);
/* 5. restore constraints */
pchan_eval->constraints.last = con_last;
if (con_eval->prev) {
/* hook up prev to this one again */
con_eval->prev->next = con_eval;
}
else {
/* set as first again */
pchan_eval->constraints.first = con_eval;
}
/* 6. recalculate pose with new inv-mat applied */
/* this one is unnecessary? (DEG seems to update correctly without)
+ if we leave this in, we have to click "Set Inverse" twice to see updates...
BKE_pose_where_is(depsgraph, scene, ob_eval); */
}
}
static void child_get_inverse_matrix_owner_object(
Depsgraph *depsgraph, Scene *scene, Object *ob, bConstraint *con, float invmat[4][4])
{
/* nullify inverse matrix first */
unit_m4(invmat);
if (ob) {
Object workob;
/* make sure we passed the correct constraint */
BLI_assert(BLI_findindex(&ob->constraints, con) != -1);
UNUSED_VARS_NDEBUG(con);
/* use BKE_object_workob_calc_parent to find inverse - just like for normal parenting */
BKE_object_workob_calc_parent(depsgraph, scene, ob, &workob);
invert_m4_m4(invmat, workob.obmat);
}
short flag_backup = con->flag;
con->flag &= ~(CONSTRAINT_DISABLE | CONSTRAINT_OFF);
BKE_object_eval_constraints(depsgraph, scene, ob);
con->flag = flag_backup;
}
/* ChildOf Constraint - set inverse callback */
static int childof_set_inverse_exec(bContext *C, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
Scene *scene = CTX_data_scene(C);
Object *ob = ED_object_active_context(C);
bConstraint *con = edit_constraint_property_get(op, ob, CONSTRAINT_TYPE_CHILDOF);
bChildOfConstraint *data = (con) ? (bChildOfConstraint *)con->data : NULL;
const int owner = RNA_enum_get(op->ptr, "owner");
/* despite 3 layers of checks, we may still not be able to find a constraint */
if (data == NULL) {
@ -991,12 +904,11 @@ static int childof_set_inverse_exec(bContext *C, wmOperator *op)
return OPERATOR_CANCELLED;
}
if (owner == EDIT_CONSTRAINT_OWNER_OBJECT) {
child_get_inverse_matrix_owner_object(depsgraph, scene, ob, con, data->invmat);
}
else if (owner == EDIT_CONSTRAINT_OWNER_BONE) {
child_get_inverse_matrix_owner_bone(depsgraph, op, scene, ob, data->invmat);
}
/* Set a flag to request recalculation on next update. */
data->flag |= CHILDOF_SET_INVERSE;
/* Force constraint to run, it will perform the recalculation. */
force_evaluation_if_constraint_disabled(C, ob, con);
ED_object_constraint_update(bmain, ob);
WM_event_add_notifier(C, NC_OBJECT | ND_CONSTRAINT, ob);
@ -1231,12 +1143,9 @@ void CONSTRAINT_OT_followpath_path_animate(wmOperatorType *ot)
static int objectsolver_set_inverse_exec(bContext *C, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
Scene *scene = CTX_data_scene(C);
Object *ob = ED_object_active_context(C);
bConstraint *con = edit_constraint_property_get(op, ob, CONSTRAINT_TYPE_OBJECTSOLVER);
bObjectSolverConstraint *data = (con) ? (bObjectSolverConstraint *)con->data : NULL;
const int owner = RNA_enum_get(op->ptr, "owner");
/* despite 3 layers of checks, we may still not be able to find a constraint */
if (data == NULL) {
@ -1246,12 +1155,11 @@ static int objectsolver_set_inverse_exec(bContext *C, wmOperator *op)
return OPERATOR_CANCELLED;
}
if (owner == EDIT_CONSTRAINT_OWNER_OBJECT) {
child_get_inverse_matrix_owner_object(depsgraph, scene, ob, con, data->invmat);
}
else if (owner == EDIT_CONSTRAINT_OWNER_BONE) {
child_get_inverse_matrix_owner_bone(depsgraph, op, scene, ob, data->invmat);
}
/* Set a flag to request recalculation on next update. */
data->flag |= OBJECTSOLVER_SET_INVERSE;
/* Force constraint to run, it will perform the recalculation. */
force_evaluation_if_constraint_disabled(C, ob, con);
ED_object_constraint_update(bmain, ob);
WM_event_add_notifier(C, NC_OBJECT | ND_CONSTRAINT, ob);

@ -1096,6 +1096,8 @@ typedef enum eChildOf_Flags {
CHILDOF_SIZEY = (1 << 7),
CHILDOF_SIZEZ = (1 << 8),
CHILDOF_ALL = 511,
/* Temporary flag used by the Set Inverse operator. */
CHILDOF_SET_INVERSE = (1 << 9),
} eChildOf_Flags;
/* Pivot Constraint */
@ -1147,6 +1149,8 @@ typedef enum eCameraSolver_Flags {
/* ObjectSolver Constraint -> flag */
typedef enum eObjectSolver_Flags {
OBJECTSOLVER_ACTIVECLIP = (1 << 0),
/* Temporary flag used by the Set Inverse operator. */
OBJECTSOLVER_SET_INVERSE = (1 << 1),
} eObjectSolver_Flags;
/* ObjectSolver Constraint -> flag */

@ -973,11 +973,18 @@ static void rna_def_constraint_childof(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Scale Z", "Use Z Scale of Parent");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
prop = RNA_def_property(srna, "set_inverse_pending", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, NULL, "flag", CHILDOF_SET_INVERSE);
RNA_def_property_ui_text(
prop, "Set Inverse Pending", "Set to true to request recalculation of the inverse matrix");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
prop = RNA_def_property(srna, "inverse_matrix", PROP_FLOAT, PROP_MATRIX);
RNA_def_property_float_sdna(prop, NULL, "invmat");
RNA_def_property_multi_array(prop, 2, rna_matrix_dimsize_4x4);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_ui_text(prop, "Inverse Matrix", "Transformation matrix to apply before");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
}
static void rna_def_constraint_python(BlenderRNA *brna)
@ -3152,6 +3159,12 @@ static void rna_def_constraint_object_solver(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Active Clip", "Use active clip defined in scene");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
prop = RNA_def_property(srna, "set_inverse_pending", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, NULL, "flag", OBJECTSOLVER_SET_INVERSE);
RNA_def_property_ui_text(
prop, "Set Inverse Pending", "Set to true to request recalculation of the inverse matrix");
RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update");
/* object */
prop = RNA_def_property(srna, "object", PROP_STRING, PROP_NONE);
RNA_def_property_string_sdna(prop, NULL, "object");

@ -156,9 +156,9 @@ class ChildOfTest(AbstractConstraintTests):
context = self.constraint_context('Child Of', owner_name='Child Of.object.owner')
bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of')
self.matrix_test('Child Of.object.owner', Matrix((
(0.9228900671005249, 0.23250490427017212, -0.035540513694286346, 0.10000000149011612),
(-0.011224273592233658, 0.9838480949401855, 0.24731633067131042, 0.21246682107448578),
(0.0383986234664917, -0.3163823187351227, 0.9553266167640686, 0.27248233556747437),
(0.9992386102676392, 0.019843991845846176, -0.03359176218509674, 0.10000000149011612),
(-0.017441775649785995, 0.997369647026062, 0.0703534483909607, 0.2000001221895218),
(0.034899499267339706, -0.06971398741006851, 0.996956467628479, 0.3000001311302185),
(0.0, 0.0, 0.0, 1.0),
)))
@ -188,6 +188,29 @@ class ChildOfTest(AbstractConstraintTests):
bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of')
self.matrix_test('Child Of.armature.owner', initial_matrix)
def test_vertexgroup_simple_parent(self):
"""Child Of: simple evaluation of vertex group parent."""
initial_matrix = Matrix((
(-0.8076590895652771, 0.397272527217865, 0.4357309341430664, 1.188504934310913),
(-0.4534659683704376, -0.8908230066299438, -0.028334975242614746, 1.7851561307907104),
(0.3769024908542633, -0.22047416865825653, 0.8996308445930481, 3.4457669258117676),
(0.0, 0.0, 0.0, 1.0),
))
self.matrix_test('Child Of.vertexgroup.owner', initial_matrix)
context = self.constraint_context('Child Of', owner_name='Child Of.vertexgroup.owner')
bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of')
self.matrix_test('Child Of.vertexgroup.owner', Matrix((
(0.9992386102676392, 0.019843988120555878, -0.03359176218509674, 0.10000000149011612),
(-0.017441775649785995, 0.997369647026062, 0.0703534483909607, 0.20000000298023224),
(0.03489949554204941, -0.06971397995948792, 0.9969563484191895, 0.30000001192092896),
(0.0, 0.0, 0.0, 1.0),
)))
bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of')
self.matrix_test('Child Of.vertexgroup.owner', initial_matrix)
class ObjectSolverTest(AbstractConstraintTests):
layer_collection = 'Object Solver'
@ -205,9 +228,9 @@ class ObjectSolverTest(AbstractConstraintTests):
context = self.constraint_context('Object Solver')
bpy.ops.constraint.objectsolver_set_inverse(context, constraint='Object Solver')
self.matrix_test('Object Solver.owner', Matrix((
(0.9992386102676392, 0.019843988120555878, -0.03359176218509674, 0.10000000149011612),
(-0.017441775649785995, 0.997369647026062, 0.0703534483909607, 0.20000000298023224),
(0.03489949554204941, -0.06971397995948792, 0.9969563484191895, 0.30000001192092896),
(0.9992387294769287, 0.019843989983201027, -0.03359176591038704, 0.10000025480985641),
(-0.017441747710108757, 0.9973697662353516, 0.07035345584154129, 0.1999993920326233),
(0.034899502992630005, -0.06971398741006851, 0.996956467628479, 0.29999980330467224),
(0.0, 0.0, 0.0, 1.0),
)))