I had a random idea for how I could create a sky-beam type of effect with Shader in Unity. I currently don't have any use for it but I had fun making it and it might come in handy at some point.

Something I learned early while making visual effects is that it requires three things.

  • Good geometry (mesh)
  • Good texture maps.
  • Good shader.

Create the mesh

The mesh is made in Maya. Its just a long cylinder with its bottom flattened out. The UV covers half of the mesh in width and then mirrors back, and covers it the entire length, wrapping perfectly around the mesh.

(Mesh and UV unwrap in Maya)


Creating the texture maps

To make this effect I needed som kind wobbly flowy texture that wraps around and tiles seamlessly. I like to make all my texture by hand in Krita, an open source digital drawing tool. One of the features I like most in Krita, that other drawing tools I've tried lack, is the Wrap Around Mode. While enabled, all textures drawn becomes seamless and tilable.

Apart from the flowy texture I also made a simple gradient alpha guide.

(Wobbly flowy texture)

(Gradient alpha guide)


Writing the shader

(The entire shader can be found at the bottom of the article)

Many people making shaders for Unity today likes to use ShaderGraph. Its a good tool and its included in Unity for free. I have nothing against it but writing shaders for hand ShaderLab/CG is my preferred method.


The idea for the shader is to sample the flowy texture twice with different offset and combine the result. The gradient texture is there to made the upper and lower edge fade smoothly.


First we have the properties block. (These are the way to display variables in Unity's inspector.

_Color
sets the tinted color of the whole effect.
_ColorAddition
is used to add a set amount of color to to the whole effect. This helps the effect look smoother. To make the edges hade smoothly the dot product between the face's direction and the camera direction is used to influence the alpha output.
_Intensity
is a variable created to factor this effect.

Properties
{
   _Color ("Color", Color) = (1,1,1,1)
   _ColorAddition("Color addition", Range(0, 1)) = 0
   _MainTex ("Albedo (RGB)", 2D) = "white" {}
   _Gradient("Gradient", 2D) = "white" {}
   _Intensity("Intensity", Range(0, 10)) = 1
   _ScrollSpeed("Scroll Speed", Range(-50, 50)) = 1
}


This effects is heavily reliant on the usage of alpha blending and must use the correct Queue.

ForceNoShadowCasting
is set because this effect simulates a pillar of light and light casts no shadow (Well, not on their own).

Tags {
    "Queue" = "Transparent"
    "IgnoreProjector" = "True"
    "RenderType" = "Transparent"
    "ForceNoShadowCasting" = "True"
}


This is part of the CG code. It's unaware of the flags set in Tags section and needs to be set again. alpha:fade makes the shader apply the output additively to the back buffer.

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


These are the two texture samples used.

sampler2D _MainTex;
sampler2D _Gradient;


This is the struct used to get data into the surface shader. The shader only specifies a surface shader so Unity will handle and populates all these values automatically.

uv_MainTex
and
uv_Gradient
uses the standard naming conventions for UV coordinates for a sampler and wont need manual assignment. If there were named anything else a vertex shader would have been needed to set the UV's. worldNormal and viewDir are both Unity standard and will be assign to their proper values if the exists in the surface shader input struct. I'll be referring to the offical documentation for more information.

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


Next are the same variables as declared in the properties block, now declared in the CG part fo the code.

fixed4 _Color;
fixed _ColorAddition;
fixed _Intensity;
fixed _ScrollSpeed;


This is the most important part of the shader, the surface shader. It determines that color the shader outputs.

As this is a light effect the color information is written to the Emissions channel instead of the Albedo channel. Albedo is later subjected to lightning and will appear as black in the absence if light. The Emission channel makes the color always appear. As this is supposed to look like lights, that's a better option.

void surf (Input IN, inout SurfaceOutput o)
{
   float border = pow(abs(dot(IN.worldNormal, IN.viewDir)), _Intensity);

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

   fixed alpha = tex2D(_Gradient, IN.uv_Gradient);
   o.Emission = c.rgb * border;
   o.Alpha = c.a * alpha.r * border;
}


float border = pow(abs(dot(IN.worldNormal, IN.viewDir)), _Intensity)
; calculates the dot product between the camera direction and the direction of the polygon and uses it for the alpha blending.
_Intensity
is used to control this effect.


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

Here the main texture is sampled twice and first offset by a timer. _Time.x is a built in variable in unity and is used here to make a scrolling effect. The second sample is offset again by a set value. By sampling a texture twice at different point and multiply the result it can create an effect that looks moving.

A problem with multiplying two value both in the range 0-1 has the issue of resulting in very small values making the effect barely visible. To mend this the

_ColorAddition
is used to add some color back to the effect.


fixed alpha = tex2D(_Gradient, IN.uv_Gradient);
The alpha guide gradient is also sampled, as a normal texture and not offset by anything.

This is then used to determine the final alpha output.


The complete shader

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

       LOD 200
      Cull Back

       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 _ColorAddition;
      fixed _Intensity;
      fixed _ScrollSpeed;

       void surf (Input IN, inout SurfaceOutput o)
       {
         float border = pow(abs(dot(IN.worldNormal, IN.viewDir)), _Intensity);

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

         fixed alpha = tex2D(_Gradient, IN.uv_Gradient);
         o.Emission = c.rgb * border;
         o.Alpha = c.a * alpha.r * border;
       }
       ENDCG
   }
   FallBack "Diffuse"
}