Search Unity

  1. Looking for a job or to hire someone for a project? Check out the re-opened job forums.
    Dismiss Notice
  2. Unity 2020 LTS & Unity 2021.1 have been released.
    Dismiss Notice
  3. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

Official Shader Add-Ons: Detail Mask Tinting and Random Tint Variance

Discussion in 'Open Projects' started by Andy-Touch, Nov 13, 2020.

  1. Andy-Touch

    Andy-Touch

    A Moon Shaped Bool Unity Technologies

    Joined:
    May 5, 2014
    Posts:
    1,026
    Hey Folks!

    Thought i'd share some progress on some more Shader Graph Add-Ons that i've been working on to add to the Open Project.

    Im currently working off of the 'art-assets' branch. Work is not pushed yet; but will when its ready. :)

    I'll start with Detail Mask Tinting.

    This is the process of using a detail mask texture that stores 'area data' into different color channels. The end result would be to have a shader that allows tinting of parts of the surface.

    A typical Detail Mask looks like this; with the left being the mask and the right being how it ends up being applied to the asset. The process is not too different from PBR Texture creation where each channel stores data on Metallic, Smoothness etc values.



    Typically; I usually set up a Detail Mask with 3 tinted area variations
    • R - Main Detail
    • G - Secondary Detail
    • B - Minor Detail
    • A - Empty
    I usually keep the Alpha empty so that it can be used as a 'mask' for the base Albedo Texture. This way; there can be parts of the surface that are not tinted and retain their colors and details. A real-world example of this would be a t-shirt that has a logo printed on the chest. The base T-shirt would be stored in the Red channel but the logo would be in the Alpha channel; meaning that the t-shirt could be tinted to any color but the logo retains the same visual.

    In the Open Project; this can be used for manual customization and variance of certain game assets.

    For the Pig And Bunny, this can be used to create different clothing styles. Imagine a UI Color Picker for choosing different outfit color schemes; but without the extra need of having to create many different varying Albedo texture maps with slightly different color tints.

    Lets take a look at the Pig first:
    Unity_j557JHLEL0.png

    Three areas for clothing to be tinted!
    • R - Apron
    • G - Shirt
    • B - Head Band
    This would still allow for the core recognizable details of the pig to be retained; such as the skin and face.

    To begin with; I created the Detail Mask (manually) in Photoshop; and this is based off the Pig's base Albedo Texture:
    Photoshop_CHTOywrW8u.png

    Notice that big Alpha area? Thats mostly the pig's skin and other elements of clothing we don't want to tint (Such as shoes and trousers).

    Now that the Detail Mask has been created; time to implement it in Shader Graph!

    I noticed that the Toon Shader has a Sub Graph that has an Albedo (Texture) and Tint (Vector 4) already being input into it. Not to disturb this workflow; I wanted to make the Detail Mask Tint SubGraph before the Toon Shading SubGraph and it just feeds the result into that process. Makes it nice and modular. :)

    So I created the Detail Mask Tint SubGraph:

    Unity_jgCdPYKwbO.png

    Its quite simple; just splits the Detail Mask into different single-color channels, multiplies the tint to them and then recombines them. Admittedly, im not too happy about the multiply-add-multiply structure as it looks a bit messy so I will probably convert this to a Custom Function for simplicity and probably better performance.

    And at the end, the Blend Node uses the Detail Mask's Alpha as the Opacity (As mentioned before; to retain non-tinted details from the Albedo Texture) before being output as a Vector 4 (RGBA)

    After making a new variation of the Toon Shader Graph (named 'Toon_DetailMaskTint') and adding this Sub Graph; the root of this new Shader looks like this!

    Unity_eZD9IGPDsi.png

    And the results allow for a variety of customizations! Ive also set up the Detail Mask for the Bunny:



    For the Enemy Orgres; I thought it would be fitting to follow how 'Legend Of Zelda' handles enemy variants with most-surface color changes. But I left some familiar stand-out elements such as eyes and teeth untinted:

    Unity_FJwUgNCWyC.png

    And thats my progress so far on Detail Mask Tinting!

    I do have further plans:
    - Custom Shader GUI for the Material Inspector (The long list of properties is a bit messy)
    - Custom Function to simplify the tinting process in the Detail Mask Tint Sub-Graph
    - Make Detail Masks for the vegetation
    - Some kind of 'Color Scheme' Scriptable Object to store the color variations and then assign them to the Material instance (This way, we can have one Material for an Ogre but then multiple Color Scheme assets that can be swapped out. Juggling multiple Materials is not fun!)

    One performance down-sided is it adds one extra texture sample to the shader. But I figured that we can afford this hit as the Toon-style lends itself nicely to vibrant colors and not too many texture samples are already in use (Just Albedo and Specular Map).

    If this project was targetting mobile id perhaps look into baking the Detail Mask into the Vertex Colors or perhaps merging the Detail mask with the Albedo in the Alpha Channel (And use Float Range for the different Detail Areas).

    Ill write up a post about the Random Tint Variance next. :)
     
    Last edited: Nov 13, 2020
  2. Andy-Touch

    Andy-Touch

    A Moon Shaped Bool Unity Technologies

    Joined:
    May 5, 2014
    Posts:
    1,026
    Next addition: Random Position Tint

    Typically, the environments of games are made up of a few objects duplicated many times with different positions, rotations and scales. This can allow the quick creation of an area without having to manually create each and every environment piece individually.

    However, this brings monotony and familiar patterns. Especially in color.

    For example; If I wanted to take the Open Project Tree and make a small forest through duplication and transform tweaking; I would get something like this:

    Unity_SDfjrsfwZF.jpg

    Despite my best efforts to change their positions, rotations and scales, it is a bit 'solid' in a single color.

    If you look at a real photo of any forest, you will see that each tree has a slight variation in color:







    One way to do this in Unity would be to just duplicate the Tree Material many times and give each material a different tint of color. Whilst that would technically work; then you have more material assets to manage and it would be a relatively manual process to add the 'random variations' to every tree. Of course, we could write scripts to do things like this but thats extra work and not within the scope of non-programmers. Lets solve this with Shaders. :)

    The first challenge is random variation per tree mesh instance. If every tree is using the same Material, then we need some way for each tree instance to have a 'random' number to apply to it's color variance. Usually, Shaders don't make it easy to generate Random numbers in them and most developers tend to have a script pass that data to each object; but for our setup, there is another way!

    I stumbled across this trick when watching Shader Graph videos from Unity Korea's Youtube Channel. The exact video is this one:


    And the specific slide for the trick is this:

    chrome_g532iTNWff.png


    Ive already had my partner help me translate the important parts so you don't have to start learning Korean. :D

    Basically, the trick to get a 'random' number per mesh instance is to an object's position. Each object in the game world will have a XYZ value of where it currently is. This will usually be a value that looks a bit like this with several decimal values if you have dragged and dropped it to a new location: (35.32, -24.32, 0.043). Whilst this value is generally unique for each object (Unless two objects are in the exact same location; but thats pointless for our current use case) its not currently useful for random variance. For example; the position data looks a bit like this in the X and Z axis:

    Unity_K5oP213NkR.png

    With the 'middle' of the intersection of RGB colors being the central position (0,0,0).

    If we mapped this value directly to the Tint of the Toon_Wind Shader; it would look like this:

    Unity_HNEthxZkjZ.jpg

    So whilst we now have 'patches' of variance (without having to manually set values to each tree) there are still some challenges to solve.
    1. It doesn't look random. Its obvious that there are patches of similar color with the values meeting in middle.
    2. The further an object is from 0,0,0, the greater the value of it's position will be. This could yield strange shader results if the value is used as a multiply. IE: Any color multiplied by a large number such as 393204 is going to result in white and any color with a very low value (-30482) will result in black. Game worlds can be large so we have to factor in any position value.
    3. The trees are too vibrant. This would be ideal for a game set in Willy Wonka's Chocolate Factory but perhaps not for the Open Project. We need to have each tree's variation be slightly different to the base Albedo texture color. Different shades of green.
    To solve Challenge 1 and 2 together; we need to break the position values down into smaller chunks. Unfortunately, using XYZ Position can return strange numbers such as negative values and also values into their hundreds and thousands. We only need a slight variation! This is where 'Fraction' comes into play.

    Fraction is a function in Shaders that 'Computes The Fractional Part of the Argument'. TLDR is that it sort of 'clamps' a value within a range. Exactly like this for the Y value across X:



    We can use this to clamp the Object's XYZ Position values no matter how far away they are from 0,0,0. If I apply the 'Fraction' node to the Object Position in X and Z, it looks like this:

    Unity_mEc0nPGkQB.jpg

    Looks funky! But how does it look when applied to the trees?



    Now we are getting there! It looks random because each tree's object position is falling within that small clamped area. Technically, it is not 'random' but the results look random! The Node setup for this result looks like this; before it it output to the Tint of the Toon Shading:



    NOTE: You may notice that inbetween getting the Object Position and Fractioning it; I split and combine the XYZ positions. The purpose of this was to base the Fraction on the X and Z position of the Object, as if it was placed on a flat ground surface. The Y position is not needed for this scenario; however we might want to use it if we were to extend this shader for more vertical use-cases (Such as rocks on a mountain slope)


    Remaining; being able to control the variance and the color tint. As you can see in the above screenshot, some trees are very red/brown. And we also want the ability to 'tint' to our desired color. What if we want to apply the same shader to rocks and tint them shades of brown? Or to flowers and tint them shades of blue?

    As our fraction node is returning a value between 0 and 1, we can use a Lerp node to blend the tint value from the base Albedo texture to the Variation Tint. And by adding a small multiply of the fraction-ed value we can set how intense the tint is!

    The Sub Graph now looks like this:


    And the result:



    Thats better! And we now have extra control over how much tint variation there is (by scaling the 'random' value) and in the type of tint.

    Here is a short video of me putting together a small scene with this shader applied to several environment pieces. Im no level designer and others can construct a better scene than I but I think the results can be seen in the usefulness of this shader in breaking up the monotony of duplicate objects and the speed to quickly change the variations. :)



    Further plans:
    - Create a combined 'Toon Vegetation' Shader Graph that combines Wind and Random Position Tint.
    - Create a Custom Shader GUI; as the long list of properties is a bit messy.
     
    Last edited: Nov 13, 2020
  3. cirocontinisio

    cirocontinisio

    Unity Technologies

    Joined:
    Jun 20, 2016
    Posts:
    733
    This is super cool, and I really love the loooong and in-depth explanation.
    When are we seeing a PR for this? :)
     
    Andy-Touch likes this.
  4. awesomedata

    awesomedata

    Joined:
    Oct 8, 2014
    Posts:
    1,284
    @Andy-Touch

    I think what's "breaking" static batching in this situation is the significant amount of work being sent to the GPU in terms of material property blocks.

    While a bit low-tech -- The trick I'd try to use in this situation (for batching purposes) is to simply let the user define 5 or 6 specific "tints" to use as color indexes, with each index depending upon the local 0-1 position in the local "frac" square.
    This heavily decreases the amount of material-property-blocks you will need to set in your batches (i.e. from 100 or more to more like 5 or 6), and therefore should not (in theory) "break" static batching (as you are now requesting a much more "reasonable" amount of work -- and more importantly, waaay fewer "batches" -- from a single mesh).

    Ideally, the process would go like this:

    1. Do everything the same so far -- except resolving and picking the final "tint"
    2. Get the current local "frac" value for the object's position within the current frac "tile" (as you normally would)
    3. BUT -- Remap a range 1-5 (or 6) that represents the num color Indexes used as "tints" to a new 0-1 range
    4. Use local fract's 0-1 range to select the "tint" (color index 1-5) using the remapped 0-1 "color-index" range


      This effectively reduces the number of separate requests for material property blocks (the most expensive part!) to only 5-6 separate batches for the same mesh (down from 100 or even 1000! -- which should protect batching!)


    This should output a more "manageable" number of meshes to the GPU buffer (i.e. up to 5 or 6 variations on the material property block).
    Theoretically, you could even "combine" these color indexes to a midtone "blend" between the inversion of the 5-6 colors, depending on the vertical aspect of the frac positioning (resulting in only 10-12 indexes -- and therefore material property blocks, while still, hopefully, not breaking batching)

    Please let me know if this works! -- I haven't had time to test it myself.
     
    Andy-Touch likes this.
  5. Andy-Touch

    Andy-Touch

    A Moon Shaped Bool Unity Technologies

    Joined:
    May 5, 2014
    Posts:
    1,026
    Hey @awesomedata!

    That looks like an interesting solution!

    I wouldn't say that static batching is actually breaking; its by design that it takes similar meshes with a shared material and combines them. We just have to work around that and supply data to the shader that causes each tree to have individual position data when the combination happens.

    With your solution; you will still have the problem of the combined mesh having a singular position; so they would all return that same remap value. However, I think there is value in having more controlled tint values than a basic multiplier. Possibly with the colors packed into a LUT or maybe a Scriptable Object that holds the data that is then fed into the material. Id perhaps avoid a Gradient Node as its quite expensive and cannot be exposed to the Material Inspector. :(

    Ill definitely look into implementing your suggestion when I can; and will share the results! Or if you beat me to it; let me know how it goes! :)
     
  6. treivize

    treivize

    Joined:
    Jan 27, 2016
    Posts:
    135
    Hi @Andy-Touch,

    Tonight I came across to this article which seems to address exactly the remaining problem here: how to inject a random value in the shader without breaking static batching: http://hightalestudios.com/2017/09/reduce-draw-calls-using-vertex-colors/
    The solution is simply using a MB script assigning a random color to the vertex color of the meshrenderer. According to the article vertex color variations do not break static batching! Do you have an opinion on this technique?

    Sorry about that and forget about this post it is using dynamic batching actually...
     
    Last edited: Feb 5, 2021
  7. cirocontinisio

    cirocontinisio

    Unity Technologies

    Joined:
    Jun 20, 2016
    Posts:
    733
    Yeah the issue we talked about in Episode 4 is kind of a no-issue now, since we switched to having the vegetation materials to use instancing instead of batching them statically as one big mesh.
     
unityunity