Search Unity

Question Rotate sprite using shader graph

Discussion in 'Shader Graph' started by vambier, Aug 11, 2021.

  1. vambier

    vambier

    Joined:
    Oct 1, 2012
    Posts:
    102
    I'm trying to rotate the texture on a sprite using shader graph. The rotation is only part of a bigger shader I'm creating, and everything works but the rotation. When I try to rotate a single square sprite it works fine. But when that sprite is part of a spritesheet it doesn't work anymore. When I to rotate the texture it rotates the whole sheet, at 0 degrees rotation it's fine, but once it starts rotating it rotates the whole sheet.
    Another problem I see is rotating a rectangular sprite. When I rotate a rectangular sprite to 90 degrees the texture becomes deformed. I added screenshots for clarification.

    This is a rectangular sprite, 128x256 pixels:



    Rotated 90 degrees in game it looks like this, really flattened:



    The other problem is with spritesheets, for example this one:



    Looks like this in game:



    But when rotated for example 20 degrees:



    It looks like it rotates the whole sheet and tries to squish them into a single sprite.

    Anyone got any ideas what could be the problem and how to fix it?
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    UVs are normalized to the texture. 0,0 is the bottom left corner, 1,1 is the top right corner. This is true regardless of the resolution or aspect ratio of the texture being used. This means when you're displaying a sprite that's taller than it is wide, the UVs of the sprite's mesh are stretched out vertically. When you do a rotation the Shader Graph, you're most likely rotating the UV coordinates, and they'll still be stretched vertically on the mesh.

    You could correct for this by scaling the UVs so they're a square aspect ratio, rotating, and then scaling back to what it was so it's stretched vertically along the texture's orientation, but the end result is you'll just see the middle half of the sprite because the mesh was not rotated and it's only as wide as the original sprite.

    If you want to rotate a sprite in a way that doesn't cause weird issues like this, you either need to always use a square sprite textures, or rotate the sprite mesh instead of the UVs. Unfortunately there's not really a way to rotate sprites meshes from a shader that works 100% of the time without using a c# script to help, at which point you should just use the c# script to rotate the sprite renderer's game object transform. Normally to rotate a mesh you'd rotate the object space vertex positions as they're already (usually) centered around the pivot. In Shader Graph that'd be taking the Position node set to Object space, pass it into a Rotate About Axis node, and then plug that into the Vertex Position on the Master Stack. That'll work with a single sprite by itself, but as soon as you have two sprites using the same material & sprite (or sprite atlas) then the sprites will be batched into a single mesh and it'll no longer rotate around it's pivot, but instead it'll rotate around the world origin. You can work around that by using instanced Sprites, which may or may not work with Shader Graph shaders, or by giving each Sprite Renderer its own unique material which will prevent batching from occurring. But you really should just rotate the transform instead at that point.

    When you have an atlas, the UVs are still normalized, but across the entire atlas (since that's "the texture"). So that single sprite's mesh has had its UVs adjusted to be 0.0 to 1.0 across the vertical, and the horizontal UVs set to whatever range that sprite within the atlas has. When you rotate the UVs, you're now seeing the full range of the texture across that 0.0 1.0 vertical UV range.
     
    vambier likes this.
  3. vambier

    vambier

    Joined:
    Oct 1, 2012
    Posts:
    102
    Thanks for the extensive reply!! I went on holiday when I saw your reply so it's been a couple of weeks since I had time to look into this further.
    Obviously it would be the best way to rotate the sprite under normal circumstances. But I am making a pixel art game. And I've created a shader graph that pixelates sprites with a given ppu so that it matches te rest of the game. This way I can use non pixel art sprites that get pixelated and this works great!! But I also want to use it to pixelate rotated sprites but this gives the problems I mentioned. It works great for square sprites, in my shader graph I rotate the UV beforehand, pixelate and then rotate the UV back. But for non-square sprites, sprites that are part of a sprite sheet and/or part of an atlas it gives problems.
    The only other way I can think of is not rotating the UV at all but pixelate at a given angle, but I have no clue as how to do that.
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    For non-square sprites, regardless of if they're in an atlas or not, there's not really any other option apart from you must rotate the mesh. Or you have to make sure all of your sprites square (and not use tight bounds on the sprite). Otherwise you can fix the aspect ratio and rotation pivot in the shader, but it doesn't really matter because it'll just get cut off. Think about what happens if you rotate a non-square sprite in photoshop, the same thing happens.
    upload_2021-9-14_13-3-36.png
    It only gets worse in an atlas, because when you rotate it in the fragment shader and not the vertex shader you'll start to see stuff outside of that sprite's bounds. And really even having a square sprite doesn't entirely solve the problem if you don't have enough gutter space between the sprite and the edge of the mesh.
    upload_2021-9-14_13-9-53.png


    So, again, you have to rotate the mesh, or make considerable changes to the sprites to accommodate the technique.


    For this kind of effect, the "solution" is likely going to be to rotate both the mesh and the UV, and to use a script component on every single sprite you want to apply your shader to.

    First step is to rotate the Sprite Renderer's game object itself and not do it in the shader. The second step is you'd need to pass the UV pivot position you want to rotate around, and the rotation amount, and set it on the material used for that sprite. This can be done with a material property block using
    spriteRenderer.SetPropertyBlock()
    or on the
    spriteRenderer.material
    (which will create a unique copy of the material for you, just be sure to call
    Destroy()
    on that material if that gameobject is destroyed). For the aspect ratio you can either calculate that from the sprite's texture itself in Shader Graph using the Texel Size node, though you might also need to calculate the correct "pixel" size for your shader to use, again relative to the UVs. In your shader you'd then want to counter rotate & scale the UVs to your "pixel" space, presumably do some kind of multiply by pixel count & floor of the UVs, and then use the rotation and scale values to calculate the appropriate position on the original sprite's texture.
     
    vambier likes this.
  5. vambier

    vambier

    Joined:
    Oct 1, 2012
    Posts:
    102
    With rotating the mesh do you mean the gameobject?
    I already have a script that I have to attach to each sprite that I want to pixelate so that's not a problem. The scaling using the texel node also works like it should.
    The problems you show here are indeed the problems I ran into.
     
  6. sharebophar

    sharebophar

    Joined:
    Mar 10, 2017
    Posts:
    2
    but how does "TilemapRederer" render the rotating tile or ruleTile ?
     
  7. sharebophar

    sharebophar

    Joined:
    Mar 10, 2017
    Posts:
    2
    work it out
    Code (CSharp):
    1.                     // get the sprite size from the texture size
    2.                     float2 spriteSize = _MainTex_TexelSize.zw;
    3.  
    4.                     //float aspect = spriteSize / spriteSize.y;
    5.                     float2 aspect = _MainTex_TexelSize.zw / _MainTex_TexelSize.w;
    6.  
    7.                     // get the center of the sprite in uv space
    8.                     float2 center = float2(_CenterX, _CenterY);
    9.  
    10.                     // get the offset from the center to the current pixel
    11.                     //float2 offset = IN.texcoord;
    12.                     float2 offset = IN.texcoord - center;
    13.  
    14.                     // calculate the rotation angle in radians
    15.                     float angle = radians(_Rotation);
    16.  
    17.                     offset = offset * aspect;
    18.                     // apply the rotation matrix to the offset
    19.                     float2 rotatedOffset = mul(float2x2(cos(angle), -sin(angle), sin(angle), cos(angle)), offset);
    20.                     rotatedOffset = rotatedOffset / aspect;
    21.                     // add the center back to get the rotated uv coordinate
    22.                     float2 rotatedUV = rotatedOffset + center;
    23.                     //float2 rotatedUV = rotatedOffset;
    24.  
    25.                     // sample the texture with the rotated uv
    26.                     fixed4 col = tex2D(_MainTex, rotatedUV) * IN.color;
    27.  
    28.                     col.rgb *= col.a;
    29.  
    30.                     return col;