Friday, August 31, 2018

Reverse engineering the rendering of The Witcher 3, part 5 - drunk effect

This post is a part of the series "Reverse engineering the rendering of The Witcher 3".


Hi,

Let's take a look how drunk effect is implemented in The Witcher 3: Wild Hunt.
If you haven't played it yet, drop anything you're doing, buy it and play it see these videos:

Evening:


Night:


At first we see "double rotating" image, pretty common when you're not sober in real life. The more distant the pixel is from the center of image, the rotation effect is stronger. I posted the second video at night on purpose, because you can clearly see this rotation on stars (do you see 8 separate points?)

The second part of TW3 drunk effect, maybe not so visible at first sight, is slight zooming in and out. It's visible near the center.

It's probably obvious that this effect is typical postprocess (pixel shader). However, the order of it in pipeline may not be so obvious. It turns out that drunk effect is applied just *after* tonemapping and just before motion blur (the drunk image is input for motion blur).

Let's start the assembly game:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[2], immediateIndexed  
    dcl_constantbuffer cb3[3], immediateIndexed  
    dcl_sampler s0, mode_default  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_input_ps_siv v1.xy, position  
    dcl_output o0.xyzw  
    dcl_temps 8  
   0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000)  
   1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000)  
   2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx  
   3: dp2 r0.w, r1.xyxx, r1.xyxx  
   4: sqrt r1.z, r0.w  
   5: mul r0.w, r0.w, l(10.000000)  
   6: min r0.w, r0.w, l(1.000000)  
   7: mul r0.w, r0.w, cb3[0].y  
   8: mul r2.xyzw, r0.yzyz, r1.zzzz  
   9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw  
  10: mul r3.xy, r0.xxxx, r1.xyxx  
  11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy  
  12: add r3.xyzw, r3.xyzw, cb3[2].xyxy  
  13: add r2.xyzw, r2.xyzw, cb3[2].xyxy  
  14: mul r0.x, r0.w, cb3[0].x  
  15: mul r0.x, r0.x, l(5.000000)  
  16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw  
  17: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r2.xyzw  
  18: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0  
  19: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0  
  20: add r5.xyzw, r5.xyzw, r6.xyzw  
  21: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r2.xyzw  
  22: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0  
  23: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0  
  24: add r5.xyzw, r5.xyzw, r7.xyzw  
  25: add r5.xyzw, r6.xyzw, r5.xyzw  
  26: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r2.xyzw  
  27: mad r2.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r2.xyzw  
  28: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0  
  29: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0  
  30: add r5.xyzw, r5.xyzw, r7.xyzw  
  31: add r5.xyzw, r6.xyzw, r5.xyzw  
  32: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r2.xyxx, t0.xyzw, s0  
  33: sample_indexable(texture2d)(float,float,float,float) r2.xyzw, r2.zwzz, t0.xyzw, s0  
  34: add r5.xyzw, r5.xyzw, r6.xyzw  
  35: add r2.xyzw, r2.xyzw, r5.xyzw  
  36: mul r2.xyzw, r2.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500)  
  37: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r3.zwzw  
  38: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0  
  39: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0  
  40: add r5.xyzw, r5.xyzw, r6.xyzw  
  41: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r3.zwzw  
  42: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0  
  43: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0  
  44: add r5.xyzw, r5.xyzw, r7.xyzw  
  45: add r5.xyzw, r6.xyzw, r5.xyzw  
  46: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r3.zwzw  
  47: mad r3.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r3.xyzw  
  48: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r6.xyxx, t0.xyzw, s0  
  49: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0  
  50: add r4.xyzw, r4.xyzw, r5.xyzw  
  51: add r4.xyzw, r6.xyzw, r4.xyzw  
  52: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r3.xyxx, t0.xyzw, s0  
  53: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.zwzz, t0.xyzw, s0  
  54: add r4.xyzw, r4.xyzw, r5.xyzw  
  55: add r3.xyzw, r3.xyzw, r4.xyzw  
  56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw  
  57: mul r0.x, cb3[0].y, l(8.000000)  
  58: mul r0.xy, r0.xxxx, cb3[0].zwzz  
  59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000)  
  60: mul r1.zw, r0.zzzz, r1.xxxy  
  61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx  
  62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx  
  63: mul r0.xy, r0.xyxx, r1.zwzz  
  64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx  
  65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0  
  66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0  
  67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0  
  68: add r1.xyzw, r1.xyzw, r3.xyzw  
  69: add r1.xyzw, r4.xyzw, r1.xyzw  
  70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw  
  71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333)  
  72: mul r0.xyzw, r0.wwww, r2.xyzw  
  73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw  
  74: ret  

Two separate constant buffers are being used here. Let's check their values:


Few of them are interesting for us:
cb0_v0.x -> elapsed time (seconds)
cb0_v1.xyzw - viewport & inversed viewport size (aka pixel size)

cb3_v0.x - Rotation around pixel, always set to 1.0.
cb3_v0.y - amount of drunk effect. After triggering it, it does not go on full intensity, but rises from 0.0 to 1.0. This is it.
cv3_v1.xy - pixel offsets (more on this later). This is sin/cos pair, so you can use sincos(time) in shader if you want.
cb3_v2.xy - center of effect, usually float2( 0.5, 0.5 ).

What we want to focus on here is to understand how this works instead of blindly rewriting assembly.

We will start from first lines:

 ps_5_0  
   0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000)  
   1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000)  
   2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx  
   3: dp2 r0.w, r1.xyxx, r1.xyxx  
   4: sqrt r1.z, r0.w  

The "0" line is something i called "zoom factor", you'll see why in a minute.
Right after that (line 1), we calculate "rotation offsets". It's just input sin/cos pair multiplied by 0.05.

Lines 2-4: At first, we calculate vector from effect center to texture uv. Then we calculate it's squared distance (3) and regular distance (4) (from center to texel)

Zoomed texture coordinates


Let's take at following assembly:
   8: mul r2.xyzw, r0.yzyz, r1.zzzz  
   9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw  
  10: mul r3.xy, r0.xxxx, r1.xyxx  
  11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy  
  12: add r3.xyzw, r3.xyzw, cb3[2].xyxy  
  13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 

Since they're packed this way, we can safely analyse only one pair of floats.
For start, r0.yz are "rotation offsets", r1.z is distance from center to texel, r1.xy is vector from center to texel and r0.x is "zoom factor".

To understand it, let zoomFactor = 1.0 for now, so we can write:
   8: mul r2.xyzw, r0.yzyz, r1.zzzz  
   9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw  
  13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 
r2.xy =
(texel - center) * zoomFactor - rotationOffsets * distanceFromCenter + center; But zoomFactor = 1.0: r2.xy = texel - center - rotationOffsets * distanceFromCenter + center; r2.xy = texel - rotationOffsets * distanceFromCenter;

Similarly for r3.xy:
  10: mul r3.xy, r0.xxxx, r1.xyxx  
  11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy  
  12: add r3.xyzw, r3.xyzw, cb3[2].xyxy  

  r3.xy = rotationOffsets * distanceFromCenter + zoomFactor * (texel - center) + center 

  But zoomFactor = 1.0:
  r3.xy = rotationOffsets * distanceFromCenter + texel - center + center
  r3.xy = texel + rotationOffsets * distanceFromCenter

Sweet. So right now we basically have current TextureUV (texel) +/- rotation offsets, but what about zoomFactor? Take a look at line 0.
Basically, zoomFactor = 1.0 - 0.1 * drunkAmount. For maximum drunkAmount, zoomFactor = 0.9 and calculating zoomed texcoords is now:

  baseTexcoordsA = 0.9 * texel + 0.1 * center + rotationOffsets * distanceFromCenter
  baseTexcoordsB = 0.9 * texel + 0.1 * center - rotationOffsets * distanceFromCenter

Or, maybe more intuitive, it's just linear interpolation between normalized texture coordinates and center by some factor. This is to "zoom in" image. The best way to understand it is to play with it, so here is a link to Shadertoy which shows it in action.

Texcoords offset

The whole piece of assembly:
   2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx
   3: dp2 r0.w, r1.xyxx, r1.xyxx  
   5: mul r0.w, r0.w, l(10.000000)  
   6: min r0.w, r0.w, l(1.000000)  
   7: mul r0.w, r0.w, cb3[0].y  
  14: mul r0.x, r0.w, cb3[0].x  
  15: mul r0.x, r0.x, l(5.000000)           // texcoords offset intensity
  16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw     // texcoords offset

produces some sort of gradient, let's call it "offset intensity mask". Actually, it produces two. One in "r0.w" (we will use it later) and second, 5 times stronger, in r0.x (line 15). The latter actually serves as multiplier for texel size, so it affects offset strength.

Sampling - rotation part


Next, a series of texture sampling goes on. There are actually 2 series per 8 samplings, one in each "side". In HLSL we can write this this way:

   static const float2 pointsAroundPixel[8] =
    {
        float2(1.0, 0.0),
        float2(-1.0, 0.0),
        float2(0.707,  0.707),
        float2(-0.707, -0.707),
        float2(0.0,  1.0),
        float2(0.0, -1.0),
        float2(-0.707, 0.707),
        float2(0.707, -0.707)
    };

    float4 colorA = 0;
    float4 colorB = 0;

    int i=0;
    [unroll] for (i = 0; i < 8; i++)
    {
        colorA += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsA + texcoordsOffset * pointsAroundPixel[i] );
    }
    colorA /= 16.0;

    [unroll] for (i = 0; i < 8; i++)
    {
        colorB += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsB + texcoordsOffset * pointsAroundPixel[i] );
    }
    colorB /= 16.0;

    float4 rotationPart = colorA + colorB;

Trick is, we add to baseTexcoordsA/B additional offset lying on unit circle around pixel multiplied by previously mentioned "texcoords offset intensity". The further from center the pixel is, the radius of circle around the pixel is larger - we sample it 8 times, which is well visible on stars. The values of pointsAroundPixel (multiplies of 45 degrees):
from: https://en.wikipedia.org/wiki/Unit_circle

Sampling - zooming in/out part

The second part of drunk effect in The Witcher 3 is zooming "in and out". Let's see assembly responsible for that:

  56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw  // the rotation part is stored in r2 register

  57: mul r0.x, cb3[0].y, l(8.000000)
  58: mul r0.xy, r0.xxxx, cb3[0].zwzz
  59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000)
  60: mul r1.zw, r0.zzzz, r1.xxxy
  61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx
  62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx
  63: mul r0.xy, r0.xyxx, r1.zwzz
  64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx
  65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0
  66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0
  67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0
  68: add r1.xyzw, r1.xyzw, r3.xyzw
  69: add r1.xyzw, r4.xyzw, r1.xyzw

We see that we have three separate texture fetches, so, 3 different texture coordinates. Let's analyse how texcoords for them are calculated. But first, some inputs for this part:
  float  zoomInOutScalePixels = drunkEffectAmount * 8.0; // line 57
  float2 zoomInOutScaleNormalizedScreenCoordinates = zoomInOutScalePixels * texelSize.xy; // line 58
  float  zoomInOutAmplitude = 1.0 + 0.02*cos(time); // line 59
  float2 zoomInOutfromCenterToTexel = zoomInOutAmplitude * fromCenterToTexel; // line 60
Few words about inputs. We calculate offset in texels (e.g. 8.0 * texel size) which is later added to base uv. Amplitude simply oscillates between 0.98 and 1.02 to give "zooming" feeling, like with zoomFactor in rotation part.

Let's start from pair #1, r1.xy (line 61)
  r1.xy = fromCenterToTexel * amplitude + center
  r1.xy = (TextureUV - Center) * amplitude + Center // you can insert here zoomInOutfromCenterToTexel
  r1.xy = TextureUV * amplitude - Center * amplitude + Center
  r1.xy = TextureUV * amplitude + Center * 1.0 - Center * amplitude
  r1.xy = TextureUV * amplitude + Center * (1.0 - amplitude)
  
  r1.xy = lerp( TextureUV, Center, amplitude);
  
  So:
  float2 zoomInOutBaseTextureUV = lerp(TextureUV, Center, amplitude);

Let's check out pair #2, r3.xy (line 62)
  r3.xy = (amplitude * fromCenterToTexel) * zoomInOutScaleNormalizedScreenCoordinates
        + zoomInOutBaseTextureUV

  So:
  float2 zoomInOutAddTextureUV0 = zoomInOutBaseTextureUV
                      + zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates;


Let's check out pair #3, r0.xy (lines 63-64)
  r0.xy = zoomInOutScaleNormalizedScreenCoordinates * (amplitude * fromCenterToTexel) * 2.0 + zoomInOutBaseTextureUV

  So:
  float2 zoomInOutAddTextureUV1 = zoomInOutBaseTextureUV
  + 2.0*zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates
All the three texture fetches are added together, this results is stored in r1 register. It's worth noticing that this pixel shader uses sampler with "clamp" addressing.

Combining all together

So, right now we have result of rotating in r2 register and added 3 fetches of zooming in r1 register. Let's see the end lines of the assembly:
  70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw  
  71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333)  
  72: mul r0.xyzw, r0.wwww, r2.xyzw  
  73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw  
  74: ret  

For additional inputs: r0.w comes from line 7, it's our intensity mask and cb3[0].y is amount of drunk effect.

Let's fiind out how it works.
Okay, my first approach was "brute-force" way:
  float4 finalColor = intensityMask * (rotationPart - zoomingPart);
  finalColor = drunkIntensity * finalColor + zoomingPart;
  
  return finalColor;

But what the heck, nobody writes shaders this way
I took pen & paper and wrote this formula:
  finalColor = effectAmount * [intensityMask * (rotationPart - zoomPart)] + zoomPart
  finalColor = effectAmount * intensityMask * rotationPart - effectAmount * intensityMask * zoomPart + zooomPart

  - Let t = effectAmount * intensityMask
  - So we have:
  finalColor = t * rotationPart - t * zoomPart + zoomPart
  finalColor = t * rotationPart + zoomPart - t * zoomPart
  finalColor = t * rotationPart + (1.0 - t) * zoomPart
  finalColor = lerp( zoomingPart, rotationPart, t )

  - Finally:
  finalColor = lerp(zoomingPart, rotationPart, intensityMask * drunkIntensity);

Phew! That was quite a detailed post but this is over ;)
Personally I have learned something during writing that one and hopefully you too!

The full HLSL source is here if you are interested. I checked it with my HLSLexplorer and although there is no direct 1-1 relation with original shader, the difference is so small (1 line less) that I can safely assume it's working :)

Let me know if you liked it.
Thanks for reading! :)
M.

Tuesday, June 26, 2018

Slides from "Witcher Tricks" are available

Hi,

In case you missed it - you can grab slides from my presentation about rendering in The Witcher 3 from here (in Polish and Engish; also with notes).

The presentation covers general rendering pipeline of a single frame and more detailed description of a few techniques, including storing normals in gbuffer, the Sun, blinking stars, tonemapping and vignette.


Mateusz

Friday, February 23, 2018

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

This post is a part of the series "Reverse engineering the rendering of The Witcher 3".



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

Vignette

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.


Code

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.


Wednesday, February 7, 2018

HLSLexplorer is out!

Hi,

Finally! For the last few months, I've been quite absent.
Now when I have some free time I'm pleased to share with you with first public beta version of HLSLexplorer (internally known as D3DShaderDisassembler) :)

Quick user guide:



In the left window you type some HLSL and in the right you get its assembly once you hit F6.
Most of compilation options are up to you in the lower panel - you can hide/show it with F5.
There is a possibility to save & load presets. To start quickly you can select from "Insert" menu some simple pixel/vertex/compute shader.
You can also load from "File" menu different d3dcompiler dll.

In general, this tool has proved to be really useful for me. I've learned a lot about HLSL assembly and could reverse engineer some shaders from Witcher 3 - check out my older posts if you are interested: tone mappingeye adaptationchromatic aberration.

Download binaries from here: http://bit.ly/2nPFofZ
Source code & github repo will be available soon.

Edit: Check out this post for source code and final version.

One more thing: I'm completely aware that in its current state, HLSLexplorer is far from being usable. I will be very thankful for any feedback / feature requests / suggestions / bug reports.
I hope that at least a few of you will like it.

Cheers!

Thursday, October 26, 2017

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

This post is a part of the series "Reverse engineering the rendering of The Witcher 3".


Hello!

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!

Implementation
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:
 ps_5_0  
    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 v1.zw  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mul r0.xy, v0.xyxx, cb3[17].zwzz  
   1: mad r0.zw, v0.xxxy, cb3[17].zzzw, -cb3[17].xxxy  
   2: div r0.zw, 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) r2.xyz, 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.zw, r0.zzzw, r1.xxxx  
  15:  mul r0.zw, 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 (cb3_v17.zw = 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 *= g_Viewport.zw;  
     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,
M.

Tuesday, October 3, 2017

Reverse engineering the rendering of The Witcher 3, part 2 - eye adaptation

This post is a part of the series "Reverse engineering the rendering of The Witcher 3".


Hi everyone!

Welcome to the second part of my mini series where I demystify some rendering techniques from The Witcher 3. This time it's gonna be much, much simpler than before.

In the first part I showed you how tonemapping is done in TW3. While explaining theoretical basics, I briefly mentioned about eye adaptation. And guess what? Today I'll show how this eye adaptation is handled.

But wait, what is this eye adaptation all about and why do we need that? Wikipedia knows all about this, but let's imagine that you are in dark room (Life is Strange, anyone? :) ) or cave and you go outside, where is bright. The primary source of this brightness can be Sun, for instance.

In darkness our pupils are big to let more light through them to retinas. When it gets brightly, our pupils are becoming smaller and sometimes we blink, because it "hurts".
This change doesn't happen immediately. Eye has to adapt to changes of brightness. This is exactly why we perform eye adaptation in real time rendering.

Good example where lack of eye adaptation is noticeable is HDRToneMappingCS11 from DirectX SDK. Abrupt changes of average luminance are rather unpleasant and unnatural.

Let's get started!
For consistency, we will be analyzing the same frame as before, from Novigrad City.



And now some diving into RenderDoc frame capture. Eye adaptation is usually done just before tonemapping and The Witcher 3 is no exception.

And look at the pixel shader state:


We have 2 inputs - 2 textures, R32_FLOAT, 1x1 (one pixel).
texture0 contains average scene luminance from previous frame.
texture1 contains average scene luminance from current frame (computed just before in compute shader - I marked this in blue color).

Not surprisingly, 1 output, R32_FLOAT, 1x1.

Let's take a look at pixel shader.

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[1], immediateIndexed  
    dcl_sampler s0, mode_default  
    dcl_sampler s1, mode_default  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_output o0.xyzw  
    dcl_temps 1  
   0: sample_l(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw, s1, l(0)  
   1: sample_l(texture2d)(float,float,float,float) r0.y, l(0, 0, 0, 0), t0.yxzw, s0, l(0)  
   2: ge r0.z, r0.y, r0.x  
   3: add r0.x, -r0.y, r0.x  
   4: movc r0.z, r0.z, cb3[0].x, cb3[0].y  
   5: mad o0.xyzw, r0.zzzz, r0.xxxx, r0.yyyy  
   6: ret  

Wow, so easy! Only 7 lines of assembly :)
What is going on here? Explanation line by line:

0) Get average luminance from current frame.
1) Get average luminance from previus frame.
2) Perform a test: Is the current luminance less than or equal to luminance from previous frame?
If yes - luminance is going down, if no - luminance is getting higher.
3) Calculate difference: difference = currentLum - previousLum.
4) This conditional move (movc) assignes speed factor from constant buffer. Depending on the test from line #2, two different values can be assigned. This is smart, because you can have different adaptation speeds for both falling and rising of luminance. But in every single frame I investiagated, both values are the same, ranging from about 0.11 to 0.3.
5) Final calculation of adapted Luminance:
   adaptedLuminance = speedFactor * difference + previousLuminance.
6) End of the shader

Simple enough to implement in HLSL:
 // The Witcher 3 eye adaptation shader  
   
 cbuffer cBuffer : register (b3)  
 {  
   float4 cb3_v0;  
 }
  
 struct VS_OUTPUT_POSTFX  
 {  
   float4 Position                                             : SV_Position;  
 };  
  
 SamplerState samplerPointClamp : register (s0);  
 SamplerState samplerPointClamp2 : register (s1);  
   
 Texture2D TexPreviousAvgLuminance  : register (t0);  
 Texture2D TexCurrentAvgLuminance  : register (t1);  
   
 float4 TW3_EyeAdaptationPS(VS_OUTPUT_POSTFX Input) : SV_TARGET  
 {  
   // Get current and previous luminance.  
   float currentAvgLuminance = TexCurrentAvgLuminance.SampleLevel( samplerPointClamp2, float2(0.0, 0.0), 0 );  
   float previousAvgLuminance = TexPreviousAvgLuminance.SampleLevel( samplerPointClamp, float2(0.0, 0.0), 0 );  
     
   // Scale factor. Can be different for both falling down and rising up of luminance.  
   // It affects speed of adaptation.  
   // Small conditional test is performed here, so different speed can be set differently for both these cases.  
   float adaptationSpeedFactor = (currentAvgLuminance <= previousAvgLuminance) ? cb3_v0.x : cb3_v0.y;  
   
   // Calculate adapted luminance.  
   float adaptedLuminance = lerp( previousAvgLuminance, currentAvgLuminance, adaptationSpeedFactor );  
   return adaptedLuminance;  
 }  

It gives us the same assembly. I would suggest only changing output type to float instead of float4. No need to waste bandwidth.

So this is how eye adaptation is done in Witcher 3. Pretty easy, huh? :)
I hope you enjoyed this post! Stay tuned for more.


Edit - Decemeber 15, 2018
Hi, at the time of writing this post, I haven't recognized HLSL compiler patterns well enough to notice there is no need of writing "difference" and so on.
This is simply a linear interpolation, lerp/mix, you name it.
The way lerp(x, y, s) is performed on HLSL assembly is simply

 x + s(y-x).

And the (y-x) difference has to be stored somewhere.


Have a good day,
M.

PS. Huge thanks to Baldur Karlsson ( Twitter: @baldurk ) for RenderDoc. It simply rocks.

Thursday, September 7, 2017

Reverse engineering the rendering of The Witcher 3, part 1 - tonemapping

This post is a part of the series "Reverse engineering the rendering of The Witcher 3".


Hi!

In most of modern AAA games one of rendering stages you can encounter for sure is tonemapping.
Quick memory refreshment: In real life, there is a pretty huge luminance range, while our computer screens usually have a limited one (8bits per pixel, which gives 0-255). This is where tonemapping comes to party, because it allows to fit wider range of illumination into a limited one. Usually there are two inputs into this process: floating-point HDR image with color values exceeding 1.0 and an average luminance of scene (the latter can be calculated in a few ways, possibly with eye adaptation to simulate human's eye behavior, but this is not important here).

The next (and final) step consists of obtaining an exposure, calculating exposed color and processing it through tonemapping curve. This is where things start to be a bit messy, because new concepts appear, like "white point" and "middle gray". There are at least few popular curves and Matt Pettineo's article "A Closer Look at Tone Mapping" investigates some of them.

To be honest, I've alvays had problems with proper implementation of tonemapping in my code. There are at least a few different examples online which luckily turned out to be helpful... well, to some point. Some of them incorporate HDR luminance/white point/middle gray to account, some do not - which doesn't really help. I wanted to have a "battle-proven" implementation.

Recently I've started messing around rendering of The Witcher 3. This game has some awesome rendering trickery. And it's great, in terms of story/music/gameplay/eveything.


Ah, before I forget! This post is the first of short series which investigates some rendering solutions from The Witcher 3. It absolutely will not be as comprehensive, as Adrian Courrèges's GTA V graphics study, at least for now :)
We'll start by reverse-engineering tonemapping. Let's start!

We will be working on RenderDoc's capture from this frame from one of main quests from Novigrad City. All settings maxed:



After some search, there is a draw call for tonemapping! As I mentioned earlier, there is a HDR color buffer (texture #0, full res) and average luminance of scene (texture #1, 1x1, floating-point, calculated earlier by compute shader).


Let's take a look at pixel shader assembly:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[17], immediateIndexed  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_input_ps_siv v0.xy, position  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw  
   1: max r0.x, r0.x, cb3[4].y  
   2: min r0.x, r0.x, cb3[4].z  
   3: max r0.x, r0.x, l(0.000100)  
   4: mul r0.y, cb3[16].x, l(11.200000)  
   5: div r0.x, r0.x, r0.y  
   6: log r0.x, r0.x  
   7: mul r0.x, r0.x, cb3[16].z  
   8: exp r0.x, r0.x  
   9: mul r0.x, r0.y, r0.x  
  10: div r0.x, cb3[16].x, r0.x  
  11: ftou r1.xy, v0.xyxx  
  12: mov r1.zw, l(0, 0, 0, 0)  
  13: ld_indexable(texture2d)(float,float,float,float) r0.yzw, r1.xyzw, t0.wxyz  
  14: mul r0.xyz, r0.yzwy, r0.xxxx  
  15: mad r1.xyz, cb3[7].xxxx, r0.xyzx, cb3[7].yyyy  
  16: mul r2.xy, cb3[8].yzyy, cb3[8].xxxx  
  17: mad r1.xyz, r0.xyzx, r1.xyzx, r2.yyyy  
  18: mul r0.w, cb3[7].y, cb3[7].z  
  19: mad r3.xyz, cb3[7].xxxx, r0.xyzx, r0.wwww  
  20: mad r0.xyz, r0.xyzx, r3.xyzx, r2.xxxx  
  21: div r0.xyz, r0.xyzx, r1.xyzx  
  22: mad r0.w, cb3[7].x, l(11.200000), r0.w  
  23: mad r0.w, r0.w, l(11.200000), r2.x  
  24: div r1.x, cb3[8].y, cb3[8].z  
  25: add r0.xyz, r0.xyzx, -r1.xxxx  
  26: max r0.xyz, r0.xyzx, l(0, 0, 0, 0)  
  27: mul r0.xyz, r0.xyzx, cb3[16].yyyy  
  28: mad r1.y, cb3[7].x, l(11.200000), cb3[7].y  
  29: mad r1.y, r1.y, l(11.200000), r2.y  
  30: div r0.w, r0.w, r1.y  
  31: add r0.w, -r1.x, r0.w  
  32: max r0.w, r0.w, l(0)  
  33: div o0.xyz, r0.xyzx, r0.wwww  
  34: mov o0.w, l(1.000000)  
  35: ret  

Some things to notice here. First of all, the loaded luminance does not have to be the used one, as it is being clamped (max/min calls) to values (from constant buffer) set by artists. This is handy, because it prevents overexposing or underexposing our scene. Sounds pretty obvious, but I've never done this before. And second - anyone familiar with tonemapping curves will quickly recognize this "11.2", as it is essentialy white point value from John Hable's Uncharted2 tonemapping curve.
A-F params are loaded from cbuffer.
Okay, there are also three more parameters: cb3_v16.x, cb3_v16.y, cb3_v16.z. We can investigate their values:

Some guessing:
I think the 'x' is some sort of 'white scale' or middle gray, as it is multiplied by 11.2 (line 4), and then this is numerator in calculation of exposure adjustment (line 10).
'y' - I called it "u2 numerator multiplier", you'll see why in a moment.
'z' - "exponent param", as it is used in log/mul/exp triple (essentialy exponentiation).
Please take these variable names with a grain of salt!

Also:
cb3_v4.yz - min/max values of allowed luminance,
cb3_v7.xyz - A-C params of Uncharted2 curve,
cb3_v8.xyz - D-F params of Uncharted2 curve.


Now the hard part - writing HLSL shader with will give us exactly the same assembly.
This can be very tricky, and the longer shader = the harder this task is. Luckily, some time ago I've written a tool which allows me to quickly view hlsl->asm.
Ladies and gentlemen... please give a warm welcome to D3DShaderDisassembler! :)



After some playing with code, here is the final "The Witcher 3 Tonemapping" HLSL:

 cbuffer cBuffer : register (b3)  
 {  
   float4 cb3_v0;  
   float4 cb3_v1;  
   float4 cb3_v2;  
   float4 cb3_v3;  
   float4 cb3_v4;  
   float4 cb3_v5;  
   float4 cb3_v6;  
   float4 cb3_v7;  
   float4 cb3_v8;  
   float4 cb3_v9;  
   float4 cb3_v10;  
   float4 cb3_v11;  
   float4 cb3_v12;  
   float4 cb3_v13;  
   float4 cb3_v14;  
   float4 cb3_v15;  
   float4 cb3_v16, cb3_v17;  
 }  
   
 Texture2D     TexHDRColor          : register (t0);  
 Texture2D     TexAvgLuminance     : register (t1);  
   
 struct VS_OUTPUT_POSTFX  
 {  
   float4 Position : SV_Position;  
 };  
   
 float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x )  
 {  
      return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F;  
 }  
   
 float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier )  
 {  
      float3 numerator =  U2Func( A, B, C, D, E, F, color );  
      numerator = max( numerator, 0 );  
      numerator.rgb *= numMultiplier;  
   
      float3 denominator = U2Func( A, B, C, D, E, F, 11.2 );  
      denominator = max( denominator, 0 );  
   
      return numerator / denominator;  
 }  
   
   
   
 float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0  
 {  
      float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) );  
      avgLuminance = clamp( avgLuminance, cb3_v4.y, cb3_v4.z );  
      avgLuminance = max( avgLuminance, 1e-4 );  
   
      float scaledWhitePoint = cb3_v16.x * 11.2;  
   
      float luma = avgLuminance / scaledWhitePoint;  
      luma = pow( luma, cb3_v16.z );  
   
      luma = luma * scaledWhitePoint;  
      luma = cb3_v16.x / luma;  
   
      float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb;  
   
      float3 color = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y,   
         cb3_v8.z, luma*HDRColor, cb3_v16.y);  
   
      return float4(color, 1);  
 }  


And a quick screenshot from my tool to prove it:

VoilĂ ! :)
I believe this is quite proper implementation of TW3 Tonemapping, at least in terms of assembly.
I already have this in my framework and it works well! Stay tuned for more!

I said "quite", because I have no heck idea why denominator in ToneMapU2Func is maxed with zero. Division by 0 is undefined, right?


Well... we could end right now, but quite accidentally I've found another variant of tonemapping shader in TW3 at this frame, at beautiful dusk (interestingly, minimum graphics settings!)


Let's check this out. At first, shader assembly:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[18], immediateIndexed  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_input_ps_siv v0.xy, position  
    dcl_output o0.xyzw  
    dcl_temps 5  
   0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw  
   1: max r0.y, r0.x, cb3[9].y  
   2: max r0.x, r0.x, cb3[4].y  
   3: min r0.x, r0.x, cb3[4].z  
   4: min r0.y, r0.y, cb3[9].z  
   5: max r0.xy, r0.xyxx, l(0.000100, 0.000100, 0.000000, 0.000000)  
   6: mul r0.z, cb3[17].x, l(11.200000)  
   7: div r0.y, r0.y, r0.z  
   8: log r0.y, r0.y  
   9: mul r0.y, r0.y, cb3[17].z  
  10: exp r0.y, r0.y  
  11: mul r0.y, r0.z, r0.y  
  12: div r0.y, cb3[17].x, r0.y  
  13: ftou r1.xy, v0.xyxx  
  14: mov r1.zw, l(0, 0, 0, 0)  
  15: ld_indexable(texture2d)(float,float,float,float) r1.xyz, r1.xyzw, t0.xyzw  
  16: mul r0.yzw, r0.yyyy, r1.xxyz  
  17: mad r2.xyz, cb3[11].xxxx, r0.yzwy, cb3[11].yyyy  
  18: mul r3.xy, cb3[12].yzyy, cb3[12].xxxx  
  19: mad r2.xyz, r0.yzwy, r2.xyzx, r3.yyyy  
  20: mul r1.w, cb3[11].y, cb3[11].z  
  21: mad r4.xyz, cb3[11].xxxx, r0.yzwy, r1.wwww  
  22: mad r0.yzw, r0.yyzw, r4.xxyz, r3.xxxx  
  23: div r0.yzw, r0.yyzw, r2.xxyz  
  24: mad r1.w, cb3[11].x, l(11.200000), r1.w  
  25: mad r1.w, r1.w, l(11.200000), r3.x  
  26: div r2.x, cb3[12].y, cb3[12].z  
  27: add r0.yzw, r0.yyzw, -r2.xxxx  
  28: max r0.yzw, r0.yyzw, l(0, 0, 0, 0)  
  29: mul r0.yzw, r0.yyzw, cb3[17].yyyy  
  30: mad r2.y, cb3[11].x, l(11.200000), cb3[11].y  
  31: mad r2.y, r2.y, l(11.200000), r3.y  
  32: div r1.w, r1.w, r2.y  
  33: add r1.w, -r2.x, r1.w  
  34: max r1.w, r1.w, l(0)  
  35: div r0.yzw, r0.yyzw, r1.wwww  
  36: mul r1.w, cb3[16].x, l(11.200000)  
  37: div r0.x, r0.x, r1.w  
  38: log r0.x, r0.x  
  39: mul r0.x, r0.x, cb3[16].z  
  40: exp r0.x, r0.x  
  41: mul r0.x, r1.w, r0.x  
  42: div r0.x, cb3[16].x, r0.x  
  43: mul r1.xyz, r1.xyzx, r0.xxxx  
  44: mad r2.xyz, cb3[7].xxxx, r1.xyzx, cb3[7].yyyy  
  45: mul r3.xy, cb3[8].yzyy, cb3[8].xxxx  
  46: mad r2.xyz, r1.xyzx, r2.xyzx, r3.yyyy  
  47: mul r0.x, cb3[7].y, cb3[7].z  
  48: mad r4.xyz, cb3[7].xxxx, r1.xyzx, r0.xxxx  
  49: mad r1.xyz, r1.xyzx, r4.xyzx, r3.xxxx  
  50: div r1.xyz, r1.xyzx, r2.xyzx  
  51: mad r0.x, cb3[7].x, l(11.200000), r0.x  
  52: mad r0.x, r0.x, l(11.200000), r3.x  
  53: div r1.w, cb3[8].y, cb3[8].z  
  54: add r1.xyz, -r1.wwww, r1.xyzx  
  55: max r1.xyz, r1.xyzx, l(0, 0, 0, 0)  
  56: mul r1.xyz, r1.xyzx, cb3[16].yyyy  
  57: mad r2.x, cb3[7].x, l(11.200000), cb3[7].y  
  58: mad r2.x, r2.x, l(11.200000), r3.y  
  59: div r0.x, r0.x, r2.x  
  60: add r0.x, -r1.w, r0.x  
  61: max r0.x, r0.x, l(0)  
  62: div r1.xyz, r1.xyzx, r0.xxxx  
  63: add r0.xyz, r0.yzwy, -r1.xyzx  
  64: mad o0.xyz, cb3[13].xxxx, r0.xyzx, r1.xyzx  
  65: mov o0.w, l(1.000000)  
  66: ret  
   

It may look intimidating at first, but actually it's not that bad. After a quick analysis we can notice that there are 2 calls to Uncharted2 func with different sets of input data
(A-F, min/max luminance...). I haven't encountered such solution before.

And HLSL:
 cbuffer cBuffer : register (b3)  
 {  
   float4 cb3_v0;  
   float4 cb3_v1;  
   float4 cb3_v2;  
   float4 cb3_v3;  
   float4 cb3_v4;  
   float4 cb3_v5;  
   float4 cb3_v6;  
   float4 cb3_v7;  
   float4 cb3_v8;  
   float4 cb3_v9;  
   float4 cb3_v10;  
   float4 cb3_v11;  
   float4 cb3_v12;  
   float4 cb3_v13;  
   float4 cb3_v14;  
   float4 cb3_v15;  
   float4 cb3_v16, cb3_v17;  
 }  
   
 Texture2D     TexHDRColor     : register (t0);  
 Texture2D     TexAvgLuminance     : register (t1);  
   
 float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x )  
 {  
      return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F;  
 }  
   
 float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier )  
 {  
      float3 numerator =  U2Func( A, B, C, D, E, F, color );  
      numerator = max( numerator, 0 );  
      numerator.rgb *= numMultiplier;  
   
      float3 denominator = U2Func( A, B, C, D, E, F, 11.2 );  
      denominator = max( denominator, 0 );  
   
      return numerator / denominator;  
 }  
   
 struct VS_OUTPUT_POSTFX  
 {  
   float4 Position : SV_Position;  
 };  
   
 float getExposure(float avgLuminance, float minLuminance, float maxLuminance, float middleGray, float powParam)  
 {  
      avgLuminance = clamp( avgLuminance, minLuminance, maxLuminance );  
      avgLuminance = max( avgLuminance, 1e-4 );  
   
      float scaledWhitePoint = middleGray * 11.2;  
   
      float luma = avgLuminance / scaledWhitePoint;  
      luma = pow( luma, powParam);  
   
      luma = luma * scaledWhitePoint;  
      float exposure = middleGray / luma;  
      return exposure;  
 }  
   
 float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0  
 {  
      float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) );  
     
   
      float exposure1 = getExposure( avgLuminance, cb3_v9.y, cb3_v9.z, cb3_v17.x, cb3_v17.z);  
      float exposure2 = getExposure( avgLuminance, cb3_v4.y, cb3_v4.z, cb3_v16.x, cb3_v16.z);  
   
        
      float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb;  
   
      float3 color1 = ToneMapU2Func( cb3_v11.x, cb3_v11.y, cb3_v11.z, cb3_v12.x, cb3_v12.y,   
         cb3_v12.z, exposure1*HDRColor, cb3_v17.y);  
   
      float3 color2 = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y,   
         cb3_v8.z, exposure2*HDRColor, cb3_v16.y);  
      
      float3 finalColor = lerp( color2, color1, cb3_v13.x ); 
      return float4(finalColor, 1);  
 }  
   

So basically we have 2 sets of control params, then calculate two tonemapped colors and at the end we interpolate them. Smart!

Feel free to comment, maybe there's something I have missed.
I hope you enjoyed this post :)

Have a good day,
M.