WIP: Mesh: Refactor vertex normals for determinism #105920
|
@ -341,11 +341,6 @@ const float (*BKE_mesh_poly_normals_ensure(const struct Mesh *mesh))[3];
|
||||||
*/
|
*/
|
||||||
float (*BKE_mesh_vert_normals_for_write(struct Mesh *mesh))[3];
|
float (*BKE_mesh_vert_normals_for_write(struct Mesh *mesh))[3];
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark the mesh's vertex normals non-dirty, for when they are calculated or assigned manually.
|
|
||||||
*/
|
|
||||||
void BKE_mesh_vert_normals_clear_dirty(struct Mesh *mesh);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if the mesh vertex normals either are not stored or are dirty.
|
* Return true if the mesh vertex normals either are not stored or are dirty.
|
||||||
* This can be used to help decide whether to transfer them when copying a mesh.
|
* This can be used to help decide whether to transfer them when copying a mesh.
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
* \ingroup bke
|
* \ingroup bke
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "BLI_offset_indices.hh"
|
||||||
|
|
||||||
#include "BKE_mesh.h"
|
#include "BKE_mesh.h"
|
||||||
|
|
||||||
namespace blender::bke::mesh {
|
namespace blender::bke::mesh {
|
||||||
|
@ -66,15 +68,16 @@ void normals_calc_polys(Span<float3> vert_positions,
|
||||||
MutableSpan<float3> poly_normals);
|
MutableSpan<float3> poly_normals);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate face and vertex normals directly into result arrays.
|
* Calculate vertex normals directly into result array.
|
||||||
*
|
*
|
||||||
* \note Usually #Mesh::vert_normals() is the preferred way to access vertex normals,
|
* \note Usually #Mesh::vert_normals() is the preferred way to access vertex normals,
|
||||||
* since they may already be calculated and cached on the mesh.
|
* since they may already be calculated and cached on the mesh.
|
||||||
*/
|
*/
|
||||||
void normals_calc_poly_vert(Span<float3> vert_positions,
|
void normals_calc_verts(Span<float3> positions,
|
||||||
OffsetIndices<int> polys,
|
OffsetIndices<int> polys,
|
||||||
Span<int> corner_verts,
|
Span<int> corner_verts,
|
||||||
MutableSpan<float3> poly_normals,
|
const VertToPolyMap &vert_to_poly,
|
||||||
|
Span<float3> poly_normals,
|
||||||
MutableSpan<float3> vert_normals);
|
MutableSpan<float3> vert_normals);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -208,10 +211,62 @@ inline int edge_other_vert(const int2 &edge, const int vert)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace blender::bke::mesh
|
||||||
|
|
||||||
|
namespace blender::bke {
|
||||||
|
|
||||||
|
/** Set mesh vertex normals to known-correct values, avoiding future lazy computation. */
|
||||||
|
void mesh_vert_normals_assign(Mesh &mesh, Span<float3> vert_normals);
|
||||||
|
/** Set mesh vertex normals to known-correct values, avoiding future lazy computation. */
|
||||||
|
void mesh_vert_normals_assign(Mesh &mesh, Vector<float3> vert_normals);
|
||||||
|
|
||||||
|
} // namespace blender::bke
|
||||||
|
|
||||||
/** \} */
|
/** \} */
|
||||||
|
|
||||||
} // namespace blender::bke::mesh
|
} // namespace blender::bke::mesh
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/** \name Mesh Topology Caches
|
||||||
|
* \{ */
|
||||||
|
|
||||||
|
namespace blender::bke::mesh {
|
||||||
|
|
||||||
|
class VertToPolyMap {
|
||||||
|
OffsetIndices<int> offsets_;
|
||||||
|
Span<int> indices_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
VertToPolyMap() = default;
|
||||||
|
VertToPolyMap(OffsetIndices<int> offsets, Span<int> indices)
|
||||||
|
: offsets_(offsets), indices_(indices)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
/* Indices of all faces using the indexed vertex. */
|
||||||
|
Span<int> operator[](const int64_t vert_index) const
|
||||||
|
{
|
||||||
|
return indices_.slice(offsets_[vert_index]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build offsets per vertex used to slice arrays containing the indices of connected
|
||||||
|
* faces or face corners (each vertex used by the same number of corners and faces).
|
||||||
|
*/
|
||||||
|
void build_poly_and_corner_by_vert_offsets(Span<int> corner_verts, MutableSpan<int> offsets);
|
||||||
|
/**
|
||||||
|
* Fill the indices of polygons connected to each vertex, ordered smallest index to largest.
|
||||||
|
* \param offsets: Encodes the number of polygons connected to each vertex.
|
||||||
|
*/
|
||||||
|
void build_vert_to_poly_indices(OffsetIndices<int> polys,
|
||||||
|
Span<int> corner_verts,
|
||||||
|
OffsetIndices<int> offsets,
|
||||||
|
MutableSpan<int> poly_indices);
|
||||||
|
|
||||||
|
} // namespace blender::bke::mesh
|
||||||
|
|
||||||
|
/** \} */
|
||||||
|
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
/** \name Inline Mesh Data Access
|
/** \name Inline Mesh Data Access
|
||||||
* \{ */
|
* \{ */
|
||||||
|
|
|
@ -158,17 +158,20 @@ struct MeshRuntime {
|
||||||
*/
|
*/
|
||||||
SubsurfRuntimeData *subsurf_runtime_data = nullptr;
|
SubsurfRuntimeData *subsurf_runtime_data = nullptr;
|
||||||
|
|
||||||
/**
|
/** Cache of lazily calculated vertex normals. Depends on #poly_normals_cache. */
|
||||||
* Caches for lazily computed vertex and polygon normals. These are stored here rather than in
|
SharedCache<Vector<float3>> vert_normals_cache;
|
||||||
* #CustomData because they can be calculated on a `const` mesh, and adding custom data layers on
|
|
||||||
* a `const` mesh is not thread-safe.
|
/** Cache of lazily calculated face normals. Depends on positions and topology. */
|
||||||
*/
|
SharedCache<Vector<float3>> poly_normals_cache;
|
||||||
bool vert_normals_dirty = true;
|
|
||||||
bool poly_normals_dirty = true;
|
|
||||||
mutable Vector<float3> vert_normals;
|
|
||||||
mutable Vector<float3> poly_normals;
|
|
||||||
|
|
||||||
/** Cache of data about edges not used by faces. See #Mesh::loose_edges(). */
|
/** Cache of data about edges not used by faces. See #Mesh::loose_edges(). */
|
||||||
|
SharedCache<Vector<int>> vert_to_corner_offset_cache;
|
||||||
|
SharedCache<Vector<int>> vert_to_poly_indices_cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache of data about the loose edges. Can be shared with other data-blocks with unchanged
|
||||||
|
* topology. Accessed with #Mesh::loose_edges().
|
||||||
|
*/
|
||||||
SharedCache<LooseEdgeCache> loose_edges_cache;
|
SharedCache<LooseEdgeCache> loose_edges_cache;
|
||||||
/** Cache of data about vertices not used by edges. See #Mesh::loose_verts(). */
|
/** Cache of data about vertices not used by edges. See #Mesh::loose_verts(). */
|
||||||
SharedCache<LooseVertCache> loose_verts_cache;
|
SharedCache<LooseVertCache> loose_verts_cache;
|
||||||
|
|
|
@ -2266,11 +2266,12 @@ void BKE_keyblock_mesh_calc_normals(const KeyBlock *kb,
|
||||||
{reinterpret_cast<blender::float3 *>(poly_normals), polys.size()});
|
{reinterpret_cast<blender::float3 *>(poly_normals), polys.size()});
|
||||||
}
|
}
|
||||||
if (vert_normals_needed) {
|
if (vert_normals_needed) {
|
||||||
blender::bke::mesh::normals_calc_poly_vert(
|
blender::bke::mesh::normals_calc_verts(
|
||||||
{reinterpret_cast<const blender::float3 *>(positions), mesh->totvert},
|
{reinterpret_cast<const blender::float3 *>(positions), mesh->totvert},
|
||||||
polys,
|
polys,
|
||||||
corner_verts,
|
corner_verts,
|
||||||
{reinterpret_cast<blender::float3 *>(poly_normals), polys.size()},
|
mesh->vert_to_poly_map(),
|
||||||
|
{reinterpret_cast<const blender::float3 *>(poly_normals), polys.size()},
|
||||||
{reinterpret_cast<blender::float3 *>(vert_normals), mesh->totvert});
|
{reinterpret_cast<blender::float3 *>(vert_normals), mesh->totvert});
|
||||||
}
|
}
|
||||||
if (loop_normals_needed) {
|
if (loop_normals_needed) {
|
||||||
|
|
|
@ -1484,10 +1484,7 @@ Mesh *BKE_mball_polygonize(Depsgraph *depsgraph, Scene *scene, Object *ob)
|
||||||
for (int i = 0; i < mesh->totvert; i++) {
|
for (int i = 0; i < mesh->totvert; i++) {
|
||||||
normalize_v3(process.no[i]);
|
normalize_v3(process.no[i]);
|
||||||
}
|
}
|
||||||
memcpy(BKE_mesh_vert_normals_for_write(mesh),
|
blender::bke::mesh_vert_normals_assign(*mesh, std::move(process.no));
|
||||||
process.no.data(),
|
|
||||||
sizeof(float[3]) * size_t(mesh->totvert));
|
|
||||||
BKE_mesh_vert_normals_clear_dirty(mesh);
|
|
||||||
|
|
||||||
BKE_mesh_calc_edges(mesh, false, false);
|
BKE_mesh_calc_edges(mesh, false, false);
|
||||||
|
|
||||||
|
|
|
@ -134,6 +134,10 @@ static void mesh_copy_data(Main *bmain, ID *id_dst, const ID *id_src, const int
|
||||||
mesh_dst->runtime->verts_no_face_cache = mesh_src->runtime->verts_no_face_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->loose_edges_cache = mesh_src->runtime->loose_edges_cache;
|
||||||
mesh_dst->runtime->looptris_cache = mesh_src->runtime->looptris_cache;
|
mesh_dst->runtime->looptris_cache = mesh_src->runtime->looptris_cache;
|
||||||
|
mesh_dst->runtime->vert_normals_cache = mesh_src->runtime->vert_normals_cache;
|
||||||
|
mesh_dst->runtime->poly_normals_cache = mesh_src->runtime->poly_normals_cache;
|
||||||
|
mesh_dst->runtime->vert_to_corner_offset_cache = mesh_src->runtime->vert_to_corner_offset_cache;
|
||||||
|
mesh_dst->runtime->vert_to_poly_indices_cache = mesh_src->runtime->vert_to_poly_indices_cache;
|
||||||
|
|
||||||
/* Only do tessface if we have no polys. */
|
/* Only do tessface if we have no polys. */
|
||||||
const bool do_tessface = ((mesh_src->totface != 0) && (mesh_src->totpoly == 0));
|
const bool do_tessface = ((mesh_src->totface != 0) && (mesh_src->totpoly == 0));
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
#include "BLI_utildefines.h"
|
#include "BLI_utildefines.h"
|
||||||
|
|
||||||
#include "BKE_customdata.h"
|
#include "BKE_customdata.h"
|
||||||
#include "BKE_mesh.h"
|
#include "BKE_mesh.hh"
|
||||||
#include "BKE_mesh_mapping.h"
|
#include "BKE_mesh_mapping.h"
|
||||||
#include "BLI_memarena.h"
|
#include "BLI_memarena.h"
|
||||||
|
|
||||||
|
@ -521,6 +521,34 @@ void BKE_mesh_origindex_map_create_looptri(MeshElemMap **r_map,
|
||||||
*r_mem = indices;
|
*r_mem = indices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace blender::bke::mesh {
|
||||||
|
|
||||||
|
void build_poly_and_corner_by_vert_offsets(const Span<int> corner_verts, MutableSpan<int> offsets)
|
||||||
|
{
|
||||||
|
BLI_assert(std::all_of(offsets.begin(), offsets.end(), [](int value) { return value == 0; }));
|
||||||
|
for (const int vert : corner_verts) {
|
||||||
|
offsets[vert]++;
|
||||||
|
}
|
||||||
|
offset_indices::accumulate_counts_to_offsets(offsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
void build_vert_to_poly_indices(const OffsetIndices<int> polys,
|
||||||
|
const Span<int> corner_verts,
|
||||||
|
const OffsetIndices<int> offsets,
|
||||||
|
MutableSpan<int> poly_indices)
|
||||||
|
{
|
||||||
|
BLI_assert(poly_indices.size() == corner_verts.size());
|
||||||
|
Array<int> counts(offsets.size(), 0);
|
||||||
|
for (const int64_t i : polys.index_range()) {
|
||||||
|
for (const int vert : corner_verts.slice(polys[i])) {
|
||||||
|
poly_indices[offsets[vert][counts[vert]]] = int(i);
|
||||||
|
counts[vert]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace blender::bke::mesh
|
||||||
|
|
||||||
namespace blender::bke::mesh_topology {
|
namespace blender::bke::mesh_topology {
|
||||||
|
|
||||||
Array<int> build_loop_to_poly_map(const OffsetIndices<int> polys)
|
Array<int> build_loop_to_poly_map(const OffsetIndices<int> polys)
|
||||||
|
|
|
@ -42,72 +42,53 @@
|
||||||
# include "BLI_timeit.hh"
|
# include "BLI_timeit.hh"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* -------------------------------------------------------------------- */
|
|
||||||
/** \name Private Utility Functions
|
|
||||||
* \{ */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A thread-safe version of #add_v3_v3 that uses a spin-lock.
|
|
||||||
*
|
|
||||||
* \note Avoid using this when the chance of contention is high.
|
|
||||||
*/
|
|
||||||
static void add_v3_v3_atomic(float r[3], const float a[3])
|
|
||||||
{
|
|
||||||
#define FLT_EQ_NONAN(_fa, _fb) (*((const uint32_t *)&_fa) == *((const uint32_t *)&_fb))
|
|
||||||
|
|
||||||
float virtual_lock = r[0];
|
|
||||||
while (true) {
|
|
||||||
/* This loops until following conditions are met:
|
|
||||||
* - `r[0]` has same value as virtual_lock (i.e. it did not change since last try).
|
|
||||||
* - `r[0]` was not `FLT_MAX`, i.e. it was not locked by another thread. */
|
|
||||||
const float test_lock = atomic_cas_float(&r[0], virtual_lock, FLT_MAX);
|
|
||||||
if (_ATOMIC_LIKELY(FLT_EQ_NONAN(test_lock, virtual_lock) && (test_lock != FLT_MAX))) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
virtual_lock = test_lock;
|
|
||||||
}
|
|
||||||
virtual_lock += a[0];
|
|
||||||
r[1] += a[1];
|
|
||||||
r[2] += a[2];
|
|
||||||
|
|
||||||
/* Second atomic operation to 'release'
|
|
||||||
* our lock on that vector and set its first scalar value. */
|
|
||||||
/* Note that we do not need to loop here, since we 'locked' `r[0]`,
|
|
||||||
* nobody should have changed it in the mean time. */
|
|
||||||
virtual_lock = atomic_cas_float(&r[0], FLT_MAX, virtual_lock);
|
|
||||||
BLI_assert(virtual_lock == FLT_MAX);
|
|
||||||
|
|
||||||
#undef FLT_EQ_NONAN
|
|
||||||
}
|
|
||||||
|
|
||||||
/** \} */
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
/** \name Public Utility Functions
|
/** \name Public Utility Functions
|
||||||
*
|
*
|
||||||
* Related to managing normals but not directly related to calculating normals.
|
* Related to managing normals but not directly related to calculating normals.
|
||||||
* \{ */
|
* \{ */
|
||||||
|
|
||||||
float (*BKE_mesh_vert_normals_for_write(Mesh *mesh))[3]
|
namespace blender::bke {
|
||||||
|
|
||||||
|
void mesh_vert_normals_assign(Mesh &mesh, Span<float3> vert_normals)
|
||||||
{
|
{
|
||||||
mesh->runtime->vert_normals.reinitialize(mesh->totvert);
|
BLI_assert(!mesh.runtime->vert_normals_cache.is_cached());
|
||||||
return reinterpret_cast<float(*)[3]>(mesh->runtime->vert_normals.data());
|
mesh.runtime->vert_normals_cache.ensure([&](Vector<float3> &r_data) { r_data = vert_normals; });
|
||||||
}
|
}
|
||||||
|
|
||||||
void BKE_mesh_vert_normals_clear_dirty(Mesh *mesh)
|
void mesh_vert_normals_assign(Mesh &mesh, Vector<float3> vert_normals)
|
||||||
{
|
{
|
||||||
mesh->runtime->vert_normals_dirty = false;
|
BLI_assert(!mesh.runtime->vert_normals_cache.is_cached());
|
||||||
BLI_assert(mesh->runtime->vert_normals.size() == mesh->totvert);
|
mesh.runtime->vert_normals_cache.ensure(
|
||||||
|
[vert_normals = std::move(vert_normals)](Vector<float3> &r_data) {
|
||||||
|
r_data = std::move(vert_normals);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace blender::bke
|
||||||
|
|
||||||
|
float (*BKE_mesh_vert_normals_for_write(Mesh *mesh))[3]
|
||||||
|
{
|
||||||
|
/* Make sure the normals aren't shared. */
|
||||||
|
using namespace blender;
|
||||||
|
Vector<float3> vert_normals = mesh->vert_normals();
|
||||||
|
mesh->runtime->vert_normals_cache.ensure(
|
||||||
|
[vert_normals = std::move(vert_normals)](Vector<float3> &r_data) {
|
||||||
|
r_data = std::move(vert_normals);
|
||||||
|
});
|
||||||
|
/* Give write access to the normals now used just by this mesh. */
|
||||||
|
return reinterpret_cast<float(*)[3]>(
|
||||||
|
const_cast<float3 *>(mesh->runtime->vert_normals_cache.data().data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BKE_mesh_vert_normals_are_dirty(const Mesh *mesh)
|
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_poly_normals_are_dirty(const Mesh *mesh)
|
bool BKE_mesh_poly_normals_are_dirty(const Mesh *mesh)
|
||||||
{
|
{
|
||||||
return mesh->runtime->poly_normals_dirty;
|
return mesh->runtime->poly_normals_cache.is_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** \} */
|
/** \} */
|
||||||
|
@ -205,92 +186,45 @@ void normals_calc_polys(const Span<float3> positions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void normals_calc_poly_vert(const Span<float3> positions,
|
/** \} */
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/** \name Mesh Normal Calculation (Polygons & Vertices)
|
||||||
|
*
|
||||||
|
* Take care making optimizations to this function as improvements to low-poly
|
||||||
|
* meshes can slow down high-poly meshes. For details on performance, see D11993.
|
||||||
|
* \{ */
|
||||||
|
|
||||||
|
void normals_calc_verts(const Span<float3> positions,
|
||||||
const OffsetIndices<int> polys,
|
const OffsetIndices<int> polys,
|
||||||
const Span<int> corner_verts,
|
const Span<int> corner_verts,
|
||||||
MutableSpan<float3> poly_normals,
|
const VertToPolyMap &vert_to_poly,
|
||||||
|
const Span<float3> poly_normals,
|
||||||
MutableSpan<float3> vert_normals)
|
MutableSpan<float3> vert_normals)
|
||||||
{
|
{
|
||||||
|
threading::parallel_for(positions.index_range(), 1024, [=](const IndexRange range) {
|
||||||
/* Zero the vertex normal array for accumulation. */
|
for (const int vert : range) {
|
||||||
{
|
const Span<int> polys_around_vert = vert_to_poly[vert];
|
||||||
memset(vert_normals.data(), 0, vert_normals.as_span().size_in_bytes());
|
if (polys_around_vert.is_empty()) {
|
||||||
|
vert_normals[vert] = math::normalize(positions[vert]);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compute poly normals, accumulating them into vertex normals. */
|
const float factor_inv = 1.0f / polys_around_vert.size();
|
||||||
{
|
float3 vert_normal(0);
|
||||||
threading::parallel_for(polys.index_range(), 1024, [&](const IndexRange range) {
|
for (const int poly_index : polys_around_vert) {
|
||||||
for (const int poly_i : range) {
|
const IndexRange poly = polys[poly_index];
|
||||||
const Span<int> poly_verts = corner_verts.slice(polys[poly_i]);
|
const int2 adjacent_verts = poly_find_adjecent_verts(poly, corner_verts, vert);
|
||||||
|
|
||||||
float3 &pnor = poly_normals[poly_i];
|
const float3 dir_prev = math::normalize(positions[adjacent_verts[0]] - positions[vert]);
|
||||||
|
const float3 dir_next = math::normalize(positions[adjacent_verts[1]] - positions[vert]);
|
||||||
|
const float factor = saacos(math::dot(dir_prev, dir_next));
|
||||||
|
|
||||||
const int i_end = poly_verts.size() - 1;
|
vert_normal += poly_normals[poly_index] * factor * factor_inv;
|
||||||
|
|
||||||
/* Polygon Normal and edge-vector. */
|
|
||||||
/* Inline version of #poly_normal_calc, also does edge-vectors. */
|
|
||||||
{
|
|
||||||
zero_v3(pnor);
|
|
||||||
/* Newell's Method */
|
|
||||||
const float *v_curr = positions[poly_verts[i_end]];
|
|
||||||
for (int i_next = 0; i_next <= i_end; i_next++) {
|
|
||||||
const float *v_next = positions[poly_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[poly_verts[i_end]];
|
|
||||||
sub_v3_v3v3(edvec_prev, positions[poly_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[poly_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 poly 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[poly_verts[i_curr]];
|
|
||||||
add_v3_v3_atomic(vnor, vnor_add);
|
|
||||||
v_curr = v_next;
|
|
||||||
copy_v3_v3(edvec_prev, edvec_next);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
vert_normals[vert] = vert_normal;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/* Normalize and validate computed vertex normals. */
|
|
||||||
{
|
|
||||||
threading::parallel_for(positions.index_range(), 1024, [&](const IndexRange range) {
|
|
||||||
for (const int vert_i : range) {
|
|
||||||
float *no = vert_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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** \} */
|
/** \} */
|
||||||
|
@ -309,6 +243,8 @@ blender::Span<blender::float3> Mesh::vert_normals() const
|
||||||
return this->runtime->vert_normals;
|
return this->runtime->vert_normals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Span<float3> poly_normals = this->poly_normals();
|
||||||
|
|
||||||
std::lock_guard lock{this->runtime->normals_mutex};
|
std::lock_guard lock{this->runtime->normals_mutex};
|
||||||
if (!this->runtime->vert_normals_dirty) {
|
if (!this->runtime->vert_normals_dirty) {
|
||||||
BLI_assert(this->runtime->vert_normals.size() == this->totvert);
|
BLI_assert(this->runtime->vert_normals.size() == this->totvert);
|
||||||
|
@ -321,16 +257,18 @@ blender::Span<blender::float3> Mesh::vert_normals() const
|
||||||
const OffsetIndices polys = this->polys();
|
const OffsetIndices polys = this->polys();
|
||||||
const Span<int> corner_verts = this->corner_verts();
|
const Span<int> corner_verts = this->corner_verts();
|
||||||
|
|
||||||
this->runtime->vert_normals.reinitialize(positions.size());
|
bke::mesh::VertToPolyMap vert_to_poly;
|
||||||
this->runtime->poly_normals.reinitialize(polys.size());
|
Span<float3> poly_normals;
|
||||||
bke::mesh::normals_calc_poly_vert(
|
threading::parallel_invoke(
|
||||||
positions, polys, corner_verts, this->runtime->poly_normals, this->runtime->vert_normals);
|
this->totvert > 1024,
|
||||||
|
[&]() { vert_to_poly = this->vert_to_poly_map(); },
|
||||||
|
[&]() { poly_normals = this->poly_normals(); });
|
||||||
|
|
||||||
this->runtime->vert_normals_dirty = false;
|
r_data.reinitialize(positions.size());
|
||||||
this->runtime->poly_normals_dirty = false;
|
bke::mesh::normals_calc_verts(
|
||||||
|
positions, polys, corner_verts, vert_to_poly, poly_normals, r_data);
|
||||||
});
|
});
|
||||||
|
return this->runtime->vert_normals_cache.data();
|
||||||
return this->runtime->vert_normals;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blender::Span<blender::float3> Mesh::poly_normals() const
|
blender::Span<blender::float3> Mesh::poly_normals() const
|
||||||
|
@ -358,8 +296,7 @@ blender::Span<blender::float3> Mesh::poly_normals() const
|
||||||
|
|
||||||
this->runtime->poly_normals_dirty = false;
|
this->runtime->poly_normals_dirty = false;
|
||||||
});
|
});
|
||||||
|
return this->runtime->poly_normals_cache.data();
|
||||||
return this->runtime->poly_normals;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const float (*BKE_mesh_vert_normals_ensure(const Mesh *mesh))[3]
|
const float (*BKE_mesh_vert_normals_ensure(const Mesh *mesh))[3]
|
||||||
|
|
|
@ -78,14 +78,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.poly_normals.clear_and_shrink();
|
|
||||||
mesh_runtime.vert_normals_dirty = true;
|
|
||||||
mesh_runtime.poly_normals_dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void free_batch_cache(MeshRuntime &mesh_runtime)
|
static void free_batch_cache(MeshRuntime &mesh_runtime)
|
||||||
{
|
{
|
||||||
if (mesh_runtime.batch_cache) {
|
if (mesh_runtime.batch_cache) {
|
||||||
|
@ -150,6 +142,26 @@ static void try_tag_verts_no_face_none(const Mesh &mesh)
|
||||||
|
|
||||||
} // namespace blender::bke
|
} // namespace blender::bke
|
||||||
|
|
||||||
|
blender::bke::mesh::VertToPolyMap Mesh::vert_to_poly_map() const
|
||||||
|
{
|
||||||
|
using namespace blender;
|
||||||
|
this->runtime->vert_to_corner_offset_cache.ensure([&](Vector<int> &r_data) {
|
||||||
|
r_data.clear();
|
||||||
|
r_data.resize(this->totvert + 1, 0);
|
||||||
|
bke::mesh::build_poly_and_corner_by_vert_offsets(this->corner_verts(), r_data);
|
||||||
|
});
|
||||||
|
const OffsetIndices<int> offsets(this->runtime->vert_to_corner_offset_cache.data());
|
||||||
|
|
||||||
|
this->runtime->vert_to_poly_indices_cache.ensure([&](blender::Vector<int> &r_data) {
|
||||||
|
r_data.reinitialize(this->totloop);
|
||||||
|
blender::bke::mesh::build_vert_to_poly_indices(
|
||||||
|
this->polys(), this->corner_verts(), offsets, r_data);
|
||||||
|
});
|
||||||
|
const Span<int> indices = this->runtime->vert_to_poly_indices_cache.data();
|
||||||
|
|
||||||
|
return {offsets, indices};
|
||||||
|
}
|
||||||
|
|
||||||
const blender::bke::LooseVertCache &Mesh::loose_verts() const
|
const blender::bke::LooseVertCache &Mesh::loose_verts() const
|
||||||
{
|
{
|
||||||
using namespace blender::bke;
|
using namespace blender::bke;
|
||||||
|
@ -274,9 +286,12 @@ void BKE_mesh_runtime_clear_geometry(Mesh *mesh)
|
||||||
{
|
{
|
||||||
/* Tagging shared caches dirty will free the allocated data if there is only one user. */
|
/* Tagging shared caches dirty will free the allocated data if there is only one user. */
|
||||||
free_bvh_cache(*mesh->runtime);
|
free_bvh_cache(*mesh->runtime);
|
||||||
reset_normals(*mesh->runtime);
|
|
||||||
free_subdiv_ccg(*mesh->runtime);
|
free_subdiv_ccg(*mesh->runtime);
|
||||||
|
mesh->runtime->vert_normals_cache.tag_dirty();
|
||||||
|
mesh->runtime->poly_normals_cache.tag_dirty();
|
||||||
mesh->runtime->bounds_cache.tag_dirty();
|
mesh->runtime->bounds_cache.tag_dirty();
|
||||||
|
mesh->runtime->vert_to_corner_offset_cache.tag_dirty();
|
||||||
|
mesh->runtime->vert_to_poly_indices_cache.tag_dirty();
|
||||||
mesh->runtime->loose_edges_cache.tag_dirty();
|
mesh->runtime->loose_edges_cache.tag_dirty();
|
||||||
mesh->runtime->loose_verts_cache.tag_dirty();
|
mesh->runtime->loose_verts_cache.tag_dirty();
|
||||||
mesh->runtime->verts_no_face_cache.tag_dirty();
|
mesh->runtime->verts_no_face_cache.tag_dirty();
|
||||||
|
@ -291,12 +306,12 @@ void BKE_mesh_runtime_clear_geometry(Mesh *mesh)
|
||||||
|
|
||||||
void BKE_mesh_tag_edges_split(struct Mesh *mesh)
|
void BKE_mesh_tag_edges_split(struct Mesh *mesh)
|
||||||
{
|
{
|
||||||
/* Triangulation didn't change because vertex positions and loop vertex indices didn't change.
|
/* 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. */
|
|
||||||
free_bvh_cache(*mesh->runtime);
|
free_bvh_cache(*mesh->runtime);
|
||||||
reset_normals(*mesh->runtime);
|
|
||||||
free_subdiv_ccg(*mesh->runtime);
|
free_subdiv_ccg(*mesh->runtime);
|
||||||
|
mesh->runtime->vert_normals_cache.tag_dirty();
|
||||||
|
mesh->runtime->vert_to_corner_offset_cache.tag_dirty();
|
||||||
|
mesh->runtime->vert_to_poly_indices_cache.tag_dirty();
|
||||||
mesh->runtime->loose_edges_cache.tag_dirty();
|
mesh->runtime->loose_edges_cache.tag_dirty();
|
||||||
mesh->runtime->loose_verts_cache.tag_dirty();
|
mesh->runtime->loose_verts_cache.tag_dirty();
|
||||||
mesh->runtime->verts_no_face_cache.tag_dirty();
|
mesh->runtime->verts_no_face_cache.tag_dirty();
|
||||||
|
@ -310,14 +325,14 @@ void BKE_mesh_tag_edges_split(struct Mesh *mesh)
|
||||||
|
|
||||||
void BKE_mesh_tag_face_winding_changed(Mesh *mesh)
|
void BKE_mesh_tag_face_winding_changed(Mesh *mesh)
|
||||||
{
|
{
|
||||||
mesh->runtime->vert_normals_dirty = true;
|
mesh->runtime->vert_normals_cache.tag_dirty();
|
||||||
mesh->runtime->poly_normals_dirty = true;
|
mesh->runtime->poly_normals_cache.tag_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BKE_mesh_tag_positions_changed(Mesh *mesh)
|
void BKE_mesh_tag_positions_changed(Mesh *mesh)
|
||||||
{
|
{
|
||||||
mesh->runtime->vert_normals_dirty = true;
|
mesh->runtime->vert_normals_cache.tag_dirty();
|
||||||
mesh->runtime->poly_normals_dirty = true;
|
mesh->runtime->poly_normals_cache.tag_dirty();
|
||||||
free_bvh_cache(*mesh->runtime);
|
free_bvh_cache(*mesh->runtime);
|
||||||
mesh->runtime->looptris_cache.tag_dirty();
|
mesh->runtime->looptris_cache.tag_dirty();
|
||||||
mesh->runtime->bounds_cache.tag_dirty();
|
mesh->runtime->bounds_cache.tag_dirty();
|
||||||
|
|
|
@ -1121,8 +1121,8 @@ void ED_mesh_update(Mesh *mesh, bContext *C, bool calc_edges, bool calc_edges_lo
|
||||||
/* Default state is not to have tessface's so make sure this is the case. */
|
/* Default state is not to have tessface's so make sure this is the case. */
|
||||||
BKE_mesh_tessface_clear(mesh);
|
BKE_mesh_tessface_clear(mesh);
|
||||||
|
|
||||||
mesh->runtime->vert_normals_dirty = true;
|
mesh->runtime->vert_normals_cache.tag_dirty();
|
||||||
mesh->runtime->poly_normals_dirty = true;
|
mesh->runtime->poly_normals_cache.tag_dirty();
|
||||||
|
|
||||||
DEG_id_tag_update(&mesh->id, 0);
|
DEG_id_tag_update(&mesh->id, 0);
|
||||||
WM_event_add_notifier(C, NC_GEOM | ND_DATA, mesh);
|
WM_event_add_notifier(C, NC_GEOM | ND_DATA, mesh);
|
||||||
|
|
|
@ -164,12 +164,12 @@ void read_mverts(Mesh &mesh, const P3fArraySamplePtr positions, const N3fArraySa
|
||||||
BKE_mesh_tag_positions_changed(&mesh);
|
BKE_mesh_tag_positions_changed(&mesh);
|
||||||
|
|
||||||
if (normals) {
|
if (normals) {
|
||||||
float(*vert_normals)[3] = BKE_mesh_vert_normals_for_write(&mesh);
|
Vector<float3> vert_normals(mesh.totvert);
|
||||||
for (const int64_t i : IndexRange(normals->size())) {
|
for (const int64_t i : IndexRange(normals->size())) {
|
||||||
Imath::V3f nor_in = (*normals)[i];
|
Imath::V3f nor_in = (*normals)[i];
|
||||||
copy_zup_from_yup(vert_normals[i], nor_in.getValue());
|
copy_zup_from_yup(vert_normals[i], nor_in.getValue());
|
||||||
}
|
}
|
||||||
BKE_mesh_vert_normals_clear_dirty(&mesh);
|
bke::mesh_vert_normals_assign(mesh, std::move(vert_normals));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -646,10 +646,8 @@ void USDMeshReader::process_normals_vertex_varying(Mesh *mesh)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MutableSpan vert_normals{(float3 *)BKE_mesh_vert_normals_for_write(mesh), mesh->totvert};
|
|
||||||
BLI_STATIC_ASSERT(sizeof(normals_[0]) == sizeof(float3), "Expected float3 normals size");
|
BLI_STATIC_ASSERT(sizeof(normals_[0]) == sizeof(float3), "Expected float3 normals size");
|
||||||
vert_normals.copy_from({(float3 *)normals_.data(), int64_t(normals_.size())});
|
bke::mesh_vert_normals_assign(*mesh, Span((float3 *)normals_.data(), int64_t(normals_.size())));
|
||||||
BKE_mesh_vert_normals_clear_dirty(mesh);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void USDMeshReader::process_normals_face_varying(Mesh *mesh)
|
void USDMeshReader::process_normals_face_varying(Mesh *mesh)
|
||||||
|
|
|
@ -29,6 +29,9 @@ class AttributeAccessor;
|
||||||
class MutableAttributeAccessor;
|
class MutableAttributeAccessor;
|
||||||
struct LooseVertCache;
|
struct LooseVertCache;
|
||||||
struct LooseEdgeCache;
|
struct LooseEdgeCache;
|
||||||
|
namespace mesh {
|
||||||
|
class VertToPolyMap;
|
||||||
|
}
|
||||||
} // namespace bke
|
} // namespace bke
|
||||||
} // namespace blender
|
} // namespace blender
|
||||||
using MeshRuntimeHandle = blender::bke::MeshRuntime;
|
using MeshRuntimeHandle = blender::bke::MeshRuntime;
|
||||||
|
@ -299,6 +302,11 @@ typedef struct Mesh {
|
||||||
/** Set cached mesh bounds to a known-correct value to avoid their lazy calculation later on. */
|
/** Set cached mesh bounds to a known-correct value to avoid their lazy calculation later on. */
|
||||||
void bounds_set_eager(const blender::Bounds<blender::float3> &bounds);
|
void bounds_set_eager(const blender::Bounds<blender::float3> &bounds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cached topology map of the faces connected to (using) each vertex.
|
||||||
|
*/
|
||||||
|
blender::bke::mesh::VertToPolyMap vert_to_poly_map() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cached information about loose edges, calculated lazily when necessary.
|
* Cached information about loose edges, calculated lazily when necessary.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -278,7 +278,7 @@ static void mesh_merge_transform(Mesh *result,
|
||||||
int cap_npolys,
|
int cap_npolys,
|
||||||
int *remap,
|
int *remap,
|
||||||
int remap_len,
|
int remap_len,
|
||||||
const bool recalc_normals_later)
|
MutableSpan<float3> dst_vert_normals)
|
||||||
{
|
{
|
||||||
using namespace blender;
|
using namespace blender;
|
||||||
int *index_orig;
|
int *index_orig;
|
||||||
|
@ -301,8 +301,7 @@ static void mesh_merge_transform(Mesh *result,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* We have to correct normals too, if we do not tag them as dirty later! */
|
/* We have to correct normals too, if we do not tag them as dirty later! */
|
||||||
if (!recalc_normals_later) {
|
if (!dst_vert_normals.is_empty()) {
|
||||||
float(*dst_vert_normals)[3] = BKE_mesh_vert_normals_for_write(result);
|
|
||||||
for (i = 0; i < cap_nverts; i++) {
|
for (i = 0; i < cap_nverts; i++) {
|
||||||
mul_mat3_m4_v3(cap_offset, dst_vert_normals[cap_verts_index + i]);
|
mul_mat3_m4_v3(cap_offset, dst_vert_normals[cap_verts_index + i]);
|
||||||
normalize_v3(dst_vert_normals[cap_verts_index + i]);
|
normalize_v3(dst_vert_normals[cap_verts_index + i]);
|
||||||
|
@ -578,11 +577,10 @@ static Mesh *arrayModifier_doArray(ArrayModifierData *amd,
|
||||||
|
|
||||||
unit_m4(current_offset);
|
unit_m4(current_offset);
|
||||||
blender::Span<blender::float3> src_vert_normals;
|
blender::Span<blender::float3> src_vert_normals;
|
||||||
float(*dst_vert_normals)[3] = nullptr;
|
Vector<float3> dst_vert_normals;
|
||||||
if (!use_recalc_normals) {
|
if (!use_recalc_normals) {
|
||||||
src_vert_normals = mesh->vert_normals();
|
src_vert_normals = mesh->vert_normals();
|
||||||
dst_vert_normals = BKE_mesh_vert_normals_for_write(result);
|
dst_vert_normals.reinitialize(result->totvert);
|
||||||
BKE_mesh_vert_normals_clear_dirty(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (c = 1; c < count; c++) {
|
for (c = 1; c < count; c++) {
|
||||||
|
@ -603,7 +601,7 @@ static Mesh *arrayModifier_doArray(ArrayModifierData *amd,
|
||||||
mul_m4_v3(current_offset, result_positions[i_dst]);
|
mul_m4_v3(current_offset, result_positions[i_dst]);
|
||||||
|
|
||||||
/* We have to correct normals too, if we do not tag them as dirty! */
|
/* We have to correct normals too, if we do not tag them as dirty! */
|
||||||
if (!use_recalc_normals) {
|
if (!dst_vert_normals.is_empty()) {
|
||||||
copy_v3_v3(dst_vert_normals[i_dst], src_vert_normals[i]);
|
copy_v3_v3(dst_vert_normals[i_dst], src_vert_normals[i]);
|
||||||
mul_mat3_m4_v3(current_offset, dst_vert_normals[i_dst]);
|
mul_mat3_m4_v3(current_offset, dst_vert_normals[i_dst]);
|
||||||
normalize_v3(dst_vert_normals[i_dst]);
|
normalize_v3(dst_vert_normals[i_dst]);
|
||||||
|
@ -752,7 +750,7 @@ static Mesh *arrayModifier_doArray(ArrayModifierData *amd,
|
||||||
start_cap_npolys,
|
start_cap_npolys,
|
||||||
vgroup_start_cap_remap,
|
vgroup_start_cap_remap,
|
||||||
vgroup_start_cap_remap_len,
|
vgroup_start_cap_remap_len,
|
||||||
use_recalc_normals);
|
dst_vert_normals);
|
||||||
/* Identify doubles with first chunk */
|
/* Identify doubles with first chunk */
|
||||||
if (use_merge) {
|
if (use_merge) {
|
||||||
dm_mvert_map_doubles(full_doubles_map,
|
dm_mvert_map_doubles(full_doubles_map,
|
||||||
|
@ -782,7 +780,7 @@ static Mesh *arrayModifier_doArray(ArrayModifierData *amd,
|
||||||
end_cap_npolys,
|
end_cap_npolys,
|
||||||
vgroup_end_cap_remap,
|
vgroup_end_cap_remap,
|
||||||
vgroup_end_cap_remap_len,
|
vgroup_end_cap_remap_len,
|
||||||
use_recalc_normals);
|
dst_vert_normals);
|
||||||
/* Identify doubles with last chunk */
|
/* Identify doubles with last chunk */
|
||||||
if (use_merge) {
|
if (use_merge) {
|
||||||
dm_mvert_map_doubles(full_doubles_map,
|
dm_mvert_map_doubles(full_doubles_map,
|
||||||
|
@ -796,6 +794,8 @@ static Mesh *arrayModifier_doArray(ArrayModifierData *amd,
|
||||||
}
|
}
|
||||||
/* done capping */
|
/* done capping */
|
||||||
|
|
||||||
|
blender::bke::mesh_vert_normals_assign(*result, std::move(dst_vert_normals));
|
||||||
|
|
||||||
/* Handle merging */
|
/* Handle merging */
|
||||||
tot_doubles = 0;
|
tot_doubles = 0;
|
||||||
if (use_merge) {
|
if (use_merge) {
|
||||||
|
|
|
@ -319,10 +319,9 @@ static Mesh *create_uv_sphere_mesh(const float radius,
|
||||||
threading::parallel_invoke(
|
threading::parallel_invoke(
|
||||||
1024 < segments * rings,
|
1024 < segments * rings,
|
||||||
[&]() {
|
[&]() {
|
||||||
MutableSpan vert_normals{reinterpret_cast<float3 *>(BKE_mesh_vert_normals_for_write(mesh)),
|
Vector<float3> vert_normals(mesh->totvert);
|
||||||
mesh->totvert};
|
|
||||||
calculate_sphere_vertex_data(positions, vert_normals, radius, segments, rings);
|
calculate_sphere_vertex_data(positions, vert_normals, radius, segments, rings);
|
||||||
BKE_mesh_vert_normals_clear_dirty(mesh);
|
bke::mesh_vert_normals_assign(*mesh, std::move(vert_normals));
|
||||||
},
|
},
|
||||||
[&]() { calculate_sphere_edge_indices(edges, segments, rings); },
|
[&]() { calculate_sphere_edge_indices(edges, segments, rings); },
|
||||||
[&]() { calculate_sphere_faces(poly_offsets, segments); },
|
[&]() { calculate_sphere_faces(poly_offsets, segments); },
|
||||||
|
|
Loading…
Reference in New Issue