Geometry Nodes: Add "Scale" input to "Curve to Mesh" node

This replaces the implicit use of the `radius` attribute on the input
curves with an input field `Scale` that gets evaluated on the point
domain of the input curves to scale the profile.

It wasn't super intuitive that the `radius` would actually act as
a scale of the profile. E.g. if the radius of the input curve was
`1 meter` the resulting profile was unscaled (scaled by 1), but
wouldn't necessarily have a size of `1 meter` (only if the profile
also had a size of 1m)! If imperial units were used, `3.28084 ft`
would correspond to a scale of 1.

This change makes this behavior a lot more clear and potentially
removes the need for the assumption that the default curve radius
is `1.0f` (Ideally, the default curve radius should be `0.01f`).

While we did consider making the `Scale` input use the `radius`
field implicitly by default, we decided against it, because it again
"hides" the dependency on the radius and the fact that the radius
is used as a scale. Letting the user make this decision seems better.

Pull Request: https://projects.blender.org/blender/blender/pulls/134187
This commit is contained in:
Falk David
2025-03-11 19:06:47 +01:00
committed by Falk David
parent d2af128245
commit a92b68939a
5 changed files with 125 additions and 16 deletions

View File

@@ -27,7 +27,7 @@
/* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 3
#define BLENDER_FILE_SUBVERSION 4
/* Minimum Blender version that supports reading file written with the current
* version. Older Blender versions will test this and cancel loading the file, showing a warning to

View File

@@ -10,6 +10,8 @@ struct Mesh;
* \ingroup bke
*/
#include "BLI_virtual_array_fwd.hh"
#include "BKE_attribute_filter.hh"
namespace blender::bke {
@@ -28,6 +30,7 @@ class CurvesGeometry;
*/
Mesh *curve_to_mesh_sweep(const CurvesGeometry &main,
const CurvesGeometry &profile,
const VArray<float> &scales,
bool fill_caps,
const bke::AttributeFilter &attribute_filter = {});
/**

View File

@@ -189,15 +189,15 @@ static void fill_mesh_positions(const int main_point_num,
const Span<float3> profile_positions,
const Span<float3> tangents,
const Span<float3> normals,
const Span<float> radii,
const Span<float> scales,
MutableSpan<float3> mesh_positions)
{
if (profile_point_num == 1) {
for (const int i_ring : IndexRange(main_point_num)) {
float4x4 point_matrix = build_point_matrix(
main_positions[i_ring], normals[i_ring], tangents[i_ring]);
if (!radii.is_empty()) {
point_matrix = math::scale(point_matrix, float3(radii[i_ring]));
if (!scales.is_empty()) {
point_matrix = math::scale(point_matrix, float3(scales[i_ring]));
}
mesh_positions[i_ring] = math::transform_point(point_matrix, profile_positions.first());
}
@@ -206,8 +206,8 @@ static void fill_mesh_positions(const int main_point_num,
for (const int i_ring : IndexRange(main_point_num)) {
float4x4 point_matrix = build_point_matrix(
main_positions[i_ring], normals[i_ring], tangents[i_ring]);
if (!radii.is_empty()) {
point_matrix = math::scale(point_matrix, float3(radii[i_ring]));
if (!scales.is_empty()) {
point_matrix = math::scale(point_matrix, float3(scales[i_ring]));
}
const int ring_vert_start = i_ring * profile_point_num;
@@ -472,6 +472,7 @@ static void foreach_curve_combination(const CurvesInfo &info,
static void build_mesh_positions(const CurvesInfo &curves_info,
const ResultOffsets &offsets,
const VArray<float> &scales,
Vector<std::byte> &eval_buffer,
Mesh &mesh)
{
@@ -499,9 +500,9 @@ static void build_mesh_positions(const CurvesInfo &curves_info,
}
const Span<float3> tangents = curves_info.main.evaluated_tangents();
const Span<float3> normals = curves_info.main.evaluated_normals();
Span<float> radii_eval;
if (const GVArray radii = *curves_info.main.attributes().lookup("radius", AttrDomain::Point)) {
radii_eval = evaluate_attribute(radii, curves_info.main, eval_buffer).typed<float>();
Span<float> eval_scales;
if (!scales.is_empty() && scales.get_if_single() != 1.0f) {
eval_scales = evaluate_attribute(scales, curves_info.main, eval_buffer).typed<float>();
}
foreach_curve_combination(curves_info, offsets, [&](const CombinationInfo &info) {
fill_mesh_positions(info.main_points.size(),
@@ -510,7 +511,7 @@ static void build_mesh_positions(const CurvesInfo &curves_info,
profile_positions.slice(info.profile_points),
tangents.slice(info.main_points),
normals.slice(info.main_points),
radii_eval.is_empty() ? radii_eval : radii_eval.slice(info.main_points),
eval_scales.is_empty() ? eval_scales : eval_scales.slice(info.main_points),
positions.slice(info.vert_range));
});
}
@@ -811,6 +812,7 @@ static void write_sharp_bezier_edges(const CurvesInfo &curves_info,
Mesh *curve_to_mesh_sweep(const CurvesGeometry &main,
const CurvesGeometry &profile,
const VArray<float> &scales,
const bool fill_caps,
const AttributeFilter &attribute_filter)
{
@@ -871,7 +873,7 @@ Mesh *curve_to_mesh_sweep(const CurvesGeometry &main,
/* Make sure curve attributes can be interpolated. */
main.ensure_can_interpolate_to_evaluated();
build_mesh_positions(curves_info, offsets, eval_buffer, *mesh);
build_mesh_positions(curves_info, offsets, scales, eval_buffer, *mesh);
mesh->tag_overlapping_none();
if (!offsets.any_single_point_main) {
@@ -996,7 +998,7 @@ static CurvesGeometry get_curve_single_vert()
Mesh *curve_to_wire_mesh(const CurvesGeometry &curve, const AttributeFilter &attribute_filter)
{
static const CurvesGeometry vert_curve = get_curve_single_vert();
return curve_to_mesh_sweep(curve, vert_curve, false, attribute_filter);
return curve_to_mesh_sweep(curve, vert_curve, {}, false, attribute_filter);
}
} // namespace blender::bke

View File

@@ -3766,6 +3766,83 @@ static void version_geometry_normal_input_node(bNodeTree &ntree)
}
}
static void do_version_node_curve_to_mesh_scale_input(bNodeTree *tree)
{
using namespace blender;
Set<bNode *> curve_to_mesh_nodes;
LISTBASE_FOREACH (bNode *, node, &tree->nodes) {
if (STREQ(node->idname, "GeometryNodeCurveToMesh")) {
curve_to_mesh_nodes.add(node);
}
}
for (bNode *curve_to_mesh : curve_to_mesh_nodes) {
if (bke::node_find_socket(*curve_to_mesh, SOCK_IN, "Scale")) {
/* Make versioning idempotent. */
continue;
}
version_node_add_socket_if_not_exist(
tree, curve_to_mesh, SOCK_IN, SOCK_FLOAT, PROP_NONE, "Scale", "Scale");
bNode &named_attribute = version_node_add_empty(*tree, "GeometryNodeInputNamedAttribute");
NodeGeometryInputNamedAttribute *named_attribute_storage =
MEM_callocN<NodeGeometryInputNamedAttribute>(__func__);
named_attribute_storage->data_type = CD_PROP_FLOAT;
named_attribute.storage = named_attribute_storage;
named_attribute.parent = curve_to_mesh->parent;
named_attribute.location[0] = curve_to_mesh->location[0] - 25;
named_attribute.location[1] = curve_to_mesh->location[1];
named_attribute.flag &= ~NODE_SELECT;
bNodeSocket *name_input = version_node_add_socket_if_not_exist(
tree, &named_attribute, SOCK_IN, SOCK_STRING, PROP_NONE, "Name", "Name");
STRNCPY(name_input->default_value_typed<bNodeSocketValueString>()->value, "radius");
version_node_add_socket_if_not_exist(
tree, &named_attribute, SOCK_OUT, SOCK_BOOLEAN, PROP_NONE, "Exists", "Exists");
version_node_add_socket_if_not_exist(
tree, &named_attribute, SOCK_OUT, SOCK_FLOAT, PROP_NONE, "Attribute", "Attribute");
bNode &switch_node = version_node_add_empty(*tree, "GeometryNodeSwitch");
NodeSwitch *switch_storage = MEM_callocN<NodeSwitch>(__func__);
switch_storage->input_type = SOCK_FLOAT;
switch_node.storage = switch_storage;
switch_node.parent = curve_to_mesh->parent;
switch_node.location[0] = curve_to_mesh->location[0] - 25;
switch_node.location[1] = curve_to_mesh->location[1];
switch_node.flag &= ~NODE_SELECT;
version_node_add_socket_if_not_exist(
tree, &switch_node, SOCK_IN, SOCK_BOOLEAN, PROP_NONE, "Switch", "Switch");
bNodeSocket *false_input = version_node_add_socket_if_not_exist(
tree, &switch_node, SOCK_IN, SOCK_FLOAT, PROP_NONE, "False", "False");
false_input->default_value_typed<bNodeSocketValueFloat>()->value = 1.0f;
version_node_add_socket_if_not_exist(
tree, &switch_node, SOCK_IN, SOCK_FLOAT, PROP_NONE, "True", "True");
version_node_add_link(*tree,
named_attribute,
*bke::node_find_socket(named_attribute, SOCK_OUT, "Exists"),
switch_node,
*bke::node_find_socket(switch_node, SOCK_IN, "Switch"));
version_node_add_link(*tree,
named_attribute,
*bke::node_find_socket(named_attribute, SOCK_OUT, "Attribute"),
switch_node,
*bke::node_find_socket(switch_node, SOCK_IN, "True"));
version_node_add_socket_if_not_exist(
tree, &switch_node, SOCK_OUT, SOCK_FLOAT, PROP_NONE, "Output", "Output");
version_node_add_link(*tree,
switch_node,
*bke::node_find_socket(switch_node, SOCK_OUT, "Output"),
*curve_to_mesh,
*bke::node_find_socket(*curve_to_mesh, SOCK_IN, "Scale"));
}
}
static bool strip_effect_overdrop_to_alphaover(Strip *strip, void * /*user_data*/)
{
if (strip->type == STRIP_TYPE_OVERDROP_REMOVED) {
@@ -5899,6 +5976,15 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain)
version_sequencer_update_overdrop(bmain);
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 405, 4)) {
FOREACH_NODETREE_BEGIN (bmain, ntree, id) {
if (ntree->type == NTREE_GEOMETRY) {
do_version_node_curve_to_mesh_scale_input(ntree);
}
}
FOREACH_NODETREE_END;
}
/* Always run this versioning; meshes are written with the legacy format which always needs to
* be converted to the new format on file load. Can be moved to a subversion check in a larger
* breaking release. */

View File

@@ -24,6 +24,8 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_input<decl::Geometry>("Profile Curve")
.only_realized_data()
.supported_type(GeometryComponent::Type::Curve);
b.add_input<decl::Float>("Scale").default_value(1.0f).min(0.0f).field_on({0}).description(
"Scale of the profile at each point");
b.add_input<decl::Bool>("Fill Caps")
.description(
"If the profile spline is cyclic, fill the ends of the generated mesh with N-gons");
@@ -32,14 +34,22 @@ static void node_declare(NodeDeclarationBuilder &b)
static Mesh *curve_to_mesh(const bke::CurvesGeometry &curves,
const GeometrySet &profile_set,
const fn::FieldContext &context,
const Field<float> &scale_field,
const bool fill_caps,
const AttributeFilter &attribute_filter)
{
Mesh *mesh;
if (profile_set.has_curves()) {
const Curves *profile_curves = profile_set.get_curves();
FieldEvaluator evaluator{context, curves.points_num()};
evaluator.add(scale_field);
evaluator.evaluate();
const VArray<float> profile_scales = evaluator.get_evaluated<float>(0);
mesh = bke::curve_to_mesh_sweep(
curves, profile_curves->geometry.wrap(), fill_caps, attribute_filter);
curves, profile_curves->geometry.wrap(), profile_scales, fill_caps, attribute_filter);
}
else {
mesh = bke::curve_to_wire_mesh(curves, attribute_filter);
@@ -50,6 +60,7 @@ static Mesh *curve_to_mesh(const bke::CurvesGeometry &curves,
static void grease_pencil_to_mesh(GeometrySet &geometry_set,
const GeometrySet &profile_set,
const Field<float> &scale_field,
const bool fill_caps,
const AttributeFilter &attribute_filter)
{
@@ -64,7 +75,10 @@ static void grease_pencil_to_mesh(GeometrySet &geometry_set,
continue;
}
const bke::CurvesGeometry &curves = drawing->strokes();
mesh_by_layer[layer_index] = curve_to_mesh(curves, profile_set, fill_caps, attribute_filter);
const bke::GreasePencilLayerFieldContext context{
grease_pencil, bke::AttrDomain::Point, layer_index};
mesh_by_layer[layer_index] = curve_to_mesh(
curves, profile_set, context, scale_field, fill_caps, attribute_filter);
}
if (mesh_by_layer.is_empty()) {
@@ -104,6 +118,7 @@ static void node_geo_exec(GeoNodeExecParams params)
{
GeometrySet curve_set = params.extract_input<GeometrySet>("Curve");
GeometrySet profile_set = params.extract_input<GeometrySet>("Profile Curve");
const Field<float> scale_field = params.extract_input<Field<float>>("Scale");
const bool fill_caps = params.extract_input<bool>("Fill Caps");
bke::GeometryComponentEditData::remember_deformed_positions_if_necessary(curve_set);
@@ -112,7 +127,10 @@ static void node_geo_exec(GeoNodeExecParams params)
curve_set.modify_geometry_sets([&](GeometrySet &geometry_set) {
if (geometry_set.has_curves()) {
const Curves &curves = *geometry_set.get_curves();
Mesh *mesh = curve_to_mesh(curves.geometry.wrap(), profile_set, fill_caps, attribute_filter);
const bke::CurvesFieldContext context{curves, bke::AttrDomain::Point};
Mesh *mesh = curve_to_mesh(
curves.geometry.wrap(), profile_set, context, scale_field, fill_caps, attribute_filter);
if (mesh != nullptr) {
mesh->mat = static_cast<Material **>(MEM_dupallocN(curves.mat));
mesh->totcol = curves.totcol;
@@ -120,7 +138,7 @@ static void node_geo_exec(GeoNodeExecParams params)
geometry_set.replace_mesh(mesh);
}
if (geometry_set.has_grease_pencil()) {
grease_pencil_to_mesh(geometry_set, profile_set, fill_caps, attribute_filter);
grease_pencil_to_mesh(geometry_set, profile_set, scale_field, fill_caps, attribute_filter);
}
geometry_set.keep_only_during_modify({GeometryComponent::Type::Mesh});
});