Mesh: Cache loose vertices #105567

Merged
Hans Goudey merged 29 commits from HooglyBoogly/blender:mesh-loose-vert-cache into main 2023-04-22 13:46:23 +02:00
16 changed files with 212 additions and 138 deletions

View File

@ -69,20 +69,25 @@ namespace blender::bke {
/**
* Cache of a mesh's loose edges, accessed with #Mesh::loose_edges(). *
*/
struct LooseEdgeCache {
struct LooseGeomCache {
/**
* A bitmap set to true for each loose edge, false if the edge is used by any face.
* Allocated only if there is at least one loose edge.
* A bitmap set to true for each loose element, false if the element is used by any face.
* Allocated only if there is at least one loose element.
*/
blender::BitVector<> is_loose_bits;
/**
* The number of loose edges. If zero, the #is_loose_bits shouldn't be accessed.
* The number of loose elements. If zero, the #is_loose_bits shouldn't be accessed.
* If less than zero, the cache has been accessed in an invalid way
* (i.e.directly instead of through #Mesh::loose_edges()).
*/
int count = -1;
};
struct LooseEdgeCache : public LooseGeomCache {
};
struct LooseVertCache : public LooseGeomCache {
};
struct MeshRuntime {
/* Evaluated mesh for objects which do not have effective modifiers.
* This mesh is used as a result of modifier stack evaluation.
@ -166,11 +171,12 @@ struct MeshRuntime {
mutable Vector<float3> vert_normals;
mutable Vector<float3> poly_normals;
/**
* A cache of data about the loose edges. Can be shared with other data-blocks with unchanged
* topology. Accessed with #Mesh::loose_edges().
*/
/** Cache of data about edges not used by faces. See #Mesh::loose_edges(). */
SharedCache<LooseEdgeCache> loose_edges_cache;
/** Cache of data about vertices not used by edges. See #Mesh::loose_verts(). */
SharedCache<LooseVertCache> loose_verts_cache;
/** Cache of data about vertices not used by faces. See #Mesh::loose_verts(). */
SharedCache<LooseVertCache> verts_no_face_cache;
/**
* A bit vector the size of the number of vertices, set to true for the center vertices of

View File

@ -1141,30 +1141,6 @@ BVHTree *bvhtree_from_mesh_looptri_ex(BVHTreeFromMesh *data,
return tree;
}
static BitVector<> loose_verts_map_get(const Span<blender::int2> edges,
int verts_num,
int *r_loose_vert_num)
{
BitVector<> loose_verts_mask(verts_num, true);
int num_linked_verts = 0;
for (const int64_t i : edges.index_range()) {
const blender::int2 &edge = edges[i];
if (loose_verts_mask[edge[0]]) {
loose_verts_mask[edge[0]].reset();
num_linked_verts++;
}
if (loose_verts_mask[edge[1]]) {
loose_verts_mask[edge[1]].reset();
num_linked_verts++;
}
}
*r_loose_vert_num = verts_num - num_linked_verts;
return loose_verts_mask;
}
static BitVector<> looptri_no_hidden_map_get(const blender::OffsetIndices<int> polys,
const VArray<bool> &hide_poly,
const int looptri_len,
@ -1237,10 +1213,14 @@ BVHTree *BKE_bvhtree_from_mesh_get(struct BVHTreeFromMesh *data,
switch (bvh_cache_type) {
case BVHTREE_FROM_LOOSEVERTS: {
int mask_bits_act_len = -1;
const BitVector<> mask = loose_verts_map_get(edges, mesh->totvert, &mask_bits_act_len);
data->tree = bvhtree_from_mesh_verts_create_tree(
0.0f, tree_type, 6, positions, mesh->totvert, mask, mask_bits_act_len);
const blender::bke::LooseVertCache &loose_verts = mesh->loose_verts();
data->tree = bvhtree_from_mesh_verts_create_tree(0.0f,
tree_type,
6,
positions,
mesh->totvert,
loose_verts.is_loose_bits,
loose_verts.count);
break;
}
case BVHTREE_FROM_VERTS: {

View File

@ -237,7 +237,8 @@ struct ResultOffsets {
Array<int> profile_indices;
/** Whether any curve in the profile or curve input has only a single evaluated point. */
bool any_single_point_curve;
bool any_single_point_main;
bool any_single_point_profile;
};
static ResultOffsets calculate_result_offsets(const CurvesInfo &info, const bool fill_caps)
{
@ -315,10 +316,8 @@ static ResultOffsets calculate_result_offsets(const CurvesInfo &info, const bool
}
}
},
[&]() {
result.any_single_point_curve = offsets_contain_single_point(main_offsets) ||
offsets_contain_single_point(profile_offsets);
});
[&]() { result.any_single_point_main = offsets_contain_single_point(main_offsets); },
[&]() { result.any_single_point_profile = offsets_contain_single_point(profile_offsets); });
return result;
}
@ -765,9 +764,13 @@ Mesh *curve_to_mesh_sweep(const CurvesGeometry &main,
positions.slice(info.vert_range));
});
if (!offsets.any_single_point_curve) {
/* If there are no single point curves, every curve combination will always have faces. */
mesh->loose_edges_tag_none();
if (!offsets.any_single_point_main) {
/* If there are no single point curves, every combination will have at least loose edges. */
mesh->tag_loose_verts_none();
if (!offsets.any_single_point_profile) {
/* If there are no single point profiles, every combination will have faces. */
mesh->loose_edges_tag_none();
}
}
SpanAttributeWriter<bool> sharp_edges;

View File

@ -190,29 +190,27 @@ void adapt_mesh_domain_corner_to_point_impl(const Mesh &mesh,
BLI_assert(r_values.size() == mesh.totvert);
const Span<int> corner_verts = mesh.corner_verts();
Array<bool> loose_verts(mesh.totvert, true);
r_values.fill(true);
for (const int corner : IndexRange(mesh.totloop)) {
const int point_index = corner_verts[corner];
loose_verts[point_index] = false;
if (!old_values[corner]) {
r_values[point_index] = false;
}
}
/* Deselect loose vertices without corners that are still selected from the 'true' default. */
/* The record fact says that the value is true.
* Writing to the array from different threads is okay because each thread sets the same value.
*/
threading::parallel_for(loose_verts.index_range(), 2048, [&](const IndexRange range) {
for (const int vert_index : range) {
if (loose_verts[vert_index]) {
r_values[vert_index] = false;
const bke::LooseVertCache &loose_verts = mesh.verts_no_face();
if (loose_verts.count > 0) {
const BitSpan bits = loose_verts.is_loose_bits;
threading::parallel_for(bits.index_range(), 2048, [&](const IndexRange range) {
for (const int vert_index : range) {
if (bits[vert_index]) {
r_values[vert_index] = false;
}
}
}
});
});
}
}
static GVArray adapt_mesh_domain_corner_to_point(const Mesh &mesh, const GVArray &varray)
@ -754,20 +752,26 @@ static bool can_simple_adapt_for_single(const Mesh &mesh,
/* All other domains are always connected to points. */
return true;
case ATTR_DOMAIN_EDGE:
/* There may be loose vertices not connected to edges. */
return ELEM(to_domain, ATTR_DOMAIN_FACE, ATTR_DOMAIN_CORNER);
if (to_domain == ATTR_DOMAIN_POINT) {
return mesh.loose_verts().count == 0;
}
return true;
case ATTR_DOMAIN_FACE:
/* There may be loose vertices or edges not connected to faces. */
if (to_domain == ATTR_DOMAIN_POINT) {
return mesh.verts_no_face().count == 0;
}
if (to_domain == ATTR_DOMAIN_EDGE) {
return mesh.loose_edges().count == 0;
}
return to_domain == ATTR_DOMAIN_CORNER;
return true;
case ATTR_DOMAIN_CORNER:
/* Only faces are always connected to corners. */
if (to_domain == ATTR_DOMAIN_POINT) {
return mesh.verts_no_face().count == 0;
}
if (to_domain == ATTR_DOMAIN_EDGE) {
return mesh.loose_edges().count == 0;
}
return to_domain == ATTR_DOMAIN_FACE;
return true;
default:
BLI_assert_unreachable();
return false;

View File

@ -130,6 +130,8 @@ static void mesh_copy_data(Main *bmain, ID *id_dst, const ID *id_src, const int
* when the source is persistent and edits to the destination mesh don't affect the caches.
* Caches will be "un-shared" as necessary later on. */
mesh_dst->runtime->bounds_cache = mesh_src->runtime->bounds_cache;
mesh_dst->runtime->loose_verts_cache = mesh_src->runtime->loose_verts_cache;
mesh_dst->runtime->verts_no_face_cache = mesh_src->runtime->verts_no_face_cache;
mesh_dst->runtime->loose_edges_cache = mesh_src->runtime->loose_edges_cache;
mesh_dst->runtime->looptris_cache = mesh_src->runtime->looptris_cache;

View File

@ -106,32 +106,90 @@ MeshRuntime::~MeshRuntime()
}
}
static int reset_bits_and_count(MutableBitSpan bits, const Span<int> indices_to_reset)
{
int count = bits.size();
for (const int vert : indices_to_reset) {
if (bits[vert]) {
bits[vert].reset();
count--;
}
}
return count;
}
static void bit_vector_with_reset_bits_or_empty(const Span<int> indices_to_reset,
const int indexed_elems_num,
BitVector<> &r_bits,
int &r_count)
{
r_bits.resize(0);
r_bits.resize(indexed_elems_num, true);
r_count = reset_bits_and_count(r_bits, indices_to_reset);
if (r_count == 0) {
r_bits.clear_and_shrink();
}
}
/**
* If there are no loose edges and no loose vertices, all vertices are used by faces.
*/
static void try_tag_verts_no_face_none(const Mesh &mesh)
{
if (mesh.runtime->loose_edges_cache.is_cached() || mesh.loose_edges().count > 0) {
return;
}
if (mesh.runtime->loose_verts_cache.is_cached() || mesh.loose_verts().count > 0) {
return;
}
mesh.runtime->verts_no_face_cache.ensure([&](LooseVertCache &r_data) {
r_data.is_loose_bits.clear_and_shrink();
r_data.count = 0;
});
}
} // namespace blender::bke
const blender::bke::LooseVertCache &Mesh::loose_verts() const
{
using namespace blender::bke;
this->runtime->loose_verts_cache.ensure([&](LooseVertCache &r_data) {
const Span<int> verts = this->edges().cast<int>();
bit_vector_with_reset_bits_or_empty(verts, this->totvert, r_data.is_loose_bits, r_data.count);
});
return this->runtime->loose_verts_cache.data();
}
const blender::bke::LooseVertCache &Mesh::verts_no_face() const
{
using namespace blender::bke;
this->runtime->verts_no_face_cache.ensure([&](LooseVertCache &r_data) {
const Span<int> verts = this->corner_verts();
bit_vector_with_reset_bits_or_empty(verts, this->totvert, r_data.is_loose_bits, r_data.count);
});
return this->runtime->verts_no_face_cache.data();
}
const blender::bke::LooseEdgeCache &Mesh::loose_edges() const
{
using namespace blender::bke;
this->runtime->loose_edges_cache.ensure([&](LooseEdgeCache &r_data) {
blender::BitVector<> &loose_edges = r_data.is_loose_bits;
loose_edges.resize(0);
loose_edges.resize(this->totedge, true);
int count = this->totedge;
for (const int edge : this->corner_edges()) {
if (loose_edges[edge]) {
loose_edges[edge].reset();
count--;
}
}
if (count == 0) {
loose_edges.clear_and_shrink();
}
r_data.count = count;
const Span<int> edges = this->corner_edges();
bit_vector_with_reset_bits_or_empty(edges, this->totedge, r_data.is_loose_bits, r_data.count);
});
return this->runtime->loose_edges_cache.data();
}
void Mesh::tag_loose_verts_none() const
{
using namespace blender::bke;
this->runtime->loose_verts_cache.ensure([&](LooseVertCache &r_data) {
r_data.is_loose_bits.clear_and_shrink();
r_data.count = 0;
});
try_tag_verts_no_face_none(*this);
}
void Mesh::loose_edges_tag_none() const
{
using namespace blender::bke;
@ -139,6 +197,7 @@ void Mesh::loose_edges_tag_none() const
r_data.is_loose_bits.clear_and_shrink();
r_data.count = 0;
});
try_tag_verts_no_face_none(*this);
}
blender::Span<MLoopTri> Mesh::looptris() const
@ -219,6 +278,8 @@ void BKE_mesh_runtime_clear_geometry(Mesh *mesh)
free_subdiv_ccg(*mesh->runtime);
mesh->runtime->bounds_cache.tag_dirty();
mesh->runtime->loose_edges_cache.tag_dirty();
mesh->runtime->loose_verts_cache.tag_dirty();
mesh->runtime->verts_no_face_cache.tag_dirty();
mesh->runtime->looptris_cache.tag_dirty();
mesh->runtime->subsurf_face_dot_tags.clear_and_shrink();
mesh->runtime->subsurf_optimal_display_edges.clear_and_shrink();
@ -237,6 +298,8 @@ void BKE_mesh_tag_edges_split(struct Mesh *mesh)
reset_normals(*mesh->runtime);
free_subdiv_ccg(*mesh->runtime);
mesh->runtime->loose_edges_cache.tag_dirty();
mesh->runtime->loose_verts_cache.tag_dirty();
mesh->runtime->verts_no_face_cache.tag_dirty();
mesh->runtime->subsurf_face_dot_tags.clear_and_shrink();
mesh->runtime->subsurf_optimal_display_edges.clear_and_shrink();
if (mesh->runtime->shrinkwrap_data) {

View File

@ -177,6 +177,9 @@ static bool is_infinite_sharp_vertex(const OpenSubdiv_Converter *converter,
return true;
}
#endif
if (storage->infinite_sharp_vertices_map == nullptr) {
return false;
}
const int vertex_index = storage->manifold_vertex_index_reverse[manifold_vertex_index];
return BLI_BITMAP_TEST_BOOL(storage->infinite_sharp_vertices_map, vertex_index);
}
@ -264,7 +267,7 @@ static void free_user_data(const OpenSubdiv_Converter *converter)
ConverterStorage *user_data = static_cast<ConverterStorage *>(converter->user_data);
MEM_SAFE_FREE(user_data->loop_uv_indices);
MEM_freeN(user_data->manifold_vertex_index);
MEM_freeN(user_data->infinite_sharp_vertices_map);
MEM_SAFE_FREE(user_data->infinite_sharp_vertices_map);
MEM_freeN(user_data->manifold_vertex_index_reverse);
MEM_freeN(user_data->manifold_edge_index_reverse);
MEM_freeN(user_data);
@ -306,7 +309,7 @@ static void init_functions(OpenSubdiv_Converter *converter)
converter->freeUserData = free_user_data;
}
static void initialize_manifold_index_array(const BLI_bitmap *used_map,
static void initialize_manifold_index_array(const blender::BitSpan not_used_map,
const int num_elements,
int **r_indices,
int **r_indices_reverse,
@ -323,7 +326,7 @@ static void initialize_manifold_index_array(const BLI_bitmap *used_map,
}
int offset = 0;
for (int i = 0; i < num_elements; i++) {
if (BLI_BITMAP_TEST_BOOL(used_map, i)) {
if (not_used_map.is_empty() || !not_used_map[i]) {
if (indices != nullptr) {
indices[i] = i - offset;
}
@ -349,42 +352,35 @@ static void initialize_manifold_index_array(const BLI_bitmap *used_map,
static void initialize_manifold_indices(ConverterStorage *storage)
{
using namespace blender;
const Mesh *mesh = storage->mesh;
const blender::Span<blender::int2> edges = storage->edges;
const blender::OffsetIndices<int> polys = storage->polys;
const blender::Span<int> corner_verts = storage->corner_verts;
const blender::Span<int> corner_edges = storage->corner_edges;
/* Set bits of elements which are not loose. */
BLI_bitmap *vert_used_map = BLI_BITMAP_NEW(mesh->totvert, "vert used map");
BLI_bitmap *edge_used_map = BLI_BITMAP_NEW(mesh->totedge, "edge used map");
for (int poly_index = 0; poly_index < mesh->totpoly; poly_index++) {
for (const int corner : polys[poly_index]) {
BLI_BITMAP_ENABLE(vert_used_map, corner_verts[corner]);
BLI_BITMAP_ENABLE(edge_used_map, corner_edges[corner]);
}
}
initialize_manifold_index_array(vert_used_map,
const bke::LooseVertCache &loose_verts = mesh->verts_no_face();
const bke::LooseEdgeCache &loose_edges = mesh->loose_edges();
initialize_manifold_index_array(loose_verts.is_loose_bits,
mesh->totvert,
&storage->manifold_vertex_index,
&storage->manifold_vertex_index_reverse,
&storage->num_manifold_vertices);
initialize_manifold_index_array(edge_used_map,
initialize_manifold_index_array(loose_edges.is_loose_bits,
mesh->totedge,
nullptr,
&storage->manifold_edge_index_reverse,
&storage->num_manifold_edges);
/* Initialize infinite sharp mapping. */
storage->infinite_sharp_vertices_map = BLI_BITMAP_NEW(mesh->totvert, "vert used map");
for (int edge_index = 0; edge_index < mesh->totedge; edge_index++) {
if (!BLI_BITMAP_TEST_BOOL(edge_used_map, edge_index)) {
const blender::int2 &edge = edges[edge_index];
BLI_BITMAP_ENABLE(storage->infinite_sharp_vertices_map, edge[0]);
BLI_BITMAP_ENABLE(storage->infinite_sharp_vertices_map, edge[1]);
if (loose_edges.count > 0) {
const Span<int2> edges = storage->edges;
storage->infinite_sharp_vertices_map = BLI_BITMAP_NEW(mesh->totvert, "vert used map");
for (int edge_index = 0; edge_index < mesh->totedge; edge_index++) {
if (loose_edges.is_loose_bits[edge_index]) {
const int2 edge = edges[edge_index];
BLI_BITMAP_ENABLE(storage->infinite_sharp_vertices_map, edge[0]);
BLI_BITMAP_ENABLE(storage->infinite_sharp_vertices_map, edge[1]);
}
}
}
/* Free working variables. */
MEM_freeN(vert_used_map);
MEM_freeN(edge_used_map);
else {
storage->infinite_sharp_vertices_map = nullptr;
}
}
static void init_user_data(OpenSubdiv_Converter *converter,

View File

@ -30,46 +30,32 @@
/** \name Update Loose Geometry
* \{ */
static void mesh_render_data_loose_geom_mesh(const MeshRenderData *mr, MeshBufferCache *cache)
static void extract_set_bits(const blender::BitSpan bits, blender::MutableSpan<int> indices)
{
using namespace blender;
BLI_bitmap *lvert_map = BLI_BITMAP_NEW(mr->vert_len, __func__);
const bke::LooseEdgeCache &loose_edges = mr->me->loose_edges();
if (loose_edges.count > 0) {
cache->loose_geom.edges.reinitialize(loose_edges.count);
int count = 0;
for (const int64_t i : loose_edges.is_loose_bits.index_range()) {
if (loose_edges.is_loose_bits[i]) {
cache->loose_geom.edges[count] = int(i);
count++;
}
}
}
/* Tag verts as not loose. */
for (const int2 &edge : mr->edges) {
BLI_BITMAP_ENABLE(lvert_map, edge[0]);
BLI_BITMAP_ENABLE(lvert_map, edge[1]);
}
int count = 0;
Array<int> loose_verts(mr->vert_len);
for (int v = 0; v < mr->vert_len; v++) {
if (!BLI_BITMAP_TEST(lvert_map, v)) {
loose_verts[count] = v;
for (const int64_t i : bits.index_range()) {
if (bits[i]) {
indices[count] = int(i);
count++;
}
}
if (count < mr->vert_len) {
cache->loose_geom.verts = loose_verts.as_span().take_front(count);
}
else {
cache->loose_geom.verts = std::move(loose_verts);
BLI_assert(count == indices.size());
}
static void mesh_render_data_loose_geom_mesh(const MeshRenderData *mr, MeshBufferCache *cache)
{
using namespace blender;
const bke::LooseEdgeCache &loose_edges = mr->me->loose_edges();
if (loose_edges.count > 0) {
cache->loose_geom.edges.reinitialize(loose_edges.count);
extract_set_bits(loose_edges.is_loose_bits, cache->loose_geom.edges);
}
MEM_freeN(lvert_map);
const bke::LooseVertCache &loose_verts = mr->me->loose_verts();
if (loose_verts.count > 0) {
cache->loose_geom.verts.reinitialize(loose_verts.count);
extract_set_bits(loose_verts.is_loose_bits, cache->loose_geom.verts);
}
}
static void mesh_render_data_loose_verts_bm(const MeshRenderData *mr,

View File

@ -417,6 +417,7 @@ Mesh *create_cuboid_mesh(const float3 &size,
const float3 bounds = size * 0.5f;
mesh->bounds_set_eager({-bounds, bounds});
mesh->tag_loose_verts_none();
return mesh;
}

View File

@ -203,6 +203,7 @@ struct AllMeshesInfo {
/** True if we know that there are no loose edges in any of the input meshes. */
bool no_loose_edges_hint = false;
bool no_loose_verts_hint = false;
};
struct AllCurvesInfo {
@ -947,6 +948,10 @@ static AllMeshesInfo preprocess_meshes(const GeometrySet &geometry_set,
info.order.begin(), info.order.end(), [](const Mesh *mesh) {
return mesh->runtime->loose_edges_cache.is_cached() && mesh->loose_edges().count == 0;
});
info.no_loose_verts_hint = std::all_of(
info.order.begin(), info.order.end(), [](const Mesh *mesh) {
return mesh->runtime->loose_verts_cache.is_cached() && mesh->loose_verts().count == 0;
});
return info;
}
@ -1155,6 +1160,9 @@ static void execute_realize_mesh_tasks(const RealizeInstancesOptions &options,
if (all_meshes_info.no_loose_edges_hint) {
dst_mesh->loose_edges_tag_none();
}
if (all_meshes_info.no_loose_verts_hint) {
dst_mesh->tag_loose_verts_none();
}
}
/** \} */

View File

@ -27,6 +27,7 @@ namespace bke {
struct MeshRuntime;
class AttributeAccessor;
class MutableAttributeAccessor;
struct LooseVertCache;
struct LooseEdgeCache;
} // namespace bke
} // namespace blender
@ -302,6 +303,15 @@ typedef struct Mesh {
* Cached information about loose edges, calculated lazily when necessary.
*/
const blender::bke::LooseEdgeCache &loose_edges() const;
/**
* Cached information about vertices that aren't used by any edges.
*/
const blender::bke::LooseVertCache &loose_verts() const;
/**
* Cached information about vertices that aren't used by faces (but may be used by loose edges).
*/
const blender::bke::LooseVertCache &verts_no_face() const;
/**
* Explicitly set the cached number of loose edges to zero. This can improve performance
* later on, because finding loose edges lazily can be skipped entirely.
@ -310,6 +320,14 @@ typedef struct Mesh {
* cache dirty. If the mesh was changed first, the relevant dirty tags should be called first.
*/
void loose_edges_tag_none() const;
/**
* Set the number of verices not connected to edges to zero. Similar to #loose_edges_tag_none().
* There may still be vertices only used by loose edges though.
*
* \note If both #loose_edges_tag_none() and #tag_loose_verts_none() are called,
* all vertices are used by faces, so #verts_no_faces() will be tagged empty as well.
*/
void tag_loose_verts_none() const;
/**
* Normal direction of polygons, defined by positions and the winding direction of face corners.

View File

@ -551,6 +551,7 @@ static void duplicate_faces(GeometrySet &geometry_set,
}
}
new_mesh->tag_loose_verts_none();
new_mesh->loose_edges_tag_none();
copy_face_attributes_without_id(edge_mapping,

View File

@ -153,6 +153,8 @@ static Mesh *create_circle_mesh(const float radius,
std::iota(corner_verts.begin(), corner_verts.end(), 0);
std::iota(corner_edges.begin(), corner_edges.end(), 0);
mesh->loose_edges_tag_none();
}
else if (fill_type == GEO_NODE_MESH_CIRCLE_FILL_TRIANGLE_FAN) {
for (const int i : poly_offsets.index_range()) {
@ -170,6 +172,7 @@ static Mesh *create_circle_mesh(const float radius,
}
}
mesh->tag_loose_verts_none();
mesh->bounds_set_eager(calculate_bounds_circle(radius, verts_num));
return mesh;

View File

@ -723,6 +723,7 @@ Mesh *create_cylinder_or_cone_mesh(const float radius_top,
}
calculate_selection_outputs(config, attribute_outputs, mesh->attributes_for_write());
mesh->tag_loose_verts_none();
mesh->loose_edges_tag_none();
mesh->bounds_set_eager(calculate_bounds_cylinder(config));

View File

@ -149,6 +149,7 @@ Mesh *create_grid_mesh(const int verts_x,
calculate_uvs(mesh, positions, corner_verts, size_x, size_y, uv_map_id);
}
mesh->tag_loose_verts_none();
mesh->loose_edges_tag_none();
const float3 bounds = float3(size_x * 0.5f, size_y * 0.5f, 0.0f);

View File

@ -333,6 +333,7 @@ static Mesh *create_uv_sphere_mesh(const float radius,
}
});
mesh->tag_loose_verts_none();
mesh->loose_edges_tag_none();
mesh->bounds_set_eager(calculate_bounds_uv_sphere(radius, segments, rings));