Sfoglia il codice sorgente

Merge pull request #3564 from kcoley/kcoley/gltfserializermaterials

adding PBR Metallic Roughness support for glTF serializer, with unit tests
David Catuhe 7 anni fa
parent
commit
0d75ccca5c

+ 2 - 1
Tools/Gulp/config.json

@@ -1459,7 +1459,8 @@
                 "files": [
                     "../../serializers/src/glTF/2.0/babylon.glTFSerializer.ts",
                     "../../serializers/src/glTF/2.0/babylon.glTFExporter.ts",
-                    "../../serializers/src/glTF/2.0/babylon.glTFData.ts"
+                    "../../serializers/src/glTF/2.0/babylon.glTFData.ts",
+                    "../../serializers/src/glTF/2.0/babylon.glTFMaterial.ts"
                 ],
                 "output": "babylon.glTF2Serializer.js"
             }

+ 5 - 6
serializers/src/glTF/2.0/babylon.glTFData.ts

@@ -3,10 +3,10 @@ module BABYLON {
      * Class for holding and downloading glTF file data
      */
     export class _GLTFData {
-        _glTFFiles: { [fileName: string]: string | Blob };
+        glTFFiles: { [fileName: string]: string | Blob };
 
         public constructor() {
-            this._glTFFiles = {};
+            this.glTFFiles = {};
         }
         /**
          * Downloads glTF data.
@@ -16,18 +16,17 @@ module BABYLON {
             * Checks for a matching suffix at the end of a string (for ES5 and lower)
             * @param str 
             * @param suffix 
-            * 
-            * @returns {boolean} indicating whether the suffix matches or not
+            * @returns - indicating whether the suffix matches or not
             */
             function endsWith(str: string, suffix: string): boolean {
                 return str.indexOf(suffix, str.length - suffix.length) !== -1;
             }
-            for (let key in this._glTFFiles) {
+            for (let key in this.glTFFiles) {
                 let link = document.createElement('a');
                 document.body.appendChild(link);
                 link.setAttribute("type", "hidden");
                 link.download = key;
-                let blob = this._glTFFiles[key];
+                let blob = this.glTFFiles[key];
                 let mimeType;
 
                 if (endsWith(key, ".glb")) {

File diff suppressed because it is too large
+ 610 - 155
serializers/src/glTF/2.0/babylon.glTFExporter.ts


+ 114 - 0
serializers/src/glTF/2.0/babylon.glTFMaterial.ts

@@ -0,0 +1,114 @@
+module BABYLON {
+    /**
+     * Utility methods for working with glTF material conversion properties
+     */
+    export class _GLTFMaterial {
+        private static dielectricSpecular = new Color3(0.04, 0.04, 0.04);
+        private static epsilon = 1e-6;
+
+        /**
+         * Converts Specular Glossiness to Metallic Roughness
+         * @param  babylonSpecularGlossiness - Babylon specular glossiness parameters
+         * @returns - Babylon metallic roughness values
+         */
+        public static ConvertToMetallicRoughness(babylonSpecularGlossiness: _IBabylonSpecularGlossiness): _IBabylonMetallicRoughness {
+            const diffuse = babylonSpecularGlossiness.diffuse;
+            const opacity = babylonSpecularGlossiness.opacity;
+            const specular = babylonSpecularGlossiness.specular;
+            const glossiness = babylonSpecularGlossiness.glossiness;
+            
+            const oneMinusSpecularStrength = 1 - Math.max(specular.r, Math.max(specular.g, specular.b));
+            const diffusePerceivedBrightness = _GLTFMaterial.PerceivedBrightness(diffuse);
+            const specularPerceivedBrightness = _GLTFMaterial.PerceivedBrightness(specular);
+            const metallic = _GLTFMaterial.SolveMetallic(diffusePerceivedBrightness, specularPerceivedBrightness, oneMinusSpecularStrength);
+            const diffuseScaleFactor = oneMinusSpecularStrength/(1 - this.dielectricSpecular.r) / Math.max(1 - metallic, this.epsilon);
+            const baseColorFromDiffuse = diffuse.scale(diffuseScaleFactor);
+            const baseColorFromSpecular = specular.subtract(this.dielectricSpecular.scale(1 - metallic)).scale(1/ Math.max(metallic, this.epsilon));
+            const lerpColor = Color3.Lerp(baseColorFromDiffuse, baseColorFromSpecular, metallic * metallic);
+            let baseColor = new Color3();
+            lerpColor.clampToRef(0, 1, baseColor);
+
+            const babylonMetallicRoughness: _IBabylonMetallicRoughness = {
+                baseColor: baseColor,
+                opacity: opacity,
+                metallic: metallic,
+                roughness: 1.0 - glossiness
+            };
+
+            return babylonMetallicRoughness;
+        }
+
+        /**
+         * Returns the perceived brightness value based on the provided color
+         * @param color - color used in calculating the perceived brightness
+         * @returns - perceived brightness value
+         */
+        private static PerceivedBrightness(color: Color3): number {
+            return Math.sqrt(0.299 * color.r * color.r + 0.587 * color.g * color.g + 0.114 * color.b * color.b);
+        }
+
+        /**
+         * Computes the metallic factor
+         * @param diffuse - diffused value
+         * @param specular - specular value
+         * @param oneMinusSpecularStrength - one minus the specular strength
+         * @returns - metallic value
+         */
+        public static SolveMetallic(diffuse: number, specular: number, oneMinusSpecularStrength: number): number {
+            if (specular < this.dielectricSpecular.r) {
+                return 0;
+            }
+
+            const a = this.dielectricSpecular.r;
+            const b = diffuse * oneMinusSpecularStrength /(1.0 - this.dielectricSpecular.r) + specular - 2.0 * this.dielectricSpecular.r;
+            const c = this.dielectricSpecular.r - specular;
+            const D = b * b - 4.0 * a * c;
+            return BABYLON.Scalar.Clamp((-b + Math.sqrt(D))/(2.0 * a));
+        }
+        
+        /**
+         * Gets the glTF alpha mode from the Babylon Material
+         * @param babylonMaterial - Babylon Material
+         * @returns - The Babylon alpha mode value
+         */
+        public static GetAlphaMode(babylonMaterial: Material): string {
+            if (babylonMaterial instanceof StandardMaterial) {
+                const babylonStandardMaterial = babylonMaterial as StandardMaterial;
+                if ((babylonStandardMaterial.alpha != 1.0) || 
+                    (babylonStandardMaterial.diffuseTexture != null && babylonStandardMaterial.diffuseTexture.hasAlpha) ||
+                    (babylonStandardMaterial.opacityTexture != null)) {
+                    return  _EGLTFAlphaModeEnum.BLEND;
+                }
+                else {
+                    return _EGLTFAlphaModeEnum.OPAQUE;
+                }
+            }
+            else if (babylonMaterial instanceof PBRMetallicRoughnessMaterial) {
+                const babylonPBRMetallicRoughness = babylonMaterial as PBRMetallicRoughnessMaterial;
+
+                switch(babylonPBRMetallicRoughness.transparencyMode) {
+                    case PBRMaterial.PBRMATERIAL_OPAQUE: {
+                        return _EGLTFAlphaModeEnum.OPAQUE;
+                    }
+                    case PBRMaterial.PBRMATERIAL_ALPHABLEND: {
+                        return _EGLTFAlphaModeEnum.BLEND;
+                    }
+                    case PBRMaterial.PBRMATERIAL_ALPHATEST: {
+                        return _EGLTFAlphaModeEnum.MASK;
+                    }
+                    case PBRMaterial.PBRMATERIAL_ALPHATESTANDBLEND: {
+                        console.warn("GLTF Exporter | Alpha test and blend mode not supported in glTF.  Alpha blend used instead.");
+                        return _EGLTFAlphaModeEnum.BLEND;
+                    }
+                    default: {
+                        throw new Error("Unsupported alpha mode " + babylonPBRMetallicRoughness.transparencyMode);
+                    }
+                }
+            }
+            else {
+                throw new Error("Unsupported Babylon material type");
+            }   
+        }
+    }
+
+}

+ 32 - 12
serializers/src/glTF/2.0/babylon.glTFSerializer.ts

@@ -1,34 +1,54 @@
 /// <reference path="../../../../dist/preview release/babylon.d.ts"/>
 
 module BABYLON {
+    export interface IGLTFExporterOptions {
+        /**
+         * Interface function which indicates whether a babylon mesh should be exported or not.
+         * @param mesh
+         * @returns boolean, which indicates whether the mesh should be exported (true) or not (false)
+         */
+        shouldExportMesh?(mesh: AbstractMesh): boolean;
+    };
     export class GLTF2Export {
         /**
          * Exports the geometry of a Mesh array in .gltf file format.
-         * If glb is set to true, exports as .glb.
-         * @param meshes 
+         * @param meshes  
          * @param materials 
+         * @param options
          * 
-         * @returns {[fileName: string]: string | Blob} Returns an object with a .gltf, .glb and associates textures
+         * @returns - Returns an object with a .gltf, .glb and associates textures
          * as keys and their data and paths as values.
          */
-        public static GLTF(scene: BABYLON.Scene, filename: string): _GLTFData {
-            let glTFPrefix = filename.replace(/\.[^/.]+$/, "");
-            let gltfGenerator = new _GLTF2Exporter(scene);
+        public static GLTF(scene: Scene, filename: string, options?: IGLTFExporterOptions): _GLTFData {
+            const glTFPrefix = filename.replace(/\.[^/.]+$/, "");
+            const gltfGenerator = new _GLTF2Exporter(scene, options);
+            if (scene.isReady) {
+                return gltfGenerator._generateGLTF(glTFPrefix);
+            }
+            else {
+                throw new Error("glTF Serializer: Scene is not ready!");
+            }
 
-            return gltfGenerator._generateGLTF(glTFPrefix);
+            
         }
         /**
          * 
          * @param meshes 
          * @param filename 
          * 
-         * @returns {[fileName: string]: string | Blob} Returns an object with a .glb filename as key and data as value
+         * @returns - Returns an object with a .glb filename as key and data as value
          */
-        public static GLB(scene: BABYLON.Scene, filename: string): _GLTFData {
-            let glTFPrefix = filename.replace(/\.[^/.]+$/, "");        
-            let gltfGenerator = new _GLTF2Exporter(scene);
+        public static GLB(scene: Scene, filename: string, options?: IGLTFExporterOptions): _GLTFData {
+            const glTFPrefix = filename.replace(/\.[^/.]+$/, "");        
+            const gltfGenerator = new _GLTF2Exporter(scene, options);
+            if (scene.isReady) {
+                return gltfGenerator._generateGLB(glTFPrefix);
+            }
+            else {
+                throw new Error("glTF Serializer: Scene is not ready!");
+            }
 
-            return gltfGenerator._generateGLB(glTFPrefix);
+            
         }
     }
 }

+ 31 - 0
src/Math/babylon.math.ts

@@ -126,6 +126,21 @@
         }
 
         /**
+         * Clamps the rgb values by the min and max values and stores the result into "result".
+         * Returns the unmodified current Color3.
+         * @param min - minimum clamping value.  Defaults to 0
+         * @param max - maximum clamping value.  Defaults to 1
+         * @param result - color to store the result into.
+         * @returns - the original Color3
+         */
+        public clampToRef(min: number = 0, max: number = 1, result: Color3): Color3 {
+            result.r = BABYLON.Scalar.Clamp(this.r, min, max);
+            result.g = BABYLON.Scalar.Clamp(this.g, min, max);
+            result.b = BABYLON.Scalar.Clamp(this.b, min, max);
+            return this;
+        }
+
+        /**
          * Returns a new Color3 set with the added values of the current Color3 and of the passed one.  
          */
         public add(otherColor: Color3): Color3 {
@@ -389,6 +404,22 @@
         }
 
         /**
+         * Clamps the rgb values by the min and max values and stores the result into "result".
+         * Returns the unmodified current Color4.
+         * @param min - minimum clamping value.  Defaults to 0
+         * @param max - maximum clamping value.  Defaults to 1
+         * @param result - color to store the result into.
+         * @returns - the original Color4
+         */
+        public clampToRef(min: number = 0, max: number = 1, result: Color4): Color4 {
+            result.r = BABYLON.Scalar.Clamp(this.r, min, max);
+            result.g = BABYLON.Scalar.Clamp(this.g, min, max);
+            result.b = BABYLON.Scalar.Clamp(this.b, min, max);
+            result.a = BABYLON.Scalar.Clamp(this.a, min, max);
+            return this;
+        }
+
+        /**
           * Multipy an RGBA Color4 value by another and return a new Color4 object
           * @param color The Color4 (RGBA) value to multiply by
           * @returns A new Color4.

+ 1 - 0
tests/unit/babylon/babylonReference.ts

@@ -1,5 +1,6 @@
 /// <reference path="../../../dist/babylon.d.ts" />
 /// <reference path="../../../dist/loaders/babylon.glTF2FileLoader.d.ts" />
+/// <reference path="../../../dist/preview release/serializers/babylon.glTF2Serializer.d.ts" />
 
 /// <reference path="../node_modules/@types/chai/index.d.ts" />
 /// <reference path="../node_modules/@types/mocha/index.d.ts" />

+ 167 - 0
tests/unit/babylon/serializers/babylon.glTFSerializer.tests.ts

@@ -0,0 +1,167 @@
+/**
+ * Describes the test suite
+ */
+describe('Babylon glTF Serializer', () => {
+    let subject: BABYLON.Engine;
+
+    /**
+     * Loads the dependencies
+     */
+    before(function (done) {
+        this.timeout(180000);
+        (BABYLONDEVTOOLS).Loader
+            .useDist()
+            .load(function () {
+                done();
+            });
+    });
+
+    /**
+     * Create a null engine subject before each test.
+     */
+    beforeEach(function () {
+        subject = new BABYLON.NullEngine({
+            renderHeight: 256,
+            renderWidth: 256,
+            textureSize: 256,
+            deterministicLockstep: false,
+            lockstepMaxSteps: 1
+        });
+    });
+
+    /**
+     * This tests the glTF serializer help functions 
+     */
+    describe('#GLTF', () => {
+        it('should get alpha mode from Babylon metallic roughness', () => {
+            let alphaMode: string;
+
+            const scene = new BABYLON.Scene(subject);
+            const babylonMaterial = new BABYLON.PBRMetallicRoughnessMaterial("metallicroughness", scene);
+            babylonMaterial.transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_OPAQUE;
+            
+            alphaMode = BABYLON._GLTFMaterial.GetAlphaMode(babylonMaterial);
+            alphaMode.should.be.equal('OPAQUE');
+            
+            babylonMaterial.transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND;
+            alphaMode = BABYLON._GLTFMaterial.GetAlphaMode(babylonMaterial);
+            alphaMode.should.be.equal('BLEND');
+
+            babylonMaterial.transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_ALPHATESTANDBLEND;
+            alphaMode = BABYLON._GLTFMaterial.GetAlphaMode(babylonMaterial);
+            alphaMode.should.be.equal('BLEND');
+
+            babylonMaterial.transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_ALPHATEST;
+            alphaMode = BABYLON._GLTFMaterial.GetAlphaMode(babylonMaterial);
+            alphaMode.should.be.equal('MASK'); 
+        });
+        it('should convert Babylon standard material to metallic roughness', () => {
+            const scene = new BABYLON.Scene(subject);
+            const babylonStandardMaterial = new BABYLON.StandardMaterial("specGloss", scene);
+            babylonStandardMaterial.diffuseColor = BABYLON.Color3.White();
+            babylonStandardMaterial.specularColor = BABYLON.Color3.Black();
+
+            const specGloss: BABYLON._IBabylonSpecularGlossiness = {
+                diffuse: BABYLON.Color3.White(),
+                specular: BABYLON.Color3.Black(),
+                glossiness: 0.25,
+                opacity: 1.0
+            };
+            const metalRough = BABYLON._GLTFMaterial.ConvertToMetallicRoughness(specGloss);
+
+            metalRough.baseColor.equals(new BABYLON.Color3(1, 1, 1)).should.be.equal(true);
+
+            metalRough.metallic.should.be.equal(0);
+            
+            metalRough.roughness.should.be.equal(0.75);
+            
+            metalRough.opacity.should.be.equal(1);
+        });
+        it('should solve for metallic', () => {
+            BABYLON._GLTFMaterial.SolveMetallic(1.0, 0.0, 1.0).should.be.equal(0);
+            BABYLON._GLTFMaterial.SolveMetallic(0.0, 1.0, 1.0).should.be.approximately(1, 1e-6);
+        });
+        it('should serialize empty Babylon scene to glTF with only asset property', (done) => {
+            mocha.timeout(10000);
+
+            const scene = new BABYLON.Scene(subject);
+            scene.executeWhenReady(function () {
+                const glTFExporter = new BABYLON._GLTF2Exporter(scene);
+                const glTFData = glTFExporter._generateGLTF('test');
+                const jsonString = glTFData.glTFFiles['test.gltf'] as string;
+                const jsonData = JSON.parse(jsonString);
+
+                Object.keys(jsonData).length.should.be.equal(1);
+                jsonData.asset.version.should.be.equal("2.0");
+                jsonData.asset.generator.should.be.equal("BabylonJS");
+
+                done();
+            });
+        });
+        it('should serialize sphere geometry in scene to glTF', (done) => {
+            mocha.timeout(10000);
+            const scene = new BABYLON.Scene(subject);
+            BABYLON.Mesh.CreateSphere('sphere', 16, 2, scene);
+
+            scene.executeWhenReady(function () {
+                const glTFExporter = new BABYLON._GLTF2Exporter(scene);
+                const glTFData = glTFExporter._generateGLTF('test');
+                const jsonString = glTFData.glTFFiles['test.gltf'] as string;
+                const jsonData = JSON.parse(jsonString);
+
+                // accessors, asset, buffers, bufferViews, meshes, nodes, scene, scenes, 
+                Object.keys(jsonData).length.should.be.equal(8);
+
+                // positions, normals, texture coords, indices
+                jsonData.accessors.length.should.be.equal(4);
+
+                // generator, version
+                Object.keys(jsonData.asset).length.should.be.equal(2);
+
+                jsonData.buffers.length.should.be.equal(1);
+
+                // positions, normals, texture coords, indices
+                jsonData.bufferViews.length.should.be.equal(4);
+
+                jsonData.meshes.length.should.be.equal(1);
+
+                jsonData.nodes.length.should.be.equal(1);
+
+                jsonData.scenes.length.should.be.equal(1);
+
+                jsonData.scene.should.be.equal(0);
+
+                done();
+            });
+        });
+        it('should serialize alpha mode and cutoff', (done) => {
+            mocha.timeout(10000);
+            const scene = new BABYLON.Scene(subject);
+
+            const plane = BABYLON.Mesh.CreatePlane('plane', 120, scene);
+            const babylonPBRMetalRoughMaterial = new BABYLON.PBRMetallicRoughnessMaterial('metalRoughMat', scene);
+            babylonPBRMetalRoughMaterial.transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND;
+            const alphaCutoff = 0.8;
+            babylonPBRMetalRoughMaterial.alphaCutOff = alphaCutoff;
+
+            plane.material = babylonPBRMetalRoughMaterial;
+
+            scene.executeWhenReady(function () {
+                const glTFExporter = new BABYLON._GLTF2Exporter(scene);
+                const glTFData = glTFExporter._generateGLTF('test');
+                const jsonString = glTFData.glTFFiles['test.gltf'] as string;
+                const jsonData = JSON.parse(jsonString);
+
+                Object.keys(jsonData).length.should.be.equal(9);
+
+                jsonData.materials.length.should.be.equal(1);
+                
+                jsonData.materials[0].alphaMode.should.be.equal('BLEND');
+                
+                jsonData.materials[0].alphaCutoff.should.be.equal(alphaCutoff);
+                
+                done();
+            });
+        });
+    });
+});