Rotating multiple textures in a shader

Hey All,

Sorry if this has been asked before. I just spent the last hour+ on Google and haven’t found a way yet so …

I’m trying to figure out how to rotate multiple textures independently within a shader. The use case is a globe with a water/land texture, as well as multiple layers of clouds and/or hurricanes. I have the clouds moving with a parallax effect … if that’s the best I can do for this project time-wise, then its pass-able. However, it would be great if I could get the hurricane textures to spin. Then I could just adjust the uv-offset through script in order to move them wherever I need. The key part is I need to be able to rotate some textures without affecting the others.

Does this seem feasible? If I haven’t explained this well enough, please just let me know.

Attached is my test hurricane image. Obviously I would cull it down to a much smaller image … right now I don’t care. Notice that the hurricane is in the CENTER of the image, so any rotation would need to work for rotating around a texture’s center.

Thank you!

PS: Although I’ve been a programmer for about 10 years, and using Unity for 2+ … I’ve never touched shaders before the past week, so don’t assume I’ll know anything :).

There’s a ton of threads discussing texture rotation. This one for example: Rotation of Texture/UVs directly from a shader - Unity Engine - Unity Discussions

If you have a UV you want to rotate separately you can either pass it from the vertex shader to the pixel shader as a second UV set (or pack it into the zw components of another UV) or do the rotation in the pixel shader with the same math.

Hey,

Thanks for the reply!

So yeah, I already went through that one. The issue isn’t that the answer isn’t there or that I haven’t seen it, its that I lack whatever beginner level information is necessary to understand how to translate that into actual shader code.

enviralDesigns wrote:
"This thread has the closest solution to what I’m looking for and I thank everyone for their contributions!
I’m stuck trying to incorporate MULTIPLE textures in this shader that all rotate at speeds independent from each other.
I’m realizing my problem lies with in the vert function, I would need to somehow modify two sets of uv’s one for each texture since it’s not the texture that’s rotating but the uv’s correct? I’ve tried creating a new uvset in struct Input but I wasn’t able to get that to work.
Any ideas? I would be for ever grateful!"

hippocoder responded:
“Thats correct, simply repeat the cos and sin calculation, and apply it to a different uv”

I get that there are two ways I can accomplish this (as you mentioned) … I just don’t know where to go to figure out what that code looks like. Again, outside of some tutorials, lots of googling, and trial and error … I’m basically brand new to the world of shaders. Obviously the long-term answer is “go learn shader code”, but given that I have a specific use-case in mind, and I assume its not very hard to do when you already know the solution, I was hoping there would be some short snippet of code I could learn from. Don’t get me wrong – I’m all for learning how to do it on my own, and I don’t mean to ask anyone to “write it for me”. I just don’t know where to go to learn how to achieve this specific use-case without first wading through who knows how much other stuff that isn’t pertinent as of this moment before I maybe stumble across what I need for this. Whether its an actual piece of code that would achieve this which I could learn from, or a resource that handles this use-case (without changes necessary for Unity), either way I would be more than grateful … without the runnable code though, I’m afraid I’m not going to know how to implement any of the methods tossed at me.

Anyway, if its too much trouble, obviously just don’t respond! If its not too much trouble and anyone can give a simple example of a shader which does this, I would be very grateful.

For Unity there’s basically two different ways to do shaders (well, 4, but one is effectively deprecated, and the other is only for platform specific stuff), surface shader and vert / frag shader. Behind the scenes Unity convers surface shaders into expanded vert / frag shaders, which then get converted into platform specific shaders, which then get compiled into the final shader code that is what’s sent to the drivers which convert that into the final form the GPU actually uses. It’s a deep rabbit hole so we’ll stick to the top two levels. The deprecated method is the “fixed function” shaders if you’re curious. This is what shaders used to look like before you could do arbitrary math, but these two now just get converted into vert / frag shaders.

So, lets start with vert frag for now. The basic rotation in the vertex shader.

Shader "Unlit/Unlit UV Rotation in vertex"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Rotation ("Rotation", Range(0,360)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
           
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Rotation;
           
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);

                // rotating UV
                const float Deg2Rad = (UNITY_PI * 2.0) / 360.0;

                float rotationRadians = _Rotation * Deg2Rad; // convert degrees to radians
                float s = sin(rotationRadians); // sin and cos take radians, not degrees
                float c = cos(rotationRadians);

                float2x2 rotationMatrix = float2x2( c, -s, s, c); // construct simple rotation matrix

                v.uv -= 0.5; // offset UV so we rotate around 0.5 and not 0.0
                v.uv = mul(rotationMatrix, v.uv); // apply rotation matrix
                v.uv += 0.5; // offset UV again so UVs are in the correct location

                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }
           
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);               
                return col;
            }
            ENDCG
        }
    }
}

This is the default “new Shader > Unlit” modified to add rotation. This is probably where you’re at now, at least something similar. The form is a little different from most of the other threads on UV rotation because most people get confused by the degree to radian conversion (ie: they don’t do it) and they’re doing the mul in the wrong order.

So, now we want multiple textures and UV sets with different rotations. This is just a matter of adding additional UVs to the v2f struct and doing the math multiple times.

Shader "Unlit/Unlit UV Rotation of multiple textures in vertex"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _RotatedTexA ("Texture", 2D) = "white" {}
        _RotationA ("Rotation", Range(0,360)) = 0.0
        _RotatedTexB ("Texture", 2D) = "white" {}
        _RotationB ("Rotation", Range(0,360)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
           
            #include "UnityCG.cginc"

            float2 rotateUV(float2 uv, float degrees)
            {
                // rotating UV
                const float Deg2Rad = (UNITY_PI * 2.0) / 360.0;

               float rotationRadians = degrees * Deg2Rad; // convert degrees to radians
               float s = sin(rotationRadians); // sin and cos take radians, not degrees
               float c = cos(rotationRadians);

                float2x2 rotationMatrix = float2x2( c, -s, s, c); // construct simple rotation matrix

                uv -= 0.5; // offset UV so we rotate around 0.5 and not 0.0
                uv = mul(rotationMatrix, uv); // apply rotation matrix
                uv += 0.5; // offset UV again so UVs are in the correct location

                return uv;
            }

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 uv2 : TEXCOORD1; // Addition additional UV to pass
                UNITY_FOG_COORDS(2) // changed from 1 to 2 since uv2 is using TEXCOORD1 now
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _RotatedTexA;
            float4 _RotatedTexA_ST;
            float _RotationA;
            sampler2D _RotatedTexB;
            float4 _RotatedTexB_ST;
            float _RotationB;
           
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);

                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.uv2.xy = TRANSFORM_TEX(rotateUV(v.uv, _RotationA), _RotatedTexA);
                o.uv2.zw = TRANSFORM_TEX(rotateUV(v.uv, _RotationB), _RotatedTexB);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }
           
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);

                // sample rotated textures
                fixed4 colA = tex2D(_RotatedTexA, i.uv2.xy);
                fixed4 colB = tex2D(_RotatedTexB, i.uv2.zw);

                // adding the textures together just so you can see them all
                col = (col + colA + colB) / 3.0;

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);               
                return col;
            }
            ENDCG
        }
    }
}

The rotation code is now a separate function so we can reuse it. We also have a second UV set, a float4 instead of a float2, to the v2f struct and we’re using the xy and zw components to pack two UV sets into a single parameter for efficiency.

Now what about doing the rotation in the fragment shader?

Shader "Unlit/Unlit UV Rotation of multiple textures in fragment"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _RotatedTexA ("Texture", 2D) = "white" {}
        _RotationA ("Rotation", Range(0,360)) = 0.0
        _RotatedTexB ("Texture", 2D) = "white" {}
        _RotationB ("Rotation", Range(0,360)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
           
            #include "UnityCG.cginc"

            float2 rotateUV(float2 uv, float degrees)
            {
                // rotating UV
                const float Deg2Rad = (UNITY_PI * 2.0) / 360.0;

                float rotationRadians = degrees * Deg2Rad; // convert degrees to radians
                float s = sin(rotationRadians); // sin and cos take radians, not degrees
                float c = cos(rotationRadians);

                float2x2 rotationMatrix = float2x2( c, -s, s, c); // construct simple rotation matrix

                uv -= 0.5; // offset UV so we rotate around 0.5 and not 0.0
                uv = mul(rotationMatrix, uv); // apply rotation matrix
                uv += 0.5; // offset UV again so UVs are in the correct location

                return uv;
            }

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _RotatedTexA;
            float4 _RotatedTexA_ST;
            float _RotationA;
            sampler2D _RotatedTexB;
            float4 _RotatedTexB_ST;
            float _RotationB;
           
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);

                o.uv = v.uv;
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }
           
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                float2 mainTex_uv = TRANSFORM_TEX(i.uv, _MainTex);
                fixed4 col = tex2D(_MainTex, mainTex_uv);

                // sample rotated textures
                float2 uvA = TRANSFORM_TEX(rotateUV(i.uv, _RotationA), _RotatedTexA);
                float2 uvB = TRANSFORM_TEX(rotateUV(i.uv, _RotationB), _RotatedTexB);
                fixed4 colA = tex2D(_RotatedTexA, uvA);
                fixed4 colB = tex2D(_RotatedTexB, uvB);

                // adding the textures together just so you can see them all
                col = (col + colA + colB) / 3.0;
               
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

All of the UV code is now in the fragment shader, almost literally just copy and pasted (apart from changing v.uv to i.uv and assigning them to different variables). In the vert function we’re just passing the mesh’s UV on and nothing else. The last two shaders are identical in function, though the vertex one will generally be faster as the math is being done only for each vertex instead of every pixel.

Now you can do stuff like mix these two. Do the TRANSFORM_TEX in the vertex shader for the main tex (this is applying the scale and offset values you see in the editor, which is stored in the float4 _MainTex_ST variable), and pass along the untransformed UVs as the zw components, then do the rotation. If you don’t need or want the in editor scale and offset you can add [NoScaleOffset] in front of the texture properties at the start of the shader then remove the float4 _***_ST and TRANSFORM_TEX stuff for that texture. You can also just share those offsets, etc. Whatever you want to do. But I’ll leave that for you to figure out.

Now if you want to do this in a surface shader you can do it like the fragment version pretty easily. Just copy the rotateUV function and apply the rotation in the surf function. If you want to do it at the vertex level you’ll need to add a custom vertex function to your surf shader and add another UV set to the Input struct.

Shader "Custom/Surface UV Rotation in vertex" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        [NoScaleOffset] _RotatedTex ("Texture", 2D) = "white" {}
        _Rotation ("Rotation", Range(0,360)) = 0.0
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
       
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows vertex:vert

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        float2 rotateUV(float2 uv, float degrees)
        {
            // rotating UV
            const float Deg2Rad = (UNITY_PI * 2.0) / 360.0;

            float rotationRadians = degrees * Deg2Rad; // convert degrees to radians
            float s = sin(rotationRadians); // sin and cos take radians, not degrees
            float c = cos(rotationRadians);

            float2x2 rotationMatrix = float2x2( c, -s, s, c); // construct simple rotation matrix

            uv -= 0.5; // offset UV so we rotate around 0.5 and not 0.0
            uv = mul(rotationMatrix, uv); // apply rotation matrix
            uv += 0.5; // offset UV again so UVs are in the correct location

            return uv;
        }

        sampler2D _MainTex;
        sampler2D _RotatedTex;

        struct Input {
            float2 uv_MainTex;
            float2 rotatedUV;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        float _Rotation;

        void vert (inout appdata_full v, out Input o) {
            UNITY_INITIALIZE_OUTPUT(Input,o);
            o.rotatedUV = rotateUV(v.texcoord, _Rotation);
        }

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;

            // rotated texture
            fixed4 c2 = tex2D(_RotatedTex, IN.rotatedUV);

            // blend the two together so we can see them
            c = (c + c2) / 2.0;

            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

I also disabled the in editor texture scaling and offset for the rotated texture, just because that adds another layer of weirdness in surface shaders.

5 Likes

Wow. You sir (or siress :wink: have gone above and beyond. I cannot thank you enough. Not only did you take the time to help me solve the problem, but you wrote me an entire tutorial to properly understand it.

Thanks!! :slight_smile: