[SOLVED] Lag using custom shader -> caching shaders

I’m using a custom shader on several UI elements, and on PC it takes about half second to execute this method: getShaderParameter

The way I’m using the shader is as described in some samples:

CustomShader.prototype.initialize = function() {
    var gd = this.app.graphicsDevice;

    var vertexShader = this.vs.resource;
    var fragmentShader = "precision " + gd.precision + " float;\n";
    fragmentShader = fragmentShader + this.fs.resource;

    // A shader definition used to create a new shader.
    var shaderDefinition = {
        attributes: {
            aPosition: pc.SEMANTIC_POSITION,
            aUv0: pc.SEMANTIC_TEXCOORD0
        },
        vshader: vertexShader,
        fshader: fragmentShader
    };

    // Create the shader from the definition
    this.shader = new pc.Shader(gd, shaderDefinition);

    // Create a new material and set the shader
    this.material = new pc.Material();
    this.entity.element.material = this.material;
    this.material.setShader(this.shader);
}

I suspect that doing this for each object is not the best.
Would it help to create only one instance of the shader?
The material could also be shared for objects that have the same settings.

It’s a pity that we can’t just create a new material in the editor and assign it vertex and fragment shaders, and use it as an asset.

How do you suggest to do it?
One option could be to have a sort of MaterialFactory and create there the materials by name passing the assets to use (the script and the two GLSL files), so that if it already exists it reuses it.

Before to proceed I’d like to know your opinion and if there are other solutions.

1 Like

so after some trials and tests, the lag is due to the creation of shaders only, not of materials, so the priority was to cache the shaders.

I made then a ShaderService that I use to create a new material with custom GLSL assets.
The service checks if a shader with those assets has been already created and returns it, otherwise it creates a new shader and adds it to the cache.

Here is the service:

var ShaderService = (function(){
    var _shaders = {};
    var _app;
    
    /**
     * In case of multiple application on the same page, 
     * is better to set the app as soon as possible on startup.
     */
    function setApp(app) {
        _app = app;
    }
    
    /**
     * It creates a new material by reusing existing shader instance or by creating a new one.
     */
    function createMaterial(vertexAsset, fragmentAsset, shaderAttributes) {
        var shader = _getShader(vertexAsset, fragmentAsset, shaderAttributes);
        var material = new pc.Material();
            material.setShader(shader);
        return material;
    }
    
    /**
     * It uses the assets id to create a unique key and search for a ready instance of the shader.
     * If it's not available, it creates a new shader and caches it.
     */
    function _getShader(vertexAsset, fragmentAsset, attributes) {
        attributes = attributes || {
            aPosition: pc.SEMANTIC_POSITION,
            aUv0: pc.SEMANTIC_TEXCOORD0
        };
        var key = vertexAsset.id + '_' + fragmentAsset.id;
        if (!_shaders[key]) {
            _app = _app || pc.Application.getApplication();
            var gd = _app.graphicsDevice;

            var vertexShader = vertexAsset.resource;
            var fragmentShader = "precision " + gd.precision + " float;\n";
            fragmentShader = fragmentShader + fragmentAsset.resource;

            // A shader definition used to create a new shader.
            var shaderDefinition = {
                attributes: attributes,
                vshader: vertexShader,
                fshader: fragmentShader
            };
            
            _shaders[key] = new pc.Shader(gd, shaderDefinition);
        }
        
        return _shaders[key];
    }
    
    return {
        setApp: setApp,
        createMaterial: createMaterial
    };
})();

Here is an example of custom shader script (applied to a 2D element) that uses the ShaderService:

var CustomShader = pc.createScript('customShader');

CustomShader.attributes.add('vs', {
    type: 'asset',
    assetType: 'shader',
    title: 'Vertex Shader'
});

CustomShader.attributes.add('fs', {
    type: 'asset',
    assetType: 'shader',
    title: 'Fragment Shader'
});

CustomShader.attributes.add('customSettings', {
    type: 'number',
    title: 'custom settings',
    default: 1
});

// initialize code called once per entity
CustomShader.prototype.initialize = function() {
    
    // it creates a new material by reusing the shader if it was already created
    this.material = ShaderService.createMaterial(this.vs, this.fs);

    this.material.blend = true;
    this.material.blendSrc = pc.gfx.BLENDMODE_SRC_ALPHA;
    this.material.blendDst = pc.gfx.BLENDMODE_ONE_MINUS_SRC_ALPHA;
    
    // it assigns the new material to the element
    this.entity.element.material = this.material;

    // subscribe for attributes changes, so to update the material parameters  
    this.on('attr', this._updateMaterial, this);

    // initializes the material's parameters
    this._updateMaterial();
};

CustomShader.prototype._updateMaterial = function() {
    this.material.setParameter('customSettings', this.customSettings);
};

CustomShader.prototype.swap = function(old) {
    this.material = old.material;
    old.off('attr');
    this.on('attr', this._updateMaterial, this);
};

Waiting for a native and easier visual way offered by the editor, this seems to be a viable solution to me.

3 Likes