From 9f5f9cf51b63fd74ca1c2cc6eb8e113d866b25a0 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 25 Feb 2026 12:50:34 -0800 Subject: [PATCH 01/21] opacitymap: Initial implementation of 2-state OMM rasterizer This file will host the series of functions that implement OMM rasterization, given alpha texture data as the source. For now the functions are not exposed in meshoptimizer.h as the interface will be highly unstable. In this change, a simple recursive subdivision is used to generate 2-state OMM data. In the recursive invocations, we sample alpha data for the corners of micro-triangles to pass them to the following level; this reuses sampled positions to some extent, although the total number of samples is still a little more than optimal. For now, the resulting alpha samples are simply averaged together with the center sample and thresholded against 0.5 to determine the triangle state. This can be improved in the future. --- CMakeLists.txt | 1 + src/opacitymap.cpp | 91 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/opacitymap.cpp 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/src/opacitymap.cpp b/src/opacitymap.cpp new file mode 100644 index 000000000..10f57b605 --- /dev/null +++ b/src/opacitymap.cpp @@ -0,0 +1,91 @@ +// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details +#include "meshoptimizer.h" + +#include +#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 + +struct Texture +{ + const unsigned char* data; + size_t stride; + size_t pitch; + unsigned int width, height; +}; + +static float sampleTexture(const Texture& texture, float u, float v) +{ + int x = int(u * float(int(texture.width))); + int y = int(v * float(int(texture.height))); + + // TODO: clamp/wrap + if (unsigned(x) >= texture.width || unsigned(y) >= texture.height) + return 0.f; + + // TODO: bilinear + return texture.data[y * texture.pitch + x * texture.stride] * (1.f / 255.f); +} + +template +static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, float u0, float v0, float a0, float u1, float v1, float a1, float u2, float v2, float a2, const Texture& texture) +{ + if (level == 0) + { + // compute triangle center & sample + float uc = (u0 + u1 + u2) / 3; + float vc = (v0 + v1 + v2) / 3; + float ac = sampleTexture(texture, uc, vc); + + // we now have three corners and center alpha; for now simply average and threshold them + float avg = (a0 + a1 + a2 + ac) * 0.25f; + bool opaque = avg >= 0.5f; + + assert(States == 2); + result[index / 8] |= unsigned(opaque) << (index % 8); + return; + } + + // compute each edge midpoint & sample + float u01 = (u0 + u1) / 2, v01 = (v0 + v1) / 2; + float u02 = (u0 + u2) / 2, v02 = (v0 + v2) / 2; + float u12 = (u1 + u2) / 2, v12 = (v1 + v2) / 2; + + float a01 = sampleTexture(texture, u01, v01); + float a02 = sampleTexture(texture, u02, v02); + float a12 = sampleTexture(texture, u12, v12); + + // recursively rasterize each triangle + // note: triangles 1 and 3 have flipped winding, and 1 is additionally flipped + rasterizeOpacityRec(result, index * 4 + 0, level - 1, u0, v0, a0, u01, v01, a01, u02, v02, a02, texture); + rasterizeOpacityRec(result, index * 4 + 1, level - 1, u02, v02, a02, u12, v12, a12, u01, v01, a01, texture); + rasterizeOpacityRec(result, index * 4 + 2, level - 1, u01, v01, a01, u1, v1, a1, u12, v12, a12, texture); + rasterizeOpacityRec(result, index * 4 + 3, level - 1, u12, v12, a12, u02, v02, a02, u2, v2, a2, texture); +} + +} // namespace meshopt + +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); + + Texture texture = {texture_data, texture_stride, texture_pitch, texture_width, texture_height}; + + float alpha0 = sampleTexture(texture, uv0[0], uv0[1]); + float alpha1 = sampleTexture(texture, uv1[0], uv1[1]); + float alpha2 = sampleTexture(texture, uv2[0], uv2[1]); + + memset(result, 0, ((1 << (level * 2)) * (states / 2) + 7) / 8); + (states == 2 ? rasterizeOpacityRec<2> : rasterizeOpacityRec<4>)(result, 0, level, uv0[0], uv0[1], alpha0, uv1[0], uv1[1], alpha1, uv2[0], uv2[1], alpha2, texture); +} From 78840bb9eb2d0a6fae1005a806671d25c7d7337f Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 25 Feb 2026 13:14:35 -0800 Subject: [PATCH 02/21] opacitymap: Implement support for 4-state OMM The rasterization for 4-state OMM can encode the same opaque/transparent data with an extra "unknown" bit. In the shader, we can then either use the 4-state data as is (getting anyhit invocations for unknown state), or force it to be 2-state, in which case the two unknown states are converted to a known opaque or transparent. For determining known state, for now we use thresholds closer to the 0/1 cutoff to get a more conservative estimate. It's still the case that the triangle corners and centers could miss fine texture detail and the result will not be conservative. For unknown state, we use the same formula as the 2-state raster for improved consistency. --- src/opacitymap.cpp | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 10f57b605..1a351361f 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -17,8 +17,7 @@ namespace meshopt struct Texture { const unsigned char* data; - size_t stride; - size_t pitch; + size_t stride, pitch; unsigned int width, height; }; @@ -35,6 +34,28 @@ static float sampleTexture(const Texture& texture, float u, float v) return texture.data[y * texture.pitch + x * texture.stride] * (1.f / 255.f); } +static void rasterizeOpacity2(unsigned char* result, size_t index, float a0, float a1, float a2, float ac) +{ + // for now threshold average value; this could use a more sophisticated heuristic in the future + float average = (a0 + a1 + a2 + ac) * 0.25f; + int state = average >= 0.5f; + + result[index / 8] |= state << (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 average = (a0 + a1 + a2 + ac) * 0.25f; + + // 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 + (average >= 0.5f)); + + result[index / 4] |= state << ((index % 4) * 2); +} + template static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, float u0, float v0, float a0, float u1, float v1, float a1, float u2, float v2, float a2, const Texture& texture) { @@ -45,12 +66,8 @@ static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, float vc = (v0 + v1 + v2) / 3; float ac = sampleTexture(texture, uc, vc); - // we now have three corners and center alpha; for now simply average and threshold them - float avg = (a0 + a1 + a2 + ac) * 0.25f; - bool opaque = avg >= 0.5f; - - assert(States == 2); - result[index / 8] |= unsigned(opaque) << (index % 8); + // rasterize opacity state based on alpha values in corners and center + (States == 2) ? rasterizeOpacity2(result, index, a0, a1, a2, ac) : rasterizeOpacity4(result, index, a0, a1, a2, ac); return; } From 2b7a275bc23d28758118b2d6206fdc81cb5b4688 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 25 Feb 2026 14:07:08 -0800 Subject: [PATCH 03/21] opacitymap: Add assertins for input texture parameters For now we assert on D3D11-like texture limits and reasonable limits for stride/pitch to prevent accidental mistakes. --- src/opacitymap.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 1a351361f..e48457722 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -81,7 +81,7 @@ static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, float a12 = sampleTexture(texture, u12, v12); // recursively rasterize each triangle - // note: triangles 1 and 3 have flipped winding, and 1 is additionally flipped + // note: triangles 1 and 3 have flipped winding, and 1 is flipped upside down rasterizeOpacityRec(result, index * 4 + 0, level - 1, u0, v0, a0, u01, v01, a01, u02, v02, a02, texture); rasterizeOpacityRec(result, index * 4 + 1, level - 1, u02, v02, a02, u12, v12, a12, u01, v01, a01, texture); rasterizeOpacityRec(result, index * 4 + 2, level - 1, u01, v01, a01, u1, v1, a1, u12, v12, a12, texture); @@ -96,13 +96,19 @@ void meshopt_opacityMapRasterize(unsigned char* result, int level, int states, c 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); Texture texture = {texture_data, texture_stride, texture_pitch, texture_width, texture_height}; + // 1-bit 2-state or 2-bit 4-state per micro triangle, rounded up to whole bytes + memset(result, 0, ((1 << (level * 2)) * (states / 2) + 7) / 8); + + // rasterize all micro triangles recursively, passing corner data down to reduce redundant sampling float alpha0 = sampleTexture(texture, uv0[0], uv0[1]); float alpha1 = sampleTexture(texture, uv1[0], uv1[1]); float alpha2 = sampleTexture(texture, uv2[0], uv2[1]); - memset(result, 0, ((1 << (level * 2)) * (states / 2) + 7) / 8); (states == 2 ? rasterizeOpacityRec<2> : rasterizeOpacityRec<4>)(result, 0, level, uv0[0], uv0[1], alpha0, uv1[0], uv1[1], alpha1, uv2[0], uv2[1], alpha2, texture); } From 63a2ee069bf59c495d3a672f0b084ed9a2a65065 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 25 Feb 2026 14:36:04 -0800 Subject: [PATCH 04/21] opacitymap: Pass corner data in recursive calls by pointer Since the parameters in recursive calls are mostly shared between successive calls and there are too many of them to fit into registers, it's more efficient to pass an array of 3 floats (U/V/alpha) for each corner. This also makes it somewhat easier to follow the logic in the function, although in some places having just one index vs cI[J] was cleaner. --- src/opacitymap.cpp | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index e48457722..51888a2b5 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -57,35 +57,35 @@ static void rasterizeOpacity4(unsigned char* result, size_t index, float a0, flo } template -static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, float u0, float v0, float a0, float u1, float v1, float a1, float u2, float v2, float a2, const Texture& texture) +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 = (u0 + u1 + u2) / 3; - float vc = (v0 + v1 + v2) / 3; + 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, a0, a1, a2, ac) : rasterizeOpacity4(result, index, a0, a1, a2, ac); + (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 u01 = (u0 + u1) / 2, v01 = (v0 + v1) / 2; - float u02 = (u0 + u2) / 2, v02 = (v0 + v2) / 2; - float u12 = (u1 + u2) / 2, v12 = (v1 + v2) / 2; + 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}; - float a01 = sampleTexture(texture, u01, v01); - float a02 = sampleTexture(texture, u02, v02); - float a12 = sampleTexture(texture, u12, v12); + c01[2] = sampleTexture(texture, c01[0], c01[1]); + c02[2] = sampleTexture(texture, c02[0], c02[1]); + c12[2] = sampleTexture(texture, c12[0], c12[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, u0, v0, a0, u01, v01, a01, u02, v02, a02, texture); - rasterizeOpacityRec(result, index * 4 + 1, level - 1, u02, v02, a02, u12, v12, a12, u01, v01, a01, texture); - rasterizeOpacityRec(result, index * 4 + 2, level - 1, u01, v01, a01, u1, v1, a1, u12, v12, a12, texture); - rasterizeOpacityRec(result, index * 4 + 3, level - 1, u12, v12, a12, u02, v02, a02, u2, v2, a2, texture); + 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); } } // namespace meshopt @@ -106,9 +106,9 @@ void meshopt_opacityMapRasterize(unsigned char* result, int level, int states, c memset(result, 0, ((1 << (level * 2)) * (states / 2) + 7) / 8); // rasterize all micro triangles recursively, passing corner data down to reduce redundant sampling - float alpha0 = sampleTexture(texture, uv0[0], uv0[1]); - float alpha1 = sampleTexture(texture, uv1[0], uv1[1]); - float alpha2 = sampleTexture(texture, uv2[0], uv2[1]); + 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, uv0[0], uv0[1], alpha0, uv1[0], uv1[1], alpha1, uv2[0], uv2[1], alpha2, texture); + (states == 2 ? rasterizeOpacityRec<2> : rasterizeOpacityRec<4>)(result, 0, level, c0, c1, c2, texture); } From 8149658a413533efc5602d944335ab0b296e81df Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 26 Feb 2026 08:03:25 -0800 Subject: [PATCH 05/21] opacitymap: Add opacityMapMeasure to determine subdiv levels Given the input UV coordinates, we can compute the optimal subdivision level for each triangle based on the target size of each triangle in texels. For now we round the resulting log2 ratio to nearest; this tends to keep the average size a little closer to target compared to using floor/ceil. The subdiv level is additionally constrained to a given maximum; when target_edge is 0, all triangles are subdivided uniformly. --- src/opacitymap.cpp | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 51888a2b5..99793b8a5 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -90,6 +90,55 @@ static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, } // namespace meshopt +size_t meshopt_opacityMapMeasure(int* omm_levels, 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; + + size_t vertex_stride_float = vertex_uvs_stride / sizeof(float); + float texture_area = float(texture_width) * float(texture_height); + + 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 = unsigned(level) < unsigned(max_level) ? level : max_level; + } + + omm_indices[i / 3] = int(result); + omm_levels[result] = level; + result++; + } + + return result; +} + 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; From 5960326a2a62e7a3305805302e8a903531136685 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 26 Feb 2026 08:05:34 -0800 Subject: [PATCH 06/21] opacitymap: Add a function to determine optimal raster mip When rasterizing micro-triangles, ideally we'd like each microtriangle to be 2x2-3x3 texels. Because rasterization doesn't examine the entire contents of the microtriangle, using mips that are too detailed could lead to missing features in the opacity map. This function can be used before calling opacityMapRasterize to pass mip level data and width/height instead of the original texture for best results. --- src/opacitymap.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 99793b8a5..ebee1c3d0 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -139,6 +139,29 @@ size_t meshopt_opacityMapMeasure(int* omm_levels, int* omm_indices, const unsign return result; } +int meshopt_opacityMapRasterizeMip(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); + 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; From edd4d552dfc2ec178281fc7a51970e0245d7f06b Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 26 Feb 2026 08:30:41 -0800 Subject: [PATCH 07/21] opacitymap: Deduplicate triangle data based on UVs Instead of a 1-1 mapping, opacityMapMeasure now generates a unique index map based on UV equality. This significantly reduces the number of rasterization requests on real-world meshes; while post-rasterization deduplication is still important and will be added later, we need both to be able to reach reasonable rasterization times. For now we use the same hash scaffolding as some other files in the library do; this might need to be tweaked a little bit in the future to either change the table to store indices, or to simplify the operator== interaction. --- src/opacitymap.cpp | 114 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index ebee1c3d0..b47e04879 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -14,6 +14,18 @@ namespace meshopt // 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 +struct TriangleOMM +{ + float uvs[6]; + int level; + int index; + + bool operator==(const TriangleOMM& other) const + { + return level == other.level; // TODO: this can't compare anything else because it's used in hashLookup to check for empty item :( + } +}; + struct Texture { const unsigned char* data; @@ -34,6 +46,79 @@ static float sampleTexture(const Texture& texture, float u, float v) return texture.data[y * texture.pitch + x * texture.stride] * (1.f / 255.f); } +struct TriangleOMMHasher +{ + size_t hash(const TriangleOMM& tri) const + { + unsigned int h = tri.level; + + const unsigned int m = 0x5bd1e995; + const int r = 24; + + // MurmurHash2 + const unsigned int* uvs = reinterpret_cast(tri.uvs); + + for (int i = 0; i < 6; ++i) + { + unsigned int k = uvs[i]; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + } + + // MurmurHash2 finalizer + h ^= h >> 13; + h *= 0x5bd1e995; + h ^= h >> 15; + return h; + } + + bool equal(const TriangleOMM& lhs, const TriangleOMM& rhs) const + { + return lhs.level == rhs.level && memcmp(lhs.uvs, rhs.uvs, sizeof(lhs.uvs)) == 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; +} + static void rasterizeOpacity2(unsigned char* result, size_t index, float a0, float a1, float a2, float ac) { // for now threshold average value; this could use a more sophisticated heuristic in the future @@ -103,9 +188,21 @@ size_t meshopt_opacityMapMeasure(int* omm_levels, int* omm_indices, const unsign (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); + TriangleOMM* table = allocator.allocate(table_size); + memset(table, -1, table_size * sizeof(TriangleOMM)); // level=-1 is invalid + + TriangleOMM dummy = {}; + dummy.level = -1; + + TriangleOMMHasher hasher; + size_t result = 0; for (size_t i = 0; i < index_count; i += 3) @@ -131,9 +228,20 @@ size_t meshopt_opacityMapMeasure(int* omm_levels, int* omm_indices, const unsign level = unsigned(level) < unsigned(max_level) ? level : max_level; } - omm_indices[i / 3] = int(result); - omm_levels[result] = level; - result++; + // deduplicate rasterization requests based on UV + TriangleOMM tri = {{u0, v0, u1, v1, u2, v2}, level, 0}; + TriangleOMM* entry = hashLookup3(table, table_size, hasher, tri, dummy); + + if (entry->level < 0) + { + tri.index = int(result); + omm_levels[result] = level; + result++; + + *entry = tri; + } + + omm_indices[i / 3] = entry->index; } return result; From 2aa5a0bde67809546eb6439a914278dc5a533ab6 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 26 Feb 2026 09:33:03 -0800 Subject: [PATCH 08/21] opacitymap: Quantize UVs to a subpixel grid for deduplication In case the input UVs have small jitter, we quantize them to a subpixel grid before hashing. While this runs a small risk of collapsing different triangles together, in practice our rasterization algorithm is not sensitive enough to pick up the differences and these collisions are not common. We currently use a 4x4 subpixel grid (loosely equivalent to 16x MSAA). --- src/opacitymap.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index b47e04879..bc1342f92 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -16,7 +16,7 @@ namespace meshopt struct TriangleOMM { - float uvs[6]; + int uvs[6]; int level; int index; @@ -119,6 +119,11 @@ static T* hashLookup3(T* table, size_t buckets, const Hash& hash, const T& key, 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) { // for now threshold average value; this could use a more sophisticated heuristic in the future @@ -229,7 +234,10 @@ size_t meshopt_opacityMapMeasure(int* omm_levels, int* omm_indices, const unsign } // deduplicate rasterization requests based on UV - TriangleOMM tri = {{u0, v0, u1, v1, u2, v2}, level, 0}; + 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, 0}; TriangleOMM* entry = hashLookup3(table, table_size, hasher, tri, dummy); if (entry->level < 0) From 8a5d614c4a1015f5a85f90075adcc4d957c0e0d7 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 26 Feb 2026 09:39:15 -0800 Subject: [PATCH 09/21] opacitymap: Store triangle indices in the hash table Instead of using the hash table to store triangle data directly, we store it in an array on the side and just store indices in the hash table. This results in a reduced memory consumption (as we only need to round the index store up to a power of two), much better cache locality when the triangle reduction rate is significant, and works cleanly with our canonical hashLookup templated interface without operator== hacks. --- src/opacitymap.cpp | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index bc1342f92..1baa3d0b6 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -18,12 +18,6 @@ struct TriangleOMM { int uvs[6]; int level; - int index; - - bool operator==(const TriangleOMM& other) const - { - return level == other.level; // TODO: this can't compare anything else because it's used in hashLookup to check for empty item :( - } }; struct Texture @@ -48,8 +42,11 @@ static float sampleTexture(const Texture& texture, float u, float v) struct TriangleOMMHasher { - size_t hash(const TriangleOMM& tri) const + const TriangleOMM* data; + + size_t hash(unsigned int index) const { + const TriangleOMM& tri = data[index]; unsigned int h = tri.level; const unsigned int m = 0x5bd1e995; @@ -77,9 +74,12 @@ struct TriangleOMMHasher return h; } - bool equal(const TriangleOMM& lhs, const TriangleOMM& rhs) const + bool equal(unsigned int lhs, unsigned int rhs) const { - return lhs.level == rhs.level && memcmp(lhs.uvs, rhs.uvs, sizeof(lhs.uvs)) == 0; + const TriangleOMM& lt = data[lhs]; + const TriangleOMM& rt = data[rhs]; + + return lt.level == rt.level && memcmp(lt.uvs, rt.uvs, sizeof(lt.uvs)) == 0; } }; @@ -200,13 +200,11 @@ size_t meshopt_opacityMapMeasure(int* omm_levels, int* omm_indices, const unsign // hash map used to deduplicate triangle rasterization requests based on UV size_t table_size = hashBuckets3(index_count / 3); - TriangleOMM* table = allocator.allocate(table_size); - memset(table, -1, table_size * sizeof(TriangleOMM)); // level=-1 is invalid + unsigned int* table = allocator.allocate(table_size); + memset(table, -1, table_size * sizeof(unsigned int)); - TriangleOMM dummy = {}; - dummy.level = -1; - - TriangleOMMHasher hasher; + TriangleOMM* triangles = allocator.allocate(index_count / 3); + TriangleOMMHasher hasher = {triangles}; size_t result = 0; @@ -237,19 +235,20 @@ size_t meshopt_opacityMapMeasure(int* omm_levels, int* omm_indices, const unsign 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, 0}; - TriangleOMM* entry = hashLookup3(table, table_size, hasher, tri, dummy); - if (entry->level < 0) + 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) { - tri.index = int(result); + *entry = unsigned(result); omm_levels[result] = level; result++; - - *entry = tri; } - omm_indices[i / 3] = entry->index; + omm_indices[i / 3] = int(*entry); } return result; From c14c6265c7cf8a5e027b5f5f35f06905557454db Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 26 Feb 2026 09:55:06 -0800 Subject: [PATCH 10/21] opacitymap: Clamp invalid subdivision levels to 0 If input UVs have inf/nans then depending on fp modes we might end up with a negative level; previously we were clamping that to max_level but on further reflection it seems better to explicitly clamp to 0. --- src/opacitymap.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 1baa3d0b6..f1d1c4ec1 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -228,7 +228,8 @@ size_t meshopt_opacityMapMeasure(int* omm_levels, int* omm_indices, const unsign // round to nearest and clamp level = int(levelf + 0.5f); - level = unsigned(level) < unsigned(max_level) ? level : max_level; + level = level < 0 ? 0 : level; + level = level > max_level ? max_level : level; } // deduplicate rasterization requests based on UV From cda771c815c9076040c5e16d98b84287face0cb9 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 26 Feb 2026 10:58:24 -0800 Subject: [PATCH 11/21] opacitymap: Implement hash-based OMM compaction After rasterization, it's not uncommon to see identical rasterized results for triangles that were distinct in source data. This is redundant and can be fixed by deduplicating the bits and adjusting OMM indices to point to the new data. The new function, meshopt_opacityMapCompact, does just that. It expects a compactly stored source data and re-compacts it, adjusting offsets, levels and external indices to match. The result is the number of resulting OMM entries, which is a little awkward because the resulting size must be computed from the last offset (similarly to buildMeshlets) manually. --- src/opacitymap.cpp | 120 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index f1d1c4ec1..17b568d1b 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -83,6 +83,65 @@ struct TriangleOMMHasher } }; +struct OMMHasher +{ + const unsigned char* data; + const unsigned int* offsets; + const int* levels; + int states; + + size_t hash(unsigned int index) const + { + size_t size = ((1 << (levels[index] * 2)) * (states / 2) + 7) / 8; + const unsigned char* key = data + offsets[index]; + + unsigned int h = levels[index]; + + const unsigned int m = 0x5bd1e995; + const int r = 24; + + // MurmurHash2 + for (size_t i = 0; i < size / 4; ++i) + { + unsigned int k = *reinterpret_cast(key + i * 4); + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + } + + // MurmurHash2 tail + switch (size & 3) + { + case 3: + h ^= key[size - 3] << 16; + // fallthrough + case 2: + h ^= key[size - 2] << 8; + // fallthrough + case 1: + h ^= key[size - 1]; + h *= m; + } + + // MurmurHash2 finalizer + h ^= h >> 13; + h *= 0x5bd1e995; + h ^= h >> 15; + return h; + } + + bool equal(unsigned int lhs, unsigned int rhs) const + { + size_t size = ((1 << (levels[lhs] * 2)) * (states / 2) + 7) / 8; + + 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; @@ -180,7 +239,7 @@ static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, } // namespace meshopt -size_t meshopt_opacityMapMeasure(int* omm_levels, 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) +size_t meshopt_opacityMapMeasure(int* levels, 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; @@ -245,7 +304,7 @@ size_t meshopt_opacityMapMeasure(int* omm_levels, int* omm_indices, const unsign if (*entry == ~0u) { *entry = unsigned(result); - omm_levels[result] = level; + levels[result] = level; result++; } @@ -300,3 +359,60 @@ void meshopt_opacityMapRasterize(unsigned char* result, int level, int states, c (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 = ((1 << (level * 2)) * (states / 2) + 7) / 8; + + // 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; +} From 6b4967f0e958fafbcd0c0aff57dc4478963ae65f Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 26 Feb 2026 12:14:11 -0800 Subject: [PATCH 12/21] opacitymap: Compact triangle data to special indices if possible If input triangles have the same state for each microtriangle, they can use a special index that takes no bytes of actual data storage. We now implement this as part of compaction. All level 0 triangles can be represented with a special index; level 1 and above need to be checked. We can mostly do this checking byte-wise, except state=2 level=1 triangles that only use 4 bits out of a zero byte and require special treatment. Also clamp mips returned from RasterizeMip to 0. --- src/opacitymap.cpp | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 17b568d1b..366e1153f 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -237,6 +237,30 @@ static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, 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 = (1 << (level * 2)) * (states / 2) / 8; + + for (size_t i = 0; i < size; ++i) + if (data[i] != expected) + return 0; + + return special; +} + } // namespace meshopt size_t meshopt_opacityMapMeasure(int* levels, 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) @@ -288,7 +312,7 @@ size_t meshopt_opacityMapMeasure(int* levels, int* omm_indices, const unsigned i // round to nearest and clamp level = int(levelf + 0.5f); level = level < 0 ? 0 : level; - level = level > max_level ? max_level : level; + level = level < max_level ? level : max_level; } // deduplicate rasterization requests based on UV @@ -328,6 +352,7 @@ int meshopt_opacityMapRasterizeMip(int level, const float* uv0, const float* uv1 // round down and clamp int mip = int(levelf); + mip = mip < 0 ? 0 : mip; mip = mip < 16 ? mip : 16; // ensure the selected mip is in range @@ -390,6 +415,14 @@ size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, int* lev const unsigned char* old = data_old + offsets[i]; size_t size = ((1 << (level * 2)) * (states / 2) + 7) / 8; + // 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); From aad02d1f5079638cef174f06024fb17518e859dd Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 27 Feb 2026 07:36:33 -0800 Subject: [PATCH 13/21] opacitymap: Switch to bilinear filtering with wrap/clamp addressing We need bilinear filtering to be able to more accurately classify triangle data. In addition, this change switches from border addressing to a mixed wrap/clamp addressing: the source texture coordinate is wrapped, but we never filter across the edge so all 4 samples are local. This is helpful for performance and also means the code works better for use cases where the source texture is *not* wrapped in real rendering: as long as source coordinates are in 0..1 range, the samples will work as if addressing was clamp-to-edge. The implementation is careful to avoid using floor (which is a function call depending on compiler flags and target instruction set). --- src/opacitymap.cpp | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 366e1153f..3c2c57931 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -29,15 +29,42 @@ struct Texture static float sampleTexture(const Texture& texture, float u, float v) { - int x = int(u * float(int(texture.width))); - int y = int(v * float(int(texture.height))); + // 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; - // TODO: clamp/wrap + // 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; - // TODO: bilinear - return texture.data[y * texture.pitch + x * texture.stride] * (1.f / 255.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]; + + // blinear 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); } struct TriangleOMMHasher From 017e5a4fa6fea9fc92c9cabe8d91026eea5fc01c Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 27 Feb 2026 07:40:44 -0800 Subject: [PATCH 14/21] opacitymap: Improve coverage estimation with a better heuristic Instead of a straight average, we weigh the center sample more strongly to improve the estimation. These weights were derived by training a degenerate "network" (4=>1 reduction) to estimate coverage from a0/a1/a2/ac, and truncating the weights to two decimal digits. In the future it might make sense to switch to a slightly more complex estimator; e.g. 4=>3=>1 with intermediate ReLU provides a little higher quality results; however, for now the extra gains aren't obviously worth the extra evaluation cost, and we might need more input samples to achieve significantly better results. Also adjust the mip selection to round level further downwards; with bilinear filtering, we would actually prefer something like 3x3 footprint for microtriangles as 4 samples cover that pretty well. --- src/opacitymap.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 3c2c57931..cd1074d1a 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -212,22 +212,21 @@ inline int quantizeSubpixel(float v, unsigned int size) static void rasterizeOpacity2(unsigned char* result, size_t index, float a0, float a1, float a2, float ac) { - // for now threshold average value; this could use a more sophisticated heuristic in the future - float average = (a0 + a1 + a2 + ac) * 0.25f; - int state = average >= 0.5f; + // 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] |= state << (index % 8); + 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 average = (a0 + a1 + a2 + ac) * 0.25f; + 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 + (average >= 0.5f)); + int state = (transp | opaque) ? opaque : (2 + (coverage >= 0.5f)); result[index / 4] |= state << ((index % 4) * 2); } @@ -253,8 +252,8 @@ static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, float c02[3] = {(c0[0] + c2[0]) / 2, (c0[1] + c2[1]) / 2, 0.f}; c01[2] = sampleTexture(texture, c01[0], c01[1]); - c02[2] = sampleTexture(texture, c02[0], c02[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 @@ -378,7 +377,7 @@ int meshopt_opacityMapRasterizeMip(int level, const float* uv0, const float* uv1 float levelf = log2f(ratio > 1 ? ratio : 1); // round down and clamp - int mip = int(levelf); + int mip = int(levelf - 0.5f); mip = mip < 0 ? 0 : mip; mip = mip < 16 ? mip : 16; From 9b8cc9e9043031fcb6840212cc10ccbfee576ef7 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 27 Feb 2026 07:44:14 -0800 Subject: [PATCH 15/21] opacitymap: Refine function interfaces further This change adds "sources" to meshopt_opacityMapMeasure, which indicates the source triangle for each OMM. Without it, you'd need to scan omm_indices and figure out which first triangle maps to the OMM which is a little cumbersome. Also rename meshopt_opacityRasterizeMip to meshopt_opacityMapPreferredMip as the name signals intent better, and add meshopt_opacityMapTriangleSize which had to be implemented by the caller previously to actually do the memory layout. This API surface is now complete enough that it can be added to the header; however, it doesn't have an ideal shape so it's likely that it will change again. --- src/meshoptimizer.h | 9 +++++++++ src/opacitymap.cpp | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/meshoptimizer.h b/src/meshoptimizer.h index 06e9d9e3a..efa974cab 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_opacityMapTriangleSize(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 index cd1074d1a..97b4a540d 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -289,7 +289,7 @@ static int getSpecialIndex(const unsigned char* data, int level, int states) } // namespace meshopt -size_t meshopt_opacityMapMeasure(int* levels, 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) +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; @@ -355,6 +355,7 @@ size_t meshopt_opacityMapMeasure(int* levels, int* omm_indices, const unsigned i { *entry = unsigned(result); levels[result] = level; + sources[result] = int(i / 3); result++; } @@ -364,7 +365,15 @@ size_t meshopt_opacityMapMeasure(int* levels, int* omm_indices, const unsigned i return result; } -int meshopt_opacityMapRasterizeMip(int level, const float* uv0, const float* uv1, const float* uv2, unsigned int texture_width, unsigned int texture_height) +size_t meshopt_opacityMapTriangleSize(int level, int states) +{ + assert(level >= 0 && level <= 12); + assert(states == 2 || states == 4); + + return ((1 << (level * 2)) * (states / 2) + 7) / 8; +} + +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); @@ -440,6 +449,7 @@ size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, int* lev const unsigned char* old = data_old + offsets[i]; size_t size = ((1 << (level * 2)) * (states / 2) + 7) / 8; + 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); From 0b2b1d7c2443a573c948d1550c82ddfdf5a3d3a4 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 2 Mar 2026 15:40:17 -0800 Subject: [PATCH 16/21] opacitymap: Minor correctness/clarity cleanups - Fix UBSAN alignment violation when hashing OMM data - Fix int/unsigned mismatch when filling source triangle ids - Fix typo in a comment --- src/opacitymap.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 97b4a540d..64d212aca 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -61,7 +61,7 @@ static float sampleTexture(const Texture& texture, float u, float v) unsigned char a01 = texture.data[offset + offsety]; unsigned char a11 = texture.data[offset + offsetx + offsety]; - // blinear interpolation; we do it partially in integer space, deferring full conversion to [0, 1] until the end + // 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); @@ -130,7 +130,8 @@ struct OMMHasher // MurmurHash2 for (size_t i = 0; i < size / 4; ++i) { - unsigned int k = *reinterpret_cast(key + i * 4); + unsigned int k; + memcpy(&k, key + i * 4, sizeof(k)); k *= m; k ^= k >> r; @@ -355,7 +356,7 @@ size_t meshopt_opacityMapMeasure(int* levels, unsigned int* sources, int* omm_in { *entry = unsigned(result); levels[result] = level; - sources[result] = int(i / 3); + sources[result] = unsigned(i / 3); result++; } From d2cb4726bc3bd403d150caca68009cb1248320ef Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Tue, 3 Mar 2026 10:42:25 -0800 Subject: [PATCH 17/21] demo: Add initial opacityMap test This tests all 5 functions (measure/rasterize/compact + preferred mip & triangle size) on a basic example where a quad is mapped to a circle texture. While this test exercises compaction/special index conversion paths, the OMMs rasterized here don't end up being compacted. --- demo/tests.cpp | 112 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/demo/tests.cpp b/demo/tests.cpp index e2989d8b1..e9449b9e0 100644 --- a/demo/tests.cpp +++ b/demo/tests.cpp @@ -2936,6 +2936,116 @@ static void decodeMeshletTypical() assert(memcmp(rt, triangles, sizeof(triangles)) == 0); } +static void opacityMap() +{ + const size_t triangle_count = 2; + const size_t vertex_count = 4; + + const unsigned int indices[triangle_count * 3] = { + 0, 1, 2, + 0, 2, 3, // clang-format :-/ + }; + + const float uvs[vertex_count * 2] = { + 0.f, 0.f, + 1.f, 0.f, + 1.f, 1.f, + 0.f, 1.f, // 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] = meshopt_quantizeUnorm(dc + 0.5f, 8); + } + + // subdivision parameters + const float target_edge = 3.f; + const int max_level = 4; + + // 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 + int levels[triangle_count]; + unsigned int sources[triangle_count]; + int omm_indices[triangle_count]; + + 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); + + // state histogram for testing + int histogram[2][4] = {}; + + for (int k = 0; k < 2; ++k) + { + int states = 2 << k; + + // 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_opacityMapTriangleSize(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_opacityMapTriangleSize(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 histogram based on underlying test data; naturally this depends on rasterization specifics + 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.31f) < 1e-2f); + assert(fabsf(known - 0.73f) < 1e-2f); +} + void runTests() { decodeIndexV0(); @@ -3063,4 +3173,6 @@ void runTests() decodeMeshletSafety(); decodeMeshletBasic(); decodeMeshletTypical(); + + opacityMap(); } From 8a46bdbacf63f271fec8a828a445ce392b84d7b7 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Tue, 3 Mar 2026 11:01:40 -0800 Subject: [PATCH 18/21] demo: Use a more complex tessellation pattern for opacityMap test Instead of testing a quad, we test a 6-triangle tessellation with 4 corners (that lie outside of the circle) and 2 center triangles. We additionally flip one of the center triangles so that it's a perfect mirror of the other one and produces the same OMM data, so that compaction can deduplicate these. Some of the test assertions are only valid for the current algorithm; these may need to be tweaked in the future. --- demo/tests.cpp | 58 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/demo/tests.cpp b/demo/tests.cpp index e9449b9e0..7e7201cbd 100644 --- a/demo/tests.cpp +++ b/demo/tests.cpp @@ -2938,19 +2938,30 @@ static void decodeMeshletTypical() static void opacityMap() { - const size_t triangle_count = 2; - const size_t vertex_count = 4; + const size_t triangle_count = 6; + const size_t vertex_count = 8; const unsigned int indices[triangle_count * 3] = { - 0, 1, 2, - 0, 2, 3, // clang-format :-/ + // 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, // clang-format :-/ + 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; @@ -2966,21 +2977,13 @@ static void opacityMap() float dy = float(y) + 0.5f - center; float dc = radius - sqrtf(dx * dx + dy * dy); - texture[y * texture_size + x] = meshopt_quantizeUnorm(dc + 0.5f, 8); + texture[y * texture_size + x] = (unsigned char)meshopt_quantizeUnorm(dc + 0.5f, 8); } - // subdivision parameters - const float target_edge = 3.f; + // subdivision parameterrs + const float target_edge = 2.5f; const int max_level = 4; - // 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 - int levels[triangle_count]; - unsigned int sources[triangle_count]; - int omm_indices[triangle_count]; - - 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); - // state histogram for testing int histogram[2][4] = {}; @@ -2988,6 +2991,18 @@ static void opacityMap() { 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; @@ -3033,17 +3048,22 @@ static void opacityMap() // 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; naturally this depends on rasterization specifics + // 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.31f) < 1e-2f); - assert(fabsf(known - 0.73f) < 1e-2f); + assert(fabsf(opaque - 0.38f) < 1e-2f); + assert(fabsf(known - 0.66f) < 1e-2f); } void runTests() From 666ff4c4d3d71444bac39e5c7b8cc4c63711e418 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Tue, 3 Mar 2026 14:13:24 -0800 Subject: [PATCH 19/21] opacitymap: Reuse and simplify hashing code Instead of duplicating MurmurHash code, we keep the code in a separate function - this is identical to the function in indexgenerator but needs to support unaligned inputs, hence "u" suffix. For the OMM data, instead of using custom tail processing code (which can't be fully covered anyhow because the input is a power of two), we fold first and last byte into the seed value. The finalizer should mix these up sufficiently well. UV data is also well structured and long enough not to require the extra finalizer; this matches what we do in indexgenerator too so should be robust. --- src/opacitymap.cpp | 85 +++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 53 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 64d212aca..37aa5cf7d 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -67,38 +67,40 @@ static float sampleTexture(const Texture& texture, float u, float v) return (ax0 + (ax1 - ax0) * ry) * (1.f / 255.f); } -struct TriangleOMMHasher +static unsigned int hashUpdate4u(unsigned int h, const unsigned char* key, size_t len) { - const TriangleOMM* data; + // MurmurHash2 + const unsigned int m = 0x5bd1e995; + const int r = 24; - size_t hash(unsigned int index) const + while (len >= 4) { - const TriangleOMM& tri = data[index]; - unsigned int h = tri.level; + unsigned int k; + memcpy(&k, key, sizeof(k)); - const unsigned int m = 0x5bd1e995; - const int r = 24; + k *= m; + k ^= k >> r; + k *= m; - // MurmurHash2 - const unsigned int* uvs = reinterpret_cast(tri.uvs); + h *= m; + h ^= k; - for (int i = 0; i < 6; ++i) - { - unsigned int k = uvs[i]; + key += 4; + len -= 4; + } - k *= m; - k ^= k >> r; - k *= m; + return h; +} - h *= m; - h ^= k; - } +struct TriangleOMMHasher +{ + const TriangleOMM* data; - // MurmurHash2 finalizer - h ^= h >> 13; - h *= 0x5bd1e995; - h ^= h >> 15; - return h; + 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 @@ -120,40 +122,17 @@ struct OMMHasher size_t hash(unsigned int index) const { size_t size = ((1 << (levels[index] * 2)) * (states / 2) + 7) / 8; + assert(size > 0); + const unsigned char* key = data + offsets[index]; unsigned int h = levels[index]; - const unsigned int m = 0x5bd1e995; - const int r = 24; - - // MurmurHash2 - for (size_t i = 0; i < size / 4; ++i) - { - unsigned int k; - memcpy(&k, key + i * 4, sizeof(k)); - - k *= m; - k ^= k >> r; - k *= m; - - h *= m; - h ^= k; - } - - // MurmurHash2 tail - switch (size & 3) - { - case 3: - h ^= key[size - 3] << 16; - // fallthrough - case 2: - h ^= key[size - 2] << 8; - // fallthrough - case 1: - h ^= key[size - 1]; - h *= m; - } + // 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; From 23327df4b5fde88d7c92fc8b18f59510ce3fdec1 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Tue, 3 Mar 2026 14:33:11 -0800 Subject: [PATCH 20/21] opacitymap: Extract level size computation into a helper function We need this in many different places in the code; in getSpecialIndex we used to use a simpler computation because the data would always take at least a byte, but it can be replaced with a general version too. Also remove which we are not using and don't plan to. --- src/opacitymap.cpp | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 37aa5cf7d..592775e21 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -2,7 +2,6 @@ #include "meshoptimizer.h" #include -#include #include #include @@ -13,12 +12,11 @@ namespace meshopt // 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 - -struct TriangleOMM +inline size_t getLevelSize(int level, int states) { - int uvs[6]; - int level; -}; + // 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 { @@ -92,6 +90,12 @@ static unsigned int hashUpdate4u(unsigned int h, const unsigned char* key, size_ return h; } +struct TriangleOMM +{ + int uvs[6]; + int level; +}; + struct TriangleOMMHasher { const TriangleOMM* data; @@ -121,10 +125,8 @@ struct OMMHasher size_t hash(unsigned int index) const { - size_t size = ((1 << (levels[index] * 2)) * (states / 2) + 7) / 8; - assert(size > 0); - const unsigned char* key = data + offsets[index]; + size_t size = getLevelSize(levels[index], states); unsigned int h = levels[index]; @@ -143,7 +145,7 @@ struct OMMHasher bool equal(unsigned int lhs, unsigned int rhs) const { - size_t size = ((1 << (levels[lhs] * 2)) * (states / 2) + 7) / 8; + size_t size = getLevelSize(levels[lhs], states); return levels[lhs] == levels[rhs] && memcmp(data + offsets[lhs], data + offsets[rhs], size) == 0; } @@ -258,7 +260,7 @@ static int getSpecialIndex(const unsigned char* data, int level, int states) // 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 = (1 << (level * 2)) * (states / 2) / 8; + size_t size = getLevelSize(level, states); for (size_t i = 0; i < size; ++i) if (data[i] != expected) @@ -350,7 +352,7 @@ size_t meshopt_opacityMapTriangleSize(int level, int states) assert(level >= 0 && level <= 12); assert(states == 2 || states == 4); - return ((1 << (level * 2)) * (states / 2) + 7) / 8; + 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) @@ -387,10 +389,9 @@ void meshopt_opacityMapRasterize(unsigned char* result, int level, int states, c assert(texture_stride >= 1 && texture_stride <= 4); assert(texture_pitch >= texture_stride * texture_width); - Texture texture = {texture_data, texture_stride, texture_pitch, texture_width, texture_height}; + memset(result, 0, getLevelSize(level, states)); - // 1-bit 2-state or 2-bit 4-state per micro triangle, rounded up to whole bytes - memset(result, 0, ((1 << (level * 2)) * (states / 2) + 7) / 8); + 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])}; @@ -428,7 +429,7 @@ size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, int* lev assert(level >= 0 && level <= 12); const unsigned char* old = data_old + offsets[i]; - size_t size = ((1 << (level * 2)) * (states / 2) + 7) / 8; + 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 From 7c0344a09990260722a8c52b37c5ccf9b1b39647 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 4 Mar 2026 16:51:25 -0800 Subject: [PATCH 21/21] opacitymap: Rename TriangleSize to EntrySize This matches "Map" naming nicely (map entry size is exactly what the function returns!) and removes the ambiguity between what a "triangle" means here. --- demo/tests.cpp | 4 ++-- src/meshoptimizer.h | 2 +- src/opacitymap.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/demo/tests.cpp b/demo/tests.cpp index 7e7201cbd..26f1cf7c6 100644 --- a/demo/tests.cpp +++ b/demo/tests.cpp @@ -3010,7 +3010,7 @@ static void opacityMap() for (size_t i = 0; i < omm_count; ++i) { offsets[i] = unsigned(data_size); - data_size += meshopt_opacityMapTriangleSize(levels[i], states); + data_size += meshopt_opacityMapEntrySize(levels[i], states); } std::vector data(data_size); @@ -3043,7 +3043,7 @@ static void opacityMap() // 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_opacityMapTriangleSize(levels[compact_count - 1], states)); + 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) diff --git a/src/meshoptimizer.h b/src/meshoptimizer.h index efa974cab..8ea18827d 100644 --- a/src/meshoptimizer.h +++ b/src/meshoptimizer.h @@ -849,7 +849,7 @@ MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapMeasure(int* levels, unsigne 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_opacityMapTriangleSize(int level, int states); +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 diff --git a/src/opacitymap.cpp b/src/opacitymap.cpp index 592775e21..7f0baa7f5 100644 --- a/src/opacitymap.cpp +++ b/src/opacitymap.cpp @@ -347,7 +347,7 @@ size_t meshopt_opacityMapMeasure(int* levels, unsigned int* sources, int* omm_in return result; } -size_t meshopt_opacityMapTriangleSize(int level, int states) +size_t meshopt_opacityMapEntrySize(int level, int states) { assert(level >= 0 && level <= 12); assert(states == 2 || states == 4);