Ultrakill Frame Analysis
Introduction
For my Intermediate Graphics & Animation class we had a Frame Analysis assignment where we picked a game and looked at it using a frame debugger to understand how everything shows up on the screen. I chose to look at Ultrakill since it has a lot of interesting blood effects that together creates a lot to look at, but individual parts of it look simple which led me to think about how it might be reflected in the graphics pipeline.
Ultrakill was made in Unity, and its graphics API is D3D11. There are 292 draw calls in the capture I looked at. This number was somewhat surprising, because other captures I had in the same level hovered around 200-250 draw calls. This frame had a lot of blood effects applied, which most likely caused the spike in draw calls. I could tell they utilized forward shading since they don't have any geometry buffers and the major passes reflected typical passes in forward shading.
Major Passes
Background Environment Pass (Color Pass #1)
In this first step a lot of background objects are rendered such as the skybox, background props, and the ground. They render a lot of similar objects back to back (i.e. pillars and trees) likely since they have the same texture passed in and have similar priority.
Most of the objects were actually rendered in low poly models except for the trees which are imposters. This is because it is harder to make trees look good in low poly than simple pillars due to their complex geometry.
Draw call for a tree showing the drawn quad
Low poly mesh for a rendered object
Background Environment Pass Output
Opaque Object Pass (Color Pass #2)
In this step they draw all opaque objects. They render most of the environment objects first since they sample the same texture, then most of the enemies and dead enemies, and last the blood effects.
Packing together textures here allows for more objects to use the same shader and configurations just different UV's.
Image of the texture used in many of the environment draws.
Opaque Pass Output
Blood Stain Pass (DrawIndexedInstanced & DrawIndexed)
Here they use a depth buffer and a blood splatter texture to pick where a blood stain should be. Then apply it to existing terrain.
At first I was surprised that this step wasn't grouped into one color pass, but it makes sense since it uses a depth buffer to draw to a texture then uses that texture to draw to the scene. Doing these all together in one pass may have been harder to implement or less efficient than keeping it separate.
Stain Pass Setup
Stain Pass Output
Transparent Pass (Color Pass #3)
This step renders all objects that are transparent. The order they are rendered in is back to front. This makes sense since transparent objects care about what is behind them while opaques do not.
Something unique about this step is that it not only outputs the scene’s color, but also a render texture with transparent objects that are ahead of enemies in green and the enemies in red.
Image of other ouput in this step.
Transparent Pass Output
Outline Post Process (Dispatch)
This step creates an outline of the enemies that are blocked by transparent objects using the texture generated in the transparent pass.
I thought it was really cool to see that this step uses a compute shader meaning it doesn’t have any of the normal steps in a draw call (i.e. vertex shader, rasterization, fragment shader, etc). It's something I hadn't thought of and since it doesn't have the regular steps it makes it faster.
Outline Post Process Output
UI & Weapon Pass (Color Pass #4)
This step renders anything that needs to be rendered on top of the scene like the UI, weapons, hands, and skull. The order seems to just be rendering all of the objects first then all of the UI elements.
While most objects rendered in this pass were clearly visible, I noticed there was a smoke particle effect on the gun. It was small and semi-transparent, so it stuck out being a part of this pass.
UI & Weapon Pass Output
Vertical Flip (Color Pass #5)
This step simply reflects the image vertically so that it is right side up in gameplay. Upon further research this is something unity games do in order to match conventions between platforms.
Vertical Flip Output
Conclusion
Overall, I really enjoyed doing this frame analysis. I thought it was really interesting looking at how a game handles different things that need to be rendered and thinking about the reasoning behind why they do the things they do. The most challenging part for me was deciding which draw calls that weren't passes were important enough to include since there were a few that I chose not to include, but might have been able to talk about. I also had trouble initially navigating RenderDoc, but as I spent more time experimenting it all came together quickly.