Modeling 2D Water with Springs: Part 2

In part 1, all the groundwork for setting up the water simulation was laid. We have springs attached to the top verts of the water plane, which always want to remain in their neutral position. Once a force is applied to the springs they will oscillate back and forth based on the dampening and tension constants that are passed in.

At this point we have a Mesh (with a handy BoxCollider2D making it super easy to position and shape the water plane in the editor) and our WaterColumn struct ready to go, but nothing is happening yet. We need to implement an Update method and start applying some forces to the springs. There are different ways to get creative with this part so I’ll just touch on this particular implementation. Right now the springs don’t affect each other at all, which would make the water look pretty silly if we applied a force to any of our springs. We need to take into account what our neighbor springs are doing. Are the neighbor springs above or below the current spring? If so, apply a force in that direction based on how far it is from the current spring times some made up constant. Something like this (which shows only the spring to our right): spring[i].velocity += konstant * ( spring[i].currentHeight - spring[i+1].currentHeight).

The final piece of the simulation is detecting when something falls in the water and applying an appropriate force. This is where our BoxCollider2D (which is set as a trigger) comes in handy. We use OnTriggerEnter2D to detect anything hitting the water, then use its velocity and mass to affect our springs. The easy way to do this is to just find the nearest spring and apply the force to that spring. We don’t take the easy way out here though so we are going to do this the right way. What we will do is use the Bounds of the object that fell in the water to determine exactly which springs should be affected. Additionally, if the object falls between all of our springs (this happens with small objects on mobile sometimes) we just grab the 2 closest springs and apply the force to them. Once we’ve found all the affected springs, we first divide the force by the affected spring count. This will spread out the force. We then apply it to each spring by adding to its velocity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void splash( Bounds bounds, float velocity )
{
  // snip: setup code...

  // find all our springs within the bounds
  var xMin = bounds.min.x;
  var xMax = bounds.max.x;

  for( var i = 0; i < _columns.Length; i++ )
  {
      if( _columns[i].xPosition > xMin && _columns[i].xPosition < xMax )
          _touchedColumnIndices.Add( i );
  }

  // if we have no hits we should loop back through and find the 2 closest verts and use them
  if( _touchedColumnIndices.Count == 0 )
  {
      for( var i = 0; i < _columns.Length; i++ )
      {
          // widen our search to included divisitionWidth padding on each side so we definitely get a couple hits
          if( _columns[i].xPosition + _divisionWidth > xMin && _columns[i].xPosition - _divisionWidth < xMax )
              _touchedColumnIndices.Add( i );
      }
  }

  // snip: updating velocity on affected WaterColumns and optionally spawning a splash prefab
}

That’s all there is to the simulation. There is still more we can do to make it look better. The first gif showed what the water looks like with minimal springs/verts and only simple vertex colors. That is the low-end version of the water. Adding a refraction shader would give it a pretty neat look. You could also apply a blur shader that moves around and varies itself over time. The gif below uses a displacement map with a GrabPass to provide a bit of life to the water. Unfortunately, the low quality gif doesn’t let you see the true beauty of the effect.

Here is a snippet showing what this simple displacement shader is doing. The first pass of the shader is a GrabPass into the GrabTexture. DispTex is just a Texture2D of some noise. Playing around with different displacement textures ends up producing a variety of different looks with relative ease.

1
2
3
4
5
6
7
half4 frag( v2f i ) : COLOR
{
  float2 displacement = tex2D( _DispTex, i.grabUV / 6.0 ).xy;
  float t = i.grabUV.y + displacement.y * 0.1 - 0.07 + ( sin( i.grabUV.x * 6.0 + _Time.y ) * 0.005 );

  return tex2D( _GrabTexture, float2( i.grabUV.x, t ) ) * ( i.color * 3.0 );
}