diff --git a/assets/models/untitled.glb b/assets/models/untitled.glb new file mode 100644 index 0000000..01e1aad --- /dev/null +++ b/assets/models/untitled.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb8aca7b0b6a2c4652436eb0ae9f7e562abd491f0941455290642e89126cce9a +size 187392 diff --git a/examples/select.rs b/examples/select.rs index dc95e85..e3113a0 100644 --- a/examples/select.rs +++ b/examples/select.rs @@ -3,7 +3,6 @@ /// Example to illustrate selecting objects in 3d space /// use bevy::{ - math::Vec3A, prelude::*, render::mesh::MeshVertexAttribute, render::render_resource::{Extent3d, TextureDimension, TextureFormat}, @@ -153,10 +152,16 @@ impl Triangle { } } +#[derive(Debug)] +struct Hit { + distance: f32, + point: Vec3, +} + /// Heavily synthesized by these two things: /// * Textbook: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/ray-triangle-intersection-geometric-solution.html /// * Example: https://github.com/aevyrie/bevy_mod_raycast/blob/435d8ef100738797161ac3a9b910ea346a4ed6e6/src/raycast.rs#L43 -fn intersects(ray: &Ray, gt: &GlobalTransform, mesh: &Mesh) -> Option { +fn intersects(ray: &Ray, gt: &GlobalTransform, mesh: &Mesh) -> Option { let attr = MeshVertexAttribute::new("Vertex_Position", 0, VertexFormat::Float32x3); if let Some(verts) = mesh.attribute(attr) { if let Some(idxs) = mesh.indices() { @@ -201,12 +206,15 @@ fn intersects(ray: &Ray, gt: &GlobalTransform, mesh: &Mesh) -> Option { && triangle.normal().dot(c1) > 0.0 && triangle.normal().dot(c2) > 0.0 }; - hit.then_some(d) + hit.then_some(Hit { + distance: d, + point: p, + }) } else { None } }) - .min_by(|a, b| a.total_cmp(b)) + .min_by(|a, b| a.distance.total_cmp(&b.distance)) } _ => None, } diff --git a/src/display3d.rs b/src/display3d.rs index 9e0f086..81bd2f9 100644 --- a/src/display3d.rs +++ b/src/display3d.rs @@ -4,8 +4,9 @@ use crate::{ }; use bevy::{ core_pipeline::Skybox, - input::mouse::{MouseMotion, MouseWheel}, + input::mouse::{MouseButtonInput, MouseMotion, MouseWheel}, render::render_resource::{TextureViewDescriptor, TextureViewDimension}, + window::PrimaryWindow, }; pub(crate) struct Display3dPlugin; @@ -25,6 +26,9 @@ impl Plugin for Display3dPlugin { set_board_model.run_if(any_component_added::), set_piece_position.run_if(any_component_changed::), set_piece_texture.run_if(any_component_changed::), + select_3d + .run_if(in_state(GameState::Display3d)) + .run_if(on_event::()), ), ) .add_systems( @@ -37,6 +41,7 @@ impl Plugin for Display3dPlugin { mouse_zoom .run_if(in_state(GameState::Display3d)) .run_if(on_event::()), + selected_gizmo, ) .run_if(resource_exists::()), ) @@ -229,15 +234,18 @@ fn gizmo_system(mut gizmos: Gizmos) { /// TODO: This has bad feel, needs to be tuned fn move_camera( + buttons: Res>, mut events: EventReader, mut camera: Query<&mut Transform, (With, With)>, ) { events.iter().for_each(|MouseMotion { delta }| { - camera.iter_mut().for_each(|mut t| { - t.rotate_around(Vec3::ZERO, Quat::from_rotation_y(delta.x / 256.0)); - t.rotate_around(Vec3::ZERO, Quat::from_rotation_x(delta.y / 256.0)); - t.look_at(Vec3::ZERO, Vec3::Y); - }); + if buttons.pressed(MouseButton::Left) { + camera.iter_mut().for_each(|mut t| { + t.rotate_around(Vec3::ZERO, Quat::from_rotation_y(delta.x / 256.0)); + t.rotate_around(Vec3::ZERO, Quat::from_rotation_x(delta.y / 256.0)); + t.look_at(Vec3::ZERO, Vec3::Y); + }); + } }); } @@ -367,3 +375,66 @@ fn set_piece_texture( } }) } + +/// Function for selecting entities based on ray intersection +fn select_3d( + mut events: EventReader, + query: Query<(Entity, &Handle, &GlobalTransform)>, + meshes: Res>, + cameras: Query<(&Camera, &GlobalTransform)>, + windows: Query<&Window, With>, + parents: Query, Without)>, + children: Query<&Children>, + mut commands: Commands, + selected: Query, With)>, +) { + events + .iter() + .filter(|ev| ev.state == ButtonState::Pressed) + .for_each(|_| { + windows.iter().for_each(|window| { + window.cursor_position().and_then(|pos| { + cameras.iter().for_each(|(camera, gt)| { + camera.viewport_to_world(gt, pos).and_then(|ray| { + query + .iter() + .filter_map(|(entity, handle, gt)| { + meshes.get(handle).map(|mesh| (entity, mesh, gt)) + }) + .for_each(|(entity, mesh, gt)| { + hit3d::intersects(&ray, mesh, >).and_then(|_hit| { + parents + .iter() + .find(|&parent| { + children + .iter_descendants(parent) + .any(|child| child == entity) + }) + .iter() + .for_each(|&parent| { + // TODO: Only remove/insert component if different + selected.iter().for_each(|s| { + commands.entity(s).remove::(); + }); + commands.entity(parent).insert(game::Selected); + }); + Some(()) + }); + }); + Some(()) + }); + }); + Some(()) + }); + }); + }); +} + +fn selected_gizmo( + selected: Query<&Transform, (With, With)>, + mut gizmos: Gizmos, +) { + selected.iter().for_each(|transform| { + gizmos.cuboid(transform.clone(), Color::GREEN); + }) +} diff --git a/src/hit3d.rs b/src/hit3d.rs new file mode 100644 index 0000000..9a480da --- /dev/null +++ b/src/hit3d.rs @@ -0,0 +1,117 @@ +use bevy::render::{ + mesh::{MeshVertexAttribute, VertexAttributeValues}, + render_resource::VertexFormat, +}; + +use crate::prelude::*; + +/// Struct containing hit data +/// The point in Global (not Local) space and the distance from the Camera +#[derive(Debug)] +pub(crate) struct Hit { + distance: f32, + point: Vec3, +} + +/// A 3D Triangle used for ray-intersection tests +#[derive(Debug)] +struct Triangle { + v0: Vec3, + v1: Vec3, + v2: Vec3, +} + +/// Helper functions for ray-intersect testing +impl Triangle { + fn normal(&self) -> Vec3 { + (self.edge_a()).cross(self.edge_b()) + } + + fn edge_a(&self) -> Vec3 { + self.v1 - self.v0 + } + + fn edge_b(&self) -> Vec3 { + self.v2 - self.v0 + } + + fn edge0(&self) -> Vec3 { + self.v1 - self.v0 + } + + fn edge1(&self) -> Vec3 { + self.v2 - self.v1 + } + + fn edge2(&self) -> Vec3 { + self.v0 - self.v2 + } +} + +/// Heavily synthesized from these two resources: +/// * Textbook: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/ray-triangle-intersection-geometric-solution.html +/// * Example: https://github.com/aevyrie/bevy_mod_raycast/blob/435d8ef100738797161ac3a9b910ea346a4ed6e6/src/raycast.rs#L43 +pub(crate) fn intersects(ray: &Ray, mesh: &Mesh, gt: &GlobalTransform) -> Option { + let attr = MeshVertexAttribute::new("Vertex_Position", 0, VertexFormat::Float32x3); + if let Some(verts) = mesh.attribute(attr) { + if let Some(idxs) = mesh.indices() { + match verts { + VertexAttributeValues::Float32x3(vals) => { + idxs.iter() + .array_chunks::<3>() + // Convert arrays to vec3 + .map(|[x, y, z]| { + [ + Vec3::from_array(vals[x]), + Vec3::from_array(vals[y]), + Vec3::from_array(vals[z]), + ] + }) + // Transform each point by the global transform + .map(|[a, b, c]| { + [ + gt.transform_point(a), + gt.transform_point(b), + gt.transform_point(c), + ] + }) + // Collect everything into a triangle for easy operations + .map(|[v0, v1, v2]| Triangle { v0, v1, v2 }) + .filter_map(|triangle| { + // Calculate the distance this ray hits the plane normal to the tri + if let Some(d) = ray.intersect_plane(triangle.v0, triangle.normal()) { + // Calculate the point on that plane which intersects + let p = ray.get_point(d); + // Inside out test + let hit = { + // Determine if p is w/in edge0 + let c0 = triangle.edge0().cross(p - triangle.v0); + // Determine if p is w/in edge1 + let c1 = triangle.edge1().cross(p - triangle.v1); + // Determine if p is w/in edge2 + let c2 = triangle.edge2().cross(p - triangle.v2); + + // Check all three at once + triangle.normal().dot(c0) > 0.0 + && triangle.normal().dot(c1) > 0.0 + && triangle.normal().dot(c2) > 0.0 + }; + hit.then_some(Hit { + distance: d, + point: p, + }) + } else { + None + } + }) + .min_by(|a, b| a.distance.total_cmp(&b.distance)) + } + _ => None, + } + } else { + None + } + } else { + None + } +} diff --git a/src/main.rs b/src/main.rs index c76002b..5df5e56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,12 @@ +#![feature(iter_array_chunks)] // used in ray.rs + mod audio; mod credits; mod debug; mod display2d; mod display3d; mod game; +mod hit3d; mod loading; mod menu; mod prelude;