This post aims to create a simple visual effect for Unity to use in my character selection screen for a game I'm working on. It will be a lit up cylinder with some swirly textures on it, surrounding the character.


A visual effect need three parts.

A nice mesh.

A good shader.

Good textures.


Creating the mesh

I modeled (if you can even call it that) this cylinder in Blender. Its a simple cylinder without a top or bottom. The poly count might be a bit high but it hardly matters. It's also unwrapped to loop perfectly around from top to bottom and side to side.


That's it. Model done! Let's import it in Unity so we can start with the effect.


Writing a new Shader

I'm an old fashioned guy when it comes to shaders. I prefer to write them in CG instead if using any of the new fancy node based editors.

The shader code will be written intertwined with adding textures as we go.


Start by creating a new shader based on the standard surface shader. I named it SelectionEffect. I also created a material based on this Shader and applied it to the cylinder in the scene. First of we will strip away all superfluous parts of the file.


What I'm left with is now this.

Shader "Custom/SelectionEffect"
{
   Properties
   {
       _Color ("Color", Color) = (1,1,1,1)
       _MainTex ("Albedo (RGB)", 2D) = "white" {}
   }
   SubShader
   {
       Tags {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
            "ForceNoShadowCasting" = "True"
        }

       LOD 200

       CGPROGRAM
       #pragma surface surf Lambert alpha:fade
       #pragma target 3.0

       sampler2D _MainTex;

       struct Input
       {
           float2 uv_MainTex;
       };


       fixed4 _Color;

       void surf (Input IN, inout SurfaceOutput o)
       {
           fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
           o.Albedo = c.rgb;
           o.Alpha = c.a;
       }
       ENDCG
   }
   FallBack "Diffuse"
}

We've added some tags set it to the transparent render type instead of the opaque. We've also set it to use the transparent queue. The effect will be shown as pure light without shadows hence the ForceNoShadowCasting tag.

In the #pragma line we've changed the render type to Lambert and made sure the transparent render uses additive shading (as opposed to alpha blending).


First step goal is to add a scrolling texture.

I've made a seamless texture using Krita that will be used for this.

Import it to Unity and apply it as the main texture in the material. Increase the tiling to add some more details. I changed it from (1, 1) to (5,2). It's tiled more in the X axis as the cylinder wider across than its tall.


We need to add a new field to the properties block.

_ScrollSpeed("Scroll Speed", Range(-50, 50)) = 10

We also need to add a corresponding variable in the CG segment, right under the _Color variable.

fixed _ScrollSpeed;

Next, we will update the line in the surf function responsible for sampling the texture to add an offset to the UV based on out the _ScrollSpeed value and the built-in variable _Time.x

fixed4 c1 = tex2D (_MainTex, IN.uv_MainTex - fixed2(0, _Time.x * _ScrollSpeed)) * _Color;


To make the effect more dramatic and less predictable we will sample the texture again with a different offset and combine the result. To make sure this effects glows we will use o.Emission instead o.Rgb .


This is the surf function so far.

fixed4 c1 = tex2D (_MainTex, IN.uv_MainTex - fixed2(0, _Time.x * _ScrollSpeed)) * _Color;
fixed4 c2 = tex2D(_MainTex, IN.uv_MainTex - fixed2(0.5, _Time.x * _ScrollSpeed * 0.421)) * _Color;
fixed4 c = min(1, c1 + c2);

o.Emission = c.rgb;
o.Alpha = c.a;


Results so far:


Adding a smooth top and bottom

Next we need to make sure the top and the bottom fades. We will use a new guide texture for that purpose.


Add a new texture property and a texture sampler.

_Gradient("Gradient", 2D) = "white" {}


sampler2D _Gradient;

Add a uv variable for in the Input struct just below the uv_MainTex.

float2 uv_Gradient;

In the surf function we need to sample this texture and apply in the the alpha channel only.

fixed alpha = tex2D(_Gradient, IN.uv_Gradient);


o.Alpha = c.a * alpha;

Apply the texture in the inspector.


We have no made something with no obvious top or bottom edge.


Before we continue, lets reduce the light in our scene so we can see the effect better.


Adding a rim effect

Next we are going to make the sides more smooth. This is done by what is normally called a fresnel effect or a rim effect (technically the reverse as we will hide the rim). We will increase the alpha based on how much each pixel is facing away from the camera.


Add a new property and its corresponding variable.

_RimIntensity("Intensity", Range(0, 10)) = 1


fixed _RimIntensity;


In the Input struct we will have to add two new variables.

float3 worldNormal;
float3 viewDir;

They have to be named exactly like this. Unity will automatically populate them. The worldNormal variable will be set to a vertex's normal, in world space. The viewDir will be set to the direction of the main camera, also in world space.

In the surf function we can use this to calculate how far off from the camera a pixel is facing and later use that as an additional alpha value.


fixed rim = pow(abs(dot(IN.worldNormal, IN.viewDir)), _RimIntensity);
o.Emission = c.rgb * rim;
o.Alpha = c.a * alpha * rim;


Set the _RimIntensity value to 2 in the inspector, set the color to something nice and observer the result.



Complete shader code

Shader "Custom/SelectionEffect"
{
   Properties
   {
       _Color ("Color", Color) = (1,1,1,1)
       _MainTex ("Albedo (RGB)", 2D) = "white" {}
       _Gradient("Gradient", 2D) = "white" {}
       _ScrollSpeed("Scroll Speed", Range(-50, 50)) = 10
       _RimIntensity("Intensity", Range(0, 10)) = 1
   }
   SubShader
   {
       Tags {
           "Queue" = "Transparent"
           "IgnoreProjector" = "True"
           "RenderType" = "Transparent"
           "ForceNoShadowCasting" = "True"
       }

       LOD 200

       CGPROGRAM
       #pragma surface surf Lambert alpha:fade
       #pragma target 3.0

       sampler2D _MainTex;
       sampler2D _Gradient;

       struct Input
       {
           float2 uv_MainTex;
           float2 uv_Gradient;
           float3 worldNormal;
           float3 viewDir;
       };


       fixed4 _Color;
       fixed _ScrollSpeed;
       fixed _RimIntensity;

       void surf (Input IN, inout SurfaceOutput o)
       {
           fixed4 c1 = tex2D (_MainTex, IN.uv_MainTex - fixed2(0, _Time.x * _ScrollSpeed)) * _Color;
           fixed4 c2 = tex2D(_MainTex, IN.uv_MainTex - fixed2(0.5, _Time.x * _ScrollSpeed * 0.421)) * _Color;
           fixed4 c = min(1, c1 + c2);

           fixed alpha = tex2D(_Gradient, IN.uv_Gradient);
           fixed rim = pow(abs(dot(IN.worldNormal, IN.viewDir)), _RimIntensity);
           o.Emission = c.rgb * rim;
           o.Alpha = c.a * alpha * rim;
       }
       ENDCG
   }
   FallBack "Diffuse"
}