Shader-driven Flame Effect

I saw a cool gif of some animated flames the other day and wondered if I’d be able to recreate something similar in Unity3D. I’ve been using particle effects for almost everything up until now because I’m terrible at writing shaders but I thought I’d see what I could do. So here’s my less than perfect shader which you can copy and tweak as you desire.

flameshaderexample

I’ll try my best to explain what I’m doing in the shader so you’ll know the best places to start tweaking things.

First things first, my base texture was literally created by going to photoshop, selecting Filter->Render->Difference Clouds. I just needed something quick to test with and this ended up being perfect.

difference-clouds

You could try out some different texture and see how it affects the effect. For this example I think you’ll want to make sure you select the Alpha from Grayscale and Transparency from Alpha options in the texture import settings.

screen-shot-2016-10-19-at-10-09-51-am

Now for the shader code. Remember, I don’t really know what I’m doing here. But it works.

Shader GhostTime/FlameCutoffShader
{
     Properties {
     _Color (Main ColorColor) = (1,1,1,1)
     _MainTex (Base (RGB) Trans (A)2D) = white {}
     _SecTex (Second (RGB) Trans (A)2D) = white {}
     _Scale (ScaleFloat) = 1
     }
 
     SubShader {
     Tags {Queue=Transparent IgnoreProjector=True RenderType=Transparent DisableBatching = True }
     LOD 200
     Lighting off
     cull off
 
     CGPROGRAM
     #pragma surface surf NoLighting noforwardadd alpha
     
     sampler2D _MainTex;
     sampler2D _SecTex;
     fixed4 _Color;
     float _Scale;
     
     struct Input {
         float2 uv_MainTex;
         float3 worldPos;
     };

     fixed4 LightingNoLighting(SurfaceOutput s, fixed3 lightDir, fixed atten)
     {
         fixed4 c;
         c.rgb = s.Albedo; 
         c.a = s.Alpha;
         return c;
     }
     
     void surf (Input IN, inout SurfaceOutput o) {
        float time = _Time;

        fixed4 c = tex2D(_MainTex, IN.uv_MainTex + float2(0, time.r * 10));
        float cRight = tex2D(_SecTex, IN.uv_MainTex + float2(time.r * 0.10)).a;
        float cLeft = tex2D(_SecTex, IN.uv_MainTex + float2(time.r * –0.10)).a;

        float mixedAlpha = (c.a + 0.1) * (cRight + 0.5) * (cLeft + 0.5);

        float3 worldPos = mul(_Object2Worldfloat4(0,0,0,1)).xyz;

        float wDist = distance(worldPos, IN.worldPos);

        float distCut = _Scale/wDist;
         
        if (mixedAlpha < distCut)
        {
            o.Alpha = 1.0;
            o.Albedo = _Color + distCut;
        }
        else if (mixedAlpha – 0.01 < distCut)
           {
               o.Alpha = 1.0;
               o.Albedo = float3(000);
        }
        else
            o.Alpha = 0;
          

           
     }
     ENDCG
     }
 
     Fallback Transparent/VertexLit
 }

Firstly, you’ll notice there’s two texture slots in the properties – I used the same texture for both, but you could change them up if you wanted to experiment with the effect. The first texture is the main flames source. The _Scale property is important for adjusting to change the reach of the flames. You’ll need to play with this and adjust it based on the size of the mesh you’re using. Unfortunately I couldn’t find a good way to automatically get the scale/bounds of the mesh from inside of the shader, so you’ll probably need to set this property from script.

So in the surf function we get the time from _Time – this is essential for animating the flame over time. Below that is where we sample the textures using offsets adjusted my the time.

fixed4 c = tex2D(_MainTex, IN.uv_MainTex + float2(0, time.r * 10));
float cRight = tex2D(_SecTex, IN.uv_MainTex + float2(time.r * 0.1, 0)).a;
float cLeft = tex2D(_SecTex, IN.uv_MainTex + float2(time.r * -0.1, 0)).a;

So the main texture sampler is being offset in the Y axis by 10 times the normal time rate. The cRight and cLeft values are being adjusted in the x direction at a much slower rate, and I only need their alpha. It’s worth noting that these time multipliers are entirely arbitrary and you may want to include them as customizable properties in the head of the shader.

Now I multiply the alpha of all three of these samples to create a combined shifting alpha. I add an arbitrary amount to each one to prevent them from being decreased too significantly.
float mixedAlpha = (c.a + 0.1) * (cRight + 0.5) * (cLeft + 0.5);
The left and right samples help to keep the flames a little more random and flicker, instead of just scrolling a single texture straight up the mesh.

Now I get the distance of each point from the origin of the object, because we want the flames to start dithering as they travel farther from the source.
float3 worldPos = mul(_Object2World, float4(0,0,0,1)).xyz;
float wDist = distance(worldPos, IN.worldPos);

Now that we have the distance we use it with our _Scale property to create the cutoff value. If you’re not seeing any flames on your mesh you’ll need to adjust the _Scale property in the material properties.
float distCut = _Scale/wDist;

Now we do our check against the cutoff value.
if (mixedAlpha < distCut) { o.Alpha = 1.0; o.Albedo = _Color + distCut; } else if (mixedAlpha - 0.01 < distCut) { o.Alpha = 1.0; o.Albedo = float3(0, 0, 0); } else o.Alpha = 0; So first thing to point out here - if we are setting our alpha to 1 then we also slightly modify the colour by the distance value - this creates the effect of the flame becoming darker the further away it gets from the origin. It's a subtle gradient but you could also consider doing a lerp to a secondary colour here, etc. Next do an else if check where we modify the original alpha value by -0.01 (another arbitrary value). This section will determine the edge of the flame, which I've coloured black. It's a thin line but you could replace the -0.01 with a customizable property to adjust the width of the edges. Same goes for the edge colour. And that's pretty much it. If you have any questions I'll see what I can do, but I'm also open to any suggestions for making this shader better. I'm sure it has lots of room for improvement but hopefully this is a helpful starting point for you.