My take on shaders: VFX Master Shader (Part III)

Patrons

This shader tutorial post was brought to you by:

Erich Binder

Introduction

Hello and welcome to the third and possibly final (major) part of the VFX Master Shader tutorial! In the first and second part we covered the majority of the most common effects of the master shader, but we focused on the opaque version of the master shader.

Specifically, the total features we discussed in the first two parts are:

  • Using a grayscale noise texture as the main controlling texture
  • Applying color via a gradient map (for which you can see more in the post on gradient mapping)
  • Adding contrast and a power factor to modify the levels of the input texture
  • Clipping the object’s pixels based on a cutoff value, while also using a property to color the edges around the dissolved areas for a burning effect
  • Vertex offset based on the noise texture
  • UV displacement for more variation
  • Panning of the main texture and the displacement texture based on different values per-axis
  • Variable culling mode, in order to change whether the back/front faces are showing or hiding
  • Secondary grayscale noise texture with separate panning speeds for a lot of extra effect variety
  • Support for vertex colors, where we’ll use the alpha channel to also control the cutoff value for the clipping
  • Support for color banding / posterization for more stylized effects
  • Support for polar coordinates
  • Support for a circular/ring-shaped mask

In this part, we’ll go through the transparent version of the shader, adjusting some existing features and adding a couple of new ones. In more detail, the features we’ll cover here are:

  • Soft cutoff, as we won’t be using clipping like in the opaque version but we’ll take advantage of the actual alpha channel of our output (scandalous!). There will also be some adjustments to the “burning” effect, as we don’t have a hard cutoff now.
  • Soft blending, so that objects with a material using the shader softly intersect with opaque objects. It’s the same technique used in Unity’s own soft particles.
  • A second mask of rectangular shape, for more masking options.

With this shader you’ll be able to achieve a variety of effects. Most of my latest VFX studies rely solely on the transparent VFX master shader. Some examples using the shader are these:

Code

Here’s the code of the transparent VFX master shader:

Shader "VFX/VFXMasterShaderTransparent"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _GradientMap("Gradient map", 2D) = "white" {} 
        [HDR]_Color("Color", Color) = (1,1,1,1)

        //Secondary texture
        [Space(20)]
        [Toggle(SECONDARY_TEX)]
        _SecondTex("Second texture", float) = 0
        _SecondaryTex("Secondary texture", 2D) = "white" {}
        _SecondaryPanningSpeed("Secondary panning speed", Vector) = (0,0,0,0)
        
        _PanningSpeed("Panning speed (XY main texture - ZW displacement texture)", Vector) = (0,0,0,0)
        _Contrast("Contrast", float) = 1
        _Power("Power", float) = 1

        //Clipping
        [Space(20)]
        _Cutoff("Cutoff", Range(0, 1)) = 0
        _CutoffSoftness("Cutoff softness", Range(0, 1)) = 0
        [HDR]_BurnCol("Burn color", Color) = (1,1,1,1)
        _BurnSize("Burn size", float) = 0

        //Softness
        [Space(20)]
        [Toggle(SOFT_BLEND)]
        _SoftBlend("Soft blending", float) = 0
        _IntersectionThresholdMax("Intersection Threshold Max", float) = 1
        
        //Vertex offset
        [Space(20)]
        [Toggle(VERTEX_OFFSET)]
        _VertexOffset("Vertex offset", float) = 0
        _VertexOffsetAmount("Vertex offset amount", float) = 0

        //Displacement
        [Space(20)]
        _DisplacementAmount("Displacement", float) = 0
        _DisplacementGuide("DisplacementGuide", 2D) = "white" {}
        
        //Culling
        [Space(20)]
        [Enum(UnityEngine.Rendering.CullMode)] _Culling ("Cull Mode", Int) = 2

        //Banding
        [Space(20)]
        [Toggle(BANDING)]
		_Banding("Color banding", float) = 0
        _Bands("Number of bands", float) = 3

        //Polar coordinates
        [Space(20)]
        [Toggle(POLAR)]
        _PolarCoords("Polar coordinates", float) = 0

        //Circle mask
        [Space(20)]
        [Toggle(CIRCLE_MASK)]
        _CircleMask("Circle mask", float) = 0
        _OuterRadius("Outer radius", Range(0,1)) = 0.5
        _InnerRadius("Inner radius", Range(-1,1)) = 0
        _Smoothness("Smoothness", Range(0,1)) = 0.2

        //Rect mask
        [Space(20)]
        [Toggle(RECT_MASK)]
        _RectMask("Rectangle mask", float) = 0
        _RectWidth("Rectangle width", float) = 0
        _RectHeight("Rectangle height", float) = 0
        _RectMaskCutoff("Rectangle mask cutoff", Range(0,1)) = 0
        _RectSmoothness("Rectangle mask smoothness", Range(0,1)) = 0        
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent"}
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off
        Offset -1, -1
        Cull [_Culling]
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature SECONDARY_TEX
            #pragma shader_feature VERTEX_OFFSET
            #pragma shader_feature SOFT_BLEND
            #pragma shader_feature BANDING
            #pragma shader_feature POLAR
            #pragma shader_feature CIRCLE_MASK
            #pragma shader_feature RECT_MASK
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                fixed4 color : COLOR;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float2 displUV : TEXCOORD2;
                float2 secondaryUV : TEXCOORD3;
                float4 scrPos : TEXCOORD4;
                float4 vertex : SV_POSITION;
                fixed4 color : COLOR;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _SecondaryTex;
            float4 _SecondaryTex_ST;
            sampler2D _GradientMap;
            float _Contrast;
            float _Power;

            fixed4 _Color;

            float _Bands;

            float4 _PanningSpeed;
            float4 _SecondaryPanningSpeed;
            
            float _Cutoff;
            float _CutoffSoftness;
            fixed4 _BurnCol;
            float _BurnSize;

            sampler2D _CameraDepthTexture;
            float _IntersectionThresholdMax;

            float _VertexOffsetAmount;

            sampler2D _DisplacementGuide;
            float4 _DisplacementGuide_ST;
            float _DisplacementAmount;

            float _Smoothness;
            float _OuterRadius;
            float _InnerRadius;

            float _RectSmoothness;
            float _RectHeight;
            float _RectWidth;
            float _RectMaskCutoff;

            v2f vert (appdata v)
            {
                v2f o;
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.secondaryUV = TRANSFORM_TEX(v.uv, _SecondaryTex);

                #ifdef VERTEX_OFFSET
                float vertOffset = tex2Dlod(_MainTex, float4(o.uv + _Time.y * _PanningSpeed.xy, 1, 1)).x;
                #ifdef SECONDARY_TEX
                float secondTex = tex2Dlod(_SecondaryTex, float4(o.secondaryUV + _Time.y * _SecondaryPanningSpeed.xy, 1, 1)).x;
                vertOffset = vertOffset * secondTex * 2;
                #endif
                vertOffset = ((vertOffset * 2) - 1) * _VertexOffsetAmount;
                v.vertex.xyz += vertOffset * v.normal;
                #endif

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.displUV = TRANSFORM_TEX(v.uv, _DisplacementGuide);
                o.scrPos = ComputeScreenPos(o.vertex);
                o.color = v.color;
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {

                // sample the texture
                float2 uv = i.uv;
                float2 displUV = i.displUV;
                float2 secondaryUV = i.secondaryUV;

                //Polar coords
                #ifdef POLAR
                float2 mappedUV = (i.uv * 2) - 1;
				uv = float2(atan2(mappedUV.y, mappedUV.x) / UNITY_PI / 2.0 + 0.5, length(mappedUV));
                mappedUV = (i.displUV * 2) - 1;
                displUV = float2(atan2(mappedUV.y, mappedUV.x) / UNITY_PI / 2.0 + 0.5, length(mappedUV));
                mappedUV = (i.secondaryUV * 2) - 1;
                secondaryUV = float2(atan2(mappedUV.y, mappedUV.x) / UNITY_PI / 2.0 + 0.5, length(mappedUV));
                #endif

                //UV Panning
                uv += _Time.y * _PanningSpeed.xy;
                displUV += _Time.y * _PanningSpeed.zw;
                secondaryUV += _Time.y * _SecondaryPanningSpeed.xy;

                //Displacement
                float2 displ = tex2D(_DisplacementGuide, displUV).xy;
                displ = ((displ * 2) - 1) * _DisplacementAmount;

                float col = pow(saturate(lerp(0.5, tex2D(_MainTex, uv + displ).x, _Contrast)), _Power);
                #ifdef SECONDARY_TEX
                col = col * pow(saturate(lerp(0.5, tex2D(_SecondaryTex, secondaryUV + displ).x, _Contrast)), _Power) * 2;
                #endif

                //Masking
                #ifdef CIRCLE_MASK
                float circle = distance(i.uv, float2(0.5, 0.5));
                col *= 1 - smoothstep(_OuterRadius, _OuterRadius + _Smoothness, circle);
                col *= smoothstep(_InnerRadius, _InnerRadius + _Smoothness, circle);
                #endif

                #ifdef RECT_MASK
                float2 uvMapped = (i.uv * 2) - 1;
                float rect = max(abs(uvMapped.x / _RectWidth), abs(uvMapped.y / _RectHeight));
                col *= 1 - smoothstep(_RectMaskCutoff, _RectMaskCutoff + _RectSmoothness, rect);
                #endif
            

                float orCol = col;

                //Banding
                #ifdef BANDING
				col = round(col * _Bands) / _Bands;
				#endif

                //Transparency
                float cutoff = saturate(_Cutoff + (1 - i.color.a));
                float alpha = smoothstep(cutoff, cutoff + _CutoffSoftness, orCol);

                //Coloring
                fixed4 rampCol = tex2D(_GradientMap, float2(col, 0)) + _BurnCol * smoothstep(orCol - cutoff, orCol - cutoff + _CutoffSoftness, _BurnSize) * smoothstep(0.001, 0.5, cutoff);
                fixed4 finalCol = fixed4(rampCol.rgb * _Color.rgb * rampCol.a, 1);
                
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, finalCol);
                finalCol.a = alpha * tex2D(_MainTex, uv + displ).a * _Color.a;

                //Soft Blending
                #ifdef SOFT_BLEND
                float depth = LinearEyeDepth (tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos)));
                float diff = saturate(_IntersectionThresholdMax * (depth - i.scrPos.w));
                finalCol.a *= diff;
                #endif
                return finalCol;
            }
            ENDCG
        }
    }
}

As with the second part, I won’t go through the majority of the shader, because most of the features were covered in the previous parts. Therefore, I’ll casually ignore all the features I’ve mentioned before and I’ll just focus on the new/changed stuff.

Properties

Cutoff

The first property I add to the already pretty hefty amount of properties, is in line 23. There I add a softness value that determines how soft the cutoff will be. If you leave it at 0, you basically have the same cutoff effect as the opaque version.

Softness

In lines 28-31 I add a couple of properties associated with the soft blending. The first one is the toggle for the “SOFT_BLEND” keyword, while the “_IntersectionThresholdMax” is a property defining the effect an intersection will have on the object. The closer to 0 it is, the more transparent the object will be around the intersection area.

Rectangle mask

Finally, in lines 68-74 I have the properties concerning the rectangular mask. “_RectMask” is the toggle for the “RECT_MASK” keyword, then I have 2 properties for the width and height of the mask respectively, as well as the cutoff amount and the smoothness of the mask.

The in-between stuff

Since this is a transparent shader, first of all I have to declare that in the subshader. Therefore, in lines 78-80 I add the necessary tags to declare the shader as transparent. You’ve definitely seen these before, either here or in a Unity tips tweet that I can’t find at the moment.

Since I added two new shader features, in lines 92 and 96 I add the shader feature keywords for soft blending and rectangular mask respectively. You’d have to look at the first part if you forgot what these are and what they do.

In line 116 I add a new field to the v2f struct, which will hold the screen space position of our pixels. We’re going to need this for the soft blending later.

Vertex shader

There’s a really really small change in the vertex shader compared to the opaque version: Since we need the screen position of our pixels, we need to pass that information to the fragment shader. This is why in line 177 I use the “ComputeScreenPos” method, provided by Unity, to store that in the “scrPos” field of the v2f struct.

Fragment shader

Rectangular mask

First up, in lines 222-226, I add t he functionality for the rectangular mask. If the keyword “RECT_MASK” has been enabled, I first map the unmodified UV coordinates to the [-1,1] range, and then, in line 224, I use the SDF function for the rectangle:

max(abs(UV.x / width), abs(UV.y / height))

After I have the SDF, which will be in the form of a rectangular gradient, I can use “step” or, in this case, “smoothstep” to adjust the size of the mask by getting larger “slices”. If you are not too familiar with SDFs, I suggest checking out this post.

Since the rectangle from the SDF will be darker in the center, I have to flip it and then apply a smoothstep function to adjust the size and smoothness of the mask.

Transparency

Now we’re not using the “clip” method, so the transparency has to be handled in a different way in relation to the opaque version.

While the cutoff variable in line 237 stays the same, I use a new field called “alpha” to define the transparent areas of the object, which is determined by a smoothstep function that uses the “cutoff” variable as well as the “_CutoffSoftness” property to determine its value based on the grayscale value of the “orCol” field.

Since we’re not doing a hard cutoff now, the “burning” effect in line 241 will also need to be adjusted. Instead of multiplying the “_BurnCol” property by

 step(test, _BurnSize) 

I now multiply it by

 smoothstep(orCol - cutoff, orCol - cutoff + _CutoffSoftness, _BurnSize) 

The glowing edges won’t look as good as with the opaque version, due to the hard clipping, but the effect adds some nice color variation even in transparent mode.

The “alpha” field is then used as the alpha channel for the final color of our object and it’s multiplied by the alpha channel of the main texture, as well as the alpha channel of the main color, int order to have more control.

Soft blending

Finally, the last thing I added is the support for soft blending in lines 249-253. This is actually something I have covered twice before: in the vertical fog shader and in the water of the waterfall shader. It’s a simple depth comparison to see if our object intersects with another object. The amount of blending is determined by the “_IntersectionThresholdMax” value.

The alpha channel of the final color is then multiplied by the depth difference. Of course, all that is encapsulated into an “#ifdef” statement, to check if the “SOFT_BLEND” keyword has been enabled.

Conclusion

The main version of the VFX master shader is now complete. As I said in previous parts, you could make some parts of the shader in different ways, add more or less shader features, expose more properties etc etc. But I hope you get the gist of the useful features these shaders provide as well as the high concept of a master shader with different shader features etc. At some point I’ll probably look into custom material editors to make the usability a bit better and add a bonus part on the tutorial to show it.

I hope you enjoyed this tutorial and I hope you’ll have as much fun with this shader as I am. Can’t wait to see the awesome stuff you make with it.

See you in the next one!




Disclaimer

The code in the shader tutorials is under the CC0 license, so you can freely use it in your commercial or non-commercial projects without the need for any attribution/credits.

These posts will never go behind a paywall. But if you really really super enjoyed this post, consider buying me a coffee, or becoming my patron to get exciting benefits!

Become a Patron!

Or don't, I'm not the boss of you.

Comments

  1. Fran

    This is a masterpiece, I keep recommending your blog to everyone interested in shaders.
    Thank you for all your tutorials.

    1. Post
      Author

Comments are closed.