Sunday, March 17, 2019

Reverse engineering the rendering of The Witcher 3, part 12 - stupid sky tricks

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


Welcome,

This part will be slightly different comparing to the previous ones. Today I'd like to show you some aspects of sky shaders from The Witcher 3.

Why some "stupid tricks" instead of full shader? Well, there are a few reasons. First of all, sky shader in The Witcher 3 is quite a complex beast. Pixel Shader of 2015 version has 267 lines of assembly while PS from "Blood & Wine" DLC - 385.
Moreover, they have quite a lot of inputs which doesn't really help in struggles to reverse engineer complete (and readable!) HLSL code.

Therefore, I decided to show you some tricks from these shaders only. If I find anything new, this post will be updated.

The differences between 2015 version of the game and B&W (2016) addon are quite notable. This includes, for instance, different calculation of stars and their blinking, different approach to rendering of the Sun... Blood & Wine shader also calculates Milky Way during the night.

I'll start with some basics and switch to stupid tricks later.

At first, let's take a look at a clear night sky in The Witcher 3 (2015):

Basics

As most of modern video games, The Witcher 3 uses skydome to represent sky. Take a look at hemisphere used for this in The Witcher 3 (2015). On a side note, in this case bounding box of this mesh ranges from [0,0,0] to [1,1,1] (Z is up-axis) and has smoothly distributed UVs. We'll use them later.


The idea behind skydome is similar to skybox (mesh being used is the only difference). During vertex shader we translate a skydome with respect to observer (usually by camera position) which gives an illusion that sky is really far away - we'll never go there.

If you have been following the series for a while you know that The Witcher 3 uses reversed depth - that means, far plane is represented by 0.0f while near plane - by 1.0f. To make sure that output of skydome will be completely on far plane, we set MinDepth the same as MaxDepth of viewport parameters:


To learn how MinDepth and MaxDepth fields are used during viewport transform click here (docs.microsoft.com).

Vertex Shader

Let's start with vertex shader. In The Witcher 3 (2015) assembly of VS is as follows:
 vs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb1[4], immediateIndexed  
    dcl_constantbuffer cb2[6], immediateIndexed  
    dcl_input v0.xyz  
    dcl_input v1.xy  
    dcl_output o0.xy  
    dcl_output o1.xyz  
    dcl_output_siv o2.xyzw, position  
    dcl_temps 2  
   0: mov o0.xy, v1.xyxx  
   1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx  
   2: mov r0.w, l(1.000000)  
   3: dp4 o1.x, r0.xyzw, cb2[0].xyzw  
   4: dp4 o1.y, r0.xyzw, cb2[1].xyzw  
   5: dp4 o1.z, r0.xyzw, cb2[2].xyzw  
   6: mul r1.xyzw, cb1[0].yyyy, cb2[1].xyzw  
   7: mad r1.xyzw, cb2[0].xyzw, cb1[0].xxxx, r1.xyzw  
   8: mad r1.xyzw, cb2[2].xyzw, cb1[0].zzzz, r1.xyzw  
   9: mad r1.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw  
  10: dp4 o2.x, r0.xyzw, r1.xyzw  
  11: mul r1.xyzw, cb1[1].yyyy, cb2[1].xyzw  
  12: mad r1.xyzw, cb2[0].xyzw, cb1[1].xxxx, r1.xyzw  
  13: mad r1.xyzw, cb2[2].xyzw, cb1[1].zzzz, r1.xyzw  
  14: mad r1.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw  
  15: dp4 o2.y, r0.xyzw, r1.xyzw  
  16: mul r1.xyzw, cb1[2].yyyy, cb2[1].xyzw  
  17: mad r1.xyzw, cb2[0].xyzw, cb1[2].xxxx, r1.xyzw  
  18: mad r1.xyzw, cb2[2].xyzw, cb1[2].zzzz, r1.xyzw  
  19: mad r1.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw  
  20: dp4 o2.z, r0.xyzw, r1.xyzw  
  21: mul r1.xyzw, cb1[3].yyyy, cb2[1].xyzw  
  22: mad r1.xyzw, cb2[0].xyzw, cb1[3].xxxx, r1.xyzw  
  23: mad r1.xyzw, cb2[2].xyzw, cb1[3].zzzz, r1.xyzw  
  24: mad r1.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw  
  25: dp4 o2.w, r0.xyzw, r1.xyzw  
  26: ret  

In this scenario VS outputs only texcoords and world-space position. In Blood & Wine it also outputs normalized normal vector. I'll stay with the 2015 version as it's simpler.

Take a look at constant buffer marked as cb2:


Here we have world matrix (uniform scaling by 100 and translation by camera position). Nothing fancy. cb2_v4 and cb2_v5 are scale/bias factors which serve to transform positions of vertices from [0-1] range to [-1;1] one. But here, in terms of Z-axis (up) these coefficents will 'squeeze' it.


We have already seen similar VS in previous parts of the series. The general algorithm is to pass texcoords further, then calculate Position with scale/bias factors, then calculate PositionW in world space, then calculate final clip space position by multiplying matWorld and matViewProj matrices together -> use their product to multiply with Position to get final SV_Position.

So, the HLSL for this vertex shader would be something like this:
 struct InputStruct {  
      float3 param0 : POSITION;  
      float2 param1 : TEXCOORD;  
      float3 param2 : NORMAL;  
      float4 param3 : TANGENT;  
 };  
   
 struct OutputStruct {  
      float2 param0 : TEXCOORD0;  
      float3 param1 : TEXCOORD1;  
      float4 param2 : SV_Position;  
 };  
   
 OutputStruct EditedShaderVS(in InputStruct IN)  
 {  
      OutputStruct OUT = (OutputStruct)0;  
        
      // Simple texcoords passing  
      OUT.param0 = IN.param1;  
        
        
      // * Manually construct world and viewProj martices from float4s:  
      row_major matrix matWorld = matrix(cb2_v0, cb2_v1, cb2_v2, float4(0,0,0,1) );  
      matrix matViewProj = matrix(cb1_v0, cb1_v1, cb1_v2, cb1_v3);  
   
      // * Some optional fun with worldMatrix  
      // a) Scale  
      //matWorld._11 = matWorld._22 = matWorld._33 = 0.225f;  
   
      // b) Translate  
      // X Y Z  
      //matWorld._14 = 520.0997;  
      //matWorld._24 = 74.4226;  
      //matWorld._34 = 113.9;  
   
      // Local space - note the scale+bias here!  
      //float3 meshScale = float3(2.0, 2.0, 2.0);  
      //float3 meshBias = float3(-1.0, -1.0, -0.4);  
      float3 meshScale = cb2_v4.xyz;  
      float3 meshBias = cb2_v5.xyz;  
   
      float3 Position = IN.param0 * meshScale + meshBias;  
        
      // World space  
      float4 PositionW = mul(float4(Position, 1.0), transpose(matWorld) );  
      OUT.param1 = PositionW.xyz;  
   
      // Clip space - original approach from The Witcher 3  
      matrix matWorldViewProj = mul(matViewProj, matWorld);  
      OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) );  
        
      return OUT;  
 }  

Comparison of the my shader (left) and the original one (right):

The great thing about RenderDoc is that it allows to inject your own shader instead of original one and your changes do affect the pipeline until the very end of a frame. As you can see in HLSL code, I gave you some options to change scaling and translation of the final geometry. You can play with it and achieve some funny results:

Hail to the skydome!

Optimizing the vertex shader

Do you see a problem with the original vertex shader? Per-vertex matrix-matrix multiplication is completely redundant! I found it in at least few vertex shaders (for instance, in distant rain shafts).  We could optimize it by multiplying PositionW with matViewProj immediately!

So, we can replace HLSL code:
      // Clip space - original approach from The Witcher 3  
      matrix matWorldViewProj = mul(matViewProj, matWorld);  
      OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) );  

with this one:
      // Clip space - optimized version  
      OUT.param2 = mul( matViewProj, PositionW );  

An optimized version produces the following assembly:
    vs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer CB1[4], immediateIndexed  
    dcl_constantbuffer CB2[6], immediateIndexed  
    dcl_input v0.xyz  
    dcl_input v1.xy  
    dcl_output o0.xy  
    dcl_output o1.xyz  
    dcl_output_siv o2.xyzw, position  
    dcl_temps 2  
   0: mov o0.xy, v1.xyxx  
   1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx  
   2: mov r0.w, l(1.000000)  
   3: dp4 r1.x, r0.xyzw, cb2[0].xyzw  
   4: dp4 r1.y, r0.xyzw, cb2[1].xyzw  
   5: dp4 r1.z, r0.xyzw, cb2[2].xyzw  
   6: mov o1.xyz, r1.xyzx  
   7: mov r1.w, l(1.000000)  
   8: dp4 o2.x, cb1[0].xyzw, r1.xyzw  
   9: dp4 o2.y, cb1[1].xyzw, r1.xyzw  
  10: dp4 o2.z, cb1[2].xyzw, r1.xyzw  
  11: dp4 o2.w, cb1[3].xyzw, r1.xyzw  
  12: ret

As you can see, we reduced number of instructions from 26 to 12 - that's quite a change. I don't know how widespread this problem is in the game but c'mon CD Projekt Red, maybe a patch or something? :)

I'm not kidding here. You can inject my optimized shader instead of original one in RenderDoc and see for yourself that this optimization changes nothing in terms of visuals. Honestly, I don't know why CD Projekt Red decided to do per-vertex matrix-matrix multiplication...

The Sun

In The Witcher 3 (2015) calculating of atmospheric scattering and the Sun consists of two separate draw calls:

The Witcher 3 (2015) - before

The Witcher 3 (2015) - with sky

The Witcher 3 (2015) - with sky + the Sun
Rendering the Sun in 2015 version is pretty similar to the Moon in terms of geometry and blend/depth states.


On the other hand, in Blood & Wine sky with the Sun is rendered in one pass:

The Witcher 3: Blood & Wine (2016) - before sky

The Witcher 3: Blood & Wine (2016) - with sky and the Sun

No matter how you want to render the Sun at some point you will need (normalized) direction of sunlight. The most intuitive way to obtain this vector is to use spherical coordinates. Basically you need only two values representing two angles (in radians!): phi and theta. Once you have them you can assume r = 1, so it cancels, so for y-up Cartesian coordinate system we can write HLSL code like this:
 float3 vSunDir;  
 vSunDir.x = sin(fTheta)*cos(fPhi);  
 vSunDir.y = sin(fTheta)*sin(fPhi);  
 vSunDir.z = cos(fTheta);  
 vSunDir = normalize(vSunDir);  

Normally you calculate sunlight direction in your application, then pass it to constant buffer for further use.

Once we have sunlight direction we can dive into assembly of pixel shader from Blood & Wine....
  ...   
  100: add r1.xyw, -r0.xyxz, cb12[0].xyxz  
  101: dp3 r2.x, r1.xywx, r1.xywx  
  102: rsq r2.x, r2.x  
  103: mul r1.xyw, r1.xyxw, r2.xxxx  
  104: mov_sat r2.xy, cb12[205].yxyy  
  105: dp3 r2.z, -r1.xywx, -r1.xywx  
  106: rsq r2.z, r2.z  
  107: mul r1.xyw, -r1.xyxw, r2.zzzz  
  ...  

Okay. To start, cb12[0].xyz is a position of camera, while in r0.xyz we store vertex position (it's an output from vertex shader). Therefore, line 100 calculates worldToCamera vector. But take a look at lines 105-107. We could write it as normalize( -worldToCamera), which means we calculate normalized cameraToWorld vector.

  120: dp3_sat r1.x, cb12[203].yzwy, r1.xywx  

Then we calculate dot product between cameraToWorld and sunDirection vectors! Remember they have to be normalized. Also we saturate whole expression to clamp it within [0-1] range.

Cool! We have this dot product in r1.x. Let's find the next use of it...
  152: log r1.x, r1.x  
  153: mul r1.x, r1.x, cb12[203].x  
  154: exp r1.x, r1.x  
  155: mul r1.x, r2.y, r1.x  


The "log, mul, exp" triple is, simply speaking, exponentation. As you can see, we raise our cosine (dot product of normalized vectors) to some power. You may ask, why? This way we can produce gradient which will mimic our Sun. (And line 155 affects opacity of this gradient, so you can for instance set this to zero to completely hide the Sun). See some examples:

exponent = 54

exponent = 2400
Having this gradient, we use it to interpolate between skyColor and sunColor! To make sure there will be no artifacts we had to saturate in line 120.

Please take a note that this trick can be used to mimic corona phenomenon for the Moon (with lower values of the exponent). For this you will need moonDirection vector - which can be easily calculated with spherical coordinates.

Final HLSL can look similar to the following snippet:
 float3 vCamToWorld = normalize( PosW – CameraPos );  
   
 float cosTheta = saturate( dot(vSunDir, vCamToWorld) );  
 float sunGradient = pow( cosTheta, sunExponent );  
   
 float3 color = lerp( skyColor, sunColor, sunGradient );  

Moving stars

If you would make a timelapse during the night on a clear sky in The Witcher 3 you would notice that stars are not static - they slightly move across the sky with time! I noticed this quite accidentally and wanted to see how this was done.

Let's start with fact that stars in The Witcher 3 are represented with 1024x1024x6 cubemap. If you think about it, it's very handy solution as it easily allows to map directions to sample the cubemap.

Consider the following piece of assembly:
  159: add r1.xyz, -v1.xyzx, cb1[8].xyzx  
  160: dp3 r0.w, r1.xyzx, r1.xyzx  
  161: rsq r0.w, r0.w  
  162: mul r1.xyz, r0.wwww, r1.xyzx  
  163: mul r2.xyz, cb12[204].zwyz, l(0.000000, 0.000000, 1.000000, 0.000000)  
  164: mad r2.xyz, cb12[204].yzwy, l(0.000000, 1.000000, 0.000000, 0.000000), -r2.xyzx  
  165: mul r4.xyz, r2.xyzx, cb12[204].zwyz  
  166: mad r4.xyz, r2.zxyz, cb12[204].wyzw, -r4.xyzx  
  167: dp3 r4.x, r1.xyzx, r4.xyzx  
  168: dp2 r4.y, r1.xyxx, r2.yzyy  
  169: dp3 r4.z, r1.xyzx, cb12[204].yzwy  
  170: dp3 r0.w, r4.xyzx, r4.xyzx  
  171: rsq r0.w, r0.w  
  172: mul r2.xyz, r0.wwww, r4.xyzx  
  173: sample_indexable(texturecube)(float,float,float,float) r4.xyz, r2.xyzx, t0.xyzw, s0  

To calculate final sampling vector (line 173), we start by calculating normalized worldToCamera vector (lines 159-162).

Then we calculate 2 cross products (163-164, 165-166) with moonDirection and later perform 3 dot products to get final sampling vector. HLSL:

 float3 vWorldToCamera = normalize( g_CameraPos.xyz - Input.PositionW.xyz );  
 float3 vMoonDirection = cb12_v204.yzw;  
   
 float3 vStarsSamplingDir = cross( vMoonDirection, float3(0, 0, 1) );  
 float3 vStarsSamplingDir2 = cross( vStarsSamplingDir, vMoonDirection );  
   
 float dirX = dot( vWorldToCamera, vStarsSamplingDir2 );  
 float dirY = dot( vWorldToCamera, vStarsSamplingDir );  
 float dirZ = dot( vWorldToCamera, vMoonDirection);  
 float3 dirXYZ = normalize( float3(dirX, dirY, dirZ) );  
   
 float3 starsColor = texNightStars.Sample( samplerAnisoWrap, dirXYZ ).rgb;  

Note to self: This is really well-thought and I definitely have to investigate it in more details.
Note to readers: If you know more about this operation, let me know!

Blinking stars

Another nice trick I wanted to investigate in more details is blinking of stars. If you walk around, let's say, outskirts of Novigrad City and sky is clear you can notice that stars are blinking.

I was curious how this was implemented. So the difference is quite big between the 2015 version and Blood & Wine. For simplicity I'll stay with 2015 version.

So we start just after sampling starsColor from the previous section:
  174: mul r0.w, v0.x, l(100.000000)  
  175: round_ni r1.w, r0.w  
  176: mad r2.w, v0.y, l(50.000000), cb0[0].x  
  177: round_ni r4.w, r2.w  
  178: bfrev r4.w, r4.w  
  179: iadd r5.x, r1.w, r4.w  
  180: ishr r5.y, r5.x, l(13)  
  181: xor r5.x, r5.x, r5.y  
  182: imul null, r5.y, r5.x, r5.x  
  183: imad r5.y, r5.y, l(0x0000ec4d), l(0.0000000000000000000000000000000000001)  
  184: imad r5.x, r5.x, r5.y, l(146956042240.000000)  
  185: and r5.x, r5.x, l(0x7fffffff)  
  186: itof r5.x, r5.x  
  187: mad r5.y, v0.x, l(100.000000), l(-1.000000)  
  188: round_ni r5.y, r5.y  
  189: iadd r4.w, r4.w, r5.y  
  190: ishr r5.z, r4.w, l(13)  
  191: xor r4.w, r4.w, r5.z  
  192: imul null, r5.z, r4.w, r4.w  
  193: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001)  
  194: imad r4.w, r4.w, r5.z, l(146956042240.000000)  
  195: and r4.w, r4.w, l(0x7fffffff)  
  196: itof r4.w, r4.w  
  197: add r5.z, r2.w, l(-1.000000)  
  198: round_ni r5.z, r5.z  
  199: bfrev r5.z, r5.z  
  200: iadd r1.w, r1.w, r5.z  
  201: ishr r5.w, r1.w, l(13)  
  202: xor r1.w, r1.w, r5.w  
  203: imul null, r5.w, r1.w, r1.w  
  204: imad r5.w, r5.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001)  
  205: imad r1.w, r1.w, r5.w, l(146956042240.000000)  
  206: and r1.w, r1.w, l(0x7fffffff)  
  207: itof r1.w, r1.w  
  208: mul r1.w, r1.w, l(0.000000001)  
  209: iadd r5.y, r5.z, r5.y  
  210: ishr r5.z, r5.y, l(13)  
  211: xor r5.y, r5.y, r5.z  
  212: imul null, r5.z, r5.y, r5.y  
  213: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001)  
  214: imad r5.y, r5.y, r5.z, l(146956042240.000000)  
  215: and r5.y, r5.y, l(0x7fffffff)  
  216: itof r5.y, r5.y  
  217: frc r0.w, r0.w  
  218: add r0.w, -r0.w, l(1.000000)  
  219: mul r5.z, r0.w, r0.w  
  220: mul r0.w, r0.w, r5.z  
  221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000)  
  222: mad r0.w, r0.w, l(-2.000000), r5.z  
  223: frc r2.w, r2.w  
  224: add r2.w, -r2.w, l(1.000000)  
  225: mul r5.z, r2.w, r2.w  
  226: mul r2.w, r2.w, r5.z  
  227: mul r5.z, r5.z, l(3.000000)  
  228: mad r2.w, r2.w, l(-2.000000), r5.z  
  229: mad r4.w, r4.w, l(0.000000001), -r5.x  
  230: mad r4.w, r0.w, r4.w, r5.x  
  231: mad r5.x, r5.y, l(0.000000001), -r1.w  
  232: mad r0.w, r0.w, r5.x, r1.w  
  233: add r0.w, -r4.w, r0.w  
  234: mad r0.w, r2.w, r0.w, r4.w  
  235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx  
  236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0  
  237: log r4.xyz, r4.xyzx  
  238: mul r4.xyz, r4.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)  
  239: exp r4.xyz, r4.xyzx  
  240: log r2.xyz, r2.xyzx  
  241: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)  
  242: exp r2.xyz, r2.xyzx  
  243: mul r2.xyz, r2.xyzx, r4.xyzx  

Huh. Let's take a look at the very end of this quite big piece of assembly.

Once we sampled starsColor in line 173 we calculate some offset value. This offset is used to perturb first sampling direction (r2.xyz, line 235), then we sample stars cubemap again, perform gamma correction on these two values (237-242) and multiply them together (243).

Simple, isn't it? Well, not really. Think about this offset for a while. It must be different across whole skydome - stars blinking the same way would look very unrealistic.

To make sure that offset will be as diverse as possible we will take advantage of UVs wrapped across skydome (v0.xy) and elapsed time from constant buffer (cb[0].x).

If you are unfamiliar with this intimidating ishr/xor/and thing, take a look at lightnings effect to learn more about integer noise.

So as you can see, integer noise is called here four times, but it's different now comparing to lightnings. To make results even more random the input integer for noise is a sum (iadd) and reversing bits is performed (reversebits instrinsic; bfrev instruction).

Okay, easy now. Let's start from start.
We have 4 "iterations" of integer noise. I analyzed the assembly and calculation of all 4 iterations looks like this:
 int getInt( float x )
 {
     return asint( floor(x) );
 }
 
 int getReverseInt( float x )
 {
     return reversebits( getInt(x) );
 }

 // * Inputs - UV and elapsed time in seconds  
 float2 starsUV;  
 starsUV.x = 100.0 * Input.TextureUV.x;       
 starsUV.y = 50.0  * Input.TextureUV.y + g_fTime;  
             
 // * Iteration 1  
 int iStars1_A = getReverseInt( starsUV.y );
 int iStars1_B = getInt( starsUV.x );            
   
 float fStarsNoise1 = integerNoise( iStars1_A + iStars1_B );  
             
   
 // * Iteration 2  
 int iStars2_A = getReverseInt( starsUV.y );  
 int iStars2_B = getInt( starsUV.x - 1.0 ); 
   
 float fStarsNoise2 = integerNoise( iStars2_A + iStars2_B );  
        
   
 // * Iteration 3  
 int iStars3_A = getReverseInt( starsUV.y - 1.0 );
 int iStars3_B = getInt( starsUV.x );
   
 float fStarsNoise3 = integerNoise( iStars3_A + iStars3_B );  
             
   
 // * Iteration 4  
 int iStars4_A = getReverseInt( starsUV.y - 1.0 ); 
 int iStars4_B = getInt( starsUV.x - 1.0 ); 
   
 float fStarsNoise4 = integerNoise( iStars4_A + iStars4_B );  

The final outputs of all these 4 iterations are (follow itof instructions to find them):

Iteration 1 - r5.x,
Iteration 2 - r4.w,
Iteration 3 - r1.w,
Iteration 4 - r5.y

After the last itof (line 216) we have:
  217: frc r0.w, r0.w   
  218: add r0.w, -r0.w, l(1.000000)   
  219: mul r5.z, r0.w, r0.w   
  220: mul r0.w, r0.w, r5.z   
  221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000)   
  222: mad r0.w, r0.w, l(-2.000000), r5.z   
  223: frc r2.w, r2.w   
  224: add r2.w, -r2.w, l(1.000000)   
  225: mul r5.z, r2.w, r2.w   
  226: mul r2.w, r2.w, r5.z   
  227: mul r5.z, r5.z, l(3.000000)   
  228: mad r2.w, r2.w, l(-2.000000), r5.z   

These lines calculate values for S-curve for weights based on fractional part of UVs, just like in case of lightnings. So:

  float s_curve( float x )   
  {   
    float x2 = x * x;   
    float x3 = x2 * x;   
      
    // -2x^3 + 3x^2   
    return -2.0*x3 + 3.0*x2;   
  }  
   
 ...  
 
 // lines 217-222
 float weightX = 1.0 - frac( starsUV.x );  
 weightX = s_curve( weightX );  
   
 // lines 223-228
 float weightY = 1.0 - frac( starsUV.y );  
 weightY = s_curve( weightY );  

As you can expect, these factors serve to interpolate noise smoothly and generate final offset for sampling coordinates:
  229: mad r4.w, r4.w, l(0.000000001), -r5.x   
  230: mad r4.w, r0.w, r4.w, r5.x   
  float noise0 = lerp( fStarsNoise1, fStarsNoise2, weightX );  
   
  231: mad r5.x, r5.y, l(0.000000001), -r1.w   
  232: mad r0.w, r0.w, r5.x, r1.w   
  float noise1 = lerp( fStarsNoise3, fStarsNoise4, weightX );  
   
  233: add r0.w, -r4.w, r0.w   
  234: mad r0.w, r2.w, r0.w, r4.w   
  float offset = lerp( noise0, noise1, weightY );            
   
  235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx   
  236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0   
  float3 starsPerturbedDir = dirXYZ + offset * 0.0005;  
    
  float3 starsColorDisturbed = texNightStars.Sample( samplerAnisoWrap, starsPerturbedDir ).rgb;

Here is small visualization of calculated offset:

Once we have starsColorDisturbed, the hardest part is over. Phew!

The next step is to perform gamma correction on both starsColor and starsColorDisturbed and multiply them:
  starsColor = pow( starsColor, 2.2 );  
  starsColorDisturbed = pow( starsColorDisturbed, 2.2 );  
   
  float3 starsFinal = starsColor * starsColorDisturbed;  

Stars - the final touches

We have starsFinal in r1.xyz. What's happening at the end of processing stars is this:
  256: log r1.xyz, r1.xyzx  
  257: mul r1.xyz, r1.xyzx, l(2.500000, 2.500000, 2.500000, 0.000000)  
  258: exp r1.xyz, r1.xyzx  
  259: min r1.xyz, r1.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000)  
  260: add r0.w, -cb0[9].w, l(1.000000)  
  261: mul r1.xyz, r0.wwww, r1.xyzx  
  262: mul r1.xyz, r1.xyzx, l(10.000000, 10.000000, 10.000000, 0.000000)  

This is much, much easier comparing to blinking and moving stars.
So we start with raising starsFinal to power of 2.5 - this allows to control density of stars. Pretty clever. Then, we make sure the maximum color of stars is float3(1, 1, 1).

cb0[9].w is used to control general visibility of stars. So in daytime expect this to be set to 1.0 (which yields in multiplying by zero) and 0.0 during nights.

At the end we boost visibility of stars by 10. And this is over! :)

Summary

In this post I presented some cool tricks I found while investigating sky shader from The Witcher 3. I hope you enjoyed it. Thanks for reading!

Take care,
M.

Monday, March 4, 2019

Reverse engineering the rendering of The Witcher 3, part 11 - lightnings

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


Welcome back!

In the 11th part of the series we will take a look how lightnings are rendered in The Witcher 3: Wild Hunt.

Following the distant rain shafts effect, lightnings are rendered slightly after, but still in forward pass. You can see them in action in the following video:



They last for a very short time; therefore, it will be the best to play this video with 0.25 speed.
You can see that they are not static images; their intensity slightly changes with time.

There are many similarities here with distant rain shafts in terms of rendernig nuances, like the same blending (additive blending) and depth (test enabled, no depth write) states.

Scene without lightning

Scene with lightning
In terms of geometry, lightnings in The Witcher 3 are tree-like meshes, this particular lightning is represented by the following one:

It's provided with UV coordinates and normal vectors. They will be useful in the vertex shader stage.

Vertex Shader

Let's take a look at the assembly of the vertex shader:

 vs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb2[6], immediateIndexed  
    dcl_input v0.xyz  
    dcl_input v1.xy  
    dcl_input v2.xyz  
    dcl_input v4.xyzw  
    dcl_input v5.xyzw  
    dcl_input v6.xyzw  
    dcl_input v7.xyzw  
    dcl_output o0.xy  
    dcl_output o1.xyzw  
    dcl_output_siv o2.xyzw, position  
    dcl_temps 3  
   0: mov o0.xy, v1.xyxx  
   1: mov o1.xyzw, v7.xyzw  
   2: mul r0.xyzw, v5.xyzw, cb1[0].yyyy  
   3: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw  
   4: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw  
   5: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw  
   6: mov r1.w, l(1.000000)  
   7: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx  
   8: dp4 r2.x, r1.xyzw, v4.xyzw  
   9: dp4 r2.y, r1.xyzw, v5.xyzw  
  10: dp4 r2.z, r1.xyzw, v6.xyzw  
  11: add r2.xyz, r2.xyzx, -cb1[8].xyzx  
  12: dp3 r1.w, r2.xyzx, r2.xyzx  
  13: rsq r1.w, r1.w  
  14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w  
  15: mul r1.w, r1.w, l(0.000001)  
  16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000)  
  17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx  
  18: mov r1.w, l(1.000000)  
  19: dp4 o2.x, r1.xyzw, r0.xyzw  
  20: mul r0.xyzw, v5.xyzw, cb1[1].yyyy  
  21: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw  
  22: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw  
  23: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw  
  24: dp4 o2.y, r1.xyzw, r0.xyzw  
  25: mul r0.xyzw, v5.xyzw, cb1[2].yyyy  
  26: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw  
  27: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw  
  28: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw  
  29: dp4 o2.z, r1.xyzw, r0.xyzw  
  30: mul r0.xyzw, v5.xyzw, cb1[3].yyyy  
  31: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw  
  32: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw  
  33: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw  
  34: dp4 o2.w, r1.xyzw, r0.xyzw  
  35: ret  

There are many similarities here compared to the vertex shader from the one from distant rain shafts so I won't repeat myself. The one major difference I want to show are lines 11-18:

  11: add r2.xyz, r2.xyzx, -cb1[8].xyzx  
  12: dp3 r1.w, r2.xyzx, r2.xyzx  
  13: rsq r1.w, r1.w  
  14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w  
  15: mul r1.w, r1.w, l(0.000001)  
  16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000)  
  17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx  
  18: mov r1.w, l(1.000000)  
  19: dp4 o2.x, r1.xyzw, r0.xyzw  

To start, cb1[8].xyz is camera position while r2.xyz is world-space position, so line 11 calculates vector from camera to world position. Then, lines 12-15 compute length( worldPos - cameraPos) * 0.000001.

v2.xyz is normal vector from input geometry. Line 16 unpacks it from [0-1] range to [-1;1] one.
Then, the final world-space position is calculated:

finalWorldPos = worldPos + length( worldPos - cameraPos) * 0.000001 * normalVector


HLSL snippet for this operation would be something like this:

      ...  
      // final world-space position  
      float3 vNormal = Input.NormalW * 2.0 - 1.0;  
      float lencameratoworld = length( PositionL - g_cameraPos.xyz) * 0.000001;  
   
      PositionL += vNormal*lencameratoworld;  
   
      // SV_Posiiton   
      float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld );   
      Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) );      
   
      return Output;   

Such operation causes slight "explosion" of the mesh (in the direction of normal vector). I did some simple experiments and replaced 0.000001 with a few different values, see the results:

0.000002

0.000005

0.00001

0.000025

Pixel Shader

Ok, we're done with the vertex shader, time to see the assembly of pixel shader!
 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[1], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb4[5], immediateIndexed  
    dcl_input_ps linear v0.x  
    dcl_input_ps linear v1.w  
    dcl_output o0.xyzw  
    dcl_temps 1  
   0: mad r0.x, cb0[0].x, cb4[4].x, v0.x  
   1: add r0.y, r0.x, l(-1.000000)  
   2: round_ni r0.y, r0.y  
   3: ishr r0.z, r0.y, l(13)  
   4: xor r0.y, r0.y, r0.z  
   5: imul null, r0.z, r0.y, r0.y  
   6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001)  
   7: imad r0.y, r0.y, r0.z, l(146956042240.000000)  
   8: and r0.y, r0.y, l(0x7fffffff)  
   9: round_ni r0.z, r0.x  
  10: frc r0.x, r0.x  
  11: add r0.x, -r0.x, l(1.000000)  
  12: ishr r0.w, r0.z, l(13)  
  13: xor r0.z, r0.z, r0.w  
  14: imul null, r0.w, r0.z, r0.z  
  15: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001)  
  16: imad r0.z, r0.z, r0.w, l(146956042240.000000)  
  17: and r0.z, r0.z, l(0x7fffffff)  
  18: itof r0.yz, r0.yyzy  
  19: mul r0.z, r0.z, l(0.000000001)  
  20: mad r0.y, r0.y, l(0.000000001), -r0.z  
  21: mul r0.w, r0.x, r0.x  
  22: mul r0.x, r0.x, r0.w  
  23: mul r0.w, r0.w, l(3.000000)  
  24: mad r0.x, r0.x, l(-2.000000), r0.w  
  25: mad r0.x, r0.x, r0.y, r0.z  
  26: add r0.y, -cb4[2].x, cb4[3].x  
  27: mad_sat r0.x, r0.x, r0.y, cb4[2].x  
  28: mul r0.x, r0.x, v1.w  
  29: mul r0.yzw, cb4[0].xxxx, cb4[1].xxyz  
  30: mul r0.xyzw, r0.xyzw, cb2[2].wxyz  
  31: mul o0.xyz, r0.xxxx, r0.yzwy  
  32: mov o0.w, r0.x  
  33: ret  

The good thing: this code is not that long.
The bad thing:
   3: ishr r0.z, r0.y, l(13)  
   4: xor r0.y, r0.y, r0.z  
   5: imul null, r0.z, r0.y, r0.y  
   6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001)  
   7: imad r0.y, r0.y, r0.z, l(146956042240.000000)  
   8: and r0.y, r0.y, l(0x7fffffff)  
...what the heck is this???

To be honest, it's not the first time when I see that piece of cra... assembly in shaders of The Witcher 3. But when I found it for the first time, I was like: "oh crap, wtf is this?".

Indeed, you can find something like this in a few shaders of TW3. I won't go now through many adventures I've had with this one, but let me just say the answer is integer noise:

 // For more details see: http://libnoise.sourceforge.net/noisegen/  
 float integerNoise( int n )  
 {  
      n = (n >> 13) ^ n;  
      int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff;  
      return ((float)nn / 1073741824.0);  
 }  

Phew. As you can see, it's invoked 2 times in the pixel shader. Following the guidance from its website gives us some useful tips how to implement smooth noise properly. I'll back to this in a minute.

Take a look at line 0, we perform animation here based on the following formula:

animation = elapsedTime * animationSpeed + TextureUV.x

These values, after being floored (round_ni instruction) are subsequent entry points to integer noise. Generally we calculate value of the noise for two integers and then calculate final, interpolated value between them (see libnoise's website for details).

Okay, this is integer noise while all previously mentioned values (also floored values) are floats!
Please notice that there are no ftoi instructions here. My guess is that programmers from CD Projekt Red used here asint HLSL intrinsic function which performs "reinterpret_cast" of floating-point values and treat it like integer pattern.

The interpolation weight for two values is calculated in lines 10-11

interpolationWeight = 1.0 - frac( animation );

Such approach allows to interpolate between values with time.
To make a smooth noise, this interpolant is passed to SCurve function:
 float s_curve( float x )  
 {  
   float x2 = x * x;  
   float x3 = x2 * x;  
     
   // -2x^3 + 3x^2  
   return -2.0*x3 + 3.0*x2;  
 }

Smoothstep function [libnoise.sourceforge.net]

This function is known as "smoothstep". But as you can see from the assembly, this is not  smoothstep instrinsic from HLSL. The intrinsic performs some clamps to make sure the values will be correct. Since we know that our interpolationWeight will always be in [0-1] range we can safely omit these checks.

Calculating the final value includes a few multiplications. Please take a look that the final output alpha can change, depending on noise value. This is handy, because it will affect opacity of the rendered lightning - just like in real life.

The final pixel shader:
 cbuffer cbPerFrame : register (b0)  
 {  
   float4 cb0_v0;  
   float4 cb0_v1;  
   float4 cb0_v2;  
   float4 cb0_v3;  
 }  
   
 cbuffer cbPerFrame : register (b2)  
 {  
   float4 cb2_v0;  
   float4 cb2_v1;  
   float4 cb2_v2;  
   float4 cb2_v3;  
 }  
   
 cbuffer cbPerFrame : register (b4)  
 {  
   float4 cb4_v0;  
   float4 cb4_v1;  
   float4 cb4_v2;  
   float4 cb4_v3;  
   float4 cb4_v4;  
 }  
   
 struct VS_OUTPUT  
 {  
   float2 Texcoords : Texcoord0;  
   float4 InstanceLODParams : INSTANCE_LOD_PARAMS;  
   float4 PositionH : SV_Position;  
 };  
   
 // Shaders in TW3 use integer noise.  
 // For more details see: http://libnoise.sourceforge.net/noisegen/  
 float integerNoise( int n )  
 {  
   n = (n >> 13) ^ n;  
   int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff;  
   return ((float)nn / 1073741824.0);  
 }  
   
 float s_curve( float x )  
 {  
   float x2 = x * x;  
   float x3 = x2 * x;  
   
   // -2x^3 + 3x^2  
   return -2.0*x3 + 3.0*x2;  
 }  
   
 float4 Lightning_TW3_PS( in VS_OUTPUT Input ) : SV_Target
 {  
   // * Inputs  
   float elapsedTime = cb0_v0.x;  
   float animationSpeed = cb4_v4.x;  
   
   float minAmount = cb4_v2.x;  
   float maxAmount = cb4_v3.x;  
   
   float colorMultiplier = cb4_v0.x;  
   float3 colorFilter = cb4_v1.xyz;  
   float3 lightningColorRGB = cb2_v2.rgb;  
   
   
   // Animation using time and X texcoord  
   float animation = elapsedTime * animationSpeed + Input.Texcoords.x;  
   
   // Input parameters for Integer Noise.  
   // They are floored and please note there are using asint.  
   // That might be an optimization to avoid "ftoi" instructions.  
   int intX0 = asint( floor(animation) );  
   int intX1 = asint( floor(animation-1.0) );  
   
   float n0 = integerNoise( intX0 );  
   float n1 = integerNoise( intX1 );    
   
   // We interpolate "backwards" here.  
   float weight = 1.0 - frac(animation);  
   
   // Following the instructions from libnoise, we perform  
   // smooth interpolation here with cubic s-curve function.  
   float noise = lerp( n0, n1, s_curve(weight) );  
   
   // Make sure we are in [0.0 - 1.0] range.  
   float lightningAmount = saturate( lerp(minAmount, maxAmount, noise) );  
   lightningAmount *= Input.InstanceLODParams.w;    // 1.0  
   lightningAmount *= cb2_v2.w;             // 1.0  
   
   // Calculate final lightning color   
   float3 lightningColor = colorMultiplier * colorFilter;  
   lightningColor *= lighntingColorRGB;  
   
   float3 finalLightningColor = lightningColor * lightningAmount;  
   return float4( finalLightningColor, lightningAmount );  
 }  

Summary

In this post I described how lightnings are rendered in The Witcher 3.
I'm more than happy that output assembly from my shader is the same as the original one!

On the left - my shader, on the right - original assembly


I hope you enjoyed it! Thanks for reading.

Feel free to comment and take care,
M.

Sunday, January 20, 2019

Reverse engineering the rendering of The Witcher 3, part 10 - distant rain shafts

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


Welcome to the 10th part of the series! Woohooo! :)

See previous parts here.

This time we will take a look at really cool atmospheric effect I really like - distant rain/light shafts near the horizon. The easiest way to encounter them in the game is visiting Skellige Islands.



Personally I really love such atmospherical phenomena and I was really curious how graphics programmers from CD Projekt Red implemented this. Let's find out!

Here are two screenshots before and after applying rain shafts:

Before rain shafts

After rain shafts

Geometry

Our first stop is geometry. The idea is to use small cylinder:
Cylinder in local space
In terms of local space position, it's pretty small - the range of position is in ( 0.0 - 1.0 ).

The input layout for this draw call looks like this...

What is important to us here: Texcoords and Instance_Transform.

Texcoords are wrapped pretty simply: U of both upper and lower base is in [0.02777 - 1.02734] range. V is equal to 1.0 on lower base and equal to 0.0 on upper one. As you can see, it's pretty simple to even generate this mesh procedurally.

As we have this small cylinder in local space, we multiply it by world matrix which is provided by per-instance INSTANCE_TRANSFORM input element. Let's check values of this matrix:



It looks quite intimidating, isn't it? Don't worry, let's do some decomposing and see what this matrix hides!
 XMMATRIX mat( -227.7472,  159.8043,  374.0736, -116.4951,  
               -194.7577, -173.3836, -494.4982,  238.6908,  
               -14.16466, -185.4743,  784.564,   -1.45565,  
                0.0, 0.0, 0.0, 1.0 );  
   
      mat = XMMatrixTranspose( mat );  
   
      XMVECTOR vScale;  
      XMVECTOR vRotateQuat;  
      XMVECTOR vTranslation;  
      XMMatrixDecompose( &vScale, &vRotateQuat, &vTranslation, mat );  
   
      // Rotation matrix...  
      XMMATRIX matRotate = XMMatrixRotationQuaternion( vRotateQuat );  

Results are really interesting:

vRotateQuat: (0.0924987569, -0.314900011, 0.883411944, -0.334462732)
vScale: (299.999969, 300.000000, 1000.00012)
vTranslation: (-116.495102, 238.690796, -1.45564997)

It's important to know camera position at this particular frame: ( -116.5338, 234.8695, 2.09 )

As you can see, we scale the cylinder to make it pretty big in world space ( in TW3 Z axis is up-one), translate it with respect to camera position and rotate.
Here is the cylinder after vertex shader transform:

Cylinder after transforming by vertex shader. See how it's placed relative to view frustum


Vertex Shader

Input geometry and vertex shader are are strictly dependent on each other.
Let's take a closer look at assembly of vertex shader:

 vs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb1[7], immediateIndexed  
    dcl_constantbuffer cb2[6], immediateIndexed  
    dcl_input v0.xyz  
    dcl_input v1.xy  
    dcl_input v4.xyzw  
    dcl_input v5.xyzw  
    dcl_input v6.xyzw  
    dcl_input v7.xyzw  
    dcl_output o0.xyz  
    dcl_output o1.xyzw  
    dcl_output_siv o2.xyzw, position  
    dcl_temps 2  
   0: mov o0.xy, v1.xyxx  
   1: mul r0.xyzw, v5.xyzw, cb1[6].yyyy  
   2: mad r0.xyzw, v4.xyzw, cb1[6].xxxx, r0.xyzw  
   3: mad r0.xyzw, v6.xyzw, cb1[6].zzzz, r0.xyzw  
   4: mad r0.xyzw, cb1[6].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw  
   5: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx  
   6: mov r1.w, l(1.000000)  
   7: dp4 o0.z, r1.xyzw, r0.xyzw  
   8: mov o1.xyzw, v7.xyzw  
   9: mul r0.xyzw, v5.xyzw, cb1[0].yyyy  
  10: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw  
  11: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw  
  12: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw  
  13: dp4 o2.x, r1.xyzw, r0.xyzw  
  14: mul r0.xyzw, v5.xyzw, cb1[1].yyyy  
  15: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw  
  16: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw  
  17: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw  
  18: dp4 o2.y, r1.xyzw, r0.xyzw  
  19: mul r0.xyzw, v5.xyzw, cb1[2].yyyy  
  20: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw  
  21: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw  
  22: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw  
  23: dp4 o2.z, r1.xyzw, r0.xyzw  
  24: mul r0.xyzw, v5.xyzw, cb1[3].yyyy  
  25: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw  
  26: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw  
  27: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw  
  28: dp4 o2.w, r1.xyzw, r0.xyzw  
  29: ret  

Together with simple passing of Texcoords (line 0) and Instance_LOD_Params (line 8), two more things are needed for output: SV_Position (obviously) and Height (.z component) of world position.

Remember that local space is in [0-1] range? Well, the vertex shader uses scale&bias to adjust local position just before applying world matrix. Smart!
In this case, we have scale = float3(4, 4, 2) and bias = float3(-2, -2, -1).

The pattern you can notice between line 9 and 28 is multiplying two row-major martices.
Just take a look at final vertex shader in HLSL :)
 cbuffer cbPerFrame : register (b1)  
 {  
   row_major float4x4 g_viewProjMatrix;  
   row_major float4x4 g_rainShaftsViewProjMatrix;  
 }  
   
 cbuffer cbPerObject : register (b2)  
 {  
   float4x4 g_mtxWorld;  
   float4 g_modelScale;  
   float4 g_modelBias;  
 }  
   
 struct VS_INPUT  
 {  
   float3 PositionW : POSITION;  
   float2 Texcoord : TEXCOORD;  
   float3 NormalW : NORMAL;  
   float3 TangentW : TANGENT;  
   float4 InstanceTransform0 : INSTANCE_TRANSFORM0;  
   float4 InstanceTransform1 : INSTANCE_TRANSFORM1;  
   float4 InstanceTransform2 : INSTANCE_TRANSFORM2;  
   float4 InstanceLODParams  : INSTANCE_LOD_PARAMS;  
 };  
   
 struct VS_OUTPUT  
 {  
   float3 TexcoordAndZ : Texcoord0;  
   
   float4 LODParams : LODParams;  
   float4 PositionH : SV_Position;  
 };  
   
 VS_OUTPUT RainShaftsVS( VS_INPUT Input )  
 {  
   VS_OUTPUT Output = (VS_OUTPUT)0;  
   
   // simple data passing  
   Output.TexcoordAndZ.xy = Input.Texcoord;  
   Output.LODParams = Input.InstanceLODParams;  
   
   // world space  
   float3 meshScale = g_modelScale.xyz;  // float3( 4, 4, 2 );
   float3 meshBias =  g_modelBias.xyz;   // float3( -2, -2, -1 );
   float3 PositionL = Input.PositionW * meshScale + meshBias;  
   
   // Manually build instanceWorld matrix from float4s:  
   float4x4 matInstanceWorld = float4x4(Input.InstanceTransform0, Input.InstanceTransform1,  
   Input.InstanceTransform2 , float4(0, 0, 0, 1) );  
   
   // World-space Height (.z)  
   float4x4 matWorldInstanceLod = mul( g_rainShaftsViewProjMatrix, matInstanceWorld );  
   Output.TexcoordAndZ.z = mul( float4(PositionL, 1.0), transpose(matWorldInstanceLod) ).z;  
   
   // SV_Posiiton  
   float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld );  
   Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) );       
 
   return Output;  
 }   


Comparison between my VS (left) and original one (right):


The differences do not affect calculations ;) I injected my VS into frame and everything is alright!


Pixel Shader

Finally....! For start, I'll show you inputs:
There are two textures involved: noise texture and depth buffer:



Values from constant buffers:





And pixel shader assembly:
 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[8], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb12[23], immediateIndexed  
    dcl_constantbuffer cb4[8], immediateIndexed  
    dcl_sampler s0, mode_default  
    dcl_sampler s15, mode_default  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t15  
    dcl_input_ps linear v0.xyz  
    dcl_input_ps linear v1.w  
    dcl_input_ps_siv v2.xy, position  
    dcl_output o0.xyzw  
    dcl_temps 1  
   0: mul r0.xy, cb0[0].xxxx, cb4[5].xyxx  
   1: mad r0.xy, v0.xyxx, cb4[4].xyxx, r0.xyxx  
   2: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t0.xyzw, s0  
   3: add r0.y, -cb4[2].x, cb4[3].x  
   4: mad_sat r0.x, r0.x, r0.y, cb4[2].x  
   5: mul r0.x, r0.x, v0.y  
   6: mul r0.x, r0.x, v1.w  
   7: mul r0.x, r0.x, cb4[1].x  
   8: mul r0.yz, v2.xxyx, cb0[1].zzwz  
   9: sample_l(texture2d)(float,float,float,float) r0.y, r0.yzyy, t15.yxzw, s15, l(0)  
  10: mad r0.y, r0.y, cb12[22].x, cb12[22].y  
  11: mad r0.y, r0.y, cb12[21].x, cb12[21].y  
  12: max r0.y, r0.y, l(0.000100)  
  13: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y  
  14: add r0.y, r0.y, -v0.z  
  15: mul_sat r0.y, r0.y, cb4[6].x  
  16: mul_sat r0.x, r0.y, r0.x  
  17: mad r0.y, cb0[7].y, r0.x, -r0.x  
  18: mad r0.x, cb4[7].x, r0.y, r0.x  
  19: mul r0.xyz, r0.xxxx, cb4[0].xyzx  
  20: log r0.xyz, r0.xyzx  
  21: mul r0.xyz, r0.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)  
  22: exp r0.xyz, r0.xyzx  
  23: mul r0.xyz, r0.xyzx, cb2[2].xyzx  
  24: mul o0.xyz, r0.xyzx, cb2[2].wwww  
  25: mov o0.w, l(0)  
  26: ret  

Phew! Quite a lot of stuff, but actually it's not that bad.


So, what happens here? At first we calculate animated UVs using elapsed time from cbuffer (cb0[0].x) and some scale/offsets. These texcoords are used to sample from noise texture (line 2).

Once we have noise value from texture, we interpolate between min/max values (usually 0 and 1).
Then we perform some multiplications, like by V tex coordinate (remember that V coordinate comes from 1 to 0?) - line 5

This way we calculated "intensity mask" - it looks like this:



Notice that distant objects (lighthouse, mountains...) are gone. This happens because the cylinder passes depth test - the cylinder is not on far plane and is drawn in front of aforementioned objects:
depth test
We want to mimic that the rain shafts are further (not necessarily on far plane, though). To achieve that, we compute another mask, "far objects mask".

So we compute it with the following formula:
farObjectsMask = saturate( (FrustumDepth - CylinderWorldSpaceHeight) * 0.001 );

(0.001 comes from cbuffer)

which gives us desired mask:


( I explained a bit how frustum depth is extracted from depth buffer in my post about sharpen )

Personally, I think this could be done cheaper, without calculating world-space height in VS by multiplying frustum depth by smaller number, like 0.0004.


Then we multiply both masks, which yields the final one:


Having this final mask (line 16) we have another interpolation which pretty much does nothing (at least in tested scenario), then we multiply the final mask with shafts color (line 19), perform gamma correction (lines 20-22) and perform final multiplications (23-24).

At the end we return color with zero alpha. This is because blending is enabled in this pass:

FinalColor = SourceColor * 1.0 + (1.0 - SourceAlpha) * DestColor.

If you are a bit rusty how blending works, a quick explanation:
SourceColor is RGB output from the pixel shader while DestColor is current RGB color of pixel in render target. Because SourceAlpha is always 0.0, the afromentioned equation simplifies to:

FinalColor = SourceColor + DestColor.

Simply speaking, we perform additive blending here. If this pixel shader returns (0, 0, 0) the color will remain the same.

Here is final HLSL - I think after this description it will be much easier to follow:
 struct VS_OUTPUT  
 {  
   float3 TexcoordAndWorldspaceHeight : Texcoord0;  
   float4 LODParams : LODParams;    // float4(1,1,1,1)  
   float4 PositionH : SV_Position;  
 };  
   
 float getFrustumDepth( in float depth )  
 {  
   // from [1-0] to [0-1]  
   float d = depth * cb12_v22.x + cb12_v22.y;  
   
   // special coefficents  
   d = d * cb12_v21.x + cb12_v21.y;  
   
   // return frustum depth  
   return 1.0 / max(d, 1e-4);  
 }  
   
 float4 EditedShaderPS( in VS_OUTPUT Input ) : SV_Target0  
 {  
   // * Input from Vertex Shader  
   float2 InputUV = Input.TexcoordAndWorldspaceHeight.xy;  
   float WorldHeight = Input.TexcoordAndWorldspaceHeight.z;  
   float LODParam = Input.LODParams.w;  
   
   // * Inputs  
   float elapsedTime = cb0_v0.x;  
   float2 uvAnimation = cb4_v5.xy;  
   float2 uvScale = cb4_v4.xy;    
   float minValue = cb4_v2.x; // 0.0  
   float maxValue = cb4_v3.x; // 1.0  
   float3 shaftsColor = cb4_v0.rgb;  // RGB( 147, 162, 173 )  
   
   float3 finalColorFilter = cb2_v2.rgb; // float3( 1.175, 1.296, 1.342 );  
   float finalEffectIntensity = cb2_v2.w;  
   
   float2 invViewportSize = cb0_v1.zw;  
   
   float depthScale = cb4_v6.x;  // 0.001  
   
   // sample noise  
   float2 uvOffsets = elapsedTime * uvAnimation;  
   float2 uv = InputUV * uvScale + uvOffsets;    
   float disturb = texture0.Sample( sampler0, uv ).x;  
   
   // * Intensity mask  
   float intensity = saturate( lerp(minValue, maxValue, disturb) );  
   intensity *= InputUV.y;   // transition from (0, 1)  
   intensity *= LODParam;   // usually 1.0  
   intensity *= cb4_v1.x;   // 1.0    
   
   // Sample depth  
   float2 ScreenUV = Input.PositionH.xy * invViewportSize;  
   float hardwareDepth = texture15.SampleLevel( sampler15, ScreenUV, 0 ).x;  
   float frustumDepth = getFrustumDepth( hardwareDepth );  
   
   
   // * Calculate mask covering distant objects behind cylinder.  
   
   // Seems that the input really is world-space height (.z component, see vertex shader)  
   float depth = frustumDepth - WorldHeight;  
   float distantObjectsMask = saturate( depth * depthScale );  
   
   // * calculate final mask  
   float finalEffectMask = saturate( intensity * distantObjectsMask );  
   
   // cb0_v7.y and cb4_v7.x are set to 1.0 so I didn't bother with naming them :)  
   float paramX = finalEffectMask;  
   float paramY = cb0_v7.y * finalEffectMask;  
   float effectAmount = lerp(paramX, paramY, cb4_v7.x);  
   
   // color of shafts comes from contant buffer  
   float3 effectColor = effectAmount * shaftsColor;  
   
   // gamma correction  
   effectColor = pow(effectColor, 2.2);  
   
   // final multiplications  
   effectColor *= finalColorFilter;  
   effectColor *= finalEffectIntensity;  
   
   // return with zero alpha 'cause the blending used here is:  
   // SourceColor * 1.0 + (1.0 - SrcAlpha) * DestColor  
   return float4( effectColor, 0.0 );  
 }   

I'm happy to say that my PS produces the same assembly as original is ;)

I hope you enjoyed it.
Thanks for reading! :)

M.