How to Use meshopt_generateVertexRemap with Custom Floating-Point Tolerance

Use meshopt_generateVertexRemapCustom instead of the standard variant, providing a callback function that compares vertex Euclidean distance against your tolerance epsilon, and pass the tolerance value through the context parameter.

The meshoptimizer library provides vertex deduplication through meshopt_generateVertexRemap, but this standard function requires binary-identical positions. When you need to merge vertices that are separated by small floating-point distances—such as welding duplicate vertices from disparate mesh sources or accounting for quantization errors—you must use meshopt_generateVertexRemapCustom with a custom equality callback.

Understanding meshopt_generateVertexRemapCustom

The custom variant, declared in src/meshoptimizer.h#L88, extends the standard remap logic by allowing you to supply a C function pointer that determines vertex equivalence.

size_t meshopt_generateVertexRemapCustom(
    unsigned int* destination,
    const unsigned int* indices,
    size_t index_count,
    const float* vertex_positions,
    size_t vertex_count,
    size_t vertex_positions_stride,
    int (*callback)(void*, unsigned int, unsigned int),
    void* context);

Key parameters:

  • destination – Pre-allocated array of size vertex_count to receive the remap table.
  • vertex_positions – Pointer to packed float3 position data (XYZ tightly packed or interleaved).
  • vertex_positions_stride – Byte offset between consecutive vertices (must be ≥ 12 and a multiple of 4).
  • callback – Your custom function returning 1 if vertices should merge, 0 if distinct.
  • context – Opaque pointer passed unchanged to every callback invocation (ideal for tolerance values).

According to the implementation in src/indexgenerator.cpp#L422, the algorithm first groups vertices by binary-identical positions to build candidate pairs, then invokes your callback to determine final equivalence. This mechanism lets you override the strict equality check with floating-point tolerance logic.

Implementing a Floating-Point Tolerance Callback

The callback receives two vertex indices and your context pointer. To implement tolerance-based welding:

  1. Cast the context pointer to access your epsilon value and vertex buffer pointer.
  2. Calculate the memory offset for each vertex using vertex_positions_stride.
  3. Compute the squared Euclidean distance between the two positions.
  4. Return 1 if distance² ≤ epsilon², otherwise return 0.

Avoid globals by packing your tolerance, stride, and position pointer into a struct passed as context:

struct WeldContext {
    float epsilon;           // Tolerance value (e.g., 0.001f)
    size_t strideBytes;      // Vertex stride in bytes
    const float* positions;  // Base pointer to position array
};

Position Fetch Helper

Since vertex_positions_stride may include additional attributes (normals, UVs), calculate positions manually:

static inline const float* getPosition(const float* base, size_t stride, unsigned int idx) {
    const unsigned char* ptr = reinterpret_cast<const unsigned char*>(base) + idx * stride;
    return reinterpret_cast<const float*>(ptr);
}

Complete Working Example

Below is a self-contained C++ example that welds vertices closer than 0.001f units. This pattern appears in the test suite at demo/tests.cpp#L1668.

#include <meshoptimizer.h>
#include <cmath>
#include <cstdio>
#include <vector>

struct CallbackContext {
    float epsilon;
    size_t strideBytes;
    const float* positions;
};

static inline const float* getPosition(const float* base, size_t stride, unsigned int idx) {
    const unsigned char* ptr = reinterpret_cast<const unsigned char*>(base) + idx * stride;
    return reinterpret_cast<const float*>(ptr);
}

// Callback: returns 1 if distance <= epsilon
static int toleranceCallback(void* rawCtx, unsigned int a, unsigned int b) {
    const CallbackContext* ctx = static_cast<const CallbackContext*>(rawCtx);
    const float* pa = getPosition(ctx->positions, ctx->strideBytes, a);
    const float* pb = getPosition(ctx->positions, ctx->strideBytes, b);
    
    float dx = pa[0] - pb[0];
    float dy = pa[1] - pb[1];
    float dz = pa[2] - pb[2];
    float dist2 = dx*dx + dy*dy + dz*dz;
    
    return dist2 <= ctx->epsilon * ctx->epsilon ? 1 : 0;
}

int main() {
    // Input: 6 vertices where v4 duplicates v1 and v5 duplicates v3
    const float positions[] = {
        0.0f, 0.0f, 0.0f,   // v0
        1.0f, 0.0f, 0.0f,   // v1
        0.0f, 1.0f, 0.0f,   // v2
        0.0f, 0.0f, 1.0f,   // v3
        1.0f, 0.0f, 0.0f,   // v4 (duplicate of v1)
        0.0f, 0.0f, 1.0f    // v5 (duplicate of v3)
    };
    const size_t vertexCount = 6;
    const size_t stride = sizeof(float) * 3;
    
    const unsigned int indices[] = {0,1,2, 0,3,4, 2,5,0};
    const size_t indexCount = 9;

    // Generate remap table with 0.001f tolerance
    std::vector<unsigned int> remap(vertexCount);
    CallbackContext ctx = { 0.001f, stride, positions };
    
    size_t uniqueCount = meshopt_generateVertexRemapCustom(
        remap.data(),
        indices,
        indexCount,
        positions,
        vertexCount,
        stride,
        toleranceCallback,
        &ctx);

    printf("Unique vertices: %zu\n", uniqueCount);
    
    // Compact vertex buffer
    std::vector<float> compacted(uniqueCount * 3);
    meshopt_remapVertexBuffer(compacted.data(), positions, vertexCount, stride, remap.data());
    
    // Compact index buffer
    std::vector<unsigned int> newIndices(indexCount);
    meshopt_remapIndexBuffer(newIndices.data(), indices, indexCount, remap.data());
    
    return 0;
}

Compacting Buffers After Remapping

The remap table produced by meshopt_generateVertexRemapCustom maps old vertex indices to new compacted indices. To apply it:

  • meshopt_remapVertexBuffer – Copies surviving unique vertices into a new compacted array.
  • meshopt_remapIndexBuffer – Rewrites triangle indices to point to the new vertex locations.

Pass the same remap array to both functions to ensure consistency between your geometry and topology.

Summary

  • meshopt_generateVertexRemap requires exact binary matches, while meshopt_generateVertexRemapCustom accepts a callback for fuzzy equality.
  • The callback receives vertex indices and a context pointer, enabling you to pass floating-point tolerances without global state.
  • First group by binary-identical positions (handled internally), then use your callback to Weld vertices within the tolerance distance.
  • Always follow remap generation with meshopt_remapVertexBuffer and meshopt_remapIndexBuffer to produce render-ready compacted buffers.
  • Reference implementation details in src/indexgenerator.cpp and usage examples in demo/tests.cpp.

Frequently Asked Questions

What is the difference between meshopt_generateVertexRemap and meshopt_generateVertexRemapCustom?

meshopt_generateVertexRemap deduplicates only vertices with byte-identical position values, suitable for indexed meshes with exact duplicates. meshopt_generateVertexRemapCustom adds a callback parameter that lets you define custom equivalence—such as floating-point tolerance or attribute-aware matching—giving you precise control over the welding process.

Can I use meshopt_generateVertexRemap with a tolerance parameter directly?

No. The standard function has no tolerance parameter. You must use meshopt_generateVertexRemapCustom and supply your own callback function that implements the distance check, as shown in the example above.

How do I pass the tolerance value to the callback without global variables?

Pack the tolerance into a struct along with your vertex buffer pointer and stride, then pass a pointer to that struct as the context argument. The library forwards this pointer to every callback invocation, allowing thread-safe, reentrant code with multiple different tolerances.

What value should I use for vertex_positions_stride?

Set it to the byte offset between consecutive vertex positions. For tightly packed float3 data, use sizeof(float) * 3 (12 bytes). For interleaved vertex formats containing additional attributes (normals, UVs), use the full struct size (e.g., sizeof(Vertex)), ensuring the value is at least 12 and divisible by 4.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →