Skip to content

Commit 248a476

Browse files
authored
Merge pull request #1053 from zeux/idxfilter
Implement index filtering for redundant triangles
2 parents a6ef7b6 + e6941c6 commit 248a476

5 files changed

Lines changed: 233 additions & 3 deletions

File tree

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ When optimizing a mesh, to maximize rendering efficiency you should typically fe
4242
3. (optional) Overdraw optimization
4343
4. Vertex fetch optimization
4444
5. Vertex quantization
45-
6. (optional) Shadow indexing
45+
6. Index filtering
46+
7. (optional) Shadow indexing
4647

4748
### Indexing
4849

@@ -145,6 +146,18 @@ unsigned short pz = meshopt_quantizeHalf(v.z);
145146

146147
Since quantized vertex attributes often need to remain in their compact representations for efficient transfer and storage, they are usually dequantized during vertex processing by configuring the GPU vertex input correctly to expect normalized integers or half precision floats, which often needs no or minimal changes to the shader code. When CPU dequantization is required instead, `meshopt_dequantizeHalf` can be used to convert half precision values back to single precision; for normalized integer formats, the dequantization just requires dividing by 2^N-1 for unorm and 2^(N-1)-1 for snorm variants. For example, manually reversing `meshopt_quantizeUnorm(v, 10)` can be done by dividing by 1023.
147148

149+
### Index filtering
150+
151+
Some meshes may contain triangles that are processed during rendering but do not contribute to the rendered result. If any two vertices of a triangle result in the same position after vertex shader, the triangle is degenerate and will be skipped by the rasterizer. Some triangles may also be duplicates of an earlier triangle with the same post-transform positions and winding, in which case only one of the triangles will be visible depending on depth testing settings (assuming blending is disabled). In either case, such triangles require extra processing and removing them may improve rasterization or ray tracing performance; this library provides an algorithm that removes such triangles from the index buffer:
152+
153+
```c++
154+
indices.resize(meshopt_filterIndexBuffer(&indices[0], &indices[0], indices.size(), &vertices[0].x, vertices.size(), sizeof(float) * 3, sizeof(Vertex)));
155+
```
156+
157+
Note that the example above assumes only positions are relevant for transforming the vertices, but for deformable meshes skinning data may need to be added to the vertex portion used as a key; `meshopt_filterIndexBufferMulti` can be useful for these cases if the relevant data is not contiguous.
158+
159+
Filtering after quantization is convenient because quantization may increase the number of redundant triangles if triangles had similar but not identical vertex positions before quantization. However, filtering can be done at any point in the pipeline as soon as the index buffer becomes available; you could also run vertex fetch optimization after filtering, since it will naturally filter out any vertices that may become unused after redundant triangles are eliminated, potentially saving extra memory.
160+
148161
### Shadow indexing
149162

150163
Many rendering pipelines require meshes to be rendered to depth-only targets, such as shadow maps or during a depth pre-pass, in addition to color/G-buffer targets. While using the same geometry data for both cases is possible, reducing the number of unique vertices for depth-only rendering can be beneficial, especially when the source geometry has many attribute seams due to faceted shading or lightmap texture seams.
@@ -923,6 +936,7 @@ Currently, the following APIs are experimental:
923936
- `meshopt_computePositionExponent` function
924937
- `meshopt_opacityMap*` functions (`meshopt_opacityMapMeasure`, `meshopt_opacityMapRasterize`, `meshopt_opacityMapCompact`, `meshopt_opacityMapEntrySize`)
925938
- `meshopt_generateTangents` function and `meshopt_Tangent*` flags
939+
- `meshopt_filterIndexBuffer` and `meshopt_filterIndexBufferMulti` functions
926940
927941
## License
928942

demo/main.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,21 @@ void spatialClusterPoints(const Mesh& mesh, size_t cluster_size)
14841484
(end - start) * 1000);
14851485
}
14861486

1487+
void filterTriangles(const Mesh& mesh)
1488+
{
1489+
double start = timestamp();
1490+
1491+
std::vector<unsigned int> newindices(mesh.indices.size());
1492+
newindices.resize(meshopt_filterIndexBuffer(&newindices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(float) * 3, sizeof(Vertex)));
1493+
1494+
double end = timestamp();
1495+
1496+
printf("FilterIB : %d triangles -> %d (%.2f%% redundant) in %.2f msec\n",
1497+
int(mesh.indices.size() / 3), int(newindices.size() / 3),
1498+
double((mesh.indices.size() - newindices.size()) / 3) / double(mesh.indices.size() / 3) * 100.0,
1499+
(end - start) * 1000);
1500+
}
1501+
14871502
void tessellationAdjacency(const Mesh& mesh)
14881503
{
14891504
double start = timestamp();
@@ -1814,6 +1829,7 @@ void process(const char* path)
18141829
meshlets(copy, /* scan= */ false, /* uniform= */ true, /* flex= */ false, /* spatial= */ true);
18151830

18161831
shadow(copy);
1832+
filterTriangles(copy);
18171833
tessellationAdjacency(copy);
18181834
provoking(copy);
18191835

demo/tests.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2555,6 +2555,44 @@ static void simplifyUpdateLocked(unsigned int options)
25552555
assert(vb[3][3] == 0.2f);
25562556
}
25572557

2558+
static void filterTriangles()
2559+
{
2560+
// v0/v3 match fully; v0/v4 match on prefix only
2561+
const unsigned int vb[] = {
2562+
1, 0, 10,
2563+
2, 0, 20,
2564+
3, 0, 30,
2565+
1, 1, 10,
2566+
1, 1, 99 //
2567+
};
2568+
2569+
const unsigned int ib[] = {
2570+
0, 1, 2, //
2571+
3, 1, 2, // dup of (0,1,2)
2572+
4, 1, 2, // dup prefix
2573+
2, 1, 0, // opposite winding of (0,1,2)
2574+
0, 4, 1 // degen prefix
2575+
};
2576+
2577+
// prefix only: vertex key is first 4 bytes
2578+
unsigned int sib[15];
2579+
size_t slen = meshopt_filterIndexBuffer(sib, ib, 15, vb, 5, 4, 12);
2580+
2581+
unsigned int expecteds[] = {0, 1, 2, 2, 1, 0};
2582+
assert(slen == sizeof(expecteds) / sizeof(expecteds[0]));
2583+
assert(memcmp(sib, expecteds, sizeof(expecteds)) == 0);
2584+
2585+
// prefix+suffix: vertex key is first 4 and last 4 bytes
2586+
const meshopt_Stream streams[] = {{vb + 0, 4, 12}, {vb + 2, 4, 12}};
2587+
2588+
unsigned int mib[15];
2589+
size_t mlen = meshopt_filterIndexBufferMulti(mib, ib, 15, 5, streams, 2);
2590+
unsigned int expectedm[] = {0, 1, 2, 4, 1, 2, 2, 1, 0, 0, 4, 1};
2591+
2592+
assert(mlen == sizeof(expectedm) / sizeof(expectedm[0]));
2593+
assert(memcmp(mib, expectedm, sizeof(expectedm)) == 0);
2594+
}
2595+
25582596
static void adjacency()
25592597
{
25602598
// 0 1/4
@@ -3399,6 +3437,7 @@ void runTests()
33993437
simplifyUpdateLocked(0);
34003438
simplifyUpdateLocked(meshopt_SimplifySparse);
34013439

3440+
filterTriangles();
34023441
adjacency();
34033442
tessellation();
34043443
provoking();

src/indexgenerator.cpp

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,30 @@ struct VertexCustomHasher
127127
}
128128
};
129129

130+
struct TriangleKey
131+
{
132+
unsigned int a, b, c;
133+
134+
bool operator==(const TriangleKey& other) const
135+
{
136+
return a == other.a && b == other.b && c == other.c;
137+
}
138+
};
139+
140+
struct TriangleKeyHasher
141+
{
142+
size_t hash(const TriangleKey& k) const
143+
{
144+
// Optimized Spatial Hashing for Collision Detection of Deformable Objects
145+
return (k.a * 73856093) ^ (k.b * 19349663) ^ (k.c * 83492791);
146+
}
147+
148+
bool equal(const TriangleKey& lhs, const TriangleKey& rhs) const
149+
{
150+
return lhs == rhs;
151+
}
152+
};
153+
130154
struct EdgeHasher
131155
{
132156
const unsigned int* remap;
@@ -304,6 +328,63 @@ static void generateShadowBuffer(unsigned int* destination, const unsigned int*
304328
}
305329
}
306330

331+
static size_t filterIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const unsigned int* remap, size_t vertex_count, meshopt_Allocator& allocator)
332+
{
333+
(void)vertex_count;
334+
335+
TriangleKeyHasher hasher = {};
336+
TriangleKey empty = {~0u, ~0u, ~0u};
337+
338+
size_t face_count = index_count / 3;
339+
size_t table_size = hashBuckets(face_count);
340+
TriangleKey* table = allocator.allocate<TriangleKey>(table_size);
341+
memset(table, -1, table_size * sizeof(TriangleKey));
342+
343+
size_t write = 0;
344+
345+
for (size_t i = 0; i < face_count; ++i)
346+
{
347+
unsigned int a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2];
348+
assert(a < vertex_count && b < vertex_count && c < vertex_count);
349+
350+
unsigned int ra = remap[a], rb = remap[b], rc = remap[c];
351+
352+
// degenerate triangle
353+
if (ra == rb || ra == rc || rb == rc)
354+
continue;
355+
356+
// canonicalize triangle wrt winding preserving rotation
357+
if (rb < ra && rb < rc)
358+
{
359+
// abc -> bca
360+
unsigned int t = ra;
361+
ra = rb, rb = rc, rc = t;
362+
}
363+
else if (rc < ra && rc < rb)
364+
{
365+
// abc -> cab
366+
unsigned int t = rc;
367+
rc = rb, rb = ra, ra = t;
368+
}
369+
370+
TriangleKey key = {ra, rb, rc};
371+
TriangleKey* entry = hashLookup(table, table_size, hasher, key, empty);
372+
373+
// duplicate triangle
374+
if (entry->a != ~0u)
375+
continue;
376+
377+
*entry = key;
378+
379+
destination[write + 0] = a;
380+
destination[write + 1] = b;
381+
destination[write + 2] = c;
382+
write += 3;
383+
}
384+
385+
return write;
386+
}
387+
307388
} // namespace meshopt
308389

309390
size_t meshopt_generateVertexRemap(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size)
@@ -399,11 +480,49 @@ void meshopt_remapIndexBuffer(unsigned int* destination, const unsigned int* ind
399480
}
400481
}
401482

483+
size_t meshopt_filterIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride)
484+
{
485+
using namespace meshopt;
486+
487+
assert(index_count % 3 == 0);
488+
assert(vertex_size > 0 && vertex_size <= 256);
489+
assert(vertex_size <= vertex_stride);
490+
491+
meshopt_Allocator allocator;
492+
VertexHasher hasher = {static_cast<const unsigned char*>(vertices), vertex_size, vertex_stride};
493+
494+
unsigned int* remap = allocator.allocate<unsigned int>(vertex_count);
495+
generateVertexRemap(remap, indices, index_count, vertex_count, hasher, allocator);
496+
497+
return filterIndexBuffer(destination, indices, index_count, remap, vertex_count, allocator);
498+
}
499+
500+
size_t meshopt_filterIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count)
501+
{
502+
using namespace meshopt;
503+
504+
assert(index_count % 3 == 0);
505+
assert(stream_count > 0 && stream_count <= 16);
506+
507+
for (size_t i = 0; i < stream_count; ++i)
508+
{
509+
assert(streams[i].size > 0 && streams[i].size <= 256);
510+
assert(streams[i].size <= streams[i].stride);
511+
}
512+
513+
meshopt_Allocator allocator;
514+
VertexStreamHasher hasher = {streams, stream_count};
515+
516+
unsigned int* remap = allocator.allocate<unsigned int>(vertex_count);
517+
generateVertexRemap(remap, indices, index_count, vertex_count, hasher, allocator);
518+
519+
return filterIndexBuffer(destination, indices, index_count, remap, vertex_count, allocator);
520+
}
521+
402522
void meshopt_generateShadowIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride)
403523
{
404524
using namespace meshopt;
405525

406-
assert(indices);
407526
assert(index_count % 3 == 0);
408527
assert(vertex_size > 0 && vertex_size <= 256);
409528
assert(vertex_size <= vertex_stride);
@@ -418,7 +537,6 @@ void meshopt_generateShadowIndexBufferMulti(unsigned int* destination, const uns
418537
{
419538
using namespace meshopt;
420539

421-
assert(indices);
422540
assert(index_count % 3 == 0);
423541
assert(stream_count > 0 && stream_count <= 16);
424542

src/meshoptimizer.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,27 @@ MESHOPTIMIZER_API void meshopt_remapVertexBuffer(void* destination, const void*
103103
*/
104104
MESHOPTIMIZER_API void meshopt_remapIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const unsigned int* remap);
105105

106+
/**
107+
* Experimental: Filter out redundant triangles from the index buffer and return the number of remaining indices
108+
* Triangles are considered redundant if they are degenerate (two vertices have the same vertex key) or duplicate (matching triangle was present earlier).
109+
* First vertex_size bytes of every vertex are compared for equality; typically vertex_size should be set to the size of the position attribute.
110+
* Note that duplicate triangles with opposite windings are preserved, as they may be needed for double-sided rendering.
111+
*
112+
* destination must contain enough space for the resulting index buffer (index_count elements)
113+
*/
114+
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_filterIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride);
115+
116+
/**
117+
* Experimental: Filter out redundant triangles from the index buffer and return the number of remaining indices
118+
* Triangles are considered redundant if they are degenerate (two vertices have the same vertex key) or duplicate (matching triangle was present earlier).
119+
* All bytes in specified streams are compared for equality; streams should include attributes relevant for position transform (e.g. bone influences).
120+
* Note that duplicate triangles with opposite windings are preserved, as they may be needed for double-sided rendering.
121+
*
122+
* destination must contain enough space for the resulting index buffer (index_count elements)
123+
* stream_count must be <= 16
124+
*/
125+
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_filterIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count);
126+
106127
/**
107128
* Generate index buffer that can be used for more efficient rendering when only a subset of the vertex attributes is necessary
108129
* All vertices that are binary equivalent (wrt first vertex_size bytes) map to the first vertex in the original vertex buffer.
@@ -1012,6 +1033,10 @@ inline size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const
10121033
template <typename T>
10131034
inline void meshopt_remapIndexBuffer(T* destination, const T* indices, size_t index_count, const unsigned int* remap);
10141035
template <typename T>
1036+
inline size_t meshopt_filterIndexBuffer(T* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride);
1037+
template <typename T>
1038+
inline size_t meshopt_filterIndexBufferMulti(T* destination, const T* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count);
1039+
template <typename T>
10151040
inline void meshopt_generateShadowIndexBuffer(T* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride);
10161041
template <typename T>
10171042
inline void meshopt_generateShadowIndexBufferMulti(T* destination, const T* indices, size_t index_count, size_t vertex_count, const meshopt_Stream* streams, size_t stream_count);
@@ -1266,6 +1291,24 @@ inline void meshopt_remapIndexBuffer(T* destination, const T* indices, size_t in
12661291
meshopt_remapIndexBuffer(out.data, indices ? in.data : NULL, index_count, remap);
12671292
}
12681293

1294+
template <typename T>
1295+
inline size_t meshopt_filterIndexBuffer(T* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride)
1296+
{
1297+
meshopt_IndexAdapter<T> in(NULL, indices, index_count);
1298+
meshopt_IndexAdapter<T> out(destination, NULL, index_count);
1299+
1300+
return meshopt_filterIndexBuffer(out.data, in.data, index_count, vertices, vertex_count, vertex_size, vertex_stride);
1301+
}
1302+
1303+
template <typename T>
1304+
inline size_t meshopt_filterIndexBufferMulti(T* destination, const T* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count)
1305+
{
1306+
meshopt_IndexAdapter<T> in(NULL, indices, index_count);
1307+
meshopt_IndexAdapter<T> out(destination, NULL, index_count);
1308+
1309+
return meshopt_filterIndexBufferMulti(out.data, in.data, index_count, vertex_count, streams, stream_count);
1310+
}
1311+
12691312
template <typename T>
12701313
inline void meshopt_generateShadowIndexBuffer(T* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride)
12711314
{

0 commit comments

Comments
 (0)