Geometry Nodes: Allow attribute nodes to use different domains

Currently every attribute node assumes that the attribute exists on the
"points" domain, so it generally isn't possible to work with attributes
on other domains like edges, polygons, and corners.

This commit adds a heuristic to each attribute node to determine the
correct domain for the result attribute. In general, it works like this:
 - If the output attribute already exists, use that domain.
 - Otherwise, use the highest priority domain of the input attributes.
 - If none of the inputs are attributes, use the default domain (points).

For the implementation I abstracted the check a bit, but in each
node has a slightly different situation, so we end up with slightly
different `get_result_domain` functions in each node. I think this makes
sense, it keeps the code flexible and more easily understandable.

Note that we might eventually want to expose a domain drop-down to some
of the nodes. But that will be a separate discussion; this commit focuses
on making a more useful choice automatically.

Differential Revision: https://developer.blender.org/D10389
This commit is contained in:
Hans Goudey 2021-02-12 12:46:17 -06:00
parent ba03f7f0b1
commit d7c2c889a6
Notes: blender-bot 2023-02-14 11:28:39 +01:00
Referenced by issue #84297, Expose more built-in data as attributes
16 changed files with 312 additions and 48 deletions

View File

@ -222,6 +222,10 @@ class GeoNodeExecParams {
const GeometryComponent &component,
const CustomDataType default_type) const;
AttributeDomain get_highest_priority_input_domain(Span<std::string> names,
const GeometryComponent &component,
const AttributeDomain default_domain) const;
private:
/* Utilities for detecting common errors at when using this class. */
void check_extract_input(StringRef identifier, const CPPType *requested_type = nullptr) const;

View File

@ -338,6 +338,52 @@ CustomDataType attribute_data_type_highest_complexity(Span<CustomDataType> data_
return most_complex_type;
}
/**
* \note Generally the order should mirror the order of the domains
* established in each component's ComponentAttributeProviders.
*/
static int attribute_domain_priority(const AttributeDomain domain)
{
switch (domain) {
#if 0
case ATTR_DOMAIN_CURVE:
return 0;
#endif
case ATTR_DOMAIN_POLYGON:
return 1;
case ATTR_DOMAIN_EDGE:
return 2;
case ATTR_DOMAIN_POINT:
return 3;
case ATTR_DOMAIN_CORNER:
return 4;
default:
/* Domain not supported in nodes yet. */
BLI_assert(false);
return 0;
}
}
/**
* Domains with a higher "information density" have a higher priority, in order
* to choose a domain that will not lose data through domain conversion.
*/
AttributeDomain attribute_domain_highest_priority(Span<AttributeDomain> domains)
{
int highest_priority = INT_MIN;
AttributeDomain highest_priority_domain = ATTR_DOMAIN_CORNER;
for (const AttributeDomain domain : domains) {
const int priority = attribute_domain_priority(domain);
if (priority > highest_priority) {
highest_priority = priority;
highest_priority_domain = domain;
}
}
return highest_priority_domain;
}
} // namespace blender::nodes
bool geo_node_poll_default(bNodeType *UNUSED(ntype), bNodeTree *ntree)

View File

@ -45,6 +45,7 @@ void update_attribute_input_socket_availabilities(bNode &node,
const bool name_is_available = true);
CustomDataType attribute_data_type_highest_complexity(Span<CustomDataType>);
AttributeDomain attribute_domain_highest_priority(Span<AttributeDomain> domains);
Array<uint32_t> get_geometry_element_ids_as_uints(const GeometryComponent &component,
const AttributeDomain domain);

View File

@ -42,27 +42,44 @@ static void geo_node_attribute_color_ramp_layout(uiLayout *layout,
namespace blender::nodes {
static AttributeDomain get_result_domain(const GeometryComponent &component,
StringRef input_name,
StringRef result_name)
{
/* Use the domain of the result attribute if it already exists. */
ReadAttributePtr result_attribute = component.attribute_try_get_for_read(result_name);
if (result_attribute) {
return result_attribute->domain();
}
/* Otherwise use the input attribute's domain if it exists. */
ReadAttributePtr input_attribute = component.attribute_try_get_for_read(input_name);
if (input_attribute) {
return input_attribute->domain();
}
return ATTR_DOMAIN_POINT;
}
static void execute_on_component(const GeoNodeExecParams &params, GeometryComponent &component)
{
const bNode &bnode = params.node();
NodeAttributeColorRamp *node_storage = (NodeAttributeColorRamp *)bnode.storage;
const std::string result_name = params.get_input<std::string>("Result");
const std::string input_name = params.get_input<std::string>("Attribute");
/* Always output a color attribute for now. We might want to allow users to customize.
* Using the type of an existing attribute could work, but does not have a real benefit
* currently. */
const CustomDataType result_type = CD_PROP_COLOR;
const AttributeDomain result_domain = get_result_domain(component, input_name, result_name);
const std::string result_name = params.get_input<std::string>("Result");
/* Once we support more domains at the user level, we have to decide how the result domain is
* chosen. */
const AttributeDomain result_domain = ATTR_DOMAIN_POINT;
OutputAttributePtr attribute_result = component.attribute_try_get_for_output(
result_name, result_domain, result_type);
if (!attribute_result) {
return;
}
const std::string input_name = params.get_input<std::string>("Attribute");
FloatReadAttribute attribute_in = component.attribute_get_for_read<float>(
input_name, result_domain, 0.0f);

View File

@ -69,14 +69,27 @@ static void geo_node_attribute_combine_xyz_update(bNodeTree *UNUSED(ntree), bNod
*node, "Z", (GeometryNodeAttributeInputMode)node_storage->input_type_z);
}
static AttributeDomain get_result_domain(const GeometryComponent &component,
const GeoNodeExecParams &params,
StringRef result_name)
{
/* Use the domain of the result attribute if it already exists. */
ReadAttributePtr result_attribute = component.attribute_try_get_for_read(result_name);
if (result_attribute) {
return result_attribute->domain();
}
/* Otherwise use the highest priority domain from existing input attributes, or the default. */
return params.get_highest_priority_input_domain({"X", "Y", "Z"}, component, ATTR_DOMAIN_POINT);
}
static void combine_attributes(GeometryComponent &component, const GeoNodeExecParams &params)
{
const std::string result_name = params.get_input<std::string>("Result");
/* The result domain is always point for now. */
const AttributeDomain result_domain = ATTR_DOMAIN_POINT;
if (result_name.empty()) {
return;
}
const AttributeDomain result_domain = get_result_domain(component, params, result_name);
OutputAttributePtr attribute_result = component.attribute_try_get_for_output(
result_name, result_domain, CD_PROP_FLOAT3);

View File

@ -240,20 +240,31 @@ static CustomDataType get_data_type(GeometryComponent &component,
return CD_PROP_FLOAT;
}
static AttributeDomain get_result_domain(const GeometryComponent &component,
const GeoNodeExecParams &params,
StringRef result_name)
{
/* Use the domain of the result attribute if it already exists. */
ReadAttributePtr result_attribute = component.attribute_try_get_for_read(result_name);
if (result_attribute) {
return result_attribute->domain();
}
/* Otherwise use the highest priority domain from existing input attributes, or the default. */
return params.get_highest_priority_input_domain({"A", "B"}, component, ATTR_DOMAIN_POINT);
}
static void attribute_compare_calc(GeometryComponent &component, const GeoNodeExecParams &params)
{
const bNode &node = params.node();
NodeAttributeCompare *node_storage = (NodeAttributeCompare *)node.storage;
const FloatCompareOperation operation = static_cast<FloatCompareOperation>(
node_storage->operation);
/* The result type of this node is always float. */
const CustomDataType result_type = CD_PROP_BOOL;
/* The result domain is always point for now. */
const AttributeDomain result_domain = ATTR_DOMAIN_POINT;
/* Get result attribute first, in case it has to overwrite one of the existing attributes. */
const std::string result_name = params.get_input<std::string>("Result");
const CustomDataType result_type = CD_PROP_BOOL;
const AttributeDomain result_domain = get_result_domain(component, params, result_name);
OutputAttributePtr attribute_result = component.attribute_try_get_for_output(
result_name, result_domain, result_type);
if (!attribute_result) {

View File

@ -67,16 +67,32 @@ static void geo_node_attribute_fill_update(bNodeTree *UNUSED(ntree), bNode *node
namespace blender::nodes {
static AttributeDomain get_result_domain(const GeometryComponent &component,
const GeoNodeExecParams &params,
StringRef attribute_name)
{
/* Use the domain of the result attribute if it already exists. */
ReadAttributePtr result_attribute = component.attribute_try_get_for_read(attribute_name);
if (result_attribute) {
return result_attribute->domain();
}
/* Otherwise use the input domain chosen in the interface. */
const bNode &node = params.node();
return static_cast<AttributeDomain>(node.custom2);
}
static void fill_attribute(GeometryComponent &component, const GeoNodeExecParams &params)
{
const bNode &node = params.node();
const CustomDataType data_type = static_cast<CustomDataType>(node.custom1);
const AttributeDomain domain = static_cast<AttributeDomain>(node.custom2);
const std::string attribute_name = params.get_input<std::string>("Attribute");
if (attribute_name.empty()) {
return;
}
const bNode &node = params.node();
const CustomDataType data_type = static_cast<CustomDataType>(node.custom1);
const AttributeDomain domain = get_result_domain(component, params, attribute_name);
OutputAttributePtr attribute = component.attribute_try_get_for_output(
attribute_name, domain, data_type);
if (!attribute) {

View File

@ -202,19 +202,40 @@ static void do_math_operation(Span<float> span_input,
UNUSED_VARS_NDEBUG(success);
}
static AttributeDomain get_result_domain(const GeometryComponent &component,
const GeoNodeExecParams &params,
const NodeMathOperation operation,
StringRef result_name)
{
/* Use the domain of the result attribute if it already exists. */
ReadAttributePtr result_attribute = component.attribute_try_get_for_read(result_name);
if (result_attribute) {
return result_attribute->domain();
}
/* Otherwise use the highest priority domain from existing input attributes, or the default. */
const AttributeDomain default_domain = ATTR_DOMAIN_POINT;
if (operation_use_input_b(operation)) {
if (operation_use_input_c(operation)) {
return params.get_highest_priority_input_domain({"A", "B", "C"}, component, default_domain);
}
return params.get_highest_priority_input_domain({"A", "B"}, component, default_domain);
}
return params.get_highest_priority_input_domain({"A"}, component, default_domain);
}
static void attribute_math_calc(GeometryComponent &component, const GeoNodeExecParams &params)
{
const bNode &node = params.node();
const NodeAttributeMath *node_storage = (const NodeAttributeMath *)node.storage;
const NodeMathOperation operation = static_cast<NodeMathOperation>(node_storage->operation);
const std::string result_name = params.get_input<std::string>("Result");
/* The result type of this node is always float. */
const CustomDataType result_type = CD_PROP_FLOAT;
/* The result domain is always point for now. */
const AttributeDomain result_domain = ATTR_DOMAIN_POINT;
const AttributeDomain result_domain = get_result_domain(
component, params, operation, result_name);
/* Get result attribute first, in case it has to overwrite one of the existing attributes. */
const std::string result_name = params.get_input<std::string>("Result");
OutputAttributePtr attribute_result = component.attribute_try_get_for_output(
result_name, result_domain, result_type);
if (!attribute_result) {

View File

@ -125,10 +125,25 @@ static void do_mix_operation(const CustomDataType result_type,
}
}
static AttributeDomain get_result_domain(const GeometryComponent &component,
const GeoNodeExecParams &params,
StringRef result_name)
{
/* Use the domain of the result attribute if it already exists. */
ReadAttributePtr result_attribute = component.attribute_try_get_for_read(result_name);
if (result_attribute) {
return result_attribute->domain();
}
/* Otherwise use the highest priority domain from existing input attributes, or the default. */
return params.get_highest_priority_input_domain({"A", "B"}, component, ATTR_DOMAIN_POINT);
}
static void attribute_mix_calc(GeometryComponent &component, const GeoNodeExecParams &params)
{
const bNode &node = params.node();
const NodeAttributeMix *node_storage = (const NodeAttributeMix *)node.storage;
const std::string result_name = params.get_input<std::string>("Result");
/* Use the highest complexity data type among the inputs and outputs, that way the node will
* never "remove information". Use CD_PROP_BOOL as the lowest complexity data type, but in any
@ -139,14 +154,7 @@ static void attribute_mix_calc(GeometryComponent &component, const GeoNodeExecPa
params.get_input_attribute_data_type("Result", component, CD_PROP_BOOL),
});
/* Once we support more domains at the user level, we have to decide how the result domain is
* chosen. */
AttributeDomain result_domain = ATTR_DOMAIN_POINT;
const std::string result_name = params.get_input<std::string>("Result");
const ReadAttributePtr result_attribute_read = component.attribute_try_get_for_read(result_name);
if (result_attribute_read) {
result_domain = result_attribute_read->domain();
}
const AttributeDomain result_domain = get_result_domain(component, params, result_name);
OutputAttributePtr attribute_result = component.attribute_try_get_for_output(
result_name, result_domain, result_type);

View File

@ -148,9 +148,12 @@ static void attribute_calc_proximity(GeometryComponent &component,
GeometrySet &geometry_set_target,
GeoNodeExecParams &params)
{
/* This node works on the "point" domain, since that is where positions are stored. */
const AttributeDomain result_domain = ATTR_DOMAIN_POINT;
const std::string result_attribute_name = params.get_input<std::string>("Result");
OutputAttributePtr distance_attribute = component.attribute_try_get_for_output(
result_attribute_name, ATTR_DOMAIN_POINT, CD_PROP_FLOAT);
result_attribute_name, result_domain, CD_PROP_FLOAT);
ReadAttributePtr position_attribute = component.attribute_try_get_for_read("position");
BLI_assert(position_attribute->custom_data_type() == CD_PROP_FLOAT3);

View File

@ -153,17 +153,32 @@ Array<uint32_t> get_geometry_element_ids_as_uints(const GeometryComponent &compo
return hashes;
}
static AttributeDomain get_result_domain(const GeometryComponent &component,
const GeoNodeExecParams &params,
StringRef attribute_name)
{
/* Use the domain of the result attribute if it already exists. */
ReadAttributePtr result_attribute = component.attribute_try_get_for_read(attribute_name);
if (result_attribute) {
return result_attribute->domain();
}
/* Otherwise use the input domain chosen in the interface. */
const bNode &node = params.node();
return static_cast<AttributeDomain>(node.custom2);
}
static void randomize_attribute(GeometryComponent &component,
const GeoNodeExecParams &params,
const int seed)
{
const bNode &node = params.node();
const CustomDataType data_type = static_cast<CustomDataType>(node.custom1);
const AttributeDomain domain = static_cast<AttributeDomain>(node.custom2);
const std::string attribute_name = params.get_input<std::string>("Attribute");
if (attribute_name.empty()) {
return;
}
const bNode &node = params.node();
const CustomDataType data_type = static_cast<CustomDataType>(node.custom1);
const AttributeDomain domain = get_result_domain(component, params, attribute_name);
OutputAttributePtr attribute = component.attribute_try_get_for_output(
attribute_name, domain, data_type);

View File

@ -48,29 +48,51 @@ static void geo_node_attribute_sample_texture_layout(uiLayout *layout,
namespace blender::nodes {
static AttributeDomain get_result_domain(const GeometryComponent &component,
StringRef result_attribute_name,
StringRef map_attribute_name)
{
/* Use the domain of the result attribute if it already exists. */
ReadAttributePtr result_attribute = component.attribute_try_get_for_read(result_attribute_name);
if (result_attribute) {
return result_attribute->domain();
}
/* Otherwise use the name of the map attribute. */
ReadAttributePtr map_attribute = component.attribute_try_get_for_read(map_attribute_name);
if (map_attribute) {
return map_attribute->domain();
}
/* The node won't execute in this case, but we still have to return a value. */
return ATTR_DOMAIN_POINT;
}
static void execute_on_component(GeometryComponent &component, const GeoNodeExecParams &params)
{
const bNode &node = params.node();
Tex *texture = reinterpret_cast<Tex *>(node.id);
const std::string result_attribute_name = params.get_input<std::string>("Result");
if (texture == nullptr) {
return;
}
const std::string result_attribute_name = params.get_input<std::string>("Result");
const std::string mapping_name = params.get_input<std::string>("Mapping");
if (!component.attribute_exists(mapping_name)) {
return;
}
const AttributeDomain result_domain = get_result_domain(
component, result_attribute_name, mapping_name);
OutputAttributePtr attribute_out = component.attribute_try_get_for_output(
result_attribute_name, ATTR_DOMAIN_POINT, CD_PROP_COLOR);
result_attribute_name, result_domain, CD_PROP_COLOR);
if (!attribute_out) {
return;
}
Float3ReadAttribute mapping_attribute = component.attribute_get_for_read<float3>(
mapping_name, ATTR_DOMAIN_POINT, {0, 0, 0});
mapping_name, result_domain, {0, 0, 0});
MutableSpan<Color4f> colors = attribute_out->get_span<Color4f>();
for (const int i : IndexRange(mapping_attribute.size())) {

View File

@ -69,22 +69,49 @@ static void extract_input(const int index, const Span<float3> &input, MutableSpa
}
}
static AttributeDomain get_result_domain(const GeometryComponent &component,
const GeoNodeExecParams &params,
StringRef result_name_x,
StringRef result_name_y,
StringRef result_name_z)
{
/* Use the highest priority domain from any existing attribute outputs. */
Vector<AttributeDomain, 3> output_domains;
ReadAttributePtr attribute_x = component.attribute_try_get_for_read(result_name_x);
ReadAttributePtr attribute_y = component.attribute_try_get_for_read(result_name_y);
ReadAttributePtr attribute_z = component.attribute_try_get_for_read(result_name_z);
if (attribute_x) {
output_domains.append(attribute_x->domain());
}
if (attribute_y) {
output_domains.append(attribute_y->domain());
}
if (attribute_z) {
output_domains.append(attribute_z->domain());
}
if (output_domains.size() > 0) {
return attribute_domain_highest_priority(output_domains);
}
/* Otherwise use the domain of the input attribute, or the default. */
return params.get_highest_priority_input_domain({"Vector"}, component, ATTR_DOMAIN_POINT);
}
static void separate_attribute(GeometryComponent &component, const GeoNodeExecParams &params)
{
const std::string result_name_x = params.get_input<std::string>("Result X");
const std::string result_name_y = params.get_input<std::string>("Result Y");
const std::string result_name_z = params.get_input<std::string>("Result Z");
/* The node is only for float3 to float conversions. */
const CustomDataType input_type = CD_PROP_FLOAT3;
const CustomDataType result_type = CD_PROP_FLOAT;
/* The result domain is always point for now. */
const AttributeDomain result_domain = ATTR_DOMAIN_POINT;
/* No output to write to. */
if (result_name_x.empty() && result_name_y.empty() && result_name_z.empty()) {
return;
}
/* The node is only for float3 to float conversions. */
const CustomDataType input_type = CD_PROP_FLOAT3;
const CustomDataType result_type = CD_PROP_FLOAT;
const AttributeDomain result_domain = get_result_domain(
component, params, result_name_x, result_name_y, result_name_z);
ReadAttributePtr attribute_input = params.get_input_attribute(
"Vector", component, result_domain, input_type, nullptr);
if (!attribute_input) {

View File

@ -327,12 +327,35 @@ static void do_math_operation_fl3_to_fl(const Float3ReadAttribute &input_a,
UNUSED_VARS_NDEBUG(success);
}
static AttributeDomain get_result_domain(const GeometryComponent &component,
const GeoNodeExecParams &params,
const NodeVectorMathOperation operation,
StringRef result_name)
{
/* Use the domain of the result attribute if it already exists. */
ReadAttributePtr result_attribute = component.attribute_try_get_for_read(result_name);
if (result_attribute) {
return result_attribute->domain();
}
/* Otherwise use the highest priority domain from existing input attributes, or the default. */
const AttributeDomain default_domain = ATTR_DOMAIN_POINT;
if (operation_use_input_b(operation)) {
if (operation_use_input_c(operation)) {
return params.get_highest_priority_input_domain({"A", "B", "C"}, component, default_domain);
}
return params.get_highest_priority_input_domain({"A", "B"}, component, default_domain);
}
return params.get_highest_priority_input_domain({"A"}, component, default_domain);
}
static void attribute_vector_math_calc(GeometryComponent &component,
const GeoNodeExecParams &params)
{
const bNode &node = params.node();
const NodeAttributeVectorMath *node_storage = (const NodeAttributeVectorMath *)node.storage;
const NodeVectorMathOperation operation = (NodeVectorMathOperation)node_storage->operation;
const std::string result_name = params.get_input<std::string>("Result");
/* The number and type of the input attribute depend on the operation. */
const CustomDataType read_type_a = CD_PROP_FLOAT3;
@ -343,7 +366,8 @@ static void attribute_vector_math_calc(GeometryComponent &component,
/* The result domain is always point for now. */
const CustomDataType result_type = operation_get_result_type(operation);
const AttributeDomain result_domain = ATTR_DOMAIN_POINT;
const AttributeDomain result_domain = get_result_domain(
component, params, operation, result_name);
ReadAttributePtr attribute_a = params.get_input_attribute(
"A", component, result_domain, read_type_a, nullptr);
@ -366,7 +390,6 @@ static void attribute_vector_math_calc(GeometryComponent &component,
}
/* Get result attribute first, in case it has to overwrite one of the existing attributes. */
const std::string result_name = params.get_input<std::string>("Result");
OutputAttributePtr attribute_result = component.attribute_try_get_for_output(
result_name, result_domain, result_type);
if (!attribute_result) {

View File

@ -147,16 +147,17 @@ static void determine_final_data_type_and_domain(Span<const GeometryComponent *>
AttributeDomain *r_domain)
{
Vector<CustomDataType> data_types;
Vector<AttributeDomain> domains;
for (const GeometryComponent *component : components) {
ReadAttributePtr attribute = component->attribute_try_get_for_read(attribute_name);
if (attribute) {
data_types.append(attribute->custom_data_type());
/* TODO: Use highest priority domain. */
*r_domain = attribute->domain();
domains.append(attribute->domain());
}
}
*r_type = attribute_data_type_highest_complexity(data_types);
*r_domain = attribute_domain_highest_priority(domains);
}
static void fill_new_attribute(Span<const GeometryComponent *> src_components,

View File

@ -17,6 +17,8 @@
#include "NOD_geometry_exec.hh"
#include "NOD_type_callbacks.hh"
#include "node_geometry_util.hh"
namespace blender::nodes {
const bNodeSocket *GeoNodeExecParams::find_available_socket(const StringRef name) const
@ -104,6 +106,40 @@ CustomDataType GeoNodeExecParams::get_input_attribute_data_type(
return default_type;
}
/**
* If any of the corresponding input sockets are attributes instead of single values,
* use the highest priority attribute domain from among them.
* Otherwise return the default domain.
*/
AttributeDomain GeoNodeExecParams::get_highest_priority_input_domain(
Span<std::string> names,
const GeometryComponent &component,
const AttributeDomain default_domain) const
{
Vector<AttributeDomain, 8> input_domains;
for (const std::string &name : names) {
const bNodeSocket *found_socket = this->find_available_socket(name);
BLI_assert(found_socket != nullptr); /* A socket should be available socket for the name. */
if (found_socket == nullptr) {
continue;
}
if (found_socket->type == SOCK_STRING) {
const std::string name = this->get_input<std::string>(found_socket->identifier);
ReadAttributePtr attribute = component.attribute_try_get_for_read(name);
if (attribute) {
input_domains.append(attribute->domain());
}
}
}
if (input_domains.size() > 0) {
return attribute_domain_highest_priority(input_domains);
}
return default_domain;
}
void GeoNodeExecParams::check_extract_input(StringRef identifier,
const CPPType *requested_type) const
{