From bab54b6f219fc714ff8e3bb388bd8b512c251529 Mon Sep 17 00:00:00 2001 From: Jonas H Date: Thu, 5 Mar 2026 15:05:19 +0100 Subject: [PATCH] picking + entity names --- src/bundles/camera.rs | 1 + src/bundles/spotlight.rs | 4 +- src/components/mod.rs | 3 + src/components/tree_instances.rs | 27 ++++ src/editor/inspector.rs | 118 ++++++++++++++- src/editor/mod.rs | 3 +- src/main.rs | 37 ++++- src/picking.rs | 245 +++++++++++++++++++++++++------ src/world.rs | 12 +- 9 files changed, 400 insertions(+), 50 deletions(-) create mode 100644 src/components/tree_instances.rs diff --git a/src/bundles/camera.rs b/src/bundles/camera.rs index 3127ae7..58488b2 100644 --- a/src/bundles/camera.rs +++ b/src/bundles/camera.rs @@ -20,6 +20,7 @@ impl Bundle for CameraBundle let transform = Transform::from_position(self.position); world.cameras.insert(camera_entity, camera_component); world.transforms.insert(camera_entity, transform); + world.names.insert(camera_entity, "Camera".to_string()); Ok(camera_entity) } } diff --git a/src/bundles/spotlight.rs b/src/bundles/spotlight.rs index 2dba3a1..e1d9f04 100644 --- a/src/bundles/spotlight.rs +++ b/src/bundles/spotlight.rs @@ -19,6 +19,7 @@ impl Bundle for SpotlightBundle let transform = Transform::from_matrix(self.light_data.transform); world.transforms.insert(entity, transform); world.spotlights.insert(entity, self.light_data.component); + world.names.insert(entity, "Spotlight".to_string()); if let Some(tag) = self.light_data.tag { if tag == "lighthouse" @@ -34,12 +35,13 @@ impl Bundle for SpotlightBundle pub fn spawn_spotlights(world: &mut World, spotlights: Vec) { - for light_data in spotlights + for (index, light_data) in spotlights.into_iter().enumerate() { let entity = world.spawn(); let transform = Transform::from_matrix(light_data.transform); world.transforms.insert(entity, transform); world.spotlights.insert(entity, light_data.component); + world.names.insert(entity, format!("Spotlight_{}", index)); if let Some(tag) = light_data.tag { if tag == "lighthouse" diff --git a/src/components/mod.rs b/src/components/mod.rs index ce140f6..1722881 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -9,12 +9,15 @@ pub mod movement; pub mod noclip; pub mod physics; pub mod rotate; +pub mod tree_instances; pub use camera::CameraComponent; pub use dissolve::DissolveComponent; pub use follow::FollowComponent; pub use input::InputComponent; +pub use jump::JumpComponent; pub use mesh::MeshComponent; pub use movement::MovementComponent; pub use physics::PhysicsComponent; pub use rotate::RotateComponent; +pub use tree_instances::TreeInstancesComponent; diff --git a/src/components/tree_instances.rs b/src/components/tree_instances.rs new file mode 100644 index 0000000..e404c39 --- /dev/null +++ b/src/components/tree_instances.rs @@ -0,0 +1,27 @@ +use crate::loaders::mesh::InstanceData; +use std::rc::Rc; +use wgpu::Buffer; + +pub struct TreeInstancesComponent +{ + pub instances: Vec, + pub dissolve_amounts: Vec, + pub dissolve_targets: Vec, + pub transition_speed: f32, + pub instance_buffer: Rc, +} + +impl TreeInstancesComponent +{ + pub fn new(instances: Vec, instance_buffer: Rc) -> Self + { + let num_instances = instances.len(); + Self { + instances, + dissolve_amounts: vec![0.0; num_instances], + dissolve_targets: vec![0.0; num_instances], + transition_speed: 5.0, + instance_buffer, + } + } +} diff --git a/src/editor/inspector.rs b/src/editor/inspector.rs index d204e01..2341499 100644 --- a/src/editor/inspector.rs +++ b/src/editor/inspector.rs @@ -1,7 +1,11 @@ use dear_imgui_rs::{Condition, Context}; use dear_imgui_wgpu::{WgpuInitInfo, WgpuRenderer}; +use glam::EulerRot; use sdl3_sys::events::SDL_Event; +use crate::entity::EntityHandle; +use crate::world::World; + pub struct FrameStats { pub fps: f32, @@ -57,7 +61,12 @@ impl Inspector self.imgui.io().want_capture_mouse() } - pub fn build_ui(&mut self, stats: &FrameStats) + pub fn build_ui( + &mut self, + stats: &FrameStats, + world: &World, + selected_entity: Option, + ) { let ui = self.imgui.frame(); ui.window("Inspector") @@ -68,6 +77,113 @@ impl Inspector ui.text(format!("Physics: {:.1} ms", stats.physics_budget_ms)); ui.text(format!("Draw calls: {}", stats.draw_call_count)); }); + + if let Some(entity) = selected_entity + { + ui.window("Entity") + .position([10.0, 120.0], Condition::FirstUseEver) + .build(|| { + let name = world + .names + .get(entity) + .cloned() + .unwrap_or_else(|| format!("Entity #{}", entity)); + ui.text(format!("Name: {}", name)); + ui.text(format!("ID: {}", entity)); + + ui.separator(); + ui.text("Transform"); + + if let Some(transform) = world.transforms.get(entity) + { + let p = transform.position; + ui.text(format!(" Pos ({:.2}, {:.2}, {:.2})", p.x, p.y, p.z)); + + let (ex, ey, ez) = transform.rotation.to_euler(EulerRot::XYZ); + ui.text(format!( + " Rot ({:.1}, {:.1}, {:.1}) deg", + ex.to_degrees(), + ey.to_degrees(), + ez.to_degrees() + )); + + let s = transform.scale; + ui.text(format!(" Scale ({:.2}, {:.2}, {:.2})", s.x, s.y, s.z)); + } + else + { + ui.text(" (no transform)"); + } + + ui.separator(); + ui.text("Components"); + + if world.meshes.get(entity).is_some() + { + ui.text(" Mesh"); + } + if world.physics.get(entity).is_some() + { + ui.text(" Physics"); + } + if let Some(m) = world.movements.get(entity) + { + ui.text(format!( + " Movement (max_speed {:.1})", + m.max_walking_speed + )); + } + if world.jumps.get(entity).is_some() + { + ui.text(" Jump"); + } + if world.inputs.get(entity).is_some() + { + ui.text(" Input"); + } + if world.player_tags.get(entity).is_some() + { + ui.text(" [Player]"); + } + if world.tree_tags.get(entity).is_some() + { + ui.text(" [Tree]"); + } + if let Some(cam) = world.cameras.get(entity) + { + ui.text(format!( + " Camera fov={:.0} near={:.2} far={:.0}", + cam.fov.to_degrees(), + cam.near, + cam.far + )); + } + if let Some(spot) = world.spotlights.get(entity) + { + let o = spot.offset; + ui.text(format!( + " Spotlight offset ({:.1}, {:.1}, {:.1})", + o.x, o.y, o.z + )); + } + if let Some(ti) = world.tree_instances.get(entity) + { + ui.text(format!(" TreeInstances ({})", ti.instances.len())); + } + if world.follows.get(entity).is_some() + { + ui.text(" Follow"); + } + if world.rotates.get(entity).is_some() + { + ui.text(" Rotate"); + } + if let Some(d) = world.dissolves.get(entity) + { + ui.text(format!(" Dissolve ({:.2})", d.amount)); + } + }); + } } pub fn render(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 566fff5..5928389 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -78,5 +78,6 @@ pub fn editor_loop( { camera_noclip_system(world, input_state, delta); } - editor.inspector.build_ui(stats); + let selected = editor.selected_entity; + editor.inspector.build_ui(stats, world, selected); } diff --git a/src/main.rs b/src/main.rs index 62016d9..095647d 100755 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod editor; mod entity; mod loaders; mod physics; +mod picking; mod postprocess; mod render; mod snow; @@ -165,12 +166,14 @@ fn main() -> Result<(), Box> editor.active = !editor.active; if editor.active { + stop_camera_following(&mut world, camera_entity); sdl_context.mouse().set_relative_mouse_mode(&window, false); editor.right_mouse_held = false; input_state.mouse_captured = false; } else { + start_camera_following(&mut world, camera_entity); input_state.mouse_captured = true; sdl_context.mouse().set_relative_mouse_mode(&window, true); } @@ -196,11 +199,38 @@ fn main() -> Result<(), Box> { editor.right_mouse_held = false; input_state.mouse_captured = false; - start_camera_following(&mut world, camera_entity); sdl_context.mouse().set_relative_mouse_mode(&window, false); continue; } + Event::MouseButtonDown { + mouse_btn: MouseButton::Left, + x, + y, + .. + } if editor.active && !editor.wants_mouse() => + { + if let Some(view) = crate::systems::camera_view_matrix(&world) + { + if let Some((_, cam)) = world.active_camera() + { + let projection = cam.projection_matrix(); + let (win_w, win_h) = window.size(); + let ray = crate::picking::Ray::from_screen_position( + *x, + *y, + win_w, + win_h, + &view, + &projection, + ); + editor.selected_entity = crate::picking::pick_entity(&ray, &world); + render::set_selected_entity(editor.selected_entity); + } + } + continue; + } + _ => {} } @@ -303,8 +333,9 @@ fn main() -> Result<(), Box> if editor.active { - let screen_view = - frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); + let screen_view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = render::with_device(|d| { d.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("ImGui Encoder"), diff --git a/src/picking.rs b/src/picking.rs index e331a5b..babdd98 100644 --- a/src/picking.rs +++ b/src/picking.rs @@ -1,7 +1,10 @@ -use crate::camera::Camera; -use crate::mesh::Mesh; use glam::{Mat4, Vec3, Vec4}; +use crate::components::TreeInstancesComponent; +use crate::entity::EntityHandle; +use crate::loaders::mesh::Mesh; +use crate::world::World; + pub struct Ray { pub origin: Vec3, @@ -15,7 +18,8 @@ impl Ray screen_y: f32, screen_width: u32, screen_height: u32, - camera: &Camera, + view: &Mat4, + projection: &Mat4, ) -> Self { let ndc_x = (2.0 * screen_x) / screen_width as f32 - 1.0; @@ -23,10 +27,8 @@ impl Ray let clip_coords = Vec4::new(ndc_x, ndc_y, -1.0, 1.0); - let view_matrix = camera.view_matrix(); - let projection_matrix = camera.projection_matrix(); - let inv_projection = projection_matrix.inverse(); - let inv_view = view_matrix.inverse(); + let inv_projection = projection.inverse(); + let inv_view = view.inverse(); let eye_coords = inv_projection * clip_coords; let eye_coords = Vec4::new(eye_coords.x, eye_coords.y, -1.0, 0.0); @@ -34,54 +36,215 @@ impl Ray let world_coords = inv_view * eye_coords; let direction = Vec3::new(world_coords.x, world_coords.y, world_coords.z).normalize(); - Ray { - origin: camera.position, - direction, - } + let origin = inv_view.col(3).truncate(); + + Ray { origin, direction } } - pub fn intersects_mesh(&self, mesh: &Mesh, transform: &Mat4) -> Option + pub fn intersects_aabb(&self, min: Vec3, max: Vec3) -> Option { - let inv_transform = transform.inverse(); - let local_origin = inv_transform.transform_point3(self.origin); - let local_direction = inv_transform.transform_vector3(self.direction).normalize(); + let inv_dir = Vec3::new( + 1.0 / self.direction.x, + 1.0 / self.direction.y, + 1.0 / self.direction.z, + ); - let mut closest_distance = f32::MAX; - let mut hit = false; + let t1 = (min - self.origin) * inv_dir; + let t2 = (max - self.origin) * inv_dir; - for triangle_idx in (0..mesh.num_indices).step_by(3) + let tmin = t1.min(t2); + let tmax = t1.max(t2); + + let tenter = tmin.x.max(tmin.y).max(tmin.z); + let texit = tmax.x.min(tmax.y).min(tmax.z); + + if tenter <= texit && texit >= 0.0 { - let distance = - self.intersects_triangle_local(local_origin, local_direction, mesh, triangle_idx); - - if let Some(d) = distance - { - if d < closest_distance - { - closest_distance = d; - hit = true; - } - } - } - - if hit - { - Some(closest_distance) + Some(tenter.max(0.0)) } else { None } } +} - fn intersects_triangle_local( - &self, - local_origin: Vec3, - local_direction: Vec3, - _mesh: &Mesh, - _triangle_idx: u32, - ) -> Option +fn transform_aabb(aabb_min: Vec3, aabb_max: Vec3, model: &Mat4) -> (Vec3, Vec3) +{ + let corners = [ + Vec3::new(aabb_min.x, aabb_min.y, aabb_min.z), + Vec3::new(aabb_max.x, aabb_min.y, aabb_min.z), + Vec3::new(aabb_min.x, aabb_max.y, aabb_min.z), + Vec3::new(aabb_max.x, aabb_max.y, aabb_min.z), + Vec3::new(aabb_min.x, aabb_min.y, aabb_max.z), + Vec3::new(aabb_max.x, aabb_min.y, aabb_max.z), + Vec3::new(aabb_min.x, aabb_max.y, aabb_max.z), + Vec3::new(aabb_max.x, aabb_max.y, aabb_max.z), + ]; + + let mut world_min = Vec3::splat(f32::MAX); + let mut world_max = Vec3::splat(f32::MIN); + for corner in &corners + { + let world_corner = model.transform_point3(*corner); + world_min = world_min.min(world_corner); + world_max = world_max.max(world_corner); + } + (world_min, world_max) +} + +fn intersect_triangle(origin: Vec3, dir: Vec3, v0: Vec3, v1: Vec3, v2: Vec3) -> Option +{ + let edge1 = v1 - v0; + let edge2 = v2 - v0; + let h = dir.cross(edge2); + let a = edge1.dot(h); + if a.abs() < 1e-7 + { + return None; + } + let f = 1.0 / a; + let s = origin - v0; + let u = f * s.dot(h); + if !(0.0..=1.0).contains(&u) + { + return None; + } + let q = s.cross(edge1); + let v = f * dir.dot(q); + if v < 0.0 || u + v > 1.0 + { + return None; + } + let t = f * edge2.dot(q); + if t > 1e-7 + { + Some(t) + } + else { None } } + +/// Ray–mesh triangle intersection in local space, returning world-space distance. +/// +/// Transforms the ray into local mesh space, tests every triangle with +/// Möller–Trumbore, then maps the closest local hit back to a world-space +/// distance so results can be compared across entities. +fn intersect_mesh_triangles(ray: &Ray, mesh: &Mesh, model: &Mat4) -> Option +{ + if mesh.cpu_indices.is_empty() + { + return None; + } + + let inv_model = model.inverse(); + let local_origin = inv_model.transform_point3(ray.origin); + let local_dir = inv_model.transform_vector3(ray.direction); + + let mut closest_t = f32::MAX; + let mut hit = false; + + for tri in mesh.cpu_indices.chunks_exact(3) + { + let v0 = Vec3::from(mesh.cpu_positions[tri[0] as usize]); + let v1 = Vec3::from(mesh.cpu_positions[tri[1] as usize]); + let v2 = Vec3::from(mesh.cpu_positions[tri[2] as usize]); + + if let Some(t) = intersect_triangle(local_origin, local_dir, v0, v1, v2) + { + if t < closest_t + { + closest_t = t; + hit = true; + } + } + } + + if hit + { + let local_hit = local_origin + local_dir * closest_t; + let world_hit = model.transform_point3(local_hit); + Some((world_hit - ray.origin).length()) + } + else + { + None + } +} + +fn intersect_instances( + ray: &Ray, + mesh: &Mesh, + tree_instances: &TreeInstancesComponent, +) -> Option +{ + let mut closest: Option = None; + + for instance in &tree_instances.instances + { + let instance_model = Mat4::from_scale_rotation_translation( + instance.scale, + instance.rotation, + instance.position, + ); + let (world_min, world_max) = transform_aabb(mesh.aabb_min, mesh.aabb_max, &instance_model); + + if let Some(d) = ray.intersects_aabb(world_min, world_max) + { + if closest.map_or(true, |c| d < c) + { + closest = Some(d); + } + } + } + + closest +} + +pub fn pick_entity(ray: &Ray, world: &World) -> Option +{ + let mut closest_entity = None; + let mut closest_distance = f32::MAX; + + for (&entity, mesh_component) in &world.meshes.components + { + let transform = match world.transforms.get(entity) + { + Some(t) => t, + None => continue, + }; + + let model = transform.to_matrix(); + + let distance = if let Some(tree_instances) = world.tree_instances.get(entity) + { + intersect_instances(ray, &mesh_component.mesh, tree_instances) + } + else + { + let (world_min, world_max) = transform_aabb( + mesh_component.mesh.aabb_min, + mesh_component.mesh.aabb_max, + &model, + ); + if ray.intersects_aabb(world_min, world_max).is_none() + { + continue; + } + intersect_mesh_triangles(ray, &mesh_component.mesh, &model) + }; + + if let Some(d) = distance + { + if d < closest_distance + { + closest_distance = d; + closest_entity = Some(entity); + } + } + } + + closest_entity +} diff --git a/src/world.rs b/src/world.rs index a29e72f..641760f 100644 --- a/src/world.rs +++ b/src/world.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use crate::components::dissolve::DissolveComponent; use crate::components::follow::FollowComponent; -use crate::components::jump::JumpComponent; use crate::components::lights::spot::SpotlightComponent; +use crate::components::tree_instances::TreeInstancesComponent; use crate::components::{ - CameraComponent, InputComponent, MeshComponent, MovementComponent, PhysicsComponent, - RotateComponent, + CameraComponent, InputComponent, JumpComponent, MeshComponent, MovementComponent, + PhysicsComponent, RotateComponent, }; use crate::entity::{EntityHandle, EntityManager}; use crate::state::StateMachine; @@ -84,6 +84,8 @@ pub struct World pub dissolves: Storage, pub follows: Storage, pub rotates: Storage, + pub tree_instances: Storage, + pub names: Storage, } impl World @@ -106,6 +108,8 @@ impl World dissolves: Storage::new(), follows: Storage::new(), rotates: Storage::new(), + tree_instances: Storage::new(), + names: Storage::new(), } } @@ -130,6 +134,8 @@ impl World self.dissolves.remove(entity); self.follows.remove(entity); self.rotates.remove(entity); + self.tree_instances.remove(entity); + self.names.remove(entity); self.entities.despawn(entity); }