diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cfd4b5af..8d4576f0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ set(SOURCES src/indexgenerator.cpp src/meshletcodec.cpp src/meshletutils.cpp + src/opacitymap.cpp src/overdrawoptimizer.cpp src/partition.cpp src/quantization.cpp diff --git a/demo/tests.cpp b/demo/tests.cpp index e2989d8b1..26f1cf7c6 100644 --- a/demo/tests.cpp +++ b/demo/tests.cpp @@ -2936,6 +2936,136 @@ static void decodeMeshletTypical() assert(memcmp(rt, triangles, sizeof(triangles)) == 0); } +static void opacityMap() +{ + const size_t triangle_count = 6; + const size_t vertex_count = 8; + + const unsigned int indices[triangle_count * 3] = { + // 4 corner triangles + 0, 4, 7, + 1, 5, 4, + 2, 6, 5, + 3, 7, 6, + + // 2 center triangles (2x larger) + 4, 6, 5, // note: this triangle is flipped from its correct orientation to produce the same OMM to test compaction + 4, 6, 7, // clang-format :-/ + }; + + const float uvs[vertex_count * 2] = { + 0.f, 0.f, + 1.f, 0.f, + 1.f, 1.f, + 0.f, 1.f, + 0.5f, 0.f, + 1.f, 0.5f, + 0.5f, 1.f, + 0.f, 0.5f, // clang-format :-/ + }; + + const unsigned int texture_size = 32; + unsigned char texture[texture_size * texture_size]; + + float center = float(texture_size) * 0.5f; + float radius = 10.f; + + for (unsigned int y = 0; y < texture_size; ++y) + for (unsigned int x = 0; x < texture_size; ++x) + { + float dx = float(x) + 0.5f - center; + float dy = float(y) + 0.5f - center; + float dc = radius - sqrtf(dx * dx + dy * dy); + + texture[y * texture_size + x] = (unsigned char)meshopt_quantizeUnorm(dc + 0.5f, 8); + } + + // subdivision parameterrs + const float target_edge = 2.5f; + const int max_level = 4; + + // state histogram for testing + int histogram[2][4] = {}; + + for (int k = 0; k < 2; ++k) + { + int states = 2 << k; + + int levels[triangle_count]; + unsigned int sources[triangle_count]; + int omm_indices[triangle_count]; + + // compute level and source triangle per OMM; note that this can also deduplicate OMMs based on UVs but in our case the UVs are unique + size_t omm_count = meshopt_opacityMapMeasure(levels, sources, omm_indices, indices, triangle_count * 3, uvs, vertex_count, sizeof(float) * 2, texture_size, texture_size, max_level, target_edge); + assert(omm_count <= triangle_count); + + // validate expected levels/special indices based on underlying test data; depends on implementation specifics, might change + assert(levels[0] == 2 && levels[1] == 2 && levels[2] == 2 && levels[3] == 2); + assert(levels[4] == 3 && levels[5] == 3); + + // layout OMM data + std::vector offsets(omm_count); + size_t data_size = 0; + + for (size_t i = 0; i < omm_count; ++i) + { + offsets[i] = unsigned(data_size); + data_size += meshopt_opacityMapEntrySize(levels[i], states); + } + + std::vector data(data_size); + + for (size_t i = 0; i < omm_count; ++i) + { + unsigned int tri = sources[i]; + assert(tri < triangle_count); + + const float* uv0 = &uvs[indices[tri * 3 + 0] * 2]; + const float* uv1 = &uvs[indices[tri * 3 + 1] * 2]; + const float* uv2 = &uvs[indices[tri * 3 + 2] * 2]; + + // we can use mip 0 to rasterize for maximally conservative rasterization, or use the preferred mip for best performance + int mip = meshopt_opacityMapPreferredMip(levels[i], uv0, uv1, uv2, texture_size, texture_size); + assert(mip >= 0 && mip <= 5); + + meshopt_opacityMapRasterize(&data[offsets[i]], levels[i], states, uv0, uv1, uv2, texture, 1, texture_size, texture_size, texture_size); + + size_t micro_triangle_count = size_t(1) << (levels[i] * 2); + + for (size_t j = 0; j < micro_triangle_count; ++j) + { + unsigned char byte = data[offsets[i] + (j >> (states == 2 ? 3 : 2))]; + int state = (byte >> (states == 2 ? j & 7 : (j & 3) * 2)) & (states - 1); + histogram[k][state]++; + } + } + + // compact OMMs; note, this can be done per mesh but for optimal reuse this should be done for all meshes in model/scene at once + size_t compact_count = meshopt_opacityMapCompact(&data[0], data.size(), levels, &offsets[0], omm_count, omm_indices, triangle_count, states); + assert(compact_count <= omm_count); + data.resize(compact_count == 0 ? 0 : offsets[compact_count - 1] + meshopt_opacityMapEntrySize(levels[compact_count - 1], states)); + + // after compaction, some OMM indices may be replaced with a special index (-4..-1) + for (size_t i = 0; i < triangle_count; ++i) + assert(omm_indices[i] < 0 || size_t(omm_indices[i]) < compact_count); + + // validate expected levels/special indices based on underlying test data; depends on implementation specifics, might change + assert(levels[0] == 3 && levels[1] == 3); // note: OMM data got compacted so we only have 3-level OMMs left + assert(omm_indices[0] == -1 && omm_indices[1] == -1 && omm_indices[2] == -1 && omm_indices[3] == -1); + assert(compact_count == 1 && omm_indices[4] == 0 && omm_indices[5] == 0); // we force the two center triangles to use opposite orientations to make sure their data matches + } + + // validate expected histogram based on underlying test data; depends on rasterization specifics, might change + assert(histogram[0][0] == histogram[1][0] + histogram[1][2]); + assert(histogram[0][1] == histogram[1][1] + histogram[1][3]); + + float opaque = float(histogram[0][1]) / float(histogram[0][0] + histogram[0][1]); + float known = float(histogram[1][0] + histogram[1][1]) / float(histogram[1][0] + histogram[1][1] + histogram[1][2] + histogram[1][3]); + + assert(fabsf(opaque - 0.38f) < 1e-2f); + assert(fabsf(known - 0.66f) < 1e-2f); +} + void runTests() { decodeIndexV0(); @@ -3063,4 +3193,6 @@ void runTests() decodeMeshletSafety(); decodeMeshletBasic(); decodeMeshletTypical(); + + opacityMap(); } diff --git a/src/meshoptimizer.h b/src/meshoptimizer.h index 06e9d9e3a..8ea18827d 100644 --- a/src/meshoptimizer.h +++ b/src/meshoptimizer.h @@ -842,6 +842,15 @@ MESHOPTIMIZER_API void meshopt_spatialSortTriangles(unsigned int* destination, c */ MESHOPTIMIZER_API void meshopt_spatialClusterPoints(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t cluster_size); +/** + * Experimental: Opacity micromap generator + */ +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapMeasure(int* levels, unsigned int* sources, int* omm_indices, const unsigned int* indices, size_t index_count, const float* vertex_uvs, size_t vertex_count, size_t vertex_uvs_stride, unsigned int texture_width, unsigned int texture_height, int max_level, float target_edge); +MESHOPTIMIZER_EXPERIMENTAL void meshopt_opacityMapRasterize(unsigned char* result, int level, int states, const float* uv0, const float* uv1, const float* uv2, const unsigned char* texture_data, size_t texture_stride, size_t texture_pitch, unsigned int texture_width, unsigned int texture_height); +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, int* levels, unsigned int* offsets, size_t omm_count, int* omm_indices, size_t triangle_count, int states); +MESHOPTIMIZER_EXPERIMENTAL int meshopt_opacityMapPreferredMip(int level, const float* uv0, const float* uv1, const float* uv2, unsigned int texture_width, unsigned int texture_height); +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapEntrySize(int level, int states); + /** * Quantize a float into half-precision (as defined by IEEE-754 fp16) floating point value * Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp new file mode 100644 index 000000000..7f0baa7f5 --- /dev/null +++ b/src/opacitymap.cpp @@ -0,0 +1,468 @@ +// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details +#include "meshoptimizer.h" + +#include +#include +#include + +namespace meshopt +{ + +// opacity micromaps use a "bird" traversal order which recursively subdivides the triangles: +// https://docs.vulkan.org/spec/latest/_images/micromap-subd.svg +// note that triangles 0 and 2 have the same winding as the source triangle, however triangles 1 (flipped) +// and 3 (upright) have flipped winding; this is obvious from the level 2 subdivision in the diagram above +inline size_t getLevelSize(int level, int states) +{ + // 1-bit 2-state or 2-bit 4-state per micro triangle, rounded up to whole bytes + return ((1 << (level * 2)) * (states >> 1) + 7) >> 3; +} + +struct Texture +{ + const unsigned char* data; + size_t stride, pitch; + unsigned int width, height; +}; + +static float sampleTexture(const Texture& texture, float u, float v) +{ + // wrap texture coordinates; floor is expensive so only call it if we're outside of [0, 1] range (+eps) + u = fabsf(u - 0.5f) > 0.5f ? u - floorf(u) : u; + v = fabsf(v - 0.5f) > 0.5f ? v - floorf(v) : v; + + // convert from [0, 1] to pixel grid and shift so that texel centers are on an integer grid + u = u * float(int(texture.width)) - 0.5f; + v = v * float(int(texture.height)) - 0.5f; + + // clamp u/v along the left/top edge to avoid extrapolation since we don't interpolate across the edge + u = u < 0 ? 0.f : u; + v = v < 0 ? 0.f : v; + + // note: u/v is now in [0, size-0.5]; float->int->float is usually less expensive than floor + int x = int(u); + int y = int(v); + float rx = u - float(x); + float ry = v - float(y); + + // safeguard: this should not happen but if it ever does, ensure the accesses are inbounds + if (unsigned(x) >= texture.width || unsigned(y) >= texture.height) + return 0.f; + + // clamp the offsets instead of wrapping for simplicity and performance + size_t offset = size_t(y) * texture.pitch + x * texture.stride; + size_t offsetx = (x + 1 < int(texture.width)) ? texture.stride : 0; + size_t offsety = (y + 1 < int(texture.height)) ? texture.pitch : 0; + + unsigned char a00 = texture.data[offset]; + unsigned char a10 = texture.data[offset + offsetx]; + unsigned char a01 = texture.data[offset + offsety]; + unsigned char a11 = texture.data[offset + offsetx + offsety]; + + // bilinear interpolation; we do it partially in integer space, deferring full conversion to [0, 1] until the end + float ax0 = float(a00) + float(a10 - a00) * rx; + float ax1 = float(a01) + float(a11 - a01) * rx; + return (ax0 + (ax1 - ax0) * ry) * (1.f / 255.f); +} + +static unsigned int hashUpdate4u(unsigned int h, const unsigned char* key, size_t len) +{ + // MurmurHash2 + const unsigned int m = 0x5bd1e995; + const int r = 24; + + while (len >= 4) + { + unsigned int k; + memcpy(&k, key, sizeof(k)); + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + key += 4; + len -= 4; + } + + return h; +} + +struct TriangleOMM +{ + int uvs[6]; + int level; +}; + +struct TriangleOMMHasher +{ + const TriangleOMM* data; + + size_t hash(unsigned int index) const + { + const TriangleOMM& tri = data[index]; + + return hashUpdate4u(tri.level, reinterpret_cast(tri.uvs), sizeof(tri.uvs)); + } + + bool equal(unsigned int lhs, unsigned int rhs) const + { + const TriangleOMM& lt = data[lhs]; + const TriangleOMM& rt = data[rhs]; + + return lt.level == rt.level && memcmp(lt.uvs, rt.uvs, sizeof(lt.uvs)) == 0; + } +}; + +struct OMMHasher +{ + const unsigned char* data; + const unsigned int* offsets; + const int* levels; + int states; + + size_t hash(unsigned int index) const + { + const unsigned char* key = data + offsets[index]; + size_t size = getLevelSize(levels[index], states); + + unsigned int h = levels[index]; + + // MurmurHash2 for large keys, simple fold for small; note that size is a power of two + if (size < 4) + h ^= key[0] | (key[size - 1] << 8); + else + h = hashUpdate4u(h, key, size); + + // MurmurHash2 finalizer + h ^= h >> 13; + h *= 0x5bd1e995; + h ^= h >> 15; + return h; + } + + bool equal(unsigned int lhs, unsigned int rhs) const + { + size_t size = getLevelSize(levels[lhs], states); + + return levels[lhs] == levels[rhs] && memcmp(data + offsets[lhs], data + offsets[rhs], size) == 0; + } +}; + +static size_t hashBuckets3(size_t count) +{ + size_t buckets = 1; + while (buckets < count + count / 4) + buckets *= 2; + + return buckets; +} + +template +static T* hashLookup3(T* table, size_t buckets, const Hash& hash, const T& key, const T& empty) +{ + assert(buckets > 0); + assert((buckets & (buckets - 1)) == 0); + + size_t hashmod = buckets - 1; + size_t bucket = hash.hash(key) & hashmod; + + for (size_t probe = 0; probe <= hashmod; ++probe) + { + T& item = table[bucket]; + + if (item == empty) + return &item; + + if (hash.equal(item, key)) + return &item; + + // hash collision, quadratic probing + bucket = (bucket + probe + 1) & hashmod; + } + + assert(false && "Hash table is full"); // unreachable + return NULL; +} + +inline int quantizeSubpixel(float v, unsigned int size) +{ + return int(v * float(int(size) * 4) + (v >= 0 ? 0.5f : -0.5f)); +} + +static void rasterizeOpacity2(unsigned char* result, size_t index, float a0, float a1, float a2, float ac) +{ + // basic coverage estimator from center and corner values; trained to minimize error + float coverage = (a0 + a1 + a2) * 0.12f + ac * 0.64f; + + result[index / 8] |= (coverage >= 0.5f) << (index % 8); +} + +static void rasterizeOpacity4(unsigned char* result, size_t index, float a0, float a1, float a2, float ac) +{ + int transp = (a0 < 0.25f) & (a1 < 0.25f) & (a2 < 0.25f) & (ac < 0.25f); + int opaque = (a0 > 0.75f) & (a1 > 0.75f) & (a2 > 0.75f) & (ac > 0.75f); + float coverage = (a0 + a1 + a2) * 0.12f + ac * 0.64f; + + // treat state as known if thresholding of corners & centers against wider bounds is consistent + // for unknown states, we currently use the same formula as the 2-state opacity for better consistency with forced 2-state + int state = (transp | opaque) ? opaque : (2 + (coverage >= 0.5f)); + + result[index / 4] |= state << ((index % 4) * 2); +} + +template +static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, const float* c0, const float* c1, const float* c2, const Texture& texture) +{ + if (level == 0) + { + // compute triangle center & sample + float uc = (c0[0] + c1[0] + c2[0]) / 3; + float vc = (c0[1] + c1[1] + c2[1]) / 3; + float ac = sampleTexture(texture, uc, vc); + + // rasterize opacity state based on alpha values in corners and center + (States == 2) ? rasterizeOpacity2(result, index, c0[2], c1[2], c2[2], ac) : rasterizeOpacity4(result, index, c0[2], c1[2], c2[2], ac); + return; + } + + // compute each edge midpoint & sample + float c01[3] = {(c0[0] + c1[0]) / 2, (c0[1] + c1[1]) / 2, 0.f}; + float c12[3] = {(c1[0] + c2[0]) / 2, (c1[1] + c2[1]) / 2, 0.f}; + float c02[3] = {(c0[0] + c2[0]) / 2, (c0[1] + c2[1]) / 2, 0.f}; + + c01[2] = sampleTexture(texture, c01[0], c01[1]); + c12[2] = sampleTexture(texture, c12[0], c12[1]); + c02[2] = sampleTexture(texture, c02[0], c02[1]); + + // recursively rasterize each triangle + // note: triangles 1 and 3 have flipped winding, and 1 is flipped upside down + rasterizeOpacityRec(result, index * 4 + 0, level - 1, c0, c01, c02, texture); + rasterizeOpacityRec(result, index * 4 + 1, level - 1, c02, c12, c01, texture); + rasterizeOpacityRec(result, index * 4 + 2, level - 1, c01, c1, c12, texture); + rasterizeOpacityRec(result, index * 4 + 3, level - 1, c12, c02, c2, texture); +} + +static int getSpecialIndex(const unsigned char* data, int level, int states) +{ + int first = data[0] & (states == 2 ? 1 : 3); + int special = -(1 + first); + + // at level 0, every micromap can be converted to a special index + if (level == 0) + return special; + + // at level 1 with 2 states, the byte is partially filled so we need a separate check + if (level == 1 && states == 2) + return (data[0] & 15) == ((-first) & 15) ? special : 0; + + // otherwise we need to check that all bytes are consistent with the first value and we can do this byte-wise + int expected = first * (states == 2 ? 0xff : 0x55); + size_t size = getLevelSize(level, states); + + for (size_t i = 0; i < size; ++i) + if (data[i] != expected) + return 0; + + return special; +} + +} // namespace meshopt + +size_t meshopt_opacityMapMeasure(int* levels, unsigned int* sources, int* omm_indices, const unsigned int* indices, size_t index_count, const float* vertex_uvs, size_t vertex_count, size_t vertex_uvs_stride, unsigned int texture_width, unsigned int texture_height, int max_level, float target_edge) +{ + using namespace meshopt; + + assert(index_count % 3 == 0); + assert(vertex_uvs_stride >= 8 && vertex_uvs_stride <= 256); + assert(vertex_uvs_stride % sizeof(float) == 0); + assert(unsigned(texture_width - 1) < 16384 && unsigned(texture_height - 1) < 16384); + assert(max_level > 0 && max_level <= 12); + assert(target_edge >= 0); + + (void)vertex_count; + + meshopt_Allocator allocator; + + size_t vertex_stride_float = vertex_uvs_stride / sizeof(float); + float texture_area = float(texture_width) * float(texture_height); + + // hash map used to deduplicate triangle rasterization requests based on UV + size_t table_size = hashBuckets3(index_count / 3); + unsigned int* table = allocator.allocate(table_size); + memset(table, -1, table_size * sizeof(unsigned int)); + + TriangleOMM* triangles = allocator.allocate(index_count / 3); + TriangleOMMHasher hasher = {triangles}; + + size_t result = 0; + + for (size_t i = 0; i < index_count; i += 3) + { + unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2]; + assert(a < vertex_count && b < vertex_count && c < vertex_count); + + float u0 = vertex_uvs[a * vertex_stride_float + 0], v0 = vertex_uvs[a * vertex_stride_float + 1]; + float u1 = vertex_uvs[b * vertex_stride_float + 0], v1 = vertex_uvs[b * vertex_stride_float + 1]; + float u2 = vertex_uvs[c * vertex_stride_float + 0], v2 = vertex_uvs[c * vertex_stride_float + 1]; + + int level = max_level; + + if (target_edge > 0) + { + // compute ratio of edge length (in texels) to target and determine subdivision level + float uvarea = fabsf((u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0)) * 0.5f * texture_area; + float ratio = sqrtf(uvarea) / target_edge; + float levelf = log2f(ratio > 1 ? ratio : 1); + + // round to nearest and clamp + level = int(levelf + 0.5f); + level = level < 0 ? 0 : level; + level = level < max_level ? level : max_level; + } + + // deduplicate rasterization requests based on UV + int su0 = quantizeSubpixel(u0, texture_width), sv0 = quantizeSubpixel(v0, texture_height); + int su1 = quantizeSubpixel(u1, texture_width), sv1 = quantizeSubpixel(v1, texture_height); + int su2 = quantizeSubpixel(u2, texture_width), sv2 = quantizeSubpixel(v2, texture_height); + + TriangleOMM tri = {{su0, sv0, su1, sv1, su2, sv2}, level}; + triangles[result] = tri; // speculatively write triangle data to give hasher a way to compare it + + unsigned int* entry = hashLookup3(table, table_size, hasher, unsigned(result), ~0u); + + if (*entry == ~0u) + { + *entry = unsigned(result); + levels[result] = level; + sources[result] = unsigned(i / 3); + result++; + } + + omm_indices[i / 3] = int(*entry); + } + + return result; +} + +size_t meshopt_opacityMapEntrySize(int level, int states) +{ + assert(level >= 0 && level <= 12); + assert(states == 2 || states == 4); + + return meshopt::getLevelSize(level, states); +} + +int meshopt_opacityMapPreferredMip(int level, const float* uv0, const float* uv1, const float* uv2, unsigned int texture_width, unsigned int texture_height) +{ + assert(level >= 0 && level <= 12); + assert(unsigned(texture_width - 1) < 16384 && unsigned(texture_height - 1) < 16384); + + float texture_area = float(texture_width) * float(texture_height); + + // compute log2 of edge length (in texels) + float uvarea = fabsf((uv1[0] - uv0[0]) * (uv2[1] - uv0[1]) - (uv2[0] - uv0[0]) * (uv1[1] - uv0[1])) * 0.5f * texture_area; + float ratio = sqrtf(uvarea) / float(1 << level); + float levelf = log2f(ratio > 1 ? ratio : 1); + + // round down and clamp + int mip = int(levelf - 0.5f); + mip = mip < 0 ? 0 : mip; + mip = mip < 16 ? mip : 16; + + // ensure the selected mip is in range + while (mip > 0 && (texture_width >> mip) == 0 && (texture_height >> mip) == 0) + mip--; + + return mip; +} + +void meshopt_opacityMapRasterize(unsigned char* result, int level, int states, const float* uv0, const float* uv1, const float* uv2, const unsigned char* texture_data, size_t texture_stride, size_t texture_pitch, unsigned int texture_width, unsigned int texture_height) +{ + using namespace meshopt; + + assert(level >= 0 && level <= 12); + assert(states == 2 || states == 4); + assert(unsigned(texture_width - 1) < 16384 && unsigned(texture_height - 1) < 16384); + assert(texture_stride >= 1 && texture_stride <= 4); + assert(texture_pitch >= texture_stride * texture_width); + + memset(result, 0, getLevelSize(level, states)); + + Texture texture = {texture_data, texture_stride, texture_pitch, texture_width, texture_height}; + + // rasterize all micro triangles recursively, passing corner data down to reduce redundant sampling + float c0[3] = {uv0[0], uv0[1], sampleTexture(texture, uv0[0], uv0[1])}; + float c1[3] = {uv1[0], uv1[1], sampleTexture(texture, uv1[0], uv1[1])}; + float c2[3] = {uv2[0], uv2[1], sampleTexture(texture, uv2[0], uv2[1])}; + + (states == 2 ? rasterizeOpacityRec<2> : rasterizeOpacityRec<4>)(result, 0, level, c0, c1, c2, texture); +} + +size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, int* levels, unsigned int* offsets, size_t omm_count, int* omm_indices, size_t triangle_count, int states) +{ + using namespace meshopt; + + assert(states == 2 || states == 4); + + meshopt_Allocator allocator; + + unsigned char* data_old = allocator.allocate(data_size); + memcpy(data_old, data, data_size); + + size_t table_size = hashBuckets3(omm_count); + unsigned int* table = allocator.allocate(table_size); + memset(table, -1, table_size * sizeof(unsigned int)); + + OMMHasher hasher = {data, offsets, levels, states}; + + int* remap = allocator.allocate(omm_count); + + size_t next = 0; + size_t offset = 0; + + for (size_t i = 0; i < omm_count; ++i) + { + int level = levels[i]; + assert(level >= 0 && level <= 12); + + const unsigned char* old = data_old + offsets[i]; + size_t size = getLevelSize(level, states); + assert(offsets[i] + size <= data_size); + + // try to convert to a special index if all micro-triangle states are the same + int special = getSpecialIndex(old, level, states); + if (special < 0) + { + remap[i] = special; + continue; + } + + // speculatively write data to give hasher a way to compare it + memcpy(data + offset, old, size); + offsets[next] = unsigned(offset); + levels[next] = level; + + unsigned int* entry = hashLookup3(table, table_size, hasher, unsigned(next), ~0u); + + if (*entry == ~0u) + { + *entry = unsigned(next); + next++; + offset += size; + } + + remap[i] = int(*entry); + } + + // remap triangle indices to new indices or special indices + for (size_t i = 0; i < triangle_count; ++i) + { + assert(unsigned(omm_indices[i]) < omm_count); + omm_indices[i] = remap[omm_indices[i]]; + } + + return next; +}