Search Unity

  1. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  2. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

Access and use TextureCoordinate in Shader

Discussion in 'Shaders' started by CBFackh, Oct 10, 2017.

  1. CBFackh

    CBFackh

    Joined:
    Jul 7, 2015
    Posts:
    4
    Good day,

    I am trying to get my ColorReplacement-Shader to work with SpriteAtlas.

    At first, how does my ColorReplacement-Shader work?
    The shader uses 3 textures:
    _MainTex ==> the sprites texture
    SpriteTex.PNG
    _AreaTex ==> a single channel texture that stores areas to swap color
    AreaTex.PNG
    _ColorTex ==> a single 1x256 texture that defines the colors of each area.

    Within the fragment shader:
    1. The shader samples the color from _MainTex.
    2. Then picks the alpha value from the _AreaTex.
    3. Then picks the replacementColor from _ColorTex based on the picked alpha.
    4. Finally, multiplies the original color with the replacementColor and applies alpha from _MainTex

    This works fine for single sprites.
    But with a SpriteAtlas, it becomes tricky.

    The adjusted Shader for the SpriteAtlas
    My approach so far: I want to subtract the textureRect.position of the sprite from the TEXCOORD in the fragment shader. Therefore I send the textureRect position to the shader via script:

    Code (CSharp):
    1. var result = new Vector4(sprite.textureRect.position.x, sprite.textureRect.position.y, 0, 0);
    2.  
    3. MySpriteRenderer.material.SetVector("_AtlasPosition", result);
    Within the shader, I use this position as well as the "_MainTex_TexelSize" to substract these values from the TEXCOORD:

    Code (CSharp):
    1. fixed4 frag(v2f IN) : SV_Target
    2.     {
    3.         fixed4 c = SampleSpriteTexture(IN.texcoord);
    4.         fixed4 areaTex = tex2D(_AreaTex, IN.texcoord - _MainTex_TexelSize.xy * _AtlasPosition.xy);
    5.         fixed4 swapCol = tex2D(_SwapTex, float2(areaTex.a, 0)) * c;
    6.         fixed4 final = swapCol * IN.color;
    7.         final.rgb *= c.a;
    8.         return final;
    9.     }
    That code still works, as long as I use single sprites. But as soon as I use a sprite form an atlas, it seems like, the _AreaTex is scaled up in relation to the _MainTex. The result looks as follows:
    Offsetted.PNG

    So, I think I have to scale the coordinates base on _MainTex size (2048x2048 px) to _AreaTex size (699 x 294px)

    Any hints or help are welcome! :)

    Many thanks beforehand.
    Christian
     

    Attached Files:

    mannyhams likes this.
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,366
    UVs are a 0.0 to 1.0 range where 0.0, 0.0 is the bottom left corner and 1.0, 1.0 is the top right. IN.texcoord is in UV space, where as the textureRect is a pixel location.

    This is all information you seem to already understand as you're correctly multiplying the pixel position by the texel size which should put it into UV space. What you're missing is the scale of the UV is still the full atlas size, so while you may be properly offsetting the position so the bottom left corners are aligned, the area texture itself is scaled to be the size of the entire atlas and not the single sprite.

    To correct for that you need to pass the rect's size as well.

    Code (CSharp):
    1. var result = new Vector4(sprite.textureRect.position.x, sprite.textureRect.position.y, sprite.textureRect.size.x, sprite.textureRect.size.y);
    Code (CSharp):
    1. // multiply both the position offset and size by the texel size to bring them into UV space
    2. float4 atlasOffsetScale = _AtlasPosition * _MainTexSize.xyxy;
    3. // apply UV position offset and scale
    4. fixed4 areaTex = tex2D(_AreaTex, IN.texcoord - atlasOffsetScale.xy / atlasOffsetScale.zw);
    However I wonder if there might be an easier way to go about what you're doing. The sprite and the area textures are both greyscale. Is there a reason to not pack both the sprite and the area texture into the same sprite on different color channels?
     
    mannyhams likes this.
  3. CBFackh

    CBFackh

    Joined:
    Jul 7, 2015
    Posts:
    4
    Many thanks for your help @bgolus !
    The shader works properly with atlases now. I just hat to add brackets in your 4. line:

    Code (CSharp):
    1. // multiply both the position offset and size by the texel size to bring them into UV space
    2. float4 atlasOffsetScale = _AtlasPosition * _MainTex_TexelSize.xyxy;
    3. // apply UV position offset and scale
    4. fixed4 areaTex = tex2D(_AreaTex, (IN.texcoord - atlasOffsetScale.xy) / atlasOffsetScale.zw);
    Regarding your thoughts of combining both textures into one:
    I already tried that but encountered problems regarding the texture compression. As the shader works with the exact alpha value, the _AreaTex info should not be read from a compressed texture. When the texture is compressed, many of the pixels are not correctly uncompressed and show different alpha values. Therefore these pixels are not colorized by the shader.

    As I am not very deep into graphics programming and texture compression, I did not find any solution than separating the textures and leaving the _AreaTex uncompressed.

    It would be awesome to be able to combine the textures as I am working on the reduction of the apps size and memory usage (which is crashing older iOS devices right now).

    Any thoughts from you about that problem would be appreciated.
     
  4. Tomyn

    Tomyn

    Joined:
    Jan 19, 2018
    Posts:
    5
    Apologies for bringing back up an old thread, But I recently tried this technique with unity 2018.4 my c# script consists of:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class AtlasPosition : MonoBehaviour
    4. {
    5.     [SerializeField] SpriteRenderer sRenderer;
    6.  
    7.     private void Start()
    8.     {
    9.         UpdateMainTexture();
    10.     }
    11.  
    12.     public void UpdateMainTexture()
    13.     {
    14.         var sprite = sRenderer.sprite;
    15.         Material material = sRenderer.sharedMaterial;
    16.  
    17.         Vector4 atlasPosition = new Vector4(
    18.             sprite.textureRect.position.x,
    19.             sprite.textureRect.position.y,
    20.             sprite.textureRect.size.x,
    21.             sprite.textureRect.size.y);
    22.  
    23.         material.SetVector("_AtlasPosition", atlasPosition);
    24.     }
    25. }
    26.  
    And the shader I'm testing with is really simple:
    Code (CSharp):
    1. Shader "TechArt2D/AtlasedSprite"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex("Texture", 2D) = "white" {}
    6.         _AtlasPosition("Atlas Position", Vector) = (0,0,0,0)
    7.     }
    8.         SubShader
    9.         {
    10.             Tags
    11.             {
    12.                 "RenderType" = "Transparent"
    13.                 "Queue" = "Transparent"
    14.                 "IgnoreProjector" = "True"
    15.                 "CanUseSpriteAtlas" = "True"
    16.             }
    17.  
    18.             Cull Back
    19.             Lighting Off
    20.             ZWrite Off
    21.  
    22.             Pass
    23.             {
    24.  
    25.                 Blend SrcAlpha OneMinusSrcAlpha
    26.  
    27.                 CGPROGRAM
    28.                 #pragma vertex vert
    29.                 #pragma fragment frag
    30.  
    31.                 #include "UnityCG.cginc"
    32.  
    33.                 struct appdata
    34.                 {
    35.                     float4 vertex : POSITION;
    36.                     float2 uv : TEXCOORD0;
    37.                 };
    38.  
    39.                 struct v2f
    40.                 {
    41.                     float2 uv : TEXCOORD0;
    42.                     float4 vertex : SV_POSITION;
    43.                 };
    44.  
    45.                 sampler2D _MainTex;
    46.                 float4 _MainTex_ST;
    47.                 float4 _MainTex_TexelSize;
    48.  
    49.                 float4 _AtlasPosition;
    50.  
    51.                 v2f vert(appdata v)
    52.                 {
    53.                     v2f o;
    54.                     UNITY_INITIALIZE_OUTPUT(v2f, o);
    55.  
    56.                     o.vertex = UnityObjectToClipPos(v.vertex);
    57.                     o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    58.                     return o;
    59.                 }
    60.  
    61.                 fixed4 frag(v2f i) : SV_Target
    62.                 {
    63.                     float4 atlasOffsetScale = _AtlasPosition * _MainTex_TexelSize.xyxy;
    64.                     float2 alteredUV = (i.uv - atlasOffsetScale.xy) / atlasOffsetScale.zw;
    65.  
    66.                     half4 col = tex2D(_MainTex, alteredUV);
    67.  
    68.                     return col;
    69.                 }
    70.                 ENDCG
    71.             }
    72.         }
    73. }
    The weird thing is I've done this exact thing with the same code before and It was working as expected, this time though I'm getting the opposite results where I see the whole atlas within the bounds of the sprite and I can't tell why. Does anyone have any suggestions? Below I've included an image of the unexpected results I'm getting.

    AtlasIssue.PNG