Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question How to pass unique gameobject data to a UI shader

Discussion in 'Shader Graph' started by Aazadan, Oct 24, 2021.

  1. Aazadan

    Aazadan

    Joined:
    Jun 21, 2014
    Posts:
    14
    So what I'm trying to do is write a shader that allows for UI icons that sample only a single texture atlas, can be tinted different colors based on sprite location (for example applying a different tint to a foreground and background), can exist on a single game object (so no child objects needed for foreground/background to layer them) with the exception of text, and most importantly, is capable of batching.

    My current set up is using a custom a lit sprite graph with a single texture atlas input arranged in an 8x8 grid. This atlas is a solid black background with the foreground details being white. I then read this and apply one tint to the black portion and another to the white portion. I then handle the button shape with the same technique but using a procedural rounded rectangle (or theoretically, any shape I want), and passing that into my alpha.

    As long as my Image component has a 9 sliced sprite in the source image, this allows me to size my icons without distorting the borders of my icon (why this works, I honestly have no idea, as the provided sprite doesn't actually render, and the shape of the sprite doesn't seem to matter, only the 9 slice dimensions, but I'll go with it).

    However, there's one major flaw in this approach which is that it can't display unique icons without breaking batching. In order to access my texture atlas, I have to provide the correct tiling and offset locations to the material. This means one material per atlas location. I am currently instantiating my material to create the unique icons but I'm wanting this to batch which material instances obviously won't do.

    The way I figured I could do this was to have some sort of data I can pass to the shader that's unique per object. Then I can read that value, and perform the necessary calculations. For example, when determining my color tinting, I have colors set as constants and I can XOR the input against a flag, lerp the 0 or 1 flag against a Vector4.zero and the color I want, and then take the maximum of this output against each flag. So for 4 possible values, I would have lerped outputs of the color I want in one channel, 0 in the other 3, and can then max them all to get my final value. This does work to give me the correct tinting, doesn't require using branching logic, and it does batch (I can also do it with add and multiply but everything I read said lerp and max would calculate faster).

    In the past, when trying to solve similar issues with 3d meshes, I've been able to write to vertex data to pass in information, to create a tint or even to use the vertex color channel to pass in data. However, that doesn't work in this case as I don't have a sprite mesh. I'm using Image, and it seems a Sprite Renderer is required for a sprite mesh, but since this is in a UI canvas I can't use that.

    When researching this to see if I could find ideas from anyone else, I saw a post from years ago from the Unity team which said to use additional UV channels and write to UV data. Unfortunately, that was several years ago and at this point I can't add additional UV channels, and even if I could UV's are read only.

    My next thought was to pass in data that is specific to the game object. As far as I've been able to find in shadergraph, the only information I can read from on the object is the position node, the object node (haven't gotten this to work, but since it seems Z scale doesn't exist in UI anyways even if it did work it's effectively just the position node), and the UV node. Since I can't write to UV's the UV node is out, and since I haven't been able to get the object node to work, that's out. That leaves me with only the position node that I can read from.

    Since this is in the UI and it only needs to be 2d I technically have the Z position value available and the UI fortunately renders entirely by hierarchy so z position doesn't change what renders on top of what.

    This solution can work for smaller numbers, for example lets say I want to replicate a typical button state color tint with multiple colors. I can set the z position to 0, 1, 2, and 3 (I don't need selected in this case), pass my z position into the shader, and then calculate the color set that I want.

    However, this also looks really weird in the editor when things don't line up correctly. Perhaps more importantly, the amount of data I have available with this approach is limited to my far plane. Currently that's 1000. If I bumped that up to 1024 I would have 10 bits of data I can pass in. However, going that deep greatly increases how weird the UI looks in the editor. Additionally, the further back you go, the more detail gets lost from the icons themselves, so things like rounded corners, outlines, and fine details in the icons are blurred, distorted, or lost. Basically they start looking lower resolution even without mipmaps.

    This is where my texture atlas grid size becomes relevant. In order to use this technique to give each icon a unique graphic in the atlas, an 8x8 grid requires 6 bits, 3 for the x axis and 3 for the y axis. Additionally, I need 2 bits for my colors, and possibly in the future 3 bits. So I need either 8 or 9 bits out of my possible 10 to make this work.

    While this might be a possible solution at the moment for what I want to do, it feels like it lacks any sort of scalability, and it feels like this is the sort of hacky solution that would eventually break because I can see a future where a Rect Transform doesn't have a Z position.

    So the question is, does anyone have a suggestion for a better way to approach this? I had thought about trying to make the gaps between numbers smaller by using floating point Z positions and the same bit flag logic but that would just make things look a little better in the editor. I think it would still run into all the same issues that I'm concerned about long term with this approach.