Saturday, December 22, 2018

Reverse engineering the rendering of The Witcher 3, part 8 - The Moon and lunar phases

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


Welcome,

In the 8th part of this series I will investigate the Moon shader from The Witcher 3 (more specifically, from "Blood and Wine" expansion pack).

The Moon is an important element of night sky and can be quite challenging to make it believable, but in TW3 for me it's just a pleasure to walk around during the night.
Just take a look at this scene!


Before I will get to the pixel shader, few words about rendering nuances. In terms of geometry it's just a sphere (see below) which comes with texture coordinates, normal and tangent vectors. The vertex shader calculates world space position as well as normalized normal, tangent, and bitangent (using cross product) vectors multiplied by world matrix.
To make sure that the Moon lies completely on far plane, MinDepth and MaxDepth fields of D3D11_VIEWPORT structure are set to 0.0 (the same trick is used for skydome). The Moon is rendered just after sky.

Sphere used to draw the Moon
Alright, I think we are ready to go. Let's see the pixel shader:
 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[1], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb12[267], immediateIndexed  
    dcl_sampler s0, mode_default  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_input_ps linear v1.w  
    dcl_input_ps linear v2.xyzw  
    dcl_input_ps linear v3.xy  
    dcl_input_ps linear v4.xy  
    dcl_output o0.xyzw  
    dcl_temps 3  
   0: mov r0.x, -cb0[0].w  
   1: mov r0.y, l(0)  
   2: add r0.xy, r0.xyxx, v2.xyxx  
   3: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, r0.xyxx, t0.xyzw, s0  
   4: add r0.xyz, r0.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)  
   5: log r0.w, r0.w  
   6: mul r0.w, r0.w, l(2.200000)  
   7: exp r0.w, r0.w  
   8: add r0.xyz, r0.xyzx, r0.xyzx  
   9: dp3 r1.x, r0.xyzx, r0.xyzx  
  10: rsq r1.x, r1.x  
  11: mul r0.xyz, r0.xyzx, r1.xxxx  
  12: mul r1.xy, r0.yyyy, v3.xyxx  
  13: mad r0.xy, v4.xyxx, r0.xxxx, r1.xyxx  
  14: mad r0.xy, v2.zwzz, r0.zzzz, r0.xyxx  
  15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w  
  16: mul r0.z, r0.z, l(6.283185)  
  17: sincos r1.x, r2.x, r0.z  
  18: mov r2.y, r1.x  
  19: dp2_sat r0.x, r0.xyxx, r2.xyxx  
  20: mul r0.xyz, r0.xxxx, cb12[266].xyzx  
  21: mul r0.xyz, r0.xyzx, r0.wwww  
  22: mul r0.xyz, r0.xyzx, cb2[2].xyzx  
  23: add_sat r0.w, -v1.w, l(1.000000)  
  24: mul r0.w, r0.w, cb2[2].w  
  25: mul o0.xyz, r0.wwww, r0.xyzx  
  26: mov o0.w, l(0)  
  27: ret  

The main reason I selected shader from "Blood and Wine" expansions pack is simple - it's shorter ;)

At first we calculate offset for texture sampling.
cb0[0].w is used as offset along X axis. Using this simple trick we can simulate rotation of the Moon along its axis.

Example values from constant buffer


There is one texture  (1024x512) attached as input. We have normal map encoded in RGB channels and in alpha channel - color of the Moon's surface. Smart!

Alpha channel of the texture - color of the Moon's surface. (c) CD Projekt Red

RGB channels of the texture - normal map. (c) CD Projekt Red
Once we have proper texture coordinates, we sample RGBA channels. We have to unpack normal map and perform gamma correction of surface color. So far our HLSL shader can be written for example like this:
 float4 MoonPS(in InputStruct IN) : SV_Target0  
 {  
   // Texcoords offset  
   float2 uvOffsets = float2(-cb0_v0.w, 0.0);  
     
   // Final texcoords  
   float2 uv = IN.param2.xy + uvOffsets;  
   
   // Sample texture  
   float4 sampledTexture = texture0.Sample( sampler0, uv);  
   
   // Moon surface color - perform gamma correction  
   float moonColorTex = pow(sampledTexture.a, 2.2 );  
   
   // Unpack normal from [0,1] to [-1,1] range.  
   // Note: sampledTexture.xyz * 2.0 - 1.0 works the same way  
   float3 sampledNormal = normalize((sampledTexture.xyz - 0.5) * 2);  

The next is step is to perform normal mapping, but only on XY components. (In The Witcher 3, Z-axis is up and whole Z channel of the texture is 1.0) . We can do it like this:
   // Tangent space vectors  
   float3 Tangent = IN.param4.xyz;  
   float3 Normal = float3(IN.param2.zw, IN.param3.w);  
   float3 Bitangent = IN.param3.xyz;  
        
   // TBN matrix   
   float3x3 TBN = float3x3(Tangent, Bitangent, Normal);  
        
   // Calculate XY normal vector  
   // Squeeze TBN matrix to float3x2: 3 rows, 2 columns  
   float2 vNormal = mul(sampledNormal, (float3x2)TBN).xy;  

Now it's time for my favourite part of this shader. Take a look at lines 15-16 again:
  15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w  
  16: mul r0.z, r0.z, l(6.283185)

Well, what's this mysterious 0.033864? It seems to make no sense at first sight, but if we calculate its reciprocal, we'll get ~29.53, which is length of synodic month in days! Now this is what I call attention to detail!
We can safely assume that cb0[0].y is number of days which passed during gameplay. Additional bias which was used as X-axis offset of texture is used here.

Once we have this ratio, we multiply it by 2*Pi.
Then, using sincos, we calculate another 2d vector.

By calculating dot product between normal vector and "lunar" one lunar phase is simulated.
   // Lunar phase.  
   // We calculate days/29.53 + bias.  
   float phase = cb0_v0.y * (1.0 / SYNODIC_MONTH_LENGTH) + cb0_v0.w;  
   
   // Multiply by 2*PI. This way 29.53 will be a full period  
   // for sin/cos functions.  
   phase *= TWOPI;  
        
   // Calculate sine and cosine of lunar phase.  
   float outSin = 0.0;  
   float outCos = 0.0;  
   sincos(phase, outSin, outCos);  
        
   // Calculate lunar phase  
   float lunarPhase = saturate( dot(vNormal, float2(outCos, outSin)) );  

See some screenshots with various lunar phases:




The last step is to perform a series of multiplications to calculate final color.
   // Perform a series of multiplications to calculate final color.  
   
   // cb12_v266.xyz is used to boost Moon's glow and color.  
   // for example (1.54, 2.82, 4.13)  
   float3 moonSurfaceGlowColor = cb12_v266.xyz;  
   
   float3 moonColor = lunarPhase * moonSurfaceGlowColor;  
   moonColor = moonColorTex * moonColor;  
     
   // cb_v2.xyz is probably a filter, like (1.0, 1.0, 1.0)  
   moonColor *= cb2_v2.xyz;  
        
   // I'm not really sure what this thing is, maybe some horizon opacity value.  
   // Anyway, it doesn't seem to have that much influence to final color  
   // as parameters above.  
   float paramHorizon = saturate(1.0 - IN.param1.w);  
   paramHorizon *= cb2_v2.w;  
        
   moonColor *= paramHorizon;  
   
   // Output final color with zero alpha  
   return float4(moonColor, 0.0);  

You may wonder why this shader outputs 0.0 alpha. Well, the Moon is rendered with blending enabled:
Such approach allows us to have background (sky) color if this shader returns black one.

If you are interested in full shader, it's here. It has some big constant buffers and should be ready to inject instead original one in RenderDoc (just rename "MoonPS" to "EditedShaderPS").

Last but not least, I wanted to share results with you:
On the left - my shader, on the right - original shader from the game.
The difference is really minor which has no impact on results.

As you can see, this shader was quite easy to reconstruct.
I hope you enjoyed it.

Thanks for reading!

1 comment:

  1. Hello, I would like to ask you if possible to explain what is actually happening on line 330 of the final shader when you say that a 3x3 TBN matrix is being squeezed into a 3x2 matrix.

    I have already asked a question about that at:

    https://stackoverflow.com/questions/63151185/hlsl-what-happens-when-i-squish-a-3x3-matrix-into-3x2-matrix

    So the answer I got would mean that the result of such squeezing would be a 3x2 matrix:

    IN.tangent.x IN.binormal.x
    IN.normal.x IN.tangent.y
    IN.binormal.y IN.normal.y

    But then what is the use of such odd matrix? I would be super grateful if you cared to explain what is actually happening (and what is the idea) when you do the:

    float2 vNormal = mul(sampledNormal, (float3x2)TBN).xy;

    I undestood the sampledNormal is being transposed into a 1x3 row matrix and then we basically do a dot product with the columns of the new 3x2 matrix to get a float2, but I completely cannot wrap my head around the use of that and when trying to reproduce that shader in Unity I get the weirdest results.

    ReplyDelete