Mesh: Share normals caches by splitting vertex and face calculation #110479

Merged
Hans Goudey merged 14 commits from HooglyBoogly/blender:mesh-vert-face-normals-split into main 2023-08-25 23:06:12 +02:00
8 changed files with 159 additions and 185 deletions

View File

@ -73,16 +73,19 @@ void normals_calc_faces(Span<float3> vert_positions,
MutableSpan<float3> face_normals);
/**
* Calculate face and vertex normals directly into result arrays.
* Calculate vertex normals directly into the result array.
*
* \note Vertex and face normals can be calculated at the same time with
* #normals_calc_faces_and_verts, which can have performance benefits in some cases.
*
* \note Usually #Mesh::vert_normals() is the preferred way to access vertex normals,
HooglyBoogly marked this conversation as resolved Outdated

Worth adding a note about normals_calc_faces_verts in the doc-string as it may be preferred.

Worth adding a note about `normals_calc_faces_verts` in the doc-string as it may be preferred.
* since they may already be calculated and cached on the mesh.
*/
void normals_calc_face_vert(Span<float3> vert_positions,
OffsetIndices<int> faces,
Span<int> corner_verts,
MutableSpan<float3> face_normals,
MutableSpan<float3> vert_normals);
void normals_calc_verts(Span<float3> vert_positions,
OffsetIndices<int> faces,
Span<int> corner_verts,
Span<float3> face_normals,
MutableSpan<float3> vert_normals);
/** \} */

View File

@ -72,10 +72,6 @@ struct MeshRuntime {
Mesh *mesh_eval = nullptr;
std::mutex eval_mutex;
/* A separate mutex is needed for normal calculation, because sometimes
* the normals are needed while #eval_mutex is already locked. */
std::mutex normals_mutex;
/** Needed to ensure some thread-safety during render data pre-processing. */
std::mutex render_mutex;
@ -140,15 +136,9 @@ struct MeshRuntime {
*/
SubsurfRuntimeData *subsurf_runtime_data = nullptr;
/**
* Caches for lazily computed vertex and face normals. These are stored here rather than in
* #CustomData because they can be calculated on a `const` mesh, and adding custom data layers on
* a `const` mesh is not thread-safe.
*/
bool vert_normals_dirty = true;
bool face_normals_dirty = true;
mutable Vector<float3> vert_normals;
mutable Vector<float3> face_normals;
/** Caches for lazily computed vertex and face normals. */
SharedCache<Vector<float3>> vert_normals_cache;
SharedCache<Vector<float3>> face_normals_cache;
/** Cache of data about edges not used by faces. See #Mesh::loose_edges(). */
SharedCache<LooseEdgeCache> loose_edges_cache;

View File

@ -2258,11 +2258,11 @@ void BKE_keyblock_mesh_calc_normals(const KeyBlock *kb,
{reinterpret_cast<blender::float3 *>(face_normals), faces.size()});
}
if (vert_normals_needed) {
blender::bke::mesh::normals_calc_face_vert(
blender::bke::mesh::normals_calc_verts(
positions,
faces,
corner_verts,
{reinterpret_cast<blender::float3 *>(face_normals), faces.size()},
{reinterpret_cast<const blender::float3 *>(face_normals), faces.size()},
{reinterpret_cast<blender::float3 *>(vert_normals), mesh->totvert});
}
if (loop_normals_needed) {

View File

@ -131,6 +131,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->vert_normals_cache = mesh_src->runtime->vert_normals_cache;
mesh_dst->runtime->face_normals_cache = mesh_src->runtime->face_normals_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;

View File

@ -95,27 +95,25 @@ namespace blender::bke {
void mesh_vert_normals_assign(Mesh &mesh, Span<float3> vert_normals)
{
mesh.runtime->vert_normals.clear();
mesh.runtime->vert_normals.extend(vert_normals);
mesh.runtime->vert_normals_dirty = false;
mesh.runtime->vert_normals_cache.ensure([&](Vector<float3> &r_data) { r_data = vert_normals; });
}
void mesh_vert_normals_assign(Mesh &mesh, Vector<float3> vert_normals)
{
mesh.runtime->vert_normals = std::move(vert_normals);
mesh.runtime->vert_normals_dirty = false;
mesh.runtime->vert_normals_cache.ensure(
[&](Vector<float3> &r_data) { r_data = std::move(vert_normals); });
}
} // namespace blender::bke
bool BKE_mesh_vert_normals_are_dirty(const Mesh *mesh)
{
return mesh->runtime->vert_normals_dirty;
return mesh->runtime->vert_normals_cache.is_dirty();
}
bool BKE_mesh_face_normals_are_dirty(const Mesh *mesh)
{
return mesh->runtime->face_normals_dirty;
return mesh->runtime->face_normals_cache.is_dirty();
}
/** \} */
@ -198,99 +196,101 @@ void normals_calc_faces(const Span<float3> positions,
BLI_assert(faces.size() == face_normals.size());
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
for (const int i : range) {
face_normals[i] = face_normal_calc(positions, corner_verts.slice(faces[i]));
face_normals[i] = normal_calc_ngon(positions, corner_verts.slice(faces[i]));
}
});
}
void normals_calc_face_vert(const Span<float3> positions,
const OffsetIndices<int> faces,
const Span<int> corner_verts,
MutableSpan<float3> face_normals,
MutableSpan<float3> vert_normals)
static void normalize_and_validate(MutableSpan<float3> normals, const Span<float3> positions)
{
/* Zero the vertex normal array for accumulation. */
{
memset(vert_normals.data(), 0, vert_normals.as_span().size_in_bytes());
}
/* Compute face normals, accumulating them into vertex normals. */
{
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
for (const int face_i : range) {
const Span<int> face_verts = corner_verts.slice(faces[face_i]);
float3 &pnor = face_normals[face_i];
const int i_end = face_verts.size() - 1;
/* Polygon Normal and edge-vector. */
/* Inline version of #face_normal_calc, also does edge-vectors. */
{
zero_v3(pnor);
/* Newell's Method */
const float *v_curr = positions[face_verts[i_end]];
for (int i_next = 0; i_next <= i_end; i_next++) {
const float *v_next = positions[face_verts[i_next]];
add_newell_cross_v3_v3v3(pnor, v_curr, v_next);
v_curr = v_next;
}
if (UNLIKELY(normalize_v3(pnor) == 0.0f)) {
pnor[2] = 1.0f; /* Other axes set to zero. */
}
}
/* Accumulate angle weighted face normal into the vertex normal. */
/* Inline version of #accumulate_vertex_normals_poly_v3. */
{
float edvec_prev[3], edvec_next[3], edvec_end[3];
const float *v_curr = positions[face_verts[i_end]];
sub_v3_v3v3(edvec_prev, positions[face_verts[i_end - 1]], v_curr);
normalize_v3(edvec_prev);
copy_v3_v3(edvec_end, edvec_prev);
for (int i_next = 0, i_curr = i_end; i_next <= i_end; i_curr = i_next++) {
const float *v_next = positions[face_verts[i_next]];
/* Skip an extra normalization by reusing the first calculated edge. */
if (i_next != i_end) {
sub_v3_v3v3(edvec_next, v_curr, v_next);
normalize_v3(edvec_next);
}
else {
copy_v3_v3(edvec_next, edvec_end);
}
/* Calculate angle between the two face edges incident on this vertex. */
const float fac = saacos(-dot_v3v3(edvec_prev, edvec_next));
const float vnor_add[3] = {pnor[0] * fac, pnor[1] * fac, pnor[2] * fac};
float *vnor = vert_normals[face_verts[i_curr]];
add_v3_v3_atomic(vnor, vnor_add);
v_curr = v_next;
copy_v3_v3(edvec_prev, edvec_next);
}
}
threading::parallel_for(normals.index_range(), 1024, [&](const IndexRange range) {
for (const int vert_i : range) {
float *no = normals[vert_i];
if (UNLIKELY(normalize_v3(no) == 0.0f)) {
/* Following Mesh convention; we use vertex coordinate itself for normal in this case. */
normalize_v3_v3(no, positions[vert_i]);
}
});
}
}
});
}
/* Normalize and validate computed vertex normals. */
static void accumulate_face_normal_to_vert(const Span<float3> positions,
const Span<int> face_verts,
const float3 &face_normal,
MutableSpan<float3> vert_normals)
{
const int i_end = face_verts.size() - 1;
/* Accumulate angle weighted face normal into the vertex normal. */
/* Inline version of #accumulate_vertex_normals_poly_v3. */
{
threading::parallel_for(positions.index_range(), 1024, [&](const IndexRange range) {
for (const int vert_i : range) {
float *no = vert_normals[vert_i];
float edvec_prev[3], edvec_next[3], edvec_end[3];
const float *v_curr = positions[face_verts[i_end]];
sub_v3_v3v3(edvec_prev, positions[face_verts[i_end - 1]], v_curr);
normalize_v3(edvec_prev);
copy_v3_v3(edvec_end, edvec_prev);
if (UNLIKELY(normalize_v3(no) == 0.0f)) {
/* Following Mesh convention; we use vertex coordinate itself for normal in this case. */
normalize_v3_v3(no, positions[vert_i]);
}
for (int i_next = 0, i_curr = i_end; i_next <= i_end; i_curr = i_next++) {
const float *v_next = positions[face_verts[i_next]];
/* Skip an extra normalization by reusing the first calculated edge. */
if (i_next != i_end) {
sub_v3_v3v3(edvec_next, v_curr, v_next);
normalize_v3(edvec_next);
}
});
else {
copy_v3_v3(edvec_next, edvec_end);
}
/* Calculate angle between the two face edges incident on this vertex. */
const float fac = saacos(-dot_v3v3(edvec_prev, edvec_next));
const float vnor_add[3] = {face_normal[0] * fac, face_normal[1] * fac, face_normal[2] * fac};
float *vnor = vert_normals[face_verts[i_curr]];
add_v3_v3_atomic(vnor, vnor_add);
v_curr = v_next;
copy_v3_v3(edvec_prev, edvec_next);
}
}
}
void normals_calc_verts(const Span<float3> positions,
const OffsetIndices<int> faces,
const Span<int> corner_verts,
const Span<float3> face_normals,
MutableSpan<float3> vert_normals)
{
memset(vert_normals.data(), 0, vert_normals.as_span().size_in_bytes());
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
for (const int face_i : range) {
const Span<int> face_verts = corner_verts.slice(faces[face_i]);
accumulate_face_normal_to_vert(positions, face_verts, face_normals[face_i], vert_normals);
}
});
normalize_and_validate(vert_normals, positions);
}
static void normals_calc_faces_and_verts(const Span<float3> positions,
HooglyBoogly marked this conversation as resolved Outdated

suggestion, but this reads a bit like face-verts which is sometimes used for faces-of-verts: I find this makes it clearer that the function is calculating both the values at once: normals_calc_faces_and_verts.
(this knit-pick applies to the old function too).

*suggestion*, but this reads a bit like face-verts which is sometimes used for faces-of-verts: I find this makes it clearer that the function is calculating both the values at once: `normals_calc_faces_and_verts`. (this knit-pick applies to the old function too).
const OffsetIndices<int> faces,
const Span<int> corner_verts,
MutableSpan<float3> face_normals,
MutableSpan<float3> vert_normals)
{
memset(vert_normals.data(), 0, vert_normals.as_span().size_in_bytes());
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
for (const int face_i : range) {
const Span<int> face_verts = corner_verts.slice(faces[face_i]);
face_normals[face_i] = normal_calc_ngon(positions, face_verts);
accumulate_face_normal_to_vert(positions, face_verts, face_normals[face_i], vert_normals);
}
});
normalize_and_validate(vert_normals, positions);
}
/** \} */
} // namespace blender::bke::mesh
@ -302,62 +302,50 @@ void normals_calc_face_vert(const Span<float3> positions,
blender::Span<blender::float3> Mesh::vert_normals() const
{
using namespace blender;
if (!this->runtime->vert_normals_dirty) {
BLI_assert(this->runtime->vert_normals.size() == this->totvert);
return this->runtime->vert_normals;
if (this->runtime->vert_normals_cache.is_cached()) {
return this->runtime->vert_normals_cache.data();
}
std::lock_guard lock{this->runtime->normals_mutex};
if (!this->runtime->vert_normals_dirty) {
BLI_assert(this->runtime->vert_normals.size() == this->totvert);
return this->runtime->vert_normals;
const Span<float3> positions = this->vert_positions();
const OffsetIndices faces = this->faces();
const Span<int> corner_verts = this->corner_verts();
/* Calculating only vertex normals based on precalculated face normals is faster, but if face
* normals are dirty, calculating both at the same time can be slightly faster. Since normal
* calculation commonly has a significant performance impact, we maintain both code paths. */
if (this->runtime->face_normals_cache.is_cached()) {
const Span<float3> face_normals = this->face_normals();
this->runtime->vert_normals_cache.ensure([&](Vector<float3> &r_data) {
r_data.reinitialize(positions.size());
bke::mesh::normals_calc_verts(positions, faces, corner_verts, face_normals, r_data);
});
}
else {
Vector<float3> face_normals(faces.size());
this->runtime->vert_normals_cache.ensure([&](Vector<float3> &r_data) {
r_data.reinitialize(positions.size());
bke::mesh::normals_calc_faces_and_verts(
positions, faces, corner_verts, face_normals, r_data);
});
this->runtime->face_normals_cache.ensure(
[&](Vector<float3> &r_data) { r_data = std::move(face_normals); });
}
/* Isolate task because a mutex is locked and computing normals is multi-threaded. */
threading::isolate_task([&]() {
const Span<float3> positions = this->vert_positions();
const OffsetIndices faces = this->faces();
const Span<int> corner_verts = this->corner_verts();
this->runtime->vert_normals.reinitialize(positions.size());
this->runtime->face_normals.reinitialize(faces.size());
bke::mesh::normals_calc_face_vert(
positions, faces, corner_verts, this->runtime->face_normals, this->runtime->vert_normals);
this->runtime->vert_normals_dirty = false;
this->runtime->face_normals_dirty = false;
});
return this->runtime->vert_normals;
return this->runtime->vert_normals_cache.data();
}
blender::Span<blender::float3> Mesh::face_normals() const
{
using namespace blender;
if (!this->runtime->face_normals_dirty) {
BLI_assert(this->runtime->face_normals.size() == this->faces_num);
return this->runtime->face_normals;
}
std::lock_guard lock{this->runtime->normals_mutex};
if (!this->runtime->face_normals_dirty) {
BLI_assert(this->runtime->face_normals.size() == this->faces_num);
return this->runtime->face_normals;
}
/* Isolate task because a mutex is locked and computing normals is multi-threaded. */
threading::isolate_task([&]() {
this->runtime->face_normals_cache.ensure([&](Vector<float3> &r_data) {
const Span<float3> positions = this->vert_positions();
const OffsetIndices faces = this->faces();
const Span<int> corner_verts = this->corner_verts();
this->runtime->face_normals.reinitialize(faces.size());
bke::mesh::normals_calc_faces(positions, faces, corner_verts, this->runtime->face_normals);
this->runtime->face_normals_dirty = false;
r_data.reinitialize(faces.size());
bke::mesh::normals_calc_faces(positions, faces, corner_verts, r_data);
});
return this->runtime->face_normals;
return this->runtime->face_normals_cache.data();
}
void BKE_mesh_ensure_normals_for_display(Mesh *mesh)

View File

@ -62,14 +62,6 @@ static void free_bvh_cache(MeshRuntime &mesh_runtime)
}
}
static void reset_normals(MeshRuntime &mesh_runtime)
{
mesh_runtime.vert_normals.clear_and_shrink();
mesh_runtime.face_normals.clear_and_shrink();
mesh_runtime.vert_normals_dirty = true;
mesh_runtime.face_normals_dirty = true;
}
static void free_batch_cache(MeshRuntime &mesh_runtime)
{
if (mesh_runtime.batch_cache) {
@ -261,9 +253,10 @@ void BKE_mesh_runtime_clear_geometry(Mesh *mesh)
{
/* Tagging shared caches dirty will free the allocated data if there is only one user. */
free_bvh_cache(*mesh->runtime);
reset_normals(*mesh->runtime);
free_subdiv_ccg(*mesh->runtime);
mesh->runtime->bounds_cache.tag_dirty();
mesh->runtime->vert_normals_cache.tag_dirty();
mesh->runtime->face_normals_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();
@ -279,11 +272,9 @@ void BKE_mesh_runtime_clear_geometry(Mesh *mesh)
void BKE_mesh_tag_edges_split(Mesh *mesh)
{
/* Triangulation didn't change because vertex positions and loop vertex indices didn't change.
* Face normals didn't change either, but tag those anyway, since there is no API function to
* only tag vertex normals dirty. */
/* Triangulation didn't change because vertex positions and loop vertex indices didn't change. */
free_bvh_cache(*mesh->runtime);
reset_normals(*mesh->runtime);
mesh->runtime->vert_normals_cache.tag_dirty();
free_subdiv_ccg(*mesh->runtime);
if (mesh->runtime->loose_edges_cache.is_cached() &&
mesh->runtime->loose_edges_cache.data().count != 0)
@ -310,14 +301,14 @@ void BKE_mesh_tag_edges_split(Mesh *mesh)
void BKE_mesh_tag_face_winding_changed(Mesh *mesh)
{
mesh->runtime->vert_normals_dirty = true;
mesh->runtime->face_normals_dirty = true;
mesh->runtime->vert_normals_cache.tag_dirty();
mesh->runtime->face_normals_cache.tag_dirty();
}
void BKE_mesh_tag_positions_changed(Mesh *mesh)
{
mesh->runtime->vert_normals_dirty = true;
mesh->runtime->face_normals_dirty = true;
mesh->runtime->vert_normals_cache.tag_dirty();
mesh->runtime->face_normals_cache.tag_dirty();
free_bvh_cache(*mesh->runtime);
mesh->runtime->looptris_cache.tag_dirty();
mesh->runtime->bounds_cache.tag_dirty();

View File

@ -1316,13 +1316,13 @@ static void pbvh_faces_update_normals(PBVH *pbvh, Span<PBVHNode *> nodes, Mesh &
VectorSet<int> verts_to_update;
threading::parallel_invoke(
[&]() {
MutableSpan<float3> face_normals = mesh.runtime->face_normals;
threading::parallel_for(faces_to_update.index_range(), 512, [&](const IndexRange range) {
for (const int i : faces_to_update.as_span().slice(range)) {
face_normals[i] = mesh::face_normal_calc(positions, corner_verts.slice(faces[i]));
}
mesh.runtime->face_normals_cache.ensure([&](Vector<float3> &r_data) {
threading::parallel_for(faces_to_update.index_range(), 512, [&](const IndexRange range) {
for (const int i : faces_to_update.as_span().slice(range)) {
r_data[i] = mesh::face_normal_calc(positions, corner_verts.slice(faces[i]));
}
});
});
mesh.runtime->face_normals_dirty = false;
},
[&]() {
/* Update all normals connected to affected faces, even if not explicitly tagged. */
@ -1339,18 +1339,18 @@ static void pbvh_faces_update_normals(PBVH *pbvh, Span<PBVHNode *> nodes, Mesh &
}
});
const Span<float3> face_normals = mesh.runtime->face_normals;
MutableSpan<float3> vert_normals = mesh.runtime->vert_normals;
threading::parallel_for(verts_to_update.index_range(), 1024, [&](const IndexRange range) {
for (const int vert : verts_to_update.as_span().slice(range)) {
float3 normal(0.0f);
for (const int face : pbvh->pmap[vert]) {
normal += face_normals[face];
const Span<float3> face_normals = mesh.face_normals();
mesh.runtime->vert_normals_cache.ensure([&](Vector<float3> &r_data) {
threading::parallel_for(verts_to_update.index_range(), 1024, [&](const IndexRange range) {
for (const int vert : verts_to_update.as_span().slice(range)) {
float3 normal(0.0f);
for (const int face : pbvh->pmap[vert]) {
normal += face_normals[face];
}
r_data[vert] = math::normalize(normal);
}
vert_normals[vert] = math::normalize(normal);
}
});
});
mesh.runtime->vert_normals_dirty = false;
}
static void node_update_mask_redraw(PBVH &pbvh, PBVHNode &node)

View File

@ -183,8 +183,8 @@ static void rna_Mesh_update(Mesh *mesh,
/* Default state is not to have tessface's so make sure this is the case. */
BKE_mesh_tessface_clear(mesh);
mesh->runtime->vert_normals_dirty = true;
mesh->runtime->face_normals_dirty = true;
mesh->runtime->vert_normals_cache.tag_dirty();
mesh->runtime->face_normals_cache.tag_dirty();
DEG_id_tag_update(&mesh->id, 0);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, mesh);