Rotate World UV projection?

Hey,

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

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldProj = mul(unity_ObjectToWorld, v.vertex).xzy;
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {

                fixed4 col = tex2D(_Pixels, i.worldProj*35);
                return col;
            }

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

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldProj = mul(unity_ObjectToWorld, v.vertex).xzy;
                o.worldProj  = rotate45DegreesInX;
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {

                fixed4 col = tex2D(_Pixels, i.worldProj*35);
                return col;
            }

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.

struct v2f {
  float4 vertex : SV_POSITION;
  float2 worldProj : TEXCOORD0; // float2 instead of float3
};

v2f vert (appdata v)
{
  v2f o;
  o.vertex = UnityObjectToClipPos(v.vertex);
  float3 worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)).xyz;

  float s, c;
  sincos(45.0 * (UNITY_PI / 180.0), s, c);
  float2x2 rotationMatrix = float2x2(c, -s, s, c);
  o.worldProj = mul(worldPos.xz, rotationMatrix);

  return o;
}
1 Like

Thanks as always Bgolus

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

Shader "TMCShaderQuarantine/RotateDlight" {
    Properties
    {
        _DirectionalLight("Base (RGB)", 2D) = "white" {}
    }

    Subshader
    {
        Tags{ "RenderType" = "Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest
            #include "UnityCG.cginc"

        struct v2f
        {
            float4 pos    : SV_POSITION;
            float2 worldProj : TEXCOORD3;
            float4 objectOrigin : TEXCOORD4;
        };

        v2f vert(appdata_base v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);

            float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
            o.worldProj = worldPos.yz;

            o.objectOrigin = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0)); 
            o.worldProj.xy = (o.worldProj.xy / 5.8) + ((o.objectOrigin.xy / -5.8));

            float s, c;
            sincos((_Time.z*10) * (UNITY_PI / 180.0), s, c);
            float2x2 rotationMatrix = float2x2(c, -s, s, c) ;
            rotationMatrix *= 0.5;
            rotationMatrix += 0.5;
            rotationMatrix = (rotationMatrix * 2) - 1;
            o.worldProj = mul(o.worldProj.xy, rotationMatrix);
            o.worldProj += 0.5;

            return o;
    }

    sampler2D _DirectionalLight;

    fixed4 frag(v2f i) : SV_Target
    {
        fixed4 dLgt = tex2D(_DirectionalLight, i.worldProj); 
        return dLgt;
    }

        ENDCG
    }

    }
        Fallback "Transparent/VertexLit"

}

Heres the video of me emulating the effect I’m after in maya

5484963–561867–2020-02-16 06-58-15.zip (3.54 MB)

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.

// c#
Matrix4x4 lightProjection = Matrix4x4.TRS(
    Vector3.zero, // unused
    Quaternion.Euler(pitch, yaw, roll), // the light rotation
    Vector3.one * ballDiameter // pre-scale matrix
    );
material.SetMatrix("_LightProjection", lightProjection);

// shader
// outside of function
float3x3 _LightProjection; // note only 3x3, this does rotation and scale only

// in vertex shader
o.worldProj = mul(_LightProjection, worldPos.xyz - objOrigin.xyz).y; // only use y
o.worldProj = o.worldProj + 0.5; // adjust so 0.5 is at the center of the ball
// prescaled matrix should put 0.0 and 1.0 at the extents of the ball

// in frag shader
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:

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.

He Bglous, sorry to be an unbearable pain in the rear, but… I must be missing something because I’m getting this…5493301--563065--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)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class DirectBallLight : MonoBehaviour
{
    public Material _Material;
    public float pitch;
    public float yaw;
    public float roll;
    public float IT;
    public float ballDiameter = 5.8f;

    void Update()
    {
        Matrix4x4 lightProjection = Matrix4x4.TRS
       (
       Vector3.zero, // unused
       Quaternion.Euler(pitch, yaw, roll), // the light rotation
       Vector3.one * ballDiameter  // pre-scale matrix
       );
        _Material.SetMatrix("_LightProjection", lightProjection);
        _Material.SetFloat("_IsTalking", IT);
    }
}
Shader "TMCShaderQuarantine/BgolusDirectionLight" {
    Properties
    {
    _DirectionalLight("Base (RGB)", 2D) = "white" {}
   
    }

        Subshader
    {
        Tags{ "RenderType" = "Opaque" }
        Pass
        {

        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma fragmentoption ARB_precision_hint_fastest
        #include "UnityCG.cginc"
        struct appdata
        {
            float4 vertex    : POSITION;
            float4 objectOrigin : TEXCOORD3;
            float2 worldProj : TEXCOORD4;
        };
        struct v2f
        {
            float4 vertex    : SV_POSITION;
            float4 objectOrigin : TEXCOORD3;
            float2 worldProj : TEXCOORD4;

        };
        float3x3 _LightProjection;
        float _IsTalking;
        v2f vert(appdata v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.objectOrigin = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0));
            float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
            o.worldProj = mul(_LightProjection, worldPos.xyz - o.objectOrigin.xyz).y; // only use y
            o.worldProj = o.worldProj + 0.5; // adjust so 0.5 is at the center of the ball
            return o;
        }

        sampler2D _DirectionalLight;

        fixed4 frag(v2f i) : SV_Target
        {
            fixed4 lighting = tex2D(_DirectionalLight, i.worldProj.xx);
            return lighting + _IsTalking;
        }

            ENDCG
        }
    }
}

Swap that to a float4x4 instead. Apparently Unity doesn’t like it when you define an input matrix as a float3x3.

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.


Which one is which? The right ball is using the custom shader.

DirectBallLight.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class DirectBallLight : MonoBehaviour
{
    public Transform _LightDummy;
    public Transform _Ball;
    public Material _Material;
    public float ballDiameter = 1.0f;
    void Update()
    {
        if (_LightDummy == null || _Ball == null || _Material == null)
            return;

        Matrix4x4 lightProjection = Matrix4x4.TRS(
            Vector3.one * 0.5f,
            Quaternion.Inverse(_LightDummy.rotation) * _Ball.rotation,
            Vector3.one / ballDiameter
        );

        _Material.SetMatrix("_LightProjection", lightProjection);
    }
}

DirectBallLight.shader

Shader "DirectBallLight" {
    Properties
    {
        _MainTex("Base (RGB)", 2D) = "white" {}
        [NoScaleOffset] _DirectionalLight("Light", 2D) = "grey" {}
    }
    Subshader
    {
        Tags { "RenderType" = "Opaque" "DisableBatching" = "True" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest
            #include "UnityCG.cginc"

            struct v2f
            {
                float4 vertex    : SV_POSITION;
                float2 uv        : TEXCOORD0;
                float2 worldProj : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
   
            sampler2D _DirectionalLight;
            half4x4 _LightProjection;

            v2f vert(appdata_full v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

                o.worldProj = mul(_LightProjection, half4(v.vertex.xyz,1)).zy;
                return o;
            }
   
            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                fixed4 lighting = tex2D(_DirectionalLight, i.worldProj);
                return col * lighting;
            }
            ENDCG
        }
    }
}

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.

5493571--563137--ballLight.png

Fantastic Bgolus, you’re my new favourite person. (I really think that I should be paying you!)
cough-doacourse-cough

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.