[SOLVED] Cloning top level - missing materials

I’m cloning an Entity with model components and entity children (with their own models).

The clones do not have copies of the assigned materials. So I’ve added them to the top model. but realise I have to traverse down the tree and find and copy them all.
Is there a way to clone() and get all the children’s fully copied ?

Aside: I’m programmatically creating many Entities all using the same model. I’m doing it using this.app.assets.find("objectname.json", "model"); approach. Is this approach using the asset system using instances or taking up lots of duplicate room ? Or should I be cloning ?

you can see it here.
If you double click on an item - it will clone it and put it in the first white square.
clicking on that square will move it up and you can see the missing materials on the children

Interesting. I mean I’d expect what you are doing to work. There are “asset materials” and “entity materials” which control whether all versions share one material or get their own (you’d normally use that because you want to “fade out” one thing, or flash it or something).

Personally if I had a model I used everywhere I’d just stick the result of this.app.assets.find(“xyz”) in a global variable (or you know, something module static) and keep using it from there rather than searching every time. That said, I can’t think why it isn’t working if there isn’t a bug. If you don’t mind sharing your project ID I’ll fork it and take a look if you like.

Looks really nice btw.

ok the project is here:

All the relevant code for this issue is in setup.js

The Setup.prototype.initialize (on line 35) calls Setup.prototype.build_plinth (on line 384) which does all the work of initially creating the entities.
The entities are a top_node with a model component which is the plinth, and several children for semester blocks and the subject name and paperid. All of which are structured under top_node.
These are all also recorded (for easy traversal) in this.entities

When you do an L2 gesture then the function add_to_course (line 296) is the one that does the cloning (nto this.courses) and which is not maintaining the materials.

Setup.prototype.add_to_course = function(paper) {
    // add selected paper to current course
    var idx = this.current_course_id;
    var courses_container = this.app.root.findByName("Courses");
    var course = this.courses[idx];
    var transform_node = course.children[0];
    // have we added it already
    var names = this.get_course_papernames(course);
    if (names.indexOf(paper.name) == -1) {
        // add a clone into the Course
        var copy = paper.clone(); // hey where's my materials
        // ugly single repair :(
        var mat = paper.model.meshInstances[0].mat;
        this.slam_material(copy, paper.model.meshInstances[0].material);
        course.children[0].addChild(copy); // under transform node
        // order it positionally
        this.layout(course.children[0], idx);

You can see my ugly bodge to get the top_node model (plinth) to have its material.

Nicely written that - though wow, you do like returning arrays from functions :smile:

Ok so when I trace through .clone() I see it’s doing nothing to clone the material of the meshInstance at all. If you want to do that you are going to have to do it yourself for now, probably needs a fault report in the Engine. It is cloning the default material of the model (which is used in the absence of meshInstance materials from memory). That is still set to the system default material so nothing much is happening there either.

I guess you are going to need to write a clone the clones materials method or maybe you can utilise the model material rather than using the one on meshInstances.


function clone(entity) {
     var result = entity.clone();
     if(result.model) {
         var source = entity.model.model.meshInstances
         result.model.model.meshInstances.forEach(function(i, index) { i.material = source[index].material });
    return result;

///And if you are me and monkey patch everything

ver oldClone = pc.Entity.prototype.clone
pc.Entity.prototype.clone = function() {
   var result = oldClone.call(this);
     if(result.model) {
         var source = this.model.model.meshInstances
         result.model.model.meshInstances.forEach(function(i, index) { i.material = source[index].material });
    return result;

IDK you might also want to clone the material in that loop, but if you don’t have to it would be an advantage.

Thanks for the insight. As usual, very helpful.

OK - as the models don’t change I can just write a more dedicated slam materials to force them all on.
But you can see why I assumed cloning an Entity would also clone, or reinstance their materials.
So glad to see I’m not really using it wrong.

Ha ha yes - returning arrays. I’m an old time lisp and now Python hacker. I can’t quite really glom onto how I’m supposed to be using Javascript effectively. I thought about returning {“name”: value, …} instead but you never know what they’re called and how many there are. so going with arrays as lists instead.
What’s the accepted way ?

I see your monkeypatching which is great for a specific project but I’d lose track of what the expected behaviour was as i moved between projects (too old, memory failing :))


I made a PR that should fix this here https://github.com/playcanvas/engine/pull/1000 but I’m not merging it yet just to make sure it doesn’t break anything. Feel free to try it though and see if it fixes your issue. (You could just monkeypatch pc.ModelComponentSystem#cloneComponent as a test).

Well I have libraries of these things I just install lol. I guess in JS we tend to return objects so that the properties of them are clear in the code. With ES6 structuring and destructuring operators make this even more common.

let {length, x} = someObjectWithLengthAndX; //destructuring
console.log(length * x);

But don’t go doing that unless you are using Babel or a very modern browser as the target :slight_smile:

sorry @vaios I had a go at monkypatching it but it surpasses my abilities :frowning:

What @vaios means is, replace PlayCanvas’ implementation of cloneComponent by adding the following to the top of one of your scripts in your project:

pc.ModelComponentSystem.prototype.cloneComponent = function (entity, clone) {
             var data = {
                 type: entity.model.type,
                 asset: entity.model.asset,
                 castShadows: entity.model.castShadows,
                 receiveShadows: entity.model.receiveShadows,
                 castShadowsLightmap: entity.model.castShadowsLightmap,
                 lightmapped: entity.model.lightmapped,
                 lightmapSizeMultiplier: entity.model.lightmapSizeMultiplier,
                 isStatic: entity.model.isStatic,
                 enabled: entity.model.enabled,
                 mapping: pc.extend({}, entity.model.mapping)
             // if original has a different material
             // than the assigned materialAsset then make sure we
             // clone that one instead of the materialAsset one
             var materialAsset = entity.model.materialAsset;
             if (!(materialAsset instanceof pc.Asset) && materialAsset != null) {
                 materialAsset = this.app.assets.get(materialAsset);
             var material = entity.model.material;
             if (!material ||
                 material === pc.ModelHandler.DEFAULT_MATERIAL ||
                 !materialAsset ||
                 material === materialAsset.resource) {
                 data.materialAsset = materialAsset;
             var component = this.addComponent(clone, data);
             if (!data.materialAsset)
                 component.material = material;
             if (entity.model.model) {
                 var meshInstances = entity.model.model.meshInstances;
                  var meshInstancesClone = component.model.meshInstances;
                  for (var i = 0; i < meshInstances.length; i++) {
                      meshInstancesClone[i].mask = meshInstances[i].mask;
                      meshInstancesClone[i].material = meshInstances[i].material;
                      meshInstancesClone[i].layer = meshInstances[i].layer;
                      meshInstancesClone[i].receiveShadow = meshInstances[i].receiveShadow;

excellent thanks @will
I tried it and worked just fine. Thanks for the explanation as well. Its coming clearer to me.
So much better than my ugly bodge

Don’t forget to remove the patch when the fix finally gets deployed. Should be in the next few days… Maybe keep an eye on GitHub. :slight_smile: