piątek, 23 lutego 2018

Reverse engineering the rendering of The Witcher 3, part 4 - vignette

Welcome back! This time we are going to understand and reverse engineer vignette used in The Witcher 3: Wild Hunt.


Vignetting is one of the most widespread postprocessing effects used in games. It's popular in photography as well. Subtly darker corners can produce nice looking effect. There are few types of vignetting. For instance, Unreal Engine 4 uses natural one.

But let's back to The Witcher 3. Click here for interactive comparison to see difference between vignette on/off. It's from The Witcher 3 perf guide from NVIDIA.

Screenshot from The Witcher 3 with enabled vignette.
Please notice that the upper left corner (sky) is not as much darkened as the other parts of image.
I will back to this later.

Implementation details

First of all, there is a minor difference in vignette used in the original version of The Witcher 3 (released May 19, 2015) and The Witcher 3: Blood and Wine. In the former, "inverse gradient" is calculated within pixel shader, while in the latter it was precalculated to 256x256 2d texture:
256x256 texture used as "inverse gradient" in Blood & Wine.
I will use shader from Blood & Wine (btw: great game).
Like in most games, vignette in Witcher 3 is calculated in the final postprocess pixel shader. Let's take a look at assembly:

  44: log r0.xyz, r0.xyzx  
  45: mul r0.xyz, r0.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000)  
  46: exp r0.xyz, r0.xyzx  
  47: mul r1.xyz, r0.xyzx, cb3[9].xyzx  
  48: sample_indexable(texture2d)(float,float,float,float) r0.w, v1.zwzz, t2.yzwx, s2  
  49: log r2.xyz, r1.xyzx  
  50: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)  
  51: exp r2.xyz, r2.xyzx  
  52: dp3 r1.w, r2.xyzx, cb3[6].xyzx  
  53: add_sat r1.w, -r1.w, l(1.000000)  
  54: mul r1.w, r1.w, cb3[6].w  
  55: mul_sat r0.w, r0.w, r1.w  
  56: mad r0.xyz, -r0.xyzx, cb3[9].xyzx, cb3[7].xyzx  
  57: mad r0.xyz, r0.wwww, r0.xyzx, r1.xyzx  

Interesting! Looks like vignette uses both gamma (line 46) and linear (line 51) spaces to calculate.
At line 48 we sample "inverse gradient" texture.

cb3[9].xyz is not related to vignette. In every tested frame it was set to float3(1.0, 1.0, 1.0) so this is probably final filter used in fade-in / fade-out effects.

There are three main parameters for TW3 vignette:
  • Opacity ( cb3[6].w ) - Affects intensity of the vignette. 0 - no vignette, 1 - max vignette. From my observations it looks like in base The Witcher 3 is somewhere around 1.0, while in Blood & Wine it oscillates somewhere 0.15.
  • Color ( cb3[7].xyz ) - The great thing about TW3 vignette is possibility to change color of it. It doesn't have to be black, but in practice.. It's usually set as float3( 3.0 / 255.0, 4.0 / 255.0, 5.0 / 255.0 ) and so on - in general multiplies of 0.00392156 = 1.0/255.0
  • Weights ( cb3[6].xyz ) - This is very interesting parameter. I've always seen "flat" vignette, like this:
Typical vignette mask
          But using weights (line 52) we can get very interesting results:
TW3 Vignette mask calculated using weights
        Weights are close to 1.0. Take a look at frame's constant buffer data from one of frames from Blood&Wine (magic world with rainbow): This is why bright pixels from previously mentioned sky were not really affected by vignette.

The calculated mask is used to interpolate values between image color and vignette's color.


Here is my implementation of TW3 vignette in HLSL.
GammaToLinear = pow(color, 2.2)

 // The Witcher 3 vignette.  
 // Input color is in gamma space  
 // Output color is in gamma space as well.  
 float3 Vignette_TW3( in float3 gammaColor, in float3 vignetteColor, in float3 vignetteWeights,  
                      in float vignetteOpacity, in Texture2D texVignette, in float2 texUV )  
      // Calculate vignette amount based on color in *LINEAR* color space and vignette weights.  
      float vignetteWeight = dot( GammaToLinear( gammaColor ), vignetteWeights );  
      // We need to keep vignette weight in [0-1] range  
      vignetteWeight = saturate( 1.0 - vignetteWeight );  
      // Multiply by opacity  
      vignetteWeight *= vignetteOpacity;  
      // Obtain vignette mask (here is texture; you can also calculate your custom mask here)  
      float sampledVignetteMask = texVignette.Sample( samplerLinearClamp, texUV ).x;  
      // Final (inversed) vignette mask  
      float finalInvVignetteMask = saturate( vignetteWeight * sampledVignetteMask );  
      // final composite in gamma space  
      float3 Color = lerp( gammaColor, vignetteColor, finalInvVignetteMask );
      // * uncomment to debug vignette mask:  
      // return 1.0 - finalInvVignetteMask;  
      // Return final color  
      return Color;  

I hope you like it :) Feel free to comment. You can also try my HLSLexplorer which helped me greatly in understanding HLSL assembly and you can also check my previous posts about Witcher 3 rendering techniques.

As always, please take names of variables with grain of salt - TW3 shaders are processed with D3DStripShader so basically I know almost nothing, it's all about guessing. I am also not responsible for any damages done to you hardware due to this shader ;)

Thanks for reading!

Bonus: Calculating gradient

In The Witcher 3 from 2015 inverse gradient is calculated within pixel shader instead of sampling precalculated texture. Let's take a look at the assembly:
  35: add r2.xy, v1.zwzz, l(-0.500000, -0.500000, 0.000000, 0.000000)  
  36: dp2 r1.w, r2.xyxx, r2.xyxx  
  37: sqrt r1.w, r1.w  
  38: mad r1.w, r1.w, l(2.000000), l(-0.550000)  
  39: mul_sat r2.w, r1.w, l(1.219512)  
  40: mul r2.z, r2.w, r2.w  
  41: mul r2.xy, r2.zwzz, r2.zzzz  
  42: dp4 r1.w, l(-0.100000, -0.105000, 1.120000, 0.090000), r2.xyzw  
  43: min r1.w, r1.w, l(0.940000)  

Luckily for us, this is pretty easy. In HLSL this would be something like this:
 float TheWitcher3_2015_Mask( in float2 uv )  
      float distanceFromCenter = length( uv - float2(0.5, 0.5) );  
      float x = distanceFromCenter * 2.0 - 0.55;  
      x = saturate( x * 1.219512 );          // 1.219512 = 100/82  
      float x2 = x * x;  
      float x3 = x2 * x;  
      float x4 = x2 * x2;  
      float outX = dot( float4(x4, x3, x2, x), float4(-0.10, -0.105, 1.12, 0.09) );  
      outX = min( outX, 0.94 );  
      return outX;  

So we simply calculate distance from center to texel, doing some magic  (multiply, saturate...) with it and then... we calculate polynomial! Awesome.

Brak komentarzy:

Prześlij komentarz