As I have quite a bit more to share, I've decided to divide this post in 2 parts. The first one will do a brief explanation on physical lights, while the second will focus on plausible shadows (which are my first step towards area lighting).
Physical Lights
Are a nice to have feature. Instead of specifying intensity and color with some arbitrary values - one specifies luminous power (with lm), temperature of light source and an arbitrary value of color.
Fig. 1 - From left to right - Tungsten bulb with 500W and 1000W, Simulated point light with temperature of sun and intensity of 10k lumens, Simulated point light with temperature of overcast sky and intensity of 20k lumens. Tone mapping was enabled.
Using lumens to describe intensity of point/spot lights, and temperature to describe colors allows to simulate various lights based on their actual parameters. To allow for lights with additional colors (red, green, etc.), additional color parameter is introduced.
/// <summary>Convert temperature of black body into RGB color</summary>
/// <param name="temperature">Temperature of black body (in Kelvin)</param>
Engine::float4 TemperatureToColor(float temperature)
{
float tmp = temperature / 100.0f;
Engine::float4 result;
if (tmp <= 66.0f)
{
result.x = 255.0f;
result.y = tmp;
result.y = 99.4708025861f * log(result.y) - 161.1195681661f;
if (tmp <= 19.0f)
{
result.z = 0.0f;
}
else
{
result.z = tmp - 10.0f;
result.z = 138.5177312231f * log(result.z) - 305.0447927307f;
}
}
else
{
result.x = tmp - 60.0f;
result.x = 329.698727446f * pow(result.x, -0.1332047592f);
result.y = tmp - 60.0f;
result.y = 288.1221695283f * pow(result.y, -0.0755148492f);
result.z = 255.0f;
}
return result / 255.0f;
}
Fig. 2 - Snippet for calculating color from temperature.
I intentionally missed one thing - attenuation - which is very important to make all of this work properly. The main reason to do so was that I'd like to talk about it shortly when I finish my work on area lights.
Plausible Shadows
Speaking of area lights, the main challenge in realtime rendering and area lights are definitely shadows. As of today I haven't seen any game doing area lights shadows properly. Most of them either don't cast shadows at all, or use a cheap shadow map, possibly with standard NxN filtering (some do precompute light map, which often suffers on poor resolution - and doesn't allow shadow casting from dynamic objects).
Before I'm going to finish my implementation of area lights I have to attempt to implement so solid shadowing technique for area lights that works properly with dyanmic objects.
First of all, I had to switch from shadow map per light to a solution that has one huge texture where ALL lights that casts shadows render their shadow maps into. This is a necessary requirement for me to easily be able to cast shadows from all lights during F.e. GI computation. On the other hand this makes filtering a bit more tricky, especially for more specific filters.
There are still some minor problems with my approach including:
- Proper claming
- Removing seams for point lights shadows
Those haven't stopped me from trying to implement nice looking PCSS (Percentage Closer Soft Shadows), and compare against standard PCF (Percentage Close Filtering) in terms of quality.
Fig. 3 - Left PCSS, Right PCF - While PCSS does look indeed like shadows from area light source, it is far from perfect mainly due to noise and still somehow limited light size.
For convenience I'm adding source code for my PCSS. I've taken some of the values from Unity's implementation of PCSS that seems to picked the values quite well. Reference: https://github.com/TheMasonX/UnityPCSS
inline float PCSS_Noise(float3 location)
{
float3 skew = location + 0.2127f + location.x * location.y * location.z * 0.3713f;
float3 rnd = 4.789f * sin(489.123f * (skew));
return frac(rnd.x * rnd.y * rnd.z * (1.0 + skew.x));
}
inline float2 PCSS_Rotate(float2 pos, float2 rotation)
{
return float2(pos.x * rotation.x - pos.y * rotation.y, pos.y * rotation.x + pos.x * rotation.y);
}
inline float2 PCSS_BlockerDistance(Texture2D<float2> tex, SamplerState state, float3 projCoord, float searchUV, float2 rotation)
{
int blockers = 0;
float avgBlockerDistance = 0.0f;
for (int i = 0; i < (int)PCSS_SampleCount; i++)
{
float2 offset = PCSS_Samples[i] * searchUV;
offset = PCSS_Rotate(offset, rotation);
float z = tex.SampleLevel(state, projCoord.xy + offset, 0.0f).x;
if (z < projCoord.z)
{
blockers++;
avgBlockerDistance += z;
}
}
avgBlockerDistance /= blockers;
return float2(avgBlockerDistance, (float)blockers);
}
inline float PCSS_PCFFilter(Texture2D<float2> tex, SamplerState state, float3 projCoord, float filterRadiusUV, float penumbra, float2 rotation, float2 grad)
{
float sum = 0.0f;
for (int i = 0; i < (int)PCSS_SampleCount; i++)
{
float2 offset = PCSS_Samples[i] * filterRadiusUV;
offset = PCSS_Rotate(offset, rotation);
sum += tex.SampleLevel(state, projCoord.xy + offset, 0.0f).x < projCoord.z ? 0.0f : 1.0f;
}
sum /= (float)PCSS_SampleCount;
return sum;
}
inline float ShadowMapPCSS(Texture2D<float2> tex, SamplerState state, float3 projCoord, float resolution, float pixelSize, float lightSize)
{
float2 uv = projCoord.xy;
float depth = projCoord.z;
float zAwareDepth = depth;
float rotationAngle = Random(projCoord.xy) * 3.1415926;
float2 rotation = float2(cos(rotationAngle), sin(rotationAngle));
float searchSize = lightSize * saturate(zAwareDepth - .02) / zAwareDepth;
float2 blockerInfo = PCSS_BlockerDistance(tex, state, projCoord, searchSize, rotation);
if (blockerInfo.y < 1.0)
{
return 1.0f;
}
else
{
float penumbra = max(zAwareDepth - blockerInfo.x, 0.0);
float filterRadiusUV = penumbra * lightSize;
float2 grad = frac(projCoord.xy * resolution + 0.5f);
float shadow = PCSS_PCFFilter(tex, state, projCoord, filterRadiusUV, penumbra, rotation, grad);
return shadow;
}
}
Fig. 4 - PCSS source code
To allow for more smooth and less noisy shadows I've tried to think off a way using mip-mapped texture atlas. And while having additional problems (possibly solvable), it is possible to achieve very smooth nice looking noise-less penumbrae shadows.
Fig. 5 - Soft penumbrae shadows from my attempt.
This is probably all for today from me. If possible I'd like to dig a bit more into shadows and area lights next time, but who knows - I might get attracted by something completely different.
Thanks for reading!