GLTFFileLoader support for EXT_mesh_features extension

This glTF extension is a multi-vendor extension. This extension can be implemented using the glTF loader extension mechanism. I wouldn’t be opposed to adding this to the list of extensions we support if someone is willing to contribute the code.|

Ported from GitHub

Hello Thomas,

I’m currently working on adding support for GLTF 2.0 extensions to the Babylon.js loader, specifically EXT_mesh_features, EXT_instance_features, and the related EXT_structural_metadata.

Since I’m already familiar with the Babylon.js loader (having contributed to some standard and vendor extensions that are now in production), I’d like to ask the Babylon.js team about the preferred architecture for integrating these new extensions.

Currently, these are vendor-specific extensions. While they are being used in some standards like OGC 3D Tiles, there’s still an open question about whether they should be embedded directly into the core GLTF loader. Another key consideration is how to expose these new features to the user, given that they primarily provide IDs whose meaning is application-specific.

I have my own ideas on this, but if these extensions could eventually be submitted as a PR to the Babylon.js engine, I want to ensure they align with any architectural guidelines or constraints the team may have.

Here’s the base implementation I have so far:

import { Geometry, Mesh, Nullable } from "@babylonjs/core";
import { IGLTFLoaderExtension } from "@babylonjs/loaders";
import { GLTFLoader, IMeshPrimitive, IProperty, ITextureInfo } from "@babylonjs/loaders/glTF/2.0";

const NAME = "EXT_mesh_features";

export type FeatureIdAttribute = number; // An integer value used to construct a string in the format `_FEATURE_ID_<set index>` which is a reference to a key in `mesh.primitives.attributes` (e.g. a value of `0` corresponds to `_FEATURE_ID_0`).

// A texture containing feature IDs
export interface IFeatureIdTexture extends ITextureInfo {
    channels: Array<number>; // Texture channels containing feature IDs, identified by index. Feature IDs may be packed into multiple channels if a single channel does not have sufficient bit depth to represent all feature ID values. The values are packed in little-endian order.
}

export interface IFeatureId extends IProperty {
    featureCount: number; // The number of unique features in the attribute or texture.
    nullFeatureId?: number; // A value that indicates that no feature is associated with this vertex or texel.
    label?: string; // A label assigned to this feature ID set. Labels must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`.
    attribute?: FeatureIdAttribute; // An attribute containing feature IDs. When `attribute` and `texture` are omitted the feature IDs are assigned to vertices by their index.
    texture?: IFeatureIdTexture; // A texture containing feature IDs.
    propertyTable?: number; // the index of the property table containing per-feature property values. Only applicable when using the `EXT_structural_metadata` extension.
}

/**
 * [Specification](https://github.com/CesiumGS/glTF/blob/3d-tiles-next/extensions/2.0/Vendor/EXT_mesh_features/README.md)
 * [Playground Sample]()
 */
// eslint-disable-next-line @typescript-eslint/naming-convention
export class EXT_mesh_features implements IGLTFLoaderExtension {
    /**
     * The name of this extension.
     */
    public readonly name = NAME;
    /**
     * Defines whether this extension is enabled.
     */
    public enabled: boolean;

    private _loader: GLTFLoader;

    /**
     * @internal
     */
    constructor(loader: GLTFLoader) {
        this._loader = loader;
        this.enabled = this._loader.isExtensionUsed(NAME);
    }

    public dispose(): void {
        (this._loader as any) = null;
    }

    public _loadVertexDataAsync?(context: string, primitive: IMeshPrimitive, babylonMesh: Mesh): Nullable<Promise<Geometry>> {
        ...
    }
}

GLTFLoader.RegisterExtension(NAME, (loader) => new EXT_mesh_features(loader));

with the following augmentation

declare module "@babylonjs/loaders/glTF/2.0" {
    interface IProperty extends GLTF2.IProperty {}
}

I’d appreciate any thoughts or guidance on how best to proceed. Thanks!

1 Like

Hi @Guillaume_Pelletier ! Adding @bghgary and @ryantrem who have been working on the loader and might have some recommendations.

1 Like

Hi @Guillaume_Pelletier! Good to hear from you. Hope everything is well.

I think it’s okay to put into core. Thanks to @ryantrem, we can now dynamically load extensions only when they are used in the glTF, so it shouldn’t be a problem.

For the mesh attributes, we can just add them to the geometry. The other metadata for different properties can follow what the ExtrasAsMetadata loader extension is doing.

2 Likes

Thanks for the input! And yes, I’m happy to hear from you guys. I missed you.

Given the example, I may easily make it work using the following code:

/**
 * @internal
 */
public _loadVertexDataAsync(
    context: string,
    primitive: IMeshPrimitive,
    babylonMesh: Mesh
): Nullable<Promise<Geometry>> {
    this._assignFeatureIds(babylonMesh, primitive.extensions?.EXT_mesh_features);
    return null;
}

private _assignFeatureIds(babylonObject: any, gltfProp: IProperty): void {
    if (HasFeatureIds(gltfProp)) {
        babylonObject.featureIds = babylonObject.featureIds ?? [];
        for (const i of gltfProp.featureIds) {
            babylonObject.featureIds.push(i);
        }
    }
}

However, the unknown attributes are not loaded. Even when returning null to allow the default behavior, the list of attributes is already defined.

In the context of the following glTF :

 [
  {
    "primitives": [
      {
        "extensions": {
          "EXT_mesh_features": {
            "featureIds": [
              {
                "featureCount": 4,
                "attribute": 0
              }
            ]
          }
        },
        "attributes": {
          "POSITION": 1,
          "NORMAL": 2,
          "_FEATURE_ID_0": 3
        },
        "indices": 0,
        "material": 0,
        "mode": 4
      }
    ]
  }
]

The _FEATURE_ID_0 attribute is not loaded.

Moreover, when dealing with extensions for textures ids in a glTF model like:

[
  {
    "primitives": [
      {
        "extensions": {
          "EXT_mesh_features": {
            "featureIds": [
              {
                "featureCount": 4,
                "texture": {
                  "index": 1,
                  "texCoord": 0,
                  "channels": [0]
                }
              }
            ]
          }
        },
        "attributes": {
          "POSITION": 1,
          "NORMAL": 2,
          "TEXCOORD_0": 3
        },
        "indices": 0,
        "material": 0,
        "mode": 4
      }
    ]
  }
]

You can spot a TEXCOORD_0 attribute, which collides with the standard GLTF and Babylon.js TEXCOORD_0 kind in this line

So what would be the best way to handle the loading of attributes without needing to rewrite the entire function? work arround with the TEXCOORD_xxxx duplicate ? I believe I might have to duplicate the entire function, but you may have a better idea, because many private or internal types are involved. …:rocket:

**** EDIT ****
In the meantime, I duplicated the _loadVertexDataAsync function with a proper type workaround, and that did the job. However, I haven’t handled the TEXCOORD_xxx duplication yet.
**** EDIT ****
done the duplicate… will test and post the code tomorrow… late in Paris now…

You can duplicate the core function for testing, but ideally, we should modify this function to support what we need for this extension. I’m not sure what that should look like and will have to think about it.

I’m not sure I get the full picture of the duplication issue. What happens if there are UVs and texture feature ids? Is that allowed?

I will try another, simpler approach. Regarding the duplicate TexCoord, it was a misunderstanding on my part.
The following version successfully loads GLTF samples used by Cesium.
Now, I need to experiment with it a bit to ensure that we expose all useful information correctly.

import * as GLTF2 from "babylonjs-gltf2interface";
import { BaseTexture, BoundingInfo, Geometry, Mesh, Nullable, TmpVectors, VertexBuffer } from "@babylonjs/core";
import { IGLTFLoaderExtension } from "@babylonjs/loaders";
import { GLTFLoader, IProperty, ITextureInfo, IMeshPrimitive, ArrayItem, IAccessor } from "@babylonjs/loaders/glTF/2.0";

const NAME = "EXT_mesh_features";

export type FeatureIdAttribute = number; // An integer value used to construct a string in the format `_FEATURE_ID_<set index>` which is a reference to a key in `mesh.primitives.attributes` (e.g. a value of `0` corresponds to `_FEATURE_ID_0`).

// A texture containing feature IDs
export interface IFeatureIdTexture extends ITextureInfo {
    channels: Array<number>; // Texture channels containing feature IDs, identified by index. Feature IDs may be packed into multiple channels if a single channel does not have sufficient bit depth to represent all feature ID values. The values are packed in little-endian order.
}

export interface IFeatureId extends IProperty {
    featureCount: number; // The number of unique features in the attribute or texture.
    nullFeatureId?: number; // A value that indicates that no feature is associated with this vertex or texel.
    attribute?: FeatureIdAttribute; // An attribute containing feature IDs. When `attribute` and `texture` are omitted the feature IDs are assigned to vertices by their index.
    texture?: IFeatureIdTexture; // A texture containing feature IDs.
    propertyTable?: number; // the index of the property table containing per-feature property values. Only applicable when using the `EXT_structural_metadata` extension.
    label?: string; // A label assigned to this feature ID set. Labels must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`.

    // specific to babylon
    vertexAttributeKind: string; // the kind of vertice attribute binded with this feature
    textureData?: BaseTexture; // the optional texture.
}

export interface IHasFeatureIds {
    featureIds: Array<IFeatureId>;
}

export function HasFeatureIds(b: unknown): b is IHasFeatureIds {
    if (b === null || b === undefined || typeof b !== "object") return false;
    const obj = b as Partial<IHasFeatureIds>;
    return obj.featureIds !== undefined && Array.isArray(obj.featureIds) && obj.featureIds.length != 0;
}

/**
 * [Specification](https://github.com/CesiumGS/glTF/blob/3d-tiles-next/extensions/2.0/Vendor/EXT_mesh_features/README.md)
 * [Playground Sample]()
 */
// eslint-disable-next-line @typescript-eslint/naming-convention
export class EXT_mesh_features implements IGLTFLoaderExtension {
    private static VerticeKindPrefix = "fid";
    private static uvKindPrefix = "uv";
    /**
     * The name of this extension.
     */
    public readonly name = NAME;
    /**
     * Defines whether this extension is enabled.
     */
    public enabled: boolean;

    private _loader: GLTFLoader;

    /**
     * @internal
     */
    constructor(loader: GLTFLoader) {
        this._loader = loader;
        this.enabled = this._loader.isExtensionUsed(NAME);
    }

    public dispose(): void {
        (this._loader as any) = null;
    }

    /**
     * @internal
     */
    public _loadVertexDataAsync(context: string, primitive: IMeshPrimitive, babylonMesh: Mesh): Nullable<Promise<Geometry>> {
        const gltfProp = primitive.extensions?.EXT_mesh_features;
        if (HasFeatureIds(gltfProp)) {
            const babylonGeometry = new Geometry(babylonMesh.name, (<any>this._loader)._babylonScene);

            //#region extension specific
            const babylonObject: any = babylonMesh; // this is the object where we decide to put the feature ids accessor.
            const featureIds: Array<IFeatureId> = (babylonObject.featureIds = babylonObject.featureIds ?? []);
            for (const i of gltfProp.featureIds) {
                featureIds.push(i);
            }
            //#endregion extension specific

            const attributes = primitive.attributes;
            if (!attributes) {
                throw new Error(`${context}: Attributes are missing`);
            }

            const promises = new Array<Promise<any>>();

            if (primitive.indices == undefined) {
                babylonMesh.isUnIndexed = true;
            } else {
                const accessor: any = ArrayItem.Get(`${context}/indices`, (<any>this._loader)._gltf.accessors, primitive.indices);
                promises.push(
                    this._loader._loadIndicesAccessorAsync(`/accessors/${accessor.index}`, accessor).then((data) => {
                        babylonGeometry.setIndices(data);
                    })
                );
            }

            const loadAttribute = (name: string, kind: string, callback?: (accessor: IAccessor) => void) => {
                if (attributes[name] == undefined) {
                    return;
                }

                babylonMesh._delayInfo = babylonMesh._delayInfo || [];
                if (babylonMesh._delayInfo.indexOf(kind) === -1) {
                    babylonMesh._delayInfo.push(kind);
                }

                const accessor: any = ArrayItem.Get(`${context}/attributes/${name}`, (<any>this._loader)._gltf.accessors, attributes[name]);
                promises.push(
                    this._loader._loadVertexAccessorAsync(`/accessors/${accessor.index}`, accessor, kind).then((babylonVertexBuffer) => {
                        if (babylonVertexBuffer.getKind() === VertexBuffer.PositionKind && !this._loader.parent.alwaysComputeBoundingBox && !babylonMesh.skeleton) {
                            if (accessor.min && accessor.max) {
                                const min = TmpVectors.Vector3[0].copyFromFloats(...(accessor.min as [number, number, number]));
                                const max = TmpVectors.Vector3[1].copyFromFloats(...(accessor.max as [number, number, number]));
                                if (accessor.normalized && accessor.componentType !== GLTF2.AccessorComponentType.FLOAT) {
                                    let divider = 1;
                                    switch (accessor.componentType) {
                                        case GLTF2.AccessorComponentType.BYTE:
                                            divider = 127.0;
                                            break;
                                        case GLTF2.AccessorComponentType.UNSIGNED_BYTE:
                                            divider = 255.0;
                                            break;
                                        case GLTF2.AccessorComponentType.SHORT:
                                            divider = 32767.0;
                                            break;
                                        case GLTF2.AccessorComponentType.UNSIGNED_SHORT:
                                            divider = 65535.0;
                                            break;
                                    }
                                    const oneOverDivider = 1 / divider;
                                    min.scaleInPlace(oneOverDivider);
                                    max.scaleInPlace(oneOverDivider);
                                }
                                babylonGeometry._boundingInfo = new BoundingInfo(min, max);
                                babylonGeometry.useBoundingInfoFromGeometry = true;
                            }
                        }
                        babylonGeometry.setVerticesBuffer(babylonVertexBuffer, accessor.count);
                    })
                );

                if (kind == VertexBuffer.MatricesIndicesExtraKind) {
                    babylonMesh.numBoneInfluencers = 8;
                }

                if (callback) {
                    callback(accessor);
                }
            };

            const attributeMappings: [string, string, Nullable<(accessor: IAccessor) => void>][] = [
                ["TEXCOORD_0", VertexBuffer.UVKind, null],
                ["TEXCOORD_1", VertexBuffer.UV2Kind, null],
                ["TEXCOORD_2", VertexBuffer.UV3Kind, null],
                ["TEXCOORD_3", VertexBuffer.UV4Kind, null],
                ["TEXCOORD_4", VertexBuffer.UV5Kind, null],
                ["TEXCOORD_5", VertexBuffer.UV6Kind, null],

                ["POSITION", VertexBuffer.PositionKind, null],
                ["NORMAL", VertexBuffer.NormalKind, null],
                ["TANGENT", VertexBuffer.TangentKind, null],

                ["JOINTS_0", VertexBuffer.MatricesIndicesKind, null],
                ["WEIGHTS_0", VertexBuffer.MatricesWeightsKind, null],
                ["JOINTS_1", VertexBuffer.MatricesIndicesExtraKind, null],
                ["WEIGHTS_1", VertexBuffer.MatricesWeightsExtraKind, null],
                [
                    "COLOR_0",
                    VertexBuffer.ColorKind,
                    (accessor) => {
                        if (accessor.type === GLTF2.AccessorType.VEC4) {
                            babylonMesh.hasVertexAlpha = true;
                        }
                    },
                ],
            ];

            //#region extension specific
            // this is where we load the features id..
            let vfidCount = 0;
            const implicit: Array<IFeatureId> = [];
            for (const fid of featureIds) {
                if (fid.attribute != undefined) {
                    // Feature ID by Vertex
                    const n = this._buildKind("_FEATURE_ID_", fid.attribute);
                    fid.vertexAttributeKind = this._buildKind(EXT_mesh_features.VerticeKindPrefix, fid.attribute);
                    attributeMappings.push([n, fid.vertexAttributeKind, null]);
                    vfidCount++;
                    continue;
                }
                if (fid.texture?.texCoord != undefined) {
                    fid.vertexAttributeKind = this._buildKind(EXT_mesh_features.uvKindPrefix, fid.texture?.texCoord);
                    this._loader.loadTextureInfoAsync(context, fid.texture).then((babylonTexture) => {
                        fid.textureData = babylonTexture;
                    });
                    continue;
                }
                // When both featureId.attribute and featureId.texture are undefined,
                // then the feature ID value for each vertex is given implicitly, via
                // the index of the vertex. In this case, the featureCount must match
                // the number of vertices of the mesh primitive.
                // push these into stack for later process (we need this to know the number of feature by vertex already declared)
                implicit.push(fid);
            }

            // loop over the implicit feature id, creating and set buffer.
            for (const fid of implicit) {
                fid.vertexAttributeKind = this._buildKind(EXT_mesh_features.VerticeKindPrefix, vfidCount++);
                const buffer = this._buildVertexBufferForImplicitId(fid.featureCount, fid.vertexAttributeKind);
                babylonGeometry.setVerticesBuffer(buffer, fid.featureCount);
            }
            //#end region extension specific

            attributeMappings.forEach(([attributeName, vertexKind, callback]) => {
                loadAttribute(attributeName, vertexKind, callback == null ? undefined : callback);
            });

            return Promise.all(promises).then(() => {
                return babylonGeometry;
            });
        }
        return null; // default behavior if no feature ids defined..
    }

    private _buildVertexBufferForImplicitId(count: number, kind: string): VertexBuffer {
        const generatedIndices = Array.from({ length: count }, (_, i) => i);
        const engine = (<any>this._loader)._babylonScene.getEngine();
        // TODO : optimise the size/type depending the count..
        return new VertexBuffer(engine, generatedIndices, kind, false, undefined, 4, false, 0, 1, GLTF2.AccessorComponentType.FLOAT);
    }

    private _buildKind(prefix: string, n: number): string {
        return `${prefix}${n}`;
    }
}

GLTFLoader.RegisterExtension(NAME, (loader) => new EXT_mesh_features(loader));

1 Like

another milestone with “EXT_instance_features”

import { IndicesArray, Nullable, TransformNode } from "@babylonjs/core";
import { IGLTFLoaderExtension } from "@babylonjs/loaders";
import { ArrayItem, EXT_mesh_gpu_instancing, GLTFLoader, IEXTInstanceFeatures, INode } from "@babylonjs/loaders/glTF/2.0";
import { EXT_mesh_features, IFeatureId, IHasFeatureIds } from "./EXT_mesh_features";

const NAME = "EXT_instance_features";

export interface IHasInstanceIds extends IHasFeatureIds {
    thinInstanceIds: Array<IndicesArray>;
}

/**
 * [Specification](https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_instance_features)
 * [Playground Sample]()
 */
// eslint-disable-next-line @typescript-eslint/naming-convention
export class EXT_instance_features implements IGLTFLoaderExtension {
    /**
     * The name of this extension.
     */
    public readonly name = NAME;
    /**
     * Defines whether this extension is enabled.
     */
    public enabled: boolean;

    private _loader: GLTFLoader;

    /**
     * @internal
     */
    constructor(loader: GLTFLoader) {
        this._loader = loader;
        // according the specification :  Each node that is extended with EXT_instance_features must also define an
        // EXT_mesh_gpu_instancing extension object, and is invalid without this dependency.
        this.enabled = this._loader.isExtensionUsed(NAME) && this._loader.isExtensionUsed(EXT_mesh_gpu_instancing.name);
    }

    public dispose(): void {
        (this._loader as any) = null;
    }

    public loadNodeAsync(context: string, node: INode, assign: (babylonTransformNode: TransformNode) => void): Nullable<Promise<TransformNode>> {
        if (node.extensions) {
            const ext_gpu_instancing = node.extensions[EXT_mesh_gpu_instancing.name];
            if (ext_gpu_instancing) {
                return GLTFLoader.LoadExtensionAsync<IEXTInstanceFeatures, TransformNode>(context, node, this.name, (extensionContext, extension) => {
                    const promise = this._loader.loadNodeAsync(`/nodes/${node.index}`, node, assign);
                    if (!node._primitiveBabylonMeshes) {
                        return promise;
                    }

                    for (const babylonMesh of node._primitiveBabylonMeshes) {
                        const babylonObject: any = babylonMesh;
                        const gpu_instancing_context = `${context}.${EXT_mesh_gpu_instancing.name}`;

                        const featureIds: Array<IFeatureId> = (babylonObject.featureIds = babylonObject.featureIds ?? []);
                        for (const fid of extension.featureIds) {
                            featureIds.push(fid);
                            if (fid.attribute != undefined) {
                                // Feature ID by Vertex
                                const n = EXT_mesh_features.BuildKind("_FEATURE_ID_", fid.attribute);
                                const accessor = ArrayItem.Get(`${gpu_instancing_context}/attributes/${n}`, this._loader.gltf.accessors, ext_gpu_instancing.attributes[n]);
                                this._loader._loadIndicesAccessorAsync(`/accessors/${accessor.bufferView}`, accessor).then((data) => {
                                    babylonObject.thinInstanceIds = babylonObject.thinInstanceIds ?? [];
                                    babylonObject.thinInstanceIds.push(data);
                                });
                            }
                        }
                    }
                    return promise;
                });
            }
        }
        return null;
    }
}

GLTFLoader.RegisterExtension(NAME, (loader) => new EXT_instance_features(loader));

2 Likes

FYI, I have finalized EXT_structural_metadata. We now need to merge similar code related to loading primitives. I will first add a common parent class to encapsulate this logic, making it easier to move the function to the loader afterward.

I won’t post the metadata type definition here as it is quite long.

Let me know if you’d like any further refinements! :rocket:

import * as GLTF2 from "babylonjs-gltf2interface";
import { IGLTFLoaderExtension } from "@babylonjs/loaders";
import { ArrayItem, GLTFLoader, IAccessor, IMeshPrimitive } from "@babylonjs/loaders/glTF/2.0";
import { IMetadataSchema, IMetadataPropertyTable, IMetadataPropertyAttribute, IMetadataPropertyTexture } from "./EXT_structural_metadata.types";
import { BoundingInfo, Geometry, Mesh, Nullable, TmpVectors, VertexBuffer } from "@babylonjs/core";

const NAME = "EXT_structural_metadata";

export interface IStructuralMetadata {
    schema: IMetadataSchema;
    propertyTables?: Array<IMetadataPropertyTable>;
    propertyAttributes?: Array<IMetadataPropertyAttribute>;
    propertyTextures?: Array<IMetadataPropertyTexture>;
}

export function IsStructuralMetadata(b: unknown): b is IStructuralMetadata {
    if (b === null || b === undefined || typeof b !== "object") return false;
    const obj = b as Partial<IStructuralMetadata>;
    return obj.schema !== undefined;
}

export interface IHasStructuralMetadata {
    structuralMetadata?: IStructuralMetadata;
}

export function IsHasStructuralMetadata(b: unknown): b is IStructuralMetadata {
    if (b === null || b === undefined || typeof b !== "object") return false;
    const obj = b as Partial<IHasStructuralMetadata>;
    return obj.structuralMetadata !== undefined;
}

/**
 * [Specification](https://github.com/CesiumGS/glTF/blob/3d-tiles-next/extensions/2.0/Vendor/EXT_structural_metadata/README.md)
 * [Playground Sample]()
 */
// eslint-disable-next-line @typescript-eslint/naming-convention
export class EXT_structural_metadata implements IGLTFLoaderExtension {
    /**
     * The name of this extension.
     */
    public readonly name = NAME;
    /**
     * Defines whether this extension is enabled.
     */
    public enabled: boolean;

    private _loader: GLTFLoader;
    private _metadata: Nullable<IStructuralMetadata> = null;

    /**
     * @internal
     */
    constructor(loader: GLTFLoader) {
        this._loader = loader;
        this.enabled = this._loader.isExtensionUsed(NAME);
    }

    /** @internal */
    public onLoading(): void {
        const extensions = this._loader.gltf.extensions;
        if (extensions && extensions[this.name]) {
            const extension = extensions[this.name] as any;
            const babylonObject: any = this._loader.rootBabylonMesh;
            babylonObject.structuralMetadata = {
                schema: extension.schema,
                propertyTables: extension.propertyTables,
                propertyAttributes: extension.propertyAttributes,
                propertyTextures: extension.propertyTextures,
            };
            this._metadata = babylonObject.structuralMetadata;
        }
    }

    public _loadVertexDataAsync(context: string, primitive: IMeshPrimitive, babylonMesh: Mesh): Nullable<Promise<Geometry>> {
        if (primitive.extensions) {
            const attributes = primitive.attributes;
            if (!attributes) {
                throw new Error(`${context}: Attributes are missing`);
            }

            const babylonGeometry = new Geometry(babylonMesh.name, (<any>this._loader)._babylonScene);
            const promises = new Array<Promise<any>>();
            const loadAttribute = (name: string, kind: string, callback?: (accessor: IAccessor) => void) => {
                if (attributes[name] == undefined) {
                    return;
                }

                babylonMesh._delayInfo = babylonMesh._delayInfo || [];
                if (babylonMesh._delayInfo.indexOf(kind) === -1) {
                    babylonMesh._delayInfo.push(kind);
                }

                const accessor: any = ArrayItem.Get(`${context}/attributes/${name}`, (<any>this._loader)._gltf.accessors, attributes[name]);
                promises.push(
                    this._loader._loadVertexAccessorAsync(`/accessors/${accessor.index}`, accessor, kind).then((babylonVertexBuffer) => {
                        if (babylonVertexBuffer.getKind() === VertexBuffer.PositionKind && !this._loader.parent.alwaysComputeBoundingBox && !babylonMesh.skeleton) {
                            if (accessor.min && accessor.max) {
                                const min = TmpVectors.Vector3[0].copyFromFloats(...(accessor.min as [number, number, number]));
                                const max = TmpVectors.Vector3[1].copyFromFloats(...(accessor.max as [number, number, number]));
                                if (accessor.normalized && accessor.componentType !== GLTF2.AccessorComponentType.FLOAT) {
                                    let divider = 1;
                                    switch (accessor.componentType) {
                                        case GLTF2.AccessorComponentType.BYTE:
                                            divider = 127.0;
                                            break;
                                        case GLTF2.AccessorComponentType.UNSIGNED_BYTE:
                                            divider = 255.0;
                                            break;
                                        case GLTF2.AccessorComponentType.SHORT:
                                            divider = 32767.0;
                                            break;
                                        case GLTF2.AccessorComponentType.UNSIGNED_SHORT:
                                            divider = 65535.0;
                                            break;
                                    }
                                    const oneOverDivider = 1 / divider;
                                    min.scaleInPlace(oneOverDivider);
                                    max.scaleInPlace(oneOverDivider);
                                }
                                babylonGeometry._boundingInfo = new BoundingInfo(min, max);
                                babylonGeometry.useBoundingInfoFromGeometry = true;
                            }
                        }
                        babylonGeometry.setVerticesBuffer(babylonVertexBuffer, accessor.count);
                    })
                );

                if (kind == VertexBuffer.MatricesIndicesExtraKind) {
                    babylonMesh.numBoneInfluencers = 8;
                }

                if (callback) {
                    callback(accessor);
                }
            };

            const attributeMappings: [string, string, Nullable<(accessor: IAccessor) => void>][] = [
                ["TEXCOORD_0", VertexBuffer.UVKind, null],
                ["TEXCOORD_1", VertexBuffer.UV2Kind, null],
                ["TEXCOORD_2", VertexBuffer.UV3Kind, null],
                ["TEXCOORD_3", VertexBuffer.UV4Kind, null],
                ["TEXCOORD_4", VertexBuffer.UV5Kind, null],
                ["TEXCOORD_5", VertexBuffer.UV6Kind, null],

                ["POSITION", VertexBuffer.PositionKind, null],
                ["NORMAL", VertexBuffer.NormalKind, null],
                ["TANGENT", VertexBuffer.TangentKind, null],

                ["JOINTS_0", VertexBuffer.MatricesIndicesKind, null],
                ["WEIGHTS_0", VertexBuffer.MatricesWeightsKind, null],
                ["JOINTS_1", VertexBuffer.MatricesIndicesExtraKind, null],
                ["WEIGHTS_1", VertexBuffer.MatricesWeightsExtraKind, null],
                [
                    "COLOR_0",
                    VertexBuffer.ColorKind,
                    (accessor) => {
                        if (accessor.type === GLTF2.AccessorType.VEC4) {
                            babylonMesh.hasVertexAlpha = true;
                        }
                    },
                ],
            ];

            //#region extension specific
            const extension = primitive.extensions[EXT_structural_metadata.name];
            if (extension && this._metadata) {
                if (extension.propertyAttributes && this._metadata.propertyAttributes) {
                    for (const i of extension.propertyAttributes) {
                        if (i >= 0 && i < this._metadata.propertyAttributes.length) {
                            const ref = this._metadata.propertyAttributes[i];
                            for (const propertyName in ref.properties) {
                                const v = ref.properties[propertyName];
                                attributeMappings.push([v.attribute, v.attribute, null]);
                            }
                        }
                    }
                }
                if (extension.propertyTextures && this._metadata.propertyTextures) {
                    for (const i of extension.propertyTextures) {
                        if (i >= 0 && i < this._metadata.propertyTextures.length) {
                            const ref = this._metadata.propertyTextures[i];
                            for (const propertyName in ref.properties) {
                                const textInfos = ref.properties[propertyName];
                                this._loader.loadTextureInfoAsync(context, textInfos).then((babylonTexture) => {
                                    ref.textures = ref.textures || [];
                                    ref.textures[propertyName] = babylonTexture;
                                });
                            }
                        }
                    }
                }
            }
            //#end region extension specific

            attributeMappings.forEach(([attributeName, vertexKind, callback]) => {
                loadAttribute(attributeName, vertexKind, callback == null ? undefined : callback);
            });

            return Promise.all(promises).then(() => {
                return babylonGeometry;
            });
        }
        return null;
    }

    public dispose(): void {
        (this._loader as any) = null;
    }
}

GLTFLoader.RegisterExtension(NAME, (loader) => new EXT_structural_metadata(loader));