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
This commit is contained in:
committed by
Sybren A. Stüvel
parent
e2872c0bfe
commit
8f41d46d74
75
release/datafiles/icons_svg/con_geometryattribute.svg
Normal file
75
release/datafiles/icons_svg/con_geometryattribute.svg
Normal file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="1500"
|
||||
viewBox="0 0 1500 1500"
|
||||
width="1500"
|
||||
version="1.1"
|
||||
id="svg3"
|
||||
sodipodi:docname="con_attribute5.svg"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs3" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#303030"
|
||||
showgrid="true"
|
||||
id="namedview1"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.11313708"
|
||||
inkscape:cx="-1520.2796"
|
||||
inkscape:cy="1842.8971"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g2">
|
||||
<inkscape:grid
|
||||
id="grid5"
|
||||
units="px"
|
||||
spacingx="100"
|
||||
spacingy="100"
|
||||
color="#4772b3"
|
||||
opacity="0.2"
|
||||
visible="true"
|
||||
originx="0"
|
||||
originy="0" />
|
||||
</sodipodi:namedview>
|
||||
<g
|
||||
fill="#fff"
|
||||
id="g3">
|
||||
<g
|
||||
enable-background="new"
|
||||
transform="matrix(100 0 0 100 -53000.503 35200.51)"
|
||||
id="g2">
|
||||
<path
|
||||
id="path3"
|
||||
style="stroke-width:0.12;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 539.30503,-349.0051 a 1.8,1.8 0 0 1 -1.8,1.8 1.8,1.8 0 0 1 -1.8,-1.8 1.8,1.8 0 0 1 1.8,-1.8 1.8,1.8 0 0 1 1.8,1.8 z" />
|
||||
<path
|
||||
id="circle4"
|
||||
style="stroke-width:0.12;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 544.30503,-340.0051 a 1.799994,1.8 0 0 1 -1.79999,1.8 1.799994,1.8 0 0 1 -1.8,-1.8 1.799994,1.8 0 0 1 1.8,-1.8 1.799994,1.8 0 0 1 1.79999,1.8 z" />
|
||||
<path
|
||||
id="circle5"
|
||||
style="stroke-width:0.12;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 534.30503,-340.0051 a 1.7999865,1.8 0 0 1 -1.79999,1.8 1.7999865,1.8 0 0 1 -1.79998,-1.8 1.7999865,1.8 0 0 1 1.79998,-1.8 1.7999865,1.8 0 0 1 1.79999,1.8 z" />
|
||||
<path
|
||||
id="circle7"
|
||||
style="stroke-width:0.12;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 541.80503,-344.2551 a 1.7999865,1.8 0 0 1 -1.79998,1.8 1.7999865,1.8 0 0 1 -1.79999,-1.8 1.7999865,1.8 0 0 1 1.79999,-1.8 1.7999865,1.8 0 0 1 1.79998,1.8 z" />
|
||||
<path
|
||||
id="circle8"
|
||||
style="stroke-width:0.120001;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 536.80506,-344.2551 a 1.800027,1.8 0 0 1 -1.80003,1.8 1.800027,1.8 0 0 1 -1.80003,-1.8 1.800027,1.8 0 0 1 1.80003,-1.8 1.800027,1.8 0 0 1 1.80003,1.8 z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -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.
|
||||
|
||||
@@ -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<blender::float3>());
|
||||
return;
|
||||
case CON_ATTRIBUTE_QUATERNION:
|
||||
quat_to_mat4(r_matrix, *value.get<blender::float4>());
|
||||
return;
|
||||
case CON_ATTRIBUTE_4X4MATRIX:
|
||||
copy_m4_m4(r_matrix, value.get<blender::float4x4>()->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<blender::bke::GeometryComponent::Type> 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<bGeometryAttributeConstraint *>(con->data);
|
||||
MEM_SAFE_FREE(data->attribute_name);
|
||||
}
|
||||
|
||||
static void geometry_attribute_id_looper(bConstraint *con, ConstraintIDFunc func, void *userdata)
|
||||
{
|
||||
bGeometryAttributeConstraint *data = static_cast<bGeometryAttributeConstraint *>(con->data);
|
||||
func(con, (ID **)&data->target, false, userdata);
|
||||
}
|
||||
|
||||
static void geometry_attribute_copy_data(bConstraint *con, bConstraint *srccon)
|
||||
{
|
||||
const auto *src = static_cast<bGeometryAttributeConstraint *>(srccon->data);
|
||||
auto *dst = static_cast<bGeometryAttributeConstraint *>(con->data);
|
||||
dst->attribute_name = BLI_strdup_null(src->attribute_name);
|
||||
}
|
||||
|
||||
static void geometry_attribute_new_data(void *cdata)
|
||||
{
|
||||
bGeometryAttributeConstraint *data = static_cast<bGeometryAttributeConstraint *>(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<bGeometryAttributeConstraint *>(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<bGeometryAttributeConstraint *>(con->data);
|
||||
bConstraintTarget *ct = static_cast<bConstraintTarget *>(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<bGeometryAttributeConstraint *>(
|
||||
con->data);
|
||||
|
||||
if (!VALID_CONS_TARGET(ct)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unit_m4(ct->matrix);
|
||||
|
||||
const bke::AttrDomain domain = domain_value_to_attribute(
|
||||
static_cast<Attribute_Domain>(acon->domain));
|
||||
const bke::AttrType sample_data_type = type_value_to_attribute(
|
||||
static_cast<Attribute_Data_Type>(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<bke::AttributeAccessor> 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<int>(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<Attribute_Data_Type>(acon->data_type));
|
||||
type.destruct(sampled_value);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void geometry_attribute_evaluate(bConstraint *con, bConstraintOb *cob, ListBase *targets)
|
||||
{
|
||||
bConstraintTarget *ct = static_cast<bConstraintTarget *>(targets->first);
|
||||
const bGeometryAttributeConstraint *data = static_cast<bGeometryAttributeConstraint *>(
|
||||
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<bGeometryAttributeConstraint *>(
|
||||
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<bTransformCacheConstraint *>(con->data);
|
||||
data->reader = nullptr;
|
||||
data->reader_object_path[0] = '\0';
|
||||
break;
|
||||
}
|
||||
case CONSTRAINT_TYPE_GEOMETRY_ATTRIBUTE: {
|
||||
bGeometryAttributeConstraint *data = static_cast<bGeometryAttributeConstraint *>(
|
||||
con->data);
|
||||
BLO_read_string(reader, &data->attribute_name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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? */
|
||||
|
||||
@@ -272,6 +272,7 @@ if(WITH_BLENDER)
|
||||
con_floor
|
||||
con_followpath
|
||||
con_followtrack
|
||||
con_geometryattribute
|
||||
con_kinematic
|
||||
con_locktrack
|
||||
con_loclike
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
tests/files/constraints/constraints.blend
(Stored with Git LFS)
BIN
tests/files/constraints/constraints.blend
(Stored with Git LFS)
Binary file not shown.
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user