Basic 3d select implemented0

selection-refactor
Elijah Voigt 2 years ago
parent fc2a0850d7
commit df74674c96

BIN
assets/models/untitled.glb (Stored with Git LFS)

Binary file not shown.

@ -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<f32> {
fn intersects(ray: &Ray, gt: &GlobalTransform, mesh: &Mesh) -> Option<Hit> {
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<f32> {
&& 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,
}

@ -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::<Board3d>),
set_piece_position.run_if(any_component_changed::<BoardIndex>),
set_piece_texture.run_if(any_component_changed::<Side>),
select_3d
.run_if(in_state(GameState::Display3d))
.run_if(on_event::<MouseButtonInput>()),
),
)
.add_systems(
@ -37,6 +41,7 @@ impl Plugin for Display3dPlugin {
mouse_zoom
.run_if(in_state(GameState::Display3d))
.run_if(on_event::<MouseWheel>()),
selected_gizmo,
)
.run_if(resource_exists::<debug::DebugEnabled>()),
)
@ -229,15 +234,18 @@ fn gizmo_system(mut gizmos: Gizmos) {
/// TODO: This has bad feel, needs to be tuned
fn move_camera(
buttons: Res<Input<MouseButton>>,
mut events: EventReader<MouseMotion>,
mut camera: Query<&mut Transform, (With<Display3d>, With<Camera>)>,
) {
events.iter().for_each(|MouseMotion { delta }| {
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<MouseButtonInput>,
query: Query<(Entity, &Handle<Mesh>, &GlobalTransform)>,
meshes: Res<Assets<Mesh>>,
cameras: Query<(&Camera, &GlobalTransform)>,
windows: Query<&Window, With<PrimaryWindow>>,
parents: Query<Entity, (With<Piece3d>, Without<game::Selected>)>,
children: Query<&Children>,
mut commands: Commands,
selected: Query<Entity, (With<game::Selected>, With<Piece3d>)>,
) {
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, &gt).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::<game::Selected>();
});
commands.entity(parent).insert(game::Selected);
});
Some(())
});
});
Some(())
});
});
Some(())
});
});
});
}
fn selected_gizmo(
selected: Query<&Transform, (With<game::Selected>, With<Piece3d>)>,
mut gizmos: Gizmos,
) {
selected.iter().for_each(|transform| {
gizmos.cuboid(transform.clone(), Color::GREEN);
})
}

@ -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<Hit> {
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
}
}

@ -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;

Loading…
Cancel
Save