1use libdqg::camera::Camera;
2use libdqg::ecs::Entity;
3use libdqg::glam;
4use libdqg::renderer::Model;
5use libdqg::world::{Renderable, World};
6
7const CAMERA_GIZMO_PICK_RADIUS: f32 = 0.3;
10
11pub fn pick(world: &World, camera: &Camera, ndc_x: f32, ndc_y: f32) -> Option<Entity> {
20 let inv_view_proj = camera.build_view_projection_matrix().inverse();
21
22 let near = inv_view_proj.project_point3(glam::Vec3::new(ndc_x, ndc_y, 0.0));
23 let far = inv_view_proj.project_point3(glam::Vec3::new(ndc_x, ndc_y, 1.0));
24 let ray_origin = near;
25 let ray_dir = (far - near).normalize();
26
27 let mut best: Option<(Entity, f32)> = None;
28
29 for (entity, renderable) in world.renderables.iter() {
30 let Some(transform) = world.transforms.get(entity) else { continue };
31 let matrix = transform.to_mat4();
32
33 let hit_t = match renderable {
34 Renderable::Model(model) => pick_model(model, matrix, ray_origin, ray_dir),
35 Renderable::Sprite(_) => {
36 let center = matrix.transform_point3(renderable.bounding_center());
37 let scale = transform.scale.abs();
40 let radius = renderable.bounding_radius() * scale.x.max(scale.y).max(scale.z);
41 ray_sphere_intersection(ray_origin, ray_dir, center, radius)
42 }
43 };
44
45 if let Some(t) = hit_t {
46 if best.is_none_or(|(_, best_t)| t < best_t) {
47 best = Some((entity, t));
48 }
49 }
50 }
51
52 for (entity, _) in world.cameras.iter() {
53 let Some(transform) = world.transforms.get(entity) else { continue };
54 if let Some(t) = ray_sphere_intersection(ray_origin, ray_dir, transform.position, CAMERA_GIZMO_PICK_RADIUS) {
55 if best.is_none_or(|(_, best_t)| t < best_t) {
56 best = Some((entity, t));
57 }
58 }
59 }
60
61 best.map(|(entity, _)| entity)
62}
63
64fn pick_model(model: &Model, matrix: glam::Mat4, ray_origin: glam::Vec3, ray_dir: glam::Vec3) -> Option<f32> {
67 let mut nearest: Option<f32> = None;
68
69 for mesh in &model.meshes {
70 for tri in mesh.indices.chunks_exact(3) {
71 let v0 = matrix.transform_point3(mesh.positions[tri[0] as usize]);
72 let v1 = matrix.transform_point3(mesh.positions[tri[1] as usize]);
73 let v2 = matrix.transform_point3(mesh.positions[tri[2] as usize]);
74
75 if let Some(t) = ray_triangle_intersection(ray_origin, ray_dir, v0, v1, v2) {
76 if nearest.is_none_or(|n| t < n) {
77 nearest = Some(t);
78 }
79 }
80 }
81 }
82
83 nearest
84}
85
86fn ray_triangle_intersection(
89 origin: glam::Vec3, dir: glam::Vec3, v0: glam::Vec3, v1: glam::Vec3, v2: glam::Vec3,
90) -> Option<f32> {
91 const EPSILON: f32 = 1e-6;
92
93 let edge1 = v1 - v0;
94 let edge2 = v2 - v0;
95 let h = dir.cross(edge2);
96 let a = edge1.dot(h);
97 if a.abs() < EPSILON {
98 return None; }
100
101 let f = 1.0 / a;
102 let s = origin - v0;
103 let u = f * s.dot(h);
104 if u < 0.0 || u > 1.0 {
105 return None;
106 }
107
108 let q = s.cross(edge1);
109 let v = f * dir.dot(q);
110 if v < 0.0 || u + v > 1.0 {
111 return None;
112 }
113
114 let t = f * edge2.dot(q);
115 if t > EPSILON { Some(t) } else { None }
116}
117
118fn ray_sphere_intersection(origin: glam::Vec3, dir: glam::Vec3, center: glam::Vec3, radius: f32) -> Option<f32> {
119 let m = origin - center;
120 let b = m.dot(dir);
121 let c = m.dot(m) - radius * radius;
122 if c > 0.0 && b > 0.0 {
123 return None;
124 }
125 let discriminant = b * b - c;
126 if discriminant < 0.0 {
127 return None;
128 }
129 let t = -b - discriminant.sqrt();
130 Some(t.max(0.0))
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use libdqg::world::{CameraComponent, Transform};
137
138 #[test]
139 fn pick_hits_a_camera_entity_via_its_bounding_sphere() {
140 let mut camera = Camera { position: glam::Vec3::new(0.0, 0.0, 5.0), yaw: 0.0, pitch: 0.0, aspect: 1.0, fov: 45.0, znear: 0.1, zfar: 100.0 };
141 camera.look_at(glam::Vec3::ZERO);
142 let mut world = World::new(camera);
143 let entity = world.spawn_empty("Cam", Transform::default());
144 world.set_camera(entity, CameraComponent::default());
145
146 assert_eq!(pick(&world, &camera, 0.0, 0.0), Some(entity));
147 assert_eq!(pick(&world, &camera, 0.99, 0.99), None);
148 }
149}