in Maths, Shaders, Tutorial, Unity

How to Simulate Smoke with Shaders

This post will show how to simulate the diffusion of smoke using shaders. This part of the tutorial focuses on the Maths and the code necessary to recreate the smoke effect. To learn how to set up your project, check out the first part: How to Use Shaders For Simulations.

texture6

Introduction

Creating realistic smoke in games has always been a challenge. The reason behind this is the fact that the large scale behaviour of smoke is determined by the complex interactions of billions of tiny particles, floating in air. Throughout the history of game development there have been many attempts to simulate smoke, mostly based either on particles or trails (animation below).smoke1Neither of these, however, are able to reproduce with fidelity the actual behaviours of a fluid when exposed to perturbations. To compensate for that, we need to simulate fluid dynamics. Like it happens with physics, you don’t actually need to simulate every aspects of fluid dynamics; you want something that looks good, but that is not too computational intensive.

Part 1. The Maths

There are two main approaches to fluid dynamics: Lagrangian and Eulerian. While the former uses virtual particles to simulate the moving particles in a fluid, the latter divides the space into a grid. For this tutorial, we will focus on a simple grid-based smoke simulation. The value of each cell x,y in the grid indicates the flow F_{x,y} (or “amount of smoke”) contained; you can imagine it as a way of approximating its “density” in a given space. Simulating how the smoke behaves is now a matter of understanding how the density diffuses between adjacent cells.

Another assumption we introduce is that a cell can exchange flow only with its four immediate neighbours. This given a good approximation and reduces the number of texture lookups we need to perform for each pixel to five.

There are two components that determines how diffusion works. The first one is the incoming flow F^{in} from the four neighbour cells:

    \[F^{in}_{x,y}= \frac{ F^{out}_{x+1,y} + F^{out}_{x,y+1} + F^{out}_{x-1,y} + F^{out}_{x,y-1} }{4}\]

The outgoing flow of each cell is divided equally to its four neighbours, hence the \frac{1}{4} coefficient.

The second component is the the outgoing flow F^{out}. Not all the flow is transmitted at once, so we’ll use the diffusion coefficient f to indicate the rate at which the amount of smoke in a cell is diffused to its four neighbours:

    \[F^{out}_{x,y}= f \cdot F_{x,y}\]

To sum it up, the net balance of F_{x,y} after a diffuse iteration is:

    \[F^{in+out}_{x,y}= f\cdot\left( \frac{F_{x+1,y} + F_{x,y+1} + F_{x-1,y} + F_{x,y-1} }{4} + F_{x,y} \right)\]

Part 2. The Shader

The post How to Use Shaders for Simulations shows how a fragment shader can be used to iterate over a texture. We will use the same technique, encoding the flow (or amount of smoke) into the alpha channel of the main texture _MainTex. This allows to easily integrate a smoke effect into your game by simply overlaying a textured quad.

Grid representation

The first problem we encounter is the fact that we are using a grid-based approach, but this is a concept that is not present in a shader. Yes, images are indeed made out of pixels, but this is a concept that is not that easily accessible in a fragment shader. If we are using a standard Unity quad for this experiment, the pixels drawn by the shader are addressed by the quad UV. By knowing the size of the render texture we are using, we can use the value to find out which pixels we’re currently drawing. As a general approach, we will force the UV values of the fragment shader to assume values at fixed intervals, simulating a grid:

fixed2 uv = round(i.uv * _Pixels) / _Pixels;
half s = 1 / _Pixels;

The variable s indicates the distance between two cells. Now we can sample a texture like we can do with a grid:

// Neighbour cells
float cl = tex2D(_MainTex, uv + fixed2(-s,  0)).a;	// F[x-1, y  ]: Centre Left
float tc = tex2D(_MainTex, uv + fixed2( 0, -s)).a;	// F[x,   y-1]: Top Centre
float cc = tex2D(_MainTex, uv + fixed2( 0,  0)).a;	// F[x,   y  ]: Centre Centre
float bc = tex2D(_MainTex, uv + fixed2( 0, +s)).a;	// F[x,   y+1]: Bottom Centre
float cr = tex2D(_MainTex, uv + fixed2(+s,  0)).a;	// F[x+1, y  ]: Centre Right

If you prefer, you can use a more intuitive grid-like notation:

#define ARRAY(T,X,Y) (tex2D((T), uv + fixed2(s*(X), s*(Y))))
float cc = ARRAY(_MainTex, 0,0).a; // F[x+0, y+0]: Centre Centre

The last step is to implement the diffusion step:

float4 frag (v2f_img i) : COLOR
{
	// Cell centre
	fixed2 uv = round(i.uv * _Pixels) / _Pixels;

	// Neighbour cells
	half s = 1 / _Pixels;
	float cl = tex2D(_MainTex, uv + fixed2(-s, 0)).a;	// Centre Left
	float tc = tex2D(_MainTex, uv + fixed2(-0, -s)).a;	// Top Centre
	float cc = tex2D(_MainTex, uv + fixed2(0, 0)).a;	// Centre Centre
	float bc = tex2D(_MainTex, uv + fixed2(0, +s)).a;	// Bottom Centre
	float cr = tex2D(_MainTex, uv + fixed2(+s, 0)).a;	// Centre Right

	// Diffusion step
	float factor = 
		_Dissipation *
		(
			0.25 * (cl + tc + bc + cr)
			- cc
		);
	cc += factor;

	return float4(1, 1, 1, cc);	
}

Running this exact fragment shader, however, will not work as expected. Floating point arithmetic will collapse small numbers to zero, eventually stopping the smoke from flowing. To avoid this, Omar Shehata suggests in How to Write a Smoke Shader to enforce a minimum flowing quantity:

// Minimum flow
if (factor >= -_Minimum && factor < 0.0)
	factor = -_Minimum;
cc += factor;

This provides a nice fluid dynamic which uniformly diffuses smoke in every direction, until it disspates.smoke2

⭐ Suggested Unity Assets ⭐
Unity is free, but you can upgrade to Unity Pro or Unity Plus subscriptions plans to get more functionality and training resources to power up your projects.

Simulating turbulences

The formula derived causes smoke to diffuse uniformly and in all directions. If we want the smoke to move up, we need to promote transmission to the upper cell.

texture6

In general, we can add four coefficients for each cell (L_{x,y}, R_{x,y}, D_{x,y} and U_{x,y}) that indicates the amount of flow to transmit in each direction.

    \[F^{in+out}_{x,y} = f \cdot \left[ \begin{matrix} F_{x+1,y} L_{x+1,y} + F_{x,y+1} D_{x,y+1} + F_{x-1,y} R_{x-1,y} + F_{x,y-1} U_{x,y-1} + \\ +F_{x,y}\left( L_{x,y}+R_{x,y}+D_{x,y}+U_{x,y} \right) \end{matrix} \right ]\]

When L_{x,y}=R_{x,y}=D_{x,y}=U_{x,y}=\frac{1}{4}, we obtain the equation derived in the previous paragraph. If we set U_{x,y} to a higher value, we simulate an upward movements of the smoke. By using this improved equation we can simulate obstacles, for instance by setting certain coefficients to zero, preventing the diffusion of the flow in certain regions. Updating these velocities at runtime can also simulate winds and other turbulences in the fluid medium where the smoke is moving.

All the four coefficients can be baked into an additional texture, called _VelocityTex, which accommodates L_{x,y}, R_{x,y}, D_{x,y} and U_{x,y} in its RGBA components, respectively.

Conclusion & Download

Become a Patron!

The technique shown in this tutorial is perfect to simulate diffusion phenomenon. Both smoke, water and temperature follows a similar pattern. The series Creeper World bases its gameplay entirely on a similar approach; a sentient blob is expanding, following exactly the diffusion algorithm explored in this tutorial.

What is really missing from this effect is the relationship between the float and the velocity. Our solution keeps these two entities separate, but in a real scenario this is not the case. The state of the art solution when it comes to fluid simulation is given by the Navier-Stoke equations, which described fluid dynamics with incredible precision. Unfortunately, they are rather complicated and GPU intensive. For a primer on this technique, you can refer to Fast Fluid Dynamics Simulation on the GPU.

You can download the full Unity project of this tutorial here.

The next part of this tutorial (How to Simulate Cellular Automata with Shaders) will iterate on grid-based technique to implement one of the most interesting cellular automata: Conway’s Game of Life. Cellular automata will be used in a later tutorial to simulate the flow of water.

Other resources

💖 Support this blog

This website exists thanks to the contribution of patrons on Patreon. If you think these posts have either helped or inspired you, please consider supporting this blog.

Patreon Patreon_button
Twitter_logo

YouTube_logo
📧 Stay updated

You will be notified when a new tutorial is released!

📝 Licensing

You are free to use, adapt and build upon this tutorial for your own projects (even commercially) as long as you credit me.

You are not allowed to redistribute the content of this tutorial on other platforms, especially the parts that are only available on Patreon.

If the knowledge you have gained had a significant impact on your project, a mention in the credit would be very appreciated. ❤️🧔🏻

Write a Comment

Comment

  1. Hi Alan, I’m getting the error (srcAttach < m_CurrentFramebuffer.colorCount && "We should always resolve only current RT") when running the project.

    • I did some investigating and a colleague told me this is a Mac OS specific bug and to get around you need a RenderTexture.active = null (or turn off anti-aliasing on the RT).

      public class ApplyShader : MonoBehaviour
      {
      public Material material;
      public RenderTexture texture;
      private RenderTexture buffer;

      public Texture initialTexture; // first texture

      void Start ()
      {
      Graphics.Blit(initialTexture, texture);
      RenderTexture.active = null;

      buffer = new RenderTexture(texture.width, texture.height, texture.depth, texture.format);
      }

      // Postprocess the image
      public void UpdateTexture()
      {
      Graphics.Blit(texture, buffer, material);
      Graphics.Blit(buffer, texture);
      RenderTexture.active = null;

      }

      // Updates regularly
      private float lastUpdateTime = 0;
      public float updateInterval = 0.1f; // s
      public void Update ()
      {
      if (Time.time > lastUpdateTime + updateInterval)
      {
      UpdateTexture();
      lastUpdateTime = Time.time;
      }

      }
      }

  2. Hey Alan
    the link to “How to Use Shaders for Simulations” isn’t working, same on cellular tutorial. But if you use google you’ll find another page in the website with the same title.
    Thanks for the cool stuff.

  3. Hey, Alan, great tutorial.
    Could you explain what exactly is _Pixels ? What type it is? How should I declare it in Properties and in Pass scopes? In Properties, I put
    _Pixels(“Base (RGB) Trans (A))”, 2D) = “white” {}
    and in Pass I put
    sampler2D _Pixels
    but the line fixed2 uv = round( i.uv * _Pixels ) / _Pixels; didn’t work.
    Thank you!

Webmentions

  • How to Simulate Cellular Automata with Shaders - Alan Zucconi July 17, 2020

    […] Part 2. How to Simulate Smoke with Shaders […]

  • How to Use Shaders for Simulations - Alan Zucconi July 17, 2020

    […] next part of this tutorial (How to Simulate Smoke with Shaders) will focus on how this technique can be used to simulate the diffusion component of particles […]