Search Unity

Rotate World UV projection?

Discussion in 'Shaders' started by tmcthee, Jan 23, 2020.

  1. tmcthee

    tmcthee

    Joined:
    Mar 8, 2013
    Posts:
    119
    Hey,

    I have a shader that projects a texture along the world Y axis.

    Code (CSharp):
    1.             v2f vert (appdata v)
    2.             {
    3.                 v2f o;
    4.                 o.vertex = UnityObjectToClipPos(v.vertex);
    5.                 o.worldProj = mul(unity_ObjectToWorld, v.vertex).xzy;
    6.                 return o;
    7.             }
    8.             fixed4 frag (v2f i) : SV_Target
    9.             {
    10.  
    11.                 fixed4 col = tex2D(_Pixels, i.worldProj*35);
    12.                 return col;
    13.             }
    Is there a bit of maths I can do to worldProj to rotate if slightly, say 45 degrees, so it's projecting diagonally through the scene, rather than parallel to one of the world axis?

    Something like

    Code (CSharp):
    1.             v2f vert (appdata v)
    2.             {
    3.                 v2f o;
    4.                 o.vertex = UnityObjectToClipPos(v.vertex);
    5.                 o.worldProj = mul(unity_ObjectToWorld, v.vertex).xzy;
    6.                 o.worldProj  = rotate45DegreesInX;
    7.                 return o;
    8.             }
    9.             fixed4 frag (v2f i) : SV_Target
    10.             {
    11.  
    12.                 fixed4 col = tex2D(_Pixels, i.worldProj*35);
    13.                 return col;
    14.             }
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Search for "rotate uv in shader" and you'll find a ton of examples.

    The main thing to understand is the
    tex2D()
    function is only using the
    .xy
    values of the
    i.worldProj
    input (so only the xz in world space, since you're swizzling in the vertex shader). You don't need to pass the float3 swizzled world position, just the float2 of the final UVs.
    Code (csharp):
    1. struct v2f {
    2.   float4 vertex : SV_POSITION;
    3.   float2 worldProj : TEXCOORD0; // float2 instead of float3
    4. };
    5.  
    6. v2f vert (appdata v)
    7. {
    8.   v2f o;
    9.   o.vertex = UnityObjectToClipPos(v.vertex);
    10.   float3 worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)).xyz;
    11.  
    12.   float s, c;
    13.   sincos(45.0 * (UNITY_PI / 180.0), s, c);
    14.   float2x2 rotationMatrix = float2x2(c, -s, s, c);
    15.   o.worldProj = mul(worldPos.xz, rotationMatrix);
    16.  
    17.   return o;
    18. }
     
    tmcthee likes this.
  3. tmcthee

    tmcthee

    Joined:
    Mar 8, 2013
    Posts:
    119
    Thanks as always Bgolus
     
  4. tmcthee

    tmcthee

    Joined:
    Mar 8, 2013
    Posts:
    119
    Hey bgolus,
    So I don't think I explained my question very well.
    I'm not trying to rotate the texture so much as I'm trying to rotate the projection. And, because I'm using worldpos to project the texture, effectively I'm trying to rotate the world, and then use that to project the texture.

    (Still don't think I'm explaining this well.)

    Basically I'm trying to create an effect for a mobile game that uses a projected texture to fake a directional light on a ball. (I don't want to use actual lighting because vertex lighting doesn't look good enough and pixel lights will be (i assume) significantly less efficient than using textures (if I can get them to work)

    At the moment the texture projects straight through x. This looks fine but it always looks like the light is pointing straight down. That's acceptable for this project, but it would be nice if I could rotate the texture to give the lighting some directionality as the ball moves in the scene.

    The method you suggested above will work, but only while the ball moves in one axis. For this game the ball can move in both x and z.

    (Even now I don't think I'm explaining this... I'll attach a video of me manually simulating the effect in Maya)

    Here's the shader so far

    Code (CSharp):
    1. Shader "TMCShaderQuarantine/RotateDlight" {
    2.     Properties
    3.     {
    4.         _DirectionalLight("Base (RGB)", 2D) = "white" {}
    5.     }
    6.  
    7.     Subshader
    8.     {
    9.         Tags{ "RenderType" = "Opaque" }
    10.  
    11.         Pass
    12.         {
    13.             CGPROGRAM
    14.             #pragma vertex vert
    15.             #pragma fragment frag
    16.             #pragma fragmentoption ARB_precision_hint_fastest
    17.             #include "UnityCG.cginc"
    18.  
    19.         struct v2f
    20.         {
    21.             float4 pos    : SV_POSITION;
    22.             float2 worldProj : TEXCOORD3;
    23.             float4 objectOrigin : TEXCOORD4;
    24.         };
    25.  
    26.         v2f vert(appdata_base v)
    27.         {
    28.             v2f o;
    29.             o.pos = UnityObjectToClipPos(v.vertex);
    30.  
    31.             float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    32.             o.worldProj = worldPos.yz;
    33.  
    34.             o.objectOrigin = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0));
    35.             o.worldProj.xy = (o.worldProj.xy / 5.8) + ((o.objectOrigin.xy / -5.8));
    36.  
    37.             float s, c;
    38.             sincos((_Time.z*10) * (UNITY_PI / 180.0), s, c);
    39.             float2x2 rotationMatrix = float2x2(c, -s, s, c) ;
    40.             rotationMatrix *= 0.5;
    41.             rotationMatrix += 0.5;
    42.             rotationMatrix = (rotationMatrix * 2) - 1;
    43.             o.worldProj = mul(o.worldProj.xy, rotationMatrix);
    44.             o.worldProj += 0.5;
    45.  
    46.             return o;
    47.     }
    48.  
    49.     sampler2D _DirectionalLight;
    50.  
    51.     fixed4 frag(v2f i) : SV_Target
    52.     {
    53.         fixed4 dLgt = tex2D(_DirectionalLight, i.worldProj);
    54.         return dLgt;
    55.     }
    56.  
    57.         ENDCG
    58.     }
    59.  
    60.     }
    61.         Fallback "Transparent/VertexLit"
    62.  
    63. }
     
    Last edited: Feb 16, 2020
  5. tmcthee

    tmcthee

    Joined:
    Mar 8, 2013
    Posts:
    119
    Heres the video of me emulating the effect I'm after in maya
     

    Attached Files:

  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Ah, so you want to project a gradient texture on the side of a ball to fake lighting.

    So, for that you'll need a 3d rotation matrix, or do two 2D rotations. Understand the sine & cosine and resulting
    float2x2
    in the original example is that rotation matrix. So you'd need to do that to the world space xz, then to the rotated world space xy to get the final local space xy. Really at that point you only need the y and can just ignore the x entirely and only pass the y. It'd be much easier to pass a matrix in from script rather than trying to generate it in the shader, especially if you're aiming for mobile.

    Code (csharp):
    1. // c#
    2. Matrix4x4 lightProjection = Matrix4x4.TRS(
    3.     Vector3.zero, // unused
    4.     Quaternion.Euler(pitch, yaw, roll), // the light rotation
    5.     Vector3.one * ballDiameter // pre-scale matrix
    6.     );
    7. material.SetMatrix("_LightProjection", lightProjection);
    8.  
    9. // shader
    10. // outside of function
    11. float3x3 _LightProjection; // note only 3x3, this does rotation and scale only
    12.  
    13. // in vertex shader
    14. o.worldProj = mul(_LightProjection, worldPos.xyz - objOrigin.xyz).y; // only use y
    15. o.worldProj = o.worldProj + 0.5; // adjust so 0.5 is at the center of the ball
    16. // prescaled matrix should put 0.0 and 1.0 at the extents of the ball
    17.  
    18. // in frag shader
    19. fixed4 lighting = tex2D(_DirectionalLight, i.worldProj.xx);
    With a little more work you could construct your matrix to work on the ball's local space vertex positions so you don't have to calculate the world position and object's pivot. To do that get the ball's rotation, construct a rotation matrix with that (
    Matrix4x4.Rotate(ball.tranform.rotation)
    ) and multiply that and the light projection matrix together (I always forget which order) in the script before setting it on the material. Then in the shader it'd just end up being:
    Code (csharp):
    1. o.worldProj = mul(_LightProjection, v.vertex.xyz).y + 0.5;
    Technically you could do that "0.5" offset in c# too by calculating an offset position from the ball for the matrix, but that might end up being slower overall since that would require a 4x4 matrix multiply instead of a 3x3 matrix multiply.
     
  7. tmcthee

    tmcthee

    Joined:
    Mar 8, 2013
    Posts:
    119
    He Bglous, sorry to be an unbearable pain in the rear, but... I must be missing something because I'm getting this... directionalLightBug.png
    Which should look like a smiley face test texture....

    I don't think my c# script is talking to the _LightProjection matrix in the shader, because when I change the values in the script, nothing is happening to the texture at all...
    The script and shader are talking, I tested with something else, so I'm a bit baffled.
    What have I done wrong (I bet it's something stupid)

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [ExecuteInEditMode]
    6. public class DirectBallLight : MonoBehaviour
    7. {
    8.     public Material _Material;
    9.     public float pitch;
    10.     public float yaw;
    11.     public float roll;
    12.     public float IT;
    13.     public float ballDiameter = 5.8f;
    14.  
    15.     void Update()
    16.     {
    17.         Matrix4x4 lightProjection = Matrix4x4.TRS
    18.        (
    19.        Vector3.zero, // unused
    20.        Quaternion.Euler(pitch, yaw, roll), // the light rotation
    21.        Vector3.one * ballDiameter  // pre-scale matrix
    22.        );
    23.         _Material.SetMatrix("_LightProjection", lightProjection);
    24.         _Material.SetFloat("_IsTalking", IT);
    25.     }
    26. }
    27.  
    Code (CSharp):
    1. Shader "TMCShaderQuarantine/BgolusDirectionLight" {
    2.     Properties
    3.     {
    4.     _DirectionalLight("Base (RGB)", 2D) = "white" {}
    5.    
    6.     }
    7.  
    8.         Subshader
    9.     {
    10.         Tags{ "RenderType" = "Opaque" }
    11.         Pass
    12.         {
    13.  
    14.         CGPROGRAM
    15.         #pragma vertex vert
    16.         #pragma fragment frag
    17.         #pragma fragmentoption ARB_precision_hint_fastest
    18.         #include "UnityCG.cginc"
    19.         struct appdata
    20.         {
    21.             float4 vertex    : POSITION;
    22.             float4 objectOrigin : TEXCOORD3;
    23.             float2 worldProj : TEXCOORD4;
    24.         };
    25.         struct v2f
    26.         {
    27.             float4 vertex    : SV_POSITION;
    28.             float4 objectOrigin : TEXCOORD3;
    29.             float2 worldProj : TEXCOORD4;
    30.  
    31.         };
    32.         float3x3 _LightProjection;
    33.         float _IsTalking;
    34.         v2f vert(appdata v)
    35.         {
    36.             v2f o;
    37.             o.vertex = UnityObjectToClipPos(v.vertex);
    38.             o.objectOrigin = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0));
    39.             float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    40.             o.worldProj = mul(_LightProjection, worldPos.xyz - o.objectOrigin.xyz).y; // only use y
    41.             o.worldProj = o.worldProj + 0.5; // adjust so 0.5 is at the center of the ball
    42.             return o;
    43.         }
    44.  
    45.         sampler2D _DirectionalLight;
    46.  
    47.         fixed4 frag(v2f i) : SV_Target
    48.         {
    49.             fixed4 lighting = tex2D(_DirectionalLight, i.worldProj.xx);
    50.             return lighting + _IsTalking;
    51.         }
    52.  
    53.             ENDCG
    54.         }
    55.     }
    56. }
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Swap that to a
    float4x4
    instead. Apparently Unity doesn't like it when you define an input matrix as a
    float3x3
    .
     
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Another bug I had in the original code is the scale needs to be divided, not multiplied. Here's some tweaks I made. Script uses a dummy object (in this case, an actual directional light) to get the orientation, and uses the ball's world rotation so the transform can be applied directly to the
    v.vertex
    instead of extracting the object space location. It's also doing the offset in the matrix, since that ended up being easier than I expected and doesn't seem to be significantly slower.

    One of these balls is using the included shader, one of them is using the Legacy/Diffuse shader.
    upload_2020-2-18_10-34-48.png
    The right ball is using the custom shader.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. [ExecuteInEditMode]
    5. public class DirectBallLight : MonoBehaviour
    6. {
    7.     public Transform _LightDummy;
    8.     public Transform _Ball;
    9.     public Material _Material;
    10.     public float ballDiameter = 1.0f;
    11.     void Update()
    12.     {
    13.         if (_LightDummy == null || _Ball == null || _Material == null)
    14.             return;
    15.  
    16.         Matrix4x4 lightProjection = Matrix4x4.TRS(
    17.             Vector3.one * 0.5f,
    18.             Quaternion.Inverse(_LightDummy.rotation) * _Ball.rotation,
    19.             Vector3.one / ballDiameter
    20.         );
    21.  
    22.         _Material.SetMatrix("_LightProjection", lightProjection);
    23.     }
    24. }
    Code (CSharp):
    1. Shader "DirectBallLight" {
    2.     Properties
    3.     {
    4.         _MainTex("Base (RGB)", 2D) = "white" {}
    5.         [NoScaleOffset] _DirectionalLight("Light", 2D) = "grey" {}
    6.     }
    7.     Subshader
    8.     {
    9.         Tags { "RenderType" = "Opaque" "DisableBatching" = "True" }
    10.         Pass
    11.         {
    12.             CGPROGRAM
    13.             #pragma vertex vert
    14.             #pragma fragment frag
    15.             #pragma fragmentoption ARB_precision_hint_fastest
    16.             #include "UnityCG.cginc"
    17.  
    18.             struct v2f
    19.             {
    20.                 float4 vertex    : SV_POSITION;
    21.                 float2 uv        : TEXCOORD0;
    22.                 float2 worldProj : TEXCOORD1;
    23.             };
    24.  
    25.             sampler2D _MainTex;
    26.             float4 _MainTex_ST;
    27.    
    28.             sampler2D _DirectionalLight;
    29.             half4x4 _LightProjection;
    30.  
    31.             v2f vert(appdata_full v)
    32.             {
    33.                 v2f o;
    34.                 o.vertex = UnityObjectToClipPos(v.vertex);
    35.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    36.  
    37.                 o.worldProj = mul(_LightProjection, half4(v.vertex.xyz,1)).zy;
    38.                 return o;
    39.             }
    40.    
    41.             fixed4 frag(v2f i) : SV_Target
    42.             {
    43.                 fixed4 col = tex2D(_MainTex, i.uv);
    44.                 fixed4 lighting = tex2D(_DirectionalLight, i.worldProj);
    45.                 return col * lighting;
    46.             }
    47.             ENDCG
    48.         }
    49.     }
    50. }
    The light texture is setup so that it's a gradient with the left edge being the light side and right edge being dark. I've included the gradient I used to test above.
     

    Attached Files:

  10. tmcthee

    tmcthee

    Joined:
    Mar 8, 2013
    Posts:
    119
    Fantastic Bgolus, you're my new favourite person. (I really think that I should be paying you!)
    *cough-doacourse-cough*
     
    Last edited: Feb 19, 2020
  11. tmcthee

    tmcthee

    Joined:
    Mar 8, 2013
    Posts:
    119
    Thinking a bit more about this. I suppose, now that we know the orientation of the fake light. I wonder how it would look to project a "shadow map" along the y axis. I might give that a go in future.