From 8f41d46d74f51fb5cd6bc55d4af622368a6b6387 Mon Sep 17 00:00:00 2001 From: Cartesian Caramel Date: Tue, 7 Oct 2025 15:14:22 +0200 Subject: [PATCH] Constraint: Geometry Attribute Add a new constraint called "Geometry Attribute", which directly samples vector, quaternion, or 4x4 matrix attributes from geometry and applies these to an object's or bone's transform. This can be used to transfer data generated by geometry nodes to the object or bone level. By default the constraint will sample a vector on the vertex domain. The default attribute is `position` for simplicity, but the attribute value does not have to have anything to do with neither the transform of the geometry object nor the geometry itself. Pull Request: https://projects.blender.org/blender/blender/pulls/136477 --- .../icons_svg/con_geometryattribute.svg | 75 +++++ .../startup/bl_ui/properties_constraint.py | 42 +++ .../blender/blenkernel/intern/constraint.cc | 298 ++++++++++++++++++ .../intern/builder/deg_builder_relations.cc | 10 + .../blender/editors/datafiles/CMakeLists.txt | 1 + source/blender/editors/include/UI_icons.hh | 1 + .../editors/space_outliner/outliner_draw.cc | 3 + .../blender/makesdna/DNA_constraint_types.h | 56 ++++ .../blender/makesrna/intern/rna_constraint.cc | 149 +++++++++ tests/files/constraints/constraints.blend | 4 +- tests/python/bl_constraints.py | 39 +++ 11 files changed, 676 insertions(+), 2 deletions(-) create mode 100644 release/datafiles/icons_svg/con_geometryattribute.svg diff --git a/release/datafiles/icons_svg/con_geometryattribute.svg b/release/datafiles/icons_svg/con_geometryattribute.svg new file mode 100644 index 00000000000..49ce0f7d4e8 --- /dev/null +++ b/release/datafiles/icons_svg/con_geometryattribute.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + diff --git a/scripts/startup/bl_ui/properties_constraint.py b/scripts/startup/bl_ui/properties_constraint.py index f4680d49d2d..08558886fd0 100644 --- a/scripts/startup/bl_ui/properties_constraint.py +++ b/scripts/startup/bl_ui/properties_constraint.py @@ -977,8 +977,35 @@ class ConstraintButtonsPanel: self.draw_influence(layout, con) + def draw_geometry_attribute(self, context): + layout = self.layout + con = self.get_constraint(context) + layout.use_property_split = True + layout.use_property_decorate = True + + self.target_template(layout, con, False) + layout.prop(con, "apply_target_transform", text="Offset with Target Transform") + + layout.prop(con, "attribute_name", text="Attribute Name") + layout.prop(con, "data_type", text="Data Type") + layout.prop(con, "domain", text="Domain") + layout.prop(con, "sample_index", text="Sample Index") + + layout.separator() + layout.prop(con, "mix_mode", text="Mix Mode", text_ctxt=i18n_contexts.constraint) + + if con.data_type == 'FLOAT4X4': + row = layout.row(heading="Enabled") + row.prop(con, "mix_loc", text="Location", toggle=True) + row.prop(con, "mix_rot", text="Rotation", toggle=True) + row.prop(con, "mix_scl", text="Scale", toggle=True) + row.label(icon='BLANK1') + + self.draw_influence(layout, con) # Parent class for constraint sub-panels. + + class ConstraintButtonsSubPanel: bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' @@ -1664,6 +1691,18 @@ class BONE_PT_bKinematicConstraint(BoneConstraintPanel, ConstraintButtonsPanel, def draw(self, context): self.draw_kinematic(context) + # Geometry Attribute Constraint. + + +class OBJECT_PT_bGeometryAttributeConstraint(ObjectConstraintPanel, ConstraintButtonsPanel, Panel): + def draw(self, context): + self.draw_geometry_attribute(context) + + +class BONE_PT_bGeometryAttributeConstraint(BoneConstraintPanel, ConstraintButtonsPanel, Panel): + def draw(self, context): + self.draw_geometry_attribute(context) + classes = ( # Object Panels @@ -1704,6 +1743,8 @@ classes = ( OBJECT_PT_bTransformCacheConstraint_layers, OBJECT_PT_bArmatureConstraint, OBJECT_PT_bArmatureConstraint_bones, + OBJECT_PT_bGeometryAttributeConstraint, + # Bone panels BONE_PT_bChildOfConstraint, BONE_PT_bTrackToConstraint, @@ -1743,6 +1784,7 @@ classes = ( BONE_PT_bTransformCacheConstraint_layers, BONE_PT_bArmatureConstraint, BONE_PT_bArmatureConstraint_bones, + BONE_PT_bGeometryAttributeConstraint, ) if __name__ == "__main__": # only for live edit. diff --git a/source/blender/blenkernel/intern/constraint.cc b/source/blender/blenkernel/intern/constraint.cc index 9a9c5268c68..80dd51fa593 100644 --- a/source/blender/blenkernel/intern/constraint.cc +++ b/source/blender/blenkernel/intern/constraint.cc @@ -59,6 +59,7 @@ #include "BKE_displist.h" #include "BKE_editmesh.hh" #include "BKE_fcurve_driver.h" +#include "BKE_geometry_set_instances.hh" #include "BKE_global.hh" #include "BKE_idprop.hh" #include "BKE_lib_id.hh" @@ -5440,6 +5441,289 @@ static bConstraintTypeInfo CTI_TRANSFORM_CACHE = { /*evaluate_constraint*/ transformcache_evaluate, }; +/* ---------- Geometry Attribute Constraint ----------- */ + +static blender::bke::AttrDomain domain_value_to_attribute(const Attribute_Domain domain) +{ + switch (domain) { + case CON_ATTRIBUTE_DOMAIN_POINT: + return blender::bke::AttrDomain::Point; + case CON_ATTRIBUTE_DOMAIN_EDGE: + return blender::bke::AttrDomain::Edge; + case CON_ATTRIBUTE_DOMAIN_FACE: + return blender::bke::AttrDomain::Face; + case CON_ATTRIBUTE_DOMAIN_FACE_CORNER: + return blender::bke::AttrDomain::Corner; + case CON_ATTRIBUTE_DOMAIN_CURVE: + return blender::bke::AttrDomain::Curve; + case CON_ATTRIBUTE_DOMAIN_INSTANCE: + return blender::bke::AttrDomain::Instance; + } + BLI_assert_unreachable(); + return blender::bke::AttrDomain::Point; +} + +static blender::bke::AttrType type_value_to_attribute(const Attribute_Data_Type data_type) +{ + switch (data_type) { + case CON_ATTRIBUTE_VECTOR: + return blender::bke::AttrType::Float3; + case CON_ATTRIBUTE_QUATERNION: + return blender::bke::AttrType::Quaternion; + case CON_ATTRIBUTE_4X4MATRIX: + return blender::bke::AttrType::Float4x4; + } + BLI_assert_unreachable(); + return blender::bke::AttrType::Float3; +} + +static void value_attribute_to_matrix(float r_matrix[4][4], + const blender::GPointer value, + const Attribute_Data_Type data_type) +{ + switch (data_type) { + case CON_ATTRIBUTE_VECTOR: + copy_v3_v3(r_matrix[3], *value.get()); + return; + case CON_ATTRIBUTE_QUATERNION: + quat_to_mat4(r_matrix, *value.get()); + return; + case CON_ATTRIBUTE_4X4MATRIX: + copy_m4_m4(r_matrix, value.get()->ptr()); + return; + } + BLI_assert_unreachable(); +} + +static bool component_is_available(const blender::bke::GeometrySet &geometry, + const blender::bke::GeometryComponent::Type type, + const blender::bke::AttrDomain domain) +{ + if (const blender::bke::GeometryComponent *component = geometry.get_component(type)) { + return component->attribute_domain_size(domain) != 0; + } + return false; +} + +static const blender::bke::GeometryComponent *find_source_component( + const blender::bke::GeometrySet &geometry, const blender::bke::AttrDomain domain) +{ + /* Choose the other component based on a consistent order, rather than some more complicated + * heuristic. This is the same order visible in the spreadsheet and used in the ray-cast node. */ + static const blender::Array supported_types = { + blender::bke::GeometryComponent::Type::Mesh, + blender::bke::GeometryComponent::Type::PointCloud, + blender::bke::GeometryComponent::Type::Curve, + blender::bke::GeometryComponent::Type::Instance, + blender::bke::GeometryComponent::Type::GreasePencil}; + for (const blender::bke::GeometryComponent::Type src_type : supported_types) { + if (component_is_available(geometry, src_type, domain)) { + return geometry.get_component(src_type); + } + } + + return nullptr; +} + +static void geometry_attribute_free_data(bConstraint *con) +{ + bGeometryAttributeConstraint *data = static_cast(con->data); + MEM_SAFE_FREE(data->attribute_name); +} + +static void geometry_attribute_id_looper(bConstraint *con, ConstraintIDFunc func, void *userdata) +{ + bGeometryAttributeConstraint *data = static_cast(con->data); + func(con, (ID **)&data->target, false, userdata); +} + +static void geometry_attribute_copy_data(bConstraint *con, bConstraint *srccon) +{ + const auto *src = static_cast(srccon->data); + auto *dst = static_cast(con->data); + dst->attribute_name = BLI_strdup_null(src->attribute_name); +} + +static void geometry_attribute_new_data(void *cdata) +{ + bGeometryAttributeConstraint *data = static_cast(cdata); + data->attribute_name = BLI_strdup("position"); + data->flags = MIX_LOC | MIX_ROT | MIX_SCALE; +} + +static int geometry_attribute_get_tars(bConstraint *con, ListBase *list) +{ + if (!con || !list) { + return 0; + } + bGeometryAttributeConstraint *data = static_cast(con->data); + bConstraintTarget *ct; + + SINGLETARGETNS_GET_TARS(con, data->target, ct, list); + + return 1; +} + +static void geometry_attribute_flush_tars(bConstraint *con, ListBase *list, const bool no_copy) +{ + if (!con || !list) { + return; + } + bGeometryAttributeConstraint *data = static_cast(con->data); + bConstraintTarget *ct = static_cast(list->first); + + SINGLETARGETNS_FLUSH_TARS(con, data->target, ct, list, no_copy); +} + +static bool geometry_attribute_get_tarmat(Depsgraph * /*depsgraph*/, + bConstraint *con, + bConstraintOb * /*cob*/, + bConstraintTarget *ct, + float /*ctime*/) +{ + using namespace blender; + const bGeometryAttributeConstraint *acon = static_cast( + con->data); + + if (!VALID_CONS_TARGET(ct)) { + return false; + } + + unit_m4(ct->matrix); + + const bke::AttrDomain domain = domain_value_to_attribute( + static_cast(acon->domain)); + const bke::AttrType sample_data_type = type_value_to_attribute( + static_cast(acon->data_type)); + const bke::GeometrySet &target_eval = bke::object_get_evaluated_geometry_set(*ct->tar); + + const bke::GeometryComponent *component = find_source_component(target_eval, domain); + if (component == nullptr) { + return false; + } + + const std::optional optional_attributes = component->attributes(); + if (!optional_attributes.has_value()) { + return false; + } + + const bke::AttributeAccessor &attributes = *optional_attributes; + const GVArray attribute = *attributes.lookup(acon->attribute_name, domain, sample_data_type); + + if (attribute.is_empty()) { + return false; + } + + const int index = std::clamp(acon->sample_index, 0, attribute.size() - 1); + + const CPPType &type = attribute.type(); + BUFFER_FOR_CPP_TYPE_VALUE(type, sampled_value); + attribute.get_to_uninitialized(index, sampled_value); + + value_attribute_to_matrix(ct->matrix, + GPointer(type, sampled_value), + static_cast(acon->data_type)); + type.destruct(sampled_value); + + return true; +} + +static void geometry_attribute_evaluate(bConstraint *con, bConstraintOb *cob, ListBase *targets) +{ + bConstraintTarget *ct = static_cast(targets->first); + const bGeometryAttributeConstraint *data = static_cast( + con->data); + + /* Only evaluate if there is a target. */ + if (!VALID_CONS_TARGET(ct)) { + return; + } + + float target_mat[4][4]; + if (data->mix_mode == CON_ATTRIBUTE_MIX_REPLACE) { + copy_m4_m4(target_mat, cob->matrix); + } + else { + unit_m4(target_mat); + } + + float prev_location[3]; + float prev_rotation[3][3]; + float prev_size[3]; + mat4_to_loc_rot_size(prev_location, prev_rotation, prev_size, target_mat); + + float next_location[3]; + float next_rotation[3][3]; + float next_size[3]; + mat4_to_loc_rot_size(next_location, next_rotation, next_size, ct->matrix); + + switch (data->data_type) { + case CON_ATTRIBUTE_VECTOR: + loc_rot_size_to_mat4(target_mat, next_location, prev_rotation, prev_size); + break; + case CON_ATTRIBUTE_QUATERNION: + loc_rot_size_to_mat4(target_mat, prev_location, next_rotation, prev_size); + break; + case CON_ATTRIBUTE_4X4MATRIX: + if ((data->flags & MIX_LOC) && (data->flags & MIX_ROT) && (data->flags & MIX_SCALE)) { + copy_m4_m4(target_mat, ct->matrix); + } + else { + if (data->flags & MIX_LOC) { + copy_v3_v3(prev_location, next_location); + } + if (data->flags & MIX_ROT) { + copy_m3_m3(prev_rotation, next_rotation); + } + if (data->flags & MIX_SCALE) { + copy_v3_v3(prev_size, next_size); + } + loc_rot_size_to_mat4(target_mat, prev_location, prev_rotation, prev_size); + } + break; + } + + /* Finally, combine the matrices. */ + switch (data->mix_mode) { + case CON_ATTRIBUTE_MIX_REPLACE: + copy_m4_m4(cob->matrix, target_mat); + break; + /* Simple matrix multiplication. */ + case CON_ATTRIBUTE_MIX_BEFORE_FULL: + mul_m4_m4m4(cob->matrix, target_mat, cob->matrix); + break; + case CON_ATTRIBUTE_MIX_AFTER_FULL: + mul_m4_m4m4(cob->matrix, cob->matrix, target_mat); + break; + /* Fully separate handling of channels. */ + case CON_ATTRIBUTE_MIX_BEFORE_SPLIT: + mul_m4_m4m4_split_channels(cob->matrix, target_mat, cob->matrix); + break; + case CON_ATTRIBUTE_MIX_AFTER_SPLIT: + mul_m4_m4m4_split_channels(cob->matrix, cob->matrix, target_mat); + break; + } + + if (data->apply_target_transform) { + mul_m4_m4m4(cob->matrix, ct->tar->object_to_world().ptr(), cob->matrix); + } +} + +static bConstraintTypeInfo CTI_ATTRIBUTE = { + /*type*/ CONSTRAINT_TYPE_GEOMETRY_ATTRIBUTE, + /*size*/ sizeof(bGeometryAttributeConstraint), + /*name*/ N_("Geometry Attribute"), + /*struct_name*/ "bGeometryAttributeConstraint", + /*free_data*/ geometry_attribute_free_data, + /*id_looper*/ geometry_attribute_id_looper, + /*copy_data*/ geometry_attribute_copy_data, + /*new_data*/ geometry_attribute_new_data, + /*get_constraint_targets*/ geometry_attribute_get_tars, + /*flush_constraint_targets*/ geometry_attribute_flush_tars, + /*get_target_matrix*/ geometry_attribute_get_tarmat, + /*evaluate_constraint*/ geometry_attribute_evaluate, +}; + /* ************************* Constraints Type-Info *************************** */ /* All of the constraints API functions use #bConstraintTypeInfo structs to carry out * and operations that involve constraint specific code. @@ -5483,6 +5767,7 @@ static void constraints_init_typeinfo() constraintsTypeInfo[28] = &CTI_OBJECTSOLVER; /* Object Solver Constraint */ constraintsTypeInfo[29] = &CTI_TRANSFORM_CACHE; /* Transform Cache Constraint */ constraintsTypeInfo[30] = &CTI_ARMATURE; /* Armature Constraint */ + constraintsTypeInfo[31] = &CTI_ATTRIBUTE; /* Attribute Transform Constraint */ } const bConstraintTypeInfo *BKE_constraint_typeinfo_from_type(int type) @@ -6469,6 +6754,12 @@ void BKE_constraint_blend_write(BlendWriter *writer, ListBase *conlist) break; } + case CONSTRAINT_TYPE_GEOMETRY_ATTRIBUTE: { + bGeometryAttributeConstraint *data = static_cast( + con->data); + BLO_write_string(writer, data->attribute_name); + break; + } } } @@ -6530,6 +6821,13 @@ void BKE_constraint_blend_read_data(BlendDataReader *reader, ID *id_owner, ListB bTransformCacheConstraint *data = static_cast(con->data); data->reader = nullptr; data->reader_object_path[0] = '\0'; + break; + } + case CONSTRAINT_TYPE_GEOMETRY_ATTRIBUTE: { + bGeometryAttributeConstraint *data = static_cast( + con->data); + BLO_read_string(reader, &data->attribute_name); + break; } } } diff --git a/source/blender/depsgraph/intern/builder/deg_builder_relations.cc b/source/blender/depsgraph/intern/builder/deg_builder_relations.cc index bcc2f18f207..79e1cc96069 100644 --- a/source/blender/depsgraph/intern/builder/deg_builder_relations.cc +++ b/source/blender/depsgraph/intern/builder/deg_builder_relations.cc @@ -1530,6 +1530,16 @@ void DepsgraphRelationBuilder::build_constraints(ID *id, ComponentKey target_transform_key(&ct->tar->id, NodeType::TRANSFORM); add_relation(target_transform_key, constraint_op_key, cti->name); } + else if (con->type == CONSTRAINT_TYPE_GEOMETRY_ATTRIBUTE) { + /* Constraints which require the target object geometry attributes. */ + ComponentKey target_key(&ct->tar->id, NodeType::GEOMETRY); + add_relation(target_key, constraint_op_key, cti->name); + + /* NOTE: The target object's transform is used when the 'Apply target transform' flag + * is set.*/ + ComponentKey target_transform_key(&ct->tar->id, NodeType::TRANSFORM); + add_relation(target_transform_key, constraint_op_key, cti->name); + } else { /* Standard object relation. */ /* TODO: loc vs rot vs scale? */ diff --git a/source/blender/editors/datafiles/CMakeLists.txt b/source/blender/editors/datafiles/CMakeLists.txt index 2093ffad8e6..bc71fb9e55a 100644 --- a/source/blender/editors/datafiles/CMakeLists.txt +++ b/source/blender/editors/datafiles/CMakeLists.txt @@ -272,6 +272,7 @@ if(WITH_BLENDER) con_floor con_followpath con_followtrack + con_geometryattribute con_kinematic con_locktrack con_loclike diff --git a/source/blender/editors/include/UI_icons.hh b/source/blender/editors/include/UI_icons.hh index a4d617f3715..c2d8d0f6ace 100644 --- a/source/blender/editors/include/UI_icons.hh +++ b/source/blender/editors/include/UI_icons.hh @@ -493,6 +493,7 @@ DEF_ICON(WORDWRAP_ON) /* CONSTRAINTS */ DEF_ICON_MODIFIER(CON_ACTION) DEF_ICON_MODIFIER(CON_ARMATURE) +DEF_ICON_MODIFIER(CON_GEOMETRYATTRIBUTE) DEF_ICON_MODIFIER(CON_CAMERASOLVER) DEF_ICON_MODIFIER(CON_CHILDOF) DEF_ICON_MODIFIER(CON_CLAMPTO) diff --git a/source/blender/editors/space_outliner/outliner_draw.cc b/source/blender/editors/space_outliner/outliner_draw.cc index 63c19da64c1..9e2c170e8fc 100644 --- a/source/blender/editors/space_outliner/outliner_draw.cc +++ b/source/blender/editors/space_outliner/outliner_draw.cc @@ -2751,6 +2751,9 @@ TreeElementIcon tree_element_get_icon(TreeStoreElem *tselem, TreeElement *te) case CONSTRAINT_TYPE_SHRINKWRAP: data.icon = ICON_CON_SHRINKWRAP; break; + case CONSTRAINT_TYPE_GEOMETRY_ATTRIBUTE: + data.icon = ICON_CON_GEOMETRYATTRIBUTE; + break; default: data.icon = ICON_DOT; diff --git a/source/blender/makesdna/DNA_constraint_types.h b/source/blender/makesdna/DNA_constraint_types.h index a6431e56f06..658aebde4a7 100644 --- a/source/blender/makesdna/DNA_constraint_types.h +++ b/source/blender/makesdna/DNA_constraint_types.h @@ -562,6 +562,61 @@ typedef struct bTransformCacheConstraint { char reader_object_path[/*FILE_MAX*/ 1024]; } bTransformCacheConstraint; +/* bGeometryAttributeConstraint->flag */ +typedef enum eGeometryAttributeConstraint_Flags { + APPLY_TARGET_TRANSFORM = (1 << 0), + MIX_LOC = (1 << 1), + MIX_ROT = (1 << 2), + MIX_SCALE = (1 << 3), +} eGeometryAttributeConstraint_Flags; + +/* Geometry Attribute Constraint */ +typedef struct bGeometryAttributeConstraint { + struct Object *target; + char *attribute_name; + int32_t sample_index; + uint8_t apply_target_transform; + uint8_t mix_mode; + /* #Attribute_Domain */ + uint8_t domain; + /* #Attribute_Data_Type */ + uint8_t data_type; + /* #eGeometryAttributeConstraint_Flags */ + uint8_t flags; + char _pad0[7]; +} bGeometryAttributeConstraint; + +/* Atrtibute Domain */ +typedef enum Attribute_Domain { + CON_ATTRIBUTE_DOMAIN_POINT = 0, + CON_ATTRIBUTE_DOMAIN_EDGE = 1, + CON_ATTRIBUTE_DOMAIN_FACE = 2, + CON_ATTRIBUTE_DOMAIN_FACE_CORNER = 3, + CON_ATTRIBUTE_DOMAIN_CURVE = 4, + CON_ATTRIBUTE_DOMAIN_INSTANCE = 5, +} Attribute_Domain; + +/* Atrtibute Data Type*/ +typedef enum Attribute_Data_Type { + CON_ATTRIBUTE_VECTOR = 0, + CON_ATTRIBUTE_QUATERNION = 1, + CON_ATTRIBUTE_4X4MATRIX = 2, +} Attribute_Data_Type; + +/** Attribute Component Mix Mode */ +typedef enum Attribute_MixMode { + /* Replace rotation channel values. */ + CON_ATTRIBUTE_MIX_REPLACE = 0, + /* Multiply the copied transformation on the left, handling loc/rot/scale separately. */ + CON_ATTRIBUTE_MIX_BEFORE_SPLIT = 1, + /* Multiply the copied transformation on the right, handling loc/rot/scale separately. */ + CON_ATTRIBUTE_MIX_AFTER_SPLIT = 2, + /* Multiply the copied transformation on the left, using simple matrix multiplication. */ + CON_ATTRIBUTE_MIX_BEFORE_FULL = 3, + /* Multiply the copied transformation on the right, using simple matrix multiplication. */ + CON_ATTRIBUTE_MIX_AFTER_FULL = 4, +} Attribute_MixMode; + /* ------------------------------------------ */ /* bConstraint->type @@ -607,6 +662,7 @@ typedef enum eBConstraint_Types { CONSTRAINT_TYPE_OBJECTSOLVER = 28, CONSTRAINT_TYPE_TRANSFORM_CACHE = 29, CONSTRAINT_TYPE_ARMATURE = 30, + CONSTRAINT_TYPE_GEOMETRY_ATTRIBUTE = 31, /* This should be the last entry in this list. */ NUM_CONSTRAINT_TYPES, diff --git a/source/blender/makesrna/intern/rna_constraint.cc b/source/blender/makesrna/intern/rna_constraint.cc index 5d2a379836d..d72d58ceb5c 100644 --- a/source/blender/makesrna/intern/rna_constraint.cc +++ b/source/blender/makesrna/intern/rna_constraint.cc @@ -165,6 +165,11 @@ const EnumPropertyItem rna_enum_constraint_type_items[] = { ICON_CON_SHRINKWRAP, "Shrinkwrap", "Restrict movements to surface of target mesh"}, + {CONSTRAINT_TYPE_GEOMETRY_ATTRIBUTE, + "GEOMETRY_ATTRIBUTE", + ICON_CON_GEOMETRYATTRIBUTE, + "Geometry Attribute", + "Retrieve transform from target geometry attribute data"}, {0, nullptr, 0, nullptr, nullptr}, }; @@ -381,6 +386,8 @@ static StructRNA *rna_ConstraintType_refine(PointerRNA *ptr) return &RNA_ObjectSolverConstraint; case CONSTRAINT_TYPE_TRANSFORM_CACHE: return &RNA_TransformCacheConstraint; + case CONSTRAINT_TYPE_GEOMETRY_ATTRIBUTE: + return &RNA_GeometryAttributeConstraint; default: return &RNA_UnknownType; } @@ -3611,6 +3618,147 @@ static void rna_def_constraint_transform_cache(BlenderRNA *brna) RNA_define_lib_overridable(false); } +static void rna_def_constraint_geometry_attribute(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + static const EnumPropertyItem domain_items[] = { + {CON_ATTRIBUTE_DOMAIN_POINT, "POINT", 0, "Point"}, + {CON_ATTRIBUTE_DOMAIN_EDGE, "EDGE", 0, "Edge"}, + {CON_ATTRIBUTE_DOMAIN_FACE, "FACE", 0, "Face"}, + {CON_ATTRIBUTE_DOMAIN_FACE_CORNER, "FACE_CORNER", 0, "Face Corner"}, + {CON_ATTRIBUTE_DOMAIN_CURVE, "CURVE", 0, "Spline"}, + {CON_ATTRIBUTE_DOMAIN_INSTANCE, "INSTANCE", 0, "Instance"}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + static const EnumPropertyItem type_items[] = { + {CON_ATTRIBUTE_VECTOR, "VECTOR", 0, "Vector", "Vector data type, affects position"}, + {CON_ATTRIBUTE_QUATERNION, + "QUATERNION", + 0, + "Quaternion", + "Quaternion data type, affects rotation"}, + {CON_ATTRIBUTE_4X4MATRIX, + "FLOAT4X4", + 0, + "4x4 Matrix", + "4x4 Matrix data type, affects transform"}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + static const EnumPropertyItem attribute_mix_mode_items[] = { + {CON_ATTRIBUTE_MIX_REPLACE, + "REPLACE", + 0, + "Replace", + "Replace the original transformation with the trasnform from the attribute"}, + RNA_ENUM_ITEM_SEPR, + {CON_ATTRIBUTE_MIX_BEFORE_FULL, + "BEFORE_FULL", + 0, + "Before Original (Full)", + "Apply copied transformation before original, using simple matrix multiplication as if " + "the constraint target is a parent in Full Inherit Scale mode. " + "Will create shear when combining rotation and non-uniform scale."}, + {CON_ATTRIBUTE_MIX_BEFORE_SPLIT, + "BEFORE_SPLIT", + 0, + "Before Original (Split Channels)", + "Apply copied transformation before original, handling location, rotation and scale " + "separately, similar to a sequence of three Copy constraints"}, + RNA_ENUM_ITEM_SEPR, + {CON_ATTRIBUTE_MIX_AFTER_FULL, + "AFTER_FULL", + 0, + "After Original (Full)", + "Apply copied transformation after original, using simple matrix multiplication as if " + "the constraint target is a child in Full Inherit Scale mode. " + "Will create shear when combining rotation and non-uniform scale."}, + {CON_ATTRIBUTE_MIX_AFTER_SPLIT, + "AFTER_SPLIT", + 0, + "After Original (Split Channels)", + "Apply copied transformation after original, handling location, rotation and scale " + "separately, similar to a sequence of three Copy constraints"}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + srna = RNA_def_struct(brna, "GeometryAttributeConstraint", "Constraint"); + RNA_def_struct_ui_text(srna, + "Geometry Attribute Constraint", + "Create a constraint-based relationship with an attribute from geometry"); + RNA_def_struct_sdna_from(srna, "bGeometryAttributeConstraint", "data"); + RNA_def_struct_ui_icon(srna, ICON_CON_GEOMETRYATTRIBUTE); + + RNA_define_lib_overridable(true); + + prop = RNA_def_property(srna, "target", PROP_POINTER, PROP_NONE); + RNA_def_property_pointer_sdna(prop, nullptr, "target"); + RNA_def_property_pointer_funcs(prop, nullptr, nullptr, nullptr, nullptr); + RNA_def_property_ui_text(prop, "Target", "Target geometry object"); + RNA_def_property_flag(prop, PROP_EDITABLE); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_dependency_update"); + + prop = RNA_def_property(srna, "attribute_name", PROP_STRING, PROP_NONE); + RNA_def_property_string_sdna(prop, nullptr, "attribute_name"); + RNA_def_property_ui_text( + prop, "Attribute Name", "Name of the attribute to retrieve the transform from"); + RNA_def_property_flag(prop, PROP_EDITABLE); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update"); + + prop = RNA_def_property(srna, "domain", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "domain"); + RNA_def_property_enum_items(prop, domain_items); + RNA_def_property_ui_text(prop, "Domain Type", "Attribute domain"); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update"); + + prop = RNA_def_property(srna, "apply_target_transform", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "apply_target_transform", 1); + RNA_def_property_ui_text( + prop, + "Target Transform", + "Apply the target object's world transform on top of the attribute's transform"); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update"); + + prop = RNA_def_property(srna, "data_type", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "data_type"); + RNA_def_property_enum_items(prop, type_items); + RNA_def_property_ui_text(prop, "Data Type", "Select data type of attribute"); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update"); + + prop = RNA_def_property(srna, "sample_index", PROP_INT, PROP_NONE); + RNA_def_property_int_sdna(prop, nullptr, "sample_index"); + RNA_def_property_range(prop, 0, INT_MAX); + RNA_def_property_ui_text(prop, "Sample Index", "Sample Index"); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update"); + + prop = RNA_def_property(srna, "mix_loc", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flags", MIX_LOC); + RNA_def_property_ui_text(prop, "Mix Location", "Mix Location"); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update"); + + prop = RNA_def_property(srna, "mix_rot", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flags", MIX_ROT); + RNA_def_property_ui_text(prop, "Mix Rotation", "Mix Rotation"); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update"); + + prop = RNA_def_property(srna, "mix_scl", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flags", MIX_SCALE); + RNA_def_property_ui_text(prop, "Mix Scale", "Mix Scale"); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update"); + + prop = RNA_def_property(srna, "mix_mode", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "mix_mode"); + RNA_def_property_enum_items(prop, attribute_mix_mode_items); + RNA_def_property_ui_text( + prop, "Mix Mode", "Specify how the copied and existing transformations are combined"); + RNA_def_property_update(prop, NC_OBJECT | ND_CONSTRAINT, "rna_Constraint_update"); + + RNA_define_lib_overridable(false); +} + /* Define the base struct for constraints. */ void RNA_def_constraint(BlenderRNA *brna) @@ -3768,6 +3916,7 @@ void RNA_def_constraint(BlenderRNA *brna) rna_def_constraint_camera_solver(brna); rna_def_constraint_object_solver(brna); rna_def_constraint_transform_cache(brna); + rna_def_constraint_geometry_attribute(brna); } #endif diff --git a/tests/files/constraints/constraints.blend b/tests/files/constraints/constraints.blend index 46d8f640d12..48bc11ebdd7 100644 --- a/tests/files/constraints/constraints.blend +++ b/tests/files/constraints/constraints.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e121810a4e6cd0b32b7670cd86088ebd84c1bc05d2166cb265cc4e2e22694e06 -size 145628 +oid sha256:d3abe96da32a67c57691a491b80cd40ba3e1f5686173d349796bc5380750c6da +size 933385 diff --git a/tests/python/bl_constraints.py b/tests/python/bl_constraints.py index 4e76d46f422..1e7374614a3 100644 --- a/tests/python/bl_constraints.py +++ b/tests/python/bl_constraints.py @@ -502,6 +502,45 @@ class ActionConstraintTest(AbstractConstraintTests): ))) +class GeometryAttributeConstraintTest(AbstractConstraintTests): + layer_collection = "Geometry Attribute" + + def test_mix_modes(self): + owner = bpy.context.scene.objects["Geometry Attribute.owner"] + con = owner.constraints["Geometry Attribute"] + con.apply_target_transform = False + + # This should produce the matrix as stored in the geometry attribute. + con.mix_mode = 'REPLACE' + self.matrix_test(owner.name, Matrix( + ((0.32139378786087036, -0.41721203923225403, 0.8500824570655823, -1.0), + (0.5566704273223877, 0.809456467628479, 0.18681080639362335, -1.0), + (-0.7660444378852844, 0.41317591071128845, 0.49240389466285706, 0.0), + (0.0, 0.0, 0.0, 1.0))), + ) + + con.mix_mode = 'BEFORE_SPLIT' + self.matrix_test(owner.name, Matrix( + ((0.32139378786087036, -0.41721203923225403, 0.8500824570655823, -0.8999999761581421), + (0.5566704273223877, 0.809456467628479, 0.18681080639362335, -0.800000011920929), + (-0.7660444378852844, 0.41317591071128845, 0.49240389466285706, 0.30000001192092896), + (0.0, 0.0, 0.0, 1.0))), + ) + + def test_apply_target_transform(self): + owner = bpy.context.scene.objects["Geometry Attribute.owner"] + con = owner.constraints["Geometry Attribute"] + con.apply_target_transform = True + + con.mix_mode = 'REPLACE' + self.matrix_test(owner.name, Matrix( + ((0.5681133270263672, -0.2360532432794571, 0.788369357585907, -0.923704206943512), + (0.3527687191963196, 0.9353533983230591, 0.02585163712501526, -0.5034461617469788), + (-0.7435062527656555, 0.26342537999153137, 0.6146588921546936, 0.012183472514152527), + (0.0, 0.0, 0.0, 1.0))), + ) + + def main(): global args import argparse