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