Last time we learned about blending colors on sprites using RGB masks and sine. Today we're going to look at three different techniques for dynamically coloring UI graphics in Unity and the best times to use each, and then you'll be able to tackle all kinds of UI challenges.
Technique #1: Separate cutouts for each color
In this technique, after you make the image you want to use in your UI, you separate each area that is supposed to be colored at runtime into a separate totally white image. Then in the game you lay each image on top of each other so they appear to be part of the same picture.
How to use
Download the images attached to this post called meter-small-rotated, meter-small, and portrait-bg-small, and import them into your project. Select them all in your project files and change the texture type to Sprite and click Apply.
Note: these images are from an earlier game of mine called Delenda Airlines and you can use them to practice, but you can't use them in a game you're going to sell. If you fall in love with them you could probably pretty easily make some images that are similar and then you'd be golden!
In your scene, add a new canvas by right clicking in the Hierarchy and selecting UI > Canvas.
Set canvas scaler to Scale With Screen Size and reference resolution to1920x1080.
Inside the canvas, add three new images by right clicking and selecting UI > Image. In the Source Image slot for each image, drag in each of the sprites. The order in the hierarchy should be like this:
And you should end up with this:
Change the values on the Meter image to match these:
Then change the color and fill amount to see what happens!
Pretty spiffy, eh?
How Not To Use
We also had some graphics intended for use with menus and textboxes.
Put em together in the hierarchy and hey presto!
Great, right? Well, unfortunately the whole thing fell apart the moment I tried to use these assets to make windows of variable size.
First of all, what do you parent to what? Makes sense to parent the background to the border, but if you do that it'll show up in front of the border, which we don't want. And then say you wanna expand the box to fit some text in there?
erm. no problem, I'll have the inner part set to stretch and fill the area!
Oh dangit. Maybe I just need to fiddle with the anchors...
Well to make a long story short, for most of these buttons, I was unable to fiddle with the UI elements such that they would reliably be flush with each other in every situation that came up. Ultimately I just had the artist come up with sprites for each desired variant.
This tragedy could have been avoided -- if at the time I had known how to shader!
Individual Sprite Cutout Summary
- No custom shaders required
- Can test in edit mode
- Each UI element can be changed individually and if you save, changes persist if the editor is closed.
- Have as many dynamically set colors as you are willing to keep track of cutouts for.
- Alignment can be a nightmare
- Can make your hierarchy super difficult to read
- Really difficult to deal with stencil masks
- potential Z fighting issues
Ideal use case: A UI element whose size is never going to change and doesn't contain a lot of child elements. Health meters and HUD icons are good uses for this technique.
Technique #2: Use a color mask and set the colors via the material inspector
In this technique you have your final image and a color mask image. You use a custom shader to combine them, then set the dynamic colors in the material.
This technique has a few weaknesses. Suppose you have a menu you want to be pink and a menu you want to be purple, and both use the same base textures? If you're using a UI image, you have to create a separate material for each variation, and if your project is large, you end up really hating to add more clutter to it. Also, let's say you have two UI elements sharing the same material and you want to change the color of just one while the game is running. When you do, you end up causing a new material to be instantiated, and if you do this a lot, it can really add up and eat up resources.
Q: What about using material property blocks?
A: Unity's UI elements are incompatible with material property block! Prop blocked!
- Just one hierarchy object. No clutter or alignment issues!
- Can be tested in-scene without any custom script
- Up to 3 dynamically set colors
- Have to make a separate material for each color variation
- If colors are changed at runtime, you either have to alter the actual material asset (which means the changes are permanent) or you have to spawn a new material.
- You must use a custom shader.
- Your colors won't play nice with 3rd party libraries expecting an ordinary image, such as DOTween.
Ideal use case: a UI object that is fairly unique that will have lots of other objects parented to it and requires many custom colors, but doesn't need to have its color changed much at runtime. For example, a main menu.
Technique #3: Use a color mask and take mask channel from the vertex color
You heard me.
If you’ve been following these tutorials in order, you already know that when we use a sprite or UI image, the color from the component is included in the app data provided in the vertex function. Nearly all of the time, you’ll want to multiply the final color in the fragment function by the vertex color, because when we set a color in a sprite renderer or Image component, we’re expecting that to tint the entire texture, but there’s no rule saying we have to use it that way.
How to use
Download f-mask and f-maskbase into your project. (I hope you like it, I drew it myself! 😂) Import both of these with the texture type Sprite. If you don't already have one in your scene, make a canvas and add a UI image to it. Put the f-maskbase sprite into the UI image. If you don't know how to do this, scroll up to the section on Technique 1 and read about adding a canvas and images to your scene there, then come back here. When you have your scene set up, you should have something like this:
Make a new material called MaskedPassenger. (It's cuz this lady is one of the passenger characters in Delenda Airlines!) Make a new unlit shader called MaskedVertexColor and assign the shader to the material. Drag the material into the slot on the Image component. You'll end up with this.
Oh dear! Time to fire up Visual Studio and set this right!
Making the shader
First, let's change the name up top to Xibanya/Unlit/MaskedVertexColor
And now for properties.
When we put the attribute PerRendererData, we're saying we're going to get that property passed in from some other component. In this case, an Image or Sprite component. The NoScaleOffset attribute prevents the scale and offset fields from being drawn in the inspector. We don't want them because we know our mask is going to align with our main texture, so there's no need to ever mess with its scale.
Replace the existing tags with these, and add the culling and blend options and etc.
If you're not sure what these mean, you can get a full explanation in the earlier tutorials on making sprite shaders! Basically, when we make a sprite or UI shader, we're nearly always going to want to set it up like this.
After the CGPROGRAM block, get rid of the stuff about fog and add
Then update the appdata and v2f structs to include color. I also like to align the colons and order all the stuff not labeled TEXCOORD before the TEXCOORD interpolators (items in the struct), but this is just to make the code easier for me to read and isn't necessary. I also like to name the SV_POSITION interpolator "pos" but that's not necessary either. Do whatever makes it easier for you to understand your own code!
Also be sure to declare the _Mask texture. You can do that by adding it to the end of the line with the _MainTex declaration or put it on its own line.
In the vert function, delete the fog macro and instead transfer the color data.
In the frag function, we can start by deleting the fog macro there and then think about how we want to unpack the mask. We can only use one channel, so let's use the alpha channel. Since that's the only one we need, we can unpack it right into a half like this.
Then the actual blend:
col.rgb = lerp(col.rgb, col.rgb * i.color.rgb, mask);
This means we'll use the base texture color multiplied by the vertex color where the mask value is closest to one.
Then to wrap it up, we'll multiply the final alpha by the vertex color alpha:
Why do that? Well a lot of 3rd party libraries that have functions for fading UI images (such as DOTween) do the fade on the vertex color alpha value. This ensures that even though we're using a custom shader, this image will be compatible with anything that tries to fade it in or out without any special code.
All right! 71 lines, short n' sweet! Save and head back to Unity. With the image selected, expand the material section in the inspector and drag f-mask into the Mask slot.
Then in the image component play around with the color!
So, to sum this technique up:
- Just one hierarchy object, great for sliced images.
- Can use the same material on all color variants.
- Won't leak materials.
- Can be tested in edit mode without any custom script.
- Color is stable, if the scene is saved then it won’t be lost on reload.
- Works with batching or instancing
- Alpha fade works with all your favorite plugins
- Have to use a custom shader
- Only one dynamic color
- Can't tint the entire sprite at once (or if you add a color property for this, you run into the same drawbacks as in Technique 2.)
Ideal use case: an object you will have many variants of or will need to change the dynamic color of frequently, sliced UI objects with dynamic content fitting. Example: a frequently used textbox with a dynamically colored title bar, a common NPC portrait with dynamically colored accessories.
BONUS, Technique #4: make your UI out of sprites and world space text
Use sprites, set the mask colors via material property block, use a script to tape them to the player's face at all times.
- instanced colors!
- Requires a custom shader
- Colors can't be set in edit mode without a script
- this is a galaxy brain idea
- if you parent this to the main camera you will have problems
- if you don't parent this to the main camera you will have problems
- the edge cases will drive you mad
- just try it
- I warned you
None of these techniques are better or worse than each other, they just have strengths and weaknesses. Using masked colors like this is a great way to get lots of mileage out of your existing assets, so everything we covered today is good to have in your shader toolbox. Also tuck the concept of vertex color and material property blocks away for later, we'll be returning to those ideas soon. In the meantime, see what cool graphics you can come up using these techniques. And you know, there's no rule saying the vertex color you get from a sprite renderer or image has to actually be used as a color. What might you do with 3 floats you can set dynamically at runtime? Food for thought...
The shader we made today is attached to this post as MaskedVertexColor.shader. If you have any questions or want to share what you come up with, let me know in the comments here, on Twitter, or in Discord. And if this tutorial helped you out, please consider becoming a patron!
This tutorial is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike4.0 International License. The code shared with this tutorial is licensed under a CreativeCommonsAttribution 4.0 International License. The images shared with this tutorial are for educational use only and cannot be redistributed.