czwartek, 26 października 2017

Reverse engineering the rendering of The Witcher 3, part 3 - chromatic aberration


Welcome to the third episode of my mini series where I demystify some rendering techniques from The Witcher 3.

Today we will take a closer look at chromatic aberration.

Chromatic aberration is an effect known mostly from cheaper lenses. It occurs because lenses have different refractive index for different wavelenghts of visible light. The result of this is visible distortion.

Not everyone likes it though. Luckily in The Witcher 3 this effect is very slight and therefore is not disturbing during gameplay  (at least for me). However, you can disable it if you want to.

Let's take a closer look at an example scene with and without chromatic aberration:
Chromatic aberration: on

Chromatic aberration: off
Okay, do you see any difference near the corners? Me neither. Let's try different scene:

Chromatic aberration: On (#2). Notice slight "red" distortion in marked region.

Ah! Much better! There is bigger contrast between dark and bright regions and in the corner we can see slight distortion.

As you can see, this effect is really slight. Anyway, I was curious how this was implemented.
So let's go now to the most interesting part: code!

The first thing to do is to find proper draw call with pixel shader.
Actually, chromatic aberration is part of a bigger "final postprocess" pixel shader, which consists of chromatic aberration, vignette and gamma correction, all in one PS.

So let's take a closer look at pixel shader assembly:
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[18], immediateIndexed  
    dcl_sampler s1, mode_default  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_input_ps_siv v0.xy, position  
    dcl_input_ps linear  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mul r0.xy, v0.xyxx, cb3[17].zwzz  
   1: mad, v0.xxxy, cb3[17].zzzw, -cb3[17].xxxy  
   2: div, r0.zzzw, cb3[17].xxxy  
   3: dp2 r1.x, r0.zwzz, r0.zwzz  
   4: sqrt r1.x, r1.x  
   5: add r1.y, r1.x, -cb3[16].y  
   6: mul_sat r1.y, r1.y, cb3[16].z  
   7: sample_l(texture2d)(float,float,float,float), r0.xyxx, t0.xyzw, s1, l(0)  
   8: lt r1.z, l(0), r1.y  
   9: if_nz r1.z  
  10:  mul r1.y, r1.y, r1.y  
  11:  mul r1.y, r1.y, cb3[16].x  
  12:  max r1.x, r1.x, l(0.000100)  
  13:  div r1.x, r1.y, r1.x  
  14:  mul, r0.zzzw, r1.xxxx  
  15:  mul, r0.zzzw, cb3[17].zzzw  
  16:  mad r0.xy, -r0.zwzz, l(2.000000, 2.000000, 0.000000, 0.000000), r0.xyxx  
  17:  sample_l(texture2d)(float,float,float,float) r2.x, r0.xyxx, t0.xyzw, s1, l(0)  
  18:  mad r0.xy, v0.xyxx, cb3[17].zwzz, -r0.zwzz  
  19:  sample_l(texture2d)(float,float,float,float) r2.y, r0.xyxx, t0.xyzw, s1, l(0)  
  20: endif  

And cbuffer values:

Okay, let's try to understand what's going on here.

cb3_v17.xy is essentialy center of chromatic aberration, so the first lines are essentially calculating 2d vector from texel coords ( = inverse viewport size) to "chromatic aberration center" and its length, then some maths, test and branching.

When chromatic aberration is applied, we calculate offsets using some values from constant buffer and we distort R and G channels.

Generally, the closer to corners of screen, the more intense the effect is. Line 10 is quite an interesting one, because it makes pixels to "come closer", especially when we exaggerate the aberration.

And I'm pleased to share with you with my implementation of it. As always, please take names of variables with (large) grain of salt. And note this effect is done *prior* to gamma correction.

 void ChromaticAberration( float2 uv, inout float3 color )  
   // User-defined params  
   float2 chromaticAberrationCenter = float2(0.5, 0.5);  
   float chromaticAberrationCenterAvoidanceDistance = 0.2;  
   float fA = 1.25;  
   float fChromaticAbberationIntensity = 30;  
   float fChromaticAberrationDistortionSize = 0.75;  
   // Calculate vector  
   float2 chromaticAberrationOffset = uv - chromaticAberrationCenter;  
   chromaticAberrationOffset = chromaticAberrationOffset / chromaticAberrationCenter;  
   float chromaticAberrationOffsetLength = length(chromaticAberrationOffset);  
   // To avoid applying chromatic aberration in center, subtract small value from  
   // just calculated length.  
   float chromaticAberrationOffsetLengthFixed = chromaticAberrationOffsetLength - chromaticAberrationCenterAvoidanceDistance;  
   float chromaticAberrationTexel = saturate(chromaticAberrationOffsetLengthFixed * fA);  
   float fApplyChromaticAberration = (0.0 < chromaticAberrationTexel);  
   if (fApplyChromaticAberration)  
     chromaticAberrationTexel *= chromaticAberrationTexel;  
     chromaticAberrationTexel *= fChromaticAberrationDistortionSize;  
     chromaticAberrationOffsetLength = max(chromaticAberrationOffsetLength, 1e-4);  
     float fMultiplier = chromaticAberrationTexel / chromaticAberrationOffsetLength;  
     chromaticAberrationOffset *= fMultiplier;  
     chromaticAberrationOffset *=;  
     chromaticAberrationOffset *= fChromaticAbberationIntensity;  
     float2 offsetUV = -chromaticAberrationOffset * 2 + uv;  
     color.r = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).r;  
     offsetUV = uv - chromaticAberrationOffset;  
     color.g = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).g;  

I've added "fChromaticAberrationIntensity" to increase size of offset, therefore, intensity of the effect, as name suggets (TW3 = 1.0).

Intensity = 40:

So this is it! I hope you have enjoyed this post.
Stay tuned for more, at least few more effects are waiting to be reverse engineered! :)

Have a good day,

Brak komentarzy:

Prześlij komentarz