Unable to load glb model locally and I am confused by the import/plugin documentation

This is a long post, so I bolded some “headers” with the following breakdown:

  • Intro & GitHub Repo
  • Issue & Goal
  • Code Blocks
  • The Actual Questions

Intro plus Project Repo

This is my first Babylon.js project, and I am making things more difficult for myself. I am working in AspNet with Blazor, and all of what I am doing with Babylon.js is through JSInterop… Which is leading to some confusion with documentation as I am needing to do some extra work to figure out what needs to happen to integrate JavaScript functions and features… And I also can’t replicate my issue in the playground for the same reason. Apologies for that.

My current project is available on GitHub here: BabylonBlazorTest

It is building on an open-source project that is already getting Babylon to work in .Net Blazor, which can be found here: BabylonBlazor. The changes I have made so far can be identified by my commits as opposed to Alex’s.

That being said, here is my issue:

For anyone testing or working with the project, it is using the BabylonBlazor.App project as the startup project, with the relevant webpage being SingleModel.razor found within the BabylonBlazor.AppShared project (weird Blazor Server/Client stuff).

I need to be able to load in a 3D model for rendering. The end goal would be to have a live application with the ability to browse for local files or files stored on a separate compute within the network, and load them in for viewing. Currently, I am just testing a render of a single model stored in the root/3DModels/ directory. That is failing with a “404 Unable to Load” error:

***Exception:Microsoft.JSInterop.JSException: Error status: 404  - Unable to load ./3DModels/leather_armchair.glb
LoadFileError: Error status: 404  - Unable to load ./3DModels/leather_armchair.glb
    at t [as constructor] (https://cdn.babylonjs.com/babylon.js:1:626360)
    at t [as constructor] (https://cdn.babylonjs.com/babylon.js:1:626712)
    at new t (https://cdn.babylonjs.com/babylon.js:1:627449)
    at https://cdn.babylonjs.com/babylon.js:1:632173
    at XMLHttpRequest.e (https://cdn.babylonjs.com/babylon.js:1:633548)
   at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, Object[] args)
   at Babylon.Blazor.Babylon.Scene.Create3DAsset(String filepath) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\Babylon\Scene.cs:line 229
   at Babylon.Blazor.ModelCreator.CreateAsync(BabylonCanvasBase canvas) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\ModelCreator.cs:line 44
   at Babylon.Blazor.BabylonCanvasBase.InitializeSzene(BabylonInstance babylonInstance, String canvasId) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\BabylonCanvasBase.cs:line 47
   at Babylon.Blazor.BabylonCanvasBase.OnAfterRenderAsync(Boolean firstRender) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\BabylonCanvasBase.cs:line 105

I initially followed the documentation for “Loading Assets From Memory”, which is causing the 404 error. I then tried an alternative BABYLON.AppendSceneAsync() function as described in “Loading Any File Type”, but that is giving me an “addPendingData is not a function” error related to JSInterop.

I am not sure whether my issue is related to not understanding how to get the gltf loader plugin to work within this kind of project architecture, if it is how I am using the Babylon functions, or an issue with my JSInterop stuff. I am new to all of this, but I want to see if I could get this to work.

Here are some code blocks going through the steps to render the web page and scene:

The script imports are done within “App.razor” and are loaded in for every web page as a result:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BabylonBlazorApp</title>
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link href="/css/open-iconic/font/css/open-iconic-bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="BabylonBlazor.App.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>

<body>
    <Routes />
    <script src="https://cdn.babylonjs.com/babylon.js"></script>
    <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
    <script src="https://cdn.babylonjs.com/loaders/babylon.glTFFileLoader.js"></script>
    <script type="module" src="_content/Babylon.Blazor/babylonInterop.js"></script>
    <script src="_framework/blazor.web.js"></script>
</body>

</html>

“SingleModel.razor” is the component (or web page) that should be loading the scene:

@page "/model"
@using Babylon.Blazor
@* @rendermode InteractiveWebAssembly *@
@rendermode InteractiveAuto

<h3>Armchair</h3>
<p>What a Nice Leather Armchair!</p>

@* The image tag below was to test that it could route to where files were stored.  Not Needed. *@
@* <img src="./3DModels/poster.webp" alt="Leather Armchair" width="500" height="600" /> *@

<div style="height: 600px;">
    <BabylonCanvas CanvasId="Canvas3" ModelFilepath=@ModelFilepath />
</div>
@code {
    string ModelFilepath = "./3DModels/leather_armchair.glb";

    private async Task InitDataAsync()
    {
        // Fake await line
        await Task.FromResult(1);

    }


    protected override async Task OnInitializedAsync()
    {
        await InitDataAsync();
    }

}

“ModelCreator.cs” - Manages the creation of the scene and scene elements

using Babylon.Blazor.Babylon;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;

namespace Babylon.Blazor
{
    /// <summary>
    /// Class ModelCreator.
    /// Create 3D scene from description
    /// Implements the <see cref="Babylon.Blazor.SceneCreator"/>
    /// </summary>
    /// <seealso cref="Babylon.Blazor.SceneCreator"/>
    public class ModelCreator : SceneCreator
    {
        private readonly string _modelFilepath = "";

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelCreator"/> class.
        /// </summary>
        /// <param name="babylonInstance">The babylon interop library instance.</param>
        /// <param name="canvasId">The canvas identifier.</param>
        public ModelCreator(BabylonInstance babylonInstance, string canvasId, string modelFilepath)
            : base(babylonInstance, canvasId)
        {
            _modelFilepath = modelFilepath;
        }

        public override async Task CreateAsync(BabylonCanvasBase canvas)
        {
            Engine engine = await BabylonInstance.CreateEngine(CanvasId, true);
            Scene scene = await engine.CreateScene();

            // set rotation center
            var cameraTarget = await BabylonInstance.CreateVector3(0, 0, 0);
            // set camera
            var camera = await scene.CreateArcRotateCamera("Camera", 3 * Math.PI / 2, 3 * Math.PI / 8, 10, cameraTarget, CanvasId);
            var hemisphericLightDirection = await BabylonInstance.CreateVector3(1, 1, 0);
            var light1 = await scene.CreateHemispehericLight("light1", hemisphericLightDirection, 0.98);

            // load the model
            var assetUrl = await scene.Create3DAsset(_modelFilepath);
            await scene.AppendSceneGLB(assetUrl, scene);

            // This was a test of a direct file load as described at https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes
            // Throwing an error "addPendingData is not a function" with JS Interop
            //await scene.AppendSceneFile(_modelFilepath, scene);

            await camera.SetAutoRotate(canvas.UseAutoRotate, canvas.IdleRotationSpeed);
            await BabylonInstance.RunRenderLoop(engine, scene);
        }
    }
}

My additions to the “Scene.cs” file that controls the calls to “babylonInterop.js”:
Debug mode stops here and does not step into the .js file

        /// <summary>
        /// Creates a 3D asset to render from a filepath to a .glb file.
        /// </summary>
        /// <param name="filepath">Filepath to the .glb file.</param>
        /// <returns>An asset url that can be added to a scene.</returns>
        public async Task<string> Create3DAsset(string filepath)
        {
            var assetUrl = await BabylonInstance.InvokeAsync<string>(
                "create3DAsset",
                filepath,
                JsObjRef);
            return (string)assetUrl;
        }

        /// <summary>Updates the scene with a 3D Model, assumed to be a .glb file.</summary>
        /// <param name="assetUrl">Asset URL of the 3D Model in .glb format.</param>
        /// <param name="scene">The BABYLON scene that needs to be updated.</param>
        public async Task AppendSceneGLB(string assetUrl, Scene scene)
        {
            var options = new { pluginExtension = ".glb" };

            await BabylonInstance.InvokeVoidAsync(
                "appendSceneAsync",
                assetUrl,
                scene,
                options,
                JsObjRef);
        }

        // This was a test of a direct file load as described at https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes
        // Throwing an error "addPendingData is not a function" with JS Interop
        public async Task AppendSceneFile(string filepath, Scene scene)
        {
            await BabylonInstance.InvokeVoidAsync(
                "appendSceneAsync2",
                filepath,
                scene,
                JsObjRef);
        }

My additions to the “babylonInterop.js” file:

export async function create3DAsset(filepath) {
    console.log("create3DAsset", filepath);
    var assetArrayBuffer = await BABYLON.Tools.LoadFileAsync(filepath, true);
    var assetBlob = new Blob([assetArrayBuffer]);
    var assetUrl = URL.createObjectURL(assetBlob);
    return assetUrl;
}

export async function appendSceneAsync(assetUrl, scene, options) {
    console.log("appendSceneAsync", assetUrl, options.pluginExtension);

    await BABYLON.AppendSceneAsync(assetUrl, scene, options);
}


// This was a test of a direct file load as described at https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes
// Throwing an error "addPendingData is not a function" with JS Interop
export async function appendSceneAsync2(filepath, scene) {
    console.log("appendSceneAsync2", filepath);

    await BABYLON.AppendSceneAsync(filepath, scene);
}

Finally

This is a weird topic, as it is simultaneously an issue and a question, but I feel it fits better as a question. What causes these 404 Unable to Load errors with .glb model files? Is it an issue with not utilizing the correct BABYLON functions to convert the model to a better file type, or a failure to properly use the gltf file loader plugin? Also, if anyone with experience in JSInterop could give me some advice on how to read the Babylon.js documentation with the perspective of needing to wrap it, any help would be appreciated!

Are you able to load any file as Blob?

If I understood your question correctly, no I can’t.

I tried a .gltf file with the same 404 Unable to Load error. I also tried an .stl file, and I got an “r.addPendingData is not a function” error. The only changes I made were the filepath to the model (I am storing everything in the same 3DModels directory) and updating the “pluginExtension = ” bit in the Scene.cs file:

        public async Task AppendSceneGLB(string assetUrl, Scene scene)
        {
            var options = new { pluginExtension = ".stl" }; // Changed this line for testing

            await BabylonInstance.InvokeVoidAsync(
                "appendSceneAsync",
                assetUrl,
                scene,
                options,
                JsObjRef);
        }
***Exception:Microsoft.JSInterop.JSException: r.addPendingData is not a function
TypeError: r.addPendingData is not a function
    at https://cdn.babylonjs.com/babylon.js:1:4126154
    at https://cdn.babylonjs.com/babylon.js:1:1813787
    at Object.next (https://cdn.babylonjs.com/babylon.js:1:1813892)
    at https://cdn.babylonjs.com/babylon.js:1:1812735
    at new Promise (<anonymous>)
    at s (https://cdn.babylonjs.com/babylon.js:1:1812480)
    at Dg (https://cdn.babylonjs.com/babylon.js:1:4125822)
    at https://cdn.babylonjs.com/babylon.js:1:4127370
    at new Promise (<anonymous>)
    at Fg (https://cdn.babylonjs.com/babylon.js:1:4127339)
   at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, Object[] args)
   at Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeVoidAsync(IJSObjectReference jsObjectReference, String identifier, Object[] args)
   at Babylon.Blazor.Babylon.Scene.AppendSceneGLB(String assetUrl, Scene scene) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\Babylon\Scene.cs:line 243
   at Babylon.Blazor.ModelCreator.CreateAsync(BabylonCanvasBase canvas) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\ModelCreator.cs:line 45
   at Babylon.Blazor.BabylonCanvasBase.InitializeSzene(BabylonInstance babylonInstance, String canvasId) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\BabylonCanvasBase.cs:line 47
   at Babylon.Blazor.BabylonCanvasBase.OnAfterRenderAsync(Boolean firstRender) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\BabylonCanvasBase.cs:line 105
The thread '.NET TP Worker' (43908) has exited with code 0 (0x0).
The program '[51228] BabylonBlazor.App.exe' has exited with code 4294967295 (0xffffffff).
The thread 'BabylonBlazorApp' (0) has exited with code 0 (0x0).
The program 'water' has exited with code 4294967295 (0xffffffff).
The program '' has exited with code 4294967295 (0xffffffff).

I mean any usual file with vanilla JS ways of loading files.
For example, if you are able to load .txt file, there shouldn’t be any problem to load .obj or .babylon files.

This has gotten me going down a rabbit hole of client-server architecture things, which I did not know before. I think I am failing entirely to properly pass the files from the server to the client in order for the client to render the model files using Babylon.

This is not really an issue with my use of Babylon, but I am new to client-server architectures and haven’t had to work with data streams before, which is what appears to be the actual issue. I found some information on making it possible for a client to download a file through a data stream, which utilizes arrayBuffer, Blob, and URL.createObjectURL() like what I am attempting to do in the babylonInterop.js file (where the 404 error is occurring):

export async function create3DAsset(filepath) {
    console.log("create3DAsset", filepath); // this prints in the log right before the 404 error
    var assetArrayBuffer = await BABYLON.Tools.LoadFileAsync(filepath, true);
    var assetBlob = new Blob([assetArrayBuffer]);
    var assetUrl = URL.createObjectURL(assetBlob);
    return assetUrl;
}

and the example I found online discussing downloading files from a stream:

<script>
  window.downloadFileFromStream = async (fileName, contentStreamReference) => {
    const arrayBuffer = await contentStreamReference.arrayBuffer();
    const blob = new Blob([arrayBuffer]);
    const url = URL.createObjectURL(blob);
    const anchorElement = document.createElement('a');
    anchorElement.href = url;
    anchorElement.download = fileName ?? '';
    anchorElement.click();
    anchorElement.remove();
    URL.revokeObjectURL(url);
  }
</script>

I am still reading up on data streams, which looks like what I need to be implementing, but I am not sure why the code in the create3DAsset() function is failing to do so… Or is it a problem of me running this all on localhost rather than from a published build?

I changed out my local .glb files for a url to a .glb file on the playground. This got me past an issue with attempting to get a file to the client, so that is one issue that may be solved on its own once this is published to a test server.

I can not get the model to render, due to another issue that a friend found only one instance of from googling, and that is from this very forum thread, and that is this "addPendingData is not a function):

***Exception:Microsoft.JSInterop.JSException: scene.addPendingData is not a function
TypeError: scene.addPendingData is not a function
    at https://cdn.babylonjs.com/babylon.max.js:116441:27
    at step (https://cdn.babylonjs.com/babylon.max.js:203:21)
    at Object.next (https://cdn.babylonjs.com/babylon.max.js:184:51)
    at https://cdn.babylonjs.com/babylon.max.js:177:69
    at new Promise (<anonymous>)
    at __awaiter (https://cdn.babylonjs.com/babylon.max.js:173:10)
    at appendSceneImplAsync (https://cdn.babylonjs.com/babylon.max.js:116419:61)
    at https://cdn.babylonjs.com/babylon.max.js:116549:13
    at new Promise (<anonymous>)
    at appendSceneSharedAsync (https://cdn.babylonjs.com/babylon.max.js:116547:12)
   at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, Object[] args)
   at Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeVoidAsync(IJSObjectReference jsObjectReference, String identifier, Object[] args)
   at Babylon.Blazor.Babylon.Scene.AppendSceneGLB(String assetUrl, Scene scene) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\Babylon\Scene.cs:line 243
   at Babylon.Blazor.ModelCreator.CreateAsync(BabylonCanvasBase canvas) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\ModelCreator.cs:line 45
   at Babylon.Blazor.BabylonCanvasBase.InitializeSzene(BabylonInstance babylonInstance, String canvasId) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\BabylonCanvasBase.cs:line 47
   at Babylon.Blazor.BabylonCanvasBase.OnAfterRenderAsync(Boolean firstRender) in C:\Users\jgoulding\Source\Repos\MSBabylonBlazorTest\Babylon.Blazor.Lib\BabylonCanvasBase.cs:line 105

This is attempting to use the AppendSceneAsyc(path, scene) function. Another friend of mine told me that it is because “addPendingData” is deprecated and the CDN of Babylon.js we are using is too new compared to the code, and he claims that he reverted back to v4.2.0 and this error stopped. I switched to the max.js CDN in order to step through the library, and I am not sure what is going on with this anymore.

I did find one forum post from 2021 that you assisted with that also gets a “scene._____ is not a function” error, but it seems unrelated to the issue I am facing now: Forum Post

If you need CDN for specific Babylon version like 4.2.0 it is here - https://cdnjs.com/libraries/babylonjs/4.2.0

If there is code within this repository that is actually causing this “addPendingData is not a function” error due to deprecation, I would rather find that code than revert back 3 major versions… Or at this point I may just need to abandon building on top of this project and build from scratch.

the cdn is always pointing to the latest version so 7.xx.x for now. So you may just face a version mismatch here.

that being said, based on what you share I do not believe this is the issue

the thing is that you are getting a 404 so does that mean the file is really not found?

Ideally if you could share a live repro we could check the console and the network tab (I tried the examples on your repo but they seem to work well)

This is very unlikely as we would have faced it already. The Playground points to the preview cdn directly

You are not experiencing the issues I described within the repo?

Also, I figured out (sort of) the file loading issue. I switched from trying to use a file stored within the server’s root folder to using a .glb model file from the playground. Someone mentioned the issue likely was that Windows permissions were getting weird with both the server and client running on the same machine. There is more testing that I would need to do, but for now I can at least get past that problem.

Currently, I can’t load anything due to the “addPendingData is not a function” issue. The code I added is up to date with version 7, but something else in the repo I forked off of is not playing nice with my code. I would rather not revert back to version 4 and would prefer to resolve the issue preventing me from using version 7, but I am not having any success in finding the problem.

Are you able to extract the Babylon part from the code and see if it will work?

That will be my next step, or to be more accurate my next step is just building out the project in JavaScript to show it works, and then doing it again in Blazor with JSInterop.

Can you share a live version of the issue? A website that showcase the error (not the repo, a fully deployed version)

Not at this time with my current resources, sadly. I can try to set one up tonight.

1 Like

So I ended up just doing things very differently to avoid the errors entirely. To begin with, I tossed out the idea of building on top of the open source library, as something with that project’s code combined with the commits I added was causing the “addPendingData is not a function” issue.

Instead, I just built a demo Blazor app using JSInterop files with the Javascript being taken from the Playground (with some corrections I had to make to allow it to run that seems to be related to Javascript variable scopes). I could successfully render 3D models without error. Here is a JSInterop Javascript file of the playground .html download scripts for the test scene with a sphere and ground plane as an example. The major notes are needing to specify “window.engine” and “window.scene” rather than just “engine” and “scene” as appears in the .html page you can download. The global variables are also barely used and I believe scene remains null for the entire run.

export function createTestScene(canvasId) {
    var engine = null;
    var scene = null;
    var sceneToRender = null;

    /*console.log("Entered createTestScene");*/
    var canvas = document.getElementById(canvasId);
    if (canvas == null) {
        alert("canvas id '" + canvasId + "' is not found");
    }

    var startRenderLoop = function (engine, canvas) {
        /*console.log("Entered startRenderLoop");*/
        engine.runRenderLoop(function () {
            if (sceneToRender && sceneToRender.activeCamera) {
                sceneToRender.render();
            }
        });
    }

    var createDefaultEngine = function () {
        /*console.log("Entered createDefaultEngine");*/
        var result = new BABYLON.Engine(canvas, true);
        return result;
    };
    const createScene = function () {
        /*console.log("Entered createScene");*/
        // This creates a basic Babylon Scene object (non-mesh)
        const scene = new BABYLON.Scene(engine);

        // This creates and positions a free camera (non-mesh)
        const camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);

        // This targets the camera to scene origin
        camera.setTarget(BABYLON.Vector3.Zero());

        // This attaches the camera to the canvas
        camera.attachControl(canvas, true);

        // This creates a light, aiming 0,1,0 - to the sky (non-mesh)
        const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);

        // Default intensity is 1. Let's dim the light a small amount
        light.intensity = 0.7;

        // Our built-in 'sphere' shape.
        const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2, segments: 32 }, scene);

        // Move the sphere upward 1/2 its height
        sphere.position.y = 1;

        // Our built-in 'ground' shape.
        const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 6, height: 6 }, scene);

        return scene;
    };
    window.initFunction = async function () {


        /*console.log("Entered window.initFunction");*/
        var asyncEngineCreation = async function () {

            /*console.log("Entered asyncEngineCreation");*/
            try {
                return createDefaultEngine();
            } catch (e) {
                console.log("the available createEngine function failed. Creating the default engine instead");
                return createDefaultEngine();
            }
        }

        var tempEngine = await asyncEngineCreation();

        window.engine = tempEngine;
        if (!window.engine) throw 'engine should not be null.';
        startRenderLoop(window.engine, canvas);
        window.scene = createScene();
    };
    initFunction().then(() => {
        sceneToRender = window.scene // This must be window.scene as just scene causes scoping issues.
    });

    // Resize
    window.addEventListener("resize", function () {
        /*console.log("Entered addEventListener");*/
        window.engine.resize();
    });
}

I did have issues with “404 Unable to load”, which comes from Javascript not being allowed to read local files. I struggled with getting a file data stream to work, and I decided to just not do it. Instead, I used the npm serve package to spin up a local server of the file directory to retrieve the .glb files from. Had an issue with CORS policy, but set the headers appropriately for the file server in a serve.json file as detailed on the npm serve documentation: npm serve. Note: serve and serve-handler configurations have strict expectations for formatting. I used “*” for the Allow Methods because this dictionary approach they are using expects a single string for value, rather than any array or list object.

{
	"headers": [
	{
		"source" : "/3D_Models/*.glb",
		"headers" : [
		{
			"key" : "Access-Control-Allow-Origin",
			"value" : "<Localhost URL and Port of Application>"
		},
		{
			"key" : "Access-Control-Allow-Methods",
			"value" : "*"
		}]
	}
	]
}

There is one issue I am experiencing from testing (“WebGL… location is not from current program” that occurs after swapping between 3 or 4 models to render on a web page), but that is unrelated to anything mentioned in this forum thread, and I will be spending time researching that to see if I can resolve it on my own prior to creating a new thread on that issue.

Thank you for your help!

1 Like