From bb293e5677e0185aa0038c0041a5568c102da125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20T=C3=B6nne?= Date: Wed, 9 Jul 2025 09:53:00 +0200 Subject: [PATCH] Regression test for armature deformation on lattice This is a basic armature deformation test for #141535 using Lattices instead of Mesh as the target object type. Lattice deformation was briefly broken, which is caught by this test. The test adds the general-purpose `unit_test_compare` function to lattice object data. It only compares lattice point counts and positions for now, more data can be added later if necessary. The `MeshTest` class did not support lattice object types yet, so needed some changes. The Curves case was already supported, but only by full conversion to mesh data, without actually using the `unit_test_compare` function specific for curves geometry. This is unchanged, because applying constructive modifiers on curves does not work. If it were not for this limitation the test could do actual curves comparisons now. For lattice support the `MeshTest` class comparison function has been generalized to all supported object data types. It runs the appropriate `unit_test_compare` api function and validation where supported (only meshes at this point). Pull Request: https://projects.blender.org/blender/blender/pulls/141546 --- .../blenkernel/BKE_geometry_compare.hh | 11 ++++ .../blenkernel/intern/geometry_compare.cc | 34 ++++++++++ .../makesrna/intern/rna_lattice_api.cc | 31 +++++++++ tests/files/modeling/modifiers.blend | 4 +- tests/python/modifiers.py | 15 +++++ tests/python/modules/mesh_test.py | 66 ++++++++++++------- 6 files changed, 134 insertions(+), 27 deletions(-) diff --git a/source/blender/blenkernel/BKE_geometry_compare.hh b/source/blender/blenkernel/BKE_geometry_compare.hh index 5af51f6cd68..c487d924c86 100644 --- a/source/blender/blenkernel/BKE_geometry_compare.hh +++ b/source/blender/blenkernel/BKE_geometry_compare.hh @@ -7,6 +7,8 @@ #include "BKE_curves.hh" #include "BKE_mesh_types.hh" +#include "DNA_lattice_types.h" + /** \file * \ingroup bke */ @@ -46,4 +48,13 @@ std::optional compare_curves(const CurvesGeometry &curves1, const CurvesGeometry &curves2, float threshold); +/** + * \brief Checks if the two lattices are different, returning the type of mismatch if any. + * + * \returns The type of mismatch that was detected, if there is any. + */ +std::optional compare_lattices(const Lattice &lattice1, + const Lattice &lattice2, + float threshold); + } // namespace blender::bke::compare_geometry diff --git a/source/blender/blenkernel/intern/geometry_compare.cc b/source/blender/blenkernel/intern/geometry_compare.cc index 24088e57614..98fa6ab8ffa 100644 --- a/source/blender/blenkernel/intern/geometry_compare.cc +++ b/source/blender/blenkernel/intern/geometry_compare.cc @@ -18,6 +18,9 @@ #include "BKE_geometry_compare.hh" +#include "DNA_curve_types.h" +#include "DNA_lattice_types.h" + namespace blender::bke::compare_geometry { enum class GeoMismatch : int8_t { @@ -1022,4 +1025,35 @@ std::optional compare_curves(const CurvesGeometry &curves1, return std::nullopt; } +std::optional compare_lattices(const Lattice &lattice1, + const Lattice &lattice2, + float threshold) +{ + if (lattice1.pntsu != lattice2.pntsu) { + return GeoMismatch::NumPoints; + } + if (lattice1.pntsv != lattice2.pntsv) { + return GeoMismatch::NumPoints; + } + if (lattice1.pntsw != lattice2.pntsw) { + return GeoMismatch::NumPoints; + } + + const int num_points = lattice1.pntsu * lattice1.pntsv * lattice1.pntsw; + const Span bpoints1 = {lattice1.def, num_points}; + const Span bpoints2 = {lattice2.def, num_points}; + for (const int i : IndexRange(num_points)) { + const float3 co1 = bpoints1[i].vec; + const float3 co2 = bpoints2[i].vec; + for (const int component : IndexRange(3)) { + if (values_different(co1, co2, threshold, component)) { + return GeoMismatch::PointAttributes; + } + } + } + + /* No mismatches found. */ + return std::nullopt; +} + } // namespace blender::bke::compare_geometry diff --git a/source/blender/makesrna/intern/rna_lattice_api.cc b/source/blender/makesrna/intern/rna_lattice_api.cc index 6711d0a1ebc..d603b60ca38 100644 --- a/source/blender/makesrna/intern/rna_lattice_api.cc +++ b/source/blender/makesrna/intern/rna_lattice_api.cc @@ -13,6 +13,21 @@ #include "rna_internal.hh" /* own include */ #ifdef RNA_RUNTIME + +# include "BKE_geometry_compare.hh" + +static const char *rna_Lattice_unit_test_compare(Lattice *lt, Lattice *lt2, float threshold) +{ + using namespace blender::bke::compare_geometry; + const std::optional mismatch = compare_lattices(*lt, *lt2, threshold); + + if (!mismatch) { + return "Same"; + } + + return mismatch_to_string(mismatch.value()); +} + static void rna_Lattice_transform(Lattice *lt, const float mat[16], bool shape_keys) { BKE_lattice_transform(lt, (const float(*)[4])mat, shape_keys); @@ -39,6 +54,22 @@ void RNA_api_lattice(StructRNA *srna) RNA_def_boolean(func, "shape_keys", false, "", "Transform Shape Keys"); RNA_def_function(srna, "update_gpu_tag", "rna_Lattice_update_gpu_tag"); + + func = RNA_def_function(srna, "unit_test_compare", "rna_Lattice_unit_test_compare"); + RNA_def_pointer(func, "lattice", "Lattice", "", "Lattice to compare to"); + RNA_def_float_factor(func, + "threshold", + FLT_EPSILON * 60, + 0.0f, + FLT_MAX, + "Threshold", + "Comparison tolerance threshold", + 0.0f, + FLT_MAX); + /* return value */ + parm = RNA_def_string( + func, "result", "nothing", 64, "Return value", "String description of result of comparison"); + RNA_def_function_return(func, parm); } #endif diff --git a/tests/files/modeling/modifiers.blend b/tests/files/modeling/modifiers.blend index 25e75fe97f0..a8c3b5b7358 100644 --- a/tests/files/modeling/modifiers.blend +++ b/tests/files/modeling/modifiers.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5315deaf56d915d9608ac946e98caad79c1f9613124e24e634f6d67a8f9c553 -size 11563916 +oid sha256:f6efef2004ac186610b7fd615e9fb4c56afc144a774d10beeafa9d051ace280d +size 11622132 diff --git a/tests/python/modifiers.py b/tests/python/modifiers.py index bd7334c4eba..579cc55e753 100644 --- a/tests/python/modifiers.py +++ b/tests/python/modifiers.py @@ -377,6 +377,21 @@ def main(): 'use_vertex_groups': True, 'vertex_group': "Mask", 'use_multi_modifier': True})])]), + SpecMeshTest("ArmatureLatticeEnvelope", "testLatticeArmatureEnvelope", "expectedLatticeArmatureEnvelope", + [ModifierSpec('armature', 'ARMATURE', + {'object': bpy.data.objects['testArmatureLatticeEnvelope'], + 'use_vertex_groups': False, + 'use_bone_envelopes': True})]), + SpecMeshTest("ArmatureLatticeVGroup", "testLatticeArmatureVGroup", "expectedLatticeArmatureVGroup", + [ModifierSpec('armature', 'ARMATURE', + {'object': bpy.data.objects['testArmatureLatticeVGroup'], + 'use_vertex_groups': True, + 'use_bone_envelopes': False})]), + SpecMeshTest("ArmatureLatticeNoVGroup", "testLatticeArmatureNoVGroup", "expectedLatticeArmatureNoVGroup", + [ModifierSpec('armature', 'ARMATURE', + {'object': bpy.data.objects['testArmatureLatticeNoVGroup'], + 'use_vertex_groups': True, + 'use_bone_envelopes': False})]), ] boolean_basename = "CubeBooleanDiffBMeshObject" diff --git a/tests/python/modules/mesh_test.py b/tests/python/modules/mesh_test.py index 7a35e809e8c..cda2464b111 100644 --- a/tests/python/modules/mesh_test.py +++ b/tests/python/modules/mesh_test.py @@ -289,7 +289,7 @@ class MeshTest(ABC): print("Compare evaluated and expected object in Blender.\n") return False - result = self.compare_meshes( + result = self.compare_object_data( self.evaluated_object, self.expected_object, self.threshold, @@ -412,9 +412,9 @@ class MeshTest(ABC): self.expected_object = self.evaluated_object @staticmethod - def compare_meshes(evaluated_object, expected_object, threshold, allow_index_change): + def compare_object_data(evaluated_object, expected_object, threshold, allow_index_change): """ - Compares evaluated object mesh with expected object mesh. + Compares evaluated object data with expected object data. :arg evaluated_object: first object for comparison. :arg expected_object: second object for comparison. @@ -422,32 +422,48 @@ class MeshTest(ABC): :return: dict: Contains results of different comparisons. """ objects = bpy.data.objects - evaluated_test_mesh = objects[evaluated_object.name].data - expected_mesh = expected_object.data + evaluated_test_data = objects[evaluated_object.name].data + expected_data = expected_object.data result_codes = {} - if threshold: - result_mesh = expected_mesh.unit_test_compare( - mesh=evaluated_test_mesh, threshold=threshold) + if evaluated_object.type == 'CURVE': + unit_test_compare_args = {"curves": evaluated_test_data} + report_name = "Curves" + validate_func = None + elif evaluated_object.type == 'MESH': + unit_test_compare_args = {"mesh": evaluated_test_data} + report_name = "Mesh" + def validate_func(): return evaluated_test_data.validate(verbose=True) + elif evaluated_object.type == 'LATTICE': + unit_test_compare_args = {"lattice": evaluated_test_data} + report_name = "Lattice" + validate_func = None else: - result_mesh = expected_mesh.unit_test_compare( - mesh=evaluated_test_mesh) + raise Exception("This object type is not yet supported!") - if result_mesh == "Same": - result_codes['Mesh Comparison'] = (True, result_mesh) - elif allow_index_change and result_mesh == "The geometries are the same up to a change of indices": - result_codes['Mesh Comparison'] = (True, result_mesh) + if threshold: + result_data = expected_data.unit_test_compare( + threshold=threshold, **unit_test_compare_args) else: - result_codes['Mesh Comparison'] = (False, result_mesh) + result_data = expected_data.unit_test_compare( + **unit_test_compare_args) + + if result_data == "Same": + result_codes[f'{report_name} Comparison'] = (True, result_data) + elif allow_index_change and result_data == "The geometries are the same up to a change of indices": + result_codes[f'{report_name} Comparison'] = (True, result_data) + else: + result_codes[f'{report_name} Comparison'] = (False, result_data) # Validation check. - result_validation = evaluated_test_mesh.validate(verbose=True) - if result_validation: - result_validation = "Invalid Mesh" - result_codes['Mesh Validation'] = (False, result_validation) - else: - result_validation = "Valid" - result_codes['Mesh Validation'] = (True, result_validation) + if validate_func: + result_validation = validate_func() + if result_validation: + result_validation = f"Invalid {report_name}" + result_codes[f'{report_name} Validation'] = (False, result_validation) + else: + result_validation = "Valid" + result_codes[f'{report_name} Validation'] = (True, result_validation) return result_codes @@ -614,16 +630,16 @@ class SpecMeshTest(MeshTest): scene.frame_set(modifier_spec.frame_end) def _apply_modifier(self, test_object, modifier_name): - # Modifier automatically gets applied when converting from Curve to Mesh. if test_object.type == 'CURVE': + # Cannot apply constructive modifiers on curves, convert to mesh entirely. bpy.ops.object.convert(target='MESH') - elif test_object.type == 'MESH': + elif test_object.type in ['MESH', 'LATTICE']: bpy.ops.object.modifier_apply(modifier=modifier_name) else: raise Exception("This object type is not yet supported!") def _apply_all_modifiers(self, test_object): - if test_object.type in ['CURVE', 'MESH']: + if test_object.type in ['CURVE', 'MESH', 'LATTICE']: bpy.ops.object.convert(target='MESH') else: raise Exception("This object type is not yet supported!")