TMPro UGUI Color gradient applied per character

@Stephan_B It seems that when I toggle on the Color Gradient and fill in a horizontal gradient the gradient is being applied per character instead of the whole text. Am I missing some setting?
I’m using Unity 2020.3.13 with TextMeshPro 3.0.6. I’ve reïmported the shaders just to be sure I am up to date.

What I’m trying to implement is a color transition for a slider label as described in this thread How to make text color change depending on background color ?

The result I am trying to create is to start with a color gradient and when the slider is under the text to transition to white for readability.
But the gradient is applied per character which is an unwanted effect in this case.

7323634--888994--upload_2021-7-13_15-34-49.png

The slider fill and Text are based on the same gradient. so once the slider fills up enough it becomes unreadable. I can’t set it to white at all times either because the slider has a white background.
Fixing that with an outline becomes rather ugly.

7323634--888997--upload_2021-7-13_15-35-33.png

I didn’t see any example of this in the TMPro package, so I guess this hasn’t been requested often.

That is the expected behavior as the gradient sets the vertex colors of individual characters.

Are all the characters of your text objects expected to be affected by the gradient? If so, I would suggest using the full distance field shader where you can set a texture on the face of the text. You can control if this texture is applied per character, line or the whole text object via Horizontal / Vertical Mapping options in the text object inspector.

The object in question is a progress bar. There are multiple progress bars in the scene active at a time.
So I need a material instance per progress bar.

I’m just a bit confused on how I should animate the texture offset.
I am storing the fontMaterial into a private field.
Whenever the Slider changes value I call a method that changes the texture offset.
However, nothing changes. Whenever I do this in Edit mode with the slider calling in Editor time, it seems to work fine.
But at runtime, it just doesn’t. Am I missing something?

public TextMeshProUGUI Label;
private Material fontMaterial;

public Material FontMaterial
{
    get
    {
        if (fontMaterial == null)
        {
            fontMaterial = Label.fontMaterial;
        }

        return fontMaterial;
    }
}

public void OnValueChangedHandler(float value)
{
    if (value > 0.45f && value < 0.55f)
    {
        var percentage = Mathf.Clamp01(1f - (0.535f - value) / 0.072f);
        var offsetValue = Mathf.Lerp(0.05f, -0.95f, percentage);
        FontMaterial.SetTextureOffset(ShaderUtilities.ID_FaceTex, new Vector2(offsetValue, 0f));
    }
    else if (value < 0.45f)
    {
        FontMaterial.SetTextureOffset(ShaderUtilities.ID_FaceTex, new Vector2(0.05f, 0f));
    }
    else if (value > 0.55f)
    {
        FontMaterial.SetTextureOffset(ShaderUtilities.ID_FaceTex, new Vector2(-0.95f, 0f));
    }
}

In the meantime, whilst you figure out what’s going on and someone else figures out what’s going on, you can add a very strong white “shadow” and/or outline to the font, so that it’s visible when the red fill of the progress bar begins to creep over the numbers.

Yeah that is something I already have done in the past, before I could figure out the gradient part of the problem.
Setting it HorizontalMapping to Line fixed that.

I have been trying different things yesterday and it drove me nuts.
Tried again today and eventually came up with this:

public TextMeshProUGUI Label;
private Material fontMaterial;

private void Awake()
{
    fontMaterial = new Material(Label.fontSharedMaterial);
    Label.fontMaterial = fontMaterial;
}

public void OnValueChangedHandler(float value)
{
    if (value > 0.45f && value < 0.55f)
    {
        var percentage = Mathf.Clamp01(1f - (0.535f - value) / 0.072f);
        var offsetValue = Mathf.Lerp(0.05f, -0.95f, percentage);
        Label.materialForRendering.SetTextureOffset(ShaderUtilities.ID_FaceTex, new Vector2(offsetValue, 0f));
    }
    else if (value < 0.45f)
    {
        Label.materialForRendering.SetTextureOffset(ShaderUtilities.ID_FaceTex, new Vector2(0.05f, 0f));
    }
    else if (value > 0.55f)
    {
        Label.materialForRendering.SetTextureOffset(ShaderUtilities.ID_FaceTex, new Vector2(-0.95f, 0f));
    }
}

private void OnDestroy()
{
    // Destroy the font material instance
    Destroy(fontMaterial);
}

So on Awake it creates a new Material because it has to be an Instance. I’ve got multiple objects with different properties. Since MaterialPropertyBlocks aren’t a thing for this, new material instances have to be made.

Using .fontMaterial everytime the value changes did nothing.
I tried changing the .fontMaterial and then use the .fontSharedMaterial which did nothing either.
When I used .materialForRendering I noticed that the values were changing (but I hadn’t set a new Material instance, so all texts became the same value). Made the material an Instance in Awake, kept materialForRendering to change the offset.

Tested it and it works, yay. Took me a while though.
And of course destroying the material when the object is destroyed to clean up.

You have patience and resilience and determination I can do nought but admire immensely.

Well done.

I have no idea how what you do works, nor how it works!

But it’s incredibly impressive that you got this done!!!

In a simpler world, for simpler people (me) can the slider be used as a mask for a white copy of the red text? As the slider comes across, it reveals the white text, which is at a higher sort order (and/or z position), is what I’m imagining.

And how I’d probably do this, as it’s the only way I can see and conceive to do this, but it does mean having two text objects, and possible two sliders… one as the visual representation of the slider, and one as a mask.

Yeah don’t mind the values being used. It’s a specific case component.
I needed it to transition from a gradient color text to a white color text as soon as the slider is hitting the text.
The texture is 5% white and 95% gradient. The texture offset is based on a calculation.
0.05 means that the text is full gradient.
-0.95 is full white.
I calculate the percentage that needs to be white and apply that.

I didn’t go with a mask for performance reasons. And normally animating shader properties is quite simple. With TMPro it just took some figuring out which material property to use.

For you!

I’ll hold you to this, too :wink: and be asking questions!

I stick with Legacy Animation and things that have and Editor GUI I can access and can animate. Otherwise I’m lost.

I know this struggle. I’ve had some fun inside the default shader. Will have to tinker more. Not really sure what’s what, yet.

You mentioned the performance of masking approaches being slow.

I’m super naive on this.

How much faster is a shader based approach to reveals?

I’m going to need do a lot of masking and revealing for my UI needs.

I’ve only just started considering how to structure my UI, and it took me ages to find the new location for the new docs on the old system… so I’ll be reading this about 20x today, and then spend the next 3 days trying to get it to work, if my past endeavours are any guide:

https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-RectMask2D.html

Don’t worry too much about it yet. Just do your thing first. When you look at the profiler to check the performance you can always decide to look for solutions.

I’m a designer, I work the other way around - design to the limitations. If I know (for example) shaders will be 20x faster, I’m gonna immediately learn enough about shaders, and make the reveals 5x bigger, and 4x more of them.

Can you give an approximation like “Shaders are… nX faster”?

One can’t give an estimate on that. It depends on many things. Simply create something and test the performance on the target device. I think it is good to design around limitations. But performance is a subject on its own. Some will say, don’t prematurely optimize. Personally I’d like to write performant code by default (I’m a programmer) so I don’t have to worry about it later.
So yeah while I understand what you’re doing, I’d still say try things out.
When you use a lot of Mask components you can also check on the target hardware when it’ll cripple performance and call that your limit.

It’s totally going off-topic now :wink:

I’ve tried that mask with the canvas. It’s horrible. The performance is woeful. On a Mac. Might be different on other platforms, but I saw my CPU/GPU get hotter than doing anything other than full blown Premiere rendering.

I’ve found this, though… is it useful for masking?

https://docs.unity3d.com/Packages/com.unity.textmeshpro@3.0/api/TMPro.TMP_MaterialManager.html?q=mask#:~:text=AddMaskingMaterial(Material%2C%20Material%2C%20Int32)

Getting more and more off-topic than this thread was meant asside.

Try using either V-sync or seting the target frame rate to something within an expected range. It’s not really useful if a game is pumping many more frames above the monitors refresh rate. It puts extra strain on the CPU & GPU.

I have no clue what it is used for. I don’t do a lot with masking.
I don’t do a lot of fancy things with TMPro at all. I use it because it is rendering sharper than the regular Text component.
In this thread’s case I needed to change the color under a certain condition. But apart from that… nothing fancy.

But if you’re running into performance issues with certain UI scenarios you can always start a thread in the UGUI section.
Explaining that you’re a designer (not a programmer), what problem you’re running into and what you’ve tried to do to solve it.
There is mostly someone willing to help you out.
I kind of don’t want to sidetrack this thread any much longer than necessary :stuck_out_tongue: