瀏覽代碼

glTF serializer: add support for KHR_lights_punctual

Kacey Coley 6 年之前
父節點
當前提交
a7e932403b

+ 6 - 7
inspector/src/components/actionTabs/tabs/toolsTabComponent.tsx

@@ -2,7 +2,7 @@ import * as React from "react";
 import { PaneComponent, IPaneComponentProps } from "../paneComponent";
 import { LineContainerComponent } from "../lineContainerComponent";
 import { ButtonLineComponent } from "../lines/buttonLineComponent";
-
+import { Node } from "babylonjs/node";
 import { Nullable } from "babylonjs/types";
 import { VideoRecorder } from "babylonjs/Misc/videoRecorder";
 import { Tools } from "babylonjs/Misc/tools";
@@ -12,7 +12,6 @@ import { StandardMaterial } from "babylonjs/Materials/standardMaterial";
 import { PBRMaterial } from "babylonjs/Materials/PBR/pbrMaterial";
 import { CubeTexture } from "babylonjs/Materials/Textures/cubeTexture";
 import { Texture } from "babylonjs/Materials/Textures/texture";
-import { TransformNode } from "babylonjs/Meshes/transformNode";
 import { SceneSerializer } from "babylonjs/Misc/sceneSerializer";
 import { Mesh } from "babylonjs/Meshes/mesh";
 
@@ -69,12 +68,12 @@ export class ToolsTabComponent extends PaneComponent {
         this.setState({ tag: "Stop recording" });
     }
 
-    shouldExport(transformNode: TransformNode): boolean {
+    shouldExport(node: Node): boolean {
 
         // No skybox
-        if (transformNode instanceof Mesh) {
-            if (transformNode.material) {
-                const material = transformNode.material as PBRMaterial | StandardMaterial | BackgroundMaterial;
+        if (node instanceof Mesh) {
+            if (node.material) {
+                const material = node.material as PBRMaterial | StandardMaterial | BackgroundMaterial;
                 const reflectionTexture = material.reflectionTexture;
                 if (reflectionTexture && reflectionTexture.coordinatesMode === Texture.SKYBOX_MODE) {
                     return false;
@@ -89,7 +88,7 @@ export class ToolsTabComponent extends PaneComponent {
         const scene = this.props.scene;
 
         GLTF2Export.GLBAsync(scene, "scene", {
-            shouldExportTransformNode: (transformNode) => this.shouldExport(transformNode)
+            shouldExportNode: (node) => this.shouldExport(node)
         }).then((glb: GLTFData) => {
             glb.downloadFiles();
         });

+ 185 - 0
serializers/src/glTF/2.0/Extensions/KHR_lights_punctual.ts

@@ -0,0 +1,185 @@
+import { SpotLight } from "babylonjs/Lights/spotLight";
+import { Vector3 } from "babylonjs/Maths/math";
+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 { Tools, Nullable, DirectionalLight, Quaternion } from 'babylonjs';
+
+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/1048d162a44dbcb05aefc1874bfd423cf60135a6/extensions/2.0/Khronos/KHR_lights_punctual/README.md) (Experimental)
+ */
+export class KHR_lights 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._exporter;
+        delete this._lights;
+    }
+
+    /** @hidden */
+    public onExporting(): void {
+        if (this._lights) {
+            if (this._exporter._glTF.extensionsUsed == null) {
+                this._exporter._glTF.extensionsUsed = [];
+            }
+            if (this._exporter._glTF.extensionsUsed.indexOf(NAME) == -1) {
+                this._exporter._glTF.extensionsUsed.push(NAME);
+            }
+            if (this.required) {
+                if (this._exporter._glTF.extensionsRequired == null) {
+                    this._exporter._glTF.extensionsRequired = [];
+                }
+                if (this._exporter._glTF.extensionsRequired.indexOf(NAME) == -1) {
+                    this._exporter._glTF.extensionsRequired.push(NAME);
+                }
+            }
+            if (this._exporter._glTF.extensions == null) {
+                this._exporter._glTF.extensions = {};
+            }
+            this._exporter._glTF.extensions[NAME] = this._lights;
+        }
+    }
+    public postExportNodeAsync(context: string, node: INode, babylonNode: Node): Nullable<Promise<INode>> {
+        return new Promise((resolve, reject) => {
+            if (babylonNode instanceof ShadowLight) {
+                let babylonLight: ShadowLight = babylonNode;
+                let light: ILight;
+
+                let 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) {
+                    Tools.Warn(`${context}: Light ${babylonLight.name} is not supported in {NAME}`);
+                }
+                else {
+                    let lightPosition = babylonLight.position.clone();
+                    if (!lightPosition.equalsToFloats(1.0, 1.0, 1.0)) {
+                        if (this._exporter._convertToRightHandedSystem) {
+                            lightPosition.z *= -1;
+                        }
+                        node.translation = lightPosition.asArray();
+                    }
+                    if (babylonLight.falloffType != Light.FALLOFF_GLTF) {
+                        Tools.Warn(`${context}: Light falloff for ${babylonLight.name} does not match the ${NAME} specification!`);
+                    }
+                    light = {
+                        type: lightType
+                    };
+                    if (!babylonLight.diffuse.equalsFloats(1.0, 1.0, 1.0)) {
+                        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) {
+                        let babylonSpotLight = babylonLight as SpotLight;
+                        if (!(babylonSpotLight.angle == Math.PI / 2.0)) {
+                            if (light.spot == null) {
+                                light.spot = {};
+                            }
+                            light.spot.outerConeAngle = babylonSpotLight.angle / 4;
+                        }
+                        if (!(babylonSpotLight.innerAngle == 0)) {
+                            if (light.spot == null) {
+                                light.spot = {};
+                            }
+                            light.spot.innerConeAngle = babylonSpotLight.innerAngle / 2;
+                        }
+                        let rotation = babylonSpotLight.getRotation().clone();
+                        if (!rotation.equals(Vector3.Zero())) {
+                            if (this._exporter._convertToRightHandedSystem) {
+                                rotation.z *= -1;
+                            }
+                            node.rotation = Quaternion.FromEulerAngles(rotation.x, rotation.y, rotation.z).asArray();
+                        }
+                    }
+                    else if (lightType === LightType.DIRECTIONAL) {
+                        let babylonDirectionalLight = babylonLight as DirectionalLight;
+                        let rotation = babylonDirectionalLight.getRotation().clone();
+                        if (!rotation.equals(Vector3.Zero())) {
+                            if (this._exporter._convertToRightHandedSystem) {
+                                rotation.z *= -1;
+                            }
+                            node.rotation = Quaternion.FromEulerAngles(rotation.x, rotation.y, rotation.z).asArray();
+                        }
+                    }
+                    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);
+        });
+    }
+}
+
+_Exporter.RegisterExtension(NAME, (exporter) => new KHR_lights(exporter));

+ 2 - 1
serializers/src/glTF/2.0/Extensions/index.ts

@@ -1 +1,2 @@
-export * from "./KHR_texture_transform";
+export * from "./KHR_texture_transform";
+export * from "./KHR_lights_punctual";

+ 30 - 29
serializers/src/glTF/2.0/glTFAnimation.ts

@@ -1,5 +1,5 @@
 import { AnimationSamplerInterpolation, AnimationChannelTargetPath, AccessorType, IAnimation, INode, IBufferView, IAccessor, IAnimationSampler, IAnimationChannel, AccessorComponentType } from "babylonjs-gltf2interface";
-
+import { Node } from "babylonjs/node";
 import { Nullable } from "babylonjs/types";
 import { Vector3, Quaternion } from "babylonjs/Maths/math";
 import { Tools } from "babylonjs/Misc/tools";
@@ -170,7 +170,7 @@ export class _GLTFAnimation {
     /**
      * @ignore
      * Create node animations from the transform node animations
-     * @param babylonTransformNode
+     * @param babylonNode
      * @param runtimeGLTFAnimation
      * @param idleGLTFAnimations
      * @param nodeMap
@@ -180,33 +180,35 @@ export class _GLTFAnimation {
      * @param accessors
      * @param convertToRightHandedSystem
      */
-    public static _CreateNodeAnimationFromTransformNodeAnimations(babylonTransformNode: TransformNode, runtimeGLTFAnimation: IAnimation, idleGLTFAnimations: IAnimation[], nodeMap: { [key: number]: number }, nodes: INode[], binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystem: boolean, animationSampleRate: number) {
+    public static _CreateNodeAnimationFromNodeAnimations(babylonNode: Node, runtimeGLTFAnimation: IAnimation, idleGLTFAnimations: IAnimation[], nodeMap: { [key: number]: number }, nodes: INode[], binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystem: boolean, animationSampleRate: number) {
         let glTFAnimation: IAnimation;
-        if (babylonTransformNode.animations) {
-            for (let animation of babylonTransformNode.animations) {
-                let animationInfo = _GLTFAnimation._DeduceAnimationInfo(animation);
-                if (animationInfo) {
-                    glTFAnimation = {
-                        name: animation.name,
-                        samplers: [],
-                        channels: []
-                    };
-                    _GLTFAnimation.AddAnimation(`${animation.name}`,
-                        animation.hasRunningRuntimeAnimations ? runtimeGLTFAnimation : glTFAnimation,
-                        babylonTransformNode,
-                        animation,
-                        animationInfo.dataAccessorType,
-                        animationInfo.animationChannelTargetPath,
-                        nodeMap,
-                        binaryWriter,
-                        bufferViews,
-                        accessors,
-                        convertToRightHandedSystem,
-                        animationInfo.useQuaternion,
-                        animationSampleRate
-                    );
-                    if (glTFAnimation.samplers.length && glTFAnimation.channels.length) {
-                        idleGLTFAnimations.push(glTFAnimation);
+        if (babylonNode instanceof TransformNode) {
+            if (babylonNode.animations) {
+                for (let animation of babylonNode.animations) {
+                    let animationInfo = _GLTFAnimation._DeduceAnimationInfo(animation);
+                    if (animationInfo) {
+                        glTFAnimation = {
+                            name: animation.name,
+                            samplers: [],
+                            channels: []
+                        };
+                        _GLTFAnimation.AddAnimation(`${animation.name}`,
+                            animation.hasRunningRuntimeAnimations ? runtimeGLTFAnimation : glTFAnimation,
+                            babylonNode,
+                            animation,
+                            animationInfo.dataAccessorType,
+                            animationInfo.animationChannelTargetPath,
+                            nodeMap,
+                            binaryWriter,
+                            bufferViews,
+                            accessors,
+                            convertToRightHandedSystem,
+                            animationInfo.useQuaternion,
+                            animationSampleRate
+                        );
+                        if (glTFAnimation.samplers.length && glTFAnimation.channels.length) {
+                            idleGLTFAnimations.push(glTFAnimation);
+                        }
                     }
                 }
             }
@@ -284,7 +286,6 @@ export class _GLTFAnimation {
             let byteLength = animationData.inputs.length * 4;
             bufferView = _GLTFUtilities._CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, undefined, `${name}  keyframe data view`);
             bufferViews.push(bufferView);
-
             animationData.inputs.forEach(function(input) {
                 binaryWriter.setFloat32(input);
             });

+ 94 - 75
serializers/src/glTF/2.0/glTFExporter.ts

@@ -1,4 +1,4 @@
-import { AccessorType, IBufferView, IAccessor, INode, IAsset, IScene, IMesh, IMaterial, ITexture, IImage, ISampler, IAnimation, ImageMimeType, IMeshPrimitive, IBuffer, IGLTF, MeshPrimitiveMode, AccessorComponentType } from "babylonjs-gltf2interface";
+import { AccessorType, IBufferView, IAccessor, INode, IScene, IMesh, IMaterial, ITexture, IImage, ISampler, IAnimation, ImageMimeType, IMeshPrimitive, IBuffer, IGLTF, MeshPrimitiveMode, AccessorComponentType } from "babylonjs-gltf2interface";
 
 import { FloatArray, Nullable, IndicesArray } from "babylonjs/types";
 import { Viewport, Color3, Vector2, Vector3, Vector4, Quaternion } from "babylonjs/Maths/math";
@@ -53,6 +53,10 @@ interface _IVertexAttributeData {
  */
 export class _Exporter {
     /**
+     * Stores the glTF to export
+     */
+    public _glTF: IGLTF;
+    /**
      * Stores all generated buffer views, which represents views into the main glTF buffer data
      */
     public _bufferViews: IBufferView[];
@@ -65,10 +69,6 @@ export class _Exporter {
      */
     private _nodes: INode[];
     /**
-     * Stores the glTF asset information, which represents the glTF version and this file generator
-     */
-    private _asset: IAsset;
-    /**
      * Stores all the generated glTF scenes, which stores multiple node hierarchies
      */
     private _scenes: IScene[];
@@ -109,7 +109,7 @@ export class _Exporter {
     /**
      * Stores a reference to the Babylon scene containing the source geometry and material information
      */
-    private _babylonScene: Scene;
+    public _babylonScene: Scene;
     /**
      * Stores a map of the image data, where the key is the file name and the value
      * is the image data
@@ -124,7 +124,7 @@ export class _Exporter {
     /**
      * Specifies if the Babylon scene should be converted to right-handed on export
      */
-    private _convertToRightHandedSystem: boolean;
+    public _convertToRightHandedSystem: boolean;
 
     /**
      * Baked animation sample rate
@@ -132,9 +132,9 @@ export class _Exporter {
     private _animationSampleRate: number;
 
     /**
-     * Callback which specifies if a transform node should be exported or not
+     * Callback which specifies if a node should be exported or not
      */
-    private _shouldExportTransformNode: ((babylonTransformNode: TransformNode) => boolean);
+    private _shouldExportNode: ((babylonNode: Node) => boolean);
 
     private _localEngine: Engine;
 
@@ -142,9 +142,6 @@ export class _Exporter {
 
     private _extensions: { [name: string]: IGLTFExporterExtensionV2 } = {};
 
-    private _extensionsUsed: string[];
-    private _extensionsRequired: string[];
-
     private static _ExtensionNames = new Array<string>();
     private static _ExtensionFactories: { [name: string]: (exporter: _Exporter) => IGLTFExporterExtensionV2 } = {};
 
@@ -183,6 +180,23 @@ export class _Exporter {
         return this._applyExtensions(meshPrimitive, (extension) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(context, meshPrimitive, babylonSubMesh, binaryWriter));
     }
 
+    public _extensionsPostExportNodeAsync(context: string, node: INode, babylonNode: Node): Nullable<Promise<INode>> {
+        return this._applyExtensions(node, (extension) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode));
+    }
+
+    private _forEachExtensions(action: (extension: IGLTFExporterExtensionV2) => void): void {
+        for (const name of _Exporter._ExtensionNames) {
+            const extension = this._extensions[name];
+            if (extension.enabled) {
+                action(extension);
+            }
+        }
+    }
+
+    private _extensionsOnExporting(): void {
+        this._forEachExtensions((extension) => extension.onExporting && extension.onExporting());
+    }
+
     /**
      * Load glTF serializer extensions
      */
@@ -199,9 +213,9 @@ export class _Exporter {
      * @param options Options to modify the behavior of the exporter
      */
     public constructor(babylonScene: Scene, options?: IExportOptions) {
-        this._asset = { generator: "BabylonJS", version: "2.0" };
-        this._extensionsUsed = [];
-        this._extensionsRequired = [];
+        this._glTF = {
+            asset: { generator: "BabylonJS", version: "2.0" }
+        };
         this._babylonScene = babylonScene;
         this._bufferViews = [];
         this._accessors = [];
@@ -217,7 +231,7 @@ export class _Exporter {
         this._imageData = {};
         this._convertToRightHandedSystem = this._babylonScene.useRightHandedSystem ? false : true;
         const _options = options || {};
-        this._shouldExportTransformNode = _options.shouldExportTransformNode ? _options.shouldExportTransformNode : (babylonTransformNode: TransformNode) => true;
+        this._shouldExportNode = _options.shouldExportNode ? _options.shouldExportNode : (babylonNode: Node) => true;
         this._animationSampleRate = _options.animationSampleRate ? _options.animationSampleRate : 1 / 60;
 
         this._glTFMaterialExporter = new _GLTFMaterialExporter(this);
@@ -667,52 +681,43 @@ export class _Exporter {
         let bufferView: IBufferView;
         let byteOffset: number = this._totalByteLength;
 
-        let glTF: IGLTF = {
-            asset: this._asset
-        };
-        if (this._extensionsUsed && this._extensionsUsed.length) {
-            glTF.extensionsUsed = this._extensionsUsed;
-        }
-        if (this._extensionsRequired && this._extensionsRequired.length) {
-            glTF.extensionsRequired = this._extensionsRequired;
-        }
         if (buffer.byteLength) {
-            glTF.buffers = [buffer];
+            this._glTF.buffers = [buffer];
         }
         if (this._nodes && this._nodes.length) {
-            glTF.nodes = this._nodes;
+            this._glTF.nodes = this._nodes;
         }
         if (this._meshes && this._meshes.length) {
-            glTF.meshes = this._meshes;
+            this._glTF.meshes = this._meshes;
         }
         if (this._scenes && this._scenes.length) {
-            glTF.scenes = this._scenes;
-            glTF.scene = 0;
+            this._glTF.scenes = this._scenes;
+            this._glTF.scene = 0;
         }
         if (this._bufferViews && this._bufferViews.length) {
-            glTF.bufferViews = this._bufferViews;
+            this._glTF.bufferViews = this._bufferViews;
         }
         if (this._accessors && this._accessors.length) {
-            glTF.accessors = this._accessors;
+            this._glTF.accessors = this._accessors;
         }
         if (this._animations && this._animations.length) {
-            glTF.animations = this._animations;
+            this._glTF.animations = this._animations;
         }
         if (this._materials && this._materials.length) {
-            glTF.materials = this._materials;
+            this._glTF.materials = this._materials;
         }
         if (this._textures && this._textures.length) {
-            glTF.textures = this._textures;
+            this._glTF.textures = this._textures;
         }
         if (this._samplers && this._samplers.length) {
-            glTF.samplers = this._samplers;
+            this._glTF.samplers = this._samplers;
         }
         if (this._images && this._images.length) {
             if (!shouldUseGlb) {
-                glTF.images = this._images;
+                this._glTF.images = this._images;
             }
             else {
-                glTF.images = [];
+                this._glTF.images = [];
 
                 this._images.forEach((image) => {
                     if (image.uri) {
@@ -725,10 +730,10 @@ export class _Exporter {
                         image.name = imageName;
                         image.mimeType = imageData.mimeType;
                         image.uri = undefined;
-                        if (!glTF.images) {
-                            glTF.images = [];
+                        if (!this._glTF.images) {
+                            this._glTF.images = [];
                         }
-                        glTF.images.push(image);
+                        this._glTF.images.push(image);
                     }
                 });
                 // Replace uri with bufferview and mime type for glb
@@ -740,7 +745,7 @@ export class _Exporter {
             buffer.uri = glTFPrefix + ".bin";
         }
 
-        const jsonText = prettyPrint ? JSON.stringify(glTF, null, 2) : JSON.stringify(glTF);
+        const jsonText = prettyPrint ? JSON.stringify(this._glTF, null, 2) : JSON.stringify(this._glTF);
 
         return jsonText;
     }
@@ -752,6 +757,7 @@ export class _Exporter {
      */
     public _generateGLTFAsync(glTFPrefix: string): Promise<GLTFData> {
         return this._generateBinaryAsync().then((binaryBuffer) => {
+            this._extensionsOnExporting();
             const jsonText = this.generateJSON(false, glTFPrefix, true);
             const bin = new Blob([binaryBuffer], { type: 'application/octet-stream' });
 
@@ -808,6 +814,7 @@ export class _Exporter {
      */
     public _generateGLBAsync(glTFPrefix: string): Promise<GLTFData> {
         return this._generateBinaryAsync().then((binaryBuffer) => {
+            this._extensionsOnExporting();
             const jsonText = this.generateJSON(true);
             const glbFileName = glTFPrefix + '.glb';
             const headerLength = 12;
@@ -1247,10 +1254,10 @@ export class _Exporter {
         let glTFNodeIndex: number;
         let glTFNode: INode;
         let directDescendents: Node[];
-        const nodes = [...babylonScene.transformNodes, ...babylonScene.meshes];
+        const nodes: Node[] = [...babylonScene.transformNodes, ...babylonScene.meshes, ...babylonScene.lights];
 
         return this._glTFMaterialExporter._convertMaterialsToGLTFAsync(babylonScene.materials, ImageMimeType.PNG, true).then(() => {
-            return this.createNodeMapAndAnimationsAsync(babylonScene, nodes, this._shouldExportTransformNode, binaryWriter).then((nodeMap) => {
+            return this.createNodeMapAndAnimationsAsync(babylonScene, nodes, this._shouldExportNode, binaryWriter).then((nodeMap) => {
                 this._nodeMap = nodeMap;
 
                 this._totalByteLength = binaryWriter.getByteOffset();
@@ -1259,13 +1266,13 @@ export class _Exporter {
                 }
 
                 // Build Hierarchy with the node map.
-                for (let babylonTransformNode of nodes) {
-                    glTFNodeIndex = this._nodeMap[babylonTransformNode.uniqueId];
+                for (let babylonNode of nodes) {
+                    glTFNodeIndex = this._nodeMap[babylonNode.uniqueId];
                     if (glTFNodeIndex != null) {
                         glTFNode = this._nodes[glTFNodeIndex];
-                        if (!babylonTransformNode.parent) {
-                            if (!this._shouldExportTransformNode(babylonTransformNode)) {
-                                Tools.Log("Omitting " + babylonTransformNode.name + " from scene.");
+                        if (!babylonNode.parent) {
+                            if (!this._shouldExportNode(babylonNode)) {
+                                Tools.Log("Omitting " + babylonNode.name + " from scene.");
                             }
                             else {
                                 if (this._convertToRightHandedSystem) {
@@ -1280,7 +1287,7 @@ export class _Exporter {
                             }
                         }
 
-                        directDescendents = babylonTransformNode.getDescendants(true);
+                        directDescendents = babylonNode.getDescendants(true);
                         if (!glTFNode.children && directDescendents && directDescendents.length) {
                             const children: number[] = [];
                             for (let descendent of directDescendents) {
@@ -1305,11 +1312,11 @@ export class _Exporter {
      * Creates a mapping of Node unique id to node index and handles animations
      * @param babylonScene Babylon Scene
      * @param nodes Babylon transform nodes
-     * @param shouldExportTransformNode Callback specifying if a transform node should be exported
+     * @param shouldExportNode Callback specifying if a transform node should be exported
      * @param binaryWriter Buffer to write binary data to
      * @returns Node mapping of unique id to index
      */
-    private createNodeMapAndAnimationsAsync(babylonScene: Scene, nodes: TransformNode[], shouldExportTransformNode: (babylonTransformNode: TransformNode) => boolean, binaryWriter: _BinaryWriter): Promise<{ [key: number]: number }> {
+    private createNodeMapAndAnimationsAsync(babylonScene: Scene, nodes: Node[], shouldExportNode: (babylonNode: Node) => boolean, binaryWriter: _BinaryWriter): Promise<{ [key: number]: number }> {
         let promiseChain = Promise.resolve();
         const nodeMap: { [key: number]: number } = {};
         let nodeIndex: number;
@@ -1320,25 +1327,33 @@ export class _Exporter {
         };
         let idleGLTFAnimations: IAnimation[] = [];
 
-        for (let babylonTransformNode of nodes) {
-            if (shouldExportTransformNode(babylonTransformNode)) {
+        for (let babylonNode of nodes) {
+            if (shouldExportNode(babylonNode)) {
                 promiseChain = promiseChain.then(() => {
-                    return this.createNodeAsync(babylonTransformNode, binaryWriter).then((node) => {
-                        const directDescendents = babylonTransformNode.getDescendants(true, (node: Node) => { return (node instanceof TransformNode); });
-                        if (directDescendents.length || node.mesh != null) {
-                            this._nodes.push(node);
-                            nodeIndex = this._nodes.length - 1;
-                            nodeMap[babylonTransformNode.uniqueId] = nodeIndex;
+                    return this.createNodeAsync(babylonNode, binaryWriter).then((node) => {
+                        let promise = this._extensionsPostExportNodeAsync("createNodeAsync", node, babylonNode);
+                        if (promise == null) {
+                            Tools.Warn(`Not exporting node ${babylonNode.name}`);
                         }
+                        else {
+                            promise.then(() => {
+                                const directDescendents = babylonNode.getDescendants(true, (node: Node) => { return (node instanceof Node); });
+                                if (directDescendents.length || node.mesh != null || (node.extensions)) {
+                                    this._nodes.push(node);
+                                    nodeIndex = this._nodes.length - 1;
+                                    nodeMap[babylonNode.uniqueId] = nodeIndex;
+                                }
 
-                        if (!babylonScene.animationGroups.length && babylonTransformNode.animations.length) {
-                            _GLTFAnimation._CreateNodeAnimationFromTransformNodeAnimations(babylonTransformNode, runtimeGLTFAnimation, idleGLTFAnimations, nodeMap, this._nodes, binaryWriter, this._bufferViews, this._accessors, this._convertToRightHandedSystem, this._animationSampleRate);
+                                if (!babylonScene.animationGroups.length && babylonNode.animations.length) {
+                                    _GLTFAnimation._CreateNodeAnimationFromNodeAnimations(babylonNode, runtimeGLTFAnimation, idleGLTFAnimations, nodeMap, this._nodes, binaryWriter, this._bufferViews, this._accessors, this._convertToRightHandedSystem, this._animationSampleRate);
+                                }
+                            });
                         }
                     });
                 });
             }
             else {
-                `Excluding mesh ${babylonTransformNode.name}`;
+                `Excluding node ${babylonNode.name}`;
             }
         }
 
@@ -1366,28 +1381,32 @@ export class _Exporter {
      * @param binaryWriter Buffer for storing geometry data
      * @returns glTF node
      */
-    private createNodeAsync(babylonTransformNode: TransformNode, binaryWriter: _BinaryWriter): Promise<INode> {
+    private createNodeAsync(babylonNode: Node, binaryWriter: _BinaryWriter): Promise<INode> {
         return Promise.resolve().then(() => {
             // create node to hold translation/rotation/scale and the mesh
             const node: INode = {};
             // create mesh
             const mesh: IMesh = { primitives: [] };
 
-            if (babylonTransformNode.name) {
-                node.name = babylonTransformNode.name;
+            if (babylonNode.name) {
+                node.name = babylonNode.name;
             }
 
-            // Set transformation
-            this.setNodeTransformation(node, babylonTransformNode);
-
-            return this.setPrimitiveAttributesAsync(mesh, babylonTransformNode, binaryWriter).then(() => {
-                if (mesh.primitives.length) {
-                    this._meshes.push(mesh);
-                    node.mesh = this._meshes.length - 1;
-                }
+            if (babylonNode instanceof TransformNode) {
+                // Set transformation
+                this.setNodeTransformation(node, babylonNode);
 
+                return this.setPrimitiveAttributesAsync(mesh, babylonNode, binaryWriter).then(() => {
+                    if (mesh.primitives.length) {
+                        this._meshes.push(mesh);
+                        node.mesh = this._meshes.length - 1;
+                    }
+                    return node;
+                });
+            }
+            else {
                 return node;
-            });
+            }
         });
     }
 }

+ 15 - 1
serializers/src/glTF/2.0/glTFExporterExtension.ts

@@ -1,4 +1,5 @@
-import { ImageMimeType, IMeshPrimitive } from "babylonjs-gltf2interface";
+import { ImageMimeType, IMeshPrimitive, INode } from "babylonjs-gltf2interface";
+import { Node } from "babylonjs/node";
 
 import { Nullable } from "babylonjs/types";
 import { Texture } from "babylonjs/Materials/Textures/texture";
@@ -33,4 +34,17 @@ export interface IGLTFExporterExtensionV2 extends IGLTFExporterExtension, IDispo
      * @param binaryWriter glTF serializer binary writer instance
      */
     postExportMeshPrimitiveAsync?(context: string, meshPrimitive: IMeshPrimitive, babylonSubMesh: SubMesh, binaryWriter: _BinaryWriter): Nullable<Promise<IMeshPrimitive>>;
+
+    /**
+     * 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
+     */
+    postExportNodeAsync?(context: string, node: INode, babylonNode: Node): Nullable<Promise<INode>>;
+
+    /**
+     * Called after the exporter state changes to EXPORTING
+     */
+    onExporting?(): void;
 }

+ 5 - 5
serializers/src/glTF/2.0/glTFSerializer.ts

@@ -1,4 +1,4 @@
-import { TransformNode } from "babylonjs/Meshes/transformNode";
+import { Node } from "babylonjs/node";
 import { Scene } from "babylonjs/scene";
 import { GLTFData } from "./glTFData";
 import { _Exporter } from "./glTFExporter";
@@ -8,11 +8,11 @@ import { _Exporter } from "./glTFExporter";
  */
 export interface IExportOptions {
     /**
-     * Function which indicates whether a babylon mesh should be exported or not
-     * @param transformNode source Babylon transform node. It is used to check whether it should be exported to glTF or not
-     * @returns boolean, which indicates whether the mesh should be exported (true) or not (false)
+     * Function which indicates whether a babylon node should be exported or not
+     * @param node source Babylon node. It is used to check whether it should be exported to glTF or not
+     * @returns boolean, which indicates whether the node should be exported (true) or not (false)
      */
-    shouldExportTransformNode?(transformNode: TransformNode): boolean;
+    shouldExportNode?(node: Node): boolean;
     /**
      * The sample rate to bake animation curves
      */