Jump to content

Directional Light - Shadows not covering the whole map


Go to solution Solved by Josh,

Recommended Posts

Posted

When dealing with bigger indoor rooms and don't have setup the whole environemnt probes needed. You can see in this screen:

image.thumb.png.108e99bc212a21271a1724759a6ff7fb.png

The last (unreachable space) is marked with pink in this screenshot, As you can see the shadows don't cover the whole cave / level in view. Instead (after what i have found out)
it just covers (with a default cascade size of 4.0) a range of  64.0. So the first cascade goes from near to 4.0, the second from 4.0 to 8.0 and the last covers a range from 8.0 to 64.0. The rest just can never receive shadow as long as the camera to far away. One way to fix this would be to raise the initial cascade distance to something much higher like 40 an above. this leads to artifacts and ugly shadows for the near shadows because they are at a too low resolution for the covered area.

From a previous research (years ago) I remembered a logarithmic approach mentioned by nvidia about PSSM (https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-10-parallel-split-shadow-maps-programmable-gpus) to calculate the cascade sizes.

Josh, maybe you should try this apporach or add the ability to configure the whole cascade sizes by ourself. 
 

float get_cascade_split(float lambda, UINT current_partition, UINT number_of_partitions, float near_z, float far_z)
{
    // https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-10-parallel-split-shadow-maps-programmable-gpus

    float exp = (float)current_partition / number_of_partitions;

    // Logarithmic split scheme
    float Ci_log = near_z * std::powf((far_z / near_z), exp);

    // Uniform split scheme
    float Ci_uni = near_z + (far_z - near_z) * exp;

    // Lambda [0, 1]
    float Ci = lambda * Ci_log + (1.f - lambda) * Ci_uni;
    return Ci;
}

Vec2 camera_range = camera->GetRange();
float lambda = 1.0; // full log (0.0 = linear)
int total_cascades = 3;
for (auto cascade = 0; cascade < 3; cascade++)
{
    float cascade_near = get_cascade_split(lambda, cascade, total_cascades, camera_range.x, camera_range.y);
    float cascade_far = get_cascade_split(lambda, cascade + 1, total_cascades, camera_range.x, camera_range.y);
    Print("Cascade:" + WString(i) + " --> " + WString(cascade_near) + "-" + WString(cascade_far));
}

this returns something like this:
 

Cascade:0 --> 0.1-2.15443
Cascade:1 --> 2.15443-46.4159
Cascade:2 --> 46.4159-1000

 

  • Thanks 1
  • Windows 10 Pro 64-Bit-Version
  • NVIDIA Geforce 1080 TI
Posted

Nice! I will take a closer look at this. Maybe it will be better.

The base cascade distance actually gets passed to the shader, and you can see the calculation there, but the shadow rendering also needs to match the same calculation on the CPU side. See Shaders/PBR/Lighting.glsl:

		if (camspacepos.z <= cascadedistance * 8.0f)
		{
			int index = 0;
			shadowmat = ExtractCameraProjectionMatrix(lightIndex, index);
			if (camspacepos.z > cascadedistance) index = 1;
			if (camspacepos.z > cascadedistance * 2.0f) index = 2;
			if (camspacepos.z > cascadedistance * 4.0f) index = 3;

 

My job is to make tools you love, with the features you want, and performance you can't live without.

Posted
On 4/8/2025 at 12:40 AM, Josh said:

Nice! I will take a closer look at this. Maybe it will be better.

The base cascade distance actually gets passed to the shader, and you can see the calculation there, but the shadow rendering also needs to match the same calculation on the CPU side. See Shaders/PBR/Lighting.glsl:

		if (camspacepos.z <= cascadedistance * 8.0f)
		{
			int index = 0;
			shadowmat = ExtractCameraProjectionMatrix(lightIndex, index);
			if (camspacepos.z > cascadedistance) index = 1;
			if (camspacepos.z > cascadedistance * 2.0f) index = 2;
			if (camspacepos.z > cascadedistance * 4.0f) index = 3;

 

This is where my numbers came from and also the pink color when the shadow is out of range. The problem with the single cascade distance is that we have to set it up to 125 to cover the whole viewspace, but with this value you get very ugly shadows in a short distance.  I hope you can get it working out :)

  • Windows 10 Pro 64-Bit-Version
  • NVIDIA Geforce 1080 TI
Posted

There is a section in the linked paper called "The Practical Split Scheme".

In the practical split scheme, the split positions are determined by this equation:

0207equ02.jpg

where { clog.jpg } and { cuni.jpg } are the split positions in the logarithmic split scheme and the uniform split scheme, respectively. Theoretically, the logarithmic split scheme is designed to produce optimal distribution of perspective aliasing over the whole depth range. On the other hand, the aliasing distribution in the uniform split scheme is the same as in standard shadow maps. Figure 10-4 visualizes the three split schemes in the shadow-map space.

image.jpeg.d49b8c8a1d9f3e8a273cc25a03718c86.jpeg

I don't understand this math. I don't think it is complicated, I just don't understand all the weird variables and notations. It looks like they are mixing the linear and logaritmic results by 50% each to get the result on the right.

Okay, so I take it that this:

    float exp = (float)current_partition / number_of_partitions;

    // Logarithmic split scheme
    float Ci_log = near_z * std::powf((far_z / near_z), exp);

    

Is equal to this equation?

0208equ02.jpg

That looks right...

Okay, I will plug this in and see whta happens...

My job is to make tools you love, with the features you want, and performance you can't live without.

Posted

@klepto2 Is the first partition zero or one? get_cascase_split should return the end of the partition distance, so 1 seems like the right value to me.

My job is to make tools you love, with the features you want, and performance you can't live without.

Posted

Here is my GLSL function for determining which cascade partition is used, for a given distance:

float get_cascade_split(int current_partition, int number_of_partitions, float near_z, float far_z, float lambda)
{
		// https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-10-parallel-split-shadow-maps-programmable-gpus
		float exp = float(current_partition) / float(number_of_partitions);

		// Logarithmic split scheme
		float Ci_log = near_z * pow((far_z / near_z), exp);

		// Uniform split scheme
		float Ci_uni = near_z + (far_z - near_z) * exp;

		// Lambda [0, 1]
		float Ci = lambda * Ci_log + (1.f - lambda) * Ci_uni;
		return Ci;
}

int get_cascade_partition(float zdistance, int number_of_partitions, float near_z, float far_z, float lambda)
{
	float splitdistance;
	for (int i = 0; i < number_of_partitions - 1; ++i)
	{
		splitdistance = get_cascade_split(i + 1, 4, CameraRange.x, far_z, lambda);
		if (zdistance < splitdistance) return i;
	}
	return 3;
}

Here is the original formula, where the distance doubles each partition:

screenshot235.thumb.jpg.205625e0de502df97c0ccc488c5558ba.jpg

Here is the modified formula, using 0.5 as the Lambda value and 32 as the max distance:

screenshot234.thumb.jpg.b5fd98b7a418565ad5bf092dbd6f5f84.jpg

I don't see much difference.

My job is to make tools you love, with the features you want, and performance you can't live without.

Posted

This is pure linear, Lambda = 0

screenshot236.thumb.jpg.06d7a9d06daa97c243db61e7af0304c8.jpg

This is pure Log, Lamda = 1. The camera would have to be closer to the ground to see the first two stages, since they are so short.

screenshot237.thumb.jpg.72f57d24b2df09fec7170164fccf48de.jpg

My job is to make tools you love, with the features you want, and performance you can't live without.

Posted
10 hours ago, Josh said:

Here is my GLSL function for determining which cascade partition is used, for a given distance:

float get_cascade_split(int current_partition, int number_of_partitions, float near_z, float far_z, float lambda)
{
		// https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-10-parallel-split-shadow-maps-programmable-gpus
		float exp = float(current_partition) / float(number_of_partitions);

		// Logarithmic split scheme
		float Ci_log = near_z * pow((far_z / near_z), exp);

		// Uniform split scheme
		float Ci_uni = near_z + (far_z - near_z) * exp;

		// Lambda [0, 1]
		float Ci = lambda * Ci_log + (1.f - lambda) * Ci_uni;
		return Ci;
}

int get_cascade_partition(float zdistance, int number_of_partitions, float near_z, float far_z, float lambda)
{
	float splitdistance;
	for (int i = 0; i < number_of_partitions - 1; ++i)
	{
		splitdistance = get_cascade_split(i + 1, 4, CameraRange.x, far_z, lambda);
		if (zdistance < splitdistance) return i;
	}
	return 3;
}

Here is the original formula, where the distance doubles each partition:

screenshot235.thumb.jpg.205625e0de502df97c0ccc488c5558ba.jpg

Here is the modified formula, using 0.5 as the Lambda value and 32 as the max distance:

screenshot234.thumb.jpg.b5fd98b7a418565ad5bf092dbd6f5f84.jpg

I don't see much difference.

Why do you use just 32 as zfar? You need to cover the whole frustum? Then the formula makes sense.

  • Windows 10 Pro 64-Bit-Version
  • NVIDIA Geforce 1080 TI
Posted

If the zfar is too low you will never see shadows indoor on anything which is further away. Maybe you should make this as a shadow parameter. You already can see it your images btw. If you would make big brush above the rocks in the background , normally the rocks should be in shadow. With the low zfar they will not and still be fully lit by the light.

[Edit]
I have checked some resources (other engine and docs) and this is a small summary I think could improve the shadow handling:

  • Make the shadowgeneration more configurable
    • Add parameters for:
      • How many splits to use (maybe hardcoded to 4 for the first step)
      • The distance for the splits (number of split -1)
        • eg: godot uses ranges from 0.0 to 1.0
        • default: 0.1,0.2,0.5 
          • Cascade 1: 0.0-10 %
          • Cascade 2: 10%-20%
          • Cascade 3: 20%-50%
          • Cascade 4: 50%-100% (maxdistance)
        • optional: option to calculate the distances by the above formula 
          • using a maxdistance
          • and the lambda
      • a maximum distance
        • default maybe like now 64 or 100
        • 0.0 should be validand mean the full range (Camera Farrange)
    • The shaders should receive the splits from the client and not calculate them 
      • for the first version a vec3 might be enough when the splits are fixed to 4
  • Windows 10 Pro 64-Bit-Version
  • NVIDIA Geforce 1080 TI
Posted

This is how the cascades look when using 4 cascades, full range and a lambda of 0.98:

image.thumb.png.8ace5b673489f8e7031b8655a8167f16.png

and here with 3 cascades:
image.thumb.png.c4fec02f6fb837d0dccd43f0d3473859.png

the only downside of rendering the full frustum is the higher polycount which needs to be rendered, but from a visual aspect you should not notice the lowres shadow at cascade 3 or 4.

  • Windows 10 Pro 64-Bit-Version
  • NVIDIA Geforce 1080 TI
  • Solution
Posted

This method overload will be included in the next build:

  • DirectionalLight::SetShadowCascadeDistance(const float p0, const float p1, const float p2, const float p3)

And the editor options will show four editable values.

  • Like 1

My job is to make tools you love, with the features you want, and performance you can't live without.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...