Engine: Physical lights, plausible shadows, etc.

Published July 04, 2018
Advertisement

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.

light_temperature.thumb.jpg.bbdd2ea692156af7d03cde93a14c4b93.jpg

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.

pcss_vs_pcf.thumb.jpg.691689af2dac9056f5f8bb67d60bfc7a.jpg

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.

pcmlsm.jpg.9f2251e8822829e2d8260208ca1dd537.jpg

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!

5 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement

Latest Entries

Merry Christmas!

8272 views

Progress

4354 views

Roadmap for 2020

5981 views

DOOM: Post Mortem

5589 views

DOOM: GROOM

4883 views

DOOM: Placeholders

5478 views

Ludum Dare 45

5251 views
Advertisement