Models to share material but not Ambient Occlusion

Hi!

Is there a recommended approach to:

  1. Set a unique AO map per model (or template) while keeping the base material fully shared across all models/templates?
  2. Use a script attribute (e.g. this.aoMap as texture asset) on the model/template root to apply it to all meshInstances within that model, without material-cloning or per-instance changes?

Open for any suggested workflow to share materials and not create multiple model-specific ones. Updating a stainless-material would require a lot of headache if using model-specific materials.

Thank you!

Setting it on a MeshInstance should work I think.

Ok, but do you have any example how?

Any update to meshInstance.material would update everything on any object with the same material. And as far as I understand, you cannot set aoMap directly on the meshInstance. Or can I?

There’s this function on MeshInstance: MeshInstance | Engine API Reference - v2.12.3

That allows you to set parameters that override those set on the Material - but it’s not documented really on how this can be used.

You need to look at how standard material sets up parameters here:

textures are specifically done here, called from there:

based on this, it should be something like:

meshInstance.setParameter('texture_aoMap', texture);

That would be exactly what I’m looking for. But it doesn’t work. Code below:

var AoMapComponent = pc.createScript('aoMapComponent');

// Attribute for per-model AO map (exposed in Inspector)
AoMapComponent.attributes.add('aoMap', {
    type: 'asset',
    assetType: 'texture',
    title: 'AO Map',
    description: 'Model-specific AO texture (overrides per meshInstance, keeps shared material)'
});

AoMapComponent.prototype.initialize = function() {
    console.log('AO Component: Initializing for entity', this.entity.name);
    
    // Check Render component
    if (!this.entity.render || !this.entity.render.meshInstances.length) {
        console.warn('AO Component: Entity lacks Render component or meshes.');
        return;
    }
    console.log('AO Component: Render OK with', this.entity.render.meshInstances.length, 'meshes');

    // Function for AO application (order: set useAo + parameter BEFORE update)
    var applyAo = function() {
        // Skip if no AO set (keep shared base material)
        if (!this.aoMap || !this.aoMap.resource) {
            console.log('AO Component: No AO asset or resource set; retaining shared base material (e.g., stainless)');
            return;
        }
        
        console.log('AO Component: Applying AO via texture_aoMap uniform – resource ready!');
        
        this.entity.render.meshInstances.forEach((meshInstance) => {
            // Order: Enable AO first, set uniform, THEN update (ensures uniform binds in compilation)
            meshInstance.material.useAo = true;  // Enable AO shader
            meshInstance.setParameter('texture_aoMap', this.aoMap.resource);  // Set uniform BEFORE update
            meshInstance.material.aoStrength = 1.5;  // Stronger effect
            meshInstance.material.update();  // Update shader/bindings AFTER uniform set
            
            // Debug logs
            console.log('AO Component Debug: Uniform set?', meshInstance.parameters.texture_aoMap ? 'Yes' : 'No');
            console.log('AO Component Debug: Use AO active?', meshInstance.material.useAo);
        });
        
        console.log('AO Component: AO set via texture_aoMap on', this.entity.render.meshInstances.length, 'meshInstances – shared material preserved!');
    }.bind(this);

    // Apply directly if ready
    applyAo();

    // Load if not ready
    if (this.aoMap && !this.aoMap.resource) {
        console.log('AO Component: Loading AO asset (ID:', this.aoMap.id, ') for model...');
        this.app.assets.load(this.aoMap);
        this.aoMap.on('load', applyAo);
        this.aoMap.on('error', function(err) { console.error('AO Component: Load error:', err); });
    } else if (!this.aoMap) {
        console.log('AO Component: No AO asset; using shared base material');
    }
};

// Optional debug update (remove for production)
AoMapComponent.prototype.update = function(dt) {
    if (this.app.time % 5 < dt) console.log('AO Component: Active in runtime');
};

Maybe try and apply ao map on the material as well … just a single white pixel texture should be ok, shared by all materials. I think the shader generator only looks at the material to see what shader it needs to build. If there is no AO map there → shader does not use it, and so mesh instance override value is not used either.

Great, it works. Is this approach considered more performace friendly compared to material cloning?

Yes, this would perform better for sure.