Mesh: Share normals caches by splitting vertex and face calculation

Since vertex and face normals can be calculated separately, it simplifies
things to further separate the two caches. This makes it easier to use
`SharedCache` to avoid recalculating normals when copying meshes.

Sharing vertex normal caches with meshes with the same positions and
topology allows completely skipping recomputation as meshes are
copied. The effects are similar to e8f4010611, but normals are much
more expensive, so the benefit is larger.

In a simple test changing a large grid's generic attribute with geometry
nodes, I observed a performance improvement from 12 to 17 FPS.
Most real world situations will have smaller changes though.

Completely splitting face and vertex calculation is slightly slower
when face normals aren't already calculated, so I kept the option
to recalculate them together as well.

This simplifies investigating the changes in #105920 which resolve
non-determinism in the vertex normal calculation. If we can make the
topology map creation fast enough, that might allow simplifying this
code more in the future.

Pull Request: https://projects.blender.org/blender/blender/pulls/110479
This commit is contained in:
Hans Goudey
2023-08-25 23:06:06 +02:00
committed by Hans Goudey
parent da510b0d73
commit 383a145a19
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,
* 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,
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);