Realistic water in midcore games: Balancing FPS and visual appeal
Realistic water can play a crucial part in a game’s overall feel. However, overly complex and detailed water visuals can overburden the CPU and GPU, leading to crashes on most devices.
Let’s explore different approaches to creating water in mobile games (using our midcore project Railroad Empire as an example) and on PC.
Bonus: links to shaders with comments at the end of the article.
Approaches to water simulation
The approach depends heavily on the game’s setting. In some games, water is just a filler element, while in others, it’s integral to complex mechanics.
Broadly, water creation can be divided into two categories:
1. Wave simulation: Expensive, suitable for PC and consoles.
2. No wave simulation: Economical, suitable for mobile development.
Wave simulation
Wave simulation can be implemented in various ways, from calculating vertices directly on the CPU to baking in Vertex Animation Texture or Alembic for cinematics.
Our favorite algorithm for “water ripple” effects is Gerstner waves (also known as trochoidal waves). This is a staple for AAA projects where water is part of the gameplay.
For those interested in this algorithm, I’ve written a small setup for URP in HLSL at the end of the article. There’s a lightweight version suitable for mobile development and an advanced version using tessellation.
Further enhancements for making the water look even more realistic include:
- Putting in additional noise
- Reflections on the water
- Caustics
- Blending normals
- Depth calculation
- Creating lagoons
- Calculating the distance for rendering foam near the shore
- And many more
Although, these enhancements are more suitable for PCs and consoles than for mobile games.
On flagship devices, such setups might work, but players with mid-range devices could face errors and crashes.
In mobile development, water creation is usually limited to a few Gerstner cycles, 30-40 tris per water chunk, and 3-4 textures. The remaining power is reserved for game mechanics and UI.
Unfortunately, tessellation, caustics, and other advanced features are still a luxury for mobile games. Let’s talk about them now.
No wave simulation
“Flat water” has a much smaller impact on CPU and GPU performance. But again, it depends heavily on the setting.
For example, in Railroad Empire, water is a filler element of the scene, so we didn’t need Gerstner waves or other embellishments.
However, there were other challenges waiting.
Creating water in mobile development: The usual approach
The simplest and most common method involves mixing two normal maps, adding a cubemap for fake reflections, and ensuring proper geometry unwrapping (e.g., for rivers). This approach avoids the need for flowmaps, as additional textures can make the shader heavier.
Some developers skip normal maps altogether, saving another 4-6 FPS. Noise textures are packed into RGBA channels, lerped, and taken to overexposure, creating a pleasant glare on the water.
In some cases, a single flat mesh with primitive planar mapping and a flowmap for water direction is more rational.
Just in case: a flowmap is a texture used to determine the direction of fluid or particle flow in shaders.
How we did it (in Railroad Empire)
In Railroad Empire, we used one normal texture but maximized its potential for a midcore project. The result was soft, random highlights, just as we wanted. Fake reflections were achieved with a cubemap, and beautiful lagoons with foam were created through masking.
To avoid heavy depth calculation algorithms, we added a transparency mask to the alpha channel of one texture. Transitions near the shores were drawn manually in another channel, giving the water “volume” while minimizing texture use.
Next, we had to decide how to make the riverbeds flow. We opted for a flowmap since one requirement was that the rivers should merge organically.
To avoid constantly drawing flowmaps in 3D editors, we developed a custom Flowmap-Painter, allowing artists to create water elements directly in Unity.
The resulting shader was complex but provided a flexible approach to water implementation.
Other options
As I said, there are numerous other options, but most are not suitable for mobile development.
Some developers create black-and-white sequences of over 100 frames to get a beautiful texture of swaying waves, running it as a vertex displacement pattern. Others bake looping animations in Alembic (not supported on mobile devices), and sometimes it’s more practical to transfer surface swaying to bones.