GLTF Parser Hooks

I don’t think you can emulate it without deriving from StandardMaterial and overriding the needAlpha* functions to mimic what pbrBaseMaterial is doing.

Do you think you can show me what an overridden StandardMaterial needsAlpha need to be doing to mimic PBRBaseMaterial Alpha… I guess otherwise ALL legacy materials will to be fully OPAQUE ONLY… and that kinda sucks.

Look at the code starting here: Babylon.js/pbrBaseMaterial.ts at master · BabylonJS/Babylon.js · GitHub

I will take a look at ad see what i can come up with for support alpha on a StandardMaterial

To start, in the Exporter… i use RENDERTYPE to setup PBR OPAQUE, MASK AND BLEND alpha modes… For a LEGACY material i will use texture.alphaIsTransparency and if material has a cutoff to determine the alpha setup i will try to use in babylon… If texture.alphaIsTranspaency = false… will be OPAQUE… if texture.alphaIsTranspency = true and has cutoff… will be MASK and if texture.alphaIsTranspaency = true and NO CUTOFF will be BLEND

I will try to RIGG UP alpha setting for StandardMaterial as best i can… And again bro…Thanks for all your guidance :slight_smile:

Yo @bghgary and @Deltakosh … this is how im FAKING alpha mode in exporting LEGACY StandardMaterial from unity via GLTF Material Fallback in my CVTOOLS_unity_metadata extenstion:

In Unity i Export the Alpha Mode used for BOTH PBR and Standard based materials:

			bool hasCutoff = false;
            if (materialObj.HasProperty("_Cutoff"))
			{
				hasCutoff = true;
				material.AlphaCutoff = materialObj.GetFloat("_Cutoff");
			}

			string materialRenderType = materialObj.GetTag("RenderType", false, "");
			if (legacyMaterial == false && !String.IsNullOrEmpty(materialRenderType))
			{
				// Note: PBRMaterial Alpha Modes
				switch (materialRenderType)
				{
					case "TransparentCutout":
						material.AlphaMode = AlphaMode.MASK;
						break;
					case "Transparent":
						material.AlphaMode = AlphaMode.BLEND;
						break;
					default:
						material.AlphaMode = AlphaMode.OPAQUE;
						break;
				}
			}
			else
			{
				// Note: StandardMaterial Alpha Modes
				if (materialObj.mainTexture != null)
				{
					Texture2D mainTex = materialObj.mainTexture as Texture2D;
					if (mainTex.alphaIsTransparency == true) {
						if (hasCutoff == true) {
							material.AlphaMode = AlphaMode.MASK;
						} else {
							material.AlphaMode = AlphaMode.BLEND;
						}
					} else {
						material.AlphaMode = AlphaMode.OPAQUE;
					}
				}
				else
				{
					// Default: Base Color Alpha Blending
					material.AlphaMode = AlphaMode.BLEND;
				}
			}

That setup up alpha mode for the export… then on parsing the GLTF in babylon in the loadMaterialPropertiesAsync… if babylonMaterial instanceof BABYLON.StandardMaterial (returned from createMaterial)

then i set the alpha blending like so:

        // ALPHA PROPERTIES (Needs Blend Testing)

        /**
         * Specifies if the material will require alpha blending
         * @returns a boolean specifying if alpha blending is needed
         */
        //public needAlphaBlending(): boolean {
        //    return (this.alpha < 1.0) || (this._opacityTexture != null) || this._shouldUseAlphaFromDiffuseTexture() || this._opacityFresnelParameters && this._opacityFresnelParameters.isEnabled;
        //}
        /**
         * Specifies if this material should be rendered in alpha test mode
         * @returns a boolean specifying if an alpha test is needed.
         */
        //public needAlphaTesting(): boolean {
        //    return this._diffuseTexture != null && this._diffuseTexture.hasAlpha;
        //}
        //protected _shouldUseAlphaFromDiffuseTexture(): boolean {
        //    return this._diffuseTexture != null && this._diffuseTexture.hasAlpha && this._useAlphaFromDiffuseTexture;
        //}        

        babylonMaterial.alpha = baseColorAlpha;             // Note: Default Base Color Alpha
        const alphaMode = material.alphaMode || BABYLON.GLTF2.MaterialAlphaMode.OPAQUE;
        switch (alphaMode) {
            case BABYLON.GLTF2.MaterialAlphaMode.OPAQUE: {  // Note: Normal-Mode (OPAQUE)
                babylonMaterial.alpha = 1.0;                // Note: Reset Alpha To Opaque
                break;
            }
            case BABYLON.GLTF2.MaterialAlphaMode.MASK: {    // Note: Transparency-Cutout (ALPHATEST)
                babylonMaterial.alphaCutOff = (material.alphaCutoff == undefined ? 0.5 : material.alphaCutoff);
                if (babylonMaterial.diffuseTexture) {
                    babylonMaterial.alpha = 1.0;            // Note: Use Alpha From Texture
                    babylonMaterial.diffuseTexture.hasAlpha = true;
                }
                break;
            }
            case BABYLON.GLTF2.MaterialAlphaMode.BLEND: {   // Note: Transparency (ALPHABLEND)
                if (babylonMaterial.diffuseTexture) {
                    babylonMaterial.alpha = 1.0;            // Note: Use Alpha From Texture
                    babylonMaterial.diffuseTexture.hasAlpha = true;
                    babylonMaterial.useAlphaFromDiffuseTexture = true;
                }
                break;
            }
            default: {
                throw new Error(`${context}/alphaMode: Invalid value (${material.alphaMode})`);
            }
        }

It seems to be working on my part… do you guys see anything wrong the StandardMaterial Alpha Setup ???

I don’t see anything wrong, but we’ve had some bugs in this area for glTF PBR materials before.

Yo @bghgary … got a question about the GLTF Extension Loader…

If a gltf DOES NOT have my CVTOOLS_unity_metadata or CVTOOLS_left_handedness in the extensionUsed and/or extensionRequired section of the scene json. Does Babylon still load the extension… Or does it not even load and parse the scene with my extension if the extension name is NOT in the scene file ???

Currently, the loader doesn’t check the extensionsUsed property. It does check the extensionsRequired property to make sure the extension is enabled/exists.

Yeah i eventually figured that one out…, Soi end up using flags that get set in onloading that enable PARSING and LEFT-HAND support if the extension used:

    public onLoading(): void {
        this._parseScene = (this._loader.gltf != null && this._loader.gltf.extensionsUsed != null && this._loader.gltf.extensionsUsed.indexOf("CVTOOLS_unity_metadata") >= 0);
        this._leftHanded = (this._loader.gltf != null && this._loader.gltf.extensionsUsed != null && this._loader.gltf.extensionsUsed.indexOf("CVTOOLS_left_handedness") >= 0);
        if (this._parseScene === true) {
            this._physicsList = [];
            this._shadowList = [];
            this._scriptList = [];
            this._sceneLoaded = false;
            this._disposeRoot = false;
        }
        if (this._leftHanded === true && this._loader.rootBabylonMesh != null) { // Note: Force Left Handed System
            this._loader.rootBabylonMesh.rotationQuaternion = BABYLON.Quaternion.Identity();
            this._loader.rootBabylonMesh.scaling = BABYLON.Vector3.One();
        }
    }    

    public onReady(): void {
        if (this._parseScene === true) {
            if (this._disposeRoot === true && this._loader.rootBabylonMesh != null) this._loader.rootBabylonMesh.dispose(true);
            this._physicsList = null;
            this._shadowList = null;
            this._scriptList = null;
        }
    }    

Works beautifully :slight_smile:

Yo @bghgary … I got a situation where a material is KHR PBR Specular Glossiness.

For those materials my loadMaterialPropertiesAsync NEVER runs. I still need to be able to set some properties on a PBR … even if its a specular pbr… How can i catch the loadMaterialPropertiesAsync if the KHR specular extension BLOCK the rest of the extensions from running ?

i notice other extension using a LoadExtensionAsync and the code is wrapped in that. Example:

public loadMaterialPropertiesAsync(context: string, material: IMaterial, babylonMaterial: Material): Nullable<Promise<void>> {
        return GLTFLoader.LoadExtensionAsync<IKHRMaterialsPbrSpecularGlossiness>(context, material, this.name, (extensionContext, extension) => {
            const promises = new Array<Promise<any>>();
            promises.push(this._loader.loadMaterialBasePropertiesAsync(context, material, babylonMaterial));
            promises.push(this._loadSpecularGlossinessPropertiesAsync(extensionContext, material, extension, babylonMaterial));
            this._loader.loadMaterialAlphaProperties(context, material, babylonMaterial);
            return Promise.all(promises).then(() => { });
        });
    }

But my loadMaterialPropertiesAsync i DO NO call LoadExtensionAsync. Example:

    public loadMaterialPropertiesAsync(context: string, material: BABYLON.GLTF2.Loader.IMaterial, babylonMaterial: BABYLON.Material): BABYLON.Nullable<Promise<void>> {
        if (this._parseScene === true) {
            console.warn("CVTOOLS: LoadMaterialPropertiesAsync: " + material.name);
            if (babylonMaterial instanceof BABYLON.PBRMaterial) return this._parseDefaultMaterialPropertiesAsync(context, material, babylonMaterial);
            else if (babylonMaterial instanceof BABYLON.ShaderMaterial) return this._parseShaderMaterialPropertiesAsync(context, material, babylonMaterial);
            else return this._parseCustomMaterialPropertiesAsync(context, material, babylonMaterial)
        } else {
            return null; // Not Handled
        }
    }


    private _parseDefaultMaterialPropertiesAsync(context: string, material: BABYLON.GLTF2.Loader.IMaterial, sourceMaterial: BABYLON.Material): BABYLON.Nullable<Promise<void>> {
        //console.warn("CVTOOLS: ParseDefaultMaterialPropertiesAsync: " + material.name);
        const promises = new Array<Promise<any>>();
        promises.push(this._loader.loadMaterialPropertiesAsync(context, material, sourceMaterial));
        this._parseCommonConstantProperties(promises, context, material, sourceMaterial);
        return Promise.all(promises).then(() => { });
    }

I need my _parseCommonConstantProperties to run on ALL MATERIALS to add some extra properties from unity to it. I call that from MY loadMaterialPropertiesAsync function

How can i get around this :frowning:

This one is interesting. Both the KHR_materials_unlit and KHR_materials_pbrSpecularGlossiness extension override and handle the their respective extension such that only one of them “wins” over the default metallic/roughness implementation.

Right now, I’m thinking the way to handle this is to ensure that your extension is registered before these two extensions so that you can continue calling the loader’s default implementation which will continue checking for additional extensions. But this isn’t easy to do with the way the loader extensions are set up.

I’ll have to think about this more.

I saw that… i went and checked all the extension and PBR specular and unlit are the only ones that DO NOT continue the material chain by calling this._loader.loadMaterialPropertiesAsync

I was thinking about NOT encoding unlit and pbr specular as KHR extensions but instead encode those two under my material.extras.metadata and just parsing those two myself.

Only thing that would suck if using that GLTF OUTSIDE of the babylon eco-system EVERYTHING would just be regular PBR metallic roughness since it would not have my CVTOOLS_unity_metadata extension like babylon does :frowning:

Hey @bghgary … Im trying to give one last shot on loading script dependencies from the loadSceneAsync … But i keep getting a recursive error on the the load scene node .

I trie these load steps…

If i am parsing the scene and i need to load a script tag i call InjectScriptAsync then Parse my scene properties and then return the base loader.loadSceneAsync so it can parse all the nodes… I try and catch error and still parse scene properties and return the base loader.loadSceneAsync

if i am not handling the scene i just return null

anyways… can you take a look at this… It seems logical to me, but the way the loadSceneAsync
my custom loadSceneAsync get called twice

    /** @hidden */
    public loadSceneAsync(context: string, scene: BABYLON.GLTF2.Loader.IScene): BABYLON.Nullable<Promise<void>> {
        if (this._parseScene === true && scene.extras != null && scene.extras.metadata != null) {
            console.warn("CVTOOLS: LoadSceneAsync: " + scene.name);
            const metadata:any = scene.extras.metadata;
            let scriptid:string = null;
            let projectjs:string = null;
            let hasscript:boolean = false;
            if (document != null && metadata.script != null && metadata.script !== "" && metadata.project != null && metadata.project !== "")  {
                scriptid = metadata.script;
                projectjs = metadata.project;
                hasscript = (document.getElementById(scriptid) != null);
            }
            let loadscript:boolean = (scriptid != null && projectjs != null && hasscript === false);
            if (loadscript === true) {
                return BABYLON.SceneManager.InjectScriptAsync(scriptid, projectjs).then(() => {
                    this._parseSceneProperties(context, scene);
                    return this._loader.loadSceneAsync(context, scene);
                }).catch(() => { 
                    this._parseSceneProperties(context, scene);
                    return this._loader.loadSceneAsync(context, scene);
                });
            } else {
                this._parseSceneProperties(context, scene);
                return this._loader.loadSceneAsync(context, scene);    
            }
        } else {
            return null; // Not Handled
        }
    }

Thanks Gary As Always :slight_smile:

Sorry, somehow missed replying to this. I don’t see anything wrong with this either. Can you repro in a playground?

I’m thinking about exposing the extension name order list so that you can rearrange it as you see fit.

That would be sweet.

I don’t know how i would make it in the playground without using the Babylon GLTFLoader typescript classes.

this is how i am loading now and it works but i have to hijack the loadSceneAsync.

    /** @hidden */
    public loadSceneAsync(context: string, scene: BABYLON.GLTF2.Loader.IScene): BABYLON.Nullable<Promise<void>> {
        if (this._parseScene === true && scene.extras != null && scene.extras.metadata != null) {
            console.warn("CVTOOLS: LoadSceneAsync: " + scene.name);
            // Note: We Are Hijacking The LoadSceneAsync
            const metadata:any = scene.extras.metadata;
            let scriptid:string = null;
            let projectjs:string = null;
            if (document != null && metadata.script != null && metadata.script !== "" && metadata.project != null && metadata.project !== "")  {
                scriptid = metadata.script;
                projectjs = this._rootUrl + metadata.project;
            }
            return BABYLON.SceneManager.InjectScriptAsync(scriptid, projectjs, true).then(() => {
                this._parseSceneProperties(context, scene);
                return this._finishLoadSceneAsync(context, scene);
            }).catch(() => { 
                this._parseSceneProperties(context, scene);
                return this._finishLoadSceneAsync(context, scene);
            });
        } else {
            return null; // Not Handled
        }
    }

    private _finishLoadSceneAsync(context: string, scene: BABYLON.GLTF2.Loader.IScene): Promise<void> {
        const promises = new Array<Promise<any>>();

        this._loader.logOpen(`${context} ${scene.name || ""}`);

        if (scene.nodes) {
            for (let index of scene.nodes) {
                const node = BABYLON.GLTF2.ArrayItem.Get(`${context}/nodes/${index}`, this._loader.gltf.nodes, index);
                const xnode:any = node;
                promises.push(this._loader.loadNodeAsync(`/nodes/${xnode.index}`, xnode, (babylonMesh) => {
                    babylonMesh.parent = this._loader.rootBabylonMesh;
                }));
            }
        }

        // Link all Babylon bones for each glTF node with the corresponding Babylon transform node.
        // A glTF joint is a pointer to a glTF node in the glTF node hierarchy similar to Unity3D.
        if (this._loader.gltf.nodes) {
            for (const node of this._loader.gltf.nodes) {
                const xnode:any = node;
                if (xnode._babylonTransformNode && xnode._babylonBones) {
                    for (const babylonBone of xnode._babylonBones) {
                        babylonBone.linkTransformNode(xnode._babylonTransformNode);
                    }
                }
            }
        }

        promises.push((<any>this._loader)._loadAnimationsAsync());

        this._loader.logClose();

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

Here is the problem… if a return FAKE promise from my loadSceneAsync like im doing with the InjectScriptAsync… I CANNOT call the this.loader.loadSceneAsync to continue the scene loading chain. If i do… it calls my extension loadSceneAsync AGAIN and causes a recursive load node error in gltf.

my custom finishLoadSceneAsync i just copied GLTFLoader loadSceneAsync and removed the extension loading stuff at the top so it continues parsing all the nodes.

Its a hack, but it works… I would have to hijack the load scene async process though… only MSFT audio emitter uses loadSceneAsync so far and i don’t encode that extension in my gltf (SO FAR).

Can you see how the loader loadSceneAsync is calling my loadSceneAsync because i am return that fake inject script promise and trying to continue the load procsses after the script as loaded or failed.

Oh I see what the problem is. I should have caught this earlier. Sorry about that. I have a guard against re-entrancy but it is a synchronous guard (i.e. it will prevent the extension from being called multiple times within the same call). If you call the loader’s loadSceneAsync asychronously, then the guard will not work and it is also incorrect usage. The loader’s loadSceneAsync must be called synchronously. You just need to fix your code such that the loader’s loadSceneAsync is call outside of the promise chain.

I need to call the loader loadSceneAsync AFTER the inject script loads or fails… Where would i put the synchronous call to loadSceneAsync if NOT in the THEN of InjectScriptAsync ???

I don’t understand your scenario. What does the script do? If you need to load a script dynamically before processing the json, then I might have to add a hook for this. Is there any reason not to load the script before calling SceneLoader functions?