Hydra: Parallelize and simplify submesh export
- Only calculate normals on the necessary domain - Functions for exporting generic data - Parallelize export of multiple submeshes - Parallelize export within a single submesh - Resize vectors to correct size to avoid reallocation - Simplify hot loops to improve performance - Optimize single material case to avoid index remapping `write_submeshes` timing information (average of many runs) | Test | Before | After | | ------------------------------ | --------- | --------- | | 6 million vert mesh | 791.99 ms | 130.75 ms | | 1.5 million vert 100 materials | crash | 48.27 ms | | Mr. Elephant test file | 778.95 ms | 277.06 ms | Pull Request: https://projects.blender.org/blender/blender/pulls/113412
This commit is contained in:
@@ -6,7 +6,9 @@
|
||||
#include <pxr/base/tf/staticTokens.h>
|
||||
#include <pxr/imaging/hd/tokens.h>
|
||||
|
||||
#include "BLI_array_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_vector_set.hh"
|
||||
|
||||
#include "BKE_attribute.h"
|
||||
#include "BKE_material.h"
|
||||
@@ -217,6 +219,148 @@ const MeshData::SubMesh &MeshData::submesh(pxr::SdfPath const &id) const
|
||||
return submeshes_[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* #VtArray::resize() does value initialization of every new value, which ends up being `memset`
|
||||
* for the trivial attribute types we deal with here. This is unnecessary since every item is
|
||||
* initialized via copy from a Blender mesh here anyway. This specializes the resize call to skip
|
||||
* initialization.
|
||||
*/
|
||||
template<typename T> static void resize_uninitialized(pxr::VtArray<T> &array, const int new_size)
|
||||
{
|
||||
static_assert(std::is_trivial_v<T>);
|
||||
array.resize(new_size, [](auto /*begin*/, auto /*end*/) {});
|
||||
}
|
||||
|
||||
static std::pair<bke::MeshNormalDomain, Span<float3>> get_mesh_normals(const Mesh &mesh)
|
||||
{
|
||||
switch (mesh.normals_domain()) {
|
||||
case bke::MeshNormalDomain::Face:
|
||||
return {bke::MeshNormalDomain::Face, mesh.face_normals()};
|
||||
case bke::MeshNormalDomain::Point:
|
||||
return {bke::MeshNormalDomain::Point, mesh.vert_normals()};
|
||||
case bke::MeshNormalDomain::Corner:
|
||||
return {bke::MeshNormalDomain::Corner, mesh.corner_normals()};
|
||||
}
|
||||
BLI_assert_unreachable();
|
||||
return {};
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void gather_vert_data(const Span<int> verts,
|
||||
const bool copy_all_verts,
|
||||
const Span<T> src_data,
|
||||
MutableSpan<T> dst_data)
|
||||
{
|
||||
if (copy_all_verts) {
|
||||
array_utils::copy(src_data, dst_data);
|
||||
}
|
||||
else {
|
||||
array_utils::gather(src_data, verts, dst_data);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void gather_face_data(const Span<int> looptri_faces,
|
||||
const IndexMask &triangles,
|
||||
const Span<T> src_data,
|
||||
MutableSpan<T> dst_data)
|
||||
{
|
||||
triangles.foreach_index_optimized<int>(GrainSize(1024), [&](const int src, const int dst) {
|
||||
dst_data[dst] = src_data[looptri_faces[src]];
|
||||
});
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void gather_corner_data(const Span<MLoopTri> looptris,
|
||||
const IndexMask &triangles,
|
||||
const Span<T> src_data,
|
||||
MutableSpan<T> dst_data)
|
||||
{
|
||||
triangles.foreach_index_optimized<int>(GrainSize(1024), [&](const int src, const int dst) {
|
||||
const MLoopTri &tri = looptris[src];
|
||||
dst_data[dst * 3 + 0] = src_data[tri.tri[0]];
|
||||
dst_data[dst * 3 + 1] = src_data[tri.tri[1]];
|
||||
dst_data[dst * 3 + 2] = src_data[tri.tri[2]];
|
||||
});
|
||||
}
|
||||
|
||||
static void copy_submesh(const Mesh &mesh,
|
||||
const Span<float3> vert_positions,
|
||||
const Span<int> corner_verts,
|
||||
const Span<MLoopTri> looptris,
|
||||
const Span<int> looptri_faces,
|
||||
const std::pair<bke::MeshNormalDomain, Span<float3>> normals,
|
||||
const Span<float2> uv_map,
|
||||
const IndexMask &triangles,
|
||||
MeshData::SubMesh &sm)
|
||||
{
|
||||
resize_uninitialized(sm.face_vertex_indices, triangles.size() * 3);
|
||||
|
||||
/* If all triangles are part of this submesh and there are no loose vertices that shouldn't be
|
||||
* copied (Hydra will warn about this), vertex index compression can be completely skipped. */
|
||||
const bool copy_all_verts = triangles.size() == looptris.size() &&
|
||||
mesh.verts_no_face().count == 0;
|
||||
|
||||
int dst_verts_num;
|
||||
VectorSet<int> verts;
|
||||
if (copy_all_verts) {
|
||||
/* Copy the vertex indices from the corner indices stored in every triangle. */
|
||||
array_utils::gather(corner_verts,
|
||||
looptris.cast<int>(),
|
||||
MutableSpan(sm.face_vertex_indices.data(), sm.face_vertex_indices.size()));
|
||||
dst_verts_num = vert_positions.size();
|
||||
}
|
||||
else {
|
||||
/* Compress vertex indices to be contiguous so it's only necessary to copy values
|
||||
* for vertices actually used by the subset of triangles. */
|
||||
verts.reserve(triangles.size());
|
||||
triangles.foreach_index([&](const int src, const int dst) {
|
||||
const MLoopTri &tri = looptris[src];
|
||||
sm.face_vertex_indices[dst * 3 + 0] = verts.index_of_or_add(corner_verts[tri.tri[0]]);
|
||||
sm.face_vertex_indices[dst * 3 + 1] = verts.index_of_or_add(corner_verts[tri.tri[1]]);
|
||||
sm.face_vertex_indices[dst * 3 + 2] = verts.index_of_or_add(corner_verts[tri.tri[2]]);
|
||||
});
|
||||
dst_verts_num = verts.size();
|
||||
}
|
||||
|
||||
resize_uninitialized(sm.vertices, dst_verts_num);
|
||||
gather_vert_data(verts,
|
||||
copy_all_verts,
|
||||
vert_positions,
|
||||
MutableSpan(sm.vertices.data(), sm.vertices.size()).cast<float3>());
|
||||
|
||||
resize_uninitialized(sm.face_vertex_counts, triangles.size());
|
||||
std::fill(sm.face_vertex_counts.begin(), sm.face_vertex_counts.end(), 3);
|
||||
|
||||
const Span<float3> src_normals = normals.second;
|
||||
resize_uninitialized(sm.normals, triangles.size() * 3);
|
||||
MutableSpan dst_normals = MutableSpan(sm.normals.data(), sm.normals.size()).cast<float3>();
|
||||
switch (normals.first) {
|
||||
case bke::MeshNormalDomain::Face:
|
||||
triangles.foreach_index(GrainSize(1024), [&](const int src, const int dst) {
|
||||
std::fill_n(&dst_normals[dst * 3], 3, src_normals[looptri_faces[src]]);
|
||||
});
|
||||
break;
|
||||
case bke::MeshNormalDomain::Point:
|
||||
triangles.foreach_index(GrainSize(1024), [&](const int src, const int dst) {
|
||||
const MLoopTri &tri = looptris[src];
|
||||
dst_normals[dst * 3 + 0] = src_normals[corner_verts[tri.tri[0]]];
|
||||
dst_normals[dst * 3 + 1] = src_normals[corner_verts[tri.tri[1]]];
|
||||
dst_normals[dst * 3 + 2] = src_normals[corner_verts[tri.tri[2]]];
|
||||
});
|
||||
break;
|
||||
case bke::MeshNormalDomain::Corner:
|
||||
gather_corner_data(looptris, triangles, src_normals, dst_normals);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!uv_map.is_empty()) {
|
||||
resize_uninitialized(sm.uvs, triangles.size() * 3);
|
||||
gather_corner_data(
|
||||
looptris, triangles, uv_map, MutableSpan(sm.uvs.data(), sm.uvs.size()).cast<float2>());
|
||||
}
|
||||
}
|
||||
|
||||
void MeshData::write_submeshes(const Mesh *mesh)
|
||||
{
|
||||
const int mat_count = BKE_object_material_count_eval(reinterpret_cast<const Object *>(id));
|
||||
@@ -225,71 +369,55 @@ void MeshData::write_submeshes(const Mesh *mesh)
|
||||
submeshes_[i].mat_index = i;
|
||||
}
|
||||
|
||||
/* Fill submeshes data */
|
||||
const int *material_indices = BKE_mesh_material_indices(mesh);
|
||||
|
||||
const Span<int> looptri_faces = mesh->looptri_faces();
|
||||
const Span<float3> vert_positions = mesh->vert_positions();
|
||||
const Span<int> corner_verts = mesh->corner_verts();
|
||||
const Span<MLoopTri> looptris = mesh->looptris();
|
||||
const Span<int> looptri_faces = mesh->looptri_faces();
|
||||
|
||||
Span<float3> corner_normals;
|
||||
if (mesh->normals_domain() == blender::bke::MeshNormalDomain::Corner) {
|
||||
corner_normals = mesh->corner_normals();
|
||||
}
|
||||
const std::pair<bke::MeshNormalDomain, Span<float3>> normals = get_mesh_normals(*mesh);
|
||||
|
||||
const float2 *uv_map = static_cast<const float2 *>(
|
||||
CustomData_get_layer(&mesh->loop_data, CD_PROP_FLOAT2));
|
||||
|
||||
for (const int i : looptris.index_range()) {
|
||||
int mat_ind = material_indices ? material_indices[looptri_faces[i]] : 0;
|
||||
const MLoopTri < = looptris[i];
|
||||
SubMesh &sm = submeshes_[mat_ind];
|
||||
|
||||
sm.face_vertex_counts.push_back(3);
|
||||
sm.face_vertex_indices.push_back(corner_verts[lt.tri[0]]);
|
||||
sm.face_vertex_indices.push_back(corner_verts[lt.tri[1]]);
|
||||
sm.face_vertex_indices.push_back(corner_verts[lt.tri[2]]);
|
||||
|
||||
if (!corner_normals.is_empty()) {
|
||||
sm.normals.push_back(pxr::GfVec3f(&corner_normals[lt.tri[0]].x));
|
||||
sm.normals.push_back(pxr::GfVec3f(&corner_normals[lt.tri[1]].x));
|
||||
sm.normals.push_back(pxr::GfVec3f(&corner_normals[lt.tri[2]].x));
|
||||
}
|
||||
|
||||
if (uv_map) {
|
||||
sm.uvs.push_back(pxr::GfVec2f(&uv_map[lt.tri[0]].x));
|
||||
sm.uvs.push_back(pxr::GfVec2f(&uv_map[lt.tri[1]].x));
|
||||
sm.uvs.push_back(pxr::GfVec2f(&uv_map[lt.tri[2]].x));
|
||||
}
|
||||
}
|
||||
|
||||
/* Remove submeshes without faces */
|
||||
submeshes_.remove_if([](const SubMesh &submesh) { return submesh.face_vertex_counts.empty(); });
|
||||
if (submeshes_.is_empty()) {
|
||||
const int *material_indices = BKE_mesh_material_indices(mesh);
|
||||
if (!material_indices) {
|
||||
copy_submesh(*mesh,
|
||||
vert_positions,
|
||||
corner_verts,
|
||||
looptris,
|
||||
looptri_faces,
|
||||
normals,
|
||||
uv_map ? Span<float2>(uv_map, mesh->totloop) : Span<float2>(),
|
||||
looptris.index_range(),
|
||||
submeshes_.first());
|
||||
return;
|
||||
}
|
||||
|
||||
pxr::VtVec3fArray vertices(mesh->totvert);
|
||||
const Span<float3> positions = mesh->vert_positions();
|
||||
MutableSpan(vertices.data(), vertices.size()).copy_from(positions.cast<pxr::GfVec3f>());
|
||||
IndexMaskMemory memory;
|
||||
Array<IndexMask> triangles_by_material(submeshes_.size());
|
||||
const int max_index = std::max(mat_count - 1, 0);
|
||||
IndexMask::from_groups<int>(
|
||||
looptris.index_range(),
|
||||
memory,
|
||||
[&](const int i) { return std::min(material_indices[looptri_faces[i]], max_index); },
|
||||
triangles_by_material);
|
||||
|
||||
if (submeshes_.size() == 1) {
|
||||
submeshes_[0].vertices = std::move(vertices);
|
||||
}
|
||||
else {
|
||||
/* Optimizing submeshes: getting only used vertices, rearranged indices */
|
||||
for (SubMesh &sm : submeshes_) {
|
||||
Vector<int> index_map(vertices.size(), 0);
|
||||
for (int &face_vertex_index : sm.face_vertex_indices) {
|
||||
const int v = face_vertex_index;
|
||||
if (index_map[v] == 0) {
|
||||
sm.vertices.push_back(vertices[v]);
|
||||
index_map[v] = sm.vertices.size();
|
||||
}
|
||||
face_vertex_index = index_map[v] - 1;
|
||||
}
|
||||
threading::parallel_for(submeshes_.index_range(), 1, [&](const IndexRange range) {
|
||||
for (const int i : range) {
|
||||
copy_submesh(*mesh,
|
||||
vert_positions,
|
||||
corner_verts,
|
||||
looptris,
|
||||
looptri_faces,
|
||||
normals,
|
||||
uv_map ? Span<float2>(uv_map, mesh->totloop) : Span<float2>(),
|
||||
triangles_by_material[i],
|
||||
submeshes_[i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* Remove submeshes without faces */
|
||||
submeshes_.remove_if([](const SubMesh &submesh) { return submesh.face_vertex_counts.empty(); });
|
||||
}
|
||||
|
||||
void MeshData::update_prims()
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
namespace blender::io::hydra {
|
||||
|
||||
class MeshData : public ObjectData {
|
||||
public:
|
||||
struct SubMesh {
|
||||
pxr::VtVec3fArray vertices;
|
||||
pxr::VtIntArray face_vertex_counts;
|
||||
@@ -27,6 +28,7 @@ class MeshData : public ObjectData {
|
||||
MaterialData *mat_data = nullptr;
|
||||
};
|
||||
|
||||
private:
|
||||
Vector<SubMesh> submeshes_;
|
||||
int submeshes_count_ = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user