How to correctly encode & decode custom / additional / generic attributes (e.g., uv1) with DracoLoader + DracoExporter in threejs?

Hi everyone,

I am trying to establish a clean, long-term workflow for encoding and decoding geometries with custom attributes using the Three.js Draco pipeline. My goal is simple:

  • I export a GLB containing:
    position, normal, uv, uv1
  • I process it using Three.js DracoExporter
  • I load the resulting .drc file using DracoLoader
  • I expect to get back:
    position, normal, uv, uv1

Currently, DracoLoader only gives me position, normal, uv, and it silently drops uv1 or any additional attributes.
Because of this, I had to write a manual full-decoder using the Draco WASM module, iterating all attributes myself.

It works, but I do not want to rely on a custom decoder for long-term production, especially since I may soon need additional attributes such as:

  • uv1, uv2
  • tangents
  • vertex colors
  • skinIndex / skinWeight
  • custom per-vertex scalars

I need a scalable, supported solution.


Problem Summary

1. DracoExporter (Three.js) does not export arbitrary custom attributes by default.

It seems limited to only mesh standard attributes unless modified.

2. DracoLoader also does not automatically decode custom or generic attributes.

It maps:

  • POSITION → position
  • NORMAL → normal
  • TEX_COORD → uv (only the first one)
  • COLOR
  • TANGENT
  • …and ignores the rest (including uv1)

3. I currently solve it by decoding everything manually

I wrote a temporary solution that uses DracoDecoder directly to extract all attributes, including uv1, uv2, generic attributes, etc. Example:

const dracoModulePromise = initDracoDecoder();

async function loadDRCWithAllAttributes(url) {
    const module = await dracoModulePromise;
    const response = await fetch(Globals.env.absolutePath + '/' + url);
    const arrayBuffer = await response.arrayBuffer();

    const decoder = new module.Decoder();
    const buffer = new module.DecoderBuffer();
    buffer.Init(new Int8Array(arrayBuffer), arrayBuffer.byteLength);

    const geometryType = decoder.GetEncodedGeometryType(buffer);
    let dracoGeometry = geometryType === module.TRIANGULAR_MESH
        ? new module.Mesh()
        : new module.PointCloud();

    const status = geometryType === module.TRIANGULAR_MESH
        ? decoder.DecodeBufferToMesh(buffer, dracoGeometry)
        : decoder.DecodeBufferToPointCloud(buffer, dracoGeometry);

    if (!status.ok()) throw new Error('Draco decoding failed: ' + status.error_msg());

    const geometry = new THREE.BufferGeometry();
    const numAttributes = dracoGeometry.num_attributes();
    let uvIndex = 0;

    for (let i = 0; i < numAttributes; i++) {
        const attribute = decoder.GetAttribute(dracoGeometry, i);
        const attributeType = attribute.attribute_type();
        const numComponents = attribute.num_components();
        const numValues = dracoGeometry.num_points() * numComponents;

        const ptr = module._malloc(numValues * 4);
        decoder.GetAttributeDataArrayForAllPoints(
            dracoGeometry, attribute,
            module.DT_FLOAT32,
            numValues * 4,
            ptr
        );

        const array = new Float32Array(module.HEAPF32.buffer, ptr, numValues).slice();
        module._free(ptr);

        let attributeName;
        if (attributeType === module.POSITION) attributeName = 'position';
        else if (attributeType === module.NORMAL) attributeName = 'normal';
        else if (attributeType === module.COLOR) attributeName = 'color';
        else if (attributeType === module.TEX_COORD) {
            attributeName = uvIndex === 0 ? 'uv' : `uv${uvIndex}`;
            uvIndex++;
        }
        else if (attributeType === module.GENERIC) {
            if (numComponents === 2 && !geometry.attributes.uv1) attributeName = 'uv1';
            else attributeName = `generic_${i}`;
        }
        geometry.setAttribute(attributeName, new THREE.BufferAttribute(array, numComponents));
    }

    // Add indices
    if (geometryType === module.TRIANGULAR_MESH && dracoGeometry.num_faces() > 0) {
        const numIndices = dracoGeometry.num_faces() * 3;
        const ptr = module._malloc(numIndices * 4);
        decoder.GetTrianglesUInt32Array(dracoGeometry, numIndices * 4, ptr);
        const indices = new Uint32Array(module.HEAPU32.buffer, ptr, numIndices).slice();
        module._free(ptr);
        geometry.setIndex(new THREE.BufferAttribute(indices, 1));
    }

    module.destroy(dracoGeometry);
    module.destroy(buffer);
    return geometry;
}

This gives me all attributes (including uv1), but I don’t want to rely on maintaining my own Draco decoding layer.


What I need help with

1. What is the correct way to encode more than POSITION/NORMAL/UV using DracoExporter?

I modified DracoExporter to inject uv1 as a generic attribute, but this feels unsafe and unofficial.

Is there an official recommended workflow for:

  • additional UV sets (uv1, uv2, …)
  • tangents
  • skinIndex / skinWeight
  • any custom BufferAttribute

2. How do I decode these attributes using DracoLoader?

DracoLoader currently ignores generic attributes.

Is there:

  • a way to register custom attribute mappings?
  • an option to decode “all attributes” automatically?
  • a recommended extension pattern?

3. Metadata inside the .drc file

Inside some .drc files I see a metadata block like:

DRACO .....
info>{"type":0,"attributes":[["position",7],["normal",7],["uv",7]]}

Is there a supported way to:

  • embed attribute names
  • embed semantic type information
  • ensure the loader restores them automatically

4. Performance & production considerations

My custom decoder works, but I worry about:

  • performance cost of manual attribute copying
  • extra WASM memory allocation
  • garbage generation
  • maintaining this manually long-term

I would prefer a supported, stable solution.


Ideal workflow I am aiming for

I want to encode and decode like this:

Encoding:

Three.js DracoExporter
→ outputs .drc
→ includes position, normal, uv, uv1 (and possibly more)

Decoding:

const geom = await dracoLoader.loadAsync('floor.drc');

geom.attributes.position  // OK
geom.attributes.normal    // OK
geom.attributes.uv        // OK
geom.attributes.uv1       // EXPECTED

But right now, DracoLoader drops uv1 entirely.


Summary

I am looking for guidance on the correct, supported way to:

  1. Export custom attributes with DracoExporter
  2. Encode attribute metadata properly
  3. Decode all attributes automatically using DracoLoader
  4. Support future attributes (skin weights, tangents, custom data)

Any advice, examples, code references, or recommended direction would be greatly appreciated.
Using .drc files is a mandatory requirement for my project, so I must solve this properly.

Thank you in advance.