Skip to content

Commit c8a8302

Browse files
authored
Merge pull request #841 from zeux/partition
Experimental cluster partitioning
2 parents 568b628 + b57556d commit c8a8302

File tree

6 files changed

+557
-30
lines changed

6 files changed

+557
-30
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ set(SOURCES
2929
src/indexgenerator.cpp
3030
src/overdrawanalyzer.cpp
3131
src/overdrawoptimizer.cpp
32+
src/partition.cpp
3233
src/quantization.cpp
3334
src/simplifier.cpp
3435
src/spatialorder.cpp

demo/main.cpp

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -657,12 +657,13 @@ void simplifyComplete(const Mesh& mesh)
657657

658658
void simplifyClusters(const Mesh& mesh, float threshold = 0.2f)
659659
{
660-
// note: we use clusters that are larger than normal to give simplifier room to work; in practice you'd use cluster groups merged from smaller clusters and build a cluster DAG
661-
const size_t max_vertices = 255;
662-
const size_t max_triangles = 512;
660+
const size_t max_vertices = 64;
661+
const size_t max_triangles = 64;
662+
const size_t target_group_size = 8;
663663

664664
double start = timestamp();
665665

666+
// build clusters (meshlets) out of the mesh
666667
size_t max_meshlets = meshopt_buildMeshletsBound(mesh.indices.size(), max_vertices, max_triangles);
667668
std::vector<meshopt_Meshlet> meshlets(max_meshlets);
668669
std::vector<unsigned int> meshlet_vertices(max_meshlets * max_vertices);
@@ -672,6 +673,44 @@ void simplifyClusters(const Mesh& mesh, float threshold = 0.2f)
672673

673674
double middle = timestamp();
674675

676+
// partition clusters in groups; each group will be simplified separately and the boundaries between groups will be preserved
677+
std::vector<unsigned int> cluster_indices;
678+
cluster_indices.reserve(mesh.indices.size()); // slight underestimate, vector should realloc once
679+
std::vector<unsigned int> cluster_sizes(meshlets.size());
680+
681+
for (size_t i = 0; i < meshlets.size(); ++i)
682+
{
683+
const meshopt_Meshlet& m = meshlets[i];
684+
685+
for (size_t j = 0; j < m.triangle_count * 3; ++j)
686+
cluster_indices.push_back(meshlet_vertices[m.vertex_offset + meshlet_triangles[m.triangle_offset + j]]);
687+
688+
cluster_sizes[i] = m.triangle_count * 3;
689+
}
690+
691+
// makes sure clusters are partitioned using position-only adjacency
692+
meshopt_generateShadowIndexBuffer(&cluster_indices[0], &cluster_indices[0], cluster_indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(float) * 3, sizeof(Vertex));
693+
694+
std::vector<unsigned int> partition(meshlets.size());
695+
size_t partition_count = meshopt_partitionClusters(&partition[0], &cluster_indices[0], cluster_indices.size(), &cluster_sizes[0], cluster_sizes.size(), mesh.vertices.size(), target_group_size);
696+
697+
// convert partitions to linked lists to make it easier to iterate over (vectors of vectors would work too)
698+
std::vector<int> partnext(meshlets.size(), -1);
699+
std::vector<int> partlast(partition_count, -1);
700+
701+
for (size_t i = 0; i < meshlets.size(); ++i)
702+
{
703+
unsigned int part = partition[i];
704+
705+
if (partlast[part] >= 0)
706+
partnext[partlast[part]] = int(i);
707+
708+
partlast[part] = int(i);
709+
partnext[i] = -1;
710+
}
711+
712+
double parttime = timestamp();
713+
675714
float scale = meshopt_simplifyScale(&mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex));
676715

677716
std::vector<unsigned int> lod;
@@ -681,33 +720,47 @@ void simplifyClusters(const Mesh& mesh, float threshold = 0.2f)
681720

682721
for (size_t i = 0; i < meshlets.size(); ++i)
683722
{
684-
const meshopt_Meshlet& m = meshlets[i];
723+
if (partlast[partition[i]] < 0)
724+
continue; // part of a group that was already processed
685725

686-
size_t cluster_offset = lod.size();
726+
// mark group as processed
727+
partlast[partition[i]] = -1;
687728

688-
for (size_t j = 0; j < m.triangle_count * 3; ++j)
689-
lod.push_back(meshlet_vertices[m.vertex_offset + meshlet_triangles[m.triangle_offset + j]]);
729+
size_t group_offset = lod.size();
730+
731+
for (int j = int(i); j >= 0; j = partnext[j])
732+
{
733+
const meshopt_Meshlet& m = meshlets[j];
734+
735+
for (size_t k = 0; k < m.triangle_count * 3; ++k)
736+
lod.push_back(meshlet_vertices[m.vertex_offset + meshlet_triangles[m.triangle_offset + k]]);
737+
}
738+
739+
size_t group_triangles = (lod.size() - group_offset) / 3;
690740

741+
// simplify the group, preserving the border vertices
742+
// note: this technically also locks the exterior border; a full mesh analysis (see nanite.cpp / lockBoundary) would work better for some meshes
691743
unsigned int options = meshopt_SimplifyLockBorder | meshopt_SimplifySparse | meshopt_SimplifyErrorAbsolute;
692744

693-
float cluster_target_error = 1e-2f * scale;
694-
size_t cluster_target = size_t(float(m.triangle_count) * threshold) * 3;
695-
float cluster_error = 0.f;
696-
size_t cluster_size = meshopt_simplify(&lod[cluster_offset], &lod[cluster_offset], m.triangle_count * 3, &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), cluster_target, cluster_target_error, options, &cluster_error);
745+
float group_target_error = 1e-2f * scale;
746+
size_t group_target = size_t(float(group_triangles) * threshold) * 3;
747+
float group_error = 0.f;
748+
size_t group_size = meshopt_simplify(&lod[group_offset], &lod[group_offset], group_triangles * 3, &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), group_target, group_target_error, options, &group_error);
697749

698-
error = cluster_error > error ? cluster_error : error;
750+
error = group_error > error ? group_error : error;
699751

700-
// simplified cluster is available in lod[cluster_offset..cluster_offset + cluster_size]
701-
lod.resize(cluster_offset + cluster_size);
752+
// simplified group is available in lod[group_offset..group_offset + group_size]
753+
lod.resize(group_offset + group_size);
702754
}
703755

704756
double end = timestamp();
705757

706-
printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec, clusterized in %.2f msec\n",
758+
printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec, clusterized in %.2f msec, partitioned in %.2f msec (%d clusters in %d groups)\n",
707759
"SimplifyN", // N for Nanite
708760
int(mesh.indices.size() / 3), int(lod.size() / 3),
709761
error / scale * 100,
710-
(end - middle) * 1000, (middle - start) * 1000);
762+
(end - parttime) * 1000, (middle - start) * 1000, (parttime - middle) * 1000,
763+
int(meshlets.size()), int(partition_count));
711764
}
712765

713766
void optimize(const Mesh& mesh, const char* name, void (*optf)(Mesh& mesh))
@@ -1402,7 +1455,7 @@ void processDev(const char* path)
14021455
if (!loadMesh(mesh, path))
14031456
return;
14041457

1405-
meshlets(mesh);
1458+
simplifyClusters(mesh, 0.2f);
14061459
}
14071460

14081461
void processNanite(const char* path)

demo/nanite.cpp

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -349,26 +349,36 @@ static std::vector<std::vector<int> > partition(const std::vector<Cluster>& clus
349349
if (METIS & 1)
350350
return partitionMetis(clusters, pending, remap);
351351

352-
(void)remap;
352+
std::vector<unsigned int> cluster_indices;
353+
std::vector<unsigned int> cluster_counts(pending.size());
353354

354-
std::vector<std::vector<int> > result;
355+
size_t total_index_count = 0;
356+
for (size_t i = 0; i < pending.size(); ++i)
357+
total_index_count += clusters[pending[i]].indices.size();
355358

356-
size_t last_indices = 0;
359+
cluster_indices.reserve(total_index_count);
357360

358-
// rough merge; while clusters are approximately spatially ordered, this should use a proper partitioning algorithm
359361
for (size_t i = 0; i < pending.size(); ++i)
360362
{
361-
if (result.empty() || last_indices + clusters[pending[i]].indices.size() > kClusterSize * kGroupSize * 3)
362-
{
363-
result.push_back(std::vector<int>());
364-
last_indices = 0;
365-
}
363+
const Cluster& cluster = clusters[pending[i]];
364+
365+
cluster_counts[i] = unsigned(cluster.indices.size());
366366

367-
result.back().push_back(pending[i]);
368-
last_indices += clusters[pending[i]].indices.size();
367+
for (size_t j = 0; j < cluster.indices.size(); ++j)
368+
cluster_indices.push_back(remap[cluster.indices[j]]);
369369
}
370370

371-
return result;
371+
std::vector<unsigned int> cluster_part(pending.size());
372+
size_t partition_count = meshopt_partitionClusters(&cluster_part[0], &cluster_indices[0], cluster_indices.size(), &cluster_counts[0], cluster_counts.size(), remap.size(), kGroupSize);
373+
374+
std::vector<std::vector<int> > partitions(partition_count);
375+
for (size_t i = 0; i < partition_count; ++i)
376+
partitions[i].reserve(kGroupSize + 4);
377+
378+
for (size_t i = 0; i < pending.size(); ++i)
379+
partitions[cluster_part[i]].push_back(pending[i]);
380+
381+
return partitions;
372382
}
373383

374384
static void lockBoundary(std::vector<unsigned char>& locks, const std::vector<std::vector<int> >& groups, const std::vector<Cluster>& clusters, const std::vector<unsigned int>& remap)
@@ -481,7 +491,7 @@ static int measureUnique(std::vector<int>& used, const std::vector<unsigned int>
481491
for (size_t i = 0; i < indices.size(); ++i)
482492
{
483493
unsigned int v = indices[i];
484-
vertices += used[v] && (!locks || !(*locks)[v]);
494+
vertices += used[v] && (!locks || (*locks)[v]);
485495
used[v] = 0;
486496
}
487497

demo/tests.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,33 @@ static void meshletsSparse()
10841084
assert(memcmp(tri1, ibd + 3, 3 * sizeof(unsigned int)) == 0);
10851085
}
10861086

1087+
static void partitionBasic()
1088+
{
1089+
// 0 1 2
1090+
// 3
1091+
// 4 5 6 7 8
1092+
// 9
1093+
// 10 11 12
1094+
const unsigned int ci[] = {
1095+
0, 1, 3, 4, 5, 6, //
1096+
1, 2, 3, 6, 7, 8, //
1097+
4, 5, 6, 9, 10, 11, //
1098+
6, 7, 8, 9, 11, 12, //
1099+
};
1100+
1101+
const unsigned int cc[4] = {6, 6, 6, 6};
1102+
unsigned int part[4];
1103+
1104+
assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 4, 13, 1) == 4);
1105+
assert(part[0] == 0 && part[1] == 1 && part[2] == 2 && part[3] == 3);
1106+
1107+
assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 4, 13, 2) == 2);
1108+
assert(part[0] == 0 && part[1] == 0 && part[2] == 1 && part[3] == 1);
1109+
1110+
assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 4, 13, 4) == 1);
1111+
assert(part[0] == 0 && part[1] == 0 && part[2] == 0 && part[3] == 0);
1112+
}
1113+
10871114
static size_t allocCount;
10881115
static size_t freeCount;
10891116

@@ -2142,6 +2169,8 @@ void runTests()
21422169
meshletsDense();
21432170
meshletsSparse();
21442171

2172+
partitionBasic();
2173+
21452174
customAllocator();
21462175

21472176
emptyMesh();

src/meshoptimizer.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,18 @@ struct meshopt_Bounds
589589
MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
590590
MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeMeshletBounds(const unsigned int* meshlet_vertices, const unsigned char* meshlet_triangles, size_t triangle_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
591591

592+
/**
593+
* Experimental: Cluster partitioner
594+
* Partitions clusters into groups of similar size, prioritizing grouping clusters that share vertices.
595+
*
596+
* destination must contain enough space for the resulting partiotion data (cluster_count elements)
597+
* destination[i] will contain the partition id for cluster i, with the total number of partitions returned by the function
598+
* cluster_indices should have the vertex indices referenced by each cluster, stored sequentially
599+
* cluster_index_counts should have the number of indices in each cluster; sum of all cluster_index_counts must be equal to total_index_count
600+
* target_partition_size is a target size for each partition, in clusters; the resulting partitions may be smaller or larger
601+
*/
602+
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_partitionClusters(unsigned int* destination, const unsigned int* cluster_indices, size_t total_index_count, const unsigned int* cluster_index_counts, size_t cluster_count, size_t vertex_count, size_t target_partition_size);
603+
592604
/**
593605
* Spatial sorter
594606
* Generates a remap table that can be used to reorder points for spatial locality.

0 commit comments

Comments
 (0)