../

pt-br en

Diffusion Automata v1

The goal for this Automata is to simulate the diffusion of a liquid on a solid medium, like soil, sand, etc. It was created as a prototype of a underground water diffusion for my game.

As will notice, there isn’t much complexity in this code, that’s the magic of this kind of algorithm, we can use simple rules to create complex behavior. Here, we simulate the interaction between liquids ans solids over time and space using basic arithmetic

Our board will be represented by a 2d array of vectors, this is just for convenience since we can easily group the two relevant values:

  • Humidity
    • The amount of liquid within the cell
  • Impermeability
    • The resistance of the cell to the movement of liquid trough it

The fields Rocks and Rain just represent cells that are totally impermeable or sources of humidity, respectively.

type HumidityBoard struct {
	values      [][]mgl32.Vec2 // [humidity, impermeability]
	Rocks, Rain [][]bool
	hvrX, hvrY  int
}

The logic to determine the humidity of a cell is simples, since the liquid converges to a uniform distribution over time for all the space, we assume that the new value of humidity of a cell is just the mean between it and their neighbors values. For simplicity, we only count 4 neighbors as the diagram below, where v0 is the current cell:

  v3  
v1 v0 v2
  v4  

To include permeability properties, we use a weighted mean where each factor is defined by that cell impermeability. The catch is that we use the inverse(1/value) for neighboring cells, that way we achieve the effect of a highly impermeable cell resisting either losing or gaining humidity. Then we clamp to new value to a max of 1024.

func (ba *HumidityBoard) Update() error {
	// Store the initial state as reference
	m0 := make([][]mgl32.Vec2, len(ba.values))
	copy(m0, ba.values)

	for x, row := range ba.values {
		for y, v0 := range row {
			// Skip the calculations for humidity sources and impermeable cell
			if ba.Rain[x][y] || v0[1] >= (math.MaxFloat32/5)*4 {
				continue
			}

			// Assume that borders are dry and impermeable
			v1 := mgl32.Vec2{0, math.MaxFloat32}
			v2 := mgl32.Vec2{0, math.MaxFloat32}
			v3 := mgl32.Vec2{0, math.MaxFloat32}
			v4 := mgl32.Vec2{0, math.MaxFloat32}

			// Verify if neighbor exists and use their value
			if x > 0 {
				v1 = m0[x-1][y]
			}
			if x < len(m0)-1 {
				v2 = m0[x+1][y]
			}
			if y > 0 {
				v3 = m0[x][y-1]
			}
			if y < len(m0)-1 {
				v4 = m0[x][y+1]
			}

			// Calculate the weighted mean
			r := ((v0[0] * (v0[1])) + (v1[0] / v1[1]) + (v2[0] / v2[1]) + (v3[0] / v3[1]) + (v4[0] / v4[1])) / (v0[1] + (1 / v1[1]) + (1 / v2[1]) + (1 / v3[1]) + (1 / v4[1]))

			// Clamp values to 1024
			if r > 1024 {
				r = 1024
			}

			// Updates board with new humidity value
			ba.values[x][y][0] = r
		}
	}
	return nil
}

The final result is not perfect, there are parameters that are not accounted like velocity and density of the liquid, but it’s sufficient for our porpoises. Another limitation of this method is lack of conservation of mass. As the simulation evolves the humidity spontaneously drops, which is physically impossible.

As stated previous, this is a prototype and limitation as this are not necessarily concerns for games development. Keep in mind that this algorithm is synchronous, but it’s totally possible to paralelize it.


Hope you liked this material and that it was helpful, I’m still working on this website and demos to help people that are interested in programming things beyond the web development niche. If you have any question or suggestion, please contact me through GitHub or e-mail. thank you!