Recently realized that cgmath does not provide function for projecting between screen space and world space. Similarly to GLM's glm::unProject and glm::project in C++, or further back GLU's gluUnProject and gluProject.

This post contains an implementation of those functions for cgmath in Rust. The snippets are tested against the following version of the cgmath crate.

cgmath = "0.17.0"

Screen Space to World Space Projection

Same as glm::unProject.

pub fn project_screen_to_world(
        screen: Vector3<f32>,
        view_projection: Matrix4<f32>,
        viewport: Vector4<i32>,
) -> Option<Vector3<f32>> {
    if let Some(inv_view_projection) = view_projection.invert() {
        let world = Vector4::new(
            (screen.x - (viewport.x as f32)) / (viewport.z as f32) * 2.0 - 1.0,
            // Screen Origin is Top Left    (Mouse Origin is Top Left)
//          (screen.y - (viewport.y as f32)) / (viewport.w as f32) * 2.0 - 1.0,
            // Screen Origin is Bottom Left (Mouse Origin is Top Left)
     (1.0 - (screen.y - (viewport.y as f32)) / (viewport.w as f32)) * 2.0 - 1.0, screen.z * 2.0 - 1.0,
            1.0);
        let world = inv_view_projection * world;

        if world.w != 0.0 {
            Some(world.truncate() * (1.0 / world.w))
        } else {
            None
        }
    } else {
        None
    }
}

World Space to Screen Space Projection

Same as glm::project.

pub fn project_world_to_screen(
        world: Vector3<f32>,
        view_projection: Matrix4<f32>,
        viewport: Vector4<i32>,
) -> Option<Vector3<f32>> {
    let screen = view_projection * world.extend(1.0);

    if screen.w != 0.0 {
        let mut screen = screen.truncate() * (1.0 / screen.w);

        screen.x = (screen.x + 1.0) * 0.5 * (viewport.z as f32) + (viewport.x as f32);
        // Screen Origin is Top Left    (Mouse Origin is Top Left)
//      screen.y = (screen.y + 1.0) * 0.5 * (viewport.w as f32) + (viewport.y as f32);
        // Screen Origin is Bottom Left (Mouse Origin is Top Left)
        screen.y = (1.0 - screen.y) * 0.5 * (viewport.w as f32) + (viewport.y as f32);

        // This is only correct when glDepthRangef(0.0f, 1.0f)
        screen.z = (screen.z + 1.0) * 0.5;

        Some(screen)
    } else {
        None
    }
}

Example - Screen to World Ray

Given a point on the screen, such as the mouse position (mx, my), the view projection matrix (vp), and viewport, the following example calculates a ray from the screen and into the world.

struct Ray {
    start: Point3<f32>,
    direction: Vector3<f32>,
}

let front = project_screen_to_world(Vector3::new(mx, my, 1.0), vp, viewport);
let back  = project_screen_to_world(Vector3::new(mx, my, 0.0), vp, viewport);

if let (Some(front), Some(back)) = (front, back) {
    let ray = Ray {
        start: Point3::from_vec(back),
        direction: (front - back).normalize(),
    };
}

The inverse of project_screen_to_world is project_world_to_screen, and as such can be used to calculate a 2D screen point from a 3D world point.