Преглед изворни кода

Merge pull request #7658 from BabylonJS/drigax/FixGLTFLightRoundTrip

Fix glTF serializer light orientation issues
David Catuhe пре 5 година
родитељ
комит
6e809f98cd

+ 1 - 0
dist/preview release/what's new.md

@@ -343,6 +343,7 @@
 - Fix for bug where round-tripped glTF imported scenes are encapsulated in a second root node ([#6349](https://github.com/BabylonJS/Babylon.js/issues/6349))([drigax](https://github.com/drigax) & [noalak](https://github.com/noalak))
 - Fix `HDRCubeTexture` construction, `generateHarmonics` was not properly taken into account ([Popov72](https://github.com/Popov72))
 - VideoTexture poster respects invertY ([Sebavan](https://github.com/sebavan/)
+- Fix for bug where round-tripped glTF imported scenes have incorrect light orientation, and duplicated parent nodes ([#7377](https://github.com/BabylonJS/Babylon.js/issues/7377))([drigax](https://github.com/drigax))
 
 ## Breaking changes
 

+ 230 - 176
serializers/src/glTF/2.0/Extensions/KHR_lights_punctual.ts

@@ -1,177 +1,231 @@
-import { SpotLight } from "babylonjs/Lights/spotLight";
-import { Vector3, Quaternion } from "babylonjs/Maths/math.vector";
-import { Color3 } from "babylonjs/Maths/math.color";
-import { Light } from "babylonjs/Lights/light";
-import { Node } from "babylonjs/node";
-import { ShadowLight } from "babylonjs/Lights/shadowLight";
-import { IChildRootProperty } from "babylonjs-gltf2interface";
-import { INode } from "babylonjs-gltf2interface";
-import { IGLTFExporterExtensionV2 } from "../glTFExporterExtension";
-import { _Exporter } from "../glTFExporter";
-import { Logger } from "babylonjs/Misc/logger";
-import { _GLTFUtilities } from "../glTFUtilities";
-
-const NAME = "KHR_lights_punctual";
-
-enum LightType {
-    DIRECTIONAL = "directional",
-    POINT = "point",
-    SPOT = "spot"
-}
-
-interface ILightReference {
-    light: number;
-}
-
-interface ILight extends IChildRootProperty {
-    type: LightType;
-    color?: number[];
-    intensity?: number;
-    range?: number;
-    spot?: {
-        innerConeAngle?: number;
-        outerConeAngle?: number;
-    };
-}
-
-interface ILights {
-    lights: ILight[];
-}
-
-/**
- * [Specification](https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_lights_punctual/README.md)
- */
-export class KHR_lights_punctual implements IGLTFExporterExtensionV2 {
-    /** The name of this extension. */
-    public readonly name = NAME;
-
-    /** Defines whether this extension is enabled. */
-    public enabled = true;
-
-    /** Defines whether this extension is required */
-    public required = false;
-
-    /** Reference to the glTF exporter */
-    private _exporter: _Exporter;
-
-    private _lights: ILights;
-
-    /** @hidden */
-    constructor(exporter: _Exporter) {
-        this._exporter = exporter;
-    }
-
-    /** @hidden */
-    public dispose() {
-        delete this._lights;
-    }
-
-    /** @hidden */
-    public get wasUsed() {
-        return !!this._lights;
-    }
-
-    /** @hidden */
-    public onExporting(): void {
-        this._exporter!._glTF.extensions![NAME] = this._lights;
-    }
-    /**
-     * Define this method to modify the default behavior when exporting a node
-     * @param context The context when exporting the node
-     * @param node glTF node
-     * @param babylonNode BabylonJS node
-     * @returns nullable INode promise
-     */
-    public postExportNodeAsync(context: string, node: INode, babylonNode: Node): Promise<INode> {
-        return new Promise((resolve, reject) => {
-            if (babylonNode instanceof ShadowLight) {
-                const babylonLight: ShadowLight = babylonNode;
-                let light: ILight;
-
-                const lightType = (
-                    babylonLight.getTypeID() == Light.LIGHTTYPEID_POINTLIGHT ? LightType.POINT : (
-                        babylonLight.getTypeID() == Light.LIGHTTYPEID_DIRECTIONALLIGHT ? LightType.DIRECTIONAL : (
-                            babylonLight.getTypeID() == Light.LIGHTTYPEID_SPOTLIGHT ? LightType.SPOT : null
-                        )));
-                if (lightType == null) {
-                    Logger.Warn(`${context}: Light ${babylonLight.name} is not supported in ${NAME}`);
-                }
-                else {
-                    const lightPosition = babylonLight.position.clone();
-                    let convertToRightHandedSystem = this._exporter._convertToRightHandedSystemMap[babylonNode.uniqueId];
-                    if (!lightPosition.equals(Vector3.Zero())) {
-                        if (convertToRightHandedSystem) {
-                            _GLTFUtilities._GetRightHandedPositionVector3FromRef(lightPosition);
-                        }
-                        node.translation = lightPosition.asArray();
-                    }
-                    if (lightType !== LightType.POINT) {
-                        const localAxis = babylonLight.direction;
-                        const yaw = -Math.atan2(localAxis.z, localAxis.x) + Math.PI / 2;
-                        const len = Math.sqrt(localAxis.x * localAxis.x + localAxis.z * localAxis.z);
-                        const pitch = -Math.atan2(localAxis.y, len);
-                        const lightRotationQuaternion = Quaternion.RotationYawPitchRoll(yaw, pitch, 0);
-                        if (convertToRightHandedSystem) {
-                            _GLTFUtilities._GetRightHandedQuaternionFromRef(lightRotationQuaternion);
-                        }
-                        if (!lightRotationQuaternion.equals(Quaternion.Identity())) {
-                            node.rotation = lightRotationQuaternion.asArray();
-                        }
-                    }
-
-                    if (babylonLight.falloffType !== Light.FALLOFF_GLTF) {
-                        Logger.Warn(`${context}: Light falloff for ${babylonLight.name} does not match the ${NAME} specification!`);
-                    }
-                    light = {
-                        type: lightType
-                    };
-                    if (!babylonLight.diffuse.equals(Color3.White())) {
-                        light.color = babylonLight.diffuse.asArray();
-                    }
-                    if (babylonLight.intensity !== 1.0) {
-                        light.intensity = babylonLight.intensity;
-                    }
-                    if (babylonLight.range !== Number.MAX_VALUE) {
-                        light.range = babylonLight.range;
-                    }
-
-                    if (lightType === LightType.SPOT) {
-                        const babylonSpotLight = babylonLight as SpotLight;
-                        if (babylonSpotLight.angle !== Math.PI / 2.0) {
-                            if (light.spot == null) {
-                                light.spot = {};
-                            }
-                            light.spot.outerConeAngle = babylonSpotLight.angle / 2.0;
-                        }
-                        if (babylonSpotLight.innerAngle !== 0) {
-                            if (light.spot == null) {
-                                light.spot = {};
-                            }
-                            light.spot.innerConeAngle = babylonSpotLight.innerAngle / 2.0;
-                        }
-                    }
-
-                    if (this._lights == null) {
-                        this._lights = {
-                            lights: []
-                        };
-                    }
-
-                    this._lights.lights.push(light);
-
-                    if (node.extensions == null) {
-                        node.extensions = {};
-                    }
-                    const lightReference: ILightReference = {
-                        light: this._lights.lights.length - 1
-                    };
-
-                    node.extensions[NAME] = lightReference;
-                }
-            }
-            resolve(node);
-        });
-    }
-}
-
+import { SpotLight } from "babylonjs/Lights/spotLight";
+import { Nullable } from "babylonjs/types";
+import { Vector3, Quaternion } from "babylonjs/Maths/math.vector";
+import { Color3 } from "babylonjs/Maths/math.color";
+import { Light } from "babylonjs/Lights/light";
+import { DirectionalLight } from "babylonjs/Lights/directionalLight"
+import { Node } from "babylonjs/node";
+import { ShadowLight } from "babylonjs/Lights/shadowLight";
+import { IChildRootProperty } from "babylonjs-gltf2interface";
+import { INode } from "babylonjs-gltf2interface";
+import { IGLTFExporterExtensionV2 } from "../glTFExporterExtension";
+import { _Exporter } from "../glTFExporter";
+import { Logger } from "babylonjs/Misc/logger";
+import { _GLTFUtilities } from "../glTFUtilities";
+
+const NAME = "KHR_lights_punctual";
+
+enum LightType {
+    DIRECTIONAL = "directional",
+    POINT = "point",
+    SPOT = "spot"
+}
+
+interface ILightReference {
+    light: number;
+}
+
+interface ILight extends IChildRootProperty {
+    type: LightType;
+    color?: number[];
+    intensity?: number;
+    range?: number;
+    spot?: {
+        innerConeAngle?: number;
+        outerConeAngle?: number;
+    };
+}
+
+interface ILights {
+    lights: ILight[];
+}
+
+/**
+ * [Specification](https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_lights_punctual/README.md)
+ */
+export class KHR_lights_punctual implements IGLTFExporterExtensionV2 {
+    /** The name of this extension. */
+    public readonly name = NAME;
+
+    /** Defines whether this extension is enabled. */
+    public enabled = true;
+
+    /** Defines whether this extension is required */
+    public required = false;
+
+    /** Reference to the glTF exporter */
+    private _exporter: _Exporter;
+
+    private _lights: ILights;
+
+    /** @hidden */
+    constructor(exporter: _Exporter) {
+        this._exporter = exporter;
+    }
+
+    /** @hidden */
+    public dispose() {
+        delete this._lights;
+    }
+
+    /** @hidden */
+    public get wasUsed() {
+        return !!this._lights;
+    }
+
+    /** @hidden */
+    public onExporting(): void {
+        this._exporter!._glTF.extensions![NAME] = this._lights;
+    }
+    /**
+     * Define this method to modify the default behavior when exporting a node
+     * @param context The context when exporting the node
+     * @param node glTF node
+     * @param babylonNode BabylonJS node
+     * @param nodeMap Node mapping of unique id to glTF node index
+     * @returns nullable INode promise
+     */
+    public postExportNodeAsync(context: string, node: Nullable<INode>, babylonNode: Node, nodeMap?: {[key: number]: number}): Promise<Nullable<INode>> {
+        return new Promise((resolve, reject) => {
+            if (node && babylonNode instanceof ShadowLight) {
+                const babylonLight: ShadowLight = babylonNode;
+                let light: ILight;
+
+                const lightType = (
+                    babylonLight.getTypeID() == Light.LIGHTTYPEID_POINTLIGHT ? LightType.POINT : (
+                        babylonLight.getTypeID() == Light.LIGHTTYPEID_DIRECTIONALLIGHT ? LightType.DIRECTIONAL : (
+                            babylonLight.getTypeID() == Light.LIGHTTYPEID_SPOTLIGHT ? LightType.SPOT : null
+                        )));
+                if (lightType == null) {
+                    Logger.Warn(`${context}: Light ${babylonLight.name} is not supported in ${NAME}`);
+                }
+                else {
+                    const lightPosition = babylonLight.position.clone();
+                    let convertToRightHandedSystem = this._exporter._convertToRightHandedSystemMap[babylonNode.uniqueId];
+                    if (!lightPosition.equals(Vector3.Zero())) {
+                        if (convertToRightHandedSystem) {
+                            _GLTFUtilities._GetRightHandedPositionVector3FromRef(lightPosition);
+                        }
+                        node.translation = lightPosition.asArray();
+                    }
+                    if (lightType !== LightType.POINT) {
+                        const localAxis = babylonLight.direction;
+                        const yaw = -Math.atan2(localAxis.z * (this._exporter._babylonScene.useRightHandedSystem ? -1 : 1), localAxis.x) + Math.PI / 2;
+                        const len = Math.sqrt(localAxis.x * localAxis.x + localAxis.z * localAxis.z);
+                        const pitch = -Math.atan2(localAxis.y, len);
+                        const lightRotationQuaternion = Quaternion.RotationYawPitchRoll(yaw, pitch, 0);
+                        if (convertToRightHandedSystem) {
+                            _GLTFUtilities._GetRightHandedQuaternionFromRef(lightRotationQuaternion);
+                        }
+                        if (!lightRotationQuaternion.equals(Quaternion.Identity())) {
+                            node.rotation = lightRotationQuaternion.asArray();
+                        }
+                    }
+
+                    if (babylonLight.falloffType !== Light.FALLOFF_GLTF) {
+                        Logger.Warn(`${context}: Light falloff for ${babylonLight.name} does not match the ${NAME} specification!`);
+                    }
+                    light = {
+                        type: lightType
+                    };
+                    if (!babylonLight.diffuse.equals(Color3.White())) {
+                        light.color = babylonLight.diffuse.asArray();
+                    }
+                    if (babylonLight.intensity !== 1.0) {
+                        light.intensity = babylonLight.intensity;
+                    }
+                    if (babylonLight.range !== Number.MAX_VALUE) {
+                        light.range = babylonLight.range;
+                    }
+
+                    if (lightType === LightType.SPOT) {
+                        const babylonSpotLight = babylonLight as SpotLight;
+                        if (babylonSpotLight.angle !== Math.PI / 2.0) {
+                            if (light.spot == null) {
+                                light.spot = {};
+                            }
+                            light.spot.outerConeAngle = babylonSpotLight.angle / 2.0;
+                        }
+                        if (babylonSpotLight.innerAngle !== 0) {
+                            if (light.spot == null) {
+                                light.spot = {};
+                            }
+                            light.spot.innerConeAngle = babylonSpotLight.innerAngle / 2.0;
+                        }
+                    }
+
+                    if (this._lights == null) {
+                        this._lights = {
+                            lights: []
+                        };
+                    }
+
+                    this._lights.lights.push(light);
+
+                    const lightReference: ILightReference = {
+                        light: this._lights.lights.length - 1
+                    };
+
+                    // Avoid duplicating the Light's parent node if possible.
+                    let parentBabylonNode = babylonNode.parent;
+                    if (parentBabylonNode && parentBabylonNode.getChildren().length == 1) {
+                        let parentNode = this._exporter._nodes[nodeMap![parentBabylonNode.uniqueId]];
+                        if (parentNode) {
+                            let parentNodeLocalMatrix = TmpVectors.Matrix[0];
+                            let parentInvertNodeLocalMatrix = TmpVectors.Matrix[1];
+                            let parentNodeLocalTranslation = parentNode.translation ? new Vector3(parentNode.translation[0], parentNode.translation[1], parentNode.translation[2]) : Vector3.Zero();
+                            let parentNodeLocalRotation = parentNode.rotation ? new Quaternion(parentNode.rotation[0], parentNode.rotation[1], parentNode.rotation[2], parentNode.rotation[3]) : Quaternion.Identity();
+                            let parentNodeLocalScale = parentNode.scale ? new Vector3(parentNode.scale[0], parentNode.scale[1], parentNode.scale[2]) : Vector3.One();
+
+                            Matrix.ComposeToRef(parentNodeLocalScale, parentNodeLocalRotation, parentNodeLocalTranslation, parentNodeLocalMatrix);
+                            parentNodeLocalMatrix.invertToRef(parentInvertNodeLocalMatrix);
+
+                            // Convert light local matrix to local matrix relative to grandparent, facing -Z
+                            let lightLocalMatrix = TmpVectors.Matrix[2];
+                            let nodeLocalTranslation = node.translation ? new Vector3(node.translation[0], node.translation[1], node.translation[2]) : Vector3.Zero();
+
+                            // Undo directional light positional offset
+                            if (babylonLight instanceof DirectionalLight) {
+                                nodeLocalTranslation.subtractInPlace(this._exporter._babylonScene.useRightHandedSystem ? babylonLight.direction : _GLTFUtilities._GetRightHandedPositionVector3(babylonLight.direction));
+                            }
+                            let nodeLocalRotation = this._exporter._babylonScene.useRightHandedSystem ? Quaternion.Identity() : new Quaternion(0, 1, 0, 0);
+                            if (node.rotation) {
+                                nodeLocalRotation.multiplyInPlace(new Quaternion(node.rotation[0], node.rotation[1], node.rotation[2], node.rotation[3]));
+                            }
+                            let nodeLocalScale = node.scale ? new Vector3(node.scale[0], node.scale[1], node.scale[2]) : Vector3.One();
+
+                            Matrix.ComposeToRef(nodeLocalScale, nodeLocalRotation, nodeLocalTranslation, lightLocalMatrix);
+                            lightLocalMatrix.multiplyToRef(parentInvertNodeLocalMatrix, lightLocalMatrix);
+                            let parentNewScale = TmpVectors.Vector3[0];
+                            let parentNewRotationQuaternion = TmpVectors.Quaternion[0];
+                            let parentNewTranslation = TmpVectors.Vector3[1];
+
+                            lightLocalMatrix.decompose(parentNewScale, parentNewRotationQuaternion, parentNewTranslation);
+                            parentNode.scale = parentNewScale.asArray();
+                            parentNode.rotation = parentNewRotationQuaternion.asArray();
+                            parentNode.translation = parentNewTranslation.asArray();
+
+                            if (parentNode.extensions == null) {
+                                parentNode.extensions = {};
+                            }
+                            parentNode.extensions[NAME] = lightReference;
+
+                            // Do not export the original node
+                            resolve(undefined);
+                            return;
+                        }
+                    }
+
+                    if (node.extensions == null) {
+                        node.extensions = {};
+                    }
+
+                    node.extensions[NAME] = lightReference;
+                }
+            }
+            resolve(node);
+        });
+    }
+}
+
 _Exporter.RegisterExtension(NAME, (exporter) => new KHR_lights_punctual(exporter));

+ 13 - 12
serializers/src/glTF/2.0/glTFExporter.ts

@@ -70,7 +70,7 @@ export class _Exporter {
     /**
      * Stores all the generated nodes, which contains transform and/or mesh information per node
      */
-    private _nodes: INode[];
+    public _nodes: INode[];
     /**
      * Stores all the generated glTF scenes, which stores multiple node hierarchies
      */
@@ -122,7 +122,7 @@ export class _Exporter {
     /**
      * Stores a map of the unique id of a node to its index in the node array
      */
-    private _nodeMap: { [key: number]: number };
+    public _nodeMap: { [key: number]: number };
 
     /**
      * Specifies if the source Babylon scene was left handed, and needed conversion.
@@ -155,7 +155,7 @@ export class _Exporter {
     private static _ExtensionNames = new Array<string>();
     private static _ExtensionFactories: { [name: string]: (exporter: _Exporter) => IGLTFExporterExtensionV2 } = {};
 
-    private _applyExtension<T>(node: T, extensions: IGLTFExporterExtensionV2[], index: number, actionAsync: (extension: IGLTFExporterExtensionV2, node: T) => Promise<Nullable<T>> | undefined): Promise<Nullable<T>> {
+    private _applyExtension<T>(node: Nullable<T>, extensions: IGLTFExporterExtensionV2[], index: number, actionAsync: (extension: IGLTFExporterExtensionV2, node: Nullable<T>) => Promise<Nullable<T>> | undefined): Promise<Nullable<T>> {
         if (index >= extensions.length) {
             return Promise.resolve(node);
         }
@@ -166,10 +166,10 @@ export class _Exporter {
             return this._applyExtension(node, extensions, index + 1, actionAsync);
         }
 
-        return currentPromise.then((newNode) => this._applyExtension(newNode || node, extensions, index + 1, actionAsync));
+        return currentPromise.then((newNode) => this._applyExtension(newNode, extensions, index + 1, actionAsync));
     }
 
-    private _applyExtensions<T>(node: T, actionAsync: (extension: IGLTFExporterExtensionV2, node: T) => Promise<Nullable<T>> | undefined): Promise<Nullable<T>> {
+    private _applyExtensions<T>(node: Nullable<T>, actionAsync: (extension: IGLTFExporterExtensionV2, node: Nullable<T>) => Promise<Nullable<T>> | undefined): Promise<Nullable<T>> {
         var extensions: IGLTFExporterExtensionV2[] = [];
         for (const name of _Exporter._ExtensionNames) {
             extensions.push(this._extensions[name]);
@@ -178,7 +178,7 @@ export class _Exporter {
         return this._applyExtension(node, extensions, 0, actionAsync);
     }
 
-    public _extensionsPreExportTextureAsync(context: string, babylonTexture: Texture, mimeType: ImageMimeType): Promise<Nullable<BaseTexture>> {
+    public _extensionsPreExportTextureAsync(context: string, babylonTexture: Nullable<Texture>, mimeType: ImageMimeType): Promise<Nullable<BaseTexture>> {
         return this._applyExtensions(babylonTexture, (extension, node) => extension.preExportTextureAsync && extension.preExportTextureAsync(context, node, mimeType));
     }
 
@@ -186,11 +186,11 @@ export class _Exporter {
         return this._applyExtensions(meshPrimitive, (extension, node) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(context, node, babylonSubMesh, binaryWriter));
     }
 
-    public _extensionsPostExportNodeAsync(context: string, node: INode, babylonNode: Node): Promise<Nullable<INode>> {
-        return this._applyExtensions(node, (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode));
+    public _extensionsPostExportNodeAsync(context: string, node: Nullable<INode>, babylonNode: Node, nodeMap?: {[key: number]: number}): Promise<Nullable<INode>> {
+        return this._applyExtensions(node, (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode, nodeMap));
     }
 
-    public _extensionsPostExportMaterialAsync(context: string, material: IMaterial, babylonMaterial: Material): Promise<Nullable<IMaterial>> {
+    public _extensionsPostExportMaterialAsync(context: string, material: Nullable<IMaterial>, babylonMaterial: Material): Promise<Nullable<IMaterial>> {
         return this._applyExtensions(material, (extension, node) => extension.postExportMaterialAsync && extension.postExportMaterialAsync(context, node, babylonMaterial));
     }
 
@@ -1480,8 +1480,8 @@ export class _Exporter {
             if (!this._options.shouldExportNode || this._options.shouldExportNode(babylonNode)) {
                 promiseChain = promiseChain.then(() => {
                     let convertToRightHandedSystem = this._convertToRightHandedSystemMap[babylonNode.uniqueId];
-                    return this.createNodeAsync(babylonNode, binaryWriter, convertToRightHandedSystem).then((node) => {
-                        const promise = this._extensionsPostExportNodeAsync("createNodeAsync", node, babylonNode);
+                    return this.createNodeAsync(babylonNode, binaryWriter, convertToRightHandedSystem, nodeMap).then((node) => {
+                        const promise = this._extensionsPostExportNodeAsync("createNodeAsync", node, babylonNode, nodeMap);
                         if (promise == null) {
                             Tools.Warn(`Not exporting node ${babylonNode.name}`);
                             return Promise.resolve();
@@ -1531,9 +1531,10 @@ export class _Exporter {
      * @param babylonMesh Source Babylon mesh
      * @param binaryWriter Buffer for storing geometry data
      * @param convertToRightHandedSystem Converts the values to right-handed
+     * @param nodeMap Node mapping of unique id to glTF node index
      * @returns glTF node
      */
-    private createNodeAsync(babylonNode: Node, binaryWriter: _BinaryWriter, convertToRightHandedSystem: boolean): Promise<INode> {
+    private createNodeAsync(babylonNode: Node, binaryWriter: _BinaryWriter, convertToRightHandedSystem: boolean, nodeMap?: {[key: number]: number}): Promise<INode> {
         return Promise.resolve().then(() => {
             // create node to hold translation/rotation/scale and the mesh
             const node: INode = {};

+ 5 - 4
serializers/src/glTF/2.0/glTFExporterExtension.ts

@@ -1,5 +1,6 @@
 import { ImageMimeType, IMeshPrimitive, INode, IMaterial, ITextureInfo } from "babylonjs-gltf2interface";
 import { Node } from "babylonjs/node";
+import { Nullable } from "babylonjs/types";
 
 import { Texture } from "babylonjs/Materials/Textures/texture";
 import { SubMesh } from "babylonjs/Meshes/subMesh";
@@ -25,7 +26,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo
      * @param mimeType The mime-type of the generated image
      * @returns A promise that resolves with the exported texture
      */
-    preExportTextureAsync?(context: string, babylonTexture: Texture, mimeType: ImageMimeType): Promise<Texture>;
+    preExportTextureAsync?(context: string, babylonTexture: Nullable<Texture>, mimeType: ImageMimeType): Promise<Texture>;
 
     /**
      * Define this method to get notified when a texture info is created
@@ -43,7 +44,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo
      * @param binaryWriter glTF serializer binary writer instance
      * @returns nullable IMeshPrimitive promise
      */
-    postExportMeshPrimitiveAsync?(context: string, meshPrimitive: IMeshPrimitive, babylonSubMesh: SubMesh, binaryWriter: _BinaryWriter): Promise<IMeshPrimitive>;
+    postExportMeshPrimitiveAsync?(context: string, meshPrimitive: Nullable<IMeshPrimitive>, babylonSubMesh: SubMesh, binaryWriter: _BinaryWriter): Promise<IMeshPrimitive>;
 
     /**
      * Define this method to modify the default behavior when exporting a node
@@ -52,7 +53,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo
      * @param babylonNode BabylonJS node
      * @returns nullable INode promise
      */
-    postExportNodeAsync?(context: string, node: INode, babylonNode: Node): Promise<INode>;
+    postExportNodeAsync?(context: string, node: Nullable<INode>, babylonNode: Node, nodeMap?: {[key: number]: number}): Promise<Nullable<INode>>;
 
     /**
      * Define this method to modify the default behavior when exporting a material
@@ -60,7 +61,7 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo
      * @param babylonMaterial BabylonJS material
      * @returns nullable IMaterial promise
      */
-    postExportMaterialAsync?(context: string, node: IMaterial, babylonMaterial: Material): Promise<IMaterial>;
+    postExportMaterialAsync?(context: string, node: Nullable<IMaterial>, babylonMaterial: Material): Promise<IMaterial>;
 
     /**
      * Define this method to return additional textures to export from a material

BIN
tests/validation/ReferenceImages/glTFSerializerNegativeWorldMatrix.png