Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simplify: Improve attribute metric computation #737

Merged
merged 8 commits into from
Aug 15, 2024
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,18 +255,17 @@ All algorithms presented so far don't affect visual appearance at all, with the

This library provides two simplification algorithms that reduce the number of triangles in the mesh. Given a vertex and an index buffer, they generate a second index buffer that uses existing vertices in the vertex buffer. This index buffer can be used directly for rendering with the original vertex buffer (preferably after vertex cache optimization), or a new compact vertex/index buffer can be generated using `meshopt_optimizeVertexFetch` that uses the optimal number and order of vertices.

The first simplification algorithm, `meshopt_simplify`, follows the topology of the original mesh in an attempt to preserve attribute seams, borders and overall appearance. For meshes with inconsistent topology or many seams, such as faceted meshes, it can result in simplifier getting "stuck" and not being able to simplify the mesh fully. Therefore it's critical that identical vertices are "welded" together, that is, the input vertex buffer does not contain duplicates. Additionally, it may be possible to preprocess the index buffer (e.g. with `meshopt_generateShadowIndexBuffer`) to discard any vertex attributes that aren't critical and can be rebuilt later.
The first simplification algorithm, `meshopt_simplify`, follows the topology of the original mesh in an attempt to preserve attribute seams, borders and overall appearance. For meshes with inconsistent topology or many seams, such as faceted meshes, it can result in simplifier getting "stuck" and not being able to simplify the mesh fully. Therefore it's critical that identical vertices are "welded" together, that is, the input vertex buffer does not contain duplicates. Additionally, it may be possible to preprocess the index buffer (e.g. with `meshopt_generateShadowIndexBuffer`) to weld the vertices without taking into account vertex attributes that aren't critical and can be rebuilt later.

```c++
float threshold = 0.2f;
size_t target_index_count = size_t(index_count * threshold);
float target_error = 1e-2f;
unsigned int options = 0; // meshopt_SimplifyX flags, 0 is a safe default

std::vector<unsigned int> lod(index_count);
float lod_error = 0.f;
lod.resize(meshopt_simplify(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex),
target_index_count, target_error, options, &lod_error));
target_index_count, target_error, /* options= */ 0, &lod_error));
```

Target error is an approximate measure of the deviation from the original mesh using distance normalized to `[0..1]` range (e.g. `1e-2f` means that simplifier will try to maintain the error to be below 1% of the mesh extents). Note that the simplifier attempts to produce the requested number of indices at minimal error, but because of topological restrictions and error limit it is not guaranteed to reach the target index count and can stop earlier.
Expand All @@ -290,6 +289,35 @@ When a sequence of LOD meshes is generated that all use the original vertex buff

Both algorithms can also return the resulting normalized deviation that can be used to choose the correct level of detail based on screen size or solid angle; the error can be converted to world space by multiplying by the scaling factor returned by `meshopt_simplifyScale`.

## Advanced simplification

The main simplification algorithm, `meshopt_simplify`, exposes additional options and functions that can be used to control the simplification process in more detail.

For basic customization, a number of options can be passed via `options` bitmask that adjust the behavior of the simplifier:

- `meshopt_SimplifyLockBorder` restricts the simplifier from collapsing edges that are on the border of the mesh. This can be useful for simplifying mesh subsets independently, so that the LODs can be combined without introducing cracks.
- `meshopt_SimplifyErrorAbsolute` changes the error metric from relative to absolute both for the input error limit as well as for the resulting error. This can be used instead of `meshopt_simplifyScale`.
- `meshopt_SimplifySparse` improves simplification performance assuming input indices are a sparse subset of the mesh. This can be useful when simplifying small mesh subsets independently, and is intended to be used for meshlet simplification. For consistency, it is recommended to use absolute errors when sparse simplification is desired, as this flag changes the meaning of the relative errors.

While `meshopt_simplify` is aware of attribute discontinuities by default (and infers them through the supplied index buffer) and tries to preserve them, it can be useful to provide information about attribute values. This allows the simplifier to take attribute error into account which can improve shading (by using vertex normals), texture deformation (by using texture coordinates), and may be necessary to preserve vertex colors when textures are not used in the first place. This can be done by using a variant of the simplification function that takes attribute values and weight factors, `meshopt_simplifyWithAttributes`:

```c++
const float nrm_weight = 0.5f;
const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight};

std::vector<unsigned int> lod(index_count);
float lod_error = 0.f;
lod.resize(meshopt_simplifyWithAttributes(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex),
&vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL,
target_index_count, target_error, /* options= */ 0, &lod_error));
```

The attributes are passed as a separate buffer (in the example above it's a subset of the same vertex buffer) and should be stored as consecutive floats; attribute weights are used to control the importance of each attribute in the simplification process.

When using `meshopt_simplifyWithAttributes`, it is also possible to lock certain vertices by providing a `vertex_lock` array that contains a boolean value for each vertex in the mesh. This can be useful to preserve certain vertices, such as the boundary of the mesh, with more control than `meshopt_SimplifyLockBorder` option provides.

Simplification currently assumes that the input mesh is using the same material for all triangles. If the mesh uses multiple materials, it is possible to split the mesh into subsets based on the material and simplify each subset independently, using `meshopt_SimplifyLockBorder` or `vertex_lock` to preserve material boundaries; however, this limits the collapses and as a result may reduce the resulting quality. An alternative approach is to encode information about the material into the vertex buffer, ensuring that all three vertices referencing the same triangle have the same material ID; this may require duplicating vertices on the boundary between materials. After this, simplification can be performed as usual, and after simplification per-triangle material information can be computed from the vertex material IDs. There is no need to inform the simplifier of the value of the material ID: the implicit boundaries created by duplicating vertices with conflicting material IDs will be preserved automatically.

## Mesh shading

Modern GPUs are beginning to deviate from the traditional rasterization model. NVidia GPUs starting from Turing and AMD GPUs starting from RDNA2 provide a new programmable geometry pipeline that, instead of being built around index buffers and vertex shaders, is built around mesh shaders - a new shader type that allows to provide a batch of work to the rasterizer.
Expand Down
2 changes: 1 addition & 1 deletion demo/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ void simplifyAttr(const Mesh& mesh, float threshold = 0.2f)
float target_error = 1e-2f;
float result_error = 0;

const float nrm_weight = 0.01f;
const float nrm_weight = 0.5f;
const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight};

lod.indices.resize(mesh.indices.size()); // note: simplify needs space for index_count elements in the destination array, not target_index_count
Expand Down
10 changes: 5 additions & 5 deletions demo/simplify.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
lockBorder: false,
useAttributes: false,
errorThresholdLog10: 1,
normalWeight: 0.01,
colorWeight: 0.01,
normalWeight: 0.5,
colorWeight: 1.0,

loadFile: function () {
var input = document.createElement('input');
Expand Down Expand Up @@ -96,8 +96,8 @@
guiSimplify.add(settings, 'lockBorder').onChange(simplify);
guiSimplify.add(settings, 'errorThresholdLog10', 0, 3, 0.1).onChange(simplify);
guiSimplify.add(settings, 'useAttributes').onChange(simplify);
guiSimplify.add(settings, 'normalWeight', 0, 0.1, 0.001).onChange(simplify);
guiSimplify.add(settings, 'colorWeight', 0, 0.1, 0.001).onChange(simplify);
guiSimplify.add(settings, 'normalWeight', 0, 2, 0.01).onChange(simplify);
guiSimplify.add(settings, 'colorWeight', 0, 2, 0.01).onChange(simplify);

var guiLoad = gui.addFolder('Load');
guiLoad.add(settings, 'loadFile');
Expand Down Expand Up @@ -175,7 +175,7 @@

console.timeEnd('simplify');

console.log('simplified to', res[0].length / 3);
console.log('simplified to', res[0].length / 3, 'with error', res[1]);

geo.index.array = res[0];
geo.index.count = res[0].length;
Expand Down
2 changes: 1 addition & 1 deletion demo/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ static void simplifySparse()
};

float aw[] = {
0.2f};
0.5f};

unsigned char lock[9] = {
8, 1, 8,
Expand Down
1 change: 0 additions & 1 deletion src/meshoptimizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,6 @@ MESHOPTIMIZER_API size_t meshopt_simplify(unsigned int* destination, const unsig
* attribute_weights should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position. The recommended weight range is [1e-3..1e-1], assuming attribute data is in [0..1] range.
* 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
* TODO target_error/result_error currently use combined distance+attribute error; this may change in the future
*/
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifyWithAttributes(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, const unsigned char* vertex_lock, size_t target_index_count, float target_error, unsigned int options, float* result_error);

Expand Down
52 changes: 21 additions & 31 deletions src/simplifier.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ static void quadricAdd(QuadricGrad* G, const QuadricGrad* R, size_t attribute_co
}
}

static float quadricError(const Quadric& Q, const Vector3& v)
static float quadricEval(const Quadric& Q, const Vector3& v)
{
float rx = Q.b0;
float ry = Q.b1;
Expand All @@ -621,48 +621,32 @@ static float quadricError(const Quadric& Q, const Vector3& v)
r += ry * v.y;
r += rz * v.z;

return r;
}

static float quadricError(const Quadric& Q, const Vector3& v)
{
float r = quadricEval(Q, v);
float s = Q.w == 0.f ? 0.f : 1.f / Q.w;

return fabsf(r) * s;
}

static float quadricError(const Quadric& Q, const QuadricGrad* G, size_t attribute_count, const Vector3& v, const float* va)
{
float rx = Q.b0;
float ry = Q.b1;
float rz = Q.b2;

rx += Q.a10 * v.y;
ry += Q.a21 * v.z;
rz += Q.a20 * v.x;

rx *= 2;
ry *= 2;
rz *= 2;

rx += Q.a00 * v.x;
ry += Q.a11 * v.y;
rz += Q.a22 * v.z;

float r = Q.c;
r += rx * v.x;
r += ry * v.y;
r += rz * v.z;
float r = quadricEval(Q, v);

// see quadricFromAttributes for general derivation; here we need to add the parts of (eval(pos) - attr)^2 that depend on attr
for (size_t k = 0; k < attribute_count; ++k)
{
float a = va[k];
float g = v.x * G[k].gx + v.y * G[k].gy + v.z * G[k].gz + G[k].gw;

r += a * a * Q.w;
r -= 2 * a * g;
r += a * (a * Q.w - 2 * g);
}

// TODO: weight normalization is breaking attribute error somehow
float s = 1; // Q.w == 0.f ? 0.f : 1.f / Q.w;

return fabsf(r) * s;
// note: unlike position error, we do not normalize by Q.w to retain edge scaling as described in quadricFromAttributes
return fabsf(r);
}

static void quadricFromPlane(Quadric& Q, float a, float b, float c, float d, float w)
Expand Down Expand Up @@ -729,16 +713,21 @@ static void quadricFromAttributes(Quadric& Q, QuadricGrad* G, const Vector3& p0,
Vector3 p10 = {p1.x - p0.x, p1.y - p0.y, p1.z - p0.z};
Vector3 p20 = {p2.x - p0.x, p2.y - p0.y, p2.z - p0.z};

// weight is scaled linearly with edge length
// normal = cross(p1 - p0, p2 - p0)
Vector3 normal = {p10.y * p20.z - p10.z * p20.y, p10.z * p20.x - p10.x * p20.z, p10.x * p20.y - p10.y * p20.x};
float area = sqrtf(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
float w = sqrtf(area); // TODO this needs more experimentation
float area = sqrtf(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z) * 0.5f;

// quadric is weighted with the square of edge length (= area)
// this equalizes the units with the positional error (which, after normalization, is a square of distance)
// as a result, a change in weighted attribute of 1 along distance d is approximately equivalent to a change in position of d
float w = area;

// we compute gradients using barycentric coordinates; barycentric coordinates can be computed as follows:
// v = (d11 * d20 - d01 * d21) / denom
// w = (d00 * d21 - d01 * d20) / denom
// u = 1 - v - w
// here v0, v1 are triangle edge vectors, v2 is a vector from point to triangle corner, and dij = dot(vi, vj)
// note: v2 and d20/d21 can not be evaluated here as v2 is effectively an unknown variable; we need these only as variables for derivation of gradients
const Vector3& v0 = p10;
const Vector3& v1 = p20;
float d00 = v0.x * v0.x + v0.y * v0.y + v0.z * v0.z;
Expand All @@ -748,7 +737,7 @@ static void quadricFromAttributes(Quadric& Q, QuadricGrad* G, const Vector3& p0,
float denomr = denom == 0 ? 0.f : 1.f / denom;

// precompute gradient factors
// these are derived by directly computing derivative of eval(pos) = a0 * u + a1 * v + a2 * w and factoring out common factors that are shared between attributes
// these are derived by directly computing derivative of eval(pos) = a0 * u + a1 * v + a2 * w and factoring out expressions that are shared between attributes
float gx1 = (d11 * v0.x - d01 * v1.x) * denomr;
float gx2 = (d00 * v1.x - d01 * v0.x) * denomr;
float gy1 = (d11 * v0.y - d01 * v1.y) * denomr;
Expand All @@ -773,6 +762,7 @@ static void quadricFromAttributes(Quadric& Q, QuadricGrad* G, const Vector3& p0,

// quadric encodes (eval(pos)-attr)^2; this means that the resulting expansion needs to compute, for example, pos.x * pos.y * K
// since quadrics already encode factors for pos.x * pos.y, we can accumulate almost everything in basic quadric fields
// note: for simplicity we scale all factors by weight here instead of outside the loop
Q.a00 += w * (gx * gx);
Q.a11 += w * (gy * gy);
Q.a22 += w * (gz * gz);
Expand Down