Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

My solution to tile 2D sprites

Discussion in '2D' started by Zoodinger, Jun 24, 2014.

  1. Zoodinger

    Zoodinger

    Joined:
    Jul 1, 2013
    Posts:
    43
    Hello everyone,

    A friend of mine was asking me about this, and I have noticed that there isn't a solution to this issue yet. If there is one, my apologies, but I couldn't find it. Anyway, here is my solution, which is the same one I used in my game:
    1. Go to the advanced settings of your sprite and set its Wrap mode to repeat
    2. Create a new shader and add this code:
      Code (Shader):
      1. Shader "Sprites/Tile"
      2. {
      3.     Properties
      4.     {
      5.         [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
      6.         _Color ("Tint", Color) = (1,1,1,1)
      7.         RepeatX ("Repeat X", Float) = 1
      8.         RepeatY ("Repeat Y", Float) = 1
      9.     }
      10.  
      11.     SubShader
      12.     {
      13.         Tags
      14.         {
      15.             "Queue"="Transparent"
      16.             "IgnoreProjector"="True"
      17.             "RenderType"="Transparent"
      18.             "PreviewType"="Plane"
      19.             "CanUseSpriteAtlas"="True"
      20.         }
      21.  
      22.         Lighting Off
      23.         Blend SrcAlpha OneMinusSrcAlpha
      24.         Cull Off
      25.         ZWrite Off
      26.         Fog { Mode Off }
      27.        
      28.         Pass
      29.         {
      30.         CGPROGRAM
      31.             #pragma vertex vert
      32.             #pragma fragment frag
      33.             #include "UnityCG.cginc"
      34.            
      35.             struct appdata_t
      36.             {
      37.                 half4 vertex   : POSITION;
      38.                 half4 color    : COLOR;
      39.                 half2 texcoord : TEXCOORD0;
      40.             };
      41.  
      42.             struct v2f
      43.             {
      44.                 half4 vertex   : SV_POSITION;
      45.                 fixed4 color    : COLOR;
      46.                 half2 texcoord  : TEXCOORD0;
      47.             };
      48.            
      49.             fixed4 _Color;
      50.             half RepeatX;
      51.             half RepeatY;
      52.  
      53.             v2f vert(appdata_t IN)
      54.             {
      55.                 v2f OUT;
      56.                 OUT.vertex = mul(UNITY_MATRIX_MVP, IN.vertex);
      57.                 OUT.texcoord = IN.texcoord  * float2(RepeatX, RepeatY);
      58.                 OUT.color = IN.color * _Color;
      59.                 return OUT;
      60.             }
      61.  
      62.             sampler2D _MainTex;
      63.            
      64.             fixed4 frag(v2f IN) : COLOR
      65.             {
      66.                 return tex2D(_MainTex, IN.texcoord) * IN.color;
      67.             }
      68.         ENDCG
      69.         }
      70.     }
      71. }
    3. Create a material with this shader and set its Repeat X and Repeat Y values
    4. Set this as the material of the Sprite Renderer of the object that is to tile.
    Moreover, if you so wish to have a background to follow the camera, here are some extra steps to achieve this:
    1. Change the pivot of your background sprite to Top Left
    2. Add this extension method anywhere you want (it has to be in a public static class)
      Code (CSharp):
      1. //I find this extension method extremely useful myself
      2. public static class UnityExtensions
      3. {
      4.     public static Vector3 ScreenToWorldLength(this Camera camera, Vector3 position)
      5.     {
      6.         return camera.ScreenToWorldPoint(position) - camera.ScreenToWorldPoint(Vector3.zero);
      7.     }
      8. }
    3. Then use this code in the update event of your GameObject that will display the background:
      Code (CSharp):
      1. transform.position = Camera.main.ScreenToWorldPoint(new Vector3(0, Screen.height, 0));
      2. transform.position = new Vector3(transform.position.x, transform.position.y, 1);
      3. transform.localScale = Camera.main.ScreenToWorldLength(new Vector3(Screen.width, Screen.height, 0));
      4. float repY = transform.localScale.y / transform.localScale.x * squareSize;
      5.  
      6. renderer.material.SetFloat("RepeatX", squareSize);
      7. renderer.material.SetFloat("RepeatY", repY);
    This will repeat the background squareSize times horizontally, no matter the screen width, and the height will follow the aspect ratio correctly. Of course, you should ideally only call this code when the screen dimension change to remove the extra overhead.

    Have fun!
     
    rakkarage and Nims like this.
  2. Nims

    Nims

    Joined:
    Nov 11, 2013
    Posts:
    86
    I want to say I appreciate what you have done here, thanks!

    I am still a nooby in regards to shaders, I would appreciate it if your explained why you chose the following tags:
    "Queue"="Transparent", "RenderType"="Transparent" and not a simple "RenderType"="Opaque" (Not that I really understand all the others either), is this some how related to the fact that we are dealing with 2D sprites?

    I also see that this shader only works for Sprite Mode = single, I wonder how complicated it would be to extend this to sprites which come from a texture.
     
    rakkarage likes this.
  3. Zoodinger

    Zoodinger

    Joined:
    Jul 1, 2013
    Posts:
    43
    Sorry for the delay, I had not realized there was a reply.

    I think I simply copied the source from the default Sprite shader used by Unity and edited it to change the texture coordinates. This line:
    Code (Shader):
    1. OUT.texcoord = IN.texcoord  * float2(RepeatX, RepeatY);
    is where all the magic happens in the shader. You can use this in other shaders with different tags if you like.

    Unfortunately, I don't think this method will work for atlas sprites, as (I think) changing the coordinates will increase the coordinates to include other sprites from the atlas. I might be completely wrong about this, and I cannot possibly check this any time soon. It might have been possible to wrap around the coordinates if we could somehow know, from within the shader, the minimum and maximum texture coordinates that define the area of the sprite, so that you could manually wrap them to be within those bounds. However, I don't think that's possible. If it were, it would look something like this:
    Code (Shader):
    1. float2 minCoord; // Not known :(
    2. float2 maxCoord; // Not known :(
    3.  
    4. //Vertex shader:
    5. // This converts coordinates to be in the range of [0, 1] before multiplying with repeat
    6. OUT.texcoord = (IN.texcoord - minCoord) * (maxCoord-minCoord) * float2(RepeatX, RepeatY);
    7.  
    8. //Fragment shader:
    9. // This uses the fractional part of the coordinate in order to tile it
    10. float2 texcoord = fract(IN.texcoord);
    11. // This converts coordinates to the range of [minCoord, maxCoord]
    12. texcoord = minCoord + texcoord * (maxCoord-minCoord);
    13. return tex2D(_MainTex, texcoord)  * IN.color;
    Like I said, I cannot possibly test this any time soon, so it might not work even assuming that there is a way to know the values of minCoord and maxCoord. It might be possible to calculate minCoord and maxCoord from within the game and then pass it to the shader. Hopefully, Unity will include a proper built-in way to do tiling in a future version.
     
    Nims likes this.