I've gotten a lot of questions about how the water system works in DwarfCorp recently, so I thought I'd write a post about it.
Simulating the Water
Water in the real world is made up of millions of tiny particles that flow past each other and form temporary atomic bonds. These little interactions between particles follow simple rules based on electrostatic repulsion and attraction. The net result of all these millions of interactions causes interesting effects from water pouring out of a cup to flowing rivers and waves in oceans. Unfortunately, modeling water at this level is still beyond reach of modern computers running in real-time. So we have to make serious simplifications to make something that looks "okay".
Like in Minecraft or Dwarf Fortress, the water in DwarfCorp is a "cellular automata" (CA) based system. A "cellular automaton" is a kind of program where cells (or in my case, voxels on the scale of about a meter) have values which are modified by simple rules. These simple rules can lead to some cool behaviors, but doesn't even come close to simulating real-world water.
CA-based water ranges from extraordinarily complex to fairly simple. In DwarfCorp, the rules are very, very simple. By just having a few of these easy rules, the engine is able to simulate lots and lots of water extremely quickly.
We store a grid of "Water Cells" for each voxel in a small "chunk" of the world. Each cell has a few bits of information: the water level (a byte from 0-255), a "flow" vector, and the fluid type (currently just water and lava).
Each iteration, we loop through all of the water cells which have any water in them and perform the following rules:
- Is the water level below an "evaporation threshold" for its type? If so, subtract water from it.
- Check the cell below the water. If it's occupied, don't do anything else. Otherwise, if it is below maximum capacity? If so, move as much water to the cell below as possible.
- If any water remains from steps 1 and 2, check all adjacent cells (4-connected) in order according to direction of this cell's "flow" vector. Move a random amount of water to each of the cells until we can't move any more. The "flow" vector of each neighbor becomes the vector from the current cell to the neighbor.
- Set the flow vector to zero.
That's it! For lava, we just move a slightly smaller amount to each adjacent cell in step 3. This leads to some pretty cool effects, like flowing rivers, waterfalls, basins which fill and empty, and underground channels. Unfortunately, the water in DwarfCorp does not currently support a pressure model (as in Dwarf Fortress), so water never moves *upward*. We're planning on adding this soon, however.
A lot of people have asked how I render the water, as opposed to simulating it. Water in DwarfCorp features reflections and refraction, waves, subtle texturing, and shorelines. All of this is eye-candy implemented in shaders.
Rendering The Water
The water in DwarfCorp is rendered as a surface mesh that's generated by averaging the heights together between adjacent cells, and culling away faces that are covered by water or obstacles. If we just rendered this mesh as a textured surface, it would look okay, but we wouldn't be able to get the kind of effects you see in DwarfCorp screenshots. To do this, we use pixel and vertex shaders.
There are a few effects that are layered on top of each other in DwarfCorp:
- Reflections - To do this, I first estimate the plane that represents the surface of the water. This is done via raycasting and averaging. (That step can lead to ugly artifacts if water is rapidly changing heights...). Then, we simulate a camera perspective below the plane. We then render the entire scene from this perspective, culling out anything below the water plane, and store this rendering in a texture. The pixel shader figures out what color to draw for the reflection by indexing into the reflection texture, and using Fresnel's Law.
- Refraction - This is identical to reflection, except we don't put the camera below the water plane, and we render everything *below* (rather than above) the water plane.
- Texturing - Because water with just reflections and refractions looks too perfect, we multiply in a cartoony pixel art texture to the scene.
- Shorelines - Vertices of the water that are adjacent to empty cells get a "shore counter" incremented. The higher the "shore counter", the more of the "shore shader" is mixed into the final pixel output. The "shore shader" works by thresholding on the function "sin(t + s)" where t is the time in seconds, and s is the "shore counter", and adding an arbitrary color to the pixel when its above the threshold. The effect is a set of bands which appear and disappear over time.
- Waves - The reflections and refractions are distorted by a normal map representing ripples in the water. Each vertex is also pushed around in the vertex shader by a sinusoidal function over time and position. The effect is water which moves widely up and down, and also has visual distortions.
- Splashes- Particle effects and sounds are created whenever liquid of a certain type moves more than an arbitrary amount.
That's it! Here's a video of the liquid simulation system in action!
You can play with a prototype of the water simulation in 2D here: