Geometry Nodes: output uv map from primitive nodes as anonymous attributes

This is essentially a left-over from the initial transition to fields where this was
forgotten. The mesh primitive nodes used to create a named uv map attribute
with a hard-coded name. The standard way to deal with that in geometry nodes
now is to output the attribute as a socket instead. The user can then decide
to store it as a named attribute or not.

The benefits of not always storing the named attribute in the node are:
* Improved performance and lower memory usage when the uv map is not
  used.
* It's more obvious that there actually is a uv map.
* The hard-coded name was inconsistent.

The versioning code inserts a new Store Named Attribute node that
stores the uv map immediatly. In many cases, users can probably just
remove this node without affecting their final result, but we can't
detect that.

There is one behavior change which is that the stored uv map will be
a 3d vector instead of a 2d vector which is what the nodes originally created.
We could store the uv map as 2d vector inthe Store Named Attribute node,
but that has the problem that older Blender versions don't support this
and would crash immediately. Users can just change this to 2d vector
manually if they don't care about forward compatibility.

There is a plan to support 2d vectors more natively in geometry nodes: T92765.

This change breaks forward compatibility in the case when the uv map
was used.

Differential Revision: https://developer.blender.org/D16637
This commit is contained in:
Jacques Lucke 2022-12-14 18:05:31 +01:00
parent d9192aaa6d
commit f879c20f72
Notes: blender-bot 2023-02-14 07:25:51 +01:00
Referenced by issue #91852, Output UVs from mesh primitive nodes with anonymous attributes
10 changed files with 249 additions and 42 deletions

View File

@ -25,13 +25,13 @@ extern "C" {
/* 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 show a warning if the file
* was written with too new a version. */
#define BLENDER_FILE_MIN_VERSION 305
#define BLENDER_FILE_MIN_SUBVERSION 3
#define BLENDER_FILE_MIN_SUBVERSION 4
/** User readable version string. */
const char *BKE_blender_version_string(void);

View File

@ -1130,7 +1130,7 @@ static void node_init(const bContext *C, bNodeTree *ntree, bNode *node)
ntype->initfunc(ntree, node);
}
if (ntree->typeinfo->node_add_init != nullptr) {
if (ntree->typeinfo && ntree->typeinfo->node_add_init) {
ntree->typeinfo->node_add_init(ntree, node);
}

View File

@ -823,6 +823,99 @@ static void version_geometry_nodes_replace_transfer_attribute_node(bNodeTree *nt
}
}
/**
* The mesh primitive nodes created a uv map with a hardcoded name. Now they are outputting the uv
* map as a socket instead. The versioning just inserts a Store Named Attribute node after
* primitive nodes.
*/
static void version_geometry_nodes_primitive_uv_maps(bNodeTree &ntree)
{
blender::Vector<bNode *> new_nodes;
LISTBASE_FOREACH_MUTABLE (bNode *, node, &ntree.nodes) {
if (!ELEM(node->type,
GEO_NODE_MESH_PRIMITIVE_CONE,
GEO_NODE_MESH_PRIMITIVE_CUBE,
GEO_NODE_MESH_PRIMITIVE_CYLINDER,
GEO_NODE_MESH_PRIMITIVE_GRID,
GEO_NODE_MESH_PRIMITIVE_ICO_SPHERE,
GEO_NODE_MESH_PRIMITIVE_UV_SPHERE)) {
continue;
}
bNodeSocket *primitive_output_socket = nullptr;
bNodeSocket *uv_map_output_socket = nullptr;
LISTBASE_FOREACH (bNodeSocket *, socket, &node->outputs) {
if (STREQ(socket->name, "UV Map")) {
uv_map_output_socket = socket;
}
if (socket->type == SOCK_GEOMETRY) {
primitive_output_socket = socket;
}
}
if (uv_map_output_socket != nullptr) {
continue;
}
uv_map_output_socket = nodeAddStaticSocket(
&ntree, node, SOCK_OUT, SOCK_VECTOR, PROP_NONE, "UV Map", "UV Map");
bNode *store_attribute_node = nodeAddStaticNode(
nullptr, &ntree, GEO_NODE_STORE_NAMED_ATTRIBUTE);
new_nodes.append(store_attribute_node);
store_attribute_node->parent = node->parent;
store_attribute_node->locx = node->locx + 25;
store_attribute_node->locy = node->locy;
store_attribute_node->offsetx = node->offsetx;
store_attribute_node->offsety = node->offsety;
NodeGeometryStoreNamedAttribute &storage = *static_cast<NodeGeometryStoreNamedAttribute *>(
store_attribute_node->storage);
storage.domain = ATTR_DOMAIN_CORNER;
/* Intentionally use 3D instead of 2D vectors, because 2D vectors did not exist in older
* releases and would make the file crash when trying to open it. */
storage.data_type = CD_PROP_FLOAT3;
bNodeSocket *store_attribute_geometry_input = static_cast<bNodeSocket *>(
store_attribute_node->inputs.first);
bNodeSocket *store_attribute_name_input = store_attribute_geometry_input->next;
bNodeSocket *store_attribute_value_input = nullptr;
LISTBASE_FOREACH (bNodeSocket *, socket, &store_attribute_node->inputs) {
if (socket->type == SOCK_VECTOR) {
store_attribute_value_input = socket;
break;
}
}
bNodeSocket *store_attribute_geometry_output = static_cast<bNodeSocket *>(
store_attribute_node->outputs.first);
LISTBASE_FOREACH (bNodeLink *, link, &ntree.links) {
if (link->fromsock == primitive_output_socket) {
link->fromnode = store_attribute_node;
link->fromsock = store_attribute_geometry_output;
}
}
bNodeSocketValueString *name_value = static_cast<bNodeSocketValueString *>(
store_attribute_name_input->default_value);
const char *uv_map_name = node->type == GEO_NODE_MESH_PRIMITIVE_ICO_SPHERE ? "UVMap" :
"uv_map";
BLI_strncpy(name_value->value, uv_map_name, sizeof(name_value->value));
nodeAddLink(&ntree,
node,
primitive_output_socket,
store_attribute_node,
store_attribute_geometry_input);
nodeAddLink(
&ntree, node, uv_map_output_socket, store_attribute_node, store_attribute_value_input);
}
/* Move nodes to the front so that they are drawn behind existing nodes. */
for (bNode *node : new_nodes) {
BLI_remlink(&ntree.nodes, node);
BLI_addhead(&ntree.nodes, node);
}
if (!new_nodes.is_empty()) {
nodeRebuildIDVector(&ntree);
}
}
void do_versions_after_linking_300(Main *bmain, ReportList * /*reports*/)
{
if (MAIN_VERSION_ATLEAST(bmain, 300, 0) && !MAIN_VERSION_ATLEAST(bmain, 300, 1)) {
@ -3708,18 +3801,7 @@ void blo_do_versions_300(FileData *fd, Library * /*lib*/, Main *bmain)
}
}
/**
* Versioning code until next subversion bump goes here.
*
* \note Be sure to check when bumping the version:
* - "versioning_userdef.c", #blo_do_versions_userdef
* - "versioning_userdef.c", #do_versions_theme
*
* \note Keep this message at the bottom of the function.
*/
{
/* Keep this block, even when empty. */
if (!MAIN_VERSION_ATLEAST(bmain, 305, 4)) {
LISTBASE_FOREACH (bNodeTree *, ntree, &bmain->nodetrees) {
if (ntree->type == NTREE_GEOMETRY) {
version_node_socket_name(ntree, GEO_NODE_COLLECTION_INFO, "Geometry", "Instances");
@ -3732,5 +3814,24 @@ void blo_do_versions_300(FileData *fd, Library * /*lib*/, Main *bmain)
image->seam_margin = 8;
}
}
LISTBASE_FOREACH (bNodeTree *, ntree, &bmain->nodetrees) {
if (ntree->type == NTREE_GEOMETRY) {
version_geometry_nodes_primitive_uv_maps(*ntree);
}
}
}
/**
* Versioning code until next subversion bump goes here.
*
* \note Be sure to check when bumping the version:
* - "versioning_userdef.c", #blo_do_versions_userdef
* - "versioning_userdef.c", #do_versions_theme
*
* \note Keep this message at the bottom of the function.
*/
{
/* Keep this block, even when empty. */
}
}

View File

@ -46,12 +46,14 @@ void transform_geometry_set(GeoNodeExecParams &params,
Mesh *create_line_mesh(const float3 start, const float3 delta, int count);
Mesh *create_grid_mesh(int verts_x, int verts_y, float size_x, float size_y);
Mesh *create_grid_mesh(
int verts_x, int verts_y, float size_x, float size_y, const AttributeIDRef &uv_map_id);
struct ConeAttributeOutputs {
StrongAnonymousAttributeID top_id;
StrongAnonymousAttributeID bottom_id;
StrongAnonymousAttributeID side_id;
StrongAnonymousAttributeID uv_map_id;
};
Mesh *create_cylinder_or_cone_mesh(float radius_top,

View File

@ -536,12 +536,14 @@ static void calculate_selection_outputs(const ConeConfig &config,
* If the mesh is a truncated cone or a cylinder, the side faces are unwrapped into
* a rectangle that fills the top half of the UV (or the entire UV, if there are no fills).
*/
static void calculate_cone_uvs(Mesh *mesh, const ConeConfig &config)
static void calculate_cone_uvs(const ConeConfig &config,
Mesh *mesh,
const AttributeIDRef &uv_map_id)
{
MutableAttributeAccessor attributes = mesh->attributes_for_write();
SpanAttributeWriter<float2> uv_attribute = attributes.lookup_or_add_for_write_only_span<float2>(
"uv_map", ATTR_DOMAIN_CORNER);
uv_map_id, ATTR_DOMAIN_CORNER);
MutableSpan<float2> uvs = uv_attribute.span;
Array<float2> circle(config.circle_segments);
@ -698,7 +700,9 @@ Mesh *create_cylinder_or_cone_mesh(const float radius_top,
calculate_cone_verts(config, verts);
calculate_cone_edges(config, edges);
calculate_cone_faces(config, loops, polys);
calculate_cone_uvs(mesh, config);
if (attribute_outputs.uv_map_id) {
calculate_cone_uvs(config, mesh, attribute_outputs.uv_map_id.get());
}
calculate_selection_outputs(config, attribute_outputs, mesh->attributes_for_write());
mesh->loose_edges_tag_none();
@ -747,6 +751,7 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Bool>(N_("Top")).field_source();
b.add_output<decl::Bool>(N_("Bottom")).field_source();
b.add_output<decl::Bool>(N_("Side")).field_source();
b.add_output<decl::Vector>(N_("UV Map")).field_source();
}
static void node_init(bNodeTree * /*tree*/, bNode *node)
@ -818,6 +823,9 @@ static void node_geo_exec(GeoNodeExecParams params)
if (params.output_is_required("Side")) {
attribute_outputs.side_id = StrongAnonymousAttributeID("side_selection");
}
if (params.output_is_required("UV Map")) {
attribute_outputs.uv_map_id = StrongAnonymousAttributeID("uv_map");
}
Mesh *mesh = create_cylinder_or_cone_mesh(radius_top,
radius_bottom,
@ -847,6 +855,12 @@ static void node_geo_exec(GeoNodeExecParams params)
AnonymousAttributeFieldInput::Create<bool>(
std::move(attribute_outputs.side_id), params.attribute_producer_name()));
}
if (attribute_outputs.uv_map_id) {
params.set_output(
"UV Map",
AnonymousAttributeFieldInput::Create<float3>(std::move(attribute_outputs.uv_map_id),
params.attribute_producer_name()));
}
params.set_output("Mesh", GeometrySet::create_with_mesh(mesh));
}

View File

@ -35,14 +35,16 @@ static void node_declare(NodeDeclarationBuilder &b)
.max(1000)
.description(N_("Number of vertices for the Z side of the shape"));
b.add_output<decl::Geometry>(N_("Mesh"));
b.add_output<decl::Vector>(N_("UV Map")).field_source();
}
static Mesh *create_cuboid_mesh(const float3 &size,
const int verts_x,
const int verts_y,
const int verts_z)
const int verts_z,
const AttributeIDRef &uv_map_id)
{
Mesh *mesh = geometry::create_cuboid_mesh(size, verts_x, verts_y, verts_z, "uv_map");
Mesh *mesh = geometry::create_cuboid_mesh(size, verts_x, verts_y, verts_z, uv_map_id);
BKE_id_material_eval_ensure_default_slot(&mesh->id);
return mesh;
}
@ -50,7 +52,8 @@ static Mesh *create_cuboid_mesh(const float3 &size,
static Mesh *create_cube_mesh(const float3 size,
const int verts_x,
const int verts_y,
const int verts_z)
const int verts_z,
const AttributeIDRef &uv_map_id)
{
const int dimensions = (verts_x - 1 > 0) + (verts_y - 1 > 0) + (verts_z - 1 > 0);
if (dimensions == 0) {
@ -76,20 +79,20 @@ static Mesh *create_cube_mesh(const float3 size,
}
if (dimensions == 2) {
if (verts_z == 1) { /* XY plane. */
return create_grid_mesh(verts_x, verts_y, size.x, size.y);
return create_grid_mesh(verts_x, verts_y, size.x, size.y, uv_map_id);
}
if (verts_y == 1) { /* XZ plane. */
Mesh *mesh = create_grid_mesh(verts_x, verts_z, size.x, size.z);
Mesh *mesh = create_grid_mesh(verts_x, verts_z, size.x, size.z, uv_map_id);
transform_mesh(*mesh, float3(0), float3(M_PI_2, 0.0f, 0.0f), float3(1));
return mesh;
}
/* YZ plane. */
Mesh *mesh = create_grid_mesh(verts_z, verts_y, size.z, size.y);
Mesh *mesh = create_grid_mesh(verts_z, verts_y, size.z, size.y, uv_map_id);
transform_mesh(*mesh, float3(0), float3(0.0f, M_PI_2, 0.0f), float3(1));
return mesh;
}
return create_cuboid_mesh(size, verts_x, verts_y, verts_z);
return create_cuboid_mesh(size, verts_x, verts_y, verts_z, uv_map_id);
}
static void node_geo_exec(GeoNodeExecParams params)
@ -104,9 +107,20 @@ static void node_geo_exec(GeoNodeExecParams params)
return;
}
Mesh *mesh = create_cube_mesh(size, verts_x, verts_y, verts_z);
StrongAnonymousAttributeID uv_map_id;
if (params.output_is_required("UV Map")) {
uv_map_id = StrongAnonymousAttributeID("uv_map");
}
Mesh *mesh = create_cube_mesh(size, verts_x, verts_y, verts_z, uv_map_id.get());
params.set_output("Mesh", GeometrySet::create_with_mesh(mesh));
if (uv_map_id) {
params.set_output("UV Map",
AnonymousAttributeFieldInput::Create<float3>(
std::move(uv_map_id), params.attribute_producer_name()));
}
}
} // namespace blender::nodes::node_geo_mesh_primitive_cube_cc

View File

@ -46,6 +46,7 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Bool>(N_("Top")).field_source();
b.add_output<decl::Bool>(N_("Side")).field_source();
b.add_output<decl::Bool>(N_("Bottom")).field_source();
b.add_output<decl::Vector>(N_("UV Map")).field_source();
}
static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
@ -115,6 +116,9 @@ static void node_geo_exec(GeoNodeExecParams params)
if (params.output_is_required("Side")) {
attribute_outputs.side_id = StrongAnonymousAttributeID("side_selection");
}
if (params.output_is_required("UV Map")) {
attribute_outputs.uv_map_id = StrongAnonymousAttributeID("uv_map");
}
/* The cylinder is a special case of the cone mesh where the top and bottom radius are equal. */
Mesh *mesh = create_cylinder_or_cone_mesh(radius,
@ -142,6 +146,12 @@ static void node_geo_exec(GeoNodeExecParams params)
AnonymousAttributeFieldInput::Create<bool>(
std::move(attribute_outputs.side_id), params.attribute_producer_name()));
}
if (attribute_outputs.uv_map_id) {
params.set_output(
"UV Map",
AnonymousAttributeFieldInput::Create<float3>(std::move(attribute_outputs.uv_map_id),
params.attribute_producer_name()));
}
params.set_output("Mesh", GeometrySet::create_with_mesh(mesh));
}

View File

@ -15,13 +15,17 @@
namespace blender::nodes {
static void calculate_uvs(
Mesh *mesh, Span<MVert> verts, Span<MLoop> loops, const float size_x, const float size_y)
static void calculate_uvs(Mesh *mesh,
Span<MVert> verts,
Span<MLoop> loops,
const float size_x,
const float size_y,
const AttributeIDRef &uv_map_id)
{
MutableAttributeAccessor attributes = mesh->attributes_for_write();
SpanAttributeWriter<float2> uv_attribute = attributes.lookup_or_add_for_write_only_span<float2>(
"uv_map", ATTR_DOMAIN_CORNER);
uv_map_id, ATTR_DOMAIN_CORNER);
const float dx = (size_x == 0.0f) ? 0.0f : 1.0f / size_x;
const float dy = (size_y == 0.0f) ? 0.0f : 1.0f / size_y;
@ -39,7 +43,8 @@ static void calculate_uvs(
Mesh *create_grid_mesh(const int verts_x,
const int verts_y,
const float size_x,
const float size_y)
const float size_y,
const AttributeIDRef &uv_map_id)
{
BLI_assert(verts_x > 0 && verts_y > 0);
const int edges_x = verts_x - 1;
@ -139,8 +144,8 @@ Mesh *create_grid_mesh(const int verts_x,
}
});
if (mesh->totpoly != 0) {
calculate_uvs(mesh, verts, loops, size_x, size_y);
if (uv_map_id && mesh->totpoly != 0) {
calculate_uvs(mesh, verts, loops, size_x, size_y, uv_map_id);
}
mesh->loose_edges_tag_none();
@ -175,6 +180,7 @@ static void node_declare(NodeDeclarationBuilder &b)
.max(1000)
.description(N_("Number of vertices in the Y direction"));
b.add_output<decl::Geometry>(N_("Mesh"));
b.add_output<decl::Vector>(N_("UV Map")).field_source();
}
static void node_geo_exec(GeoNodeExecParams params)
@ -188,10 +194,21 @@ static void node_geo_exec(GeoNodeExecParams params)
return;
}
Mesh *mesh = create_grid_mesh(verts_x, verts_y, size_x, size_y);
StrongAnonymousAttributeID uv_map_id;
if (params.output_is_required("UV Map")) {
uv_map_id = StrongAnonymousAttributeID("uv_map");
}
Mesh *mesh = create_grid_mesh(verts_x, verts_y, size_x, size_y, uv_map_id.get());
BKE_id_material_eval_ensure_default_slot(&mesh->id);
params.set_output("Mesh", GeometrySet::create_with_mesh(mesh));
if (uv_map_id) {
params.set_output("UV Map",
AnonymousAttributeFieldInput::Create<float3>(
std::move(uv_map_id), params.attribute_producer_name()));
}
}
} // namespace blender::nodes::node_geo_mesh_primitive_grid_cc

View File

@ -25,12 +25,17 @@ static void node_declare(NodeDeclarationBuilder &b)
.max(7)
.description(N_("Number of subdivisions on top of the basic icosahedron"));
b.add_output<decl::Geometry>(N_("Mesh"));
b.add_output<decl::Vector>(N_("UV Map")).field_source();
}
static Mesh *create_ico_sphere_mesh(const int subdivisions, const float radius)
static Mesh *create_ico_sphere_mesh(const int subdivisions,
const float radius,
const AttributeIDRef &uv_map_id)
{
const float4x4 transform = float4x4::identity();
const bool create_uv_map = bool(uv_map_id);
BMeshCreateParams bmesh_create_params{};
bmesh_create_params.use_toolflags = true;
const BMAllocTemplate allocsize = {0, 0, 0, 0};
@ -43,7 +48,7 @@ static Mesh *create_ico_sphere_mesh(const int subdivisions, const float radius)
subdivisions,
std::abs(radius),
transform.values,
true);
create_uv_map);
BMeshToMeshParams params{};
params.calc_object_remap = false;
@ -52,6 +57,18 @@ static Mesh *create_ico_sphere_mesh(const int subdivisions, const float radius)
BM_mesh_bm_to_me(nullptr, bm, mesh, &params);
BM_mesh_free(bm);
/* The code above generates a "UVMap" attribute. The code below renames that attribute, we don't
* have a simple utility for that yet though so there is some overhead right now. */
MutableAttributeAccessor attributes = mesh->attributes_for_write();
if (create_uv_map) {
const VArraySpan<float2> orig_uv_map = attributes.lookup<float2>("UVMap");
SpanAttributeWriter<float2> uv_map = attributes.lookup_or_add_for_write_only_span<float2>(
uv_map_id, ATTR_DOMAIN_CORNER);
uv_map.span.copy_from(orig_uv_map);
uv_map.finish();
}
attributes.remove("UVMap");
return mesh;
}
@ -60,8 +77,19 @@ static void node_geo_exec(GeoNodeExecParams params)
const int subdivisions = std::min(params.extract_input<int>("Subdivisions"), 10);
const float radius = params.extract_input<float>("Radius");
Mesh *mesh = create_ico_sphere_mesh(subdivisions, radius);
StrongAnonymousAttributeID uv_map_id;
if (params.output_is_required("UV Map")) {
uv_map_id = StrongAnonymousAttributeID("uv_map");
}
Mesh *mesh = create_ico_sphere_mesh(subdivisions, radius, uv_map_id.get());
params.set_output("Mesh", GeometrySet::create_with_mesh(mesh));
if (uv_map_id) {
params.set_output("UV Map",
AnonymousAttributeFieldInput::Create<float3>(
std::move(uv_map_id), params.attribute_producer_name()));
}
}
} // namespace blender::nodes::node_geo_mesh_primitive_ico_sphere_cc

View File

@ -33,6 +33,7 @@ static void node_declare(NodeDeclarationBuilder &b)
.subtype(PROP_DISTANCE)
.description(N_("Distance from the generated points to the origin"));
b.add_output<decl::Geometry>(N_("Mesh"));
b.add_output<decl::Vector>(N_("UV Map")).field_source();
}
static int sphere_vert_total(const int segments, const int rings)
@ -255,12 +256,15 @@ BLI_NOINLINE static void calculate_sphere_corners(MutableSpan<MLoop> loops,
}
}
BLI_NOINLINE static void calculate_sphere_uvs(Mesh *mesh, const float segments, const float rings)
BLI_NOINLINE static void calculate_sphere_uvs(Mesh *mesh,
const float segments,
const float rings,
const AttributeIDRef &uv_map_id)
{
MutableAttributeAccessor attributes = mesh->attributes_for_write();
SpanAttributeWriter<float2> uv_attribute = attributes.lookup_or_add_for_write_only_span<float2>(
"uv_map", ATTR_DOMAIN_CORNER);
uv_map_id, ATTR_DOMAIN_CORNER);
MutableSpan<float2> uvs = uv_attribute.span;
const float dy = 1.0f / rings;
@ -301,7 +305,10 @@ BLI_NOINLINE static void calculate_sphere_uvs(Mesh *mesh, const float segments,
uv_attribute.finish();
}
static Mesh *create_uv_sphere_mesh(const float radius, const int segments, const int rings)
static Mesh *create_uv_sphere_mesh(const float radius,
const int segments,
const int rings,
const AttributeIDRef &uv_map_id)
{
Mesh *mesh = BKE_mesh_new_nomain(sphere_vert_total(segments, rings),
sphere_edge_total(segments, rings),
@ -325,7 +332,11 @@ static Mesh *create_uv_sphere_mesh(const float radius, const int segments, const
[&]() { calculate_sphere_edge_indices(edges, segments, rings); },
[&]() { calculate_sphere_faces(polys, segments); },
[&]() { calculate_sphere_corners(loops, segments, rings); },
[&]() { calculate_sphere_uvs(mesh, segments, rings); });
[&]() {
if (uv_map_id) {
calculate_sphere_uvs(mesh, segments, rings, uv_map_id);
}
});
mesh->loose_edges_tag_none();
@ -351,8 +362,18 @@ static void node_geo_exec(GeoNodeExecParams params)
const float radius = params.extract_input<float>("Radius");
Mesh *mesh = create_uv_sphere_mesh(radius, segments_num, rings_num);
StrongAnonymousAttributeID uv_map_id;
if (params.output_is_required("UV Map")) {
uv_map_id = StrongAnonymousAttributeID("uv_map");
}
Mesh *mesh = create_uv_sphere_mesh(radius, segments_num, rings_num, uv_map_id.get());
params.set_output("Mesh", GeometrySet::create_with_mesh(mesh));
if (uv_map_id) {
params.set_output("UV Map",
AnonymousAttributeFieldInput::Create<float3>(
std::move(uv_map_id), params.attribute_producer_name()));
}
}
} // namespace blender::nodes::node_geo_mesh_primitive_uv_sphere_cc