Transparency for elements and sprites using opacityMap?

Trying to work with transparency in the context of supporting only ETC1 compression (no alpha channel). In another thread, a solution with a shader approach was advised, which gave some results, but as the edges are torn (and compression also contributes and changes the values of pixels that should be “transparent”).

Now I’m looking for how to effectively use the opacityMap replacement for sprites and UI elements. I would like some automated thing, but even in test mode there are difficulties.

Trying to manually set the opacityMap for an element or sprite I get strange behavior. Does anyone know what the problem is?

CustomOpacityMap.attributes.add('opacityMap', {type:'asset'});

CustomOpacityMap.prototype.postInitialize = function() {
    CustomOpacityMap.instance = this;
    if(this.entity.element) 
        CustomOpacityMap.setOpacityMap(this.entity.element._image.material, this.opacityMap.resource);
    else if(this.entity.sprite){
        CustomOpacityMap.setOpacityMap(this.entity.sprite.material, this.opacityMap.resource);
    }
};

CustomOpacityMap.setOpacityMap = function(material, opacityMap) {
    console.log('setOpacityMap\n', material, opacityMap);
    material.blendType = pc.BLEND_NORMAL;
    material.opacityMapChannel = 'r';
    material.opacityMap = opacityMap;
    material.update();
};

On the right is how it should look, and on the left is what it actually does.

My textures look like this.
image

I understood that the problem is that the effect of changing the opacityMap does not occur, that is, the same texture as the emissive (actual sprite) is used to calculate the transparency. So when I change the transparency channel to r (g or b), the corresponding colors become more transparent exactly in accordance with the emissiveMap (not opacityMap)

Tried also making it a non-public api as described in the code here.

Does anyone know how to get the desired behavior?

EDIT : Really don’t want to make a separate material for each sprite :frowning:

There should be an option to replace the texture for the opacity, because when the sprite is replaced, it affects the emissiveMap and the opacityMap. I can’t find ways to set them as separate textures (parameters such as offset, tiling, etc. can be left from the main texture, since in my case it’s the copy of same texture, only with the corresponding grayscale colors for setting transparency).

Still fighting.

The biggest strange thing for me now is that when changing the “texture_opacityMap” parameter, it visually looks like it also replaces the “texture_emissiveMap”, but the correct textures are written in the parameters, if believe the console.

Left and right elements lose their emission map. (Center element with manually adjusted material for reference only).

var CustomOpacityMap = pc.createScript('customOpacityMap');

CustomOpacityMap.testArray = [];
CustomOpacityMap.clonedMaterial = null;

//CustomOpacityMap.attributes.add('emissiveMap', {type:'asset'});
CustomOpacityMap.attributes.add('opacityMap', {type:'asset'});

CustomOpacityMap.prototype.postInitialize = function() {
    CustomOpacityMap.testArray.push(this);
    if(this.entity.element) this.changeOpacityMap();
};

/**@param {pc.ElementComponent} target */
CustomOpacityMap.prototype.changeOpacityMap = function(){
    if(CustomOpacityMap.clonedMaterial === null){
        /**@type {pc.StandardMaterial} */
        const newMaterial = this.entity.element.material.clone();
        newMaterial.blendType = pc.BLEND_NORMAL;
        newMaterial.opacityMapChannel = 'r';
        newMaterial.emissive.set(1, 1, 1);
        newMaterial.opacity = 1;
        newMaterial.update();
        CustomOpacityMap.clonedMaterial = newMaterial;
    }
    this.entity.element.material = CustomOpacityMap.clonedMaterial;
    console.log('element', this.entity.element);
    console.log('renderable', this.entity.element._image._renderable);
    //this.entity.element.material.opacityMap = this.opacityMap.resource;

    const parameters = this.entity.element._image._renderable.meshInstance.getParameters();
    console.log('parameters', parameters);

    this.entity.element._image._renderable.setParameter('texture_opacityMap', this.opacityMap.resource);
    //this.entity.element._image._renderable.setParameter('texture_emissiveMap', this.emissiveMap.resource);
    //this.entity.element.material.updateUniforms();
    //this.entity.element.material.update();
};

What’s new in the code is that I create a new material that will be used for all elements with a custom opacityMap (due to the need to change the opacityMapChannel to ‘r’ from ‘a’ just for it) and leave the standard material unchanged.

Adding this on line 22 solves the problem, it seems in the default material opacityMap and emissiveMap refer to the same pc.Texture object, it’s not very obvious as it still uses 2 parameters.

newMaterial.opacityMap = new pc.Texture(this.app.graphicsDevice);

var CustomOpacityMap = pc.createScript('customOpacityMap');

CustomOpacityMap.testArray = [];
CustomOpacityMap.clonedMaterial = null;

CustomOpacityMap.attributes.add('opacityMap', {type:'asset'});

CustomOpacityMap.prototype.postInitialize = function() {
    CustomOpacityMap.testArray.push(this);
    if(this.entity.element) this.changeOpacityMap();
};

/**@param {pc.ElementComponent} target */
CustomOpacityMap.prototype.changeOpacityMap = function(){
    if(CustomOpacityMap.clonedMaterial === null){
        /**@type {pc.StandardMaterial} */
        const newMaterial = this.entity.element.material.clone();
        newMaterial.blendType = pc.BLEND_NORMAL;
        newMaterial.opacityMapChannel = 'r';
        newMaterial.emissive.set(1, 1, 1);
        newMaterial.opacity = 1;
        newMaterial.opacityMap = new pc.Texture(this.app.graphicsDevice);
        newMaterial.update();
        CustomOpacityMap.clonedMaterial = newMaterial;
    }
    this.entity.element.material = CustomOpacityMap.clonedMaterial;
    this.entity.element._image._renderable.setParameter('texture_opacityMap', this.opacityMap.resource);
};

Now I’m trying to automate the assignment of these opacityMaps. The preliminary plan is to override some prototype methods of the engine components/systems so that when assigning a texture to element/sprite Component, it searches the registry for a texture name that corresponds to the format “opacity-” + originalTextureName and, if there is one, binds it in the same way as a sprite asset is bound to an atlas asset.

1 Like

‘opacity-’ prefix only thing needed (both compressed from editor with Alpha = false)
image

const app = pc.Application.getApplication();
//console.log('opacityMapping init', app);
/**@type {pc.Asset} */
const textureAssets = app.assets.filter((asset)=>asset.type === 'textureatlas' || asset.type === 'texture');
for(const textureAsset of textureAssets){
    /**@type {pc.Asset} */
    const opacityMapAsset = app.assets.find('opacity-'+textureAsset.name);
    textureAsset.data.opacityMapAssetId = opacityMapAsset !== null ? opacityMapAsset.id : -1;

    if(opacityMapAsset !== null) {
        //console.log(textureAsset.name, '->', opacityMapAsset.name);
        //app.assets.load(textureAsset);
        textureAsset.ready(()=>{
            textureAsset.resource.opacityMapAsset = opacityMapAsset;
            app.assets.load(opacityMapAsset);
            opacityMapAsset.ready(()=>{
                textureAsset.resource.opacityMap = opacityMapAsset.resource;
                //console.log('textureAtlas', textureAsset.resource);
            });
        });
    }
}

// IMAGE ELEMENT COMPONENT

/**@param {pc.Asset} opacityMapAsset*/
pc.ImageElement.prototype._onOpacityMapLoaded = function(opacityMapAsset){
    if(this._spriteAsset && this._spriteAsset != null){
        const spriteAsset = app.assets.get(this._spriteAsset);
        const atlasAssetId = spriteAsset.data.textureAtlasAsset;
        /**@type {pc.Asset} */
        const atlasAsset = app.assets.get(atlasAssetId);
        const opacityMapAssetId = atlasAsset.data.opacityMapAssetId;
        if(opacityMapAssetId !== opacityMapAsset.id) return;
        //console.log('.sprite _onOpacityMapLoaded', opacityMapAsset.name, opacityMapAsset);
        spriteAsset.resource.atlas.opacityMap = opacityMapAsset.resource;
        this._onSpriteAssetLoad(spriteAsset);
    }
    else if(this._textureAsset && this._textureAsset != null){
        const textureAsset = app.assets.get(this._textureAsset);
        const opacityMapAssetId = textureAsset.data.opacityMapAssetId;
        if(opacityMapAssetId !== opacityMapAsset.id) return;
        //console.log('.texture _onOpacityMapLoaded', opacityMapAsset.name, opacityMapAsset);
        textureAsset.resource.opacityMap = opacityMapAsset.resource;
        this._onTextureLoad(textureAsset);
    }
};

pc.ImageElement.prototype._updateMaterial = function(screenSpace) {
    const mask = !!this._mask;
    const nineSliced = !!(this.sprite && this.sprite.renderMode === pc.SPRITE_RENDERMODE_SLICED);
    const nineTiled = !!(this.sprite && this.sprite.renderMode === pc.SPRITE_RENDERMODE_TILED);
    if (!this._hasUserMaterial()) {

        if(this.sprite && this.sprite.atlas && this.sprite.atlas.opacityMap 
            || this.texture && this.texture.opacityMap)
            this._material = this._system.getImageElementMaterialWithOpacityMap(screenSpace, mask, nineSliced, nineTiled);
        else 
            this._material = this._system.getImageElementMaterial(screenSpace, mask, nineSliced, nineTiled);
    }
    //console.log('_updateMaterial', this._material.name, this._element.entity.name)
    if (this._renderable) {
        // culling is always true for non-screenspace (frustrum is used); for screenspace, use the 'cull' property
        this._renderable.setCull(!this._element._isScreenSpace() || this._element._isScreenCulled());
        this._renderable.setMaterial(this._material);
        this._renderable.setScreenSpace(screenSpace);
        this._renderable.setLayer(screenSpace ? pc.LAYER_HUD : pc.LAYER_WORLD);
    }
}

// .sprite
pc.ImageElement.prototype._onSpriteAssetLoad = function(spriteAsset){
    if (!spriteAsset || !spriteAsset.resource) {
        this.sprite = null;
        //console.warn('_onSpriteAssetLoad fail', this._entity.name);
    } else {
        /**@type {pc.AssetRegistry} */
        const assets = app.assets;
        const atlasAssetId = spriteAsset.data.textureAtlasAsset;
        /**@type {pc.Asset} */
        const atlasAsset = app.assets.get(atlasAssetId);
        const opacityMapAssetId = atlasAsset.data.opacityMapAssetId;
        //console.warn('_onSpriteAssetLoad', this._entity.name);

        if (!spriteAsset.resource.atlas) {
            if (atlasAssetId) {
                assets.off('load:' + atlasAssetId, this._onTextureAtlasLoad, this);
                assets.once('load:' + atlasAssetId, this._onTextureAtlasLoad, this);
            }
        } else if(opacityMapAssetId !== -1 && !atlasAsset.resource.opacityMap){
            /**@type {pc.Asset} */
            const opacityMapAsset = app.assets.get(opacityMapAssetId);
            //console.log('opacity check', spriteAsset.resource.atlas.texture.name, opacityMapAsset);

            if(!opacityMapAsset.resource){
                assets.load(opacityMapAsset);
                assets.off('load:' + opacityMapAssetId, this._onOpacityMapLoaded, this);
                assets.once('load:' + opacityMapAssetId, this._onOpacityMapLoaded, this);
            } else {
                //opacityMapAsset.ready(this._onOpacityMapLoaded, this);
                this._onOpacityMapLoaded(opacityMapAsset);
                //app.once('update', ()=>this._onOpacityMapLoaded(opacityMapAsset));
            }
            
        } else {
            
            this.sprite = spriteAsset.resource;
        }
    }
};

Object.defineProperty(pc.ImageElement.prototype, 'sprite', {
    get: function() {
        return this._sprite;
    },
    set: function(value) {
        //console.log('set sprite', value, this);
        // Your custom implementation here
        if (this._sprite === value) return;

        if (this._sprite) {
            this._unbindSprite(this._sprite);
        }

        if (this._spriteAsset) {
            const spriteAsset = this._system.app.assets.get(this._spriteAsset);
            if (spriteAsset && spriteAsset.resource !== value) {
                this.spriteAsset = null;
            }
        }

        this._sprite = value;

        if (this._sprite) {
            this._bindSprite(this._sprite);

            // clear texture if sprite is being set
            if (this._textureAsset) {
                this.textureAsset = null;
            }
        }

        if (this._sprite && this._sprite.atlas && this._sprite.atlas.texture) {
            // default texture just uses emissive and opacity maps
            //console.log('atlas', this._sprite.atlas);
            //console.log('opacityMap', this._sprite.atlas.opacityMap);
            this._renderable.setParameter('texture_emissiveMap', this._sprite.atlas.texture);
            this._renderable.setParameter('texture_opacityMap', this._sprite.atlas.opacityMap ? this._sprite.atlas.opacityMap : this._sprite.atlas.texture);
            //console.log('parameters', this._renderable.meshInstance.parameters);
        } else {
            // clear texture params
            this._renderable.deleteParameter('texture_emissiveMap');
            this._renderable.deleteParameter('texture_opacityMap');
        }

        // clamp frame
        if (this._sprite) {
            this._spriteFrame = pc.math.clamp(this._spriteFrame, 0, this._sprite.frameKeys.length - 1);
        }

        this._updateSprite();
    }
});

pc.ElementComponentSystem.prototype.getImageElementMaterialWithOpacityMap = function(screenSpace, mask, nineSliced, nineSliceTiled){
    const indexString = 'opacity-' + screenSpace.toString() + mask.toString() + nineSliced.toString() + nineSliceTiled.toString();
    /**@type {pc.StandardMaterial} */
    var material = this.getImageElementMaterialWithOpacityMap[indexString];
    if(material !== undefined) 
    {
        //console.log('return from array', material);
        return material;
    }
    material = this.getImageElementMaterial(screenSpace, mask, nineSliced, nineSliceTiled).clone();
    //material.opacity = 1;
    //material.emissive = pc.Color.WHITE;
    material.emissiveMap = new pc.Texture(app.graphicsDevice, {
        width: 1,
        height: 1,
        format: pc.PIXELFORMAT_RGB8,
        name: 'element-system'
    });
    material.opacityMap = new pc.Texture(app.graphicsDevice, {
        width: 1,
        height: 1,
        format: pc.PIXELFORMAT_RGB8,
        name: 'element-system'
    });
    material.name += 'WithOpacityMap'
    material.opacityMapChannel = 'r';
    this.getImageElementMaterialWithOpacityMap[indexString] = material;
    this.defaultImageMaterials.push(material);
    material.update();
    //console.log('create and return', material);
    return material;
};

// .texture
pc.ImageElement.prototype._onTextureLoad = function(textureAsset) {
    const opacityMapAssetId = textureAsset.data.opacityMapAssetId;
    if(opacityMapAssetId !== -1 && !textureAsset.resource.opacityMap){
        /**@type {pc.Asset} */
        const opacityMapAsset = app.assets.get(opacityMapAssetId);
        //console.log('opacity check', spriteAsset.resource.atlas.texture.name, opacityMapAsset);

        if(!opacityMapAsset.resource){
            app.assets.load(opacityMapAsset);
            app.assets.off('load:' + opacityMapAssetId, this._onOpacityMapLoaded, this);
            app.assets.once('load:' + opacityMapAssetId, this._onOpacityMapLoaded, this);
        } else {
            //opacityMapAsset.ready(this._onOpacityMapLoaded, this);
            this._onOpacityMapLoaded(opacityMapAsset);
            //app.once('update', ()=>this._onOpacityMapLoaded(opacityMapAsset));
        }
    } else {
        this.texture = textureAsset.resource;
    }
}

Object.defineProperty(pc.ImageElement.prototype, 'texture', {
    get: function(){
        return this._texture;
    },
    set: function(value){
        if (this._texture === value) return;

        if (this._textureAsset) {
            const textureAsset = this._system.app.assets.get(this._textureAsset);
            if (textureAsset && textureAsset.resource !== value) {
                this.textureAsset = null;
            }
        }

        this._texture = value;

        if (value) {

            // clear sprite asset if texture is set
            if (this._spriteAsset) {
                this.spriteAsset = null;
            }

            // default texture just uses emissive and opacity maps
            this._renderable.setParameter('texture_emissiveMap', this._texture);
            this._renderable.setParameter('texture_opacityMap', this._texture.opacityMap ? this._texture.opacityMap : this._texture);
            this._colorUniform[0] = this._color.r;
            this._colorUniform[1] = this._color.g;
            this._colorUniform[2] = this._color.b;
            this._renderable.setParameter('material_emissive', this._colorUniform);
            this._renderable.setParameter('material_opacity', this._color.a);
            //console.log('set .texture', this._renderable);

            // if texture's aspect ratio changed and the element needs to preserve aspect ratio, refresh the mesh
            const newAspectRatio = this._texture.width / this._texture.height;
            if (newAspectRatio !== this._targetAspectRatio) {
                this._targetAspectRatio = newAspectRatio;
                if (this._element.fitMode !== pc.FITMODE_STRETCH) {
                    this.refreshMesh();
                }
            }
            if(this._texture.opacityMap){
                this._updateMaterial(this._element._isScreenSpace());
            }
        } else {
            // clear texture params
            this._renderable.deleteParameter('texture_emissiveMap');
            this._renderable.deleteParameter('texture_opacityMap');

            // reset target aspect ratio and refresh mesh if there is an aspect ratio setting
            // this is needed in order to properly reset the mesh to 'stretch' across the entire element bounds
            // when resetting the texture
            this._targetAspectRatio = -1;
            if (this._element.fitMode !== pc.FITMODE_STRETCH) {
                this.refreshMesh();
            }
        }
    }
});


// SPRITE COMPONENT

Object.defineProperty(pc.SpriteAnimationClip.prototype, 'sprite', {
    get: function() {
        return this._sprite;
    },
    set: function(value) {
        if (this._sprite) {
            this._sprite.off('set:meshes', this._onSpriteMeshesChange, this);
            this._sprite.off('set:pixelsPerUnit', this._onSpritePpuChanged, this);
            this._sprite.off('set:atlas', this._onSpriteMeshesChange, this);
            if (this._sprite.atlas) {
                this._sprite.atlas.off('set:texture', this._onSpriteMeshesChange, this);
            }
        }

        this._sprite = value;

        if (this._sprite) {
            this._sprite.on('set:meshes', this._onSpriteMeshesChange, this);
            this._sprite.on('set:pixelsPerUnit', this._onSpritePpuChanged, this);
            this._sprite.on('set:atlas', this._onSpriteMeshesChange, this);

            if (this._sprite.atlas) {
                this._sprite.atlas.on('set:texture', this._onSpriteMeshesChange, this);
            }
        }

        if (this._component.currentClip === this) {
            let mi;

            // if we are clearing the sprite clear old mesh instance parameters
            if (!value || !value.atlas) {
                mi = this._component._meshInstance;
                if (mi) {
                    mi.deleteParameter('texture_emissiveMap');
                    mi.deleteParameter('texture_opacityMap');
                }

                this._component._hideModel();
            } else {
                // otherwise show sprite

                // update texture
                if (value.atlas.texture) {
                    mi = this._component._meshInstance;
                    if (mi) {
                        mi.setParameter('texture_emissiveMap', value.atlas.texture);
                        mi.setParameter('texture_opacityMap', value.atlas.opacityMap ? value.atlas.opacityMap : value.atlas.texture);
                    }

                    if (this._component.enabled && this._component.entity.enabled) {
                        this._component._showModel();
                    }
                }

                // if we have a time then force update
                // frame based on the time (check if fps is not 0 otherwise time will be Infinity)

                /* eslint-disable no-self-assign */
                if (this.time && this.fps) {
                    this.time = this.time;
                } else {
                    // if we don't have a time
                    // then force update frame counter
                    this.frame = this.frame;
                }
                /* eslint-enable no-self-assign */
            }
        }
    }
});

pc.SpriteAnimationClip.prototype._onSpriteAssetLoad = function(spriteAsset) {
    if (!spriteAsset || !spriteAsset.resource) {
        this.sprite = null;
        //console.warn('_onSpriteAssetLoad fail', this._entity.name);
    } else {
        /**@type {pc.AssetRegistry} */
        const assets = app.assets;
        const atlasAssetId = spriteAsset.data.textureAtlasAsset;
        /**@type {pc.Asset} */
        const atlasAsset = app.assets.get(atlasAssetId);
        //console.warn('_onSpriteAssetLoad', this._entity.name);

        if (!spriteAsset.resource.atlas) {
            if (atlasAssetId) {
                assets.off('load:' + atlasAssetId, this._onTextureAtlasLoad, this);
                assets.once('load:' + atlasAssetId, this._onTextureAtlasLoad, this);
            }
        } else if(atlasAsset.data.opacityMapAssetId !== -1 && !atlasAsset.resource.opacityMap){
            /**@type {pc.Asset} */
            const opacityMapAsset = app.assets.get(atlasAsset.data.opacityMapAssetId);
            //console.log('opacity check', spriteAsset.resource.atlas.texture.name, opacityMapAsset);

            if(!opacityMapAsset.resource){
                assets.load(opacityMapAsset);
                assets.off('load:' + atlasAsset.data.opacityMapAssetId, this._onOpacityMapLoaded, this);
                assets.once('load:' + atlasAsset.data.opacityMapAssetId, this._onOpacityMapLoaded, this);
            } else {
                //opacityMapAsset.ready(this._onOpacityMapLoaded, this);
                //app.once('update', ()=>this._onOpacityMapLoaded(opacityMapAsset));
                this._onOpacityMapLoaded(opacityMapAsset);
            }
            
        } else {
            //console.warn('_onSpriteAssetLoad try', this._component.entity.name);
            this.sprite = spriteAsset.resource;
        }
    }
}

/**@param {pc.Asset} opacityMapAsset*/
pc.SpriteAnimationClip.prototype._onOpacityMapLoaded = function(opacityMapAsset){
    if(!this._spriteAsset || this._spriteAsset == null) return;
    const spriteAsset = app.assets.get(this._spriteAsset);
    const atlasAssetId = spriteAsset.data.textureAtlasAsset;
    /**@type {pc.Asset} */
    const atlasAsset = app.assets.get(atlasAssetId);
    const opacityMapAssetId = atlasAsset.data.opacityMapAssetId;
    if(opacityMapAssetId !== opacityMapAsset.id) return;
    //console.log('SPRITE COMPONENT _onOpacityMapLoaded', opacityMapAsset.name, opacityMapAsset);
    spriteAsset.resource.atlas.opacityMap = opacityMapAsset.resource;
    this._onSpriteAssetLoad(spriteAsset);
};


const PARAM_EMISSIVE_MAP = 'texture_emissiveMap';
const PARAM_OPACITY_MAP = 'texture_opacityMap';
const PARAM_EMISSIVE = 'material_emissive';
const PARAM_OPACITY = 'material_opacity';
const PARAM_INNER_OFFSET = 'innerOffset';
const PARAM_OUTER_SCALE = 'outerScale';
const PARAM_ATLAS_RECT = 'atlasRect';

pc.SpriteComponent.prototype._showFrame = function(frame) {
    if (!this.sprite) return;
    const mesh = this.sprite.meshes[frame];
    // if mesh is null then hide the mesh instance
    if (!mesh) {
        if (this._meshInstance) {
            this._meshInstance.mesh = null;
            this._meshInstance.visible = false;
        }
        return;
    }
    const material = this.sprite.atlas.opacityMap ? this._getSpriteMaterialWithOpacityMap() : this._getSpriteMaterial();

    if (!this._meshInstance) {
        this._meshInstance = new pc.MeshInstance(mesh, this._material, this._node);
        this._meshInstance.castShadow = false;
        this._meshInstance.receiveShadow = false;
        this._meshInstance.drawOrder = this._drawOrder;
        this._model.meshInstances.push(this._meshInstance);
        // set overrides on mesh instance
        this._colorUniform[0] = this._color.r;
        this._colorUniform[1] = this._color.g;
        this._colorUniform[2] = this._color.b;
        this._meshInstance.setParameter(PARAM_EMISSIVE, this._colorUniform);
        this._meshInstance.setParameter(PARAM_OPACITY, this._color.a);
        // now that we created the mesh instance, add the model to the scene
        if (this.enabled && this.entity.enabled) {
            this._showModel();
        }
    }
    // update material
    if (this._meshInstance.material !== material) {
        this._meshInstance.material = material;
    }
    // update mesh
    if (this._meshInstance.mesh !== mesh) {
        this._meshInstance.mesh = mesh;
        this._meshInstance.visible = true;
        // reset aabb
        this._meshInstance._aabbVer = -1;
    }
    // set texture params
    if (this.sprite.atlas && this.sprite.atlas.texture) {
        this._meshInstance.setParameter(PARAM_EMISSIVE_MAP, this.sprite.atlas.texture);
        this._meshInstance.setParameter(PARAM_OPACITY_MAP, this.sprite.atlas.opacityMap ? this.sprite.atlas.opacityMap : this.sprite.atlas.texture);
    } else {
        // no texture so reset texture params
        this._meshInstance.deleteParameter(PARAM_EMISSIVE_MAP);
        this._meshInstance.deleteParameter(PARAM_OPACITY_MAP);
    }
    // for 9-sliced
    if (this.sprite.atlas && (this.sprite.renderMode === pc.SPRITE_RENDERMODE_SLICED || this.sprite.renderMode === pc.SPRITE_RENDERMODE_TILED)) {
        // set custom aabb function
        this._meshInstance._updateAabbFunc = this._updateAabbFunc;
        // calculate inner offset
        const frameData = this.sprite.atlas.frames[this.sprite.frameKeys[frame]];
        if (frameData) {
            const borderWidthScale = 2 / frameData.rect.z;
            const borderHeightScale = 2 / frameData.rect.w;
            this._innerOffset.set(
                frameData.border.x * borderWidthScale,
                frameData.border.y * borderHeightScale,
                frameData.border.z * borderWidthScale,
                frameData.border.w * borderHeightScale
            );
            const tex = this.sprite.atlas.texture;
            this._atlasRect.set(frameData.rect.x / tex.width,
                                frameData.rect.y / tex.height,
                                frameData.rect.z / tex.width,
                                frameData.rect.w / tex.height
            );
        } else {
            this._innerOffset.set(0, 0, 0, 0);
        }
        // set inner offset and atlas rect on mesh instance
        this._innerOffsetUniform[0] = this._innerOffset.x;
        this._innerOffsetUniform[1] = this._innerOffset.y;
        this._innerOffsetUniform[2] = this._innerOffset.z;
        this._innerOffsetUniform[3] = this._innerOffset.w;
        this._meshInstance.setParameter(PARAM_INNER_OFFSET, this._innerOffsetUniform);
        this._atlasRectUniform[0] = this._atlasRect.x;
        this._atlasRectUniform[1] = this._atlasRect.y;
        this._atlasRectUniform[2] = this._atlasRect.z;
        this._atlasRectUniform[3] = this._atlasRect.w;
        this._meshInstance.setParameter(PARAM_ATLAS_RECT, this._atlasRectUniform);
    } else {
        this._meshInstance._updateAabbFunc = null;
    }
    this._updateTransform();
    //console.log('showFrame end', this);
}

pc.SpriteComponent.prototype._getSpriteMaterial = function(){
    if (this.sprite.renderMode === pc.SPRITE_RENDERMODE_SLICED) {
        return this.system.default9SlicedMaterialSlicedMode;
    } else if (this.sprite.renderMode === pc.SPRITE_RENDERMODE_TILED) {
        return this.system.default9SlicedMaterialTiledMode;
    } else {
        return this.system.defaultMaterial;
    }
};

pc.SpriteComponent.prototype._getSpriteMaterialWithOpacityMap = function(){
    //console.log('_getSpriteMaterial', this)
    /**@type {pc.StandardMaterial} */
    var material;
    if (this.sprite.renderMode === pc.SPRITE_RENDERMODE_SLICED) {
        material = this.system.default9SlicedMaterialSlicedModeWithOpacityMap;
        if(material) 
            return material;
        else {
            material = this.system.default9SlicedMaterialSlicedMode.clone();
            this.system.default9SlicedMaterialSlicedModeWithOpacityMap = material;
        }
    } else if (this.sprite.renderMode === pc.SPRITE_RENDERMODE_TILED) {
        material = this.system.default9SlicedMaterialTiledModeWithOpacityMap;
        if(material) 
            return material;
        else {
            material = this.system.default9SlicedMaterialTiledMode.clone();
            this.system.default9SlicedMaterialTiledModeWithOpacityMap = material;
        }
    } else {
        material = this.system.defaultMaterialWithOpacityMap;
        if(material) 
            return material;
        else{
            material = this.system.defaultMaterial.clone();
            this.system.defaultMaterialWithOpacityMap = material;
        }
    }
    material.name += 'WithOpacityMap';
    material.emissiveMap = new pc.Texture(app.graphicsDevice, {
        width: 1,
        height: 1,
        format: pc.PIXELFORMAT_RGB8,
        name: 'element-system'
    });
    material.opacityMap = new pc.Texture(app.graphicsDevice, {
        width: 1,
        height: 1,
        format: pc.PIXELFORMAT_RGB8,
        name: 'element-system'
    });
    material.opacityMapChannel = 'r';
    return material;
};

Edit 1: Some fixes for support ui masking and non-preloaded assets.

Edit 2: Some fixes for stability of dynamic loading along with mask and other image-element tools.

Edit 3: Some improvements in logic for different use cases. Added support for sprite component (including animated).

Edit 4: Added support for using .textureAsset

Note: Sprite replacement from code better via spriteAsset property, not sprite property (or make sure you loaded ‘opacity-’ texture before assigning sprite).

1 Like