VFX | Let’s Make a Flamethrower!

Introduction

Today I decided I wanted to make a flamethrower, because why not? I’ll be creating this effect to look as real as possible inside of Unity. I’ll then give anyone reading all the code/textures to implement this themselves.

Warning: This will not be a full tutorial, anyone that wishes to learn more about how to create the flamethrower from scratch can either contact me or needs to have moderate knowledge on particle creation to follow along.

Particle Shape

Flamethrowers have a cone-like shape that emits from a central point and expands in all directions, like a funnel. If you create a particle effect and mess with the “Shape” settings a little bit you should get something like this.

ParticleShape.gif

Material & Animation

Once you get the shape down to where you like it we’ll need it to actually look like fire. I find an additive shader makes for a nice blend, plus realistic fire helps a lot!

Create a Material inside your project and add this shader with this texture to it! I’ve also added some properties that you can tweak to your liking.

Shader "Effects/ParticleAdd" {
Properties {
	_TintColor ("Tint Color", Color) = (0.5,0.5,0.5,0.5)
	_MainTex ("Particle Texture", 2D) = "white" {}
	_ColorStrength ("Color strength", Float) = 1.0
	_InvFade ("Soft Particles Factor", Range(0.01,3.0)) = 1.0
}

Category {
	Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
	Blend SrcAlpha One
	Cull Off 
	Lighting Off 
	ZWrite Off 
	Fog { Mode Off}
	
	SubShader {
		Pass {
		
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_particles

			#include "UnityCG.cginc"

			sampler2D _MainTex;
			fixed4 _TintColor;
			fixed _ColorStrength;
			
			struct appdata_t {
				float4 vertex : POSITION;
				fixed4 color : COLOR;
				float2 texcoord : TEXCOORD0;
			};

			struct v2f {
				float4 vertex : POSITION;
				fixed4 color : COLOR;
				float2 texcoord : TEXCOORD0;
				#ifdef SOFTPARTICLES_ON
				float4 projPos : TEXCOORD1;
				#endif
			};
			
			float4 _MainTex_ST;

			v2f vert (appdata_t v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				#ifdef SOFTPARTICLES_ON
				o.projPos = ComputeScreenPos (o.vertex);
				COMPUTE_EYEDEPTH(o.projPos.z);
				#endif
				o.color = v.color;
				o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
				return o;
			}

			sampler2D _CameraDepthTexture;
			float _InvFade;
			
			fixed4 frag (v2f i) : COLOR
			{
				#ifdef SOFTPARTICLES_ON
				float sceneZ = LinearEyeDepth (UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos))));
				float partZ = i.projPos.z;
				float fade = saturate (_InvFade * (sceneZ-partZ));
				i.color.a *= fade;
				#endif
				
				return 2.0f * i.color * _TintColor * tex2D(_MainTex, i.texcoord) * _ColorStrength;
			}
			ENDCG 
		}
	}	
}
}

Fire texture sheet 8×8

Fire.png

Applying The Heat

Now that you have your material created go ahead and add it to the “Material” slot under “Renderer”.

Whoa! wait that doesn’t look good at all what gives?

You’re right, you see the texture above is a sprite sheet, in other words, it’s a series of images or collection of “frames” that make up an animated image, in our case fire, we have a total of 64 frames (8×8).

Let’s turn on the “Texture Sheet Animation” toggle on our particle system. Then in the Tiles property place X: 8 and Y: 8. Looks much better right?

You should now have something similar to this!

MaterialApplication.gif

Adding The Core

Now our particle is really starting to look like fire, it feels a bit too cold to be fire, though. You see fire usually has an extremely hot core, with the outside flickering out.

Let’s go ahead and duplicate our fire particle, change the radius of the shape to 0 create a new material, this time we’ll use an alpha blended shader, this will add the bright look to our core.

Here’s the shader:

Shader "Effects/AlphaBlended" {
	Properties {
	_TintColor ("Tint Color", Color) = (0.5,0.5,0.5,1)
	_ColorStrength ("Color strength", Float) = 1.0
	_MainTex ("Particle Texture", 2D) = "white" {}
	_InvFade ("Soft Particles Factor", Range(0.01,7.0)) = 1.0
}

Category {
	Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
				Blend SrcAlpha OneMinusSrcAlpha
				Cull Off 
				Lighting Off 
				ZWrite Off

	SubShader {
		Pass {
				
			
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_particles
			
			#include "UnityCG.cginc"

			sampler2D _MainTex;
			fixed4 _TintColor;
			fixed _ColorStrength;
			
			struct appdata_t {
				float4 vertex : POSITION;
				fixed4 color : COLOR;
				float2 texcoord : TEXCOORD0;
			};

			struct v2f {
				float4 vertex : POSITION;
				fixed4 color : COLOR;
				float2 texcoord : TEXCOORD0;
				#ifdef SOFTPARTICLES_ON
				float4 projPos : TEXCOORD1;
				#endif
			};
			
			float4 _MainTex_ST;

			v2f vert (appdata_t v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				#ifdef SOFTPARTICLES_ON
				o.projPos = ComputeScreenPos (o.vertex);
				COMPUTE_EYEDEPTH(o.projPos.z);
				#endif
				o.color = v.color;
				o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
				return o;
			}

			sampler2D _CameraDepthTexture;
			float _InvFade;
			
			fixed4 frag (v2f i) : COLOR
			{
				#ifdef SOFTPARTICLES_ON
				float sceneZ = LinearEyeDepth (UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos))));
				float partZ = i.projPos.z;
				float fade = saturate (_InvFade * (sceneZ-partZ));
				i.color.a *= fade;
				#endif

				fixed4 tex = tex2D(_MainTex, i.texcoord);
				fixed4 res = 2 * i.color * tex * _TintColor * _ColorStrength;
				res.a = saturate(res.a);
				return res;
			}
			ENDCG 
		}
	}	
}

}

You should get something like this 🙂

CoreAdded.gif

Ah, much more full! This is starting to really come together. But what’s fire without smoke right? Let’s go ahead and add that next!

Time To Smoke

The best thing about the smoke particles is that they’re basically an exact copy of the core. The only difference is the Texture sheet you’ll be using! Go ahead and duplicate the core and create a new Material and add this texture to it and change the “Texture Sheet Animation” Tiles property to X:12 Y:7:

Smoke2.png

You should end up with something like this 🙂

SmokeParticles.gif

Nice and smoky right? This is definitely looking like real fire now. We just need to add a couple more finishing touches before we call it a day.

Finishing Touches: Distortion

Let’s Bill Nye real quick and talk about why we’re putting the final touches on this fire. Fire creates heat, heat rises. Now, heat causes the particles in the air to move faster, called particle acceleration. The acceleration causes things to get bigger or expand, which is why cakes rise in the oven, water can boil over, this expansion decreases density.

Now, onto my point! When you look at something, you’re actually just seeing reflected light from that object. When you look through a flamethrower and see the wall behind it, you’re seeing the light that was reflected off the wall, that light then traveled through the cold air behind the flamethrower through the hot air produced by the flamethrower back through cold air to hit your eyes, so when the light travels through different densities it gets refracted, exactly like looking at something through water.

Okay enough of that, let’s make things distorted!

The way I went about creating this shader wasn’t something trivial and took a lot of time to implement. Not to get too complex and wordy, I’m essential using the normal map, and the main texture to distort what’s behind it, this is done using the depth buffer. I have the shader calculate the distance from the depth texture to the camera this allows me to grab a position and then warp what’s rendered to the screen using a normal map.

Your distortion particle can be a duplicate of any particles you just created. Next, we should change the speed to be much slower than our fire particle, and change the radius to be a little bit bigger as this should just be the heat coming off of the fire.

Next, we create a Material and add our distortion shader to it along with another texture sheet with our Texture Sheet Animation property set to X:8 Y:4. (This feels extremely repetitive right now).

Here’s the shader

Shader "Effects/Distortion/ParticlesCutOut" {
Properties {
        _TintColor ("Tint Color", Color) = (1,1,1,1)
		_MainTex ("Base (RGB) Gloss (A)", 2D) = "black" {}
		_CutOut ("CutOut (A)", 2D) = "black" {}
        _BumpMap ("Normalmap", 2D) = "bump" {}
		_ColorStrength ("Color Strength", Float) = 1
		_BumpAmt ("Distortion", Float) = 10
		_InvFade ("Soft Particles Factor", Float) = 1.0
}

Category {

	Tags { "Queue"="Transparent"  "IgnoreProjector"="True"  "RenderType"="Transparent" }
	Blend SrcAlpha OneMinusSrcAlpha
	Cull Off 
	ZWrite Off 
	Fog { Mode Off}

	SubShader {
		GrabPass {							
			Name "_GrabTexture"
 		}
		Pass {
			Name "BASE"
			Tags { "LightMode" = "Always" }
			
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#pragma multi_compile_particles
#include "UnityCG.cginc"

struct appdata_t {
	float4 vertex : POSITION;
	float2 texcoord: TEXCOORD0;
	fixed4 color : COLOR;
};

struct v2f {
	float4 vertex : POSITION;
	float4 uvgrab : TEXCOORD0;
	float2 uvbump : TEXCOORD1;
	float2 uvmain : TEXCOORD2;
	float2 uvcutout : TEXCOORD3;
	fixed4 color : COLOR;
	#ifdef SOFTPARTICLES_ON
		float4 projPos : TEXCOORD4;
	#endif
};

sampler2D _MainTex;
sampler2D _CutOut;
sampler2D _BumpMap;

float _BumpAmt;
float _ColorStrength;
sampler2D _GrabTexture;
float4 _GrabTexture_TexelSize;
fixed4 _TintColor;

float4 _BumpMap_ST;
float4 _MainTex_ST;
float4 _CutOut_ST;

v2f vert (appdata_t v)
{
	v2f o;
	o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
	#ifdef SOFTPARTICLES_ON
		o.projPos = ComputeScreenPos (o.vertex);
		COMPUTE_EYEDEPTH(o.projPos.z);
	#endif
	o.color = v.color;
	#if UNITY_UV_STARTS_AT_TOP
	float scale = -1.0;
	#else
	float scale = 1.0;
	#endif
	o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
	o.uvgrab.zw = o.vertex.zw;
	o.uvbump = TRANSFORM_TEX( v.texcoord, _BumpMap );
	o.uvmain = TRANSFORM_TEX( v.texcoord, _MainTex );
	o.uvcutout = TRANSFORM_TEX( v.texcoord, _CutOut );
	
	return o;
}

sampler2D _CameraDepthTexture;
float _InvFade;

half4 frag( v2f i ) : COLOR
{
	#ifdef SOFTPARTICLES_ON
		if(_InvFade > 0.0001)	{
		float sceneZ = LinearEyeDepth (UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos))));
		float partZ = i.projPos.z;
		float fade = saturate (_InvFade * (sceneZ-partZ));
		i.color.a *= fade*fade;
	}
	#endif

	half2 bump = UnpackNormal(tex2D( _BumpMap, i.uvbump )).rg;
	float2 offset = bump * _BumpAmt * _GrabTexture_TexelSize.xy * i.color.a;
	i.uvgrab.xy = offset * i.uvgrab.z + i.uvgrab.xy;
	
	half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
	half4 tex = tex2D(_MainTex, i.uvmain);
	half cut = tex2D(_CutOut, i.uvcutout).a;
	half4 emission = col * i.color + tex * _ColorStrength * _TintColor * i.color * i.color.a;
    emission.a = _TintColor.a * cut;
	return emission;
}
ENDCG
		}
	}

	SubShader {
		Blend DstColor Zero
		Pass {
			Name "BASE"
			SetTexture [_MainTex] {	combine texture }
		}
	}
}

}

Main Texture

Aura2.png

Cutout

SmokeTileCutOut.png

Normal Map

Aura2_Normal.png

You’re all done! Look at your pretty new flamethrower! Now go burn some people! 🙂

Final Result

Flamethrower_Final.gif

Happy coding! 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s