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();

.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;

// ...

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;