Advanced Billboard Shader + World-Space UI Support

October 1, 2018
5m
Unity 3DTech ArtShaders

As you know a billboard is basically a plane with a texture on it that always facing the camera.
This is an example of what we are going for.

This tutorial is going to be pretty straight-forward and easy to follow along you will learn how to make a billboard shader that not only keeps looking at the camera but also keeps its relative scaling intact.

We will also provide an option to keep it rendered on top of all the other objects in the scene. This will be most useful for world-space UI that needs to be rendered on top of other geometry.

We will be making 2 shaders here,

  1. Modified Default Unlit shader:- This one is a general shader ( easy to modify further ).
  2. Modified Default UI shader:- This one supports whatever a UI shader supports along with our billboarding capabilities.

So let's get started with making the first one. As usual create a new Unlit shader and dive into the properties we need.


Properties
{
_MainTex ("Texture Image", 2D) = "white" {}
_Scaling("Scaling", Float) = 1.0
[Toggle] _KeepConstantScaling("Keep Constant Scaling", Int) = 1
[Enum(RenderOnTop, 0,RenderWithTest, 4)] _ZTest("Render on top", Int) = 1
}

Don't get distracted by those shader attributes [xyz]. These are really useful little statements that help us format our material editor interface. I will be adding another tutorial which goes through all of them in detail.

Now let's look at how these were defined in the CG PROGRAM.


uniform sampler2D _MainTex;
int _KeepConstantScaling;
float _Scaling;

You might have noticed that the _ZTest property doesn't show up here, That's because it goes in the sub-shader state value and while we are there we have to set some sub-shader tags as well.


SubShader
{
Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "DisableBatching" = "True" }
ZWrite On /*Write to the depth buffer*/
ZTest [_ZTest] /*Replaces _ZTest with either 0 for rendering on top and 4 for regular z-test*/
Blend SrcAlpha OneMinusSrcAlpha /*Set how semi-transparent and transparent objects blend their colour*/
Pass {
 .
 .
 .
Note: We have set 'Disable Batching' to True. This is because if an object is dynamically batched the vertex input that we get will be in world space and we will be writing the vertex shader with the assumption that the vertex data will be in local space.
This shader only works with 'Quad' primitive or any geometry which has vertices in the y-axis. So the default plane will not work.

Time for the Vertex Shader


v2f vert(appdata v)
{
   v2f o;
   /*1*/ float relativeScaler = (_KeepConstantScaling) ? distance(mul(unity_ObjectToWorld, v.vertex), _WorldSpaceCameraPos) : 1;
   /*2*/ float4 viewSpaceOrigin = mul( UNITY_MATRIX_MV, float4( 0.0, 0.0, 0.0, 1.0));
   /*3*/ float4 scaledVertexLocalPos = float4( v.vertex.x, v.vertex.y, 0.0, 0.0) * relativeScaler * _Scaling;
   /*4*/ o.vertex = mul( UNITY_MATRIX_P, viewSpaceOrigin + scaledVertexLocalPos);
   /*5*/ o.uv = v.uv;
}

We will go through each line in detail.

  1. If we have _KeepConstantScaling value as false then we don't apply any relative-scaling. Incase we do apply relative scaling then we convert the vertex position from local to world-space and get it's distance from the camera. We assign it to relativeScaler value.
  2. mul( UNITY_MATRIX_MV, float4( 0.0, 0.0, 0.0, 1.0)), We are transforming the origin in terms of the view co-ordinates and assign it to viewSpaceOrigin.
  3. The vertices gets scaled according to our relativeScaler and _Scaling values and assign it to scaledVertexLocalPos.
  4. We then add the viewSpaceOrigin & scaledVertexLocalPos to get our view-space transformed vertex positions. Then we apply our perspective projection by mul( UNITY_MATRIX_P, viewSpaceOrigin + scaledVertexLocalPos)
  5. Assign our uv co-ordinates.

There are no modifications to the fragment shader.

To create a fully UI compatible shader we will use the 'Default-UI' shader that Unity provides us and make the same changes.

We mentioned we will be making two shaders,
The source code for the Billboard - Modified default unlit shader:

C#

1Shader "BitshiftProgrammer/Billboard"
2{
3	Properties
4	{
5		_MainTex ("Texture Image", 2D) = "white" {}
6		_Scaling("Scaling", Float) = 1.0
7		[Toggle] _KeepConstantScaling("Keep Constant Scaling", Int) = 1
8		[Enum(RenderOnTop, 0,RenderWithTest, 4)] _ZTest("Render on top", Int) = 1
9	}
10	SubShader
11	{
12		Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "DisableBatching" = "True" }
13
14		ZWrite On
15		ZTest [_ZTest]
16		Blend SrcAlpha OneMinusSrcAlpha
17		Pass
18		{
19        CGPROGRAM
20
21         #pragma vertex vert
22         #pragma fragment frag
23
24         uniform sampler2D _MainTex;
25		 int _KeepConstantScaling;
26		 float _Scaling;
27
28		struct appdata
29		{
30            float4 vertex : POSITION;
31            float2 uv : TEXCOORD0;
32         };
33
34         struct v2f 
35		 {
36            float4 vertex : SV_POSITION;
37            float2 uv : TEXCOORD0;
38         };
39 
40         v2f vert(appdata v)
41         {
42            v2f o;
43			float relativeScaler = (_KeepConstantScaling) ? distance(mul(unity_ObjectToWorld, v.vertex), _WorldSpaceCameraPos) : 1;
44            o.vertex = 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) * relativeScaler * _Scaling);
45            o.uv = v.uv;
46            return o;
47         }
48 
49         float4 frag(v2f i) : COLOR
50         {
51            return tex2D(_MainTex, float2(i.uv));
52         }
53
54         ENDCG
55      }
56   }
57}

The shader code for the Billboard UI - Modified default UI shader:

C#

1Shader "BitshiftProgrammer/BillboardUI"
2{
3    Properties
4    {
5        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
6	      _Scaling("Scaling", Float) = 1.0
7	      [Toggle] _KeepConstantScaling("Keep Constant Scaling", Float) = 1
8	      [Enum(RenderOnTop, 0,RenderWithTest, 4)] _ZTest("Render on top", Int) = 1
9        _Color ("Tint", Color) = (1,1,1,1)
10
11        _StencilComp ("Stencil Comparison", Float) = 8
12        _Stencil ("Stencil ID", Float) = 0
13        _StencilOp ("Stencil Operation", Float) = 0
14        _StencilWriteMask ("Stencil Write Mask", Float) = 255
15        _StencilReadMask ("Stencil Read Mask", Float) = 255
16
17        _ColorMask ("Color Mask", Float) = 15
18
19        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
20    }
21
22    SubShader
23    {
24        Tags
25        {
26            "Queue"="Transparent"
27            "IgnoreProjector"="True"
28            "RenderType"="Transparent"
29            "PreviewType"="Plane"
30            "CanUseSpriteAtlas"="True"
31        }
32
33        Stencil
34        {
35            Ref [_Stencil]
36            Comp [_StencilComp]
37            Pass [_StencilOp]
38            ReadMask [_StencilReadMask]
39            WriteMask [_StencilWriteMask]
40        }
41
42        Cull Off
43        Lighting Off
44        ZWrite Off
45        ZTest [_ZTest]
46        Blend SrcAlpha OneMinusSrcAlpha
47        ColorMask [_ColorMask]
48
49        Pass
50        {
51            Name "Default"
52        CGPROGRAM
53            #pragma vertex vert
54            #pragma fragment frag
55            #pragma target 2.0
56
57            #include "UnityCG.cginc"
58            #include "UnityUI.cginc"
59
60            #pragma multi_compile __ UNITY_UI_CLIP_RECT
61            #pragma multi_compile __ UNITY_UI_ALPHACLIP
62
63            struct appdata_t
64            {
65                float4 vertex   : POSITION;
66                float4 color    : COLOR;
67                float2 texcoord : TEXCOORD0;
68                UNITY_VERTEX_INPUT_INSTANCE_ID
69            };
70
71            struct v2f
72            {
73                float4 vertex   : SV_POSITION;
74                fixed4 color    : COLOR;
75                float2 texcoord  : TEXCOORD0;
76                UNITY_VERTEX_OUTPUT_STEREO
77            };
78
79            fixed4 _Color;
80            fixed4 _TextureSampleAdd;
81            float4 _ClipRect;
82	          float _KeepConstantScaling;
83	          float _Scaling;
84
85            v2f vert(appdata_t v)
86            {
87              v2f OUT;
88              UNITY_SETUP_INSTANCE_ID(v);
89              UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
90
91              float relativeScaler =  (_KeepConstantScaling) ? distance(mul(unity_ObjectToWorld, v.vertex), _WorldSpaceCameraPos) : 1;
92              OUT.vertex = 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) * relativeScaler * _Scaling);
93              OUT.texcoord = v.texcoord;
94              OUT.color = v.color * _Color;
95              return OUT;
96            }
97
98            sampler2D _MainTex;
99
100            fixed4 frag(v2f IN) : SV_Target
101            {
102                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
103
104                #ifdef UNITY_UI_CLIP_RECT
105                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
106                #endif
107
108                #ifdef UNITY_UI_ALPHACLIP
109                clip (color.a - 0.001);
110                #endif
111                return color;
112            }
113        ENDCG
114        }
115    }
116}