nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::{
    camera::components::Projection,
    world::{Entity, Vec2, Vec3, Vec4, World},
};

#[derive(Debug, Clone, Copy)]
pub struct PickingRay {
    pub origin: Vec3,
    pub direction: Vec3,
}

impl PickingRay {
    pub fn from_screen_position(world: &World, screen_pos: Vec2) -> Option<Self> {
        let window = &world.resources.window;

        let (local_pos, viewport_width, viewport_height) =
            if let Some(viewport_rect) = &window.active_viewport_rect {
                let (window_width, window_height) = window.cached_viewport_size?;
                let local = viewport_rect.to_local(screen_pos);
                let scale_x = window_width as f32 / viewport_rect.width;
                let scale_y = window_height as f32 / viewport_rect.height;
                let scaled_local = Vec2::new(local.x * scale_x, local.y * scale_y);
                (scaled_local, window_width as f32, window_height as f32)
            } else {
                let (width, height) = window.cached_viewport_size?;
                (screen_pos, width as f32, height as f32)
            };

        let camera_entity = world.resources.active_camera?;

        let camera = world.core.get_camera(camera_entity)?;
        let global_transform = world.core.get_global_transform(camera_entity)?;

        let ndc_x = (2.0 * local_pos.x) / viewport_width - 1.0;
        let ndc_y = 1.0 - (2.0 * local_pos.y) / viewport_height;

        let view_matrix = global_transform.0.try_inverse()?;
        let aspect_ratio = viewport_width / viewport_height;
        let projection_matrix = camera.projection.matrix_with_aspect(aspect_ratio);
        let inverse_vp = (projection_matrix * view_matrix).try_inverse()?;

        let (origin, direction) = match camera.projection {
            Projection::Perspective { .. } => {
                let camera_pos = global_transform.0.column(3).xyz();
                let clip_pos = Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
                let world_pos = inverse_vp * clip_pos;
                let world_pos = world_pos.xyz() / world_pos.w;
                let direction = (world_pos - camera_pos).normalize();
                (camera_pos, direction)
            }
            Projection::Orthographic { .. } => {
                let near_clip = Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
                let far_clip = Vec4::new(ndc_x, ndc_y, 0.0, 1.0);

                let near_world = inverse_vp * near_clip;
                let far_world = inverse_vp * far_clip;

                let near_point = near_world.xyz() / near_world.w;
                let far_point = far_world.xyz() / far_world.w;

                let direction = (far_point - near_point).normalize();
                (near_point, direction)
            }
        };

        Some(Self { origin, direction })
    }

    pub fn intersect_plane(&self, plane_normal: Vec3, plane_distance: f32) -> Option<Vec3> {
        let denom = nalgebra_glm::dot(&plane_normal, &self.direction);
        if denom.abs() < 1e-6 {
            return None;
        }

        let t = -(nalgebra_glm::dot(&plane_normal, &self.origin) + plane_distance) / denom;
        if t < 0.0 {
            return None;
        }

        Some(self.origin + self.direction * t)
    }

    pub fn intersect_ground_plane(&self, y_level: f32) -> Option<Vec3> {
        self.intersect_plane(Vec3::new(0.0, 1.0, 0.0), -y_level)
    }
}

#[derive(Debug, Clone)]
pub struct PickingResult {
    pub entity: Entity,
    pub distance: f32,
    pub world_position: Vec3,
}

#[derive(Debug, Clone, Copy)]
pub struct PickingOptions {
    pub max_distance: f32,
    pub ignore_invisible: bool,
}

impl Default for PickingOptions {
    fn default() -> Self {
        Self {
            max_distance: f32::INFINITY,
            ignore_invisible: true,
        }
    }
}

pub fn pick_entities(
    world: &World,
    screen_pos: Vec2,
    options: PickingOptions,
) -> Vec<PickingResult> {
    let ray = match PickingRay::from_screen_position(world, screen_pos) {
        Some(ray) => ray,
        None => return Vec::new(),
    };

    let mut results = Vec::new();

    for entity in world
        .core
        .query_entities(crate::ecs::world::BOUNDING_VOLUME)
    {
        let bounding_volume = match world.core.get_bounding_volume(entity) {
            Some(bv) => bv,
            None => continue,
        };

        let global_transform = match world.core.get_global_transform(entity) {
            Some(gt) => gt,
            None => continue,
        };

        if options.ignore_invisible
            && let Some(visible) = world.core.get_visibility(entity)
            && !visible.visible
        {
            continue;
        }

        let transformed_bv = bounding_volume.transform(&global_transform.0);

        let to_center = transformed_bv.obb.center - ray.origin;
        let projection = nalgebra_glm::dot(&to_center, &ray.direction);
        let closest_point = if projection < 0.0 {
            ray.origin
        } else {
            ray.origin + ray.direction * projection
        };

        let distance_to_sphere = nalgebra_glm::distance(&closest_point, &transformed_bv.obb.center);
        if distance_to_sphere > transformed_bv.sphere_radius {
            continue;
        }

        if let Some(distance) = transformed_bv.obb.intersect_ray(ray.origin, ray.direction)
            && distance <= options.max_distance
        {
            let world_position = ray.origin + ray.direction * distance;
            results.push(PickingResult {
                entity,
                distance,
                world_position,
            });
        }
    }

    results.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
    results
}

pub fn pick_closest_entity(world: &World, screen_pos: Vec2) -> Option<PickingResult> {
    pick_entities(world, screen_pos, PickingOptions::default())
        .into_iter()
        .next()
}

pub fn get_ground_position_from_screen(
    world: &World,
    screen_pos: Vec2,
    y_level: f32,
) -> Option<Vec3> {
    let ray = PickingRay::from_screen_position(world, screen_pos)?;
    ray.intersect_ground_plane(y_level)
}

pub fn pick_entities_in_frustum(world: &World, entities: &[Entity]) -> Vec<Entity> {
    let camera_entity = match world.resources.active_camera {
        Some(entity) => entity,
        None => return Vec::new(),
    };

    let camera = match world.core.get_camera(camera_entity) {
        Some(cam) => cam,
        None => return Vec::new(),
    };

    let global_transform = match world.core.get_global_transform(camera_entity) {
        Some(gt) => gt,
        None => return Vec::new(),
    };

    let view_matrix = match global_transform.0.try_inverse() {
        Some(vm) => vm,
        None => return Vec::new(),
    };

    let window = &world.resources.window;
    let (viewport_width, viewport_height) = match window.cached_viewport_size {
        Some(size) => size,
        None => return Vec::new(),
    };
    let aspect_ratio = viewport_width as f32 / viewport_height as f32;
    let projection_matrix = camera.projection.matrix_with_aspect(aspect_ratio);
    let view_projection = projection_matrix * view_matrix;

    let mut visible_entities = Vec::new();

    for &entity in entities {
        let bounding_volume = match world.core.get_bounding_volume(entity) {
            Some(bv) => bv,
            None => continue,
        };

        let global_transform = match world.core.get_global_transform(entity) {
            Some(gt) => gt,
            None => continue,
        };

        let transformed_bv = bounding_volume.transform(&global_transform.0);
        let center = transformed_bv.obb.center;

        let clip_pos = view_projection * Vec4::new(center.x, center.y, center.z, 1.0);
        if clip_pos.w <= 0.0 {
            continue;
        }

        let ndc = clip_pos.xyz() / clip_pos.w;
        let sphere_radius_ndc = transformed_bv.sphere_radius / clip_pos.w;

        if ndc.x + sphere_radius_ndc >= -1.0
            && ndc.x - sphere_radius_ndc <= 1.0
            && ndc.y + sphere_radius_ndc >= -1.0
            && ndc.y - sphere_radius_ndc <= 1.0
            && ndc.z + sphere_radius_ndc >= 0.0
            && ndc.z - sphere_radius_ndc <= 1.0
        {
            visible_entities.push(entity);
        }
    }

    visible_entities
}

#[cfg(feature = "physics")]
pub fn pick_entities_trimesh(
    world: &World,
    screen_pos: Vec2,
    options: PickingOptions,
) -> Vec<PickingResult> {
    use rapier3d::prelude::*;

    let ray = match PickingRay::from_screen_position(world, screen_pos) {
        Some(ray) => ray,
        None => return Vec::new(),
    };

    let rapier_ray = rapier3d::parry::query::Ray::new(
        point![ray.origin.x, ray.origin.y, ray.origin.z],
        vector![ray.direction.x, ray.direction.y, ray.direction.z],
    );

    let mut results = Vec::new();
    let picking_world = &world.resources.picking_world;

    for (handle, collider) in picking_world.collider_set.iter() {
        let entity = match picking_world.get_entity(handle) {
            Some(e) => e,
            None => continue,
        };

        if options.ignore_invisible
            && let Some(visible) = world.core.get_visibility(entity)
            && !visible.visible
        {
            continue;
        }

        if let Some(toi) =
            collider
                .shape()
                .cast_ray(collider.position(), &rapier_ray, options.max_distance, true)
        {
            let world_position = ray.origin + ray.direction * toi;
            results.push(PickingResult {
                entity,
                distance: toi,
                world_position,
            });
        }
    }

    results.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
    results
}

#[cfg(feature = "physics")]
pub fn pick_closest_entity_trimesh(world: &World, screen_pos: Vec2) -> Option<PickingResult> {
    pick_entities_trimesh(world, screen_pos, PickingOptions::default())
        .into_iter()
        .next()
}