Fix T81541: Symmetrize Transform Constraint, Y rotational axis unexpected results

The case where Y rotation is mapped to Y rotation was not handled.
This is now fixed.

Also added an automated test to make sure that the symmetrize operator
functions as intended.

Reviewed By: Sybren

Differential Revision: http://developer.blender.org/D9214
This commit is contained in:
Sebastian Parborg 2022-02-04 14:19:44 +01:00
parent 0dd3e77d71
commit 623ff64a27
Notes: blender-bot 2023-02-14 08:49:53 +01:00
Referenced by issue #81541, Symmetrize Transform Constraint, Y rotational axis unexpected results.
3 changed files with 258 additions and 2 deletions

View File

@ -581,8 +581,6 @@ static void updateDuplicateLocRotConstraintSettings(Object *ob,
{
/* This code assumes that bRotLimitConstraint and bLocLimitConstraint have the same fields in
* the same memory locations. */
BLI_assert(sizeof(bLocLimitConstraint) == sizeof(bRotLimitConstraint));
bRotLimitConstraint *limit = (bRotLimitConstraint *)curcon->data;
float local_mat[4][4], imat[4][4];
@ -798,6 +796,13 @@ static void updateDuplicateTransformConstraintSettings(Object *ob,
trans->to_min_rot[i] = temp_vec[i];
}
}
if (trans->from == TRANS_ROTATION && trans->map[1] == Y) {
/* Y Rot to Y Rot: Flip and invert */
trans->to_max_rot[1] = -trans->to_min_rot[1];
trans->to_min_rot[1] = -temp_vec[1];
}
break;
}
/* convert back to the settings space */

View File

@ -293,6 +293,13 @@ add_blender_test(
--testdir "${TEST_SRC_DIR}/animation"
)
add_blender_test(
bl_rigging_symmetrize
--python ${CMAKE_CURRENT_LIST_DIR}/bl_rigging_symmetrize.py
--
--testdir "${TEST_SRC_DIR}/animation"
)
# ------------------------------------------------------------------------------
# IO TESTS

View File

@ -0,0 +1,244 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
"""
blender -b -noaudio --factory-startup --python tests/python/bl_rigging_symmetrize.py -- --testdir /path/to/lib/tests/animation
"""
import pathlib
import sys
import unittest
import bpy
def check_loc_rot_scale(self, bone, exp_bone):
# Check if posistions are the same
self.assertEqualVector(
bone.head, exp_bone.head, "Head position", bone.name)
self.assertEqualVector(
bone.tail, exp_bone.tail, "Tail position", bone.name)
# Scale
self.assertEqualVector(
bone.scale, exp_bone.scale, "Scale", bone.name)
# Rotation
rot_mode = exp_bone.rotation_mode
self.assertEqual(bone.rotation_mode, rot_mode, "Rotations mode does not match on bone %s" % (bone.name))
if rot_mode == 'QUATERNION':
self.assertEqualVector(
bone.rotation_quaternion, exp_bone.rotation_quaternion, "Quaternion rotation", bone.name)
elif rot_mode == 'AXIS_ANGLE':
self.assertEqualVector(
bone.axis_angle, exp_bone.axis_angle, "Axis Angle rotation", bone.name)
else:
# Euler rotation
self.assertEqualVector(
bone.rotation_euler, exp_bone.rotation_euler, "Euler rotation", bone.name)
def check_parent(self, bone, exp_bone):
self.assertEqual(type(bone.parent), type(exp_bone.parent),
"Missmatching types in pose.bones[%s].parent" % (bone.name))
self.assertTrue(bone.parent is None or bone.parent.name == exp_bone.parent.name,
"Bone parent does not match on bone %s" % (bone.name))
def check_bendy_bones(self, bone, exp_bone):
bone_variables = bone.bl_rna.properties.keys()
bendy_bone_variables = [
var for var in bone_variables if var.startswith("bbone_")]
for var in bendy_bone_variables:
value = getattr(bone, var)
exp_value = getattr(exp_bone, var)
self.assertEqual(type(value), type(exp_value),
"Missmatching types in pose.bones[%s].%s" % (bone.name, var))
if isinstance(value, str):
self.assertEqual(value, exp_value,
"Missmatching value in pose.bones[%s].%s" % (bone.name, var))
elif hasattr(value, "name"):
self.assertEqual(value.name, exp_value.name,
"Missmatching value in pose.bones[%s].%s" % (bone.name, var))
else:
self.assertAlmostEqual(value, exp_value,
"Missmatching value in pose.bones[%s].%s" % (bone.name, var))
def check_ik(self, bone, exp_bone):
bone_variables = bone.bl_rna.properties.keys()
prefixes = ("ik_", "lock_ik", "use_ik")
ik_bone_variables = (
var for var in bone_variables
if var.startswith(prefixes)
)
for var in ik_bone_variables:
value = getattr(bone, var)
exp_value = getattr(exp_bone, var)
self.assertAlmostEqual(value, exp_value,
"Missmatching value in pose.bones[%s].%s" % (bone.name, var))
def check_constraints(self, input_arm, expected_arm, bone, exp_bone):
const_len = len(bone.constraints)
expo_const_len = len(exp_bone.constraints)
self.assertEqual(const_len, expo_const_len,
"Constraints missmatch on bone %s" % (bone.name))
for exp_constraint in exp_bone.constraints:
const_name = exp_constraint.name
# Make sure that the constraint exists
self.assertTrue(const_name in bone.constraints,
"Bone %s is expected to contain constraint %s, but it does not." % (
bone.name, const_name))
constraint = bone.constraints[const_name]
const_variables = constraint.bl_rna.properties.keys()
for var in const_variables:
if var == "is_override_data":
# This variable is not used for local (non linked) data.
# For local object it is not initialized, so don't check this value.
continue
value = getattr(constraint, var)
exp_value = getattr(exp_constraint, var)
self.assertEqual(type(value), type(exp_value),
"Missmatching constraint value types in pose.bones[%s].constraints[%s].%s" % (
bone.name, const_name, var))
if isinstance(value, str):
self.assertEqual(value, exp_value,
"Missmatching constraint value in pose.bones[%s].constraints[%s].%s" % (
bone.name, const_name, var))
elif hasattr(value, "name"):
# Some constraints targets the armature itself, so the armature name should missmatch.
if value.name == input_arm.name and exp_value.name == expected_arm.name:
continue
self.assertEqual(value.name, exp_value.name,
"Missmatching constraint value in pose.bones[%s].constraints[%s].%s" % (
bone.name, const_name, var))
elif isinstance(value, bool):
self.assertEqual(value, exp_value,
"Missmatching constraint boolean in pose.bones[%s].constraints[%s].%s" % (
bone.name, const_name, var))
else:
self.assertAlmostEqual(value, exp_value,
"Missmatching constraint value in pose.bones[%s].constraints[%s].%s" % (
bone.name, const_name, var))
class AbstractAnimationTest:
@classmethod
def setUpClass(cls):
cls.testdir = args.testdir
def setUp(self):
self.assertTrue(self.testdir.exists(),
'Test dir %s should exist' % self.testdir)
class ArmatureSymmetrizeTest(AbstractAnimationTest, unittest.TestCase):
def test_symmetrize_operator(self):
"""Test that the symmetrize operator is working correctly."""
bpy.ops.wm.open_mainfile(filepath=str(
self.testdir / "symm_test.blend"))
# T81541 (D9214)
arm = bpy.data.objects['transform_const_rig']
expected_arm = bpy.data.objects['expected_transform_const_rig']
self.assertEqualSymmetrize(arm, expected_arm)
# T66751 (D6009)
arm = bpy.data.objects['dragon_rig']
expected_arm = bpy.data.objects['expected_dragon_rig']
self.assertEqualSymmetrize(arm, expected_arm)
def assertEqualSymmetrize(self, input_arm, expected_arm):
# Symmetrize our input armature
bpy.context.view_layer.objects.active = input_arm
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.armature.select_all(action='SELECT')
bpy.ops.armature.symmetrize()
bpy.ops.object.mode_set(mode='OBJECT')
# Make sure that the bone count is the same
bone_len = len(input_arm.pose.bones)
expected_bone_len = len(expected_arm.pose.bones)
self.assertEqual(bone_len, expected_bone_len,
"Expected bone count to match")
for exp_bone in expected_arm.pose.bones:
bone_name = exp_bone.name
# Make sure that the bone exists
self.assertTrue(bone_name in input_arm.pose.bones,
"Armature is expected to contain bone %s, but it does not." % (bone_name))
bone = input_arm.pose.bones[bone_name]
# Loc Rot Scale
check_loc_rot_scale(self, bone, exp_bone)
# Parent settings
check_parent(self, bone, exp_bone)
# Bendy Bones
check_bendy_bones(self, bone, exp_bone)
# IK
check_ik(self, bone, exp_bone)
# Constraints
check_constraints(self, input_arm, expected_arm, bone, exp_bone)
def assertEqualVector(self, vec1, vec2, check_str, bone_name) -> None:
for idx, value in enumerate(vec1):
self.assertAlmostEqual(
value, vec2[idx], 3, "%s does not match with expected value on bone %s" % (check_str, bone_name))
def main():
global args
import argparse
if '--' in sys.argv:
argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:]
else:
argv = sys.argv
parser = argparse.ArgumentParser()
parser.add_argument('--testdir', required=True, type=pathlib.Path)
args, remaining = parser.parse_known_args(argv)
unittest.main(argv=remaining)
if __name__ == "__main__":
main()