In this section we are going to look at simulating the movement of the water surface. We model the surface as a 2D grid of particles. These particles then form the basis of a mesh by rendering triangles between them.
A particle is modelled as a point in space with a mass and velocity vector. Each particle is connected to each of its immediate eight neighbours. That is four to the left, right, in-front, and behind, then the diagonals. These connected particles all apply forces to each other as though a spring connects them, this virtual spring wants to keep the particles spaced apart but not too far apart - that is there is a configured resting distance between particles, any deviation from this distance creates a correcting force proportional to the deviation. The effect that this has is that any movement that one particle has propagates through the network of particles. In addition to the forces between particles we need to introduce some damping. This absorbs energy from the system and brings it eventually to stasis. The effect of these forces combined is to create a realistic looking water surface.
Figure1 : configuration parameters for surface simulation
Once we have the simulation of the surface we need to put some energy into it to make it move. We do that with a configurable impulse function. This creates smoothed impulses of a configurable radius. I’ve added lots of other parameters too to vary the density and shape of the impulses.
Figure2 : configuration parameters for impulses
The whole process is implemented as multiple stages of compute shaders and can easily manage a surface with 1024 x 1024 - 1 million points at good frame-rates on the reasonably modern Radeon Pro 560X in my laptop. For most uses it isn’t necessary to have such high resolution, and we will be needing some spare capacity for what comes next.
Rendering of the mesh directly from the GPU using the compute buffers updated by compute shaders is possible using the Graphics.DrawProcedural() in Unity. This requires a custom surface shader that understands the structure of the compute buffers. This isn’t very nice because it requires mixing of concerns. Both the conversion of the compute buffers into the mesh and the material characteristics must be put together into the custom shader. It is however the only efficient option available.
A repo containing the full Unity project we are using in this tutorial is available on GitLab. The Scene with the setup described in this section is JustLiquid.unity.
Continue with Part 3: Refracting Water Surface in Unity