From d034b85f3325379c40981502e44a07e5ef4c7d14 Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Fri, 21 Jan 2022 17:34:47 +0100 Subject: Geometry Nodes: new Scale Elements nodes This node can scale individual edges and faces. When multiple selected faces/edges share the same vertices, they are scaled together. The center and scaling factor is averaged in this case. For some examples see D13757. Differential Revision: https://developer.blender.org/D13757 --- source/blender/nodes/NOD_geometry.h | 1 + source/blender/nodes/NOD_static_types.h | 1 + source/blender/nodes/geometry/CMakeLists.txt | 1 + .../geometry/nodes/node_geo_scale_elements.cc | 485 +++++++++++++++++++++ 4 files changed, 488 insertions(+) create mode 100644 source/blender/nodes/geometry/nodes/node_geo_scale_elements.cc (limited to 'source/blender/nodes') diff --git a/source/blender/nodes/NOD_geometry.h b/source/blender/nodes/NOD_geometry.h index ff4036308c6..9c9e3876662 100644 --- a/source/blender/nodes/NOD_geometry.h +++ b/source/blender/nodes/NOD_geometry.h @@ -156,6 +156,7 @@ void register_node_type_geo_raycast(void); void register_node_type_geo_realize_instances(void); void register_node_type_geo_rotate_instances(void); void register_node_type_geo_sample_texture(void); +void register_node_type_geo_scale_elements(void); void register_node_type_geo_scale_instances(void); void register_node_type_geo_select_by_handle_type(void); void register_node_type_geo_separate_components(void); diff --git a/source/blender/nodes/NOD_static_types.h b/source/blender/nodes/NOD_static_types.h index 53d6d64ef84..06a1cf90fca 100644 --- a/source/blender/nodes/NOD_static_types.h +++ b/source/blender/nodes/NOD_static_types.h @@ -407,6 +407,7 @@ DefNode(GeometryNode, GEO_NODE_RESAMPLE_CURVE, def_geo_curve_resample, "RESAMPLE DefNode(GeometryNode, GEO_NODE_REVERSE_CURVE, 0, "REVERSE_CURVE", ReverseCurve, "Reverse Curve", "") DefNode(GeometryNode, GEO_NODE_ROTATE_INSTANCES, 0, "ROTATE_INSTANCES", RotateInstances, "Rotate Instances", "") DefNode(GeometryNode, GEO_NODE_SAMPLE_CURVE, def_geo_curve_sample, "SAMPLE_CURVE", SampleCurve, "Sample Curve", "") +DefNode(GeometryNode, GEO_NODE_SCALE_ELEMENTS, def_geo_scale_elements, "SCALE_ELEMENTS", ScaleElements, "Scale Elements", "") DefNode(GeometryNode, GEO_NODE_SCALE_INSTANCES, 0, "SCALE_INSTANCES", ScaleInstances, "Scale Instances", "") DefNode(GeometryNode, GEO_NODE_SEPARATE_COMPONENTS, 0, "SEPARATE_COMPONENTS", SeparateComponents, "Separate Components", "") DefNode(GeometryNode, GEO_NODE_SEPARATE_GEOMETRY, def_geo_separate_geometry, "SEPARATE_GEOMETRY", SeparateGeometry, "Separate Geometry", "") diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index 37d7843020a..37b43c26a86 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -167,6 +167,7 @@ set(SRC nodes/node_geo_raycast.cc nodes/node_geo_realize_instances.cc nodes/node_geo_rotate_instances.cc + nodes/node_geo_scale_elements.cc nodes/node_geo_scale_instances.cc nodes/node_geo_separate_components.cc nodes/node_geo_separate_geometry.cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_scale_elements.cc b/source/blender/nodes/geometry/nodes/node_geo_scale_elements.cc new file mode 100644 index 00000000000..fd599b7ca61 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_scale_elements.cc @@ -0,0 +1,485 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "BLI_array.hh" +#include "BLI_disjoint_set.hh" +#include "BLI_task.hh" +#include "BLI_vector.hh" +#include "BLI_vector_set.hh" + +#include "DNA_mesh_types.h" +#include "DNA_meshdata_types.h" + +#include "UI_interface.h" +#include "UI_resources.h" + +#include "BKE_mesh.h" + +#include "node_geometry_util.hh" + +namespace blender::nodes::node_geo_scale_elements_cc { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.add_input(N_("Geometry")).supported_type(GEO_COMPONENT_TYPE_MESH); + b.add_input(N_("Selection")).default_value(true).hide_value().supports_field(); + b.add_input(N_("Scale"), "Scale").default_value(1.0f).min(0.0f).supports_field(); + b.add_input(N_("Center")) + .subtype(PROP_TRANSLATION) + .implicit_field() + .description(N_("Origin of the scaling for each element. If multiple elements are " + "connected, their center is averaged")); + b.add_input(N_("Axis")) + .default_value({1.0f, 0.0f, 0.0f}) + .supports_field() + .description(N_("Direction in which to scale the element")); + b.add_output(N_("Geometry")); +}; + +static void node_layout(uiLayout *layout, bContext *UNUSED(C), PointerRNA *ptr) +{ + uiItemR(layout, ptr, "domain", 0, "", ICON_NONE); + uiItemR(layout, ptr, "scale_mode", 0, "", ICON_NONE); +} + +static void node_init(bNodeTree *UNUSED(tree), bNode *node) +{ + node->custom1 = ATTR_DOMAIN_FACE; + node->custom2 = GEO_NODE_SCALE_ELEMENTS_UNIFORM; +} + +static void node_update(bNodeTree *ntree, bNode *node) +{ + bNodeSocket *geometry_socket = static_cast(node->inputs.first); + bNodeSocket *selection_socket = geometry_socket->next; + bNodeSocket *scale_float_socket = selection_socket->next; + bNodeSocket *center_socket = scale_float_socket->next; + bNodeSocket *axis_socket = center_socket->next; + + const GeometryNodeScaleElementsMode mode = static_cast( + node->custom2); + const bool use_single_axis = mode == GEO_NODE_SCALE_ELEMENTS_SINGLE_AXIS; + + nodeSetSocketAvailability(ntree, axis_socket, use_single_axis); +} + +struct UniformScaleFields { + Field selection; + Field scale; + Field center; +}; + +struct UniformScaleParams { + IndexMask selection; + VArray scales; + VArray centers; +}; + +struct AxisScaleFields { + Field selection; + Field scale; + Field center; + Field axis; +}; + +struct AxisScaleParams { + IndexMask selection; + VArray scales; + VArray centers; + VArray axis_vectors; +}; + +/** + * When multiple elements share the same vertices, they are scaled together. + */ +struct ElementIsland { + /* Either face or edge indices. */ + Vector element_indices; +}; + +static float3 transform_with_uniform_scale(const float3 &position, + const float3 ¢er, + const float scale) +{ + const float3 diff = position - center; + const float3 scaled_diff = scale * diff; + const float3 new_position = center + scaled_diff; + return new_position; +} + +static float4x4 create_single_axis_transform(const float3 ¢er, + const float3 &axis, + const float scale) +{ + /* Scale along x axis. The other axis need to be orthogonal, but their specific value does not + * matter. */ + const float3 x_axis = math::normalize(axis); + float3 y_axis = math::cross(x_axis, float3(0.0f, 0.0f, 1.0f)); + if (math::is_zero(y_axis)) { + y_axis = math::cross(x_axis, float3(0.0f, 1.0f, 0.0f)); + } + y_axis = math::normalize(y_axis); + const float3 z_axis = math::cross(x_axis, y_axis); + + float4x4 transform = float4x4::identity(); + + /* Move scaling center to the origin. */ + sub_v3_v3(transform.values[3], center); + + /* `base_change` and `base_change_inv` are used to rotate space so that scaling along the + * provided axis is the same as scaling along the x axis. */ + float4x4 base_change = float4x4::identity(); + copy_v3_v3(base_change.values[0], x_axis); + copy_v3_v3(base_change.values[1], y_axis); + copy_v3_v3(base_change.values[2], z_axis); + + /* Can invert by transposing, because the matrix is orthonormal. */ + float4x4 base_change_inv = base_change.transposed(); + + float4x4 scale_transform = float4x4::identity(); + scale_transform.values[0][0] = scale; + + transform = base_change * scale_transform * base_change_inv * transform; + + /* Move scaling center back to where it was. */ + add_v3_v3(transform.values[3], center); + + return transform; +} + +using GetVertexIndicesFn = + FunctionRef &r_vertex_indices)>; + +static void scale_vertex_islands_uniformly(Mesh &mesh, + const Span islands, + const UniformScaleParams ¶ms, + const GetVertexIndicesFn get_vertex_indices) +{ + threading::parallel_for(islands.index_range(), 256, [&](const IndexRange range) { + for (const int island_index : range) { + const ElementIsland &island = islands[island_index]; + + float scale = 0.0f; + float3 center = {0.0f, 0.0f, 0.0f}; + + VectorSet vertex_indices; + for (const int poly_index : island.element_indices) { + get_vertex_indices(mesh, poly_index, vertex_indices); + center += params.centers[poly_index]; + scale += params.scales[poly_index]; + } + + /* Divide by number of elements to get the average. */ + const float f = 1.0f / island.element_indices.size(); + scale *= f; + center *= f; + + for (const int vert_index : vertex_indices) { + MVert &vert = mesh.mvert[vert_index]; + const float3 old_position = vert.co; + const float3 new_position = transform_with_uniform_scale(old_position, center, scale); + copy_v3_v3(vert.co, new_position); + } + } + }); + + /* Positions have changed, so the normals will have to be recomputed. */ + BKE_mesh_normals_tag_dirty(&mesh); +} + +static void scale_vertex_islands_on_axis(Mesh &mesh, + const Span islands, + const AxisScaleParams ¶ms, + const GetVertexIndicesFn get_vertex_indices) +{ + threading::parallel_for(islands.index_range(), 256, [&](const IndexRange range) { + for (const int island_index : range) { + const ElementIsland &island = islands[island_index]; + + float scale = 0.0f; + float3 center = {0.0f, 0.0f, 0.0f}; + float3 axis = {0.0f, 0.0f, 0.0f}; + + VectorSet vertex_indices; + for (const int poly_index : island.element_indices) { + get_vertex_indices(mesh, poly_index, vertex_indices); + center += params.centers[poly_index]; + scale += params.scales[poly_index]; + axis += params.axis_vectors[poly_index]; + } + + /* Divide by number of elements to get the average. */ + const float f = 1.0f / island.element_indices.size(); + scale *= f; + center *= f; + axis *= f; + + if (math::is_zero(axis)) { + axis = float3(1.0f, 0.0f, 0.0f); + } + + const float4x4 transform = create_single_axis_transform(center, axis, scale); + for (const int vert_index : vertex_indices) { + MVert &vert = mesh.mvert[vert_index]; + const float3 old_position = vert.co; + const float3 new_position = transform * old_position; + copy_v3_v3(vert.co, new_position); + } + } + }); + + /* Positions have changed, so the normals will have to be recomputed. */ + BKE_mesh_normals_tag_dirty(&mesh); +} + +static Vector prepare_face_islands(const Mesh &mesh, const IndexMask face_selection) +{ + /* Use the disjoint set data structure to determine which vertices have to be scaled together. */ + DisjointSet disjoint_set(mesh.totvert); + for (const int poly_index : face_selection) { + const MPoly &poly = mesh.mpoly[poly_index]; + const Span poly_loops{mesh.mloop + poly.loopstart, poly.totloop}; + for (const int loop_index : IndexRange(poly.totloop - 1)) { + const int v1 = poly_loops[loop_index].v; + const int v2 = poly_loops[loop_index + 1].v; + disjoint_set.join(v1, v2); + } + disjoint_set.join(poly_loops.first().v, poly_loops.last().v); + } + + VectorSet island_ids; + Vector islands; + /* There are at most as many islands as there are selected faces. */ + islands.reserve(face_selection.size()); + + /* Gather all of the face indices in each island into separate vectors. */ + for (const int poly_index : face_selection) { + const MPoly &poly = mesh.mpoly[poly_index]; + const Span poly_loops{mesh.mloop + poly.loopstart, poly.totloop}; + const int island_id = disjoint_set.find_root(poly_loops[0].v); + const int island_index = island_ids.index_of_or_add(island_id); + if (island_index == islands.size()) { + islands.append_as(); + } + ElementIsland &island = islands[island_index]; + island.element_indices.append(poly_index); + } + + return islands; +} + +static void get_face_vertices(const Mesh &mesh, int face_index, VectorSet &r_vertex_indices) +{ + const MPoly &poly = mesh.mpoly[face_index]; + const Span poly_loops{mesh.mloop + poly.loopstart, poly.totloop}; + for (const MLoop &loop : poly_loops) { + r_vertex_indices.add(loop.v); + } +} + +static AxisScaleParams evaluate_axis_scale_fields(FieldEvaluator &evaluator, + const AxisScaleFields &fields) +{ + AxisScaleParams out; + evaluator.set_selection(fields.selection); + evaluator.add(fields.scale, &out.scales); + evaluator.add(fields.center, &out.centers); + evaluator.add(fields.axis, &out.axis_vectors); + evaluator.evaluate(); + out.selection = evaluator.get_evaluated_selection_as_mask(); + return out; +} + +static void scale_faces_on_axis(MeshComponent &mesh_component, const AxisScaleFields &fields) +{ + Mesh &mesh = *mesh_component.get_for_write(); + mesh.mvert = static_cast( + CustomData_duplicate_referenced_layer(&mesh.vdata, CD_MVERT, mesh.totvert)); + + GeometryComponentFieldContext field_context{mesh_component, ATTR_DOMAIN_FACE}; + FieldEvaluator evaluator{field_context, mesh.totpoly}; + AxisScaleParams params = evaluate_axis_scale_fields(evaluator, fields); + + Vector island = prepare_face_islands(mesh, params.selection); + scale_vertex_islands_on_axis(mesh, island, params, get_face_vertices); +} + +static UniformScaleParams evaluate_uniform_scale_fields(FieldEvaluator &evaluator, + const UniformScaleFields &fields) +{ + UniformScaleParams out; + evaluator.set_selection(fields.selection); + evaluator.add(fields.scale, &out.scales); + evaluator.add(fields.center, &out.centers); + evaluator.evaluate(); + out.selection = evaluator.get_evaluated_selection_as_mask(); + return out; +} + +static void scale_faces_uniformly(MeshComponent &mesh_component, const UniformScaleFields &fields) +{ + Mesh &mesh = *mesh_component.get_for_write(); + mesh.mvert = static_cast( + CustomData_duplicate_referenced_layer(&mesh.vdata, CD_MVERT, mesh.totvert)); + + GeometryComponentFieldContext field_context{mesh_component, ATTR_DOMAIN_FACE}; + FieldEvaluator evaluator{field_context, mesh.totpoly}; + UniformScaleParams params = evaluate_uniform_scale_fields(evaluator, fields); + + Vector island = prepare_face_islands(mesh, params.selection); + scale_vertex_islands_uniformly(mesh, island, params, get_face_vertices); +} + +static Vector prepare_edge_islands(const Mesh &mesh, const IndexMask edge_selection) +{ + /* Use the disjoing set data structure to determine which vertices have to be scaled together. */ + DisjointSet disjoint_set(mesh.totvert); + for (const int edge_index : edge_selection) { + const MEdge &edge = mesh.medge[edge_index]; + disjoint_set.join(edge.v1, edge.v2); + } + + VectorSet island_ids; + Vector islands; + /* There are at most as many islands as there are selected edges. */ + islands.reserve(edge_selection.size()); + + /* Gather all of the edge indices in each island into separate vectors. */ + for (const int edge_index : edge_selection) { + const MEdge &edge = mesh.medge[edge_index]; + const int island_id = disjoint_set.find_root(edge.v1); + const int island_index = island_ids.index_of_or_add(island_id); + if (island_index == islands.size()) { + islands.append_as(); + } + ElementIsland &island = islands[island_index]; + island.element_indices.append(edge_index); + } + + return islands; +} + +static void get_edge_vertices(const Mesh &mesh, int edge_index, VectorSet &r_vertex_indices) +{ + const MEdge &edge = mesh.medge[edge_index]; + r_vertex_indices.add(edge.v1); + r_vertex_indices.add(edge.v2); +} + +static void scale_edges_uniformly(MeshComponent &mesh_component, const UniformScaleFields &fields) +{ + Mesh &mesh = *mesh_component.get_for_write(); + mesh.mvert = static_cast( + CustomData_duplicate_referenced_layer(&mesh.vdata, CD_MVERT, mesh.totvert)); + + GeometryComponentFieldContext field_context{mesh_component, ATTR_DOMAIN_EDGE}; + FieldEvaluator evaluator{field_context, mesh.totedge}; + UniformScaleParams params = evaluate_uniform_scale_fields(evaluator, fields); + + Vector island = prepare_edge_islands(mesh, params.selection); + scale_vertex_islands_uniformly(mesh, island, params, get_edge_vertices); +} + +static void scale_edges_on_axis(MeshComponent &mesh_component, const AxisScaleFields &fields) +{ + Mesh &mesh = *mesh_component.get_for_write(); + mesh.mvert = static_cast( + CustomData_duplicate_referenced_layer(&mesh.vdata, CD_MVERT, mesh.totvert)); + + GeometryComponentFieldContext field_context{mesh_component, ATTR_DOMAIN_EDGE}; + FieldEvaluator evaluator{field_context, mesh.totedge}; + AxisScaleParams params = evaluate_axis_scale_fields(evaluator, fields); + + Vector island = prepare_edge_islands(mesh, params.selection); + scale_vertex_islands_on_axis(mesh, island, params, get_edge_vertices); +} + +static void node_geo_exec(GeoNodeExecParams params) +{ + const bNode &node = params.node(); + const AttributeDomain domain = static_cast(node.custom1); + const GeometryNodeScaleElementsMode scale_mode = static_cast( + node.custom2); + + GeometrySet geometry = params.extract_input("Geometry"); + + Field selection_field = params.get_input>("Selection"); + Field scale_field = params.get_input>("Scale"); + Field center_field = params.get_input>("Center"); + Field axis_field; + if (scale_mode == GEO_NODE_SCALE_ELEMENTS_SINGLE_AXIS) { + axis_field = params.get_input>("Axis"); + } + + geometry.modify_geometry_sets([&](GeometrySet &geometry) { + if (!geometry.has_mesh()) { + return; + } + MeshComponent &mesh_component = geometry.get_component_for_write(); + switch (domain) { + case ATTR_DOMAIN_FACE: { + switch (scale_mode) { + case GEO_NODE_SCALE_ELEMENTS_UNIFORM: { + scale_faces_uniformly(mesh_component, {selection_field, scale_field, center_field}); + break; + } + case GEO_NODE_SCALE_ELEMENTS_SINGLE_AXIS: { + scale_faces_on_axis(mesh_component, + {selection_field, scale_field, center_field, axis_field}); + break; + } + } + break; + } + case ATTR_DOMAIN_EDGE: { + switch (scale_mode) { + case GEO_NODE_SCALE_ELEMENTS_UNIFORM: { + scale_edges_uniformly(mesh_component, {selection_field, scale_field, center_field}); + break; + } + case GEO_NODE_SCALE_ELEMENTS_SINGLE_AXIS: { + scale_edges_on_axis(mesh_component, + {selection_field, scale_field, center_field, axis_field}); + break; + } + } + break; + } + default: + BLI_assert_unreachable(); + break; + } + }); + + params.set_output("Geometry", std::move(geometry)); +} + +} // namespace blender::nodes::node_geo_scale_elements_cc + +void register_node_type_geo_scale_elements() +{ + namespace file_ns = blender::nodes::node_geo_scale_elements_cc; + + static bNodeType ntype; + + geo_node_type_base(&ntype, GEO_NODE_SCALE_ELEMENTS, "Scale Elements", NODE_CLASS_GEOMETRY); + ntype.geometry_node_execute = file_ns::node_geo_exec; + ntype.declare = file_ns::node_declare; + ntype.draw_buttons = file_ns::node_layout; + ntype.initfunc = file_ns::node_init; + ntype.updatefunc = file_ns::node_update; + nodeRegisterType(&ntype); +} -- cgit v1.2.3