Possible to get objects rotation value in unity_ObjectToWorld?

I am starting to learn shader programming so Iv’e been looking to make a billboard shader that does what I wan’t. It should be no problem however I cannot seem to wrap my head around accessing the rotation data of the object the shader currently lives on.

I can get its location and from everything i have read says its in the 4x4 model matrix returned by unity_ObjectToWorld however I have no idea how to access the rotation data in that model matrix.

Ultimately I want the billboarding sprite to change the uv position based on the models rotation so its always “facing correctly” when an object moves about the game world but i’m so new to shader coding this seems to be going over my head…

You should read up on transform matrices. The rotation and scale are stored in the 3x3 part of the matrix, with the position in the last column. Extracting the rotation into something user friendly like Euler angles is doable, but more trouble than it’s worth. You only want to find the angle between the view direction and the sprite’s forward direction.

@mgear has example of how to go about this here:
https://github.com/unitycoder/DoomStyleBillboardTest

1 Like

I actually already saw that example and ive been working off of it and I have a buggy shader that sort of half works.

Basically, I want it to change the UV position of the current texture based on the camera rotation AND the objects rotation. I think I’ve got it to a degree (heh) but it seems like part of the math here is going over my head because it only displays properly at certain angles.

Here’s the full code.

//based on unity coders doom billboard shader
//https://github.com/unitycoder/DoomStyleBillboardTest

Shader "Advanced Billboard"
{

    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Frames ("Frames", Float) = 8
    }

    SubShader
    {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
 
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha
 
        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #define PI 3.1415926535897932384626433832795
            #define RAD2DEG 57.2957795131
            #define SINGLEFRAMEANGLE (360/_Frames)
            #define UVOFFSETX (1/_Frames)
            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;
            uniform float4 _MainTex_ST;

            struct appdata {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
            };

            float _Frames;


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

                // object world position
                   float3 objWorldPos=float3(unity_ObjectToWorld._m03,unity_ObjectToWorld._m13,unity_ObjectToWorld._m23);

                //get objects current Y rotation from its rotation matrix in radians
                float heading = atan2(unity_ObjectToWorld._m20,unity_ObjectToWorld._m00)*RAD2DEG+180;

                // get angle between object and camera
                float3 fromCameraToObject = normalize(objWorldPos - _WorldSpaceCameraPos.xyz);
                float angle = atan2(fromCameraToObject.z, fromCameraToObject.x)*RAD2DEG+180;

                // get current tilesheet frame and feed it to UV also subtracts objects Y rotation frames from the current index
                int index = (angle/SINGLEFRAMEANGLE)-(heading/SINGLEFRAMEANGLE);

                o.uv = float2(v.texcoord.x*UVOFFSETX+UVOFFSETX*index,v.texcoord.y);

        
    
            o.pos = mul(UNITY_MATRIX_P,
              mul(UNITY_MATRIX_MV, float4(0.0, 0.0, 0.0, 1.0))
              + float4(v.vertex.x, v.vertex.y, 0.0, 0.0)
              * float4(1, 1, 1, 1.0));


                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                return tex2D(_MainTex,i.uv);
            }
            ENDCG
        }
    }
}

Here’s my sprite sheet if you want to test it. Just put this in a new material with 8 frames

I’m aware this will go negative and will technically be “out of index” by not displaying but I’m stuck here I can’t simply make it just spit out the absolute value or min it out at 0. none of that displays properly.

I’ll give it another go some other day but a little help would be appreciated :slight_smile:

I rewrote the shader a bit for fun. @mgear , maybe you’d like this too.

The main things that changed:

  • The original used +X axis for forward heading calculation, but Unity uses +Z axis for forward. Minor, but useful for consistency. This does make selecting it in the editor annoying since the default Quad mesh is oriented -Z forward and Unity’s selection code doesn’t take into account any shader manipulations, so trying to select the sprite while looking at the front doesn’t work… Setting Cull Off makes this a little better as it’ll at least select the backface.
  • The frame index was such that it would switch between frame 0 and frame -1 at 0 degrees. But really you want everything between -22.5 and 22.5 degrees to be frame 0 when you have 8 frames. To solve this I use round() instead of relying on the implicit floor() of converting from float to int. This is kind of the big one. Otherwise the sprites always feel like they’re slightly off of the correct rotation.

Obviously there were a few more changes too. I skip the adjustments to bring the angle ranges from [-180, 180] to [0, 360] degrees since it’s it makes no difference in the resulting angle diff and just adds some extra math. I removed unnecessary conversions from radians to degrees. Mostly for clutter reduction. Flipped around the cameraToObject vector to be objectToCamera so that both the forward vector and camera vectors are in the same “space”. Before they were flipped so that “forward” ended up being the -X axis.

Shader "Advanced Billboard"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Frames ("Frames", Float) = 8
    }

    SubShader
    {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}

        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha
        Cull Off

        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            #define TAU (UNITY_PI * 2.0)
            #define SINGLEFRAMEANGLE (TAU / _Frames)
            #define UVOFFSETX (1.0 / _Frames)

            uniform sampler2D _MainTex;
            uniform float4 _MainTex_ST;

            struct appdata {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            float _Frames;

            v2f vert (appdata v)
            {
                v2f o;

                // object's forward vector (local +Z direction)
                float3 objWorldForward = unity_ObjectToWorld._m02_m12_m22;
                //get objects current Y rotation from its rotation matrix in radians
                float objWorldHeading = atan2(objWorldForward.z, objWorldForward.x);

                // object's world position
                float3 objWorldPos = unity_ObjectToWorld._m03_m13_m23;
                // get angle between object and camera in radians
                float3 objToCam = _WorldSpaceCameraPos.xyz - objWorldPos;
                float objToCamAngle = atan2(objToCam.z, objToCam.x);

                // get angle difference between heading and camera relative position
                float angleDiff = objToCamAngle - objWorldHeading;

                // get current tilesheet frame and feed it to UV also subtracts objects Y rotation frames from the current index
                float index = round(angleDiff / SINGLEFRAMEANGLE);

                o.uv = float2(v.texcoord.x * UVOFFSETX + UVOFFSETX * index, v.texcoord.y);

                o.pos = mul(UNITY_MATRIX_P,
                  mul(UNITY_MATRIX_V, float4(objWorldPos, 1.0))
                  + float4(v.vertex.x, v.vertex.y, 0.0, 0.0));

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                return tex2D(_MainTex,i.uv);
            }
            ENDCG
        }
    }
}
3 Likes

I might have skipped too much trig in highschool.

Its nice to know the conversions to degrees were redundant. Thank you so much though this shader is a huge step in the right direction!

Only question I have though is it rotates and displays mostly perfectly but only for the first 90 degrees or so while looking at it. When viewing it outside that 90 degrees it displays nothing. There also appears to be a brief out of index when going to frames 0 to 8 and vice versa.

I might just be doing it wrong though so I’m trying some new things out with it.
EDIT: oh fun it works almost perfectly (still briefly disappearing between frames 0 and 8) when the object is rotated at certain angles (206 degrees on the y is one). I fear somehow the angleDiff calculation is a bit inaccurate.

My project is using the Light Weight Render Pipeline if that makes any functional difference.
IT DOES

works mostly fine using the standard render settings. Well it sort of mirrors its display weirdly

like this but I think i can figure that out. maybe

Ahh I see setting cull off seems to display the reverse side as well but its rotated 180 degrees so its rightside up because by default it displays upside down…this is strange since this wasnt how it displayed in the LWRP but in the regular pipeline its always been like this even in my old shader lol.

I needed to reverse a few values but it works great thanks!

The only problem is the weird selection halo of the combined faces and that’s honestly just a visual bug I can get over.

Hey it’s me again. Every time I work on this thing I learn so much more but I still don’t manage to figure it out fully lol.

I wanted to try working on a sort of x rotation for the objects but only calculated via the camera angle and I figured it would be easy given everything I already had and I was almost right.

My problem here is I’m not bothering to do a whole range of motion for this rotation. only the positive 180 degrees. here’s my spritesheet to illustrate this.

I have this functionality almost working in my shader but only problem is for the life of me I can’t get it to display the top row properly (directly above).

I have tried all sorts of poking and prodding all of which have their own failures and successes but this is the current most functional version of the shader which does most of what I need it to do.

please let me know if I am being silly I am still very new to this. Also ignore the bits of extra lighting code in there I was halfway implementing that.

// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

Shader "Advanced Billboard"
{
    Properties
    {
        _MainTex ("Albedo", 2D) = "white" {}
        _Color ("Diffuse Color", Color) = (1,1,1,1)
        _Frames ("Frames", Float) = 8
        _Yframes ("Yframes", Float) = 5
        _Smoothness ("Smoothness", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha
        //Cull Off
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #define TAU (UNITY_PI * 2.0)
            #define SINGLEFRAMEANGLE (TAU / _Frames)
            #define SINGLEYFRAMEANGLE (TAU / _Yframes)
            #define UVOFFSETX (1.0 / _Frames)
            #define UVOFFSETY (1.0 / _Yframes)

            uniform float4 _Color;
            uniform sampler2D _MainTex;
            uniform float4 _MainTex_ST;
            uniform fixed4 _LightColor0;
            struct vertexInput {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };
            struct vertex2Frag {
                float4 pos : SV_POSITION;
                float4 color : COLOR;
                float2 uv : TEXCOORD0;
                float3 normal : TEXCOORD1;
            };
            float _Frames;
            int _Yframes;

            vertex2Frag vert (vertexInput v)
            {
                vertex2Frag o;
                // object's forward vector (local +Z direction)
                float3 objWorldForward = unity_ObjectToWorld._m02_m12_m22;

                //get objects current Y rotation from its rotation matrix in radians
                float objWorldHeading = atan2(-objWorldForward.z, objWorldForward.x);
                // object's world position
                float3 objWorldPos = unity_ObjectToWorld._m03_m13_m23;

                // get angle between object and camera in radians
                float3 objToCam = _WorldSpaceCameraPos.xyz - objWorldPos;
                float objToCamAngle = atan2(-objToCam.z, objToCam.x);

                //calculates specific angles between camera and object in radians
                float objToCamYAngle =  atan2(-objToCam.y, min(min(objToCam.z,-objToCam.z),min(objToCam.x,-objToCam.x)));


                // get angle difference between heading and camera relative position
                float angleDiff = objToCamAngle - objWorldHeading;
              
                // get current tilesheet frame on the x and y axis of the spritesheet
                float index = round(angleDiff / SINGLEFRAMEANGLE);
                float yindex = round(objToCamYAngle / SINGLEYFRAMEANGLE);
                o.uv = float2(v.texcoord.x * UVOFFSETX + UVOFFSETX * index, -v.texcoord.y * UVOFFSETY + UVOFFSETY * yindex);
                float3 normalDir = normalize(_WorldSpaceCameraPos - objWorldPos);
                o.normal = v.normal;
                o.pos = mul(UNITY_MATRIX_P,
                  mul(UNITY_MATRIX_V, float4(objWorldPos, 1.0))
                  + float4(v.vertex.x, v.vertex.y, 0.0, 0.0));

                float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
                float3 diffuse =  _Color.rgb; //* _LightColor0.rgb * max(0.0,dot(normalDir, lightDirection));

                o.color = half4(diffuse, 1.0);


                return o;
            }
            fixed4 frag(vertex2Frag i) : COLOR
            {
                return tex2D(_MainTex,i.uv) * i.color;
            }
            ENDCG
        }
    }
}

To get the vertical angle you just need:

asin(normalize(objToCam).y)

edit: asin, not atan

1 Like

For the vertically offset you have to think about the fact your yaw rotation starts at 0 degrees at the 0 column index. Your row 0 (the bottom row) is -90 degrees.

1 Like

It’s beautiful!

Thanks!

// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

Shader "Advanced Billboard"
{
    Properties
    {
        _MainTex ("Albedo", 2D) = "white" {}
        _Color ("Diffuse Color", Color) = (1,1,1,1)
        _Frames ("Frames", Float) = 8
        _Yframes ("Yframes", Float) = 5
        _Smoothness ("Smoothness", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha
        //Cull Off
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #define TAU (UNITY_PI * 2.0)
            #define SINGLEFRAMEANGLE (TAU / _Frames)
            #define SINGLEYFRAMEANGLE (UNITY_PI / _Yframes)
            #define UVOFFSETX (1.0 / _Frames)
            #define UVOFFSETY (1.0 / _Yframes)

            uniform float4 _Color;
            uniform sampler2D _MainTex;
            uniform float4 _MainTex_ST;
            uniform fixed4 _LightColor0;
            struct vertexInput {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };
            struct vertex2Frag {
                float4 pos : SV_POSITION;
                float4 color : COLOR;
                float2 uv : TEXCOORD0;
                float3 normal : TEXCOORD1;
            };
            float _Frames;
            int _Yframes;

            vertex2Frag vert (vertexInput v)
            {
                vertex2Frag o;
                // object's forward vector (local +Z direction)
                float3 objWorldForward = unity_ObjectToWorld._m02_m12_m22;

                //get objects current Y rotation from its rotation matrix in radians
                float objWorldHeading = atan2(-objWorldForward.z, objWorldForward.x);
                // object's world position
                float3 objWorldPos = unity_ObjectToWorld._m03_m13_m23;

                // get angle between object and camera in radians
                float3 objToCam = _WorldSpaceCameraPos.xyz - objWorldPos;
                float objToCamAngle = atan2(-objToCam.z, objToCam.x);

                //calculates specific angles between camera and object in radians
                float objToCamYAngle =  max(asin(normalize(objToCam).y)-30,asin(normalize(objToCam).y)+30);
              

                // get angle difference between heading and camera relative position
                float angleDiff = objToCamAngle - objWorldHeading;
              
                // get current tilesheet frame on the x and y axis of the spritesheet
                float index = round(angleDiff / SINGLEFRAMEANGLE);
                float yindex = round(objToCamYAngle / SINGLEYFRAMEANGLE);
                o.uv = float2(v.texcoord.x * UVOFFSETX + UVOFFSETX * index, -v.texcoord.y * UVOFFSETY + UVOFFSETY * yindex);
                float3 normalDir = normalize(_WorldSpaceCameraPos - objWorldPos);
                o.normal = v.normal;
                o.pos = mul(UNITY_MATRIX_P,
                  mul(UNITY_MATRIX_V, float4(objWorldPos, 1.0))
                  + float4(v.vertex.x, v.vertex.y, 0.0, 0.0));

                float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
                float3 diffuse =  _Color.rgb; //* _LightColor0.rgb * max(0.0,dot(normalDir, lightDirection));

                o.color = half4(diffuse, 1.0);


                return o;
            }
            fixed4 frag(vertex2Frag i) : COLOR
            {
                return tex2D(_MainTex,i.uv) * i.color;
            }
            ENDCG
        }
    }
}

Now if you want to melt your brain you can try the next step:

1 Like