UI Elements are not being rendered after cloning material

Hey hey :slight_smile:

We are currently working on a custom shader chunk to add to one of our ui elements. The chunk itself works fine, but we noticed that it also affects all of our other ui elements. They all share the same material, so this makes sense. We tried cloning the material, adding the chunk, setting the new material on our element and updating it. But now the ui element is not visible at all and we have no idea how to fix it.

After a bit of testing, this is the minimal code to try and reproduce the issue:

TestScript.prototype.initialize() {
    var material = this.entity.element.material.clone();
    this.entity.element.material = material;
    material.update();
}

Our element originally has a sprite reference, so maybe by setting the material property we are actually overriding the sprite asset? Is there a different way to change the material? Or are we missing something else?

Thanks for the help

2 Likes

It should work :thinking:

But like you, managed to reproduce the issue. Will need a closer look by the team.

2 Likes

After some digging, we found out that our image is just transparent. Once we manually set the opacity, we also saw that it was tinted with a grey color. So we looked through the engine and found this function in system.js:

_createBaseImageMaterial() {
    var material = new StandardMaterial();

    material.diffuse.set(0, 0, 0); // black diffuse color to prevent ambient light being included
    material.emissive.set(0.5, 0.5, 0.5); // use non-white to compile shader correctly
    material.emissiveMap = this._defaultTexture;
    material.emissiveTint = true;
    material.opacityMap = this._defaultTexture;
    material.opacityMapChannel = "a";
    material.opacityTint = true;
    material.opacity = 0; // use non-1 opacity to compile shader correctly
    material.useLighting = false;
    material.useGammaTonemap = false;
    material.useFog = false;
    material.useSkybox = false;
    material.blendType = BLEND_PREMULTIPLIED;
    material.depthWrite = false;

    return material;
}

If I understand correctly, these values of the material will never change. The attributes on the element component will only ever be set on the element instance itself and through it’s setters as a uniform in the shader. So we need to set all the uniforms manually that we need to display the sprite, or force the setters to set the uniform again.

What we also found was this setter in image-element.js:

set material(value) {
    if (this._material === value) return;

    if (!value) {
        var screenSpace = this._element._isScreenSpace();
        if (this.mask) {
            value = screenSpace ? this._system.defaultScreenSpaceImageMaskMaterial : this._system.defaultImageMaskMaterial;
        } else {
            value = screenSpace ? this._system.defaultScreenSpaceImageMaterial : this._system.defaultImageMaterial;
        }
    }

    this._material = value;
    if (value) {
        this._renderable.setMaterial(value);

        // if this is not the default material then clear color and opacity overrides
        if (this._hasUserMaterial()) {
            this._renderable.deleteParameter('material_opacity');
            this._renderable.deleteParameter('material_emissive');
        } else {
            // otherwise if we are back to the defaults reset the color and opacity
            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);
        }
    }
}

Since we are setting a new material, which is now not a base material anymore, this._hasUserMaterial() returns true and the 2 uniforms 'material_opacity' and 'material_emissive' are removed. This is the reason we don’t see anything.

So our current solution would need to be something like this:

TestScript.prototype.initialize() {
    var material = this.entity.element.material.clone();
    this.entity.element.material = material;

    var opacity = this.entity.element.opacity;
    this.entity.element.opacity = 0.9; // if we set it to it's own value, the setter is gonna return early
    this.entity.element.opacity = opacity;

    var color = this.entity.element.color.clone();
    this.entity.element.color = new pc.Color(); // if we set it to it's own value, the setter is gonna return early
    this.entity.element.color = color;

    material.update();
}

This doesn’t seem like the correct way to do it though :sweat_smile:

1 Like

Created a ticket to look at this: https://github.com/playcanvas/engine/issues/3534

Looking at Spector.js, as you have said, it looks like it is being rendered but not sure where/transparent.

1 Like