Saturday, December 15, 2018

Reverse engineering the rendering of The Witcher 3, part 7b - average luminance (calculation)

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


Welcome,

This is the second part of demystifying calculating of average luminance in "The Witcher 3: Wild Hunt". Being familiar with the first part is highly recommended.

Before we start the battle with another compute shader, let's do a quick recap of what happened previously: We were working on 1/4x1/4 downscaled HDR color buffer. What we have after the first pass is histogram of luminance (structured buffer of 256 unsigned integers). We calculated logarithm of each pixel's luma, distributed it across 256 cells and increased corresponding value of structured buffer by 1 per pixel. This way, total sum of all values in these 256 cells is equal to the number of pixels.

Example output of the first pass. There are 256 elements here.
For instance, our fullscreen buffer is 1920x1080. After downscaling, the first pass used 480x270 buffer. Sum of all 256 values in the buffer would be 480 * 270 = 129 600.

After this brief introduction, we're all ready to go to the next stage: calculation.
This time only one thread group is dispatched ( Dispatch(1, 1, 1) ).

Let's see the assembly of the compute shader:
 cs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[1], immediateIndexed  
    dcl_uav_structured u0, 4  
    dcl_uav_typed_texture2d (float,float,float,float) u1  
    dcl_input vThreadIDInGroup.x  
    dcl_temps 4  
    dcl_tgsm_structured g0, 4, 256  
    dcl_thread_group 64, 1, 1  
   0: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, vThreadIDInGroup.x, l(0), u0.xxxx  
   1: store_structured g0.x, vThreadIDInGroup.x, l(0), r0.x  
   2: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)  
   3: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.w, r0.x, l(0), u0.xxxx  
   4: store_structured g0.x, r0.x, l(0), r0.w  
   5: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.y, l(0), u0.xxxx  
   6: store_structured g0.x, r0.y, l(0), r0.x  
   7: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.z, l(0), u0.xxxx  
   8: store_structured g0.x, r0.z, l(0), r0.x  
   9: sync_g_t  
  10: if_z vThreadIDInGroup.x  
  11:  mul r0.x, cb0[0].y, cb0[0].x  
  12:  ftou r0.x, r0.x  
  13:  utof r0.y, r0.x  
  14:  mul r0.yz, r0.yyyy, cb0[0].zzwz  
  15:  ftoi r0.yz, r0.yyzy  
  16:  iadd r0.x, r0.x, l(-1)  
  17:  imax r0.y, r0.y, l(0)  
  18:  imin r0.y, r0.x, r0.y  
  19:  imax r0.z, r0.y, r0.z  
  20:  imin r0.x, r0.x, r0.z  
  21:  mov r1.z, l(-1)  
  22:  mov r2.xyz, l(0, 0, 0, 0)  
  23:  loop  
  24:   breakc_nz r2.x  
  25:   ld_structured r0.z, r2.z, l(0), g0.xxxx  
  26:   iadd r3.x, r0.z, r2.y  
  27:   ilt r0.z, r0.y, r3.x  
  28:   iadd r3.y, r2.z, l(1)  
  29:   mov r1.xy, r2.yzyy  
  30:   mov r3.z, r2.x  
  31:   movc r2.xyz, r0.zzzz, r1.zxyz, r3.zxyz  
  32:  endloop  
  33:  mov r0.w, l(-1)  
  34:  mov r1.yz, r2.yyzy  
  35:  mov r1.xw, l(0, 0, 0, 0)  
  36:  loop  
  37:   breakc_nz r1.x  
  38:   ld_structured r2.x, r1.z, l(0), g0.xxxx  
  39:   iadd r1.y, r1.y, r2.x  
  40:   utof r2.x, r2.x  
  41:   utof r2.w, r1.z  
  42:   add r2.w, r2.w, l(0.500000)  
  43:   mul r2.w, r2.w, l(0.011271)  
  44:   exp r2.w, r2.w  
  45:   add r2.w, r2.w, l(-1.000000)  
  46:   mad r3.z, r2.x, r2.w, r1.w  
  47:   ilt r2.x, r0.x, r1.y  
  48:   iadd r2.w, -r2.y, r1.y  
  49:   itof r2.w, r2.w  
  50:   div r0.z, r3.z, r2.w  
  51:   iadd r3.y, r1.z, l(1)  
  52:   mov r0.y, r1.z  
  53:   mov r3.w, r1.x  
  54:   movc r1.xzw, r2.xxxx, r0.wwyz, r3.wwyz  
  55:  endloop  
  56:  store_uav_typed u1.xyzw, l(0, 0, 0, 0), r1.wwww  
  57: endif  
  58: ret  

There is one constant buffer:


Quick look at the assembly: There are two UAVs attached ( u0: input buffer from the first part
and u1: output 1x1 R32_FLOAT texture). We can see that we also have 64 threads per group and 256 elements of 4-bytes groupshared memory.

We start by filling groupshared memory with data from input buffer. We have 64 threads, so we can do it pretty much the same way as before.
To be absolutely sure all data have been loaded for futher processing, we set a barrier after that.
   // The first step is to set whole shared data with data from previous stage.  
   // Because each thread group has 64 threads, each one can fill 4 elements in one thread  
   // using a simple offset.  
   [unroll] for (uint idx=0; idx < 4; idx++)  
   {  
     const uint offset = threadID + idx*64;  
     shared_data[ offset ] = g_buffer[offset];  
   }  
   // We set a barrier here, which means we block execution of all threads in a group until all group   
   // shared accesses have been completed and all threads in the group have reached this call.  
   GroupMemoryBarrierWithGroupSync();  

All calculatons take part in one thread only, all the other ones are used just to load values from buffer to shared memory.
The 'calculating' thread has index of zero. Why? In theory, we could use any thread from [0-63] range, but by comparing with 0, we can avoid extra integer-integer comparison (ieq instruction).

The algorithm is based on specifying range of pixels which will be taken into consideration.
At line 11, we multiply width*height, getting total number of pixels and multiply them by two numbers from [0.0f-1.0f] range which indicate start and end of range. There are some clamps later to make sure that 0 <= Start <= End <= totalPixels - 1:
   // Perform calculations only with the thread with '0' index.  
   [branch] if (threadID == 0)  
   {  
     // Total number of pixels in downscaled buffer  
     uint totalPixels = cb0_v0.x * cb0_v0.y;  

     // Range of pixels (or, more specifically, range of luminance in the screen)
     // we want to incorporate in average luminance calculation.
     int pixelsToConsiderStart = totalPixels * cb0_v0.z;    
     int pixelsToConsiderEnd =  totalPixels * cb0_v0.w;  

     int pixelsMinusOne = totalPixels - 1;  

     pixelsToConsiderStart = clamp( pixelsToConsiderStart, 0, pixelsMinusOne );  
     pixelsToConsiderEnd =  clamp( pixelsToConsiderEnd, pixelsToConsiderStart, pixelsMinusOne );  


As you can see, there are two loops later. The problem with them (or, their assembly) is they have strange conditional moves at the ends. I had a hard time with reconstructing them. Also, take note at line 21. Why is there "-1"? I will reveal it in a few moments.


The purpose of the first loop is to omit pixelsToConsiderStart and give us index of buffer cell in which pixelsToConsiderStart +1 pixel is present (and also number of all pixels in previous cells).

For instance, let's assume that pixelsToConsiderStart is about 30000 and in buffer there are 37000 pixels in cell "zero" (happens during night in the game). So, we want to start analyzing luminance from pixel ~30001, which is present in cell zero. In this scenario, we will exit the loop immediately, having starting index '0' and zero ommitted pixels.

Take a look at the HLSL code:
     // Number of already processed pixels  
     int numProcessedPixels = 0;  
   
     // Luma cell [0-255]  
     int lumaValue = 0;   
   
     // Whether to continue execution of loop  
     bool bExitLoop = false;  
     
     // The purpose of the first loop is to omit "pixelsToConsiderStart" pixels.  
     // We keep number of omitted pixels from previous cells and lumaValue to use in the next loop.  
     [loop]  
     while (!bExitLoop)  
     {  
       // Get number of pixels with specific luma value.  
       uint numPixels = shared_data[lumaValue];  
   
       // Check how many pixels we would have with lumaValue  
       int tempSum = numProcessedPixels + numPixels;  
         
       // If more than pixelsToConsiderStart, exit the loop.  
       // Therefore, we will start calculating luminance from lumaValue.  
       // Simply speaking, pixelsToConsiderStart is number of "darken" pixels to omit before starting calculation.  
       [flatten]  
       if (tempSum > pixelsToConsiderStart)  
       {  
         bExitLoop = true;  
       }  
       else  
       {  
         numProcessedPixels = tempSum;  
         lumaValue++;  
       }  
     }  

This mysterious "-1" from line 21 of the assembly is related with boolean condition of  loop execution (I found it out quite accidentally).

Having the number of pixels from lumaValue cells and lumaValue itself we can go the second loop.
The purpose of the second loop is to calculate contribution of pixels and average luminance.
We start from lumaValue calculated in the first loop.

     float finalAvgLuminance = 0.0f;  
   
     // Number of omitted pixels in the first loop  
     uint numProcessedPixelStart = numProcessedPixels;  
      
     // The purpose of this loop is to calculate contribution of pixels and average luminance.  
     // We start from point calculated in the previous loop, keeping number of omitted pixels and starting lumaValue positon.  
     // We decode luma value from [0-255] range, multiply it by number of pixels which have this specific luma, and sum it up until   
     // we process pixelsToConsiderEnd pixels.   
     // After that, we divide total contribution by number of analyzed pixels.  
     bExitLoop = false;  
     [loop]  
     while (!bExitLoop)  
     {  
       // Get number of pixels with specific luma value.  
       uint numPixels = shared_data[lumaValue];  
         
       // Add to all processed pixels  
       numProcessedPixels += numPixels;  
   
       // Currently processed luma, distributed in [0-255] range (uint)  
       uint encodedLumaUint = lumaValue;  
   
       // Number of pixels with currently processed luma  
       float numberOfPixelsWithCurrentLuma = numPixels;  
   
       // Currently processed, encoded [0-255] luma (float)  
       float encodedLumaFloat = encodedLumaUint;  

At this point we have encoded luma value here [0.0f-255.f].
The decoding process is quite simple - we have to revert calculations from encoding stage.

A quick recap of encoding process:
 float luma = dot( hdrPixelColor, float3(0.2126, 0.7152, 0.0722) ); 
 ...
 float outLuma;          
   
 // because log(0) is undef and log(1) = 0  
 outLuma = luma + 1.0;  
   
 // logarithmically distribute   
 outLuma = log( outLuma );  
   
  // scale by 128, which means log(1) * 128 = 0, log(2,71828) * 128 = 128, log(7,38905) * 128 = 256  
 outLuma = outLuma * 128     
   
 // to uint  
 uint outLumaUint = min( (uint) outLuma, 255);  

To decode luma, we simply revert the encoding process, for example like this:
 // we start by adding 0.5f (we don't want to have zero result)  
 float fDecodedLuma = encodedLumaFloat + 0.5;  
 
 // and decode luminance:
   
 // Divide by 128  
 fDecodedLuma /= 128.0;   
   
 // exp(x) which cancels log(x)         
 fDecodedLuma = exp(fDecodedLuma);  
   
 // Subtract 1.0  
 fDecodedLuma -= 1.0;        


The we calculate contribution by multiplying the number of pixels which have this specific luma times decoded luma, and sum it up until we process pixelsToConsiderEnd pixels.
After that, we divide total contribution by number of analyzed pixels.

See the rest of the loop (and shader):
   // Calculate contribution of this luma  
   float fCurrentLumaContribution = numberOfPixelsWithCurrentLuma * fDecodedLuma;  
         
   // (Temporary) contribution from all previous passes and current one.  
   float tempTotalContribution = fCurrentLumaContribution + finalAvgLuminance;  
     
   
   [flatten]   
   if (numProcessedPixels > pixelsToConsiderEnd )  
   {  
     // to exit the loop  
     bExitLoop = true;  
   
     // We already processed all pixels we wanted, so perform final division here.  
     // Number of all processed pixels from user-selected start  
     int diff = numProcessedPixels - numProcessedPixelStart;  
   
     // Calculate final average luminance  
     finalAvgLuminance = tempTotalContribution / float(diff);  
   }  
   else  
   {      
     // Pass current contribution further and increase lumaValue  
     finalAvgLuminance = tempTotalContribution;   
     lumaValue++;  
   }        
 }  
   
 // Save average luminance  
 g_avgLuminance[uint2(0,0)] = finalAvgLuminance;  

The full shader is here, completely compatible with my HLSLexplorer which was crucial for me to efficiently reconstruct calculating average luminance from The Witcher 3 (well, all the other effects too!).

Phew.... some thoughts at the end. In terms of calculating average luminance, that was difficult shader to reconstruct. Main reasons:
1) strange 'deferred' checks of loop executing, it took me much more time than I initially assumed,
2) Problems with debugging this compute shader with RenderDoc (v. 1.2).
"ld_structured_indexable" operations are not completely supported, while the result of reading from index 0 is fine, all the other ones give zeroes - which makes the loops going to infinity and beyond.

Although I haven't managed to get the same assembly as the original (see screenshot with difference below), I managed to inject this shader with help of RenderDoc into the pipeline and - guess what - the output result was the same! :)
The result of the battle. Left - my shader, right- original assembly.


I hope you enjoyed it,
Thanks for reading.
M.

Thursday, December 13, 2018

Reverse engineering the rendering of The Witcher 3, part 7a - average luminance (histogram/distribution)

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


Welcome,

Calculating average luminance of current frame can be found in virtually any modern video game. Such value is often used later by eye adaptation and tonemapping. Simple approaches include calculating luma to, let's say, 5122 texture and calculating its mips and using the last one. This usually works, but is quite limiting. More sophisticated solutions use compute shaders in order to perform, for instance, parallel reduction.

Let's see how this problem was approached by CD Projekt Red in The Witcher 3. I've already investigated its tonemapping and eye adaptation (links in the first paragraph) before and average luminance is the only piece of puzzle missing so far.

To start, calculating average luminance in The Witcher 3 consists of two passes. I decided not to combine them in one post for clarity, so today I will focus on the first one - "distribution of luminance" (calculating histogram of brightness). For the second part, click here to read it.

Finding these two passes shouldn't be too difficult in your favourite frame analyzer. They are subsequent Dispatch calls, just before eye adaptation:



Let's see the inputs for this pass. There are two textures needed:
1) HDR color buffer, downscaled to 1/4 x 1/4 (for example, from 1920x1080 to 480x270),
2) Fullscreen depth buffer

HDR color buffer at 1/4 x 1/4 resolution. Notice nice trick that this buffer is a part of larger one. Reusing buffers is defnitely  a good thing.

Fullscreen depth buffer
Why downscaling color buffer? I guess it's probably all about performance :)

In terms of output for this pass, there is a structured buffer. 256 elements per 4 bytes each.
Shaders have no debug info here, so let's assume it's just a buffer of unsigned ints.

Important: The first step of calculating average luminance is calling ClearUnorderedAccessViewUint to zero all elements of the structured buffer.

Let's see assembly for compute shader (this is the first compute shader in the series!)

 cs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[3], immediateIndexed  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_uav_structured u0, 4  
    dcl_input vThreadGroupID.x  
    dcl_input vThreadIDInGroup.x  
    dcl_temps 6  
    dcl_tgsm_structured g0, 4, 256  
    dcl_thread_group 64, 1, 1  
   0: store_structured g0.x, vThreadIDInGroup.x, l(0), l(0)  
   1: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)  
   2: store_structured g0.x, r0.x, l(0), l(0)  
   3: store_structured g0.x, r0.y, l(0), l(0)  
   4: store_structured g0.x, r0.z, l(0), l(0)  
   5: sync_g_t  
   6: ftoi r1.x, cb0[2].z  
   7: mov r2.y, vThreadGroupID.x  
   8: mov r2.zw, l(0, 0, 0, 0)  
   9: mov r3.zw, l(0, 0, 0, 0)  
  10: mov r4.yw, l(0, 0, 0, 0)  
  11: mov r1.y, l(0)  
  12: loop  
  13:  utof r1.z, r1.y  
  14:  ge r1.z, r1.z, cb0[0].x  
  15:  breakc_nz r1.z  
  16:  iadd r2.x, r1.y, vThreadIDInGroup.x  
  17:  utof r1.z, r2.x  
  18:  lt r1.z, r1.z, cb0[0].x  
  19:  if_nz r1.z  
  20:   ld_indexable(texture2d)(float,float,float,float) r5.xyz, r2.xyzw, t0.xyzw  
  21:   dp3 r1.z, r5.xyzx, l(0.212600, 0.715200, 0.072200, 0.000000)  
  22:   imul null, r3.xy, r1.xxxx, r2.xyxx  
  23:   ld_indexable(texture2d)(float,float,float,float) r1.w, r3.xyzw, t1.yzwx  
  24:   eq r1.w, r1.w, cb0[2].w  
  25:   and r1.w, r1.w, cb0[2].y  
  26:   add r2.x, -r1.z, cb0[2].x  
  27:   mad r1.z, r1.w, r2.x, r1.z  
  28:   add r1.z, r1.z, l(1.000000)  
  29:   log r1.z, r1.z  
  30:   mul r1.z, r1.z, l(88.722839)  
  31:   ftou r1.z, r1.z  
  32:   umin r4.x, r1.z, l(255)  
  33:   atomic_iadd g0, r4.xyxx, l(1)  
  34:  endif  
  35:  iadd r1.y, r1.y, l(64)  
  36: endloop  
  37: sync_g_t  
  38: ld_structured r1.x, vThreadIDInGroup.x, l(0), g0.xxxx  
  39: mov r4.z, vThreadIDInGroup.x  
  40: atomic_iadd u0, r4.zwzz, r1.x  
  41: ld_structured r1.x, r0.x, l(0), g0.xxxx  
  42: mov r0.w, l(0)  
  43: atomic_iadd u0, r0.xwxx, r1.x  
  44: ld_structured r0.x, r0.y, l(0), g0.xxxx  
  45: atomic_iadd u0, r0.ywyy, r0.x  
  46: ld_structured r0.x, r0.z, l(0), g0.xxxx  
  47: atomic_iadd u0, r0.zwzz, r0.x  
  48: ret  

And constant buffer:


We know already that the first input is downscaled HDR color buffer. For FullHD, its resolution is 480x270. Take a look at Dispatch call.
Dispatch(270, 1, 1) - that means we run 270 thread groups. Simply speaking, we dispatch one thread group per one row of color buffer.

Each thread group performs on one row of HDR color buffer
Now when we have this context, let's try to figure out what this shader does.
Each thread group has 64 threads in X direction (dcl_thread_group 64, 1, 1) and also some shared memory, 256 elements, 4 bytes per each (dcl_tgsm_structured g0, 4, 256).

Note that in the shader we use SV_GroupThreadID (vThreadIDInGroup.x) [0-63] and SV_GroupID (vThreadGroupID.x) [0-269].

1) We start by setting all elements of shared memory to zero. Since we have 256 elements in shared memory, and 64 threads per group, we can do it nicely with simple loop:

   // The first step is to set whole shared data to zero.  
   // Because each thread group has 64 threads, each one can zero 4 elements using a simple offset.  
   [unroll] for (uint idx=0; idx < 4; idx++)  
   {  
     const uint offset = threadID + idx*64;  
     shared_data[ offset ] = 0;  
   }  

2) After that, we set a barrier with GroupMemoryBarrierWithGroupSync (sync_g_t). We do it to make sure all threads set elements of groupshared memory to zero before going to the next stage.

3) Now we perform loop which we can roughly write like this:
  // cb0_v0.x is width of downscaled color buffer. For 1920x1080, it's 1920/4 = 480;  
   float ViewportSizeX = cb0_v0.x;  
   [loop] for ( uint PositionX = 0; PositionX < ViewportSizeX; PositionX += 64 )  
   {  
      ...  

This is simple 'for' loop with incrementation by 64 (have you already noticed why? ;) ).

The next step it to calculate position of pixel to load.
Let's think about it.

In terms of "Y" coordinate - we can use SV_GroupID.x, because we dispatched 270 thread groups.
In terms of "X" well... we can take advantage of current thread in the group! Let's try it.

Because we have 64 threads per group, such approach will get through all pixels.
Consider thread group (0, 0, 0).
- Thread (0, 0, 0) will process pixels (0, 0), (64, 0), (128, 0), (192, 0), (256, 0), (320, 0),
(384, 0), (448, 0).
- Thread (1, 0, 0) will process pixels (1, 0), (65, 0), (129, 0), (193, 0), (257, 0), (321, 0), (385, 0), (449, 0)
...
- Thread (63, 0, 0) will process pixels (63, 0), (127, 0), (191, 0), (255, 0), (319, 0),
(383, 0), (447, 0)
This way, all pixels will be processed.

We want also to make sure that we won't load pixel out of color buffer:
  // We move along X axis, pixel by pixel. Y is GroupID.  
     uint CurrentPixelPositionX = PositionX + threadID;  
     uint CurrentPixelPositionY = groupID;  
     if ( CurrentPixelPositionX < ViewportSizeX )  
     {  
        // HDR Color buffer.  
        // Calculate screen space position of HDR color buffer, load it and calculate luma.  
        uint2 colorPos = uint2(CurrentPixelPositionX, CurrentPixelPositionY);  
        float3 color = texture0.Load( int3(colorPos, 0) ).rgb;  
        float luma = dot(color, LUMA_RGB);  

See? Pretty simple :)
I've also calculted luma (line 21 of the assembly).

Okay, we already calculated luma from color pixel, feels good. The next step is to load (no samping!) corresponding depth value.
But we have a problem here, because we attached full-resolution depth buffer. How to deal with it?
That's surprisingly simple, just multiply colorPos by some constant (cb0_v2.z). We downscaled HDR color buffer by 4, so this value is 4!
     const int iDepthTextureScale = (int) cb0_v2.z;  
     uint2 depthPos = iDepthTextureScale * colorPos;  
     float depth = texture1.Load( int3(depthPos, 0) ).x;  


So far so good! But... we came to assembly lines 24-25....
  24:   eq r2.x, r2.x, cb0[2].w  
  25:   and r2.x, r2.x, cb0[2].y  

Well. At first, we have floating-poing equality comparison, the result of it goes to r2.x and right after that we have.... what? Bitwise AND?? Seriously? On floating-point value? What the heck???

The 'eq+and' problem
Let me just say this was the most difficult part of this shader to figure out for me. I tried even some crazy asint/asfloat combinations...
What about a bit different approach? Let's just do a simple float-float comparison in HLSL

 float DummyPS() : SV_Target0  
 {  
   float test = (cb0_v0.x == cb0_v0.y);  
   return test;  
 }  

And output assembly:
   0: eq r0.x, cb0[0].y, cb0[0].x  
   1: and o0.x, r0.x, l(0x3f800000)  
   2: ret   

Interesting, isn't it? didn't expect here 'and'.
0x3f800000 is simply 1.0f... well, logical, as we have 1.0 if comparison passes, 0.0 otherwise.
What if you could 'replace' 1.0 with some other value? Like this:
 float DummyPS() : SV_Target0  
 {  
   float test = (cb0_v0.x == cb0_v0.y) ? cb0_v0.z : 0.0;  
   return test;  
 }  

And result:
   0: eq r0.x, cb0[0].y, cb0[0].x  
   1: and o0.x, r0.x, cb0[0].z  
   2: ret   

Hahah! It works :) Just magic by HLSL compiler. Note aside, if you replace 0.0 with something different, it will be just movc.


Going back to our compute shader, the next step is to check if depth value is equal to cb0_v2.w. It's always set to 0.0 - simply speaking, we check if the pixel lies on far plane (sky). If yes, we assign to this factor some value, around 0.5 (I checked few frames).

Such calculated coefficent is used for interpolation between color luma, and 'sky' luma (cb0_v2.x, often around 0.0). I guess this is to give more control how sky is important in calculating average luminance, usually by decreasing its importance. Very smart idea.
    // We check if pixel lies on far plane (sky). If yes, we can specify how it will be  
    // mixed with our values.  
    float value = (depth == cb0_v2.w) ? cb0_v2.y : 0.0;  
         
    // If 'value' is 0.0, this lerp will simply give us 'luma'. However, if 'value' is different  
    // (often around ~0.50), calculated luma can have less importance. (cb0_v2.x is usually close to 0.0).  
    float lumaOk = lerp( luma, cb0_v2.x, value );  
   

As we have lumaOk, the next step is to calculate its natural logarithm to make it distribute nicely. But wait. Let's say that lumaOk is 0.0. We know that log(0) is undefined, so we add 1.0, because log(1) = 0.0.

After that, we scale the calculated logarithm by 128 to distribute it nicely for 256 cells. Very smart!
And this is exactly where this 88.722839 comes from. It's 128 * natural logatithm(2).
It's just the way HLSL calculates logatithms.
In HLSL assembly there is only one function which calculates logarithms: log and it's base-2.
       // Let's assume that lumaOk is 0.0.  
       // log(0) is undefined  
       // log(1) = 0.  
       // calculate natural logarithm of luma  
       lumaOk = log(lumaOk + 1.0);  
         
       // Scale logarithm of luma by 128  
       lumaOk *= 128;  


Finally we calculate index of cell from logarithmically distributed luminance and add '1' to corresponding cell in shared memory.
       // Calculate proper index. Uint and since we have 256 elements in array,  
       // make sure it will not get out of bounds.  
       uint uLuma = (uint) lumaOk;  
       uLuma = min(uLuma, 255);  
   
       // Add '1' to corresponding luma value.  
       InterlockedAdd( shared_data[uLuma], 1 );  

The next step is to, again, set barrier to make sure all pixels in row have been processed.
And the last one is to add values from shared memory to structured buffer, the same way, in simple loop:
   // Wait until all pixels in this row have been processed  
   GroupMemoryBarrierWithGroupSync();  
   
   // Add calculated values to structured buffer.  
   [unroll] for (uint idx = 0; idx < 4; idx++)  
   {  
     const uint offset = threadID + idx*64;  
   
     uint data = shared_data[offset];  
     InterlockedAdd( g_buffer[offset], data );  
   }  

After all 64 threads in the thread group fill shared data, each thread will add 4 values to output buffer.

In terms of the output buffer. Let's think about it. The sum of all values of the buffer is equal to total number of pixels! (for 480x270 = 129 600). So we know now how much of pixels have specific luminance.

If you're a bit rusty with compute shaders (like me) that might not be intuitive at the first time, so get through the post a few times, take pen&paper and try to understand concepts behind this technique.

That's all! :)  That's how The Witcher 3 calculates histogram of luminance. I've certainly learned a lot during writing this post. Congatulations for people at CD Projekt Red!

If you are interested in full HLSL shader, it's here. My ambition always is to get as similar assembly as in original game and I'm more than happy that I've done this again! :)

I hope you enjoyed this post.
Thanks for reading!

Tuesday, November 13, 2018

Reverse engineering the rendering of The Witcher 3, part 6 - sharpen

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


Hi,

Today we will take a closer look at another postprocess from The Witcher 3 - sharpen.
Sharpening makes an output image a bit crisper. The effect is known from Photoshop and other image editors.

In The Witcher 3 sharpening has two presets: low and high. I will discuss differences between them later, let's take a look at some screenshots now:

"Low" setting - before
"Low" setting - after


"High" setting - before
"High" setting - after
If you want to see more (interactive) comparisons, see section in Nvidia's performance guide for The Witcher 3. As you can see, the effect is particularly visible on grass and foliage.

In this post we will investigate frame from the very beginning of the game: I selected this one purposefully, because here we see terrain (long draw distance) and skydome.

In terms of input, sharpening requires color buffer t0 (LDR, after tonemapping and lens flares) and depth buffer t1.

Let's see the pixel shader, assembly:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[3], immediateIndexed  
    dcl_constantbuffer cb12[23], immediateIndexed  
    dcl_sampler s0, mode_default  
    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 7  
   0: ftoi r0.xy, v0.xyxx  
   1: mov r0.zw, l(0, 0, 0, 0)  
   2: ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t1.xyzw  
   3: mad r0.x, r0.x, cb12[22].x, cb12[22].y  
   4: mad r0.y, r0.x, cb12[21].x, cb12[21].y  
   5: max r0.y, r0.y, l(0.000100)  
   6: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y  
   7: mad_sat r0.y, r0.y, cb3[1].z, cb3[1].w  
   8: add r0.z, -cb3[1].x, cb3[1].y  
   9: mad r0.y, r0.y, r0.z, cb3[1].x  
  10: add r0.y, r0.y, l(1.000000)  
  11: ge r0.x, r0.x, l(1.000000)  
  12: movc r0.x, r0.x, l(0), l(1.000000)  
  13: mul r0.z, r0.x, r0.y  
  14: round_z r1.xy, v0.xyxx  
  15: add r1.xy, r1.xyxx, l(0.500000, 0.500000, 0.000000, 0.000000)  
  16: div r1.xy, r1.xyxx, cb3[0].zwzz  
  17: sample_l(texture2d)(float,float,float,float) r2.xyz, r1.xyxx, t0.xyzw, s0, l(0)  
  18: lt r0.z, l(0), r0.z  
  19: if_nz r0.z  
  20:  div r3.xy, l(0.500000, 0.500000, 0.000000, 0.000000), cb3[0].zwzz  
  21:  add r0.zw, r1.xxxy, -r3.xxxy  
  22:  sample_l(texture2d)(float,float,float,float) r4.xyz, r0.zwzz, t0.xyzw, s0, l(0)  
  23:  mov r3.zw, -r3.xxxy  
  24:  add r5.xyzw, r1.xyxy, r3.zyxw  
  25:  sample_l(texture2d)(float,float,float,float) r6.xyz, r5.xyxx, t0.xyzw, s0, l(0)  
  26:  add r4.xyz, r4.xyzx, r6.xyzx  
  27:  sample_l(texture2d)(float,float,float,float) r5.xyz, r5.zwzz, t0.xyzw, s0, l(0)  
  28:  add r4.xyz, r4.xyzx, r5.xyzx  
  29:  add r0.zw, r1.xxxy, r3.xxxy  
  30:  sample_l(texture2d)(float,float,float,float) r1.xyz, r0.zwzz, t0.xyzw, s0, l(0)  
  31:  add r1.xyz, r1.xyzx, r4.xyzx  
  32:  mul r3.xyz, r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000)  
  33:  mad r1.xyz, -r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000), r2.xyzx  
  34:  max r0.z, abs(r1.z), abs(r1.y)  
  35:  max r0.z, r0.z, abs(r1.x)  
  36:  mad_sat r0.z, r0.z, cb3[2].x, cb3[2].y  
  37:  mad r0.x, r0.y, r0.x, l(-1.000000)  
  38:  mad r0.x, r0.z, r0.x, l(1.000000)  
  39:  dp3 r0.y, l(0.212600, 0.715200, 0.072200, 0.000000), r2.xyzx  
  40:  dp3 r0.z, l(0.212600, 0.715200, 0.072200, 0.000000), r3.xyzx  
  41:  max r0.w, r0.y, l(0.000100)  
  42:  div r1.xyz, r2.xyzx, r0.wwww  
  43:  add r0.y, -r0.z, r0.y  
  44:  mad r0.x, r0.x, r0.y, r0.z  
  45:  max r0.x, r0.x, l(0)  
  46:  mul r2.xyz, r0.xxxx, r1.xyzx  
  47: endif  
  48: mov o0.xyz, r2.xyzx  
  49: mov o0.w, l(1.000000)  
  50: ret  

50 lines of assembly seems like pretty doable task. Let's start it then.


Sharpen amount generation

The first step is to Load depth buffer (line 1). Note that The Witcher 3 uses revesed depth (1.0 - near, 0.0 - far). As you may know, hardware depth is mapped in non-linear way (see this article for details).

Lines 3-6 perform very interesting way of mapping this hardware depth [1.0 - 0.0] to [near-far] values (you set them during MatrixPerspectiveFov). See values from constant buffer:


Having near value of 0.2 and far of 5000 I believe you can calculate values of cb12_v21.xy this way:

cb12_v21.y = 1.0 / near
cb12_v21.x = - (1.0 / near) + (1.0 / near) * (near / far)

This piece of code appears quite often in shaders from TW3, so I believe it's just a function.

When we already have "frustum depth", line 7 uses scale/bias to create a interpolation coefficient (we use saturate here to make sure it's clamped to [0-1] range).


cb3_v1.xy are intensities of sharpening at near and far distances, respectively. Let's call them "sharpenNear" and "sharpenFar". And this is the only difference between "Low" and "High" presets of this effect in The Witcher 3.

Now it's time to use the obtained coefficient. Lines 8-9 are just lerp(sharpenNear, sharpenFar, interpolationCoeff). What is this for? Thanks to that we can have different intensity near Geralt and far away from him). See:



It may be barely visible, but here we interpolated the intensity of sharpen near the player (2.177151) with intensity of the effect far away (1.91303) based on distance. Once we have calculated it we add 1.0 (line 10) to intensity. What is this for? Let's assume that lerp from above gave us 0.0. When we add 1.0 we will have 1.0 of course and this is value which will not affect the pixel during sharpening. More on this later.

During sharpening process we don't want to affect sky. We can achieve this using simple conditional test:

   // Do not perform sharpen on sky  
   float fSkyboxTest = (fDepth >= 1.0) ? 0 : 1;  

In The Witcher 3 depth value for sky pixels is 1.0, so we use it to get some sort of "binary filter" (fun fact: step does not work properly in this case)
Now we can multiply interpolated intenstiy with "sky filter":


This multiplication takes place in line 13.
Example shader code:
   // Calculate final sharpen amount  
   float fSharpenAmount = fSharpenIntensity * fSkyboxTest;  


Sampling center of the pixel

There is an aspect of SV_Position which will be important here: half-pixel offset. It turns out that pixel at top left corner (0, 0) is not (0, 0) in terms of SV_Position.xy, but (0.5, 0.5). Wow!

Here we want to sample in center of the pixel, so take a look at lines 14-16. We can write it in HLSL:
   // Sample the center of the pixel.   
   // Get rid of "half-pixel" offset from SV_Position.xy.  
   float2 uvCenter = trunc( Input.Position.xy );  

   // Add half-pixel to make sure we will sample the center of the pixel  
   uvCenter += float2(0.5, 0.5);  
   uvCenter /= g_Viewport.xy  

And later we sample input color texture from "uvCenter" texcoords. Don't worry, the effect of the sampling will be the same as using "typical" (SV_Position.xy / ViewportSize.xy).

To sharpen or not to sharpen

The decision whether to sharpen or not is based on fSharpenAmount.

   // Get the value of current pixel  
   float3 colorCenter = TexColorBuffer.SampleLevel( samplerLinearClamp, uvCenter, 0 ).rgb;  
     
   // Final result  
   float3 finalColor = colorCenter;  
   
   if ( fSharpenAmount > 0 )  
   {  
     // do the sharpening here...  
   }  
   
  return float4( finalColor, 1 );  
   

Sharpen

It's time to look at the heart of the algorithm.
Basically:
- sample the input color texture four times at the corners of the pixel,
- add the samples and calculate average value,
- calcuate the difference between "center" and "cornerAverage",
- find maximum absolute component of the difference,
- adjust max. abs. component using scale+biasvalues,
- determine amount of the effect using max. abs. component,
- calculate luma of "centerColor" and "averageColor",
- divide the colorCenter by its luma,
- caclulate the new, interpolated luma using amount of the effect,
- multiply the colorCenter by the new luma

Seems like lots of things and it was a challenge for me to understand it, since I've never played with sharpening filters. 

Let's start with sampling pattern. As you can see in the assembly, there are four texture fetches.
It will be best to show it using this image of pixel (Paint level expert):
All fetches in the shader use bilinear sampling (D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT).

The offset from center to any corner is (±0.5, ±0.5), depending on corner.
See how this can be done in HLSL? Let's see:
    float2 uvCorner;  
    float2 uvOffset = float2( 0.5, 0.5 ) / g_Viewport.xy;  // remember about division!
    
    float3 colorCorners = 0;  
             
    // Top left corner  
    // -0,5, -0.5  
    uvCorner = uvCenter - uvOffset;  
    colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;  
   
    // Top right corner  
    // +0.5, -0.5  
    uvCorner = uvCenter + float2(uvOffset.x, -uvOffset.y);  
    colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;  
   
    // Bottom left corner  
    // -0.5, +0.5  
    uvCorner = uvCenter + float2(-uvOffset.x, uvOffset.y);  
    colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;  
   
    // Bottom right corner  
    // +0.5, +0.5  
    uvCorner = uvCenter + uvOffset;  
    colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;  

So now we have all four samples summed in "colorCorners" variable. Let's perform the next steps:

   // Calculate the average of four corners  
   float3 averageColorCorners = colorCorners / 4.0;  
   
   // Calculate the color difference  
   float3 diffColor = colorCenter - averageColorCorners;  
   
   // Find max absolute RGB component of the difference  
   float fDiffColorMaxComponent = max( abs(diffColor.x), max( abs(diffColor.y), abs(diffColor.z) ) );  
   
   // Adjust this factor  
   float fDiffColorMaxComponentScaled = saturate( fDiffColorMaxComponent * sharpenLumScale + sharpenLumBias );  
   
   // Calculate how much pixel will be sharpened.  
   // Note the "1.0" here - this is why we added "1.0" before to fSharpenIntensity.  
   float fPixelSharpenAmount = lerp(1.0, fSharpenAmount, fDiffColorMaxComponentScaled);  
    
   // Calculate luminance of "center" of the pixel and luminance of average value.  
   float lumaCenter = dot( LUMINANCE_RGB, finalColor );  
   float lumaCornersAverage = dot( LUMINANCE_RGB, averageColorCorners );  
       
   // divide "centerColor" by its luma  
   float3 fColorBalanced = colorCenter / max( lumaCenter, 1e-4 );  
     
   // Calc the new luma  
   float fPixelLuminance = lerp(lumaCornersAverage, lumaCenter, fPixelSharpenAmount);  
       
   // Calc the output color  
   finalColor = fColorBalanced * max(fPixelLuminance, 0.0);  
}

return float4(finalColor, 1.0);

The edge detection is done by calculating max. abs. component of the difference. Smart! See its visualization:
Visualization of maximum absolute component of the difference.


Phew. The final HLSL shader is available here. Sorry for quite poor formatting. Feel free to use my HLSLexplorer and play with the code.

I am happy to say that the code above gives exactly the same assembly as in the game! :)

To sum up, The Witcher 3's sharpening shader is very well written (notice that fPixelSharpenAmount is larger than 1.0! that is interesting...). Also, the primary way to modify intensity of the effect are near/far intensities. In the game, they are not constant throughout the gamplay; I collected some example values:

Skellige:

sharpenNear sharpenFar sharpenDistanceScale sharpenDistanceBias sharpenLumScale sharpenLumBias
low
0.40
0.20
0.025
-0.25
-13.33333
1.33333
high
2.0
1.8
0.025
-0.25
-13.33333
1.33333

Kaer Morhen:
sharpenNear
sharpenFar
sharpenDistanceScale
sharpenDistanceBias
sharpenLumScale
sharpenLumBias
low
0.57751
0.31303
0.06665
-0.33256
-1.0
2.0
high
2.17751
1.91303
0.06665
-0.33256
-1.0
2.0


That's it for today. I hope you enjoyed it :)
Thanks for reading!

M.

Wednesday, November 7, 2018

Few words about HLSLexplorer

Welcome,

HLSLexplorer started as a hobby project (which still is!) and was never meant to be "Godbolt for shaders" nor any "competition" for already available solutions (see Shader Playground by Tim Jones for example). I just realised during its development that someone might find it useful - that's all.

Today I am happy to say that new version of HLSLexplorer is done and now it's open source!
Let's go through the most important changes.

HLSLexplorer 1.0 in action

Support for modern DirectX compiler and AMD GCN ISA
There are three tabs on the right window. DXBC (d3dcompiler_47.dll for Shader Model 4.0-5.1) and new ones: DXIL (for modern DirectX shader compiler) and AMD GCN ISA

Real-time Pixel Shader Preview
I thought it would be nice to have an option to visualize the result of pixel shader in real time, so I made a tool to do it.

If you click Insert -> Insert simple PS, a dummy pixel shader appear on the left.
Press F7 to start pixel shader preview window:

The left window is rendering output. The right panel is a place to load textures. You can load dds textures, also png/jpg/bmp are supported. Load any texture to texture0 channel... Huh, no result. To see it, switch back to main window, tap F5 to compile shader, and you should see the output in preview window:



Let's go back to dummy pixel shader. There are some differences comparing to previous version of the application:
 cbuffer cbData : register (b12)  
 {  
     float elapsedTime;  
     uint  numFrames;  
     float2 pad;  
   
     float2 viewportSize;  
     float2 viewportInvSize;  
 }  
   
 SamplerState samplerPointClamp  : register (s0);  
 SamplerState samplerPointWrap   : register (s1);  
 SamplerState samplerLinearClamp : register (s2);  
 SamplerState samplerLinearWrap  : register (s3);  
 SamplerState samplerAnisoClamp  : register (s4);  
 SamplerState samplerAnisoWrap   : register (s5);  

Now you have access to some values which are usually available in typical scenarios, like elapsed time (in seconds), viewport / invViewport size and various samplers, you can use them in your pixel shaders and see the effect without closing HLSLexplorer. Pressing F5 updates shader in preview (assuming there is no errors of course). Please note that this feature currently works only with Shader Model 4.0 - 5.0.

HLSLexplorer goes open source
Source code is available on GitHub. If you look just for binary release, click here (Google Drive).

If you would like to build it by yourself:
HLSLexplorer is linked statically against wxWidgets 3.1.1. The compilation of wxWidgets for debug/release configurations should be easy, but please note that I use environmental variable (WXWIN) in HLSLexplorer's solution configuration (additional library directories).

Other improvements worth mentioning
- The program asks if user wants to save HLSL source from the left window when user wants to close the application,
- User can load external hlsl files into the program
- User can save disassembled shaders (DXBC, DXIL, AMD GCN ISA) to hard drive. Just switch to proper window and tap Ctrl+S. The title of saving window tells what you attempt to save.
- Slight UI tweaks
- Improved "About" window ;)

I hope you like it. Go now and disassemble some shaders! :)

Monday, November 5, 2018

Reverse engineering the rendering of The Witcher 3: Index

Welcome,

This is the index page for my "Reverse engineering the rendering of The Witcher 3" small series where I select some rendering techniques from "The Witcher 3", analyze them using RenderDoc, then grab DirectX assembly for shaders and finally try to turn these instructions back to readable HLSL shader.
I focus especially on understanding and explaining presented techniques; it's all for learning purposes after all.

The genesis of the series is simply "I want to know how it's done". Having RenderDoc and other tools which allow us to see every stage of the pipeline is great way to learn how games do rendering stuff.

I have not worked on "The Witcher 3: Wild Hunt" nor its expansion packs in any way so all of code and conclusions here are results of my analysis and tinkering.

Disclaimer: All opinions and views expressed on my blog and on this series are my own and are in no way representative of Rockstar Games.

Here is the current index:

Part 1 - tonemapping

Part 2 - eye adaptation

Part 3 - chromatic aberration

Part 4 - vignette

Part 5 - drunk effect

Part 6 - sharpen

Part 7a - average luminance, part 1 (histogram of luminance)

Part 7b - average luminance, part 2 (calculation)

Part 8 - the Moon and lunar phases

Part 9 - GBuffer

Part 10 - distant rain shafts

Part 11 - lightnings

Part 12 - stupid sky tricks

Part 13a - witcher senses, part 1 (objects & intensity map)

Part 13b - witcher senses, part 2 (outline map)

Part 13c - witcher senses, part 3 (fisheye effect & final combining)

Part 14 - cirrus clouds

Part 15 - fog

Part 16 - shooting stars

Part 17 - the Milky Way

Part 18 - color grading

Part 19 - portals

Part 20 - light shafts

Part 21 - the painted world

Extra:

Someone has done awesome job and prepared Russian version of my posts!
Translation into Russian (parts 1-5)
Translation into Russian (parts 6-10)
Translation into Russian (parts 11-13)

I'm the author of HLSLexplorer which greatly helped me in the process of learning HLSL assembly and was invaluable in writing this series. See the post for details, source code and binaries.


Enjoy! :)

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.