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!