Loading .babylon scene as a child of a specific scene graph node

Hi there,

I would like to export .babylon files for many objects in my game, load each .babylon file as needed in-game, and then apply transforms to the root of each .babylon scene. Is there a way to load all of the scene elements of a .babylon file into a specified Node, or do I generally have to know the names of the things I want in a .babylon file and manually parent them with
setParent() ?

If not, it would be an insanely useful API addition to specify a parent node when loading a .babylon, .gltf, etc, and create the node if it doesn’t exist.

Thanks!

sceneLoader.ImportMeshes(), and maybe sceneLoader.Append() supply an array of meshes that got loaded in an argument of the completion callback.

@JCPalmer According to the API docs, both of those methods you mentioned append assets directly to a Scene, not to any arbitrary node in the scene graph tree.

How do we append loaded assets into any node in the scene graph tree (and not as root nodes of the Scene)?

Those API docs for both of those methods say:

     * @param scene is the instance of BABYLON.Scene to append to

Looking to avoid that.

F.e. the desire is not this,

- Scene
  - ...loaded assets...

but this,

- Scene
  - TransformNode
    - TransformNode
      - ...loaded assets...

so that we can transform the whole loaded GLTF asset using the TransformNodes (f.e. arbitrarily rotate or scale a whole loaded asset, or have it translate relative to some other thing in the scene, etc).

You probably have all the information needed, you bringing the parent of course. Just do a one line for loop, checking if a mesh does not have a parent, it does now.

for (const mesh of meshes) {
     if (!mesh.parent) mesh.parent = myThing;
}

You may need to use setParent() instead, depending on your needs, but I’ll leave that to you.

I think I already tried that, but it doesn’t seem to be that easy!

If you are using Append, where are you getting a list of meshes? A scene may already contain other meshes before loading new ones, and Append doesn’t seem to tell me which meshes were newly loaded.

But what if none of the meshes are root nodes, and they have transformed containers?

Here’s an example with several attempts that I cannot get working: https://playground.babylonjs.com/#1SR10L#3

(copy the code, hit TS, then paste the code back in, because he editor starts it as JavaScript even though I selected TypeScript)

Code, just in case (click to expand):
var delayCreateScene = async function () {
    // Create a scene.
    var scene = new BABYLON.Scene(engine);

    // Create a default skybox with an environment.
    var hdrTexture = BABYLON.CubeTexture.CreateFromPrefilteredData("textures/environment.dds", scene);
    var skybox = scene.createDefaultSkybox(hdrTexture, true);

    // We will load a GLTF model into this node.
    const assetWrapper = new BABYLON.TransformNode('')
    scene.addTransformNode(assetWrapper)

    //// HERE: None of these work: ///////////////////////////////////////

    // const asset = await loadAssetIntoNode1("scenes/BoomBox/", "BoomBox.gltf", assetWrapper)
    const asset = await loadAssetIntoNode2("scenes/BoomBox/", "BoomBox.gltf", assetWrapper)
    // const asset = await loadAssetIntoNode3("scenes/BoomBox/", "BoomBox.gltf", assetWrapper)
    // const asset = await loadAssetIntoNode4("scenes/BoomBox/", "BoomBox.gltf", assetWrapper)

    //////////////////////////////////////////////////////////////////////

    console.log('loaded content:', asset)

    // Create a default arc rotate camera and light.
    scene.createDefaultCameraOrLight(true, true, true);

    // The default camera looks at the back of the asset.
    // Rotate the camera by 180 degrees to the front of the asset.
    (scene.activeCamera as any).alpha += Math.PI; // WTH doesn't this work here?

    return scene;
};

// This cannot possibly be reliable, none of the meshes may be root nodes.
async function loadAssetIntoNode1(base: string, file: string, parent?: BABYLON.Scene | BABYLON.TransformNode) {
    const container = await BABYLON.SceneLoader.LoadAssetContainerAsync(base, file);

    // Append glTF model to given parent.
    for (const node of container.meshes) {
        if (parent instanceof BABYLON.Scene) parent.addTransformNode(node)
        else node.parent = parent
    }

    return container
}

async function loadAssetIntoNode2(base: string, file: string, parent?: BABYLON.Scene | BABYLON.TransformNode) {
    const container = await BABYLON.SceneLoader.LoadAssetContainerAsync(base, file);

    // Append glTF model to given parent.
    for (const node of container.rootNodes) {
        if (parent instanceof BABYLON.Scene) {
            const scene = parent

            if (node instanceof BABYLON.TransformNode) {
                scene.addTransformNode(node)
            } else {
                const tnode = new BABYLON.TransformNode('')
                scene.addTransformNode(tnode)
                node.parent = tnode
            }
        } else {
            node.parent = parent
        }
    }

    return container
}

async function loadAssetIntoNode3(base: string, file: string, parent?: BABYLON.Scene | BABYLON.TransformNode) {
    const container = await BABYLON.SceneLoader.LoadAssetContainerAsync(base, file);

    const rootNodes = [...container.meshes, ...container.transformNodes, ...container.cameras, ...container.lights].filter(node => !node.parent)

    // Append glTF model to given parent.
    for (const node of rootNodes) {
        if (parent instanceof BABYLON.Scene) {
            const scene = parent

            if (node instanceof BABYLON.TransformNode) {
                scene.addTransformNode(node)
            } else {
                const tnode = new BABYLON.TransformNode('')
                scene.addTransformNode(tnode)
                node.parent = tnode
            }
        } else {
            node.parent = parent
        }
    }

    return container
}

async function loadAssetIntoNode4(base: string, file: string, parent?: BABYLON.Scene | BABYLON.TransformNode) {
    const container = await BABYLON.SceneLoader.LoadAsync(base, file);

    // Append glTF model to given parent.
    for (const node of container.rootNodes) {
        if (parent instanceof BABYLON.Scene) {
            const scene = parent

            if (node instanceof BABYLON.TransformNode) {
                scene.addTransformNode(node)
            } else {
                const tnode = new BABYLON.TransformNode('')
                scene.addTransformNode(tnode)
                node.parent = tnode
            }
        } else {
            node.parent = parent
        }
    }

    return container
}

You have widened this topic away from a .Babylon to a .GLB. I may not be the best person for a .GLB. I use geo embedded mesh sub-classes, and do not load meshes as data.

Can you please also answer the questions about what you suggested? (f.e. I do not see that .Append returns a list of meshes like you mentioned, and what if the meshes are not root nodes).


After loading an asset with SceneLoader.* methods (which is what you suggested and I’m trying to see what you’re saying which I so far don’t see), we get Babylon nodes, and it doesn’t matter what the asset source file format is.

The question is not really widened because the OP mentioned GLTF also, so the question still stands and is re-worded like so:

How do we place the Babylon nodes loaded with Babylon’s official SceneLoader.* methods as children of any arbitrary node?

More concretely, why doesn’t the example work? (It does not matter what the source file format is, the example is trying to use babylon nodes that are returned after the asset is loaded).


Another question is, how can Babylon and its docs be improved so this simple task becomes obvious?

In three.js, it is very simple:

const loader = new GLTFLoader() // f.e. GLTFLoader, OBJLoader, FBXLoader, etc

loader.load('/path/to/asset.whatever', (asset) => {
  someArbitrarySceneGraphNode.add(asset.scene)
})

It would be great for it to be as simple in Babylon as it is in Three.js.

I believe the list of meshes he mentions is from the scene parameter of onSuccess, which has a meshes array. The code shows how to access the loaded meshes from .gltf-file and append as child, but I dunno if there is a direct way to attach to scene graph node:

var sceneRoot = new BABYLON.TransformNode("sceneRoot") // some arbitrary node

BABYLON.SceneLoader.Append("scenes/BoomBox/", "BoomBox.gltf", scene, function (scene) {
    console.log(scene.meshes) // list of meshes
    scene.getMeshByName("__root__").parent = sceneRoot // or use setParent()
});

You may rename the root node after parenting, when you load several .gltf-files.

The full PG:

There is some confusion in these posts, so I will try to clear some things up. SceneLoader has multiple ways for loading an asset (Load, Append, ImportMesh, and LoadAssetContainer), all of which have an async counterpart that returns a promise instead of using callbacks. Load will load into a new scene. Append will append to an existing scene. The only methods that will give you the meshes that come from the asset are ImportMesh and LoadAssetContainer.

Here is a PG using ImportMeshAsync that will do what you want that should work with either glTF or babylon files: https://playground.babylonjs.com/#8FHM4P#2. I’ve modified some code to make them more correct based on your PG.

If you are strictly working with glTF, the loader guarantees that a __root__ node will be created and it will be meshes[0] of the resulting meshes for ImportMesh and thus you can just parent this mesh instead of looping.

There should be a way to do this with asset containers also.

(copy the code, hit TS , then paste the code back in, because he editor starts it as JavaScript even though I selected TypeScript)

The way the playground determines whether it’s TS or JS is by checking for the string class Playground. If you don’t have that string, it will default to JS.

I personally would like to improve the way we load assets to make it more obvious and easier to pass in options, but we keep backwards compatibility around here, so it will take some effort to make this happen.

1 Like

Are you implying that all models have a root node named __root__? EDIT: @bghgary mentions below this is only for GLTF.

Also the scene passed into onSuccess is not unique. If any other code anywhere else in the app loaded other assets, then scene.meshes will not contain the list of meshes that I specifically loaded (this means I can’t reliably get what I loaded).

But what if the meshes all have parents because parent nodes were used to transform the meshes in the modeling program from which GLTF was exported? (third time asking this question here)

Automatically connecting loaded assets into a scene seems unnecessary, which is why I didn’t want to use Load or Append, plus they don’t give me back my specific assets anyway, making it unusable if I want to manipulate the asset without relying on a hack such as certain types have __root__-named nodes.

I didn’t want to use ImportMeshes because based on the API the .meshes property does not include non-meshes (unless the concept of a “mesh” is convoluted in Babylon?). I need the whole imported tree, up to the top-level non-mesh root nodes.

That leaves only LoadAssetContainer which seemed to be the needed API: it returns the asset and nothing more, with no new scene, and without attaching to an existing scene, and gives me .rootNodes not just .meshes.

Why is it that in my example, attaching rootNodes to a transform node does not work?

Here’s my example again, but in your format, and the model does not render:

Do you have a deprecation cycle?

In hoping to spark improvement ideation, speaking neutrally here, Babylon has some of the most confusing APIs I’ve seen (not specifically for 3D, I’m comparing to JS libraries in general).

It may be worth cataloguing the confusing APIs and considering replacements for them after deprecation.

Not only is the above Three.js example very simple, but it is really easy to do this in PlayCanvas as well:

assets.loadFromUrl("statue.glb", "container", function (err, asset) {
    const statue = asset.resource.instantiateRenderEntity({ castShadows: true }); // even simple to change a model-wide setting regardless of the contained nodes.

    arbitraryParentNode.addChild(statue); // So easy.
})

It can be so much simpler in Babylon.

Oh I am sorry, seems I misunderstood. Because I thought you were talking about gltf-files only. I would just handle both separately (.babylon and .gltf/.glb), but an internal solution by engine wouldn’t be bad as @bghgary plans to implement.

True. I did realize it afterwards, but assumed you would still be able to track mesh unique ids manually.

If we still talk about GLTF, then in any case the root mesh won’t have a parent or am I missing something?

For gltf just take mesh named root and use getChildren(…) to get hierarchy of imported it should also include TransformNodes.

You just loaded assets into assetContainer but did not add or instantiate models to scene:

model.instantiateModelsToScene()

or

model.addAllToScene()

I had to read this maybe a dozen times before understanding the question. I think the confusion is caused by the fact that transformNode was introduced later. See below.

I didn’t suggest using Load or Append. They will not return information about the asset. These functions are intended to load into a new/existing scene.

I’ll admit that when I first looked at the Babylon naming/structure, it was not what I expected. I wouldn’t call it convoluted though. A mesh in Babylon derives from a transformNode which derives from node. transformNode was added later and didn’t exist originally. You are correct in that the list of meshes does not include non-meshes (i.e., transformNodes, lights, cameras, etc). For glTF, the top-level root node is always the first mesh in the meshes array, which is actually an empty mesh (i.e., a mesh with no geometry) and not a transformNode due to backwards compatibility. This can be used to traverse the entire tree.

I’m not sure who added rootNodes but it’s not populated by the loaders and is always empty unless instantiateModelsToScene is called. @RaananW/@carolhmj Can you take a look?

Here is a working version: https://playground.babylonjs.com/#8FHM4P#4. I would expect rootNodes to work, but it isn’t currently populated. addAllToScene or equivalent needs to be called in order for the scene to know about the nodes.

I’ll let @RaananW answer this one. :slight_smile:

I agree it can be simpler to use for some scenarios. As I said earlier, I have wanted to investigate making changes to the API, but we have to maintain backwards compatibility. Contributes welcome if you want to help.

Looking at the code, rootNodes is part of AbstractScene, which AssetContainer extends, but this array is mostly used by its other extending class, Scene, which has methods like addMesh and addLight, who check if the node doesn’t have a parent and push to rootNodes in this case. In the case of AssetContainer, the rootNodes array would have to be manually populated. I can do that in the loaders.

Just adding too that AssetContainer does have a method to create a root node for all its meshes, createRootNode, so loading all contents to a node could also look like this: Babylon.js Playground (babylonjs.com).

1 Like

Oops, I was looking a different rootNodes.

Oh, you were seeing the one on InstantiatedEntries which is the result of instantiateHierarchy, right?

1 Like

Thanks for explaining!

It would be great for that explanation to be in the API docs. That is completely not intuitive from the naming, and the type definitions. I would never assume (especially for a particular file type) that the root node is an empty mesh, and that it is specifically the first one in a list of meshes.

I get that maybe that’s there for backwards compatibility, but why wasn’t a better intuitive API added as an alternative, and possibly the old stuff deprecated? Keeping backwards compatibility is only part of the ideal picture. We need new features with new APIs that make sense, without backwards compat causing APIs to be very unintuitive.

I’m also not using exclusively GLTF, so what’s the answer for all other assets types?

Adding .rootNodes to ImportMeshes would seem to be intuitive: we’d know exactly where to get the root node(s) we from our imported model, but:

Well that makes no sense! So now there’s an API that gives me a result with rootNodes, but they aren’t the root node(s) of the thing I imported? :woozy_face:

The answer is still not clear (especially for non-GLTF models).

I’m hoping intuitive APIs can replace these old ones.

But same problem: that only works based on an undocumented quirk mentioned in this thread specifically for GLTF files.

How do we get the root node(s) of any type of model we import, without making a new scene, without adding the model to an existing scene? And then how do we properly create a parent node in a scene, and then properly parent the loaded model into that parent node?

I agree, contributions would be welcome.

New APIs are likely what should happen, but we have to do it well so that we don’t have to keep introducing new APIs that do similar things. That said, the main reason is time. If you want to help improve the code, please contribute. :slight_smile:

My PG works for meshes. It is not specific to glTF or how the glTF loader works. It can be expanded to support more types (transformNodes, etc.), but if it needs to be fully thorough, the rootNodes property should help once it is properly populated.

1 Like

I also missed createRootNode that @carolhmj mentioned (I think she meant createRootMesh, and why not createRootNode considering that I don’t want to make a new mesh, just a node, if I’m going to be making a container for the loaded meshes).

Why is model.addAllToScene(); required? (that wasn’t obvious, now I’m wondering if I’ve been doing things wrong all along)

No matter what I did in my previous examples with the node loaded to asset container, they were never going to appear in the scene because addAllToScene() was missing!

The following doesn’t work, and I would not have intuitively assumed that I needed to add everything to a scene if I’m already adding them to sub node instead of a scene: https://playground.babylonjs.com/#8FHM4P#10

This shows that there’s currently too much to consider for the task at hand: it took a while before someone mentioned I didn’t have the required addAllToScene() call.

I called this out earlier.