diff --git a/CHANGELOG.md b/CHANGELOG.md index 9755461..6b53394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,11 @@ ## [Unreleased] -* Added `Hex::as_u64` and `Hex::from_u64` +* Added `Hex::as_u64` and `Hex::from_u64` (#169) * Bumped `bevy` to 0.14.x (#173) * Bumped bevy ecosystem dependencies (#173) * Updated examples (#173) +* Added a 3d picking exampe (#177) ## 0.17.0 diff --git a/Cargo.toml b/Cargo.toml index 89f7319..50903f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,20 +45,21 @@ optional = true [dev-dependencies.bevy] version = "0.14" features = [ - "bevy_asset", - "bevy_winit", - "bevy_core_pipeline", - "bevy_pbr", - "bevy_render", - "bevy_sprite", - "bevy_text", - "default_font", - "png", - "x11", - "tonemapping_luts", - "bevy_gizmos", - # Faster compilation - "dynamic_linking", + "bevy_asset", + "bevy_winit", + "bevy_core_pipeline", + "bevy_pbr", + "bevy_render", + "bevy_sprite", + "bevy_text", + "default_font", + "png", + "x11", + "tonemapping_luts", + "bevy_gizmos", + "multi_threaded", + # Faster compilation + "dynamic_linking", ] default-features = false @@ -104,6 +105,10 @@ path = "examples/field_of_movement.rs" name = "3d_columns" path = "examples/3d_columns.rs" +[[example]] +name = "3d_picking" +path = "examples/3d_picking.rs" + [[example]] name = "mesh_builder" path = "examples/mesh_builder.rs" diff --git a/README.md b/README.md index 6bba52a..df0ee94 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,14 @@ and how to use two layouts on the same grid. > `cargo run --example 3d_columns` This example showcases the 3d hexagon columns procedural generation + +### 3d picking + +![picking](docs/3d_picking.png "3d picking example") + + > `cargo run --example 3d_picking` + +This example showcases how to use the camera ray to detect hovered 3d columns ### Mesh builder diff --git a/docs/3d_columns.png b/docs/3d_columns.png index 2664669..1ee5637 100644 Binary files a/docs/3d_columns.png and b/docs/3d_columns.png differ diff --git a/docs/3d_picking.png b/docs/3d_picking.png new file mode 100644 index 0000000..4fed3f3 Binary files /dev/null and b/docs/3d_picking.png differ diff --git a/examples/3d_picking.rs b/examples/3d_picking.rs new file mode 100644 index 0000000..4fc78f1 --- /dev/null +++ b/examples/3d_picking.rs @@ -0,0 +1,139 @@ +use bevy::{ + color::palettes::css::{WHITE, YELLOW}, + prelude::*, + render::{mesh::Indices, render_asset::RenderAssetUsages, render_resource::PrimitiveTopology}, + utils::HashMap, + window::PrimaryWindow, +}; +use hexx::{shapes, *}; +use light_consts::lux; + +/// World size of the hexagons (outer radius) +const HEX_SIZE: Vec2 = Vec2::splat(1.0); +/// World space height of hex columns +const COLUMN_HEIGHT: f32 = 10.0; +/// Map radius +const MAP_RADIUS: u32 = 20; + +pub fn main() { + App::new() + .insert_resource(AmbientLight { + brightness: lux::OFFICE, + ..default() + }) + .add_plugins(DefaultPlugins) + .add_systems(Startup, (setup_camera, setup_grid)) + .add_systems(Update, higlight_hovered) + .run(); +} + +#[derive(Debug, Resource)] +struct Map { + layout: HexLayout, + entities: HashMap, + highlighted_material: Handle, + default_material: Handle, +} + +/// 3D Orthogrpahic camera setup +fn setup_camera(mut commands: Commands) { + let transform = Transform::from_xyz(0.0, 60.0, 60.0).looking_at(Vec3::ZERO, Vec3::Y); + commands.spawn(Camera3dBundle { + transform, + ..default() + }); + let transform = Transform::from_xyz(60.0, 60.0, 00.0).looking_at(Vec3::ZERO, Vec3::Y); + commands.spawn(DirectionalLightBundle { + transform, + ..default() + }); +} + +/// Hex grid setup +fn setup_grid( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let layout = HexLayout { + hex_size: HEX_SIZE, + ..default() + }; + // materials + let default_material = materials.add(Color::Srgba(WHITE)); + let highlighted_material = materials.add(Color::Srgba(YELLOW)); + // mesh + let mesh = hexagonal_column(&layout); + let mesh_handle = meshes.add(mesh); + + let entities = shapes::hexagon(Hex::ZERO, MAP_RADIUS) + .map(|hex| { + let pos = layout.hex_to_world_pos(hex); + let id = commands + .spawn(PbrBundle { + transform: Transform::from_xyz(pos.x, -COLUMN_HEIGHT, pos.y), + mesh: mesh_handle.clone(), + material: default_material.clone_weak(), + ..default() + }) + .id(); + (hex, id) + }) + .collect(); + commands.insert_resource(Map { + layout, + entities, + highlighted_material, + default_material, + }); +} + +fn higlight_hovered( + mut commands: Commands, + map: Res, + mut highlighted: Local, + cameras: Query<(&Camera, &GlobalTransform)>, + windows: Query<&Window, With>, +) { + let window = windows.single(); + let (camera, cam_transform) = cameras.single(); + let Some(ray) = window + .cursor_position() + .and_then(|p| camera.viewport_to_world(cam_transform, p)) + else { + return; + }; + let Some(distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Dir3::Y)) else { + return; + }; + let point = ray.origin + ray.direction * distance; + let coord = map.layout.world_pos_to_hex(point.xz()); + if coord != *highlighted { + let Some(entity) = map.entities.get(&coord).copied() else { + return; + }; + commands + .entity(entity) + .insert(map.highlighted_material.clone_weak()); + commands + .entity(map.entities[&*highlighted]) + .insert(map.default_material.clone_weak()); + *highlighted = coord; + } +} + +/// Compute a bevy mesh from the layout +fn hexagonal_column(hex_layout: &HexLayout) -> Mesh { + let mesh_info = ColumnMeshBuilder::new(hex_layout, COLUMN_HEIGHT) + .without_bottom_face() + .center_aligned() + .build(); + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::RENDER_WORLD, + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, mesh_info.vertices) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, mesh_info.normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, mesh_info.uvs) + .with_inserted_indices(Indices::U16(mesh_info.indices)) +} diff --git a/src/algorithms/pathfinding.rs b/src/algorithms/pathfinding.rs index a8697af..78e7de6 100644 --- a/src/algorithms/pathfinding.rs +++ b/src/algorithms/pathfinding.rs @@ -46,7 +46,7 @@ fn reconstruct_path(came_from: &HashMap, end: Hex) -> Vec { /// * `start` - start node /// * `end` - destination node /// * `cost` - cost function taking a node pair (`a` -> `b`) and returning the -/// logical cost to go from `a` to `b` +/// logical cost to go from `a` to `b` /// /// # Examples /// diff --git a/src/hex/mod.rs b/src/hex/mod.rs index 90d1cac..4901826 100644 --- a/src/hex/mod.rs +++ b/src/hex/mod.rs @@ -698,8 +698,7 @@ impl Hex { /// Find in which [`VertexDirection`] wedge `rhs` is relative to `self`. /// /// > This method can be innaccurate in case of a *tie* between directions, - /// > prefer - /// using [`Self::diagonal_way_to`] instead + /// > prefer using [`Self::diagonal_way_to`] instead pub fn main_diagonal_to(self, rhs: Self) -> VertexDirection { self.diagonal_way_to(rhs).unwrap() } @@ -723,8 +722,7 @@ impl Hex { /// Find in which [`EdgeDirection`] wedge `rhs` is relative to `self` /// /// > This method can be innaccurate in case of a *tie* between directions, - /// > prefer - /// using [`Self::way_to`] for accuracy + /// > prefer using [`Self::way_to`] for accuracy #[must_use] pub fn main_direction_to(self, rhs: Self) -> EdgeDirection { self.way_to(rhs).unwrap() diff --git a/src/mesh/utils.rs b/src/mesh/utils.rs index b02e241..65e6f84 100644 --- a/src/mesh/utils.rs +++ b/src/mesh/utils.rs @@ -108,8 +108,7 @@ impl Face { /// /// * `mode` - the insetting behaviour mode /// * `keep_inner_face` - If set to true the insetted face will be kept, - /// otherwise - /// it will be removed + /// otherwise it will be removed #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn inset(self, mode: InsetScaleMode, scale: f32, keep_inner_face: bool) -> MeshInfo {