In 2015 Plarium, an international software development company specializing in mobile and browser games, released the MMO strategy title Vikings: War of Clans. The Vikings world is a breathtaking one where anyone can become a real Viking by upgrading their town, training troops and fighting in exciting PvP and PvE battles with millions of players from all over the world. The game is designed for iOS and Android mobile platforms and a desktop version was also be released in 2016. The app works on the Unity engine. The game visuals feature both 2D and 3D graphics.
One of the most important locations in the game — the map of the inside of the town — was carefully drawn and enhanced with eye-catching animations. While designing the map, the artists referred to pictures of the Scandinavian peninsula and Norway’s waterfalls, thus creating a realistic Viking gaming environment. The wonderful landscapes of these lands inspired the art department to create the sea bay with its sky-blue waters and the cascading waterfalls, which were animated later. Because of the technical limitations of mobile platforms, this task wasn’t easy. Anything is possible, however, when you believe you can do it!
Animating the sea bay
In PC games water animation is usually carried out using a 3D volumetric mesh, vertex animation and complex shaders. Although excellent results can be achieved using this method, it is only suitable for exceptionally powerful video cards. Vikings: War of Clans, however, was initially designed for mobile devices. We therefore decided to make animations with a simple flat mesh and animate the textures using UV coordinates. Freed-up computing resources were then directed towards creating as high a quality of product as possible, within the bounds of this simple approach. This method turned out to be efficient, however the visual component didn’t meet our expectations.
We then had the idea of using normal maps to achieve the effect of three-dimensional glare on the water. This method was much more successful than the previous one. The artists made a detailed static image of the sea and then proceeded to create the water in its dynamic state. At this stage, they took inspiration from nature itself to design the main visual effect for animating the sea realistically. Observing the seashore, the artists noticed some differences. “Deep” water has a dark saturated color and is nontransparent. The bottom of the sea and shoreline can be seen through shallow waters, however, meaning that shallow water takes on some of their color. In order to convey these nuances on the map, the bay was divided into depths and shallows. To implement what we had in mind, we adjusted the glare on the water in the normal map and also wrote a shader with two masks for the deep and the shallow water. Then came the long process of optimizing and testing the shader on mobile devices to confirm that the animation displayed correctly.
Optimizing the shader
First of all, the technical artist designed the shader prototype in a special visual editor. The prototype code was generated automatically and needed to be optimized. The first step of the process was arranging the code: deleting superfluous variables, passes and assigning distinctive names to the variables.
The next stage of work was more creative. We selected all the “heavy” methods and operations in the shader and replaced them with “lighter” ones. In addition, all operations were reduced to ones supported by hardware-based target devices. If the operations did not have a substantial impact on visual effect, they could be removed. Slightly different UV coordinates when reading the texture were therefore replaced with identical ones so that the texture was read once rather than twice.
It’s important to remember that after each stage of optimization, a visual check of the result against the initial prototype version must be conducted.
It also makes sense to include various masks in one texture. This trick was used when animating the sea for Vikings: War of Clans. It’s well known that the normal map has a vector of unit length coded in. This makes it fairly easy to reconstruct one of the vector components from two others, i.e. in the normal map one of the color components can be used for mapping the third coordinate, the normal, as well as for other, more interesting goals. When we were optimizing the shader, the normal maps for shallow and deep water were sewn in one texture: for shallow water two color components on the normal and for deep water one color component with alpha. It’s worth bearing in mind, however, that carelessly deleting texture components because they have been mistaken for spare components contradicts one of the basic recommendations when optimizing a shader: calculate as much information as you can before you begin and transfer it to the shader from the outside as parameters or in texture maps.
This information can also be mapped directly through the mesh by using vertex color, transparency and additional UV coordinates. The mesh form itself can also hold a lot of useful information. To avoid having large transparent areas in the water, for example, we fitted the shape of the mesh as closely as possible to the shape of the shore line instead of using a simple rectangular mesh with a transparency mask for the shader. As in all areas relating to graphics programming, however, it’s vital to maintain a balance here. In this particular case, we’re talking specifically about the balance between mesh complexity (vertices number) and the size of transparent areas.
The next stage of optimization was uniting variables and operations. GPU architecture is designed for working with four-component vectors that are used for transferring data from the application to the vertex shader and from the vertex shader to the fragment shader. Therefore, if there are four one-component parameters present, it makes sense to unite them all in one vector. If this is not the case, the GPU will do it automatically, but instead of one four-component vector there will be four of them. While working on the water animation in Vikings: War of Clans, we united UV coordinates for shallow and deep water into one four-component variable both for transferring from the vertex to the fragment and for performing all operations with UV coordinates. The precision of variables should also be monitored: the minimum possible precision should be aimed for and any unnecessary adjustments from one level of precision to another avoided (the same precision values must be displayed to the left and right of the equals sign).
Another important part of the optimization process was combining addition and multiplication operations so that the result was interpreted as a MAD (multiply-add) operation. In this case, several instructions become one.
The last technique used in all stages of the optimization process was transferring the calculations from the fragment shader to the vertex shader. The calculations must either be transferred to the vertex shader or removed from it and moved to the application itself as long as this doesn’t result in any significant visual distortions.
Animating rivers and waterfalls
Besides the sea bay, the rivers and waterfalls on the map also had to be animated. We used the shader with UV texture coordinates in the additive blend mode. The river flow was drawn on a static base and the effect of flowing water was achieved using a water shader.
How the shader works
The texture was animated in the direction of the V coordinate from 1 to 0 so that the entire mesh scan lay in a way that allowed the water to flow in one direction. To change the movement of river water according to its location on the map, we had to move the mesh vertices in the desired direction. However, we needed to ensure a smooth transition from river to waterfall, as in real life they are a single body of water flowing at different speeds. This task was handled using a similar approach. Basically the same shader was used, and in some cases even the same mesh. The only difference was that the UV coordinate layout for mapping the river flow was more stretched and for the waterfall it was more compact. As a result, a certain texture speed on the UV coordinates produced a different texture speed on the mesh. Visually the waterfalls have a stretched texture but this effect looks good as it looks like a stream flowing downwards.
Three ways of hiding mesh edges
As the water animation was laid over the detailed water texture, the mesh had rough edges. We managed to get rid of this undesirable visual effect by fine-tuning the shader and hiding the mesh edges with alpha transparency.
How to hide mesh edges
- Use the alpha channel written in mesh vertices (alpha vertex). This way isn’t very convenient since a heavier mesh with lots of additional vertices needs to be created.
- Use alpha mask textures as additional textures in the game build. This method isn’t effective either as we basically struggle for any textured space.
- Create the mask using the shader itself. This method proved to be the most efficient. Using seamless water texture allowed us to achieve the required result — high-quality animations of rivers and waterfalls.
A small group of professionals worked on the town map water animation for about two months. The team included a technical artist, shader specialist and several 2D artists. It was a time-consuming yet exciting process that allowed the professionals to fulfill their artistic ambitions and apply their practical experience. It’s also worth mentioning that creating animations requires more than just creativity: many hours of hard work go into optimizing the interim result and creating a high-quality final product.