picking + entity names

This commit is contained in:
Jonas H
2026-03-05 15:05:19 +01:00
parent 5e5c6a47e7
commit bab54b6f21
9 changed files with 400 additions and 50 deletions

View File

@@ -20,6 +20,7 @@ impl Bundle for CameraBundle
let transform = Transform::from_position(self.position); let transform = Transform::from_position(self.position);
world.cameras.insert(camera_entity, camera_component); world.cameras.insert(camera_entity, camera_component);
world.transforms.insert(camera_entity, transform); world.transforms.insert(camera_entity, transform);
world.names.insert(camera_entity, "Camera".to_string());
Ok(camera_entity) Ok(camera_entity)
} }
} }

View File

@@ -19,6 +19,7 @@ impl Bundle for SpotlightBundle
let transform = Transform::from_matrix(self.light_data.transform); let transform = Transform::from_matrix(self.light_data.transform);
world.transforms.insert(entity, transform); world.transforms.insert(entity, transform);
world.spotlights.insert(entity, self.light_data.component); world.spotlights.insert(entity, self.light_data.component);
world.names.insert(entity, "Spotlight".to_string());
if let Some(tag) = self.light_data.tag if let Some(tag) = self.light_data.tag
{ {
if tag == "lighthouse" if tag == "lighthouse"
@@ -34,12 +35,13 @@ impl Bundle for SpotlightBundle
pub fn spawn_spotlights(world: &mut World, spotlights: Vec<LightData>) pub fn spawn_spotlights(world: &mut World, spotlights: Vec<LightData>)
{ {
for light_data in spotlights for (index, light_data) in spotlights.into_iter().enumerate()
{ {
let entity = world.spawn(); let entity = world.spawn();
let transform = Transform::from_matrix(light_data.transform); let transform = Transform::from_matrix(light_data.transform);
world.transforms.insert(entity, transform); world.transforms.insert(entity, transform);
world.spotlights.insert(entity, light_data.component); world.spotlights.insert(entity, light_data.component);
world.names.insert(entity, format!("Spotlight_{}", index));
if let Some(tag) = light_data.tag if let Some(tag) = light_data.tag
{ {
if tag == "lighthouse" if tag == "lighthouse"

View File

@@ -9,12 +9,15 @@ pub mod movement;
pub mod noclip; pub mod noclip;
pub mod physics; pub mod physics;
pub mod rotate; pub mod rotate;
pub mod tree_instances;
pub use camera::CameraComponent; pub use camera::CameraComponent;
pub use dissolve::DissolveComponent; pub use dissolve::DissolveComponent;
pub use follow::FollowComponent; pub use follow::FollowComponent;
pub use input::InputComponent; pub use input::InputComponent;
pub use jump::JumpComponent;
pub use mesh::MeshComponent; pub use mesh::MeshComponent;
pub use movement::MovementComponent; pub use movement::MovementComponent;
pub use physics::PhysicsComponent; pub use physics::PhysicsComponent;
pub use rotate::RotateComponent; pub use rotate::RotateComponent;
pub use tree_instances::TreeInstancesComponent;

View File

@@ -0,0 +1,27 @@
use crate::loaders::mesh::InstanceData;
use std::rc::Rc;
use wgpu::Buffer;
pub struct TreeInstancesComponent
{
pub instances: Vec<InstanceData>,
pub dissolve_amounts: Vec<f32>,
pub dissolve_targets: Vec<f32>,
pub transition_speed: f32,
pub instance_buffer: Rc<Buffer>,
}
impl TreeInstancesComponent
{
pub fn new(instances: Vec<InstanceData>, instance_buffer: Rc<Buffer>) -> 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,
}
}
}

View File

@@ -1,7 +1,11 @@
use dear_imgui_rs::{Condition, Context}; use dear_imgui_rs::{Condition, Context};
use dear_imgui_wgpu::{WgpuInitInfo, WgpuRenderer}; use dear_imgui_wgpu::{WgpuInitInfo, WgpuRenderer};
use glam::EulerRot;
use sdl3_sys::events::SDL_Event; use sdl3_sys::events::SDL_Event;
use crate::entity::EntityHandle;
use crate::world::World;
pub struct FrameStats pub struct FrameStats
{ {
pub fps: f32, pub fps: f32,
@@ -57,7 +61,12 @@ impl Inspector
self.imgui.io().want_capture_mouse() 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<EntityHandle>,
)
{ {
let ui = self.imgui.frame(); let ui = self.imgui.frame();
ui.window("Inspector") ui.window("Inspector")
@@ -68,6 +77,113 @@ impl Inspector
ui.text(format!("Physics: {:.1} ms", stats.physics_budget_ms)); ui.text(format!("Physics: {:.1} ms", stats.physics_budget_ms));
ui.text(format!("Draw calls: {}", stats.draw_call_count)); 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) pub fn render(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView)

View File

@@ -78,5 +78,6 @@ pub fn editor_loop(
{ {
camera_noclip_system(world, input_state, delta); camera_noclip_system(world, input_state, delta);
} }
editor.inspector.build_ui(stats); let selected = editor.selected_entity;
editor.inspector.build_ui(stats, world, selected);
} }

View File

@@ -5,6 +5,7 @@ mod editor;
mod entity; mod entity;
mod loaders; mod loaders;
mod physics; mod physics;
mod picking;
mod postprocess; mod postprocess;
mod render; mod render;
mod snow; mod snow;
@@ -165,12 +166,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
editor.active = !editor.active; editor.active = !editor.active;
if editor.active if editor.active
{ {
stop_camera_following(&mut world, camera_entity);
sdl_context.mouse().set_relative_mouse_mode(&window, false); sdl_context.mouse().set_relative_mouse_mode(&window, false);
editor.right_mouse_held = false; editor.right_mouse_held = false;
input_state.mouse_captured = false; input_state.mouse_captured = false;
} }
else else
{ {
start_camera_following(&mut world, camera_entity);
input_state.mouse_captured = true; input_state.mouse_captured = true;
sdl_context.mouse().set_relative_mouse_mode(&window, true); sdl_context.mouse().set_relative_mouse_mode(&window, true);
} }
@@ -196,11 +199,38 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
{ {
editor.right_mouse_held = false; editor.right_mouse_held = false;
input_state.mouse_captured = false; input_state.mouse_captured = false;
start_camera_following(&mut world, camera_entity);
sdl_context.mouse().set_relative_mouse_mode(&window, false); sdl_context.mouse().set_relative_mouse_mode(&window, false);
continue; 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<dyn std::error::Error>>
if editor.active if editor.active
{ {
let screen_view = let screen_view = frame
frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); .texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = render::with_device(|d| { let mut encoder = render::with_device(|d| {
d.create_command_encoder(&wgpu::CommandEncoderDescriptor { d.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("ImGui Encoder"), label: Some("ImGui Encoder"),

View File

@@ -1,7 +1,10 @@
use crate::camera::Camera;
use crate::mesh::Mesh;
use glam::{Mat4, Vec3, Vec4}; 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 struct Ray
{ {
pub origin: Vec3, pub origin: Vec3,
@@ -15,7 +18,8 @@ impl Ray
screen_y: f32, screen_y: f32,
screen_width: u32, screen_width: u32,
screen_height: u32, screen_height: u32,
camera: &Camera, view: &Mat4,
projection: &Mat4,
) -> Self ) -> Self
{ {
let ndc_x = (2.0 * screen_x) / screen_width as f32 - 1.0; 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 clip_coords = Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
let view_matrix = camera.view_matrix(); let inv_projection = projection.inverse();
let projection_matrix = camera.projection_matrix(); let inv_view = view.inverse();
let inv_projection = projection_matrix.inverse();
let inv_view = view_matrix.inverse();
let eye_coords = inv_projection * clip_coords; let eye_coords = inv_projection * clip_coords;
let eye_coords = Vec4::new(eye_coords.x, eye_coords.y, -1.0, 0.0); 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 world_coords = inv_view * eye_coords;
let direction = Vec3::new(world_coords.x, world_coords.y, world_coords.z).normalize(); let direction = Vec3::new(world_coords.x, world_coords.y, world_coords.z).normalize();
Ray { let origin = inv_view.col(3).truncate();
origin: camera.position,
direction, Ray { origin, direction }
}
} }
pub fn intersects_mesh(&self, mesh: &Mesh, transform: &Mat4) -> Option<f32> pub fn intersects_aabb(&self, min: Vec3, max: Vec3) -> Option<f32>
{ {
let inv_transform = transform.inverse(); let inv_dir = Vec3::new(
let local_origin = inv_transform.transform_point3(self.origin); 1.0 / self.direction.x,
let local_direction = inv_transform.transform_vector3(self.direction).normalize(); 1.0 / self.direction.y,
1.0 / self.direction.z,
);
let mut closest_distance = f32::MAX; let t1 = (min - self.origin) * inv_dir;
let mut hit = false; 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 = Some(tenter.max(0.0))
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)
} }
else else
{ {
None None
} }
} }
}
fn intersects_triangle_local( fn transform_aabb(aabb_min: Vec3, aabb_max: Vec3, model: &Mat4) -> (Vec3, Vec3)
&self, {
local_origin: Vec3, let corners = [
local_direction: Vec3, Vec3::new(aabb_min.x, aabb_min.y, aabb_min.z),
_mesh: &Mesh, Vec3::new(aabb_max.x, aabb_min.y, aabb_min.z),
_triangle_idx: u32, Vec3::new(aabb_min.x, aabb_max.y, aabb_min.z),
) -> Option<f32> 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<f32>
{
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 None
} }
} }
/// Raymesh triangle intersection in local space, returning world-space distance.
///
/// Transforms the ray into local mesh space, tests every triangle with
/// MöllerTrumbore, 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<f32>
{
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<f32>
{
let mut closest: Option<f32> = 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<EntityHandle>
{
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
}

View File

@@ -2,11 +2,11 @@ use std::collections::HashMap;
use crate::components::dissolve::DissolveComponent; use crate::components::dissolve::DissolveComponent;
use crate::components::follow::FollowComponent; use crate::components::follow::FollowComponent;
use crate::components::jump::JumpComponent;
use crate::components::lights::spot::SpotlightComponent; use crate::components::lights::spot::SpotlightComponent;
use crate::components::tree_instances::TreeInstancesComponent;
use crate::components::{ use crate::components::{
CameraComponent, InputComponent, MeshComponent, MovementComponent, PhysicsComponent, CameraComponent, InputComponent, JumpComponent, MeshComponent, MovementComponent,
RotateComponent, PhysicsComponent, RotateComponent,
}; };
use crate::entity::{EntityHandle, EntityManager}; use crate::entity::{EntityHandle, EntityManager};
use crate::state::StateMachine; use crate::state::StateMachine;
@@ -84,6 +84,8 @@ pub struct World
pub dissolves: Storage<DissolveComponent>, pub dissolves: Storage<DissolveComponent>,
pub follows: Storage<FollowComponent>, pub follows: Storage<FollowComponent>,
pub rotates: Storage<RotateComponent>, pub rotates: Storage<RotateComponent>,
pub tree_instances: Storage<TreeInstancesComponent>,
pub names: Storage<String>,
} }
impl World impl World
@@ -106,6 +108,8 @@ impl World
dissolves: Storage::new(), dissolves: Storage::new(),
follows: Storage::new(), follows: Storage::new(),
rotates: Storage::new(), rotates: Storage::new(),
tree_instances: Storage::new(),
names: Storage::new(),
} }
} }
@@ -130,6 +134,8 @@ impl World
self.dissolves.remove(entity); self.dissolves.remove(entity);
self.follows.remove(entity); self.follows.remove(entity);
self.rotates.remove(entity); self.rotates.remove(entity);
self.tree_instances.remove(entity);
self.names.remove(entity);
self.entities.despawn(entity); self.entities.despawn(entity);
} }