Merge pull request #118778 from Chubercik/meshoptimizer-1.1

meshoptimizer: Update to 1.1
This commit is contained in:
Thaddeus Crews
2026-04-20 14:32:05 -05:00
15 changed files with 2387 additions and 483 deletions
+1 -1
View File
@@ -426,7 +426,7 @@ License: Apache-2.0
Files: thirdparty/meshoptimizer/*
Comment: meshoptimizer
Copyright: 2016-2024, Arseny Kapoulkine
Copyright: 2016-2026, Arseny Kapoulkine
License: Expat
Files: thirdparty/metal-cpp/*
+1 -1
View File
@@ -720,7 +720,7 @@ Update instructions:
## meshoptimizer
- Upstream: https://github.com/zeux/meshoptimizer
- Version: 1.0 (73583c335e541c139821d0de2bf5f12960a04941, 2025)
- Version: 1.1 (dc9d09ed83e1004aef47a1c3c597e0ec64848a37, 2026)
- License: MIT
Files extracted from upstream repository:
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2016-2025 Arseny Kapoulkine
Copyright (c) 2016-2026 Arseny Kapoulkine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+10 -425
View File
@@ -8,10 +8,10 @@
// The block below auto-detects SIMD ISA that can be used on the target platform
#ifndef MESHOPTIMIZER_NO_SIMD
#if defined(__SSE2__) || (defined(_MSC_VER) && defined(_M_X64))
#if defined(__SSE2__) || (defined(_MSC_VER) && defined(_M_X64) && !defined(_M_ARM64EC))
#define SIMD_SSE
#include <emmintrin.h>
#elif defined(__aarch64__) || (defined(_MSC_VER) && defined(_M_ARM64) && _MSC_VER >= 1922)
#elif defined(__aarch64__) || (defined(_MSC_VER) && (defined(_M_ARM64) || defined(_M_ARM64EC)) && _MSC_VER >= 1922)
#define SIMD_NEON
#include <arm_neon.h>
#endif
@@ -19,19 +19,10 @@
// This work is based on:
// Graham Wihlidal. Optimizing the Graphics Pipeline with Compute. 2016
// Matthaeus Chajdas. GeometryFX 1.2 - Cluster Culling. 2016
// Jack Ritter. An Efficient Bounding Sphere. 1990
// Thomas Larsson. Fast and Tight Fitting Bounding Spheres. 2008
// Ingo Wald, Vlastimil Havran. On building fast kd-Trees for Ray Tracing, and on doing that in O(N log N). 2006
namespace meshopt
{
// This must be <= 256 since meshlet indices are stored as bytes
const size_t kMeshletMaxVertices = 256;
// A reasonable limit is around 2*max_vertices or less
const size_t kMeshletMaxTriangles = 512;
// We keep a limited number of seed triangles and add a few triangles per finished meshlet
const size_t kMeshletMaxSeeds = 256;
const size_t kMeshletAddSeeds = 4;
@@ -173,116 +164,6 @@ static void clearUsed(short* used, size_t vertex_count, const unsigned int* indi
}
}
static void computeBoundingSphere(float result[4], const float* points, size_t count, size_t points_stride, const float* radii, size_t radii_stride, size_t axis_count)
{
static const float kAxes[7][3] = {
// X, Y, Z
{1, 0, 0},
{0, 1, 0},
{0, 0, 1},
// XYZ, -XYZ, X-YZ, XY-Z; normalized to unit length
{0.57735026f, 0.57735026f, 0.57735026f},
{-0.57735026f, 0.57735026f, 0.57735026f},
{0.57735026f, -0.57735026f, 0.57735026f},
{0.57735026f, 0.57735026f, -0.57735026f},
};
assert(count > 0);
assert(axis_count <= sizeof(kAxes) / sizeof(kAxes[0]));
size_t points_stride_float = points_stride / sizeof(float);
size_t radii_stride_float = radii_stride / sizeof(float);
// find extremum points along all axes; for each axis we get a pair of points with min/max coordinates
size_t pmin[7], pmax[7];
float tmin[7], tmax[7];
for (size_t axis = 0; axis < axis_count; ++axis)
{
pmin[axis] = pmax[axis] = 0;
tmin[axis] = FLT_MAX;
tmax[axis] = -FLT_MAX;
}
for (size_t i = 0; i < count; ++i)
{
const float* p = points + i * points_stride_float;
float r = radii[i * radii_stride_float];
for (size_t axis = 0; axis < axis_count; ++axis)
{
const float* ax = kAxes[axis];
float tp = ax[0] * p[0] + ax[1] * p[1] + ax[2] * p[2];
float tpmin = tp - r, tpmax = tp + r;
pmin[axis] = (tpmin < tmin[axis]) ? i : pmin[axis];
pmax[axis] = (tpmax > tmax[axis]) ? i : pmax[axis];
tmin[axis] = (tpmin < tmin[axis]) ? tpmin : tmin[axis];
tmax[axis] = (tpmax > tmax[axis]) ? tpmax : tmax[axis];
}
}
// find the pair of points with largest distance
size_t paxis = 0;
float paxisdr = 0;
for (size_t axis = 0; axis < axis_count; ++axis)
{
const float* p1 = points + pmin[axis] * points_stride_float;
const float* p2 = points + pmax[axis] * points_stride_float;
float r1 = radii[pmin[axis] * radii_stride_float];
float r2 = radii[pmax[axis] * radii_stride_float];
float d2 = (p2[0] - p1[0]) * (p2[0] - p1[0]) + (p2[1] - p1[1]) * (p2[1] - p1[1]) + (p2[2] - p1[2]) * (p2[2] - p1[2]);
float dr = sqrtf(d2) + r1 + r2;
if (dr > paxisdr)
{
paxisdr = dr;
paxis = axis;
}
}
// use the longest segment as the initial sphere diameter
const float* p1 = points + pmin[paxis] * points_stride_float;
const float* p2 = points + pmax[paxis] * points_stride_float;
float r1 = radii[pmin[paxis] * radii_stride_float];
float r2 = radii[pmax[paxis] * radii_stride_float];
float paxisd = sqrtf((p2[0] - p1[0]) * (p2[0] - p1[0]) + (p2[1] - p1[1]) * (p2[1] - p1[1]) + (p2[2] - p1[2]) * (p2[2] - p1[2]));
float paxisk = paxisd > 0 ? (paxisd + r2 - r1) / (2 * paxisd) : 0.f;
float center[3] = {p1[0] + (p2[0] - p1[0]) * paxisk, p1[1] + (p2[1] - p1[1]) * paxisk, p1[2] + (p2[2] - p1[2]) * paxisk};
float radius = paxisdr / 2;
// iteratively adjust the sphere up until all points fit
for (size_t i = 0; i < count; ++i)
{
const float* p = points + i * points_stride_float;
float r = radii[i * radii_stride_float];
float d2 = (p[0] - center[0]) * (p[0] - center[0]) + (p[1] - center[1]) * (p[1] - center[1]) + (p[2] - center[2]) * (p[2] - center[2]);
float d = sqrtf(d2);
if (d + r > radius)
{
float k = d > 0 ? (d + r - radius) / (2 * d) : 0.f;
center[0] += k * (p[0] - center[0]);
center[1] += k * (p[1] - center[1]);
center[2] += k * (p[2] - center[2]);
radius = (radius + d + r) / 2;
}
}
result[0] = center[0];
result[1] = center[1];
result[2] = center[2];
result[3] = radius;
}
struct Cone
{
float px, py, pz;
@@ -1130,11 +1011,8 @@ size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_
using namespace meshopt;
assert(index_count % 3 == 0);
assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices);
assert(max_triangles >= 1 && max_triangles <= kMeshletMaxTriangles);
(void)kMeshletMaxVertices;
(void)kMeshletMaxTriangles;
assert(max_vertices >= 3 && max_vertices <= 256);
assert(max_triangles >= 1 && max_triangles <= 512);
// meshlet construction is limited by max vertices and max triangles per meshlet
// the worst case is that the input is an unindexed stream since this equally stresses both limits
@@ -1154,8 +1032,8 @@ size_t meshopt_buildMeshletsFlex(meshopt_Meshlet* meshlets, unsigned int* meshle
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices);
assert(min_triangles >= 1 && min_triangles <= max_triangles && max_triangles <= kMeshletMaxTriangles);
assert(max_vertices >= 3 && max_vertices <= 256);
assert(min_triangles >= 1 && min_triangles <= max_triangles && max_triangles <= 512);
assert(cone_weight >= 0 && cone_weight <= 1);
assert(split_factor >= 0);
@@ -1348,8 +1226,8 @@ size_t meshopt_buildMeshletsScan(meshopt_Meshlet* meshlets, unsigned int* meshle
assert(index_count % 3 == 0);
assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices);
assert(max_triangles >= 1 && max_triangles <= kMeshletMaxTriangles);
assert(max_vertices >= 3 && max_vertices <= 256);
assert(max_triangles >= 1 && max_triangles <= 512);
meshopt_Allocator allocator;
@@ -1385,8 +1263,8 @@ size_t meshopt_buildMeshletsSpatial(struct meshopt_Meshlet* meshlets, unsigned i
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices);
assert(min_triangles >= 1 && min_triangles <= max_triangles && max_triangles <= kMeshletMaxTriangles);
assert(max_vertices >= 3 && max_vertices <= 256);
assert(min_triangles >= 1 && min_triangles <= max_triangles && max_triangles <= 512);
if (index_count == 0)
return 0;
@@ -1476,298 +1354,5 @@ size_t meshopt_buildMeshletsSpatial(struct meshopt_Meshlet* meshlets, unsigned i
return meshlet_offset;
}
meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(index_count / 3 <= kMeshletMaxTriangles);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
(void)vertex_count;
size_t vertex_stride_float = vertex_positions_stride / sizeof(float);
// compute triangle normals and gather triangle corners
float normals[kMeshletMaxTriangles][3];
float corners[kMeshletMaxTriangles][3][3];
size_t triangles = 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);
const float* p0 = vertex_positions + vertex_stride_float * a;
const float* p1 = vertex_positions + vertex_stride_float * b;
const float* p2 = vertex_positions + vertex_stride_float * c;
float p10[3] = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]};
float p20[3] = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]};
float normalx = p10[1] * p20[2] - p10[2] * p20[1];
float normaly = p10[2] * p20[0] - p10[0] * p20[2];
float normalz = p10[0] * p20[1] - p10[1] * p20[0];
float area = sqrtf(normalx * normalx + normaly * normaly + normalz * normalz);
// no need to include degenerate triangles - they will be invisible anyway
if (area == 0.f)
continue;
// record triangle normals & corners for future use; normal and corner 0 define a plane equation
normals[triangles][0] = normalx / area;
normals[triangles][1] = normaly / area;
normals[triangles][2] = normalz / area;
memcpy(corners[triangles][0], p0, 3 * sizeof(float));
memcpy(corners[triangles][1], p1, 3 * sizeof(float));
memcpy(corners[triangles][2], p2, 3 * sizeof(float));
triangles++;
}
meshopt_Bounds bounds = {};
// degenerate cluster, no valid triangles => trivial reject (cone data is 0)
if (triangles == 0)
return bounds;
const float rzero = 0.f;
// compute cluster bounding sphere; we'll use the center to determine normal cone apex as well
float psphere[4] = {};
computeBoundingSphere(psphere, corners[0][0], triangles * 3, sizeof(float) * 3, &rzero, 0, 7);
float center[3] = {psphere[0], psphere[1], psphere[2]};
// treating triangle normals as points, find the bounding sphere - the sphere center determines the optimal cone axis
float nsphere[4] = {};
computeBoundingSphere(nsphere, normals[0], triangles, sizeof(float) * 3, &rzero, 0, 3);
float axis[3] = {nsphere[0], nsphere[1], nsphere[2]};
float axislength = sqrtf(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
float invaxislength = axislength == 0.f ? 0.f : 1.f / axislength;
axis[0] *= invaxislength;
axis[1] *= invaxislength;
axis[2] *= invaxislength;
// compute a tight cone around all normals, mindp = cos(angle/2)
float mindp = 1.f;
for (size_t i = 0; i < triangles; ++i)
{
float dp = normals[i][0] * axis[0] + normals[i][1] * axis[1] + normals[i][2] * axis[2];
mindp = (dp < mindp) ? dp : mindp;
}
// fill bounding sphere info; note that below we can return bounds without cone information for degenerate cones
bounds.center[0] = center[0];
bounds.center[1] = center[1];
bounds.center[2] = center[2];
bounds.radius = psphere[3];
// degenerate cluster, normal cone is larger than a hemisphere => trivial accept
// note that if mindp is positive but close to 0, the triangle intersection code below gets less stable
// we arbitrarily decide that if a normal cone is ~168 degrees wide or more, the cone isn't useful
if (mindp <= 0.1f)
{
bounds.cone_cutoff = 1;
bounds.cone_cutoff_s8 = 127;
return bounds;
}
float maxt = 0;
// we need to find the point on center-t*axis ray that lies in negative half-space of all triangles
for (size_t i = 0; i < triangles; ++i)
{
// dot(center-t*axis-corner, trinormal) = 0
// dot(center-corner, trinormal) - t * dot(axis, trinormal) = 0
float cx = center[0] - corners[i][0][0];
float cy = center[1] - corners[i][0][1];
float cz = center[2] - corners[i][0][2];
float dc = cx * normals[i][0] + cy * normals[i][1] + cz * normals[i][2];
float dn = axis[0] * normals[i][0] + axis[1] * normals[i][1] + axis[2] * normals[i][2];
// dn should be larger than mindp cutoff above
assert(dn > 0.f);
float t = dc / dn;
maxt = (t > maxt) ? t : maxt;
}
// cone apex should be in the negative half-space of all cluster triangles by construction
bounds.cone_apex[0] = center[0] - axis[0] * maxt;
bounds.cone_apex[1] = center[1] - axis[1] * maxt;
bounds.cone_apex[2] = center[2] - axis[2] * maxt;
// note: this axis is the axis of the normal cone, but our test for perspective camera effectively negates the axis
bounds.cone_axis[0] = axis[0];
bounds.cone_axis[1] = axis[1];
bounds.cone_axis[2] = axis[2];
// cos(a) for normal cone is mindp; we need to add 90 degrees on both sides and invert the cone
// which gives us -cos(a+90) = -(-sin(a)) = sin(a) = sqrt(1 - cos^2(a))
bounds.cone_cutoff = sqrtf(1 - mindp * mindp);
// quantize axis & cutoff to 8-bit SNORM format
bounds.cone_axis_s8[0] = (signed char)(meshopt_quantizeSnorm(bounds.cone_axis[0], 8));
bounds.cone_axis_s8[1] = (signed char)(meshopt_quantizeSnorm(bounds.cone_axis[1], 8));
bounds.cone_axis_s8[2] = (signed char)(meshopt_quantizeSnorm(bounds.cone_axis[2], 8));
// for the 8-bit test to be conservative, we need to adjust the cutoff by measuring the max. error
float cone_axis_s8_e0 = fabsf(bounds.cone_axis_s8[0] / 127.f - bounds.cone_axis[0]);
float cone_axis_s8_e1 = fabsf(bounds.cone_axis_s8[1] / 127.f - bounds.cone_axis[1]);
float cone_axis_s8_e2 = fabsf(bounds.cone_axis_s8[2] / 127.f - bounds.cone_axis[2]);
// note that we need to round this up instead of rounding to nearest, hence +1
int cone_cutoff_s8 = int(127 * (bounds.cone_cutoff + cone_axis_s8_e0 + cone_axis_s8_e1 + cone_axis_s8_e2) + 1);
bounds.cone_cutoff_s8 = (cone_cutoff_s8 > 127) ? 127 : (signed char)(cone_cutoff_s8);
return bounds;
}
meshopt_Bounds meshopt_computeMeshletBounds(const unsigned int* meshlet_vertices, const unsigned char* meshlet_triangles, size_t triangle_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
{
using namespace meshopt;
assert(triangle_count <= kMeshletMaxTriangles);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
unsigned int indices[kMeshletMaxTriangles * 3];
for (size_t i = 0; i < triangle_count * 3; ++i)
{
unsigned int index = meshlet_vertices[meshlet_triangles[i]];
assert(index < vertex_count);
indices[i] = index;
}
return meshopt_computeClusterBounds(indices, triangle_count * 3, vertex_positions, vertex_count, vertex_positions_stride);
}
meshopt_Bounds meshopt_computeSphereBounds(const float* positions, size_t count, size_t positions_stride, const float* radii, size_t radii_stride)
{
using namespace meshopt;
assert(positions_stride >= 12 && positions_stride <= 256);
assert(positions_stride % sizeof(float) == 0);
assert((radii_stride >= 4 && radii_stride <= 256) || radii == NULL);
assert(radii_stride % sizeof(float) == 0);
meshopt_Bounds bounds = {};
if (count == 0)
return bounds;
const float rzero = 0.f;
float psphere[4] = {};
computeBoundingSphere(psphere, positions, count, positions_stride, radii ? radii : &rzero, radii ? radii_stride : 0, 7);
bounds.center[0] = psphere[0];
bounds.center[1] = psphere[1];
bounds.center[2] = psphere[2];
bounds.radius = psphere[3];
return bounds;
}
void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t triangle_count, size_t vertex_count)
{
using namespace meshopt;
assert(triangle_count <= kMeshletMaxTriangles);
assert(vertex_count <= kMeshletMaxVertices);
unsigned char* indices = meshlet_triangles;
unsigned int* vertices = meshlet_vertices;
// cache tracks vertex timestamps (corresponding to triangle index! all 3 vertices are added at the same time and never removed)
unsigned char cache[kMeshletMaxVertices];
memset(cache, 0, vertex_count);
// note that we start from a value that means all vertices aren't in cache
unsigned char cache_last = 128;
const unsigned char cache_cutoff = 3; // 3 triangles = ~5..9 vertices depending on reuse
for (size_t i = 0; i < triangle_count; ++i)
{
int next = -1;
int next_match = -1;
for (size_t j = i; j < triangle_count; ++j)
{
unsigned char a = indices[j * 3 + 0], b = indices[j * 3 + 1], c = indices[j * 3 + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
// score each triangle by how many vertices are in cache
// note: the distance is computed using unsigned 8-bit values, so cache timestamp overflow is handled gracefully
int aok = (unsigned char)(cache_last - cache[a]) < cache_cutoff;
int bok = (unsigned char)(cache_last - cache[b]) < cache_cutoff;
int cok = (unsigned char)(cache_last - cache[c]) < cache_cutoff;
if (aok + bok + cok > next_match)
{
next = (int)j;
next_match = aok + bok + cok;
// note that we could end up with all 3 vertices in the cache, but 2 is enough for ~strip traversal
if (next_match >= 2)
break;
}
}
assert(next >= 0);
unsigned char a = indices[next * 3 + 0], b = indices[next * 3 + 1], c = indices[next * 3 + 2];
// shift triangles before the next one forward so that we always keep an ordered partition
// note: this could have swapped triangles [i] and [next] but that distorts the order and may skew the output sequence
memmove(indices + (i + 1) * 3, indices + i * 3, (next - i) * 3 * sizeof(unsigned char));
indices[i * 3 + 0] = a;
indices[i * 3 + 1] = b;
indices[i * 3 + 2] = c;
// cache timestamp is the same between all vertices of each triangle to reduce overflow
cache_last++;
cache[a] = cache_last;
cache[b] = cache_last;
cache[c] = cache_last;
}
// reorder meshlet vertices for access locality assuming index buffer is scanned sequentially
unsigned int order[kMeshletMaxVertices];
short remap[kMeshletMaxVertices];
memset(remap, -1, vertex_count * sizeof(short));
size_t vertex_offset = 0;
for (size_t i = 0; i < triangle_count * 3; ++i)
{
short& r = remap[indices[i]];
if (r < 0)
{
r = short(vertex_offset);
order[vertex_offset] = vertices[indices[i]];
vertex_offset++;
}
indices[i] = (unsigned char)r;
}
assert(vertex_offset <= vertex_count);
memcpy(vertices, order, vertex_offset * sizeof(unsigned int));
}
#undef SIMD_SSE
#undef SIMD_NEON
-1
View File
@@ -74,7 +74,6 @@ meshopt_VertexCacheStatistics meshopt_analyzeVertexCache(const unsigned int* ind
meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const unsigned int* indices, size_t index_count, size_t vertex_count, size_t vertex_size)
{
assert(index_count % 3 == 0);
assert(vertex_size > 0 && vertex_size <= 256);
meshopt_Allocator allocator;
+4 -8
View File
@@ -19,12 +19,6 @@ const int kDecodeIndexVersion = 1;
typedef unsigned int VertexFifo[16];
typedef unsigned int EdgeFifo[16][2];
static const unsigned int kTriangleIndexOrder[3][3] = {
{0, 1, 2},
{1, 2, 0},
{2, 0, 1},
};
static const unsigned char kCodeAuxEncodingTable[16] = {
0x00, 0x76, 0x87, 0x56, 0x67, 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, 0x98, 0x01, 0x69,
0, 0, // last two entries aren't used for encoding
@@ -194,6 +188,8 @@ size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, cons
int fecmax = version >= 1 ? 13 : 15;
static const int rotations[] = {0, 1, 2, 0, 1};
// use static encoding table; it's possible to pack the result and then build an optimal table and repack
// for now we keep it simple and use the table that has been generated based on symbol frequency on a training mesh set
const unsigned char* codeaux_table = kCodeAuxEncodingTable;
@@ -211,7 +207,7 @@ size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, cons
if (fer >= 0 && (fer >> 2) < 15)
{
// note: getEdgeFifo implicitly rotates triangles by matching a/b to existing edge
const unsigned int* order = kTriangleIndexOrder[fer & 3];
const int* order = rotations + (fer & 3);
unsigned int a = indices[i + order[0]], b = indices[i + order[1]], c = indices[i + order[2]];
@@ -247,7 +243,7 @@ size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, cons
else
{
int rotation = rotateTriangle(indices[i + 0], indices[i + 1], indices[i + 2], next);
const unsigned int* order = kTriangleIndexOrder[rotation];
const int* order = rotations + rotation;
unsigned int a = indices[i + order[0]], b = indices[i + order[1]], c = indices[i + order[2]];
-5
View File
@@ -311,7 +311,6 @@ size_t meshopt_generateVertexRemap(unsigned int* destination, const unsigned int
using namespace meshopt;
assert(indices || index_count == vertex_count);
assert(!indices || index_count % 3 == 0);
assert(vertex_size > 0 && vertex_size <= 256);
meshopt_Allocator allocator;
@@ -325,7 +324,6 @@ size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const unsigne
using namespace meshopt;
assert(indices || index_count == vertex_count);
assert(index_count % 3 == 0);
assert(stream_count > 0 && stream_count <= 16);
for (size_t i = 0; i < stream_count; ++i)
@@ -345,7 +343,6 @@ size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const unsign
using namespace meshopt;
assert(indices || index_count == vertex_count);
assert(!indices || index_count % 3 == 0);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
@@ -393,8 +390,6 @@ void meshopt_remapVertexBuffer(void* destination, const void* vertices, size_t v
void meshopt_remapIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const unsigned int* remap)
{
assert(index_count % 3 == 0);
for (size_t i = 0; i < index_count; ++i)
{
unsigned int index = indices ? indices[i] : unsigned(i);
File diff suppressed because it is too large Load Diff
+613
View File
@@ -0,0 +1,613 @@
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
#include "meshoptimizer.h"
#include <assert.h>
#include <float.h>
#include <math.h>
#include <string.h>
// This work is based on:
// Matthaeus Chajdas. GeometryFX 1.2 - Cluster Culling. 2016
// Jack Ritter. An Efficient Bounding Sphere. 1990
// Thomas Larsson. Fast and Tight Fitting Bounding Spheres. 2008
namespace meshopt
{
// This must be <= 256 since meshlet indices are stored as bytes
const size_t kMeshletMaxVertices = 256;
// A reasonable limit is around 2*max_vertices or less
const size_t kMeshletMaxTriangles = 512;
static void computeBoundingSphere(float result[4], const float* points, size_t count, size_t points_stride, const float* radii, size_t radii_stride, size_t axis_count, const unsigned int* indices = NULL)
{
static const float axes[7][3] = {
// X, Y, Z
{1, 0, 0},
{0, 1, 0},
{0, 0, 1},
// XYZ, -XYZ, X-YZ, XY-Z; normalized to unit length
{0.57735026f, 0.57735026f, 0.57735026f},
{-0.57735026f, 0.57735026f, 0.57735026f},
{0.57735026f, -0.57735026f, 0.57735026f},
{0.57735026f, 0.57735026f, -0.57735026f},
};
assert(count > 0);
assert(axis_count <= sizeof(axes) / sizeof(axes[0]));
size_t points_stride_float = points_stride / sizeof(float);
size_t radii_stride_float = radii_stride / sizeof(float);
// find extremum points along all axes; for each axis we get a pair of points with min/max coordinates
unsigned int pmin[7], pmax[7];
float tmin[7], tmax[7];
for (size_t axis = 0; axis < axis_count; ++axis)
{
pmin[axis] = pmax[axis] = 0;
tmin[axis] = FLT_MAX;
tmax[axis] = -FLT_MAX;
}
for (size_t i = 0; i < count; ++i)
{
unsigned int v = indices ? indices[i] : unsigned(i);
const float* p = points + v * points_stride_float;
float r = radii[v * radii_stride_float];
for (size_t axis = 0; axis < axis_count; ++axis)
{
const float* ax = axes[axis];
float tp = ax[0] * p[0] + ax[1] * p[1] + ax[2] * p[2];
float tpmin = tp - r, tpmax = tp + r;
pmin[axis] = (tpmin < tmin[axis]) ? v : pmin[axis];
pmax[axis] = (tpmax > tmax[axis]) ? v : pmax[axis];
tmin[axis] = (tpmin < tmin[axis]) ? tpmin : tmin[axis];
tmax[axis] = (tpmax > tmax[axis]) ? tpmax : tmax[axis];
}
}
// find the pair of points with largest distance
size_t paxis = 0;
float paxisdr = 0;
for (size_t axis = 0; axis < axis_count; ++axis)
{
const float* p1 = points + pmin[axis] * points_stride_float;
const float* p2 = points + pmax[axis] * points_stride_float;
float r1 = radii[pmin[axis] * radii_stride_float];
float r2 = radii[pmax[axis] * radii_stride_float];
float d2 = (p2[0] - p1[0]) * (p2[0] - p1[0]) + (p2[1] - p1[1]) * (p2[1] - p1[1]) + (p2[2] - p1[2]) * (p2[2] - p1[2]);
float dr = sqrtf(d2) + r1 + r2;
if (dr > paxisdr)
{
paxisdr = dr;
paxis = axis;
}
}
// use the longest segment as the initial sphere diameter
const float* p1 = points + pmin[paxis] * points_stride_float;
const float* p2 = points + pmax[paxis] * points_stride_float;
float r1 = radii[pmin[paxis] * radii_stride_float];
float r2 = radii[pmax[paxis] * radii_stride_float];
float paxisd = sqrtf((p2[0] - p1[0]) * (p2[0] - p1[0]) + (p2[1] - p1[1]) * (p2[1] - p1[1]) + (p2[2] - p1[2]) * (p2[2] - p1[2]));
float paxisk = paxisd > 0 ? (paxisd + r2 - r1) / (2 * paxisd) : 0.f;
float center[3] = {p1[0] + (p2[0] - p1[0]) * paxisk, p1[1] + (p2[1] - p1[1]) * paxisk, p1[2] + (p2[2] - p1[2]) * paxisk};
float radius = paxisdr / 2;
// iteratively adjust the sphere up until all points fit
for (size_t i = 0; i < count; ++i)
{
unsigned int v = indices ? indices[i] : unsigned(i);
const float* p = points + v * points_stride_float;
float r = radii[v * radii_stride_float];
float d2 = (p[0] - center[0]) * (p[0] - center[0]) + (p[1] - center[1]) * (p[1] - center[1]) + (p[2] - center[2]) * (p[2] - center[2]);
float d = sqrtf(d2);
if (d + r > radius)
{
float k = d > 0 ? (d + r - radius) / (2 * d) : 0.f;
center[0] += k * (p[0] - center[0]);
center[1] += k * (p[1] - center[1]);
center[2] += k * (p[2] - center[2]);
radius = (radius + d + r) / 2;
}
}
result[0] = center[0];
result[1] = center[1];
result[2] = center[2];
result[3] = radius;
}
static meshopt_Bounds computeClusterBounds(const unsigned int* indices, size_t index_count, const unsigned int* corners, size_t corner_count, const float* vertex_positions, size_t vertex_positions_stride)
{
size_t vertex_stride_float = vertex_positions_stride / sizeof(float);
// compute triangle normals (.w completes plane equation)
float normals[kMeshletMaxTriangles][4];
size_t triangles = 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];
const float* p0 = vertex_positions + vertex_stride_float * a;
const float* p1 = vertex_positions + vertex_stride_float * b;
const float* p2 = vertex_positions + vertex_stride_float * c;
float p10[3] = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]};
float p20[3] = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]};
float normalx = p10[1] * p20[2] - p10[2] * p20[1];
float normaly = p10[2] * p20[0] - p10[0] * p20[2];
float normalz = p10[0] * p20[1] - p10[1] * p20[0];
float area = sqrtf(normalx * normalx + normaly * normaly + normalz * normalz);
// no need to include degenerate triangles - they will be invisible anyway
if (area == 0.f)
continue;
normalx /= area;
normaly /= area;
normalz /= area;
// record triangle normals; normal and corner 0 define a plane equation
normals[triangles][0] = normalx;
normals[triangles][1] = normaly;
normals[triangles][2] = normalz;
normals[triangles][3] = -(normalx * p0[0] + normaly * p0[1] + normalz * p0[2]);
triangles++;
}
meshopt_Bounds bounds = {};
// degenerate cluster, no valid triangles => trivial reject (cone data is 0)
if (triangles == 0)
return bounds;
const float rzero = 0.f;
// compute cluster bounding sphere; we'll use the center to determine normal cone apex as well
float psphere[4] = {};
computeBoundingSphere(psphere, vertex_positions, corner_count, vertex_positions_stride, &rzero, 0, 7, corners);
float center[3] = {psphere[0], psphere[1], psphere[2]};
// treating triangle normals as points, find the bounding sphere - the sphere center determines the optimal cone axis
float nsphere[4] = {};
computeBoundingSphere(nsphere, normals[0], triangles, sizeof(float) * 4, &rzero, 0, 3);
float axis[3] = {nsphere[0], nsphere[1], nsphere[2]};
float axislength = sqrtf(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
float invaxislength = axislength == 0.f ? 0.f : 1.f / axislength;
axis[0] *= invaxislength;
axis[1] *= invaxislength;
axis[2] *= invaxislength;
// compute a tight cone around all normals, mindp = cos(angle/2)
float mindp = 1.f;
for (size_t i = 0; i < triangles; ++i)
{
float dp = normals[i][0] * axis[0] + normals[i][1] * axis[1] + normals[i][2] * axis[2];
mindp = (dp < mindp) ? dp : mindp;
}
// fill bounding sphere info; note that below we can return bounds without cone information for degenerate cones
bounds.center[0] = center[0];
bounds.center[1] = center[1];
bounds.center[2] = center[2];
bounds.radius = psphere[3];
// degenerate cluster, normal cone is larger than a hemisphere => trivial accept
// note that if mindp is positive but close to 0, the triangle intersection code below gets less stable
// we arbitrarily decide that if a normal cone is ~168 degrees wide or more, the cone isn't useful
if (mindp <= 0.1f)
{
bounds.cone_cutoff = 1;
bounds.cone_cutoff_s8 = 127;
return bounds;
}
float maxt = 0;
// we need to find the point on center-t*axis ray that lies in negative half-space of all triangles
for (size_t i = 0; i < triangles; ++i)
{
// dot(center-t*axis-corner, trinormal) = 0
// dot(center-corner, trinormal) - t * dot(axis, trinormal) = 0
float dc = center[0] * normals[i][0] + center[1] * normals[i][1] + center[2] * normals[i][2] + normals[i][3];
float dn = axis[0] * normals[i][0] + axis[1] * normals[i][1] + axis[2] * normals[i][2];
// dn should be larger than mindp cutoff above
assert(dn > 0.f);
float t = dc / dn;
maxt = (t > maxt) ? t : maxt;
}
// cone apex should be in the negative half-space of all cluster triangles by construction
bounds.cone_apex[0] = center[0] - axis[0] * maxt;
bounds.cone_apex[1] = center[1] - axis[1] * maxt;
bounds.cone_apex[2] = center[2] - axis[2] * maxt;
// note: this axis is the axis of the normal cone, but our test for perspective camera effectively negates the axis
bounds.cone_axis[0] = axis[0];
bounds.cone_axis[1] = axis[1];
bounds.cone_axis[2] = axis[2];
// cos(a) for normal cone is mindp; we need to add 90 degrees on both sides and invert the cone
// which gives us -cos(a+90) = -(-sin(a)) = sin(a) = sqrt(1 - cos^2(a))
bounds.cone_cutoff = sqrtf(1 - mindp * mindp);
// quantize axis & cutoff to 8-bit SNORM format
bounds.cone_axis_s8[0] = (signed char)(meshopt_quantizeSnorm(bounds.cone_axis[0], 8));
bounds.cone_axis_s8[1] = (signed char)(meshopt_quantizeSnorm(bounds.cone_axis[1], 8));
bounds.cone_axis_s8[2] = (signed char)(meshopt_quantizeSnorm(bounds.cone_axis[2], 8));
// for the 8-bit test to be conservative, we need to adjust the cutoff by measuring the max. error
float cone_axis_s8_e0 = fabsf(bounds.cone_axis_s8[0] / 127.f - bounds.cone_axis[0]);
float cone_axis_s8_e1 = fabsf(bounds.cone_axis_s8[1] / 127.f - bounds.cone_axis[1]);
float cone_axis_s8_e2 = fabsf(bounds.cone_axis_s8[2] / 127.f - bounds.cone_axis[2]);
// note that we need to round this up instead of rounding to nearest, hence +1
int cone_cutoff_s8 = int(127 * (bounds.cone_cutoff + cone_axis_s8_e0 + cone_axis_s8_e1 + cone_axis_s8_e2) + 1);
bounds.cone_cutoff_s8 = (cone_cutoff_s8 > 127) ? 127 : (signed char)(cone_cutoff_s8);
return bounds;
}
} // namespace meshopt
meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(index_count / 3 <= kMeshletMaxTriangles);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
(void)vertex_count;
unsigned int cache[512];
memset(cache, -1, sizeof(cache));
unsigned int corners[kMeshletMaxTriangles * 3 + 1]; // +1 for branchless slot
size_t corner_count = 0;
for (size_t i = 0; i < index_count; ++i)
{
unsigned int v = indices[i];
assert(v < vertex_count);
unsigned int& c = cache[v & (sizeof(cache) / sizeof(cache[0]) - 1)];
// branchless append if vertex isn't in cache
corners[corner_count] = v;
corner_count += (c != v);
c = v;
}
return computeClusterBounds(indices, index_count, corners, corner_count, vertex_positions, vertex_positions_stride);
}
meshopt_Bounds meshopt_computeMeshletBounds(const unsigned int* meshlet_vertices, const unsigned char* meshlet_triangles, size_t triangle_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
{
using namespace meshopt;
assert(triangle_count <= kMeshletMaxTriangles);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
(void)vertex_count;
unsigned int indices[kMeshletMaxTriangles * 3];
size_t corner_count = 0;
for (size_t i = 0; i < triangle_count * 3; ++i)
{
unsigned char t = meshlet_triangles[i];
unsigned int index = meshlet_vertices[t];
assert(index < vertex_count);
indices[i] = index;
// meshlet_vertices[] slice should only contain vertices used by triangle indices, which is the case for any well formed meshlet
corner_count = t >= corner_count ? t + 1 : corner_count;
}
return computeClusterBounds(indices, triangle_count * 3, meshlet_vertices, corner_count, vertex_positions, vertex_positions_stride);
}
meshopt_Bounds meshopt_computeSphereBounds(const float* positions, size_t count, size_t positions_stride, const float* radii, size_t radii_stride)
{
using namespace meshopt;
assert(positions_stride >= 12 && positions_stride <= 256);
assert(positions_stride % sizeof(float) == 0);
assert((radii_stride >= 4 && radii_stride <= 256) || radii == NULL);
assert(radii_stride % sizeof(float) == 0);
meshopt_Bounds bounds = {};
if (count == 0)
return bounds;
const float rzero = 0.f;
float psphere[4] = {};
computeBoundingSphere(psphere, positions, count, positions_stride, radii ? radii : &rzero, radii ? radii_stride : 0, 7);
bounds.center[0] = psphere[0];
bounds.center[1] = psphere[1];
bounds.center[2] = psphere[2];
bounds.radius = psphere[3];
return bounds;
}
void meshopt_optimizeMeshletLevel(unsigned int* meshlet_vertices, size_t vertex_count, unsigned char* meshlet_triangles, size_t triangle_count, int level)
{
using namespace meshopt;
assert(triangle_count <= kMeshletMaxTriangles);
assert(vertex_count <= kMeshletMaxVertices);
assert(level >= 0 && level <= 9);
unsigned char* indices = meshlet_triangles;
unsigned int* vertices = meshlet_vertices;
// cache tracks vertex timestamps (corresponding to triangle index! all 3 vertices are added at the same time and never removed)
unsigned char cache[kMeshletMaxVertices];
memset(cache, 0, vertex_count);
// note that we start from a value that means all vertices aren't in cache
unsigned char cache_last = 128;
const unsigned char cache_cutoff = 3; // 3 triangles = ~5..9 vertices depending on reuse
// vertex valence is used to prioritize triangles for level>0
// note: we use 8-bit counters for performance; for outlier vertices the valence is incorrect but that just affects the heuristic
unsigned char valence[kMeshletMaxVertices];
memset(valence, 0, vertex_count);
for (size_t i = 0; i < triangle_count; ++i)
{
unsigned char a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
valence[a]++;
valence[b]++;
valence[c]++;
}
for (size_t i = 0; i < triangle_count; ++i)
{
int next = -1;
int next_score = -1;
int edges = 0;
for (size_t j = i; j < triangle_count; ++j)
{
unsigned char a = indices[j * 3 + 0], b = indices[j * 3 + 1], c = indices[j * 3 + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
// compute cache distance using unsigned 8-bit subtraction, so cache timestamp overflow is handled gracefully
unsigned char ad = (unsigned char)(cache_last - cache[a]);
unsigned char bd = (unsigned char)(cache_last - cache[b]);
unsigned char cd = (unsigned char)(cache_last - cache[c]);
int match = (ad < cache_cutoff) + (bd < cache_cutoff) + (cd < cache_cutoff);
if (level)
{
// prefer low minimum valence
int vmin = valence[a] < valence[b] ? valence[a] : valence[b];
vmin = valence[c] < vmin ? valence[c] : vmin;
// prefer vertices with smaller cache distance and valence to improve traversal locality
int score = match * 1024 + (1023 - ad - bd - cd);
score = score * 256 + (255 - vmin);
next = (score > next_score) ? int(j) : next;
next_score = (score > next_score) ? score : next_score;
// terminate after finding enough edge matches
if (match >= 2 && ++edges >= level)
break;
}
else
{
int score = match;
next = (score > next_score) ? int(j) : next;
next_score = (score > next_score) ? score : next_score;
// settle for a first edge match, which makes the function ~linear in practice
if (match >= 2)
break;
}
}
assert(next >= 0);
unsigned char a = indices[next * 3 + 0], b = indices[next * 3 + 1], c = indices[next * 3 + 2];
// shift triangles before the next one forward so that we always keep an ordered partition
// note: this could have swapped triangles [i] and [next] but that distorts the order and may skew the output sequence
memmove(indices + (i + 1) * 3, indices + i * 3, (next - i) * 3 * sizeof(unsigned char));
indices[i * 3 + 0] = a;
indices[i * 3 + 1] = b;
indices[i * 3 + 2] = c;
// cache timestamp is the same between all vertices of each triangle to reduce overflow
cache_last++;
cache[a] = cache_last;
cache[b] = cache_last;
cache[c] = cache_last;
// update vertex valences for scoring heuristic
valence[a]--;
valence[b]--;
valence[c]--;
}
// rotate triangles to maximize compressibility; only done at level >= 1 for compatibility
if (level >= 1)
{
memset(cache, 0, vertex_count);
for (size_t i = 0; i < triangle_count; ++i)
{
unsigned char a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2];
// if only the middle vertex has been used, rotate triangle to ensure new vertices are always sequential
if (!cache[a] && cache[b] && !cache[c])
{
// abc -> bca
unsigned char t = a;
a = b, b = c, c = t;
}
else if (!cache[a] && !cache[b] && !cache[c])
{
// out of three edges, the edge ab can not be reused by subsequent triangles in some encodings
// if subsequent triangles don't share edges ca or bc, we can rotate the triangle to fix this
bool needab = false, needbc = false, needca = false;
for (size_t j = i + 1; j < triangle_count && j <= i + 3; ++j)
{
unsigned char oa = indices[j * 3 + 0], ob = indices[j * 3 + 1], oc = indices[j * 3 + 2];
// note: edge comparisons are reversed as reused edges are flipped
needab |= (oa == b && ob == a) || (ob == b && oc == a) || (oc == b && oa == a);
needbc |= (oa == c && ob == b) || (ob == c && oc == b) || (oc == c && oa == b);
needca |= (oa == a && ob == c) || (ob == a && oc == c) || (oc == a && oa == c);
}
if (needab && !needbc)
{
// abc -> bca
unsigned char t = a;
a = b, b = c, c = t;
}
else if (needab && !needca)
{
// abc -> cab
unsigned char t = c;
c = b, b = a, a = t;
}
}
indices[i * 3 + 0] = a, indices[i * 3 + 1] = b, indices[i * 3 + 2] = c;
cache[a] = cache[b] = cache[c] = 1;
}
}
// reorder meshlet vertices for access locality assuming index buffer is scanned sequentially
unsigned int order[kMeshletMaxVertices];
short remap[kMeshletMaxVertices];
memset(remap, -1, vertex_count * sizeof(short));
size_t vertex_offset = 0;
for (size_t i = 0; i < triangle_count * 3; ++i)
{
short& r = remap[indices[i]];
if (r < 0)
{
r = short(vertex_offset);
order[vertex_offset] = vertices[indices[i]];
vertex_offset++;
}
indices[i] = (unsigned char)r;
}
assert(vertex_offset <= vertex_count);
memcpy(vertices, order, vertex_offset * sizeof(unsigned int));
}
void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t triangle_count, size_t vertex_count)
{
meshopt_optimizeMeshletLevel(meshlet_vertices, vertex_count, meshlet_triangles, triangle_count, 0);
}
size_t meshopt_extractMeshletIndices(unsigned int* vertices, unsigned char* triangles, const unsigned int* indices, size_t index_count)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(index_count / 3 <= kMeshletMaxTriangles);
size_t unique = 0;
// direct mapped cache for fast lookups based on low index bits; inspired by vk_lod_clusters from NVIDIA
short cache[1024];
memset(cache, -1, sizeof(cache));
for (size_t i = 0; i < index_count; ++i)
{
unsigned int v = indices[i];
unsigned int key = v & (sizeof(cache) / sizeof(cache[0]) - 1);
short c = cache[key];
// fast path: vertex has been seen before
if (c >= 0 && vertices[c] == v)
{
triangles[i] = (unsigned char)c;
continue;
}
// fast path: vertex has never been seen before
if (c < 0)
{
assert(unique < kMeshletMaxVertices);
cache[key] = short(unique);
triangles[i] = (unsigned char)unique;
vertices[unique++] = v;
continue;
}
// slow path: collision with a different vertex, so we need to look through all vertices
int pos = -1;
for (size_t j = 0; j < unique; ++j)
if (vertices[j] == v)
{
pos = int(j);
break;
}
if (pos < 0)
{
assert(unique < kMeshletMaxVertices);
pos = int(unique);
vertices[unique++] = v;
}
cache[key] = short(pos);
triangles[i] = (unsigned char)pos;
}
assert(unique <= kMeshletMaxVertices);
return unique;
}
+116 -7
View File
@@ -1,7 +1,7 @@
/**
* meshoptimizer - version 1.0
* meshoptimizer - version 1.1
*
* Copyright (C) 2016-2025, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
* Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
* Report bugs and download new versions at https://github.com/zeux/meshoptimizer
*
* This library is distributed under the MIT License. See notice at the end of this file.
@@ -12,7 +12,7 @@
#include <stddef.h>
/* Version macro; major * 1000 + minor * 10 + patch */
#define MESHOPTIMIZER_VERSION 1000 /* 1.0 */
#define MESHOPTIMIZER_VERSION 1010 /* 1.1 */
/* If no API is defined, assume default */
#ifndef MESHOPTIMIZER_API
@@ -295,6 +295,38 @@ MESHOPTIMIZER_API size_t meshopt_encodeIndexSequenceBound(size_t index_count, si
*/
MESHOPTIMIZER_API int meshopt_decodeIndexSequence(void* destination, size_t index_count, size_t index_size, const unsigned char* buffer, size_t buffer_size);
/**
* Experimental: Meshlet encoder
* Encodes meshlet data into an array of bytes that is generally smaller and compresses better compared to original.
* Returns encoded data size on success, 0 on error; the only error condition is if buffer doesn't have enough space
* This function encodes a single meshlet; when encoding multiple meshlets, additional headers may be necessary to store vertex/triangle count and encoded size.
* For maximum efficiency the meshlet being encoded should be optimized using meshopt_optimizeMeshletLevel with level 1+ (3 recommended); additionally, vertex reference data should be optimized for locality (fetch).
*
* buffer must contain enough space for the encoded meshlet (use meshopt_encodeMeshletBound to compute worst case size)
* vertices may be NULL, in which case vertex_count must be 0 and only triangle data is encoded
* vertex_count and triangle_count must be <= 256.
*/
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_encodeMeshlet(unsigned char* buffer, size_t buffer_size, const unsigned int* vertices, size_t vertex_count, const unsigned char* triangles, size_t triangle_count);
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_encodeMeshletBound(size_t max_vertices, size_t max_triangles);
/**
* Experimental: Meshlet decoder
* Decodes meshlet data from an array of bytes generated by meshopt_encodeMeshlet
* Returns 0 if decoding was successful, and an error code otherwise
* The decoder is safe to use for untrusted input, but it may produce garbage data.
*
* vertices must contain enough space for the resulting vertex data, aligned to 4 bytes (align(vertex_count * vertex_size, 4) bytes)
* vertex_size must be 2 (16-bit vertex references) or 4 (32-bit vertex references)
* triangles must contain enough space for the resulting triangle data, aligned to 4 bytes (align(triangle_count * triangle_size, 4) bytes)
* triangle_size must be 3 (8-bit triangle indices) or 4 (32-bit packed triangles, stored as (a) | (b << 8) | (c << 16))
* vertex_count, triangle_count match those used during encoding exactly; buffer_size must be equal to the encoded size returned by meshopt_encodeMeshlet.
* vertices may be NULL, in which case vertex_count must be 0 and the meshlet must contain just triangle data
*
* When using "raw" decoding (meshopt_decodeMeshletRaw), both vertices and triangles should have available space further aligned to 16 bytes for efficient SIMD decoding.
*/
MESHOPTIMIZER_EXPERIMENTAL int meshopt_decodeMeshlet(void* vertices, size_t vertex_count, size_t vertex_size, void* triangles, size_t triangle_count, size_t triangle_size, const unsigned char* buffer, size_t buffer_size);
MESHOPTIMIZER_EXPERIMENTAL int meshopt_decodeMeshletRaw(unsigned int* vertices, size_t vertex_count, unsigned int* triangles, size_t triangle_count, const unsigned char* buffer, size_t buffer_size);
/**
* Vertex buffer encoder
* Encodes vertex data into an array of bytes that is generally smaller and compresses better compared to original.
@@ -422,10 +454,12 @@ enum
meshopt_SimplifyRegularize = 1 << 4,
/* Experimental: Allow collapses across attribute discontinuities, except for vertices that are tagged with meshopt_SimplifyVertex_Protect in vertex_lock. */
meshopt_SimplifyPermissive = 1 << 5,
/* Experimental: Produce more regular triangle sizes and shapes during simplification, at a small cost to geometric and attribute quality. */
meshopt_SimplifyRegularizeLight = 1 << 6,
};
/**
* Experimental: Simplification vertex flags/locks, for use in `vertex_lock` arrays in simplification APIs
* Simplification vertex flags/locks, for use in `vertex_lock` arrays in simplification APIs
*/
enum
{
@@ -433,6 +467,8 @@ enum
meshopt_SimplifyVertex_Lock = 1 << 0,
/* Protect attribute discontinuity at this vertex; must be used together with meshopt_SimplifyPermissive option. */
meshopt_SimplifyVertex_Protect = 1 << 1,
/* Experimental: Increase priority for this vertex, making it more likely that it's preserved during simplification. */
meshopt_SimplifyVertex_Priority = 1 << 2,
};
/**
@@ -470,7 +506,7 @@ MESHOPTIMIZER_API size_t meshopt_simplify(unsigned int* destination, const unsig
* vertex_attributes should have attribute_count floats for each vertex
* attribute_weights should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position
* attribute_count must be <= 32
* vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex; 1 denotes vertices that can't be moved
* vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex composed of meshopt_SimplifyVertex_* flags
* target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1]
* options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default
* result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification
@@ -493,7 +529,7 @@ MESHOPTIMIZER_API size_t meshopt_simplifyWithAttributes(unsigned int* destinatio
* vertex_attributes should have attribute_count floats for each vertex
* attribute_weights should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position
* attribute_count must be <= 32
* vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex; 1 denotes vertices that can't be moved
* vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex composed of meshopt_SimplifyVertex_* flags
* target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1]
* options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default
* result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification
@@ -706,6 +742,15 @@ MESHOPTIMIZER_API size_t meshopt_buildMeshletsSpatial(struct meshopt_Meshlet* me
*/
MESHOPTIMIZER_API void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t triangle_count, size_t vertex_count);
/**
* Experimental: Meshlet optimizer
* Reorders meshlet vertices and triangles to maximize locality, with higher levels resulting in smaller compressed size at the cost of optimization time.
* At level 0 the result is equivalent to meshopt_optimizeMeshlet; levels >= 1 may rotate triangle corners to improve compression (which can change provoking vertex and affect OMM data).
*
* level should be in the range [0, 9] with 0 equivalent to meshopt_optimizeMeshlet and 9 being the slowest; the sweet spot for compression ratio is around 3
*/
MESHOPTIMIZER_EXPERIMENTAL void meshopt_optimizeMeshletLevel(unsigned int* meshlet_vertices, size_t vertex_count, unsigned char* meshlet_triangles, size_t triangle_count, int level);
struct meshopt_Bounds
{
/* bounding sphere, useful for frustum and occlusion culling */
@@ -743,6 +788,7 @@ struct meshopt_Bounds
*
* vertex_positions should have float3 position in the first 12 bytes of each vertex
* vertex_count should specify the number of vertices in the entire mesh, not cluster or meshlet
* indices should have at most 256 unique vertex indices
* index_count/3 and triangle_count must not exceed implementation limits (<= 512)
*/
MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
@@ -757,6 +803,15 @@ MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeMeshletBounds(const unsig
*/
MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeSphereBounds(const float* positions, size_t count, size_t positions_stride, const float* radii, size_t radii_stride);
/**
* Experimental: Extract meshlet-local vertex and triangle indices from absolute cluster indices.
* Fills triangles[] and vertices[] such that vertices[triangles[i]] == indices[i], and returns the number of unique vertices.
*
* indices should have at most 256 unique vertex indices
* index_count/3 must not exceed implementation limits (<= 512)
*/
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_extractMeshletIndices(unsigned int* vertices, unsigned char* triangles, const unsigned int* indices, size_t index_count);
/**
* Cluster partitioner
* Partitions clusters into groups of similar size, prioritizing grouping clusters that share vertices or are close to each other.
@@ -800,6 +855,49 @@ 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 (measure)
* Computes a subdivision level for each input triangle, as well as deduplicating the triangles that reference the same UVs to reduce rasterization requests.
* Returns the number of OMM entries.
*
* levels and sources must contain enough space for the worst case output (index_count/3 elements, one per resulting OMM entry)
* levels[i] will contain the subdivision level for entry i, with the total number of entries returned by the function; each entry should be rasterized from triangle index sources[i]
* omm_indices must contain enough space for the resulting OMM indices (index_count/3 elements, one per triangle)
* vertex_uvs should have float2 texture coordinate in the first 8 bytes of each vertex
* max_level specifies the maximum subdivision level (0..12)
* target_edge can be 0; when >0, triangle subdivision is adaptive and targets target_edge^2 texel area
*/
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapMeasure(unsigned char* 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);
/**
* Experimental: Opacity micromap generator (rasterize)
* Rasterizes opacity state for a single triangle entry by sampling the alpha texture, using bilinear filtering and 0.5 alpha cutoff.
*
* result should contain enough space for the output opacity data (which can be computed using meshopt_opacityMapEntrySize)
* level specifies the subdivision level (0..12)
* states should be 2 for 2-state format (opaque/transparent) and 4 for 4-state format (opaque/transparent/unknown)
* uv0/uv1/uv2 should refer to a float2 texture coordinate for each triangle corner; note that micromap data is sensitive to the corner order
* texture_data should point to the alpha channel of the first pixel, encoded as UNORM8
* texture_stride specifies the distance in bytes between consecutive pixels, e.g. 4 for RGBA input
* texture_pitch specifies the distance in bytes between consecutive rows, e.g. 4*texture_width for tightly packed RGBA input
*/
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_opacityMapEntrySize(int level, int states);
/**
* Experimental: Opacity micromap generator (compact)
* Compacts and deduplicates opacity data, merging identical micromap entries and replacing micromap states with special indices (-4..-1) when possible.
* Returns the number of OMM entries after compaction; the data array should be trimmed using the last offset/size.
*
* data should contain opacity data for all input/output entries
* levels should contain subdivision levels for all input/output entries
* offsets should contain offset into data[] for each entry
* levels[i] and offsets[i] will be updated with post-compaction level/offset for entry i, with the total number of entries returned by the function
* omm_indices should contain indices into the original OMM data, and will be updated with a new index or a special index (-4..-1) when possible
* states should be 2 for 2-state format (opaque/transparent) and 4 for 4-state format (opaque/transparent/unknown)
*/
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, unsigned char* levels, unsigned int* offsets, size_t omm_count, int* omm_indices, size_t triangle_count, 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
@@ -899,6 +997,8 @@ template <typename T>
inline size_t meshopt_encodeIndexSequence(unsigned char* buffer, size_t buffer_size, const T* indices, size_t index_count);
template <typename T>
inline int meshopt_decodeIndexSequence(T* destination, size_t index_count, const unsigned char* buffer, size_t buffer_size);
template <typename V, typename T>
inline int meshopt_decodeMeshlet(V* vertices, size_t vertex_count, T* triangles, size_t triangle_count, const unsigned char* buffer, size_t buffer_size);
inline size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level);
template <typename T>
inline size_t meshopt_simplify(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options = 0, float* result_error = NULL);
@@ -1255,6 +1355,15 @@ inline int meshopt_decodeIndexSequence(T* destination, size_t index_count, const
return meshopt_decodeIndexSequence(destination, index_count, sizeof(T), buffer, buffer_size);
}
template <typename V, typename T>
inline int meshopt_decodeMeshlet(V* vertices, size_t vertex_count, T* triangles, size_t triangle_count, const unsigned char* buffer, size_t buffer_size)
{
char types_valid[(sizeof(V) == 2 || sizeof(V) == 4) && (sizeof(T) == 1 || sizeof(T) == 4) ? 1 : -1];
(void)types_valid;
return meshopt_decodeMeshlet(vertices, vertex_count, sizeof(V), triangles, triangle_count, sizeof(T) == 1 ? 3 : 4, buffer, buffer_size);
}
inline size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level)
{
return meshopt_encodeVertexBufferLevel(buffer, buffer_size, vertices, vertex_count, vertex_size, level, -1);
@@ -1422,7 +1531,7 @@ inline void meshopt_spatialSortTriangles(T* destination, const T* indices, size_
#endif
/**
* Copyright (c) 2016-2025 Arseny Kapoulkine
* Copyright (c) 2016-2026 Arseny Kapoulkine
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
+557
View File
@@ -0,0 +1,557 @@
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
#include "meshoptimizer.h"
#include <assert.h>
#include <math.h>
#include <string.h>
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;
float widthf, heightf; // width * 256.f, height * 256.f
};
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 16.8 fixed point coordinates (rounded to nearest subpixel) with texel centers on integer grid
int uf = int(u * texture.widthf - 127.5f);
int vf = int(v * texture.heightf - 127.5f);
// clamp to avoid extrapolation past left/top edge since we don't wrap across the edge
uf = uf < 0 ? 0 : uf;
vf = vf < 0 ? 0 : vf;
// x/y are texel coordinates, rx/ry are subpixel offsets
int x = uf >> 8;
int y = vf >> 8;
int rx = uf & 255;
int ry = vf & 255;
// 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 in integer space: result is 8.16 fixed point
int ax0 = a00 * 256 + (a10 - a00) * rx;
int ax1 = a01 * 256 + (a11 - a01) * rx;
int axy = ax0 * 256 + (ax1 - ax0) * ry;
return float(axy) * (1.f / (255.f * 65536.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<const unsigned char*>(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 unsigned char* 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 <typename T, typename Hash>
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 int rasterizeEdge(float u0, float v0, float u1, float v1, int edgeres, const Texture& texture)
{
float edgestep = 1.f / float(edgeres + 1);
float ud = (u1 - u0) * edgestep, vd = (v1 - v0) * edgestep;
float u = u0, v = v0;
int mask = 0;
int count = 0;
for (int i = 0; i < edgeres; ++i)
{
u += ud;
v += vd;
float a = sampleTexture(texture, u, v);
mask |= (a >= 0.5f) << i;
count += a >= 0.5f;
}
return mask | (count << 16);
}
template <int States>
static void rasterizeOpacity0(unsigned char* result, size_t index, float a0, float a1, float a2, float ac, int e0, int e1, int e2, int edgeres)
{
int states = States;
// basic coverage estimator from center and corner values; trained to minimize error
float coverage = (a0 + a1 + a2) * 0.12f + ac * 0.64f;
if (edgeres)
{
float edgescale = 1.f / edgeres;
// if we have edge samples, we can get a better coverage estimate by including them; trained to minimize error
coverage = ac * 0.22f + float((e0 >> 16) + (e1 >> 16) + (e2 >> 16)) * edgescale * 0.23f + (a0 + a1 + a2) * 0.03f;
}
if (states == 2)
{
result[index / 8] |= (coverage >= 0.5f) << (index % 8);
return;
}
int transp = (a0 < 0.5f) & (a1 < 0.5f) & (a2 < 0.5f) & (ac < 0.5f);
int opaque = (a0 > 0.5f) & (a1 > 0.5f) & (a2 > 0.5f) & (ac > 0.5f);
// 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 unknown = 2 + (coverage >= 0.5f);
int state = (transp | opaque) ? opaque : unknown;
if (edgeres && (transp | opaque))
{
// if we have edge samples, ensure they are consistent too, falling back to unknown if not
int exp = opaque ? (1 << edgeres) - 1 : 0;
int eok = ((e0 & 0xffff) == exp) & ((e1 & 0xffff) == exp) & ((e2 & 0xffff) == exp);
state = eok ? state : unknown;
}
result[index / 4] |= state << ((index % 4) * 2);
}
template <int States>
static void rasterizeOpacity1(unsigned char* result, size_t index, int edgeres, const float* c0, const float* c1, const float* c2, const Texture& texture)
{
// 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 c20[3] = {(c2[0] + c0[0]) / 2, (c2[1] + c0[1]) / 2, 0.f};
c01[2] = sampleTexture(texture, c01[0], c01[1]);
c12[2] = sampleTexture(texture, c12[0], c12[1]);
c20[2] = sampleTexture(texture, c20[0], c20[1]);
// corner tables for each edge, and corner + edge tables for each triangle
// edges are numbered counter clockwise, 6 outer first, 3 inner last; triangle vertex and edge references are in triangle winding order
static const unsigned char edges[9][2] = {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 0}, {5, 1}, {1, 3}, {3, 5}};
static const unsigned char triangles[4][6] = {{0, 1, 5, 0, 6, 5}, {5, 3, 1, 8, 7, 6}, {1, 2, 3, 1, 2, 7}, {3, 5, 4, 8, 4, 3}};
const float* points[] = {c0, c01, c1, c12, c2, c20};
int em[9] = {};
// sample additional points on the edges to improve state estimation
if (edgeres > 0)
for (size_t i = 0; i < 9; ++i)
em[i] = rasterizeEdge(points[edges[i][0]][0], points[edges[i][0]][1], points[edges[i][1]][0], points[edges[i][1]][1], edgeres, texture);
for (size_t i = 0; i < 4; ++i)
{
const unsigned char* tri = triangles[i];
const float* p0 = points[tri[0]];
const float* p1 = points[tri[1]];
const float* p2 = points[tri[2]];
// compute triangle center & sample
float uc = (p0[0] + p1[0] + p2[0]) * (1.f / 3.f);
float vc = (p0[1] + p1[1] + p2[1]) * (1.f / 3.f);
float ac = sampleTexture(texture, uc, vc);
// rasterize opacity state based on alpha values in corners and center (and optionally edges)
rasterizeOpacity0<States>(result, index * 4 + i, p0[2], p1[2], p2[2], ac, em[tri[3]], em[tri[4]], em[tri[5]], edgeres);
}
}
template <int States>
static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, int edgeres, 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]) * (1.f / 3.f);
float vc = (c0[1] + c1[1] + c2[1]) * (1.f / 3.f);
float ac = sampleTexture(texture, uc, vc);
int e0 = 0, e1 = 0, e2 = 0;
if (edgeres > 0)
{
// sample additional points on the edges to improve state estimation
e0 = rasterizeEdge(c0[0], c0[1], c1[0], c1[1], edgeres, texture);
e1 = rasterizeEdge(c1[0], c1[1], c2[0], c2[1], edgeres, texture);
e2 = rasterizeEdge(c2[0], c2[1], c0[0], c0[1], edgeres, texture);
}
// rasterize opacity state based on alpha values in corners and center (and optionally edges)
return rasterizeOpacity0<States>(result, index, c0[2], c1[2], c2[2], ac, e0, e1, e2, edgeres);
}
// fast path: equivalent to recursive rasterization, but reuses edge data to reduce sample count
if (level == 1 && edgeres > 0)
return rasterizeOpacity1<States>(result, index, edgeres, c0, c1, c2, texture);
// 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 c20[3] = {(c2[0] + c0[0]) / 2, (c2[1] + c0[1]) / 2, 0.f};
c01[2] = sampleTexture(texture, c01[0], c01[1]);
c12[2] = sampleTexture(texture, c12[0], c12[1]);
c20[2] = sampleTexture(texture, c20[0], c20[1]);
// recursively rasterize each triangle
// note: triangles 1 and 3 have flipped winding, and 1 is flipped upside down
rasterizeOpacityRec<States>(result, index * 4 + 0, level - 1, edgeres, c0, c01, c20, texture);
rasterizeOpacityRec<States>(result, index * 4 + 1, level - 1, edgeres, c20, c12, c01, texture);
rasterizeOpacityRec<States>(result, index * 4 + 2, level - 1, edgeres, c01, c1, c12, texture);
rasterizeOpacityRec<States>(result, index * 4 + 3, level - 1, edgeres, c12, c20, 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(unsigned char* 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<unsigned int>(table_size);
memset(table, -1, table_size * sizeof(unsigned int));
TriangleOMM* triangles = allocator.allocate<TriangleOMM>(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] = (unsigned char)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);
}
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, float(int(texture_width)) * 256.f, float(int(texture_height)) * 256.f};
// determine number of edge samples for conservative state estimation
float texture_area = float(int(texture_width)) * float(int(texture_height));
float uvarea = fabsf((uv1[0] - uv0[0]) * (uv2[1] - uv0[1]) - (uv2[0] - uv0[0]) * (uv1[1] - uv0[1])) * 0.5f * texture_area;
float uvedge = sqrtf(uvarea) / float(1 << level);
// target ~2px distance between edge samples (assuming equilateral microtriangles)
int edgeres = int(uvedge * 0.75f);
edgeres = edgeres < 0 ? 0 : edgeres;
edgeres = edgeres > 7 ? 7 : edgeres;
// 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, edgeres, c0, c1, c2, texture);
}
size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, unsigned char* 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<unsigned char>(data_size);
memcpy(data_old, data, data_size);
size_t table_size = hashBuckets3(omm_count);
unsigned int* table = allocator.allocate<unsigned int>(table_size);
memset(table, -1, table_size * sizeof(unsigned int));
OMMHasher hasher = {data, offsets, levels, states};
int* remap = allocator.allocate<int>(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] = (unsigned char)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(omm_indices[i] < 0 || unsigned(omm_indices[i]) < omm_count);
omm_indices[i] = omm_indices[i] < 0 ? omm_indices[i] : remap[omm_indices[i]];
}
return next;
}
+8 -5
View File
@@ -1081,18 +1081,22 @@ static void fillFaceQuadrics(Quadric* vertex_quadrics, QuadricGrad* volume_gradi
}
}
static void fillVertexQuadrics(Quadric* vertex_quadrics, const Vector3* vertex_positions, size_t vertex_count, const unsigned int* remap, unsigned int options)
static void fillVertexQuadrics(Quadric* vertex_quadrics, const Vector3* vertex_positions, size_t vertex_count, const unsigned int* remap, const unsigned char* vertex_lock, const unsigned int* sparse_remap, unsigned int options)
{
// by default, we use a very small weight to improve triangulation and numerical stability without affecting the shape or error
float factor = (options & meshopt_SimplifyRegularize) ? 1e-1f : 1e-7f;
float factor = (options & meshopt_SimplifyRegularizeLight) ? 1e-2f : ((options & meshopt_SimplifyRegularize) ? 1e-1f : 1e-7f);
for (size_t i = 0; i < vertex_count; ++i)
{
if (remap[i] != i)
continue;
// increase regularization weight for vertices marked as priority; for now we only examine the primary vertex
unsigned int ri = sparse_remap ? sparse_remap[i] : unsigned(i);
bool priority = vertex_lock && (vertex_lock[ri] & meshopt_SimplifyVertex_Priority) != 0;
const Vector3& p = vertex_positions[i];
float w = vertex_quadrics[i].w * factor;
float w = vertex_quadrics[i].w * (priority ? 1.0f : factor);
Quadric Q;
quadricFromPoint(Q, p.x, p.y, p.z, w);
@@ -2346,7 +2350,6 @@ size_t meshopt_simplifyEdge(unsigned int* destination, const unsigned int* indic
assert(vertex_positions_stride % sizeof(float) == 0);
assert(target_index_count <= index_count);
assert(target_error >= 0);
assert((options & ~(meshopt_SimplifyLockBorder | meshopt_SimplifySparse | meshopt_SimplifyErrorAbsolute | meshopt_SimplifyPrune | meshopt_SimplifyRegularize | meshopt_SimplifyPermissive | meshopt_SimplifyInternalSolve | meshopt_SimplifyInternalDebug)) == 0);
assert(vertex_attributes_stride >= attribute_count * sizeof(float) && vertex_attributes_stride <= 256);
assert(vertex_attributes_stride % sizeof(float) == 0);
assert(attribute_count <= kMaxAttributes);
@@ -2440,7 +2443,7 @@ size_t meshopt_simplifyEdge(unsigned int* destination, const unsigned int* indic
}
fillFaceQuadrics(vertex_quadrics, volume_gradients, result, index_count, vertex_positions, remap);
fillVertexQuadrics(vertex_quadrics, vertex_positions, vertex_count, remap, options);
fillVertexQuadrics(vertex_quadrics, vertex_positions, vertex_count, remap, vertex_lock, sparse_remap, options);
fillEdgeQuadrics(vertex_quadrics, result, index_count, vertex_positions, remap, vertex_kind, loop, loopback);
if (attribute_count)
+11 -11
View File
@@ -19,7 +19,7 @@
#endif
// MSVC supports compiling SSSE3 code regardless of compile options; we use a cpuid-based scalar fallback
#if !defined(SIMD_SSE) && !defined(SIMD_AVX) && defined(_MSC_VER) && !defined(__clang__) && (defined(_M_IX86) || defined(_M_X64))
#if !defined(SIMD_SSE) && !defined(SIMD_AVX) && defined(_MSC_VER) && !defined(__clang__) && (defined(_M_IX86) || (defined(_M_X64) && !defined(_M_ARM64EC)))
#define SIMD_SSE
#define SIMD_FALLBACK
#endif
@@ -37,7 +37,7 @@
#endif
// On MSVC, we assume that ARM builds always target NEON-capable devices
#if !defined(SIMD_NEON) && defined(_MSC_VER) && (defined(_M_ARM) || defined(_M_ARM64))
#if !defined(SIMD_NEON) && defined(_MSC_VER) && (defined(_M_ARM) || defined(_M_ARM64) || defined(_M_ARM64EC))
#define SIMD_NEON
#endif
@@ -56,7 +56,7 @@
// When targeting AArch64/x64, optimize for latency to allow decoding of individual 16-byte groups to overlap
// We don't do this for 32-bit systems because we need 64-bit math for this and this will hurt in-order CPUs
#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64)
#if (defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC)) && !defined(MESHOPTIMIZER_VERTEXCODEC_SIMDNOLOPT)
#define SIMD_LATENCYOPT
#endif
@@ -816,6 +816,12 @@ inline __m128i decodeShuffleMask(unsigned char mask0, unsigned char mask1)
return _mm_unpacklo_epi64(sm0, sm1r);
}
#ifdef __GNUC__
typedef int __attribute__((aligned(1))) unaligned_int;
#else
typedef int unaligned_int;
#endif
SIMD_TARGET
inline const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsigned char* buffer, int hbits)
{
@@ -834,19 +840,13 @@ inline const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi
case 1:
case 6:
{
#ifdef __GNUC__
typedef int __attribute__((aligned(1))) unaligned_int;
#else
typedef int unaligned_int;
#endif
#ifdef SIMD_LATENCYOPT
unsigned int data32;
memcpy(&data32, data, 4);
data32 &= data32 >> 1;
// arrange bits such that low bits of nibbles of data64 contain all 2-bit elements of data32
unsigned long long data64 = ((unsigned long long)data32 << 30) | (data32 & 0x3fffffff);
unsigned long long data64 = ((unsigned long long)data32 << 30) | data32;
// adds all 1-bit nibbles together; the sum fits in 4 bits because datacnt=16 would have used mode 3
int datacnt = int(((data64 & 0x1111111111111111ull) * 0x1111111111111111ull) >> 60);
@@ -1060,7 +1060,7 @@ inline const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi
data32 &= data32 >> 1;
// arrange bits such that low bits of nibbles of data64 contain all 2-bit elements of data32
unsigned long long data64 = ((unsigned long long)data32 << 30) | (data32 & 0x3fffffff);
unsigned long long data64 = ((unsigned long long)data32 << 30) | data32;
// adds all 1-bit nibbles together; the sum fits in 4 bits because datacnt=16 would have used mode 3
int datacnt = int(((data64 & 0x1111111111111111ull) * 0x1111111111111111ull) >> 60);
+14 -15
View File
@@ -13,7 +13,7 @@
#endif
// MSVC supports compiling SSE2 code regardless of compile options; we assume all 32-bit CPUs support SSE2
#if !defined(SIMD_SSE) && defined(_MSC_VER) && !defined(__clang__) && (defined(_M_IX86) || defined(_M_X64))
#if !defined(SIMD_SSE) && defined(_MSC_VER) && !defined(__clang__) && (defined(_M_IX86) || (defined(_M_X64) && !defined(_M_ARM64EC)))
#define SIMD_SSE
#endif
@@ -23,7 +23,7 @@
#endif
// On MSVC, we assume that ARM builds always target NEON-capable devices
#if !defined(SIMD_NEON) && defined(_MSC_VER) && (defined(_M_ARM) || defined(_M_ARM64))
#if !defined(SIMD_NEON) && defined(_MSC_VER) && (defined(_M_ARM) || defined(_M_ARM64) || defined(_M_ARM64EC))
#define SIMD_NEON
#endif
@@ -222,6 +222,11 @@ static void dispatchSimd(void (*process)(T*, size_t), T* data, size_t count, siz
size_t count4 = count & ~size_t(3);
process(data, count4);
#ifdef MESHOPTIMIZER_VERTEXFILTER_SIMDNOTAIL
// optionally omit tail processing to improve code size, expecting the caller to pass aligned counts
assert(count4 == count);
(void)stride;
#else
if (count4 < count)
{
T tail[4 * 4] = {}; // max stride 4, max count 4
@@ -232,6 +237,7 @@ static void dispatchSimd(void (*process)(T*, size_t), T* data, size_t count, siz
process(tail, count - count4);
memcpy(data + count4 * stride, tail, tail_size);
}
#endif
}
inline uint64_t rotateleft64(uint64_t v, int x)
@@ -536,7 +542,7 @@ static void decodeFilterColorSimd16(unsigned short* data, size_t count)
}
#endif
#if defined(SIMD_NEON) && !defined(__aarch64__) && !defined(_M_ARM64)
#if defined(SIMD_NEON) && !defined(__aarch64__) && !(defined(_M_ARM64) || defined(_M_ARM64EC))
inline float32x4_t vsqrtq_f32(float32x4_t x)
{
float32x4_t r = vrsqrteq_f32(x);
@@ -640,7 +646,7 @@ static void decodeFilterOctSimd16(short* data, size_t count)
// compute normal length & scale
float32x4_t ll = vfmaq_f32(vfmaq_f32(vmulq_f32(x, x), y, y), z, z);
#if !defined(__aarch64__) && !defined(_M_ARM64)
#if !defined(__aarch64__) && !(defined(_M_ARM64) || defined(_M_ARM64EC))
float32x4_t rl = vrsqrteq_f32(ll);
rl = vmulq_f32(rl, vrsqrtsq_f32(vmulq_f32(rl, ll), rl)); // refine rsqrt estimate
float32x4_t s = vmulq_f32(vdupq_n_f32(32767.f), rl);
@@ -919,8 +925,7 @@ static void decodeFilterOctSimd8(signed char* data, size_t count)
static void decodeFilterOctSimd16(short* data, size_t count)
{
const v128_t sign = wasm_f32x4_splat(-0.f);
// TODO: volatile here works around LLVM mis-optimizing code; https://github.com/llvm/llvm-project/issues/149457
volatile v128_t zmask = wasm_i32x4_splat(0x7fff);
const v128_t zmask = wasm_i32x4_splat(0x7fff);
for (size_t i = 0; i < count; i += 4)
{
@@ -1069,9 +1074,6 @@ static void decodeFilterExpSimd(unsigned int* data, size_t count)
static void decodeFilterColorSimd8(unsigned char* data, size_t count)
{
// TODO: volatile here works around LLVM mis-optimizing code; https://github.com/llvm/llvm-project/issues/149457
volatile v128_t zero = wasm_i32x4_splat(0);
for (size_t i = 0; i < count; i += 4)
{
v128_t c4 = wasm_v128_load(&data[i * 4]);
@@ -1080,7 +1082,7 @@ static void decodeFilterColorSimd8(unsigned char* data, size_t count)
v128_t yf = wasm_v128_and(c4, wasm_i32x4_splat(0xff));
v128_t cof = wasm_i32x4_shr(wasm_i32x4_shl(c4, 16), 24);
v128_t cgf = wasm_i32x4_shr(wasm_i32x4_shl(c4, 8), 24);
v128_t af = wasm_v128_or(zero, wasm_u32x4_shr(c4, 24));
v128_t af = wasm_u32x4_shr(c4, 24);
// recover scale from alpha high bit
v128_t as = af;
@@ -1120,9 +1122,6 @@ static void decodeFilterColorSimd8(unsigned char* data, size_t count)
static void decodeFilterColorSimd16(unsigned short* data, size_t count)
{
// TODO: volatile here works around LLVM mis-optimizing code; https://github.com/llvm/llvm-project/issues/149457
volatile v128_t zero = wasm_i32x4_splat(0);
for (size_t i = 0; i < count; i += 4)
{
v128_t c4_0 = wasm_v128_load(&data[(i + 0) * 4]);
@@ -1136,7 +1135,7 @@ static void decodeFilterColorSimd16(unsigned short* data, size_t count)
v128_t yf = wasm_v128_and(c4_yco, wasm_i32x4_splat(0xffff));
v128_t cof = wasm_i32x4_shr(c4_yco, 16);
v128_t cgf = wasm_i32x4_shr(wasm_i32x4_shl(c4_cga, 16), 16);
v128_t af = wasm_v128_or(zero, wasm_u32x4_shr(c4_cga, 16));
v128_t af = wasm_u32x4_shr(c4_cga, 16);
// recover scale from alpha high bit
v128_t as = af;
@@ -1469,7 +1468,7 @@ void meshopt_encodeFilterColor(void* destination, size_t count, size_t stride, i
assert(unsigned((fy + fco - fcg) | (fy + fcg) | (fy - fco - fcg)) < (1u << bits));
// alpha: K-1-bit encoding with high bit set to 1
int fa = meshopt_quantizeUnorm(c[3], bits - 1) | (1 << (bits - 1));
int fa = (meshopt_quantizeUnorm(c[3], bits) >> 1) | (1 << (bits - 1));
if (stride == 4)
{
-3
View File
@@ -6,8 +6,6 @@
size_t meshopt_optimizeVertexFetchRemap(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count)
{
assert(index_count % 3 == 0);
memset(destination, -1, vertex_count * sizeof(unsigned int));
unsigned int next_vertex = 0;
@@ -30,7 +28,6 @@ size_t meshopt_optimizeVertexFetchRemap(unsigned int* destination, const unsigne
size_t meshopt_optimizeVertexFetch(void* destination, unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size)
{
assert(index_count % 3 == 0);
assert(vertex_size > 0 && vertex_size <= 256);
meshopt_Allocator allocator;