[SOLVED] Trouble accessing children in a glTF-imported file (load externally)

I’m still not sure what the actual issue/problem is here? The graphnode hierarchy is the same as the shown in blender. All graphnodes are accessible if by name or by the children order. Mesh instances are accessible as an array and can be checked against their graphnode.

Basically, what do you need to progress with your project?

During the total debugging-route within this topic, I did actually try something in regards to accessing those external nodes - as seen below:


var LoadGltfExternal = pc.createScript('loadGltfExternal');
LoadGltfExternal.attributes.add('baseUrl', {type: 'string'});
LoadGltfExternal.attributes.add('gltfFilename', {type: 'string'});

// initialize code called once per entity
LoadGltfExternal.prototype.initialize = function() {
   
    var app = this.app;
    var self = this;  self.fr =0;

 app.assets.loadFromUrl(self.baseUrl + self.gltfFilename, 'json', function (err, asset) {
        var gltf = JSON.parse(asset.resource);

        loadGltf(gltf, app.graphicsDevice, function (err, res) {
            // Wrap the model as an asset and add to the asset registry
            var asset = new pc.Asset('gltf', 'model', {
                url: ''
            });

            asset.resource = res.model;
            asset.loaded = true;
            app.assets.add(asset);

            // Add the loaded scene to the hierarchy
            self.entity.addComponent('model', {
                asset: asset
            });
        }, {
            basePath: self.baseUrl
        });
    });
};

// update code called every frame
LoadGltfExternal.prototype.update = function(dt) {
    var app = this.app;
    var self = this;
    self.fr++;
    if(self.fr>100 && self.fr<110){
    

    var testMat = self.app.root.findByName('TestMat'); console.log('TestMat @ ModelLoad : ',testMat.name);
    var meshInstance = self.app.root.findByName('ModelLoad'); 
       try{ console.log('meshInstance child Bn: '+  meshInstance.children[0].children[0].node); }catch(err){}
      try{ console.log('meshInstance grandchild Sp: '+  meshInstance.children[0].children[0].children[0].material); }catch(err){}
       try{ console.log('1: '+  meshInstance.meshes[0].name);}catch(err){}
             try{  console.log('2: '+  meshInstance.children[0].meshes[0].material);}catch(err){}
        try{   console.log('3: '+  meshInstance.children[0].children[0].meshes[0].name);}catch(err){}
      try{   console.log('4: '+  meshInstance.children[0].children[0].meshes[1].name);}catch(err){}
           try{   console.log('5: '+  meshInstance.children[0].meshes[0].name);}catch(err){}
      try{   console.log('6: '+  meshInstance.children[0].meshes[1].name);}catch(err){}
        
       try{   console.log('M1: '+  meshInstance.materials[0].name);}catch(err){}      try{   console.log('M2: '+  meshInstance.materials[1].name);}catch(err){}
        try{   console.log('MLD1: '+  meshInstance.children[0].materials[0].name);}catch(err){}      try{   console.log('MLD2: '+  meshInstance.children[0].materials[1].name);}catch(err){}

     }      
};

(the reason I didn’t write was an answer from you, and then I did not get back to it)
From all the try{}catch(){}'es, the first two worked only - the rest fails [so I would argue that I don’t have access to all the nodes :-/ ]
Between the lines (and cf the blender-scrdmp above), I (we all want) to access the two mesh’es called:
“Ud” and “Ids”, that lies below the blender objects “Bn” and “Sp”.

At ‘post 4’; “Bn” and “Sp” are shown within the output list, yes. “Ud” and “Ids”; not.

Within the PyraMid_SB3.glTF-file one will find/read:

"meshes" : [
        {
            "name" : "Ud",
            "primitives" : [
                {
                    "attributes" : {
                        "NORMAL" : 2,
                        "POSITION" : 1
                    },
                    "indices" : 0,
                    "material" : 0
                }
            ]
        },
        {
            "name" : "ids",
            "primitives" : [
                {
                    "attributes" : {
                        "NORMAL" : 5,
                        "POSITION" : 4
                    },
                    "indices" : 3,
                    "material" : 1
                }
            ]
        }

As seen above they both have a material-reference in their ‘childrens’-list/sub-node list.
Is it possible to reach them and their material-reference?
In essence: [Did the API-responsible within the PlayCanvas-organization think of such functionality, and if so; do you have the method? :slight_smile: ]

In case they did, it would help me a lot.

My ultimate goal:

  1. Load an external glTF-file with multiple objects (each having meshes).
  2. Change the color of each glTF-object at runtime/dynamically (regardless of level … if do-able at object-level, it is fine as well … cf the .glTF-file structure that contains different material-references).

My backup-plan (quite time consuming and an extra list-filling is required at ‘4’):

  1. Make a blender-project with multiple objects/meshes.
  2. Export each object/mesh as a glTF-file.
  3. Load each of these glTF-files separately.
  4. Change the color of each separate glTF-file at runtime/dynamically.

I’m not sure how GLTF meshes are related to the mesh instances in PlayCanvcas. I do wonder how the representation of multiple meshes under a node would be represented in PlayCanvas in regards to mesh instances.

For the moment, assuming that one node = one meshInstance, is this what you are looking for?

var AccessNodes = pc.createScript('accessNodes');

AccessNodes.attributes.add('greenMaterialAsset', {type: 'asset', assetType:'material'});
AccessNodes.attributes.add('redMaterialAsset', {type: 'asset', assetType:'material'});


// initialize code called once per entity
AccessNodes.prototype.initialize = function() {
    // If you want to access the mesh instance, that's a bit trickier as the are not attached the graph node
    // You would need to iterate through all the mesh instances and check the graph node it's attached to
    var i, mi;
    var meshInstances = this.entity.model.meshInstances;
    for (i = 0; i < meshInstances.length; ++i) {
        mi = meshInstances[i];
        if (mi.node.name == "Bn") {
            // This is using a referenced material but you could easily clone one and reassign
            mi.material = this.greenMaterialAsset.resource;
        }
        
        if (mi.node.name == "Sp") {
            mi.material = this.redMaterialAsset.resource;
        }
    }    
};

https://playcanvas.com/editor/scene/918604

This is from your latest example :slight_smile:

  • my project is already between 50 and 100 mb (have to use all the external sources possible … all external methods)

You can use external in the same way. Nothing really changes from it being in the Editor. Its the same gltf and bin file that is loaded via the same methods of the GLTF loader. And if it’s the splash time load you are trying to remove, you can untick preload from the assets and load them in at will later.

But for completeness, here is an example with external loaded models:
https://playcanvas.com/editor/scene/918605

Code: https://playcanvas.com/editor/code/685076?tabs=30728683

var LoadGltfExternal = pc.createScript('loadGltfExternal');
LoadGltfExternal.attributes.add('baseUrl', {type: 'string'});
LoadGltfExternal.attributes.add('gltfFilename', {type: 'string'});
LoadGltfExternal.attributes.add('greenMaterialAsset', {type: 'asset', assetType:'material'});
LoadGltfExternal.attributes.add('redMaterialAsset', {type: 'asset', assetType:'material'});

// initialize code called once per entity
LoadGltfExternal.prototype.initialize = function() {
    var app = this.app;
    var self = this;

    app.assets.loadFromUrl(self.baseUrl + self.gltfFilename, 'json', function (err, asset) {
        var gltf = JSON.parse(asset.resource);

        loadGltf(gltf, app.graphicsDevice, function (err, res) {
            // Wrap the model as an asset and add to the asset registry
            var asset = new pc.Asset('gltf', 'model', {
                url: ''
            });

            asset.resource = res.model;
            asset.loaded = true;
            app.assets.add(asset);

            // Add the loaded scene to the hierarchy
            self.entity.addComponent('model', {
                asset: asset
            });
            
            self.entity.addComponent('script', {});

            self.entity.script.create('accessNodes', { 
                attributes: {
                    greenMaterialAsset: self.greenMaterialAsset,
                    redMaterialAsset: self.redMaterialAsset
                }
            });
        }, {
            basePath: self.baseUrl
        });
    });
};

// update code called every frame
LoadGltfExternal.prototype.update = function(dt) {
    
};

// swap method called for script hot-reloading
// inherit your script state here
// LoadGltfExternal.prototype.swap = function(old) { };

// to learn more about script anatomy, please read:
// http://developer.playcanvas.com/en/user-manual/scripting/

Ok, will try that.
So that’s the usage of “preload”. Had trouble understanding the exact functionality -> seemed blurry as even the smallest projects with one internal primitive adds up to an export of several mb (?)
… so was quite pessimistic of dynamic load effects (but I guess ‘all byte-size’ of the non-preloaded files lay dorment in the “files”-folder then).

Ok, works like a charm … will write a feedback to the relevant team about a “pipeline-model” etc in relation to “How things load” {might also be able to solve myths and mystique regarding the app.fire/time events}

thanks

Preload when ticked means it downloads before the app starts (ie during the orange loading screen).

The app size when you export the project is not the same as the amount of data that the user downloads when viewing the app.

Take this example: https://developer.playcanvas.com/en/tutorials/load-assets-with-a-progress-bar/

All the assets have preload unticked so it only loads the engine, scripts and scene JSON file as part of the preload step (orange loading bar). This is 290KB.

This gets us into the app quickly.

When the app starts, there’s a script that requests the assets to be loaded:

// More information about streaming assets at runtime can be found here
// http://developer.playcanvas.com/en/user-manual/assets/preloading-and-streaming/

// Note: All the texture and material assets that would like to be streamed at runtime
// should have 'level' unticked and in this demo's case, have been tagged with
// 'level'
var Preloader = pc.createScript('preloader');

Preloader.attributes.add("loadingBar", {type: "entity", title: "Loading Bar"});
Preloader.attributes.add("runeCards", {type: "entity", title: "Rune Cards"});

// initialize code called once per entity
Preloader.prototype.initialize = function() {
    this.loadAssets();
};

Preloader.prototype.loadAssets = function() {
    var self = this;
    
    // Find all the assets that have been tagged 'level'
    // In this example, they are in the 'assets-to-load' folder
    var assets = this.app.assets.findByTag('level');
    var assetsLoaded = 0;
    var assestTotal = assets.length;
        
    // Callback function when an asset is loaded
    var onAssetLoad = function() {
        assetsLoaded += 1;        
        
        // Update the progress bar
        self.setLoadingBarProgress(assetsLoaded / assestTotal);        
        
        // Once we have loaded all the assets
        if (assetsLoaded === assestTotal) {
            self.onAssetsLoaded();
        }        
    };
    
    // Start loading all the assets
    for(var i = 0; i < assets.length; i++) {
        if (assets[i].resource) {
            onAssetLoad();
        } else {
            assets[i].once('load', onAssetLoad);
            this.app.assets.load(assets[i]);
        }
    }
    
    if (!assets.length) {
        this.onAssetsLoaded();
    }    
};

Preloader.prototype.onAssetsLoaded = function() {
    // Disable the loading bar so it is no longer in view
    this.loadingBar.enabled = false;
    
    // Show the runes now that all the assets are loaded
    this.runeCards.enabled = true;
};

Preloader.prototype.setLoadingBarProgress = function(progress) {
    var localScale = this.loadingBar.getLocalScale();
    localScale.x = pc.math.clamp(progress, 0, 1);
    this.loadingBar.setLocalScale(localScale);
};

And that loads the rest of the assets (which in this case is not very much) (97KB)

There’s a bit more nuance to this but that’s the general idea. This means that the developer can ensure that only the assets needed for what the user would see first (eg, a title screen with a menu) is loaded during the orange bar loading screen. And later can load/unload asset on demand depending on what the user does.

2 Likes

:slight_smile: perfect … that is already some of it (still needs to be put into a wrapped presentation … I guess - but up to you guys)

The export-standards within the Kronos-node structure seem to have been changed since/now:

Start of my gltf:

{
    "asset" : {
        "generator" : "Khronos glTF Blender I/O v1.2.75",
        "version" : "2.0"
    },
  • which might be the reason why I cannot use your (cf above also):

function printChildren(node, indentLevel) {
    var i = 0;
    var indentStr = "";
    for (i = 0; i < indentLevel; ++i) {
        indentStr += " ";
    }

    console.log(indentStr + node.name);

    for (i = 0; i < node.children.length; ++i) {
        printChildren(node.children[i], indentLevel+1);
    }
}

printChildren(pc.app.root, 0);
  • in the devtools console anymore? Leaning towards ‘pc.app.root’ as being the probable issue
    [Dave (I think) wrote about the ‘pc.’-library as being updated to something else in a repo-update?]

wait a sec - it works on a model loaded initially, but not when my/the model is loaded afterwards? How to access then >> can I do something to update the root-hierarchy, so the console can access it after the first load/run-of-scripts?

My goal is namely/now to access ‘animations’ and ‘materials’ in the gltf-structure:

if(self.entity.model){
meshes = self.entity.model.model.meshInstances; //works
glbAnims = self.entity.model.model.animations; console.log( glbAnims[0]); // does not work

PS: The 'function printChildren(node, indentLevel) { ...' only outputs the "nodes" but not "animations" nor "materials"
...

It was never meant to. It prints the node hierarchy, not it’s contents.

If you load the GLB as a container, you can access the materials and assets in the container resource: https://developer.playcanvas.com/en/api/pc.ContainerResource.html

ok, thanks … trying:

    app.assets.loadFromUrlAndFilename(recUrl, null, "container", function (err, asset) {

       try{   self.entity.addComponent('model', {
            asset: asset.resource.model
        });}catch(err0){}
        
       try{ 
            self.entity.addComponent("animation", {
       assets: asset.resource.animations,
            loop: true
        });
         var glbAnims= null;     glbAnims = self.entity.model.animations;
              
                    for (k = 0; k < glbAnims.length; ++k) {
                                         var anims = glbAnims[k];
                                  console.log( ":: ## "+anims[k]+ " @@ "+k);
                               }
          }catch(err2){}
        
         });
  • which gives me the error: Cannot read property ‘length’ of undefined

Animation resources are not on the model component, they are on the animation component:

            self.entity.addComponent("animation", {
       assets: asset.resource.animations,
            loop: true

ok, I found a solution to my problem by material-assignment in Blender (I was trying to access both ‘materials’ and ‘animations’) … And you did help :slight_smile: >> thanks

For future understanding (in case I run into similar issues):
I cannot write this without an error:


    self.entity.addComponent("animation", {
       assets: asset.resource.animations,
            loop: true
    
         var glbAnims= null;     glbAnims = self.entity.model.animations;
              
                    for (k = 0; k < glbAnims.length; ++k) {
                                         var anims = glbAnims[k];
                                  console.log( ":: ## "+anims[k]+ " @@ "+k);
                               } 
    });

PS: You dont have to use more than two minutes, as I already have a working solution - if it is at your fingertips, then great

I don’t understand the code in general. It looks like you are putting code directly into a JS object. Also, I don’t understand why you are trying to access animations from the model component? Animations are on the animation component as I previously mentioned.

Is this more like what you are trying to do?

    self.entity.addComponent("animation", {
        assets: asset.resource.animations,
        loop: true
    });

    var glbAnims = glbAnims = asset.resource.animations;

    for (k = 0; k < glbAnims.length; ++k) {
        var anim = glbAnims[k];
        console.log(":: ## " + anim + " @@ " + k);
    }

Well yes and no … I am a bit puzzled by the the component concept in general (the actual examples lies 95% of the time in forum-threads and not the API :-/ ). But again; you should not take that task upon you (I guess I will get there eventually, and overall the knowledge base is ‘quite ok’).

On the other hand, lets us say that I want to:

exchange the animation of an external glb/gltf
… then the content of the initially loaded glb comes into play - how would you replace it by code dynamically?
(for now, and with this headline, the focus and solved solution has been on surface/texture/materials)

Depends on what you want to change. For example, if you wanted to change materials on a model, you access the mesh instances and reassign the materials. Want to change the texture on a material? Get the material resource and change the appropriate texture maps.

What are you confused by?

What are you trying to do exactly? It’s probably going to be easier if there’s a concrete example for reference.