0
$\begingroup$

I got basic shadow mapping with a single directional light working in a small voxel game I'm working on, and I've been trying to upgrade it to Cascaded Shadow Maps. I've looked at a few tutorials - mainly this one at OGLDev and this one for LWJGL - and I've implemented it in Rust with GLium. The problem is, even after much fixing it still doesn't produce correct shadows, and it flickers and shifts when I look around. Here's a video of what's happening: https://youtu.be/bqmjqP2Y5-M

I think the problem is in how I compute the light view-projection matrix:

        // Half of the vertical FOV
        let fovy = radians(90.0) * 0.5;
        let tan_fy = fovy.tan();
        let fovx = fovy * aspect;
        let tan_fx = fovx.tan();

        // This is the camera view matrix, not projection
        let m = self.view_mat();
        // camera space -> world space
        // I'm using nalgebra for math, so this is the inverse
        let mi = m.try_inverse().unwrap();

        (0..num_cascades)
            .map(|i| {
                // Calculate frustum corners in camera space
                let near = VIEW_DISTANCE * (i as f32 / num_cascades as f32);
                let far = VIEW_DISTANCE * ((i + 1) as f32 / num_cascades as f32);
                let x_near = near * tan_fx;
                let x_far = far * tan_fx;
                let y_near = near * tan_fy;
                let y_far = far * tan_fy;

                // The frustum corners
                let n_a = Vec3::new(x_near, y_near, near);
                let n_b = Vec3::new(x_near, -y_near, near);
                let n_c = Vec3::new(-x_near, y_near, near);
                let n_d = Vec3::new(-x_near, -y_near, near);

                let f_a = Vec3::new(x_far, y_far, far);
                let f_b = Vec3::new(x_far, -y_far, far);
                let f_c = Vec3::new(-x_far, y_far, far);
                let f_d = Vec3::new(-x_far, -y_far, far);

                // Transform them to world space
                let v: Vec<_> = vec![n_a, n_b, n_c, n_d, f_a, f_b, f_c, f_d]
                    .into_iter()
                    .map(|x| (mi * na::Vector4::new(x.x, x.y, x.z, 1.0)).xyz())
                    .collect();

                // World spaace min and max points
                let min = v.iter().fold(
                    Vec3::new(10_000_000.0, 10_000_000.0, 10_000_000.0),
                    |x, a| x.zip_map(a, f32::min),
                );
                let max = v.iter().fold(
                    -Vec3::new(10_000_000.0, 10_000_000.0, 10_000_000.0),
                    |x, a| x.zip_map(a, f32::max),
                );
                // Center of the world-space bounding box of the frustum.
                // I also tried using the centroid of the frustum corners.
                let center = (min + max) * 0.5;
                let center = Point3::from(center);

                let view_up = Vec3::y();
                let view_right = Unit::new_normalize((sun_dir).cross(&view_up));
                let view_up = view_right.cross(&sun_dir);
                // world space -> light space
                let view =
                    na::Matrix4::look_at_rh(&(center - sun_dir), &center, &view_up);

                // To light space
                let v: Vec<_> = v
                    .into_iter()
                    .map(|x| (view * na::Vector4::new(x.x, x.y, x.z, 1.0)).xyz())
                    .collect();

                // And get a bounding box for orthographic projection
                let min = v.iter().fold(
                    Vec3::new(10_000_000.0, 10_000_000.0, 10_000_000.0),
                    |x, a| x.zip_map(a, f32::min),
                );
                let max = v.iter().fold(
                    -Vec3::new(10_000_000.0, 10_000_000.0, 10_000_000.0),
                    |x, a| x.zip_map(a, f32::max),
                );

                // Orthographic projection matrix encompassing the frustum
                let proj =
                    na::Matrix4::new_orthographic(min.x, max.x, min.y, max.y, min.z, max.z);

                // This is the final light view-projection matrix
                proj * view
            })
            .collect()

My shader in case that's the problem:

// Among others
uniform vec3 camera_pos;
uniform mat4 light_mats[NUM_CASCADES];
uniform sampler2DArrayShadow shadow_map;

// ...

float shadow(vec3 pos) {
    float far = 1024;
    float z = length(pos - camera_pos);
    uint i = uint(float(NUM_CASCADES) * z/far);

    float bias = 0.00001;
    vec4 map_coord = light_mats[i] * vec4(pos, 1.0);

    // * 0.5 + 0.5 because it needs to go from 0 to 1 instead of -1 to 1
    float depth = (map_coord.z - bias) / map_coord.w * 0.5 + 0.5;
    vec2 coords = map_coord.xy / map_coord.w * 0.5 + 0.5;

    return texture(shadow_map, vec4(coords, float(i), depth));
}

EDIT:

I found and fixed some of the problems, but there's still some flickering and bars of light in shadowed corners. It turns out the reason it was so hard to diagnose is because it was many problems acting simultaneously, so it didn't obviously match the symptoms of any of them.

The black lines moving around all over everything were fixed with a slope-scaled depth bias (normally done with glPolygonOffset, but I just did it in the shader.) There was a problem where if you looked in certain directions (generally along coordinate axes) actual shadows would disappear and you'd get giant shadows unrelated to the geometry moving around over everything. When I looked from the light's perspective, the shadow map was stretching out on one axis whenever that happened. I don't actually know how that caused the problem, but forcing the light's orthographic projection to be a square did fix it.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.