‘opacity-’ prefix only thing needed (both compressed from editor with Alpha = false)
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).