Search Unity

Question BilinearSample on Atlas texture

Discussion in 'Shaders' started by Danil0v3s, Nov 18, 2022.

  1. Danil0v3s

    Danil0v3s

    Joined:
    Sep 22, 2014
    Posts:
    45
    I've been trying to convert a function that works for a single sprite to use a sprite atlas instead. I've searched for a bit and it seems that I must convert the UV from the atlas perspective to the sprite perspective. I tried a few solutions but I cannot for the life of me get this working.

    This is how I'm sending data to the shader

    Code (CSharp):
    1.  
    2. var layer = frame.layers[0];
    3. var sprite = Sprites[layer.index];
    4. var rect = Rects[layer.index];
    5.  
    6. MeshRenderer.material.SetVector("_uTextSize", new Vector2(sprite.rect.width, sprite.rect.height));
    7. MeshRenderer.material.SetVector("_Rect", new Vector4(rect.x, rect.y, rect.width, rect.height));
    8.  
    And this is the shader function where it uses the UV
    Code (Boo):
    1. fixed4 bilinearSample(sampler2D indexT, sampler2D LUT, float2 uv) {
    2.     float2 TextInterval = 1.0 / _uTextSize;
    3.  
    4.     float tlLUT = tex2D(indexT, uv).x;
    5.     float trLUT = tex2D(indexT, uv + float2(TextInterval.x, 0.0)).x;
    6.     float blLUT = tex2D(indexT, uv + float2(0.0, TextInterval.y)).x;
    7.     float brLUT = tex2D(indexT, uv + TextInterval).x;
    8.  
    9.     float4 transparent = float4(0.5, 0.5, 0.5, 0.0);
    10.  
    11.     float4 tl = tlLUT == 0.0 ? transparent : float4(tex2D(LUT, float2(tlLUT, 1.0)).rgb, 1.0);
    12.     float4 tr = trLUT == 0.0 ? transparent : float4(tex2D(LUT, float2(trLUT, 1.0)).rgb, 1.0);
    13.     float4 bl = blLUT == 0.0 ? transparent : float4(tex2D(LUT, float2(blLUT, 1.0)).rgb, 1.0);
    14.     float4 br = brLUT == 0.0 ? transparent : float4(tex2D(LUT, float2(brLUT, 1.0)).rgb, 1.0);
    15.  
    16.     float2 f = frac(uv.xy * _uTextSize);
    17.     float4 tA = lerp(tl, tr, f.x);
    18.     float4 tB = lerp(bl, br, f.x);
    19.  
    20.     return lerp(tA, tB, f.y);
    21. }
    This is the end result I'm trying to achieve (single sprite) vs what I'm currently getting (atlas)
    upload_2022-11-18_20-17-8.png

    And here is what I've tried so far:
    Code (Boo):
    1.                 half2 uvs = i.uv;
    2.                 uvs.x = (uvs.x * _MainTex_TexelSize.z - _Rect.x) / _Rect.z;
    3.                 uvs.y = (uvs.y * _MainTex_TexelSize.w - _Rect.y) / _Rect.w;
    4.                 fixed4 tex = bilinearSample(_MainTex, _PaletteTex, uvs);
    Code (CSharp):
    1.                 float2 localuv = (i.uv - _Rect.xy) / (_Rect.zw - _Rect.xy);
    2.                 fixed4 tex = bilinearSample(_MainTex, _PaletteTex, localuv);
     
  2. Danil0v3s

    Danil0v3s

    Joined:
    Sep 22, 2014
    Posts:
    45
    Got it working by sending the size of the atlas texture instead of the sprite to the shader.
    Instead of
    Code (CSharp):
    1. var layer = frame.layers[0];
    2. var sprite = Sprites[layer.index];
    3. var rect = Rects[layer.index];
    4. MeshRenderer.material.SetVector("_uTextSize", new Vector2(sprite.rect.width, sprite.rect.height));
    5. MeshRenderer.material.SetVector("_Rect", new Vector4(rect.x, rect.y, rect.width, rect.height));
    I now do
    Code (CSharp):
    1. var materialTexture = MeshRenderer.material.mainTexture;
    2. if (materialTexture != null) {
    3.     MeshRenderer.material.SetVector("_uTextSize", new Vector2(materialTexture.width, materialTexture.height));
    4. }
    5.  
    And it works
    upload_2022-11-19_12-46-0.png
     
  3. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Unity does that already for you btw.

    float4 _TextureName_TexelSize.zw
    is the texture resolution.
     
  4. Danil0v3s

    Danil0v3s

    Joined:
    Sep 22, 2014
    Posts:
    45
    Hey! I tried using that but for some reason it didn't work, maybe I wans't passing the correct UV, not in front of the code now to test but I remember I tried debugging the texel size by setting it to a property and it was always 0
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    If you set it as a property, it’ll prevent Unity from assigning it automatically.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    You'll probably want to change your sample function to something like this:
    Code (CSharp):
    1. fixed4 (sampler2D indexT, float4 indexT_TexelSize, sampler2D LUT, float2 uv) {
    2.     float2 TextInterval = indexT_TexelSize.xy;
    3.     float tlLUT = tex2D(indexT, uv).x;
    4.     float trLUT = tex2D(indexT, uv + float2(TextInterval.x, 0.0)).x;
    5.     float blLUT = tex2D(indexT, uv + float2(0.0, TextInterval.y)).x;
    6.     float brLUT = tex2D(indexT, uv + TextInterval).x;
    7.     float4 transparent = float4(0.5, 0.5, 0.5, 0.0);
    8.     float4 tl = tlLUT == 0.0 ? transparent : float4(tex2D(LUT, float2(tlLUT, 1.0)).rgb, 1.0);
    9.     float4 tr = trLUT == 0.0 ? transparent : float4(tex2D(LUT, float2(trLUT, 1.0)).rgb, 1.0);
    10.     float4 bl = blLUT == 0.0 ? transparent : float4(tex2D(LUT, float2(blLUT, 1.0)).rgb, 1.0);
    11.     float4 br = brLUT == 0.0 ? transparent : float4(tex2D(LUT, float2(brLUT, 1.0)).rgb, 1.0);
    12.     float2 f = frac(uv.xy * indexT_TexelSize.zw);
    13.     float4 tA = lerp(tl, tr, f.x);
    14.     float4 tB = lerp(bl, br, f.x);
    15.     return lerp(tA, tB, f.y);
    16. }
    And then call it like this:
    Code (CSharp):
    1. bilinearSample(_MainTex, _MainTex_TexelSize, _PaletteTex, i.uv);
    You want
    float4 _MainTex_TexelSize;
    in your shader code just like it would be if it was a material property, but you do not want it as a material property as then whatever is set on the material is what it'll use, not what Unity would automatically set that value to. Unity will not automatically override material properties.
     
  7. Danil0v3s

    Danil0v3s

    Joined:
    Sep 22, 2014
    Posts:
    45
    Your suggestion worked as usual. Had to change
    float2 TextInterval = indexT_TexelSize.xy;
    to
    float2 TextInterval = indexT_TexelSize.zw;


    I was trying to create a property with a different name like
    _Test("aaa", Vector) = (0,0,0,0)
    then I was setting its value to the one from _MainTex_TexelSize but it was always (0,0,0,0).
     
    bgolus likes this.
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    The value you’re setting in the shader is several levels removed from the value you see in the material. Nothing you do in the shader can change the material’s properties you see in the editor, because it’s all intentionally a one direction data flow and temporary values.

    The material property exists as data in an asset on the CPU side.
    Those properties, along with others set by the engine, are gathered together for each mesh that is drawn. At this point any connection to the material & material property is lost (outside of some kept around for debugging).
    That data is packed up and sent from the CPU to the GPU when the mesh is drawn.
    When the GPU draws the mesh, each vertex and then each fragment, gets a unique copy of that data to work from. Any modifications to that data is kept as a local variable that is tossed out when the shader function finishes for each vertex or fragment. The only information retained is that which is output by each shader stage; usually only the vertex’s clip space position, UVs, normal, etc. is retained and passed along to the fragment, and the fragment outputs the color you see on screen, and that’s it.

    No data comes back to the CPU from the GPU, so the material property won’t … can’t … be modified by the shader.
     
    Danil0v3s likes this.
  9. Danil0v3s

    Danil0v3s

    Joined:
    Sep 22, 2014
    Posts:
    45
    Thank you for the explanation, that was enlightening!
     
  10. Danil0v3s

    Danil0v3s

    Joined:
    Sep 22, 2014
    Posts:
    45
    Hey @bgolus, sorry for reviving this old thread.. been looking for a way to super sample this so the end result doesn't look like too pixelated. It's worth remembering the source texture must be point and the palette is a 256x1 texture with an index8 format, the bilinearSample "walks around" the uvs and picks the correct color from the palette texture based on the source point...

    Being that said, do you see any way of achieving super sampling/trilinear filtering with this solution? I've tried a few methods I found on the forum but none seem to work since the mess with the UV's... I'm not really versed in shaders and this sounds super complex :(
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    So I understand, you're trying to figure out how to handle zooming out and showing the sprites at a screen size smaller than the number of texels in the original sprite, yes?

    There are few ways I can think of to go about this:

    1. Implement literal super sampling, where you sample all texels within the pixel, apply the indexed color to them, and output the average color of all visible texels. There are a few problems with this. It's hard to do right, and it's expensive. And while you could limit the number of texels you sample to some max to minimize the performance cost, that'll either mean at a certain point you get aliasing again leaving you where you started.

    2. Go old school and author (or at least generate) different sprite for different screen sizes. When the image needs to scale smaller than about 60% of the source art's texel size, swap to different smaller sprite(s). This is obviously a lot more work, but is what they would have done for old school 2D console games where things like mip mapping didn't exist.

    3. Fake it. Keep the current bilinear technique, but calculate an appropriate mip level and use that to figure out how many texels to skip between. This won't look nearly as good as real super sampling / trilinear filtering, but will reduce the aliasing significantly.

    4. Render textures. This is probably the technique I'd suggest using, which will also potentially get rid of even needing the original bilinear filtering shader. The idea is take any sprite atlas you want to recolor, and use a shader that does the indexed color remapping to render it to a new render texture that's the same size as the atlas, but has mip maps and trilinear filtering enabled. Then use that texture instead of the actual sprite atlas in your sprite material. This gets you the content reuse and full mipmapping, though it comes with increased on-device memory cost as each color now uses a copy of the sprite atlas rather than reusing it. The benefit is it's much, much cheaper to render.
     
    Danil0v3s likes this.
  12. Danil0v3s

    Danil0v3s

    Joined:
    Sep 22, 2014
    Posts:
    45
    I think you’re right about the zooming, I could also save the sprite atlases with the colors already and let Unity do the trilinear thing for me instead of having it as point. Problem would be much more disk space compared to a single single channel atlas and many indexed colors palettes

    I was looking for a way of implementing the 4th option since it looked like most AA solutions required me to tex2D, but I couldn’t find resources pointing me in the direction I needed. Would you mind outlining what I need to search in order to get this working? I could also share a small project if you’d like to give it a spin