For the next few weeks we’ll be having a regular feature on GPUOpen that we’ve affectionately dubbed “Warhammer Wednesdays”. We’re extremely lucky to have Tamas Rabel, Lead Graphics Programmer on the Total War series providing us with a detailed look at the Total War renderer as well as digging deep into some of the optimizations that the team at Creative Assembly did for the brilliant, Total War: Warhammer.
During the development of Total War: Warhammer the primary focus of the Graphics Team was performance and stability. Working closely with AMD was a great help achieving our goals. Porting to DirectX® 12 opened up new opportunities and helped us to better understand the graphics hardware.
Total War: Attila
Let me start with a quick overview of the state of the rendering pipeline after we finished Total War: Attila. In the next post or two I will walk you through the major changes and optimizations we made during Total War: Warhammer.
We started every frame with rendering shadows from the main directional light. We use cascaded shadow maps with 2-4 cascades depending on the graphics settings. We don’t render terrain into the shadow map, but – as you can imagine for an RTS – we render tons of small meshes, compared to FPS/TPS game, which tend to render fewer, but more detailed meshes. In the image below, you can see a screenshot from Total War: Attila showing the engine in full flow.
Terrain is the one of the most important part in our game. Due to being an RTS game our terrain needs are somewhat different to first-person titles. We focus on mid-far distance, rather than focusing on near. We have to break tiling patterns and must have smooth transitions between terrain types. We also have to support instant teleport (when the player clicks somewhere on the mini-map), which poses additional difficulty with streaming techniques. We also have lots of custom meshes (cliff pieces for example), which needs to blend to the terrain textures (which are already a combination of several layers).
Each battlefield consists of multiple tiles. The world map is massive in the Total War games. It would be near impossible for artists to handcraft it all. To cope with this, artists edit and create tiles. You can think of each tile as a single map, but when you start a battle on a portion of the campaign, we construct the battlefield from all the tiles (which can be of any shape and size) around. As a result, each battle is made up of several tiles. Each tile uses its own texture set. To hide tile edges, we also need to blend between the tiles itself.
For example, you can imagine a T junction where forest, desert and marshlands meet. Each tile has its own 8 texture layers, but at certain points we need to blend all three together, which means blending 24 layers per pixel potentially. Each layer has diffuse, normal and specular/gloss textures, so this can be 24×3 = 72 textures per pixel. We have no limits on how many tiles/layers can be combined.
To accommodate for these needs, we first render all the terrain geometry in a depth-only pass (including both the height field and the custom meshes). Then we run one screen-space pass per tile, which has its own blend map and projects the layers down to the geometry (you can think of it as a huge projected decal).
We have 3 textures for the GBuffer with the following layout:
The most obvious thing you probably noticed is that we don’t store anything in the alpha channels of the first two textures. The reason is because we need to blend all those properties while rendering terrain and decals and we need the alpha channel to specify the alpha amount. The normal is stored in a two channel compressed format.
You can also notice that we use diffuse/specular instead of colour/metalness. This is for historical reasons and we are in the process of switching.
Most of the lighting contribution comes from the main directional light. We have a statistically based BRDF model which is physically correct and the distribution of micro-facets is based on a Gaussian distribution. This was introduced in Total War: Rome II. It gives perfect sphere (not elongated) specular reflections and can work with a sun disk size. Ambient lights were just 6 fixed colours and there are specular probes and SSR for indirect reflections.
Total War: Attila also had a custom ambient occlusion implementation.
Particles are spawned on the CPU, but simulated and sorted on the GPU. The three types of GPU particles we support are Quad, Point Light and Projected Decal. On top of that we have a CPU pipeline with limited functionality for mesh particles.
We have a curve-based tone-mapping operator with automatic levels base on the average/min/max luminance on screen.
The Total War: Attila Pipeline (Simplified)
Next time we’ll take a look at how we measured performance in Total War and take a look at some general shader optimizations that helped us hit the performance levels we wanted.
Other posts in this series
Tamas Rabel from Creative Assembly discusses how performance was measured with the Total War Engine.
Here’s Tamas Rabel again with some juicy details about how Creative Assembly brought Total War to DirectX® 12.
Tamas Rabel talks about how Total War: Warhammer utilized asynchronous compute to extract some extra GPU performance in DirectX® 12 and delves into the process of moving some of the passes in the engine to asynchronous compute pipelines.
The final instalment in Tamas Rabel’s insight into developing the Total War engine looks at Multi-GPU.