Raw File
maths_3d.rs
/*
ENSnano, a 3d graphical application for DNA nanostructures.
    Copyright (C) 2021  Nicolas Levy <nicolaspierrelevy@gmail.com> and Nicolas Schabanel <nicolas.schabanel@ens-lyon.fr>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/
use super::{
    camera::{CameraPtr, ProjectionPtr},
    Vec3,
};

/// Use to compute the shortes line between two lines in 3D.
/// Let P1, P2, P3, P4 be 4 points.
/// We want to find the shortest line between the segment (P1, P2) and (P3, P4).
/// This line is a line (Pa, Pb) where Pa = P1 + mua (P2 - P1).
/// This function returns mua
fn mu_unprojection(p1: Vec3, p2: Vec3, p3: Vec3, p4: Vec3) -> Option<f32> {
    if (p2 - p1).cross(p4 - p3).mag() > 1e-3 {
        // http://paulbourke.net/geometry/pointlineplane/

        let d = |x: Vec3, y: Vec3, z: Vec3, w: Vec3| (x - y).dot(z - w);
        // mua = ( d1343 d4321 - d1321 d4343 ) / ( d2121 d4343 - d4321 d4321 )

        let mu_num = d(p1, p3, p4, p3) * d(p4, p3, p2, p1) - d(p1, p3, p2, p1) * d(p4, p3, p4, p3);
        let mu_den = d(p2, p1, p2, p1) * d(p4, p3, p4, p3) - d(p4, p3, p2, p1) * d(p4, p3, p2, p1);
        Some(mu_num / mu_den)
    } else {
        None
    }
}

/// Create a line that goes from the camera to a point on the screen and project that line on a
/// line of the world
pub fn unproject_point_on_line(
    objective_origin: Vec3,
    objective_direction: Vec3,
    camera: CameraPtr,
    projection: ProjectionPtr,
    x_ndc: f32,
    y_ndc: f32,
) -> Option<Vec3> {
    let p1 = camera.borrow().position;
    let p2 = ndc_to_world(x_ndc, y_ndc, camera, projection);

    let p3 = objective_origin;
    let p4 = objective_origin + objective_direction;
    let mu = mu_unprojection(p3, p4, p1, p2);

    if let Some(mu) = mu {
        // http://paulbourke.net/geometry/pointlineplane/
        Some(p3 + (p4 - p3) * mu)
    } else {
        None
    }
}

/// Shoot a ray from the camera and compute its intersection with the plane P: (p- p0).dot(n) - 0
/// if the intersection if the point p, the return value is the coordinates of the point p.
/// If the line and the plane are parallel, None is returned
pub fn unproject_point_on_plane(
    objective_origin: Vec3,
    objective_normal: Vec3,
    camera: CameraPtr,
    projection: ProjectionPtr,
    x_ndc: f32,
    y_ndc: f32,
) -> Option<Vec3> {
    let p1 = camera.borrow().position;
    let p2 = ndc_to_world(x_ndc, y_ndc, camera, projection);

    let dir = p2 - p1;

    let denom = dir.dot(objective_normal);
    if denom.abs() > 1e-3 {
        let mu = (objective_origin - p1).dot(objective_normal) / denom;
        Some(p1 + mu * dir)
    } else {
        None
    }
}

/// Convert a point on the screen into a point in the world. Usefull for casting rays
fn ndc_to_world(x_ndc: f32, y_ndc: f32, camera: CameraPtr, projection: ProjectionPtr) -> Vec3 {
    let x_screen = 2. * x_ndc - 1.;
    let y_screen = 1. - 2. * y_ndc;

    let p1 = camera.borrow().position;
    let p2 = {
        let correction = (projection.borrow().get_fovy() / 2.).tan();
        let right = camera.borrow().right_vec() * correction;
        let up = camera.borrow().up_vec() * correction;
        let direction = camera.borrow().direction();
        p1 + right * x_screen * projection.borrow().get_ratio() + up * y_screen + direction
    };
    p2
}

pub fn cast_ray(
    x_ndc: f32,
    y_ndc: f32,
    camera: CameraPtr,
    projection: ProjectionPtr,
) -> (Vec3, Vec3) {
    let target = ndc_to_world(x_ndc, y_ndc, camera.clone(), projection);
    (camera.borrow().position, target - camera.borrow().position)
}

pub struct UnalignedBoundaries {
    min_x: f32,
    max_x: f32,
    min_y: f32,
    max_y: f32,
    min_z: f32,
    max_z: f32,
    basis: Basis3D,
}

pub struct Basis3D {
    unit_x: Vec3,
    unit_y: Vec3,
    unit_z: Vec3,
}

impl Basis3D {
    pub fn from_vecs(unit_x: Vec3, unit_y: Vec3, unit_z: Vec3) -> Self {
        Self {
            unit_x,
            unit_y,
            unit_z,
        }
    }

    pub fn convert_point_to_self(&self, point: Vec3) -> Vec3 {
        Vec3::new(
            point.dot(self.unit_x),
            point.dot(self.unit_y),
            point.dot(self.unit_z),
        )
    }

    pub fn convert_point_from_self(&self, point: Vec3) -> Vec3 {
        point.x * self.unit_x + point.y * self.unit_y + point.z * self.unit_z
    }
}

impl UnalignedBoundaries {
    pub fn from_basis(basis: Basis3D) -> Self {
        Self {
            min_x: std::f32::INFINITY,
            min_y: std::f32::INFINITY,
            min_z: std::f32::INFINITY,
            max_x: std::f32::NEG_INFINITY,
            max_y: std::f32::NEG_INFINITY,
            max_z: std::f32::NEG_INFINITY,
            basis,
        }
    }

    pub fn add_point(&mut self, point: Vec3) {
        let real_point = self.basis.convert_point_to_self(point);
        self.min_x = self.min_x.min(real_point.x);
        self.max_x = self.max_x.max(real_point.x);
        self.min_y = self.min_y.min(real_point.y);
        self.max_y = self.max_y.max(real_point.y);
        self.min_z = self.min_z.min(real_point.z);
        self.max_z = self.max_z.max(real_point.z);
    }

    pub fn middle(&self) -> Option<Vec3> {
        let x = (self.min_x + self.max_x) / 2.;
        let y = (self.min_y + self.max_y) / 2.;
        let z = (self.min_z + self.max_z) / 2.;

        if x.is_normal() && y.is_normal() && z.is_normal() {
            Some(self.basis.convert_point_from_self(Vec3::new(x, y, z)))
        } else {
            None
        }
    }

    fn bounding_sphere_radius(&self) -> Option<f32> {
        let lenght_x = self.max_x - self.min_x;
        let length_y = self.max_y - self.min_y;
        let length_z = self.max_z - self.min_z;

        let max_length = lenght_x.max(length_y.max(length_z));
        if max_length.is_normal() {
            Some(max_length / 2. * 3f32.sqrt())
        } else {
            None
        }
    }

    pub fn fit_point(&self, fovy: f32, ratio: f32) -> Option<Vec3> {
        let middle = self.middle()?;
        let radius = self.bounding_sphere_radius()? * 1.1;
        let ratio_adjust = (1. / ratio).max(1.);
        let x_back = radius * ratio_adjust / 2. / (fovy / 2.).tan();

        Some(middle + x_back.max(10.) * self.basis.unit_z)
    }
}
back to top