Geometry Nodes: Field Statistic Nodes
Adds field versions of the operations from Attribute Statistic, can support grouping via Group IDs. These operations are added as separate nodes: `Field Average`, `Field Min & Max`, & `Field Variance` which group the different statistic operations according to their use. Supported Data Types: - Field Average - (Float, Vector) - Field Min & Max - (Float, Integer, Vector) - Field Variance - (Float, Vector) Pull Request: https://projects.blender.org/blender/blender/pulls/134640
This commit is contained in:
@@ -677,6 +677,9 @@ class NODE_MT_category_GEO_UTILITIES_FIELD(Menu):
|
||||
node_add_menu.add_node_type(layout, "GeometryNodeAccumulateField")
|
||||
node_add_menu.add_node_type(layout, "GeometryNodeFieldAtIndex")
|
||||
node_add_menu.add_node_type(layout, "GeometryNodeFieldOnDomain")
|
||||
node_add_menu.add_node_type(layout, "GeometryNodeFieldAverage")
|
||||
node_add_menu.add_node_type(layout, "GeometryNodeFieldMinAndMax")
|
||||
node_add_menu.add_node_type(layout, "GeometryNodeFieldVariance")
|
||||
node_add_menu.draw_assets_for_catalog(layout, "Utilities/Field")
|
||||
|
||||
|
||||
|
||||
@@ -12786,7 +12786,10 @@ static void rna_def_nodes(BlenderRNA *brna)
|
||||
define("GeometryNode", "GeometryNodeExtrudeMesh");
|
||||
define("GeometryNode", "GeometryNodeFaceOfCorner");
|
||||
define("GeometryNode", "GeometryNodeFieldAtIndex");
|
||||
define("GeometryNode", "GeometryNodeFieldAverage");
|
||||
define("GeometryNode", "GeometryNodeFieldMinAndMax");
|
||||
define("GeometryNode", "GeometryNodeFieldOnDomain");
|
||||
define("GeometryNode", "GeometryNodeFieldVariance");
|
||||
define("GeometryNode", "GeometryNodeFillCurve");
|
||||
define("GeometryNode", "GeometryNodeFilletCurve");
|
||||
define("GeometryNode", "GeometryNodeFlipFaces");
|
||||
|
||||
@@ -78,6 +78,9 @@ set(SRC
|
||||
nodes/node_geo_evaluate_closure.cc
|
||||
nodes/node_geo_evaluate_on_domain.cc
|
||||
nodes/node_geo_extrude_mesh.cc
|
||||
nodes/node_geo_field_average.cc
|
||||
nodes/node_geo_field_min_and_max.cc
|
||||
nodes/node_geo_field_variance.cc
|
||||
nodes/node_geo_flip_faces.cc
|
||||
nodes/node_geo_foreach_geometry_element.cc
|
||||
nodes/node_geo_geometry_to_instance.cc
|
||||
|
||||
332
source/blender/nodes/geometry/nodes/node_geo_field_average.cc
Normal file
332
source/blender/nodes/geometry/nodes/node_geo_field_average.cc
Normal file
@@ -0,0 +1,332 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BKE_attribute_math.hh"
|
||||
|
||||
#include "BLI_array.hh"
|
||||
#include "BLI_generic_virtual_array.hh"
|
||||
#include "BLI_math_vector.h"
|
||||
#include "BLI_vector.hh"
|
||||
#include "BLI_virtual_array.hh"
|
||||
|
||||
#include "NOD_rna_define.hh"
|
||||
#include "NOD_socket_search_link.hh"
|
||||
|
||||
#include "RNA_enum_types.hh"
|
||||
|
||||
#include "node_geometry_util.hh"
|
||||
|
||||
#include "UI_interface.hh"
|
||||
#include "UI_resources.hh"
|
||||
|
||||
#include <numeric>
|
||||
|
||||
namespace blender::nodes::node_geo_field_average_cc {
|
||||
|
||||
static void node_declare(NodeDeclarationBuilder &b)
|
||||
{
|
||||
const bNode *node = b.node_or_null();
|
||||
|
||||
if (node != nullptr) {
|
||||
const eCustomDataType data_type = eCustomDataType(node->custom1);
|
||||
b.add_input(data_type, "Value")
|
||||
.supports_field()
|
||||
.description("The values the mean and median will be calculated from");
|
||||
}
|
||||
|
||||
b.add_input<decl::Int>("Group ID", "Group Index")
|
||||
.supports_field()
|
||||
.hide_value()
|
||||
.description("An index used to group values together for multiple separate operations");
|
||||
|
||||
if (node != nullptr) {
|
||||
const eCustomDataType data_type = eCustomDataType(node->custom1);
|
||||
b.add_output(data_type, "Mean")
|
||||
.field_source_reference_all()
|
||||
.description("The sum of all values in each group divided by the size of said group");
|
||||
b.add_output(data_type, "Median")
|
||||
.field_source_reference_all()
|
||||
.description(
|
||||
"The middle value in each group when all values are sorted from lowest to highest");
|
||||
}
|
||||
}
|
||||
|
||||
static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
|
||||
{
|
||||
uiItemR(layout, ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE);
|
||||
uiItemR(layout, ptr, "domain", UI_ITEM_NONE, "", ICON_NONE);
|
||||
}
|
||||
|
||||
static void node_init(bNodeTree * /*tree*/, bNode *node)
|
||||
{
|
||||
node->custom1 = CD_PROP_FLOAT;
|
||||
node->custom2 = int16_t(AttrDomain::Point);
|
||||
}
|
||||
|
||||
enum class Operation { Mean = 0, Median = 1 };
|
||||
|
||||
static std::optional<eCustomDataType> node_type_from_other_socket(const bNodeSocket &socket)
|
||||
{
|
||||
switch (socket.type) {
|
||||
case SOCK_FLOAT:
|
||||
case SOCK_BOOLEAN:
|
||||
case SOCK_INT:
|
||||
return CD_PROP_FLOAT;
|
||||
case SOCK_VECTOR:
|
||||
case SOCK_RGBA:
|
||||
return CD_PROP_FLOAT3;
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
static void node_gather_link_searches(GatherLinkSearchOpParams ¶ms)
|
||||
{
|
||||
const NodeDeclaration &declaration = *params.node_type().static_declaration;
|
||||
search_link_ops_for_declarations(params, declaration.inputs);
|
||||
|
||||
const std::optional<eCustomDataType> type = node_type_from_other_socket(params.other_socket());
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
if (params.in_out() == SOCK_OUT) {
|
||||
params.add_item(
|
||||
IFACE_("Mean"),
|
||||
[type](LinkSearchOpParams ¶ms) {
|
||||
bNode &node = params.add_node("GeometryNodeFieldAverage");
|
||||
node.custom1 = *type;
|
||||
params.update_and_connect_available_socket(node, "Mean");
|
||||
},
|
||||
0);
|
||||
params.add_item(
|
||||
IFACE_("Median"),
|
||||
[type](LinkSearchOpParams ¶ms) {
|
||||
bNode &node = params.add_node("GeometryNodeFieldAverage");
|
||||
node.custom1 = *type;
|
||||
params.update_and_connect_available_socket(node, "Median");
|
||||
},
|
||||
-1);
|
||||
}
|
||||
else {
|
||||
params.add_item(
|
||||
IFACE_("Value"),
|
||||
[type](LinkSearchOpParams ¶ms) {
|
||||
bNode &node = params.add_node("GeometryNodeFieldAverage");
|
||||
node.custom1 = *type;
|
||||
params.update_and_connect_available_socket(node, "Value");
|
||||
},
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T> static T calculate_median(MutableSpan<T> values)
|
||||
{
|
||||
if constexpr (std::is_same<T, float3>::value) {
|
||||
Array<float> x_vals(values.size()), y_vals(values.size()), z_vals(values.size());
|
||||
|
||||
for (const int i : values.index_range()) {
|
||||
float3 value = values[i];
|
||||
x_vals[i] = value.x;
|
||||
y_vals[i] = value.y;
|
||||
z_vals[i] = value.z;
|
||||
}
|
||||
|
||||
return float3(calculate_median<float>(x_vals),
|
||||
calculate_median<float>(y_vals),
|
||||
calculate_median<float>(z_vals));
|
||||
}
|
||||
else {
|
||||
const auto middle_itr = values.begin() + values.size() / 2;
|
||||
std::nth_element(values.begin(), middle_itr, values.end());
|
||||
if (values.size() % 2 == 0) {
|
||||
const auto left_middle_itr = std::max_element(values.begin(), middle_itr);
|
||||
return math::midpoint<T>(*left_middle_itr, *middle_itr);
|
||||
}
|
||||
return *middle_itr;
|
||||
}
|
||||
}
|
||||
|
||||
class FieldAverageInput final : public bke::GeometryFieldInput {
|
||||
private:
|
||||
GField input_;
|
||||
Field<int> group_index_;
|
||||
AttrDomain source_domain_;
|
||||
Operation operation_;
|
||||
|
||||
public:
|
||||
FieldAverageInput(const AttrDomain source_domain,
|
||||
GField input,
|
||||
Field<int> group_index,
|
||||
Operation operation)
|
||||
: bke::GeometryFieldInput(input.cpp_type(), "Calculation"),
|
||||
input_(std::move(input)),
|
||||
group_index_(std::move(group_index)),
|
||||
source_domain_(source_domain),
|
||||
operation_(operation)
|
||||
{
|
||||
}
|
||||
|
||||
GVArray get_varray_for_context(const bke::GeometryFieldContext &context,
|
||||
const IndexMask & /*mask*/) const final
|
||||
{
|
||||
const AttributeAccessor attributes = *context.attributes();
|
||||
const int64_t domain_size = attributes.domain_size(source_domain_);
|
||||
if (domain_size == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const bke::GeometryFieldContext source_context{context, source_domain_};
|
||||
fn::FieldEvaluator evaluator{source_context, domain_size};
|
||||
evaluator.add(input_);
|
||||
evaluator.add(group_index_);
|
||||
evaluator.evaluate();
|
||||
const GVArray g_values = evaluator.get_evaluated(0);
|
||||
const VArray<int> group_indices = evaluator.get_evaluated<int>(1);
|
||||
|
||||
GVArray g_outputs;
|
||||
|
||||
bke::attribute_math::convert_to_static_type(g_values.type(), [&](auto dummy) {
|
||||
using T = decltype(dummy);
|
||||
if constexpr (is_same_any_v<T, int, float, float3>) {
|
||||
const VArraySpan<T> values = g_values.typed<T>();
|
||||
|
||||
if (operation_ == Operation::Mean) {
|
||||
if (group_indices.is_single()) {
|
||||
const T mean = std::reduce(values.begin(), values.end(), T()) / domain_size;
|
||||
g_outputs = VArray<T>::ForSingle(mean, domain_size);
|
||||
}
|
||||
else {
|
||||
Map<int, std::pair<T, int>> sum_and_counts;
|
||||
for (const int i : values.index_range()) {
|
||||
auto &pair = sum_and_counts.lookup_or_add(group_indices[i], std::make_pair(T(), 0));
|
||||
pair.first = pair.first + values[i];
|
||||
pair.second = pair.second + 1;
|
||||
}
|
||||
|
||||
Array<T> outputs(domain_size);
|
||||
for (const int i : values.index_range()) {
|
||||
const auto &pair = sum_and_counts.lookup(group_indices[i]);
|
||||
outputs[i] = pair.first / pair.second;
|
||||
}
|
||||
g_outputs = VArray<T>::ForContainer(std::move(outputs));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (group_indices.is_single()) {
|
||||
Array<T> sorted_values(values);
|
||||
T median = calculate_median<T>(sorted_values);
|
||||
g_outputs = VArray<T>::ForSingle(median, domain_size);
|
||||
}
|
||||
else {
|
||||
Map<int, Vector<T>> groups;
|
||||
for (const int i : values.index_range()) {
|
||||
groups.lookup_or_add(group_indices[i], Vector<T>()).append(values[i]);
|
||||
}
|
||||
|
||||
Map<int, T> medians;
|
||||
for (MutableMapItem<int, Vector<T>> group : groups.items()) {
|
||||
medians.add(group.key, calculate_median<T>(group.value));
|
||||
}
|
||||
|
||||
Array<T> outputs(domain_size);
|
||||
for (const int i : values.index_range()) {
|
||||
outputs[i] = medians.lookup(group_indices[i]);
|
||||
}
|
||||
g_outputs = VArray<T>::ForContainer(std::move(outputs));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return attributes.adapt_domain(std::move(g_outputs), source_domain_, context.domain());
|
||||
}
|
||||
|
||||
uint64_t hash() const override
|
||||
{
|
||||
return get_default_hash(input_, group_index_, source_domain_, operation_);
|
||||
}
|
||||
|
||||
bool is_equal_to(const fn::FieldNode &other) const override
|
||||
{
|
||||
if (const FieldAverageInput *other_field = dynamic_cast<const FieldAverageInput *>(&other)) {
|
||||
return input_ == other_field->input_ && group_index_ == other_field->group_index_ &&
|
||||
source_domain_ == other_field->source_domain_ &&
|
||||
operation_ == other_field->operation_;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<AttrDomain> preferred_domain(
|
||||
const GeometryComponent & /*component*/) const override
|
||||
{
|
||||
return source_domain_;
|
||||
}
|
||||
};
|
||||
|
||||
static void node_geo_exec(GeoNodeExecParams params)
|
||||
{
|
||||
const AttrDomain source_domain = AttrDomain(params.node().custom2);
|
||||
|
||||
const Field<int> group_index_field = params.extract_input<Field<int>>("Group Index");
|
||||
const GField input_field = params.extract_input<GField>("Value");
|
||||
if (params.output_is_required("Mean")) {
|
||||
params.set_output<GField>(
|
||||
"Mean",
|
||||
GField{std::make_shared<FieldAverageInput>(
|
||||
source_domain, input_field, group_index_field, Operation::Mean)});
|
||||
}
|
||||
if (params.output_is_required("Median")) {
|
||||
params.set_output<GField>(
|
||||
"Median",
|
||||
GField{std::make_shared<FieldAverageInput>(
|
||||
source_domain, input_field, group_index_field, Operation::Median)});
|
||||
}
|
||||
}
|
||||
|
||||
static void node_rna(StructRNA *srna)
|
||||
{
|
||||
static EnumPropertyItem items[] = {
|
||||
{CD_PROP_FLOAT, "FLOAT", 0, "Float", "Floating-point value"},
|
||||
{CD_PROP_FLOAT3, "FLOAT_VECTOR", 0, "Vector", "3D vector with floating-point values"},
|
||||
{0, nullptr, 0, nullptr, nullptr},
|
||||
};
|
||||
|
||||
RNA_def_node_enum(srna,
|
||||
"data_type",
|
||||
"Data Type",
|
||||
"Type of data the outputs are calculated from",
|
||||
items,
|
||||
NOD_inline_enum_accessors(custom1),
|
||||
CD_PROP_FLOAT);
|
||||
|
||||
RNA_def_node_enum(srna,
|
||||
"domain",
|
||||
"Domain",
|
||||
"",
|
||||
rna_enum_attribute_domain_items,
|
||||
NOD_inline_enum_accessors(custom2),
|
||||
int(AttrDomain::Point),
|
||||
nullptr,
|
||||
true);
|
||||
}
|
||||
|
||||
static void node_register()
|
||||
{
|
||||
static blender::bke::bNodeType ntype;
|
||||
|
||||
geo_node_type_base(&ntype, "GeometryNodeFieldAverage");
|
||||
ntype.ui_name = "Field Average";
|
||||
ntype.ui_description = "Calculate the mean and median of a given field";
|
||||
ntype.nclass = NODE_CLASS_CONVERTER;
|
||||
ntype.geometry_node_execute = node_geo_exec;
|
||||
ntype.initfunc = node_init;
|
||||
ntype.draw_buttons = node_layout;
|
||||
ntype.declare = node_declare;
|
||||
ntype.gather_link_search_ops = node_gather_link_searches;
|
||||
blender::bke::node_register_type(ntype);
|
||||
node_rna(ntype.rna_ext.srna);
|
||||
}
|
||||
NOD_REGISTER_NODE(node_register)
|
||||
|
||||
} // namespace blender::nodes::node_geo_field_average_cc
|
||||
@@ -0,0 +1,318 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BKE_attribute_math.hh"
|
||||
|
||||
#include "BLI_array.hh"
|
||||
#include "BLI_generic_virtual_array.hh"
|
||||
#include "BLI_virtual_array.hh"
|
||||
|
||||
#include "NOD_rna_define.hh"
|
||||
#include "NOD_socket_search_link.hh"
|
||||
|
||||
#include "RNA_enum_types.hh"
|
||||
|
||||
#include "node_geometry_util.hh"
|
||||
|
||||
#include "UI_interface.hh"
|
||||
#include "UI_resources.hh"
|
||||
|
||||
namespace blender::nodes::node_geo_field_min_and_max_cc {
|
||||
|
||||
static void node_declare(NodeDeclarationBuilder &b)
|
||||
{
|
||||
const bNode *node = b.node_or_null();
|
||||
|
||||
if (node != nullptr) {
|
||||
const eCustomDataType data_type = eCustomDataType(node->custom1);
|
||||
b.add_input(data_type, "Value")
|
||||
.supports_field()
|
||||
.description("The values the mininum and maximum will be calculated from");
|
||||
}
|
||||
|
||||
b.add_input<decl::Int>("Group ID", "Group Index")
|
||||
.supports_field()
|
||||
.hide_value()
|
||||
.description("An index used to group values together for multiple separate operations");
|
||||
|
||||
if (node != nullptr) {
|
||||
const eCustomDataType data_type = eCustomDataType(node->custom1);
|
||||
b.add_output(data_type, "Min")
|
||||
.field_source_reference_all()
|
||||
.description("The lowest value in each group");
|
||||
b.add_output(data_type, "Max")
|
||||
.field_source_reference_all()
|
||||
.description("The highest value in each group");
|
||||
}
|
||||
}
|
||||
|
||||
static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
|
||||
{
|
||||
uiItemR(layout, ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE);
|
||||
uiItemR(layout, ptr, "domain", UI_ITEM_NONE, "", ICON_NONE);
|
||||
}
|
||||
|
||||
static void node_init(bNodeTree * /*tree*/, bNode *node)
|
||||
{
|
||||
node->custom1 = CD_PROP_FLOAT;
|
||||
node->custom2 = int16_t(AttrDomain::Point);
|
||||
}
|
||||
|
||||
enum class Operation { Min = 0, Max = 1 };
|
||||
|
||||
static std::optional<eCustomDataType> node_type_from_other_socket(const bNodeSocket &socket)
|
||||
{
|
||||
switch (socket.type) {
|
||||
case SOCK_FLOAT:
|
||||
return CD_PROP_FLOAT;
|
||||
case SOCK_BOOLEAN:
|
||||
case SOCK_INT:
|
||||
return CD_PROP_INT32;
|
||||
case SOCK_VECTOR:
|
||||
case SOCK_RGBA:
|
||||
case SOCK_ROTATION:
|
||||
return CD_PROP_FLOAT3;
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
static void node_gather_link_searches(GatherLinkSearchOpParams ¶ms)
|
||||
{
|
||||
const NodeDeclaration &declaration = *params.node_type().static_declaration;
|
||||
search_link_ops_for_declarations(params, declaration.inputs);
|
||||
|
||||
const std::optional<eCustomDataType> type = node_type_from_other_socket(params.other_socket());
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
if (params.in_out() == SOCK_OUT) {
|
||||
params.add_item(
|
||||
IFACE_("Min"),
|
||||
[type](LinkSearchOpParams ¶ms) {
|
||||
bNode &node = params.add_node("GeometryNodeFieldMinAndMax");
|
||||
node.custom1 = *type;
|
||||
params.update_and_connect_available_socket(node, "Min");
|
||||
},
|
||||
0);
|
||||
params.add_item(
|
||||
IFACE_("Max"),
|
||||
[type](LinkSearchOpParams ¶ms) {
|
||||
bNode &node = params.add_node("GeometryNodeFieldMinAndMax");
|
||||
node.custom1 = *type;
|
||||
params.update_and_connect_available_socket(node, "Max");
|
||||
},
|
||||
-1);
|
||||
}
|
||||
else {
|
||||
params.add_item(
|
||||
IFACE_("Value"),
|
||||
[type](LinkSearchOpParams ¶ms) {
|
||||
bNode &node = params.add_node("GeometryNodeFieldMinAndMax");
|
||||
node.custom1 = *type;
|
||||
params.update_and_connect_available_socket(node, "Value");
|
||||
},
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T> struct MinMaxInfo {
|
||||
static inline const T min_initial_value = []() {
|
||||
if constexpr (std::is_same_v<T, float3>) {
|
||||
return float3(std::numeric_limits<float>::max());
|
||||
}
|
||||
else {
|
||||
return std::numeric_limits<T>::max();
|
||||
}
|
||||
}();
|
||||
|
||||
static inline const T max_initial_value = []() {
|
||||
if constexpr (std::is_same_v<T, float3>) {
|
||||
return float3(std::numeric_limits<float>::lowest());
|
||||
}
|
||||
else {
|
||||
return std::numeric_limits<T>::lowest();
|
||||
}
|
||||
}();
|
||||
};
|
||||
|
||||
class FieldMinMaxInput final : public bke::GeometryFieldInput {
|
||||
private:
|
||||
GField input_;
|
||||
Field<int> group_index_;
|
||||
AttrDomain source_domain_;
|
||||
Operation operation_;
|
||||
|
||||
public:
|
||||
FieldMinMaxInput(const AttrDomain source_domain,
|
||||
GField input,
|
||||
Field<int> group_index,
|
||||
Operation operation)
|
||||
: bke::GeometryFieldInput(input.cpp_type(), "Calculation"),
|
||||
input_(std::move(input)),
|
||||
group_index_(std::move(group_index)),
|
||||
source_domain_(source_domain),
|
||||
operation_(operation)
|
||||
{
|
||||
}
|
||||
|
||||
GVArray get_varray_for_context(const bke::GeometryFieldContext &context,
|
||||
const IndexMask & /*mask*/) const final
|
||||
{
|
||||
const AttributeAccessor attributes = *context.attributes();
|
||||
const int64_t domain_size = attributes.domain_size(source_domain_);
|
||||
if (domain_size == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const bke::GeometryFieldContext source_context{context, source_domain_};
|
||||
fn::FieldEvaluator evaluator{source_context, domain_size};
|
||||
evaluator.add(input_);
|
||||
evaluator.add(group_index_);
|
||||
evaluator.evaluate();
|
||||
const GVArray g_values = evaluator.get_evaluated(0);
|
||||
const VArray<int> group_indices = evaluator.get_evaluated<int>(1);
|
||||
|
||||
GVArray g_outputs;
|
||||
|
||||
bke::attribute_math::convert_to_static_type(g_values.type(), [&](auto dummy) {
|
||||
using T = decltype(dummy);
|
||||
if constexpr (is_same_any_v<T, int, float, float3>) {
|
||||
const VArray<T> values = g_values.typed<T>();
|
||||
|
||||
if (operation_ == Operation::Min) {
|
||||
if (group_indices.is_single()) {
|
||||
T result = MinMaxInfo<T>::min_initial_value;
|
||||
for (const int i : values.index_range()) {
|
||||
result = math::min(result, values[i]);
|
||||
}
|
||||
g_outputs = VArray<T>::ForSingle(result, domain_size);
|
||||
}
|
||||
else {
|
||||
Map<int, T> results;
|
||||
for (const int i : values.index_range()) {
|
||||
T &value = results.lookup_or_add(group_indices[i], MinMaxInfo<T>::min_initial_value);
|
||||
value = math::min(value, values[i]);
|
||||
}
|
||||
Array<T> outputs(domain_size);
|
||||
for (const int i : values.index_range()) {
|
||||
outputs[i] = results.lookup(group_indices[i]);
|
||||
}
|
||||
g_outputs = VArray<T>::ForContainer(std::move(outputs));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (group_indices.is_single()) {
|
||||
T result = MinMaxInfo<T>::max_initial_value;
|
||||
for (const int i : values.index_range()) {
|
||||
result = math::max(result, values[i]);
|
||||
}
|
||||
g_outputs = VArray<T>::ForSingle(result, domain_size);
|
||||
}
|
||||
else {
|
||||
Map<int, T> results;
|
||||
for (const int i : values.index_range()) {
|
||||
T &value = results.lookup_or_add(group_indices[i], MinMaxInfo<T>::max_initial_value);
|
||||
value = math::max(value, values[i]);
|
||||
}
|
||||
Array<T> outputs(domain_size);
|
||||
for (const int i : values.index_range()) {
|
||||
outputs[i] = results.lookup(group_indices[i]);
|
||||
}
|
||||
g_outputs = VArray<T>::ForContainer(std::move(outputs));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return attributes.adapt_domain(std::move(g_outputs), source_domain_, context.domain());
|
||||
}
|
||||
|
||||
uint64_t hash() const override
|
||||
{
|
||||
return get_default_hash(input_, group_index_, source_domain_, operation_);
|
||||
}
|
||||
|
||||
bool is_equal_to(const fn::FieldNode &other) const override
|
||||
{
|
||||
if (const FieldMinMaxInput *other_field = dynamic_cast<const FieldMinMaxInput *>(&other)) {
|
||||
return input_ == other_field->input_ && group_index_ == other_field->group_index_ &&
|
||||
source_domain_ == other_field->source_domain_ &&
|
||||
operation_ == other_field->operation_;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<AttrDomain> preferred_domain(
|
||||
const GeometryComponent & /*component*/) const override
|
||||
{
|
||||
return source_domain_;
|
||||
}
|
||||
};
|
||||
|
||||
static void node_geo_exec(GeoNodeExecParams params)
|
||||
{
|
||||
const AttrDomain source_domain = AttrDomain(params.node().custom2);
|
||||
|
||||
const Field<int> group_index_field = params.extract_input<Field<int>>("Group Index");
|
||||
const GField input_field = params.extract_input<GField>("Value");
|
||||
if (params.output_is_required("Min")) {
|
||||
params.set_output<GField>("Min",
|
||||
GField{std::make_shared<FieldMinMaxInput>(
|
||||
source_domain, input_field, group_index_field, Operation::Min)});
|
||||
}
|
||||
if (params.output_is_required("Max")) {
|
||||
params.set_output<GField>("Max",
|
||||
GField{std::make_shared<FieldMinMaxInput>(
|
||||
source_domain, input_field, group_index_field, Operation::Max)});
|
||||
}
|
||||
}
|
||||
|
||||
static void node_rna(StructRNA *srna)
|
||||
{
|
||||
static EnumPropertyItem items[] = {
|
||||
{CD_PROP_FLOAT, "FLOAT", 0, "Float", "Floating-point value"},
|
||||
{CD_PROP_INT32, "INT", 0, "Integer", "32-bit integer"},
|
||||
{CD_PROP_FLOAT3, "FLOAT_VECTOR", 0, "Vector", "3D vector with floating-point values"},
|
||||
{0, nullptr, 0, nullptr, nullptr},
|
||||
};
|
||||
|
||||
RNA_def_node_enum(srna,
|
||||
"data_type",
|
||||
"Data Type",
|
||||
"Type of data the outputs are calculated from",
|
||||
items,
|
||||
NOD_inline_enum_accessors(custom1),
|
||||
CD_PROP_FLOAT);
|
||||
|
||||
RNA_def_node_enum(srna,
|
||||
"domain",
|
||||
"Domain",
|
||||
"",
|
||||
rna_enum_attribute_domain_items,
|
||||
NOD_inline_enum_accessors(custom2),
|
||||
int(AttrDomain::Point),
|
||||
nullptr,
|
||||
true);
|
||||
}
|
||||
|
||||
static void node_register()
|
||||
{
|
||||
static blender::bke::bNodeType ntype;
|
||||
|
||||
geo_node_type_base(&ntype, "GeometryNodeFieldMinAndMax");
|
||||
ntype.ui_name = "Field Min & Max";
|
||||
ntype.ui_description = "Calculate the minimum and maximum of a given field";
|
||||
ntype.nclass = NODE_CLASS_CONVERTER;
|
||||
ntype.geometry_node_execute = node_geo_exec;
|
||||
ntype.initfunc = node_init;
|
||||
ntype.draw_buttons = node_layout;
|
||||
ntype.declare = node_declare;
|
||||
ntype.gather_link_search_ops = node_gather_link_searches;
|
||||
blender::bke::node_register_type(ntype);
|
||||
node_rna(ntype.rna_ext.srna);
|
||||
}
|
||||
NOD_REGISTER_NODE(node_register)
|
||||
|
||||
} // namespace blender::nodes::node_geo_field_min_and_max_cc
|
||||
334
source/blender/nodes/geometry/nodes/node_geo_field_variance.cc
Normal file
334
source/blender/nodes/geometry/nodes/node_geo_field_variance.cc
Normal file
@@ -0,0 +1,334 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BKE_attribute_math.hh"
|
||||
|
||||
#include "BLI_array.hh"
|
||||
#include "BLI_generic_virtual_array.hh"
|
||||
#include "BLI_virtual_array.hh"
|
||||
|
||||
#include "NOD_rna_define.hh"
|
||||
#include "NOD_socket_search_link.hh"
|
||||
|
||||
#include "RNA_enum_types.hh"
|
||||
|
||||
#include "node_geometry_util.hh"
|
||||
|
||||
#include "UI_interface.hh"
|
||||
#include "UI_resources.hh"
|
||||
|
||||
#include <numeric>
|
||||
|
||||
namespace blender::nodes::node_geo_field_variance_cc {
|
||||
|
||||
static void node_declare(NodeDeclarationBuilder &b)
|
||||
{
|
||||
const bNode *node = b.node_or_null();
|
||||
|
||||
if (node != nullptr) {
|
||||
const eCustomDataType data_type = eCustomDataType(node->custom1);
|
||||
b.add_input(data_type, "Value")
|
||||
.supports_field()
|
||||
.description("The values the standard deviation and variance will be calculated from");
|
||||
}
|
||||
|
||||
b.add_input<decl::Int>("Group ID", "Group Index")
|
||||
.supports_field()
|
||||
.hide_value()
|
||||
.description("An index used to group values together for multiple separate operations");
|
||||
|
||||
if (node != nullptr) {
|
||||
const eCustomDataType data_type = eCustomDataType(node->custom1);
|
||||
b.add_output(data_type, "Standard Deviation")
|
||||
.field_source_reference_all()
|
||||
.description("The square root of the variance for each group");
|
||||
b.add_output(data_type, "Variance")
|
||||
.field_source_reference_all()
|
||||
.description("The expected squared deviation from the mean for each group");
|
||||
}
|
||||
}
|
||||
|
||||
static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
|
||||
{
|
||||
uiItemR(layout, ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE);
|
||||
uiItemR(layout, ptr, "domain", UI_ITEM_NONE, "", ICON_NONE);
|
||||
}
|
||||
|
||||
static void node_init(bNodeTree * /*tree*/, bNode *node)
|
||||
{
|
||||
node->custom1 = CD_PROP_FLOAT;
|
||||
node->custom2 = int16_t(AttrDomain::Point);
|
||||
}
|
||||
|
||||
enum class Operation { StdDev = 0, Variance = 1 };
|
||||
|
||||
static std::optional<eCustomDataType> node_type_from_other_socket(const bNodeSocket &socket)
|
||||
{
|
||||
switch (socket.type) {
|
||||
case SOCK_FLOAT:
|
||||
case SOCK_BOOLEAN:
|
||||
case SOCK_INT:
|
||||
return CD_PROP_FLOAT;
|
||||
case SOCK_VECTOR:
|
||||
case SOCK_RGBA:
|
||||
return CD_PROP_FLOAT3;
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
static void node_gather_link_searches(GatherLinkSearchOpParams ¶ms)
|
||||
{
|
||||
const NodeDeclaration &declaration = *params.node_type().static_declaration;
|
||||
search_link_ops_for_declarations(params, declaration.inputs);
|
||||
|
||||
const std::optional<eCustomDataType> type = node_type_from_other_socket(params.other_socket());
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
if (params.in_out() == SOCK_OUT) {
|
||||
params.add_item(
|
||||
IFACE_("Standard Deviation"),
|
||||
[type](LinkSearchOpParams ¶ms) {
|
||||
bNode &node = params.add_node("GeometryNodeFieldVariance");
|
||||
node.custom1 = *type;
|
||||
params.update_and_connect_available_socket(node, "Standard Deviation");
|
||||
},
|
||||
0);
|
||||
params.add_item(
|
||||
IFACE_("Variance"),
|
||||
[type](LinkSearchOpParams ¶ms) {
|
||||
bNode &node = params.add_node("GeometryNodeFieldVariance");
|
||||
node.custom1 = *type;
|
||||
params.update_and_connect_available_socket(node, "Variance");
|
||||
},
|
||||
-1);
|
||||
}
|
||||
else {
|
||||
params.add_item(
|
||||
IFACE_("Value"),
|
||||
[type](LinkSearchOpParams ¶ms) {
|
||||
bNode &node = params.add_node("GeometryNodeFieldVariance");
|
||||
node.custom1 = *type;
|
||||
params.update_and_connect_available_socket(node, "Value");
|
||||
},
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
class FieldVarianceInput final : public bke::GeometryFieldInput {
|
||||
private:
|
||||
GField input_;
|
||||
Field<int> group_index_;
|
||||
AttrDomain source_domain_;
|
||||
Operation operation_;
|
||||
|
||||
public:
|
||||
FieldVarianceInput(const AttrDomain source_domain,
|
||||
GField input,
|
||||
Field<int> group_index,
|
||||
Operation operation)
|
||||
: bke::GeometryFieldInput(input.cpp_type(), "Calculation"),
|
||||
input_(std::move(input)),
|
||||
group_index_(std::move(group_index)),
|
||||
source_domain_(source_domain),
|
||||
operation_(operation)
|
||||
{
|
||||
}
|
||||
|
||||
GVArray get_varray_for_context(const bke::GeometryFieldContext &context,
|
||||
const IndexMask & /*mask*/) const final
|
||||
{
|
||||
const AttributeAccessor attributes = *context.attributes();
|
||||
const int64_t domain_size = attributes.domain_size(source_domain_);
|
||||
if (domain_size == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const bke::GeometryFieldContext source_context{context, source_domain_};
|
||||
fn::FieldEvaluator evaluator{source_context, domain_size};
|
||||
evaluator.add(input_);
|
||||
evaluator.add(group_index_);
|
||||
evaluator.evaluate();
|
||||
const GVArray g_values = evaluator.get_evaluated(0);
|
||||
const VArray<int> group_indices = evaluator.get_evaluated<int>(1);
|
||||
|
||||
GVArray g_outputs;
|
||||
|
||||
bke::attribute_math::convert_to_static_type(g_values.type(), [&](auto dummy) {
|
||||
using T = decltype(dummy);
|
||||
if constexpr (is_same_any_v<T, int, float, float3>) {
|
||||
const VArraySpan<T> values = g_values.typed<T>();
|
||||
|
||||
if (operation_ == Operation::StdDev) {
|
||||
if (group_indices.is_single()) {
|
||||
const T mean = std::reduce(values.begin(), values.end(), T()) / domain_size;
|
||||
const T sum_of_squared_diffs = std::reduce(
|
||||
values.begin(), values.end(), T(), [mean](T accumulator, const T &value) {
|
||||
T difference = mean - value;
|
||||
return accumulator + difference * difference;
|
||||
});
|
||||
g_outputs = VArray<T>::ForSingle(math::sqrt(sum_of_squared_diffs / domain_size),
|
||||
domain_size);
|
||||
}
|
||||
else {
|
||||
Map<int, std::pair<T, int>> sum_and_counts;
|
||||
Map<int, T> deviations;
|
||||
|
||||
for (const int i : values.index_range()) {
|
||||
auto &pair = sum_and_counts.lookup_or_add(group_indices[i], std::make_pair(T(), 0));
|
||||
pair.first = pair.first + values[i];
|
||||
pair.second = pair.second + 1;
|
||||
}
|
||||
|
||||
for (const int i : values.index_range()) {
|
||||
const auto &pair = sum_and_counts.lookup(group_indices[i]);
|
||||
T mean = pair.first / pair.second;
|
||||
T deviation = (mean - values[i]);
|
||||
deviation = deviation * deviation;
|
||||
|
||||
T &dev_sum = deviations.lookup_or_add(group_indices[i], T());
|
||||
dev_sum = dev_sum + deviation;
|
||||
}
|
||||
|
||||
Array<T> outputs(domain_size);
|
||||
for (const int i : values.index_range()) {
|
||||
const auto &pair = sum_and_counts.lookup(group_indices[i]);
|
||||
outputs[i] = math::sqrt(deviations.lookup(group_indices[i]) / pair.second);
|
||||
}
|
||||
g_outputs = VArray<T>::ForContainer(std::move(outputs));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (group_indices.is_single()) {
|
||||
const T mean = std::reduce(values.begin(), values.end(), T()) / domain_size;
|
||||
const T sum_of_squared_diffs = std::reduce(
|
||||
values.begin(), values.end(), T(), [mean](T accumulator, const T &value) {
|
||||
T difference = mean - value;
|
||||
return accumulator + difference * difference;
|
||||
});
|
||||
g_outputs = VArray<T>::ForSingle(sum_of_squared_diffs / domain_size, domain_size);
|
||||
}
|
||||
else {
|
||||
Map<int, std::pair<T, int>> sum_and_counts;
|
||||
Map<int, T> deviations;
|
||||
|
||||
for (const int i : values.index_range()) {
|
||||
auto &pair = sum_and_counts.lookup_or_add(group_indices[i], std::make_pair(T(), 0));
|
||||
pair.first = pair.first + values[i];
|
||||
pair.second = pair.second + 1;
|
||||
}
|
||||
|
||||
for (const int i : values.index_range()) {
|
||||
const auto &pair = sum_and_counts.lookup(group_indices[i]);
|
||||
T mean = pair.first / pair.second;
|
||||
T deviation = (mean - values[i]);
|
||||
deviation = deviation * deviation;
|
||||
|
||||
T &dev_sum = deviations.lookup_or_add(group_indices[i], T());
|
||||
dev_sum = dev_sum + deviation;
|
||||
}
|
||||
|
||||
Array<T> outputs(domain_size);
|
||||
for (const int i : values.index_range()) {
|
||||
const auto &pair = sum_and_counts.lookup(group_indices[i]);
|
||||
outputs[i] = deviations.lookup(group_indices[i]) / pair.second;
|
||||
}
|
||||
g_outputs = VArray<T>::ForContainer(std::move(outputs));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return attributes.adapt_domain(std::move(g_outputs), source_domain_, context.domain());
|
||||
}
|
||||
|
||||
uint64_t hash() const override
|
||||
{
|
||||
return get_default_hash(input_, group_index_, source_domain_, operation_);
|
||||
}
|
||||
|
||||
bool is_equal_to(const fn::FieldNode &other) const override
|
||||
{
|
||||
if (const FieldVarianceInput *other_field = dynamic_cast<const FieldVarianceInput *>(&other)) {
|
||||
return input_ == other_field->input_ && group_index_ == other_field->group_index_ &&
|
||||
source_domain_ == other_field->source_domain_ &&
|
||||
operation_ == other_field->operation_;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<AttrDomain> preferred_domain(
|
||||
const GeometryComponent & /*component*/) const override
|
||||
{
|
||||
return source_domain_;
|
||||
}
|
||||
};
|
||||
|
||||
static void node_geo_exec(GeoNodeExecParams params)
|
||||
{
|
||||
const AttrDomain source_domain = AttrDomain(params.node().custom2);
|
||||
|
||||
const Field<int> group_index_field = params.extract_input<Field<int>>("Group Index");
|
||||
const GField input_field = params.extract_input<GField>("Value");
|
||||
if (params.output_is_required("Standard Deviation")) {
|
||||
params.set_output<GField>(
|
||||
"Standard Deviation",
|
||||
GField{std::make_shared<FieldVarianceInput>(
|
||||
source_domain, input_field, group_index_field, Operation::StdDev)});
|
||||
}
|
||||
if (params.output_is_required("Variance")) {
|
||||
params.set_output<GField>(
|
||||
"Variance",
|
||||
GField{std::make_shared<FieldVarianceInput>(
|
||||
source_domain, input_field, group_index_field, Operation::Variance)});
|
||||
}
|
||||
}
|
||||
|
||||
static void node_rna(StructRNA *srna)
|
||||
{
|
||||
static EnumPropertyItem items[] = {
|
||||
{CD_PROP_FLOAT, "FLOAT", 0, "Float", "Floating-point value"},
|
||||
{CD_PROP_FLOAT3, "FLOAT_VECTOR", 0, "Vector", "3D vector with floating-point values"},
|
||||
{0, nullptr, 0, nullptr, nullptr},
|
||||
};
|
||||
|
||||
RNA_def_node_enum(srna,
|
||||
"data_type",
|
||||
"Data Type",
|
||||
"Type of data the outputs are calculated from",
|
||||
items,
|
||||
NOD_inline_enum_accessors(custom1),
|
||||
CD_PROP_FLOAT);
|
||||
|
||||
RNA_def_node_enum(srna,
|
||||
"domain",
|
||||
"Domain",
|
||||
"",
|
||||
rna_enum_attribute_domain_items,
|
||||
NOD_inline_enum_accessors(custom2),
|
||||
int(AttrDomain::Point),
|
||||
nullptr,
|
||||
true);
|
||||
}
|
||||
|
||||
static void node_register()
|
||||
{
|
||||
static blender::bke::bNodeType ntype;
|
||||
|
||||
geo_node_type_base(&ntype, "GeometryNodeFieldVariance");
|
||||
ntype.ui_name = "Field Variance";
|
||||
ntype.ui_description = "Calculate the standard deviation and variance of a given field";
|
||||
ntype.nclass = NODE_CLASS_CONVERTER;
|
||||
ntype.geometry_node_execute = node_geo_exec;
|
||||
ntype.initfunc = node_init;
|
||||
ntype.draw_buttons = node_layout;
|
||||
ntype.declare = node_declare;
|
||||
ntype.gather_link_search_ops = node_gather_link_searches;
|
||||
blender::bke::node_register_type(ntype);
|
||||
node_rna(ntype.rna_ext.srna);
|
||||
}
|
||||
NOD_REGISTER_NODE(node_register)
|
||||
|
||||
} // namespace blender::nodes::node_geo_field_variance_cc
|
||||
Reference in New Issue
Block a user