Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9f5f9cf
opacitymap: Initial implementation of 2-state OMM rasterizer
zeux Feb 25, 2026
78840bb
opacitymap: Implement support for 4-state OMM
zeux Feb 25, 2026
2b7a275
opacitymap: Add assertins for input texture parameters
zeux Feb 25, 2026
63a2ee0
opacitymap: Pass corner data in recursive calls by pointer
zeux Feb 25, 2026
8149658
opacitymap: Add opacityMapMeasure to determine subdiv levels
zeux Feb 26, 2026
5960326
opacitymap: Add a function to determine optimal raster mip
zeux Feb 26, 2026
edd4d55
opacitymap: Deduplicate triangle data based on UVs
zeux Feb 26, 2026
2aa5a0b
opacitymap: Quantize UVs to a subpixel grid for deduplication
zeux Feb 26, 2026
8a5d614
opacitymap: Store triangle indices in the hash table
zeux Feb 26, 2026
c14c626
opacitymap: Clamp invalid subdivision levels to 0
zeux Feb 26, 2026
cda771c
opacitymap: Implement hash-based OMM compaction
zeux Feb 26, 2026
6b4967f
opacitymap: Compact triangle data to special indices if possible
zeux Feb 26, 2026
aad02d1
opacitymap: Switch to bilinear filtering with wrap/clamp addressing
zeux Feb 27, 2026
017e5a4
opacitymap: Improve coverage estimation with a better heuristic
zeux Feb 27, 2026
9b8cc9e
opacitymap: Refine function interfaces further
zeux Feb 27, 2026
0b2b1d7
opacitymap: Minor correctness/clarity cleanups
zeux Mar 2, 2026
d2cb472
demo: Add initial opacityMap test
zeux Mar 3, 2026
8a46bdb
demo: Use a more complex tessellation pattern for opacityMap test
zeux Mar 3, 2026
666ff4c
opacitymap: Reuse and simplify hashing code
zeux Mar 3, 2026
23327df
opacitymap: Extract level size computation into a helper function
zeux Mar 3, 2026
7c0344a
opacitymap: Rename TriangleSize to EntrySize
zeux Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
132 changes: 132 additions & 0 deletions demo/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned int> 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<unsigned char> 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();
Expand Down Expand Up @@ -3063,4 +3193,6 @@ void runTests()
decodeMeshletSafety();
decodeMeshletBasic();
decodeMeshletTypical();

opacityMap();
}
9 changes: 9 additions & 0 deletions src/meshoptimizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading