Analyzing a Frame
This is the frame we’re going to be looking at. It’s the beginning scene of Lords of Shadow 2, Dracula has just awakened, enemies are knocking at his door and he is not in the best mood.
Depth Pre-pass
LoS2 appears to do what is called a depth pre-pass. What it means is you send the geometry once through the pipeline with very simple shaders, and pre-emptively populate the depth buffer. This is useful for the next pass (Gbuffer), as it attempts to avoid overdraw, so pixels with a depth value higher than the one already in the buffer (essentially, pixels that are behind) get discarded before they run the pixel shader, therefore minimizing pixel shader runs at the cost of extra geometry processing. Alpha tested geometry, like hair and a rug with holes, are also included in the pre-pass. LoS2 uses both the standard depth buffer and a depth-as-color buffer to be able to sample the depth buffer as a texture in a later stage.
The game also takes the opportunity to fill in the stencil buffer, an auxiliary buffer that is part of the depth buffer, and generally contains masks for pixel selection. I haven’t thoroughly investigated why precisely all these elements are marked, but for instance was presents higher subsurface scattering and hair and skin have its own shading, independent of the main lighting pass, which stencil allows to ignore.
- Dracula: 85
- Hair, skin and leather: 86
- Window glass/blood/dripping wax: 133
- Candles: 21
The first image below shows what the overdraw is like for this scene. A depth pre-pass helps if you have a lot of overdraw. The second image is the stencil buffer.
GBuffer Pass
LoS2 uses a deferred pipeline, fully populating 4 G-Buffers. 4 buffers is quite big for a game that was released on Xbox360 and PS3, other games get away with 3 by using several optimizations.
Normals (in World Space):
R8 | G8 | B8 | A8 |
Normal.r | Normal.g | Normal.b | SSS |
The normal buffer is populated with the three components of the world space normal and a subsurface scattering term for hair and wax (interestingly not skin). Opaque objects only transform their normal from tangent space to world space, but hair uses some form of normal shifting to give it anisotropic properties.
Albedo:
R8 | G8 | B8 | A8 |
Albedo.r | Albedo.g | Albedo.b | Alpha * AOLevels |
The albedo buffer stores all three albedo components plus an ambient occlusion term that is stored per vertex in the alpha channel of the vertex color and is modulated by an AO constant (which I presume depends on the general lighting of the scene).
Specular:
R8 | G8 | B8 | A8 |
Specular.r | Specular.g | Specular.b | Fresnel Multiplier |
dp3_pp r0.w, r2, r3 //float NdotV = dot(worldViewVector, normalizedWorldNormal);The specular buffer stores the specular color multiplied by a fresnel term that depends on the view and normal vectors. Although LoS2 does not use physically-based rendering, it includes a Fresnel term probably inspired in part by the Schlick approximation to try and brighten things up at glancing angles. It is not strictly correct, as it is done independently of the real-time lights. The Fresnel factor is also stored in the w component.
1 2 |
add_pp r1.w, -r0.w, c13.w //float3 invNdotV = 1 - NdotV; pow r0.w, r1.w, c11.y //float p = pow(invNdotV, FresnelLevels.y); |