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