Search code examples
c#unity-game-engineshadershaderlab

How to get original texture from SpriteAtlas to use in a shader as alpha mask?


I have an issue, and i need help to resolve it.

I am try to colorize some parts of sprite dynamically by custom shader and additional textures as masks. It is not a problem and i solve it easy.

Screen for better understanding:

enter image description here

This is real implementation and color blending for thirs image was realized by shader, sprite and mask.

But I am been forced to use atlases for saving RAM cause have to many sprites to load at the same time (3 states and 5 directions for each of varyous units). And this was caused problems. When i try to do this in edit mode (by using ExecuteInEditMode) all works fine. But when i press "play" button, all breaks down.

How it's look like:

enter image description here

As i understand, problem is that when I press "play" atlas was builded. So when I get sprite from it I get it susseccfully. But when I try to get texture from Sprite for seting it to Shader I have got big texture (full atlas). Shader do not know nothing about actual position on this sheet for cheking is pixel need to colorizing.

So my question are: how can i get "small" texture from sprite to set it to shader?

How I set "mask" to shader:

public void UpdateMask(Texture tex)
{
    //Debug.LogFormat("UpdateMask {0}", tex);
    m_SRenderer.GetPropertyBlock(m_SpriteMbp);
    m_SpriteMbp.SetTexture("_Mask", tex);
    m_SRenderer.SetPropertyBlock(m_SpriteMbp);
}

Some shader's pieces:

Properties 
{
    [PerRendererData] _MainTex("Sprite Texture (RGB)", 2D) = "white" {}
    [PerRendererData] _Mask("Alpha (A)", 2D)  = "white" {}

    _FriendlyColor("FriendlyColor", Color) = (1,1,1,1)
    _EnemyColor("EnemyColor", Color) = (1,1,1,1)
    _NeutralColor("NeutralColor", Color) = (1,1,1,1)

    _Intencity("BlendingIntencity", Range(0, 1)) = 0.5
    [PerRendererData] _IsFriendly("IsFriendly", Float) = 0

    [PerRendererData] _IsHighlight("Outline", Range(0, 1)) = 0
    [PerRendererData] _HighlightColor("Outline Color", Color) = (1,1,1,1)
}


fixed4 frag(v2f IN) : COLOR
{
    fixed4 mainTex = tex2D(_MainTex, IN.texcoord) * IN.color;
    fixed4 alphaMask = tex2D(_Mask, IN.texcoord) * IN.color;
    fixed4 output;
    fixed4 blendColor;

    if (alphaMask.a > 1.0e-6)
    {
        if (_IsFriendly == 4)
        {
            blendColor = fixed4(_NeutralColor.r, _NeutralColor.g, _NeutralColor.b, alphaMask.r);
        }
        else
        {
            if (_IsFriendly == 1)
            {
                blendColor = fixed4(_FriendlyColor.r, _FriendlyColor.g, _FriendlyColor.b, alphaMask.r);                     
            }
            else
            {
                blendColor = fixed4(_EnemyColor.r, _EnemyColor.g, _EnemyColor.b, alphaMask.r);
            }
        }                   
        output = BlendOverelay(mainTex, blendColor * _Intencity);
    }   
    else
    {
        output = mainTex;
        output.rgb *= output.a;
    }

    if (_IsHighlight != 0)
    {
        fixed4 blendedColor = BlendAdditive(output, _HighlightColor);
        blendedColor.a = output.a;
        blendedColor.rgb *= output.a;
        output = blendedColor;
    }

    return output;
}

Solution

  • You need to tell the sprite renderer where its position in the atlas is and how large the atlas is, so that it can convert the IN.texcoord UV in atlas space to the corresponding UV in sprite space. Then, you can sample from the alpha map using the sprite space UV

    In C#, set the atlas offset & scale information to e.g., _AtlasPosition:

    public void UpdateMask(Texture tex)
    {
        //Debug.LogFormat("UpdateMask {0}", tex);
        m_SRenderer.GetPropertyBlock(m_SpriteMbp);
        m_SpriteMbp.SetTexture("_Mask", tex);
        Vector4 result = new Vector4(sprite.textureRect.position.x, sprite.textureRect.position.y, sprite.textureRect.size.x, sprite.textureRect.size.y)
    
        m_SpriteMbp.SetVector("_AtlasPosition", result)
        m_SRenderer.SetPropertyBlock(m_SpriteMbp);
    }
    

    In shader, calculate the current UV in sprite space, and use it to sample from _Mask:

    fixed4 frag(v2f IN) : COLOR
    {
        fixed4 mainTex = tex2D(_MainTex, IN.texcoord) * IN.color;
        // multiply both the position offset and size by the texel size to bring them into UV space
        float4 atlasOffsetScale = _AtlasPosition * _MainTex_TexelSize.xyxy;
        // apply UV position offset and scale, sample from alpha mask
        fixed4 alphaMask = tex2D(_Mask, (IN.texcoord - atlasOffsetScale.xy) / atlasOffsetScale.zw) * IN.color; 
        fixed4 output;
        fixed4 blendColor;
        // ...
    

    You'll have to declare _MainTex_TexelSize in your shader if you haven't already.

    This will not work if you are using tight packing. For Sprite Packer, you will need to specify DefaultPackerPolicy in the sprite packer or specify [RECT] in the packing tag. If you are using SpriteAtlas, you will need to disable Tight Packing.

    Code sourced from this thread on the unity forums