Since the release of the latest installment in the Metro series I’ve spent a few hours looking under the hood and I think there are some things that might be interesting to other tech oriented people. My goal is not to do an extensive analysis or to dig into the shader disassembly but to see some higher level choices the developers made.
Right now there’s no widely available information from the developers about the rendering techniques used in the game. The only official source of infromation is a GDC talk which is not available anywhere online. This is a shame because the game is running on a very nice custom engine evolved from the previous Metro games and it’s one of the first titles using DXR.
Disclaimer: This writeup is not complete and I will be coming back to it and updating it when I find something worthy of adding. It’s possible that I’m missing something because it only happens later in the game or simply overlooking some detail.
It took me a few days to find a setup that works with this game and after trying several versions of RenderDoc and PIX, I ended up using Nvidia NSight for the inspection of the raytracing results. I was considering looking into the rendering without the raytracing features but NSight allowed me to check some details about that too so I decided to keep those features enabled. For all other parts of the rendering PIX was working quite well. Screenshots are taken from both applications.
One caveat with NSight, it doesn’t support saving the capture to a file so I can’t really go back to the frames that I’ve been looking at.
Another issue I ran into at the very beginning that was completely separate from any frame debug application I tried using was that the raytracing features required the latest Windows update but the game allowed enabling them in the options even without having the update installed. Enabling the features this way ended up crashing the game on start. GeForce Experience also didn’t say anything about not having the correct windows version to enable those features. This is something that could be improved on both sides.
Just for the sake of completeness I took the captures from the game running on the highest possible settings but without DLSS.
A quick breakdown of the rendering shows a pretty standard set of features, other than the raytraced GI.
Before rendering anything, the previous frame is downscaled on the compute queue and the average luminance is calculated.
The graphics queue is kicked off by rendering distortion particles (droplets on the camera) that will get used in the postprocess phase. Then a quick depth prepass lays down some depth before the Gbuffer, this seems to be rendering only the terrain.
The GBuffer pass fills out 4 render targets with the following setup, while also completing the depth buffer.
1, RGBA8 target with albedo and maybe AO in the alpha, looks very dark on some surfaces
2, RGB10A2 target with normals and maybe subsurface scattering mask in the alpha
3, RGBA8 target with some other material parameters, probably metalness and maybe roughness in the alpha, curiously the RGB channels contain exactly the same data in this case
4, RG16F target with 2D motion vectors
After the depth is filled with everything, a linear depth buffer is built and then downsampled, all this is done on the compute queue. Still on the same queue a buffer is filled with something that looks like directional lighting without the use of shadows.
On the graphics queue the GPU is busy with raytracing global illumination but more on this later.
Back on the compute queue the ambient occlusion, reflections and something that looks like edge detection is calculated.
On the graphics queue a 4 cascade shadow map is rendered into a 6k * 6k 32bit depth map. More details on this later. After the directional shadow map is complete the 3rd cascade is downsampled to 768 * 768 for some reason.
A curious thing to find in the middle of the shadow rendering: an impostor atlas is updated with some objects before the local light shadows would be rendered. Both the impostors and the local light shadow buffers are also 6k * 6k textures.
After all shadows are ready the lighting is started. This part of the rendering is quite fuzzy as there’s a lot of draws that do something obscure and it needs further inspection.
The scene rendering is finished off with the forward lit objects (eyes, particles). The visual effects are rendered into a half resolution buffer and composited back with the opaque objects by upscaling.
The final look is achieved by tonemapping the image and calculating bloom (by downscaling and then upscaling the tonemapped frame). Finally the UI is rendered into a separate buffer and together with the bloom, composited on top of the scene.
I haven’t really found the part where the antialiasing would be done so this is also something I would like to follow up on later.
Some details about the raytraced GI. The acceleration structure covers a large area in the game world. It’s probably several hundred meters with very high detail everywhere. It appears to be streamed in some way. The acceleration structure scene also doesn’t match the scene that gets rasterized, for example the buildings in the images below are not visible in the rasterized view.
Here you can see the four tiles around the player position. The lack of alpha tested geometry is also apparent. The trees have trunks but they have no leaves. There’s no grass or bushes either.
A closer view shows better the object detail and density. Every object that’s differently colored is a different bottom level acceleration structure. There’s probably hundreds just on this picture.
Interestingly the items of the player are also part of the acceleration structure but they are for some reason positioned at the feet of the player.
Some of the skinned objects seem broken in the acceleration structure. One observed issue caused stretching of the mesh (on the legs of the kid). Another issue caused different parts of a skinned character to end up in different positions. There is no stretching per say but the parts are disconnected. None of this seems to be visible in the raytraced GI, or at least I haven’t managed to spot it in game yet.
A wider shot shows how many different objects are actually in the acceleration structure. Most of these are not really going to contribute to the look of the GI results. It is also visible that there is no kind of LOD scheme. All objects are added with full detail. It would be interesting to know if this has any performance implication in the raytracing (I would assume it does).
Another shot reveals the extreme detail of the objects even further away from the player. Every knob and dial is visible and clearly readable even from this shot that has no textures. The place where I moved the camera to take this shot is at least tens of meters away from the player and there is absolutely no chance that removing this detail would cause any degradation of visual quality. Maybe the acceleration structure update would be too expensive if any kind of LOD would be used but there is a good chance that this update could be done asynchronously. This is definitely something that needs further investigation.
Directional shadow rendering
Most of the shadow rendering is simple and needs no further mentioning but there are some interesting tidbits there too.
Similarly to the acceleration structures the shadow rendering also seems to be including absolutely everything. There are objects that contribute almost nothing to the shadow map but they are still rendered. Is this because of the fidelity or there is no easy way in the engine to exclude them?
There are objects that would be hard to pick up even with screen space shadows. They don’t take much time to render but it’s interesting to see that they are not removed to save some time.
Some of the meshes rendered in the shadow map seem to have broken index buffers when inspecting the mesh, but they look correct after the vertex shader (same results in PIX and NSight). This example is the best one I found, but by far not the only one. Is this some special packing of the position?
Looks like the skinning is not only an issue in the acceleration structures. It’s interesting that this never seem to cause any artifact on the screen.
Thanks for making it to the end, there’s probably a lot more to learn from the rendering of Metro: Exodus. I will be looking at other scenes in the game and updating this article as I mentioned so maybe check back in a few weeks.
It’s always interesting to look into the rendering of a game especially when it has some feature that is unique on the market. Raytracing right now is a very popular topic and seeing how it is used by this game might give an early look at how games will do things in a few years.
If you liked this article and you would like to see more of these let me know below. Make sure to include which games would you be interested in? Otherwise if you have some suggestions how I could improve it, I would be happy to hear.