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 |
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 |
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!





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.
ReplyDeleteI 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.