From ab4c9740b2a8e5135c8d2206fb56920629d4d46c Mon Sep 17 00:00:00 2001 From: Martin Indra Date: Wed, 19 Oct 2022 21:54:09 +0200 Subject: [PATCH] Implement markers of selected movable entities Maximum number of units (which might all be selected) was decreased to 1023 so it fits into a 2D tree of depth 10 (used in the shader). Relates to #36. --- Cargo.lock | 7 +- assets/shaders/terrain.wgsl | 128 +++++++++++++++- crates/controller/Cargo.toml | 1 + crates/controller/src/selection.rs | 26 +++- crates/terrain/Cargo.toml | 4 + crates/terrain/src/lib.rs | 5 +- crates/terrain/src/marker.rs | 115 ++++++++++++++ crates/terrain/src/shader.rs | 233 ++++++++++++++++++++++++++++- 8 files changed, 504 insertions(+), 15 deletions(-) create mode 100644 crates/terrain/src/marker.rs diff --git a/Cargo.lock b/Cargo.lock index e7d4e87b8..2f56191cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1680,6 +1680,7 @@ dependencies = [ "de_behaviour", "de_core", "de_index", + "de_objects", "de_pathing", "de_spawner", "de_terrain", @@ -1848,7 +1849,9 @@ dependencies = [ "bevy", "de_core", "de_map", + "de_objects", "glam", + "itertools", "iyes_loopless", "iyes_progress", "parry3d", @@ -2480,9 +2483,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] diff --git a/assets/shaders/terrain.wgsl b/assets/shaders/terrain.wgsl index 5187b66b2..50cae829c 100644 --- a/assets/shaders/terrain.wgsl +++ b/assets/shaders/terrain.wgsl @@ -10,10 +10,26 @@ // How large (in meters) is a texture. let TEXTURE_SIZE = 16.; +let SHAPE_COLOR = vec4(1., 1., 1., 0.75); +let SHAPE_THICKNESS = 0.15; +// Keep thie array lenght in sync with /crates/terrain/src/shader.rs. +let MAX_KD_TREE_SIZE = 127u; + +struct KdTreeNode { + @align(16) location: vec2, + radius: f32, +}; + +struct KdTree { + @align(16) nodes: array, + count: u32, +}; @group(1) @binding(0) -var terrain_texture: texture_2d; +var circles: KdTree; @group(1) @binding(1) +var terrain_texture: texture_2d; +@group(1) @binding(2) var terrain_sampler: sampler; struct FragmentInput { @@ -22,6 +38,112 @@ struct FragmentInput { #import bevy_pbr::mesh_vertex_output }; +fn mix_colors(base: vec4, cover: vec4) -> vec4 { + let alpha = base.a * cover.a; + let rgb = base.rgb * cover.a + cover.rgb * (1. - cover.a); + return vec4(rgb, alpha); +} + +fn draw_circle( + base: vec4, + uv: vec2, + center: vec2, + radius: f32, +) -> vec4 { + let distance: f32 = distance(uv, center); + if distance <= (radius + SHAPE_THICKNESS) && radius <= distance { + return mix_colors(base, SHAPE_COLOR); + } + return base; +} + +struct KdRecord { + index: u32, + distance: f32, +} + +struct Next { + index: u32, + depth: u32, + potential: f32, +} + +fn nearest(uv: vec2) -> u32 { + if circles.count == 0u { + return MAX_KD_TREE_SIZE; + } + + var best: KdRecord; + best.index = 0u; + best.distance = distance(circles.nodes[0].location, uv); + + var stack_size: u32 = 1u; + // Make sure that the stack size is large enought to cover balanced three + // of size MAX_KD_TREE_SIZE. + var stack: array; + stack[0].index = 0u; + stack[0].potential = 0.; + stack[0].depth = 0u; + + while stack_size > 0u { + stack_size -= 1u; + let next = stack[stack_size]; + + if next.potential >= best.distance { + continue; + } + + let node = circles.nodes[next.index]; + + let distance = distance(node.location, uv); + if distance < best.distance { + best.index = next.index; + best.distance = distance; + } + + let axis = next.depth % 2u; + let diff = uv[axis] - node.location[axis]; + + var close = 2u * next.index + 2u; + var away = 2u * next.index + 1u; + + if diff <= 0. { + close -= 1u; + away += 1u; + } + + if away < circles.count { + stack[stack_size].index = away; + stack[stack_size].potential = abs(diff); + stack[stack_size].depth = next.depth + 1u; + stack_size += 1u; + } + + if close < circles.count { + stack[stack_size].index = close; + stack[stack_size].potential = 0.; + stack[stack_size].depth = next.depth + 1u; + stack_size += 1u; + } + } + + return best.index; +} + +fn draw_circles(base: vec4, uv: vec2) -> vec4 { + var output_color = base; + + let index = nearest(uv); + if index < MAX_KD_TREE_SIZE { + let node = circles.nodes[index]; + let center = node.location; + let radius = node.radius; + output_color = draw_circle(output_color, uv, center, radius); + } + + return output_color; +} + @fragment fn fragment(in: FragmentInput) -> @location(0) vec4 { var pbr_input: PbrInput = pbr_input_new(); @@ -58,5 +180,7 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { ); pbr_input.V = calculate_view(in.world_position, pbr_input.is_orthographic); - return tone_mapping(pbr(pbr_input)); + var output_color = tone_mapping(pbr(pbr_input)); + output_color = draw_circles(output_color, in.uv); + return output_color; } diff --git a/crates/controller/Cargo.toml b/crates/controller/Cargo.toml index e6822ed69..681ecb8a2 100644 --- a/crates/controller/Cargo.toml +++ b/crates/controller/Cargo.toml @@ -13,6 +13,7 @@ categories = ["games"] [dependencies] # DE de_core = { path = "../core", version = "0.1.0-dev" } +de_objects = { path = "../objects", version = "0.1.0-dev" } de_index = { path = "../index", version = "0.1.0-dev" } de_terrain = { path = "../terrain", version = "0.1.0-dev" } de_pathing = { path = "../pathing", version = "0.1.0-dev" } diff --git a/crates/controller/src/selection.rs b/crates/controller/src/selection.rs index 6558710b7..db3f92e1b 100644 --- a/crates/controller/src/selection.rs +++ b/crates/controller/src/selection.rs @@ -1,9 +1,12 @@ use ahash::AHashSet; -use bevy::{ - ecs::system::SystemParam, - prelude::{App, Commands, Component, Entity, EventReader, Plugin, Query, With}, +use bevy::{ecs::system::SystemParam, prelude::*}; +use de_core::{ + objects::{MovableSolid, ObjectType}, + stages::GameStage, + state::GameState, }; -use de_core::{stages::GameStage, state::GameState}; +use de_objects::{IchnographyCache, ObjectCache}; +use de_terrain::CircleMarker; use iyes_loopless::prelude::*; use crate::Labels; @@ -68,7 +71,9 @@ pub(crate) enum SelectionMode { #[derive(SystemParam)] struct Selector<'w, 's> { commands: Commands<'w, 's>, + cache: Res<'w, ObjectCache>, selected: Query<'w, 's, Entity, With>, + movable: Query<'w, 's, &'static ObjectType, With>, } impl<'w, 's> Selector<'w, 's> { @@ -82,11 +87,20 @@ impl<'w, 's> Selector<'w, 's> { }; for entity in deselect { - self.commands.entity(entity).remove::(); + let mut entity_commands = self.commands.entity(entity); + entity_commands.remove::(); + if self.movable.contains(entity) { + entity_commands.remove::(); + } } for entity in select { - self.commands.entity(entity).insert(Selected); + let mut entity_commands = self.commands.entity(entity); + entity_commands.insert(Selected); + if let Ok(&object_type) = self.movable.get(entity) { + let radius = self.cache.get_ichnography(object_type).radius(); + entity_commands.insert(CircleMarker::new(radius)); + } } } } diff --git a/crates/terrain/Cargo.toml b/crates/terrain/Cargo.toml index 021377a53..847bd7ac8 100644 --- a/crates/terrain/Cargo.toml +++ b/crates/terrain/Cargo.toml @@ -14,6 +14,7 @@ categories = ["games"] # DE de_core = { path = "../core", version = "0.1.0-dev" } de_map = { path = "../map", version = "0.1.0-dev" } +de_objects = { path = "../objects", version = "0.1.0-dev" } # Other bevy = "0.8" @@ -22,3 +23,6 @@ iyes_progress = { version = "0.4", features = [ "iyes_loopless" ] } glam = "0.21" parry3d = "0.9.0" ahash = "0.7.6" + +[dev-dependencies] +itertools = "0.10.5" diff --git a/crates/terrain/src/lib.rs b/crates/terrain/src/lib.rs index 6e6320d99..4c8795d40 100644 --- a/crates/terrain/src/lib.rs +++ b/crates/terrain/src/lib.rs @@ -1,10 +1,13 @@ mod collider; +mod marker; mod plugin; mod shader; mod terrain; use bevy::{app::PluginGroupBuilder, prelude::*}; pub use collider::TerrainCollider; +pub use marker::CircleMarker; +use marker::MarkerPlugin; use plugin::TerrainPlugin; pub use terrain::TerrainBundle; @@ -12,6 +15,6 @@ pub struct TerrainPluginGroup; impl PluginGroup for TerrainPluginGroup { fn build(&mut self, group: &mut PluginGroupBuilder) { - group.add(TerrainPlugin); + group.add(TerrainPlugin).add(MarkerPlugin); } } diff --git a/crates/terrain/src/marker.rs b/crates/terrain/src/marker.rs new file mode 100644 index 000000000..72ebce32a --- /dev/null +++ b/crates/terrain/src/marker.rs @@ -0,0 +1,115 @@ +use bevy::{ + prelude::*, + render::{ + primitives::{Aabb, Frustum, Sphere}, + view::VisibilitySystems, + }, + utils::FloatOrd, +}; +use de_core::{objects::ObjectType, projection::ToFlat, state::GameState}; +use de_objects::{ColliderCache, ObjectCache}; +use glam::Vec3A; +use iyes_loopless::prelude::*; + +use crate::shader::{Circle, TerrainMaterial, CIRCLE_CAPACITY}; + +pub(crate) struct MarkerPlugin; + +impl Plugin for MarkerPlugin { + fn build(&self, app: &mut App) { + app.add_system_to_stage( + CoreStage::PostUpdate, + update_markers + .run_in_state(GameState::Playing) + .after(VisibilitySystems::CheckVisibility), + ); + } +} + +/// A semi-transparent circle is drawn on the terrain surface below every +/// entity with this component. +#[derive(Component)] +pub struct CircleMarker { + radius: f32, +} + +impl CircleMarker { + pub fn new(radius: f32) -> Self { + Self { radius } + } + + pub(crate) fn radius(&self) -> f32 { + self.radius + } +} + +fn update_markers( + mut materials: ResMut>, + cache: Res, + camera: Query<(&Transform, &Frustum), With>, + terrains: Query<(&ComputedVisibility, &Handle)>, + markers: Query<( + &ObjectType, + &ComputedVisibility, + &GlobalTransform, + &CircleMarker, + )>, +) { + let (eye, frustum) = match camera.get_single() { + Ok((transform, frustum)) => (transform.translation, frustum), + Err(_) => return, + }; + + struct CircleWithDist { + circle: Circle, + distance_sq: FloatOrd, + } + + let mut candidates = Vec::new(); + for (&object_type, circle_visibility, transform, marker) in markers.iter() { + if !circle_visibility.is_visible_in_hierarchy() { + continue; + } + + let aabb = cache.get_collider(object_type).aabb(); + let aabb = Aabb { + center: Vec3A::from(aabb.center()), + half_extents: Vec3A::from(aabb.half_extents()), + }; + + let translation = transform.translation(); + + if intersects_frustum(frustum, transform, &aabb) { + candidates.push(CircleWithDist { + circle: Circle::new(translation.to_flat(), marker.radius()), + distance_sq: FloatOrd(eye.distance_squared(translation)), + }); + } + } + candidates.sort_unstable_by_key(|c| c.distance_sq); + + let circles: Vec = candidates + .iter() + .take(CIRCLE_CAPACITY) + .map(|c| c.circle) + .collect(); + + for (terrain_visibility, material) in terrains.iter() { + if !terrain_visibility.is_visible_in_hierarchy() { + continue; + } + + let material = materials.get_mut(material).unwrap(); + material.set_markers(circles.clone()); + } +} + +fn intersects_frustum(frustum: &Frustum, transform: &GlobalTransform, aabb: &Aabb) -> bool { + let model = transform.compute_matrix(); + let model_sphere = Sphere { + center: model.transform_point3a(aabb.center), + radius: transform.radius_vec3a(aabb.half_extents), + }; + + frustum.intersects_sphere(&model_sphere, false) && frustum.intersects_obb(aabb, &model, false) +} diff --git a/crates/terrain/src/shader.rs b/crates/terrain/src/shader.rs index 1b8919213..2cb4a399d 100644 --- a/crates/terrain/src/shader.rs +++ b/crates/terrain/src/shader.rs @@ -1,20 +1,36 @@ +use std::{cmp::Ordering, ops::Range}; + use bevy::{ prelude::{Handle, Image, Material}, reflect::TypeUuid, - render::render_resource::{AsBindGroup, ShaderRef}, + render::render_resource::{AsBindGroup, ShaderRef, ShaderType}, }; +use glam::Vec2; + +// * Keep this in sync with terrain.wgsl. +// * Keep this smaller or equal to de_core::objects::PLAYER_MAX_UNITS. +pub(crate) const CIRCLE_CAPACITY: usize = 127; #[derive(AsBindGroup, TypeUuid, Debug, Clone)] #[uuid = "9e124e04-fdf1-4836-b82d-fa2f01fddb62"] pub struct TerrainMaterial { - #[texture(0)] - #[sampler(1)] + #[uniform(0)] + circles: KdTree, + #[texture(1)] + #[sampler(2)] texture: Handle, } impl TerrainMaterial { pub(crate) fn new(texture: Handle) -> Self { - Self { texture } + Self { + circles: KdTree::empty(), + texture, + } + } + + pub(crate) fn set_markers(&mut self, circles: Vec) { + self.circles = KdTree::build(circles); } } @@ -23,3 +39,212 @@ impl Material for TerrainMaterial { "shaders/terrain.wgsl".into() } } + +#[derive(ShaderType, Debug, Clone, Copy, Default)] +pub(crate) struct Circle { + #[align(16)] + center: Vec2, + radius: f32, +} + +impl Circle { + /// Creates a new circle. + /// + /// # Panics + /// + /// * If `center` is not finite. + /// * If radius is non finite or is smaller or equal to zero. + pub(crate) fn new(center: Vec2, radius: f32) -> Self { + if !center.is_finite() { + panic!("Circle center is not finite: {:?}", center); + } + if !radius.is_finite() { + panic!("Circle radius is not finite: {:?}", radius); + } + if radius <= 0. { + panic!("Circle radius is smaller or equal to 0: {:?}", radius); + } + + Self { center, radius } + } + + fn coord(&self, axis: Axis) -> f32 { + match axis { + Axis::X => self.center.x, + Axis::Y => self.center.y, + } + } +} + +#[derive(Copy, Clone)] +enum Axis { + X, + Y, +} + +impl Axis { + fn toggle(self) -> Self { + match self { + Self::X => Self::Y, + Self::Y => Self::X, + } + } +} + +#[derive(ShaderType, Debug, Clone)] +struct KdTree { + /// Nodes of the KD tree. Given a node at `index` its left child is at `2 * + /// index + 1` and right child is at `2 * index + 2`. + #[align(16)] + nodes: [Circle; CIRCLE_CAPACITY], + count: u32, +} + +impl KdTree { + fn empty() -> Self { + Self { + nodes: [Circle::default(); CIRCLE_CAPACITY], + count: 0, + } + } + + /// Build the KD tree from a vector of circles. + /// + /// # Panics + /// + /// Panics if the number of circles is larger than maximum allowed + /// capacity. + fn build(mut circles: Vec) -> Self { + if circles.len() > CIRCLE_CAPACITY { + panic!( + "Number of circles {} is greater than shader capacity {}.", + circles.len(), + CIRCLE_CAPACITY + ); + } + + let mut tree = Self { + nodes: [Circle::default(); CIRCLE_CAPACITY], + count: circles.len() as u32, + }; + + if circles.is_empty() { + return tree; + } + + struct StackItem { + index: u16, + axis: Axis, + range: Range, + } + + let mut stack: Vec = vec![StackItem { + index: 0, + axis: Axis::X, + range: 0..circles.len(), + }]; + + while !stack.is_empty() { + let item = stack.pop().unwrap(); + let subtree = &mut circles[item.range.clone()]; + + subtree.sort_by(|a, b| { + let a = a.coord(item.axis); + let b = b.coord(item.axis); + + if a < b { + Ordering::Less + } else if a > b { + Ordering::Greater + } else { + Ordering::Equal + } + }); + + // A median cannot be used because it is desired that that the + // three occupies continuous range in tree.nodes (from 0 to + // circles.len()). + // + // The following computation guarantees that that the tree is + // balanced (the max diff between tree depth is 1) and that it each + // level/floor is filled from "left to right". + let num_bits = 31 - (subtree.len() as u32).leading_zeros(); + let max_subtree_size = (1 << num_bits) - 1; + let min_subtree_size = max_subtree_size >> 1; + let split_index = (subtree.len() - min_subtree_size - 1).min(max_subtree_size); + tree.nodes[item.index as usize] = subtree[split_index]; + + if split_index < (subtree.len() - 1) { + stack.push(StackItem { + index: 2 * item.index + 2, + axis: item.axis.toggle(), + range: (item.range.start + split_index + 1)..item.range.end, + }); + } + if split_index > 0 { + stack.push(StackItem { + index: 2 * item.index + 1, + axis: item.axis.toggle(), + range: item.range.start..(item.range.start + split_index), + }); + } + } + + tree + } +} + +#[cfg(test)] +mod tests { + use itertools::Itertools; + + use super::*; + + #[test] + fn test_kd_tree_build_many() { + let mut circles = vec![ + Circle::new(Vec2::new(1., -4.), 1.), + Circle::new(Vec2::new(-2., 1.), 1.), + Circle::new(Vec2::new(-1.5, -3.), 1.), + Circle::new(Vec2::new(2., 1.), 1.), + ]; + + for permutation in (0..circles.len()).permutations(circles.len()) { + let version: Vec = permutation.iter().map(|index| circles[*index]).collect(); + + let tree = KdTree::build(version); + assert_eq!(tree.count, 4); + assert_eq!(tree.nodes[0].center, Vec2::new(1., -4.)); + assert_eq!(tree.nodes[1].center, Vec2::new(-2., 1.)); + assert_eq!(tree.nodes[2].center, Vec2::new(2., 1.)); + assert_eq!(tree.nodes[3].center, Vec2::new(-1.5, -3.)); + } + + circles.push(Circle::new(Vec2::new(-8., 2.), 1.)); + for permutation in (0..circles.len()).permutations(circles.len()) { + let version: Vec = permutation.iter().map(|index| circles[*index]).collect(); + + let tree = KdTree::build(version); + assert_eq!(tree.count, 5); + assert_eq!(tree.nodes[0].center, Vec2::new(1., -4.)); + assert_eq!(tree.nodes[1].center, Vec2::new(-2., 1.)); + assert_eq!(tree.nodes[2].center, Vec2::new(2., 1.)); + assert_eq!(tree.nodes[3].center, Vec2::new(-1.5, -3.)); + assert_eq!(tree.nodes[4].center, Vec2::new(-8., 2.)); + } + + circles.push(Circle::new(Vec2::new(1.5, 0.), 1.)); + for permutation in (0..circles.len()).permutations(circles.len()) { + let version: Vec = permutation.iter().map(|index| circles[*index]).collect(); + + let tree = KdTree::build(version); + assert_eq!(tree.count, 6); + assert_eq!(tree.nodes[0].center, Vec2::new(1., -4.)); + assert_eq!(tree.nodes[1].center, Vec2::new(-2., 1.)); + assert_eq!(tree.nodes[2].center, Vec2::new(2., 1.)); + assert_eq!(tree.nodes[3].center, Vec2::new(-1.5, -3.)); + assert_eq!(tree.nodes[4].center, Vec2::new(-8., 2.)); + assert_eq!(tree.nodes[5].center, Vec2::new(1.5, 0.)); + } + } +}