The World of Tanks Blitz spoke about the major graphics overhaul introduced with the 9.0 update, explained how they relit the game's maps, and discussed the upgraded physics.
World of Tanks Blitz has been entertaining players for 8 years and has no plans to stop! But the mobile games industry has changed a lot in the near decade that Blitzers have been battling. As players’ tastes and expectations change with the times, so does Blitz.
In this article, we’ll take a deep dive into the major graphics overhaul that we launched with 9.0.
World of Tanks Blitz runs on a custom engine called DAVA, which allows us to more easily integrate our in-house solutions into the game. In version 9.0 we launched a map with a completely reworked lighting system.
Old Castilla vs New Castilla:
If you don’t know what PBR (Physically Based Rendering) is, there’s already a lot of great material on this topic online. But in short, PBR is a set of approaches that allows you to simulate the interaction of materials with light and the lighting itself so that images look more realistic.
Typically, PBR calculates directional lighting ("from the sun") and lighting from the environment ("sky sphere") separately. When calculating, lighting is divided into two components: diffuse light and specular light, which allows for more sophisticated rendering.
First of all, we had to decide how the lighting would interact with static objects. This includes houses, stones, benches, and fences – everything which adds that lived-in feel to the map. Before 9.0, these objects were not dynamically lit - the light calculation was made in advance and saved in textures, which were added to the game resources.
These textures are called lightmaps, and the process for calculating them is called Lightmap Baking. Since no dynamic light sources were applied to houses – regardless of the angle of view, the lighting looked the same from any side (but still looked pretty decent, considering its limitations).
But how to make it look even better? Ideally, we wanted to be able to include lighting components that add realism, like indirect lighting and self-shadowing objects in hollows, where it is more difficult for light to reach. The solution we found was to continue to bake the light, but for PBR, we now put non-finally calculated light into the lightmaps. In the first texture channel, we added shadows from a direct light source, and in the second channel, we added Ambient Occlusion.
There were 4 main arguments in favor of this approach:
- We can use component-by-component information separately to modify direct and indirect illumination, leaving the PBR calculation physical.
- Our dynamic shadows are not enabled on all devices, so we have reasonable static shadows even without shadowmaps
- Even where dynamic shadows are enabled, there is an edge beyond which the shadow is no longer calculated. But with the mixing of dynamic and static shadows, this border is hidden.
- This approach is preferable for the AO channel as it does not incur the same performance costs as calculating self-shadowing during rendering (for example, SSAO).
Lighting from the environment and lighting from a directional light source are combined into the final picture:
Dynamically calculated shadow smoothly turns into a baked one:
Previously, lighting on bushes and trees was done by baking the shading from the environment for each tree and then tinting it with the tree’s color. In 9.0, we added the ability to customize PBR-lit trees, removing spherical harmonics for coloring and using them only for Ambient Occlusion. One interesting problem we ran into was calculating the normals for tree foliage. The foliage is created with planes, and partly by billboards – planes that are always turned towards the camera.
Using the plane geometry normals leads to visible artifacts. The solution was to reorient normals: the artist sets up the ellipsoid and controls how much the normal will be rotated along it – giving the foliage the necessary volume.
To give the appearance of the tree realism and volume, we had the trees cast shadows on themselves. This resulted in a self-shadowing artifact due to the billboards being rotated into the camera to render the shadowmap. Initially, we tried to fix this by orienting the billboard in the same way as in the main rendering pass, but this was not enough to eliminate the artifact. In the end, we resolved the issue by shifting vertices in the world space when rendering foliage in shadowmap.
The self-shadowing artifact of the billboard
The fixed version
Another visual improvement included in 9.0 is the addition of smooth transitions between different levels of detail for trees on all maps. Detail models, or LODs as they are called in the industry, determine the detail level at which an object is displayed, depending on its distance from the player.
When moving away from the object, instead of a sharp substitution of one LOD for another, now both overlap, smoothly flowing from one to the other through the alphatest mesh.
One area in particular need of a visual overhaul was the landscape. With the new lighting system, the number of textures that need to be read and blended greatly increased, which came with serious rendering costs.
Each tile requires its own texture set, And in addition to these texture sets, for each of the four tiles, we read several global textures: tint, normal, and baked light. One very effective method to reduce the cost per terrain is to use a virtual texture, we did not have time to introduce this toolset in 9.0, but we plan to return to the issue in future updates. Nonetheless, we were still able to achieve a major increase in image quality by packing the textures of tiles into texture arrays – allowing the artists to work with independent scale tiles
Along with the map overhaul, we decided to completely update our vegetation rendering technology. Previously, grass was drawn with a large number of individual blades of grass. There were a lot of triangles, which led to a heavy system load on the vertex stage of the pipeline, and visually the grass already looked outdated.
A classic alternative to individual blades of grass is a grass texture with alphatest (transparency). And although the alphatest is not considered a very good solution for a mobile rendering architecture (however, neither is a large number of triangles) in our case, it significantly improved visuals without degrading performance.
As with the old approach, the new grass has multiple LODs, and each LOD is drawn in one draw call. Transitioning between LODs is smooth, using the same method as with trees. The grass is planted on the map procedurally based on the textures drawn by the artist. Lighting has also been added to the new grass, which interacts with the PBR lighting on objects and terrain.
The grass is lit using standard PBR lighting, but with a few extra hacks:
- Blending Grass Normals with Terrain Normals
- Darkening the grass towards the bottom (tinted with AO and Shadow)
- Fake self-shadowing mimics the shadows that some blades of grass cast on other blades of grass.
- The alpha channel from the grass texture is read with a texture coordinate offset that is set by the artist, plus one more offset for animation, and this sample is used as a shadow mask.
We’ve also added a feature called Bush Physics. Bushes and grass now react to explosions and tank shots, as well as to tanks moving through them. The feature is active on all maps. To simulate the movement of the bushes we procedurally generated skeletons for them
When a blast wave passes through the bone, the bone folds away from the incoming shot and moves the foliage tied to it.
For grass, the wave bend simulation occurs in the vertex shader. To simulate being pushed by a tank, the same skeletons were used, with the tank approximated by a capsule for ease of calculation. Here the main task was to write the physics of the interaction of the capsule with the bones, and not produce glitches.
Another new vegetation feature is grass compression – Grass now crumples when run over by a tank. Grass compression is based on a laying map – a texture into which tank treads are drawn in a given radius around the camera every frame. The tread box writes the direction of the crush, its strength, as well as the depth to restore the position of the tread. The strength of the crush is modulated through the length of the direction vector. Further, when rendering grass, this texture is read in the vertex shader and, based on it, the grass vertices are displaced accordingly, and the crushed grass is also tinted in the fragment shader
Rendering a map with PBR lighting requires a lot of performance. As expected, even the most modern mobile devices are not always able to render a PBR image with an acceptable FPS. The most common solution to this problem, used in many mobile games, is lowering the rendering resolution. The lower the resolution, the less expensive the PBR shader execution.
Even before the advent of PBR, the game had a "Half Resolution" option that reduced the resolution of the entire game rendering by half on each axis. This option was used mainly on ancient devices, like the iPad4. With the release of PBR, we've added another option for downscaling. Rendering resolution is now reduced only for the 3D scenes and only in battle, that is, all UI elements continue to be drawn in native resolution.
But the main feature is the dynamic change of the rendering resolution depending on the frame rendering time. This means that the resolution will decrease if the engine is unable to draw the frames in the required time, and increase when the frame rendering time is fast.
We use two different methods to determine frame rendering times:
- On iOS with the Metal API, we use GPU counters to get the real-time a frame is rendered on the GPU. We also use the tile architecture to find out and use exactly the execution time of the fragment stage, which is primarily affected by lowering the resolution.
- On other platforms, we calculate the approximate frame rendering time based on the time the render thread runs on the CPU.
Interestingly, on Android, we also tried to get the real frame rendering time using Time Queries, and this worked on many devices. But a problem arose: some devices that formally supported the required extension actually gave invalid frame rendering time values.
When planning to update the lighting in Blitz, we broke the process into several stages. A year ago, in version 8.0, we introduced higher-quality tanks into the game, this year, we took up maps.
Updating all of the maps takes a lot of work (in particular, redrawing all textures on the map), so implementing visual upgrades is something we do gradually. Our next project is to work on the quality of the engine and improve performance so that our users can enjoy playing games on new maps without overheating their phones. And at the same time, we will continue to modernize the graphics. After all, the current lighting is not the final station, but only platform 9 and three quarters. Global Illumination is still in the works!