klepto2 Posted April 7 Posted April 7 When dealing with bigger indoor rooms and don't have setup the whole environemnt probes needed. You can see in this screen: 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 1 Quote Windows 10 Pro 64-Bit-Version NVIDIA Geforce 1080 TI
Josh Posted April 7 Posted April 7 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; Quote My job is to make tools you love, with the features you want, and performance you can't live without.
klepto2 Posted April 9 Author Posted April 9 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 Quote Windows 10 Pro 64-Bit-Version NVIDIA Geforce 1080 TI
Josh Posted Wednesday at 03:00 PM Posted Wednesday at 03:00 PM 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: where { } and { } 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. 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? That looks right... Okay, I will plug this in and see whta happens... Quote My job is to make tools you love, with the features you want, and performance you can't live without.
Josh Posted Wednesday at 04:27 PM Posted Wednesday at 04:27 PM @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. Quote My job is to make tools you love, with the features you want, and performance you can't live without.
klepto2 Posted Wednesday at 04:37 PM Author Posted Wednesday at 04:37 PM One 1 Quote Windows 10 Pro 64-Bit-Version NVIDIA Geforce 1080 TI
Josh Posted Wednesday at 04:56 PM Posted Wednesday at 04:56 PM 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: Here is the modified formula, using 0.5 as the Lambda value and 32 as the max distance: I don't see much difference. Quote My job is to make tools you love, with the features you want, and performance you can't live without.
Josh Posted Wednesday at 04:59 PM Posted Wednesday at 04:59 PM This is pure linear, Lambda = 0 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. Quote My job is to make tools you love, with the features you want, and performance you can't live without.
klepto2 Posted Thursday at 03:47 AM Author Posted Thursday at 03:47 AM 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: Here is the modified formula, using 0.5 as the Lambda value and 32 as the max distance: 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. Quote Windows 10 Pro 64-Bit-Version NVIDIA Geforce 1080 TI
Josh Posted Thursday at 04:16 AM Posted Thursday at 04:16 AM I don't think it will look good, but I'll try it... Quote My job is to make tools you love, with the features you want, and performance you can't live without.
klepto2 Posted Thursday at 04:51 AM Author Posted Thursday at 04:51 AM 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 Quote Windows 10 Pro 64-Bit-Version NVIDIA Geforce 1080 TI
klepto2 Posted Thursday at 12:29 PM Author Posted Thursday at 12:29 PM This is how the cascades look when using 4 cascades, full range and a lambda of 0.98: and here with 3 cascades: 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. Quote Windows 10 Pro 64-Bit-Version NVIDIA Geforce 1080 TI
Solution Josh Posted Saturday at 01:10 AM Solution Posted Saturday at 01:10 AM 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. 1 Quote My job is to make tools you love, with the features you want, and performance you can't live without.
Recommended Posts
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.