diff --git a/assets/shaders/rally_point.wgsl b/assets/shaders/rally_point.wgsl new file mode 100644 index 00000000..f52d0142 --- /dev/null +++ b/assets/shaders/rally_point.wgsl @@ -0,0 +1,43 @@ +#import bevy_pbr::mesh_view_bindings +#import bevy_pbr::mesh_bindings + +struct CustomMaterial { + color: vec4, + pointiness: f32, + speed: f32, + length: f32, + spacing: f32, + fade: f32, +}; + +@group(1) @binding(0) +var material: CustomMaterial; + +const COLOR: vec4 = vec4(0.0, 0.5, 0.0, 0.8); +const POINTINESS: f32 = 2.; +const SPEED: f32 = 3.; +const LENGTH: f32 = 1.; +const SPACING: f32 = 0.5; +const FADE: f32 = 3.; + +@fragment +fn fragment( + #import bevy_pbr::mesh_vertex_output +) -> @location(0) vec4 { + let world_space_length: f32 = length(mesh.model[0].xyz); + let scaled_x: f32 = uv.x * world_space_length; + let offset_y: f32 = abs(uv.y - 0.5) * POINTINESS; + let scaled_time: f32 = globals.time * SPEED; + let total_length = LENGTH + SPACING; + + let value = scaled_x + offset_y - scaled_time; + // Ensure that the result of the modulo operation is always positive + let positive_modulo = (value % total_length + total_length) % total_length; + let alpha = step(SPACING, positive_modulo); + + let start_fade: f32 = (floor(value / total_length) * total_length + scaled_time) / FADE; + let end_fade: f32 = (world_space_length - ((ceil(value / total_length) * total_length + scaled_time))) / FADE; + let fade = min(1., min(start_fade, end_fade)); + + return COLOR * vec4(1., 1., 1., alpha * fade); +} diff --git a/crates/construction/src/manufacturing.rs b/crates/construction/src/manufacturing.rs index c560b342..507a3895 100644 --- a/crates/construction/src/manufacturing.rs +++ b/crates/construction/src/manufacturing.rs @@ -15,7 +15,9 @@ use de_core::{ use de_index::SpatialQuery; use de_objects::SolidObjects; use de_pathing::{PathQueryProps, PathTarget, UpdateEntityPath}; -use de_signs::UpdatePoleLocationEvent; +use de_signs::{ + LineLocation, UpdateLineEndEvent, UpdateLineLocationEvent, UpdatePoleLocationEvent, +}; use de_spawner::{ObjectCounter, SpawnBundle}; use parry2d::bounding_volume::Aabb; use parry3d::math::Isometry; @@ -309,13 +311,20 @@ fn configure( solids: SolidObjects, new: Query<(Entity, &Transform, &ObjectType), Added>, mut pole_events: EventWriter, + mut line_events: EventWriter, ) { for (entity, transform, &object_type) in new.iter() { let solid = solids.get(object_type); - if solid.factory().is_some() { + if let Some(factory) = solid.factory() { + let start = transform.transform_point(factory.position().to_msl()); let local_aabb = solid.ichnography().local_aabb(); let delivery_location = DeliveryLocation::initial(local_aabb, transform); pole_events.send(UpdatePoleLocationEvent::new(entity, delivery_location.0)); + let end = delivery_location.0.to_msl(); + line_events.send(UpdateLineLocationEvent::new( + entity, + LineLocation::new(start, end), + )); commands .entity(entity) .insert((AssemblyLine::default(), delivery_location)); @@ -327,14 +336,15 @@ fn change_locations( mut events: EventReader, mut locations: Query<&mut DeliveryLocation>, mut pole_events: EventWriter, + mut line_events: EventWriter, ) { for event in events.iter() { if let Ok(mut location) = locations.get_mut(event.factory()) { + let owner = event.factory(); location.0 = event.position(); - pole_events.send(UpdatePoleLocationEvent::new( - event.factory(), - event.position(), - )); + pole_events.send(UpdatePoleLocationEvent::new(owner, event.position())); + let end = event.position().to_msl(); + line_events.send(UpdateLineEndEvent::new(owner, end)); } } } diff --git a/crates/controller/src/selection/bookkeeping.rs b/crates/controller/src/selection/bookkeeping.rs index 63249bb3..a535a880 100644 --- a/crates/controller/src/selection/bookkeeping.rs +++ b/crates/controller/src/selection/bookkeeping.rs @@ -1,7 +1,7 @@ use ahash::AHashSet; use bevy::{ecs::system::SystemParam, prelude::*}; use de_core::{baseset::GameSet, gamestate::GameState}; -use de_signs::{UpdateBarVisibilityEvent, UpdatePoleVisibilityEvent}; +use de_signs::{UpdateBarVisibilityEvent, UpdateLineVisibilityEvent, UpdatePoleVisibilityEvent}; use de_terrain::MarkerVisibility; use crate::SELECTION_BAR_ID; @@ -167,6 +167,7 @@ fn selected_system( mut markers: Query<&mut MarkerVisibility>, mut bars: EventWriter, mut poles: EventWriter, + mut lines: EventWriter, ) { for event in events.iter() { if let Ok(mut visibility) = markers.get_mut(event.0) { @@ -180,6 +181,7 @@ fn selected_system( )); poles.send(UpdatePoleVisibilityEvent::new(event.0, true)); + lines.send(UpdateLineVisibilityEvent::new(event.0, true)); } } @@ -188,6 +190,7 @@ fn deselected_system( mut markers: Query<&mut MarkerVisibility>, mut bars: EventWriter, mut poles: EventWriter, + mut lines: EventWriter, ) { for event in events.iter() { if let Ok(mut visibility) = markers.get_mut(event.0) { @@ -201,5 +204,6 @@ fn deselected_system( )); poles.send(UpdatePoleVisibilityEvent::new(event.0, false)); + lines.send(UpdateLineVisibilityEvent::new(event.0, false)); } } diff --git a/crates/signs/src/lib.rs b/crates/signs/src/lib.rs index 12234070..219b4b65 100644 --- a/crates/signs/src/lib.rs +++ b/crates/signs/src/lib.rs @@ -1,11 +1,16 @@ use bars::BarsPlugin; pub use bars::{UpdateBarValueEvent, UpdateBarVisibilityEvent}; use bevy::{app::PluginGroupBuilder, prelude::*}; +use line::LinePlugin; +pub use line::{ + LineLocation, UpdateLineEndEvent, UpdateLineLocationEvent, UpdateLineVisibilityEvent, +}; use markers::MarkersPlugin; use pole::PolePlugin; pub use pole::{UpdatePoleLocationEvent, UpdatePoleVisibilityEvent}; mod bars; +mod line; mod markers; mod pole; @@ -22,5 +27,6 @@ impl PluginGroup for SignsPluginGroup { .add(BarsPlugin) .add(MarkersPlugin) .add(PolePlugin) + .add(LinePlugin) } } diff --git a/crates/signs/src/line.rs b/crates/signs/src/line.rs new file mode 100644 index 00000000..7cc9d8fe --- /dev/null +++ b/crates/signs/src/line.rs @@ -0,0 +1,237 @@ +use ahash::AHashMap; +use bevy::prelude::*; +use bevy::reflect::TypeUuid; +use bevy::render::render_resource::{AsBindGroup, ShaderRef}; +use de_core::baseset::GameSet; +use de_core::cleanup::DespawnOnGameExit; +use de_core::objects::Active; +use de_core::state::AppState; + +/// Width of the line that goes to the pole. +const LINE_WIDTH: f32 = 1.; +/// Offset above mean sea level of the line, stopping z-fighting with the floor. +const LINE_OFFSET: Vec3 = Vec3::new(0., 1e-3, 0.); + +pub(crate) struct LinePlugin; + +impl Plugin for LinePlugin { + fn build(&self, app: &mut App) { + app.add_plugin(MaterialPlugin::::default()) + .add_event::() + .add_event::() + .add_event::() + .add_system(setup.in_schedule(OnEnter(AppState::InGame))) + .add_system(cleanup.in_schedule(OnExit(AppState::InGame))) + .add_system( + update_line_end + .in_base_set(GameSet::PostUpdate) + .run_if(in_state(AppState::InGame)) + .run_if(on_event::()) + .in_set(LinesSet::LineEnd), + ) + .add_system( + update_line_location + .in_base_set(GameSet::PostUpdate) + .run_if(in_state(AppState::InGame)) + .run_if(on_event::()) + .in_set(LinesSet::LocationEvents) + .after(LinesSet::LineEnd), + ) + .add_system( + update_line_visibility + .in_base_set(GameSet::PostUpdate) + .run_if(in_state(AppState::InGame)) + .run_if(on_event::()) + .in_set(LinesSet::VisibilityEvents) + .after(LinesSet::LocationEvents), + ) + .add_system( + owner_despawn + .in_base_set(GameSet::PostUpdate) + .run_if(in_state(AppState::InGame)) + .in_set(LinesSet::Despawn) + .after(LinesSet::VisibilityEvents), + ); + } +} + +#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, SystemSet)] +enum LinesSet { + LineEnd, + LocationEvents, + VisibilityEvents, + Despawn, +} + +// Passed to the `rally_point.wgsl` shader +#[derive(AsBindGroup, TypeUuid, Debug, Clone)] +#[uuid = "d0fae52d-f398-4416-9b72-9039093a6c34"] +pub struct LineMaterial {} + +impl Material for LineMaterial { + fn fragment_shader() -> ShaderRef { + "shaders/rally_point.wgsl".into() + } + + fn alpha_mode(&self) -> AlphaMode { + AlphaMode::Blend + } +} + +#[derive(Clone, Copy)] +pub struct LineLocation { + start: Vec3, + end: Vec3, +} + +impl LineLocation { + pub fn new(start: Vec3, end: Vec3) -> Self { + Self { start, end } + } + + /// A transform matrix from a plane with points at `(-0.5, 0. -0.5), (0.5, 0. -0.5), + /// (0.5, 0., 0.5), (-0.5, 0., -0.5)` to the line start and end with the `LINE_WIDTH`. + fn transform(&self) -> Transform { + let line_direction = self.end - self.start; + let perpendicular_direction = + Vec3::new(-line_direction.z, line_direction.y, line_direction.x).normalize() + * LINE_WIDTH; + let x_axis = line_direction.extend(0.); + let z_axis = perpendicular_direction.extend(0.); + let w_axis = (self.start + line_direction / 2. + LINE_OFFSET).extend(1.); + Transform::from_matrix(Mat4::from_cols(x_axis, Vec4::Y, z_axis, w_axis)) + } +} + +pub struct UpdateLineVisibilityEvent { + owner: Entity, + visible: bool, +} + +impl UpdateLineVisibilityEvent { + pub fn new(owner: Entity, visible: bool) -> Self { + Self { owner, visible } + } +} + +pub struct UpdateLineLocationEvent { + owner: Entity, + location: LineLocation, +} + +impl UpdateLineLocationEvent { + pub fn new(owner: Entity, location: LineLocation) -> Self { + Self { owner, location } + } +} + +pub struct UpdateLineEndEvent { + owner: Entity, + end: Vec3, +} + +impl UpdateLineEndEvent { + pub fn new(owner: Entity, end: Vec3) -> Self { + Self { owner, end } + } +} + +#[derive(Resource)] +struct LineMesh(Handle, Handle); + +#[derive(Resource, Default)] +struct LineEntities(AHashMap); + +#[derive(Resource, Default)] +struct LineLocations(AHashMap); + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.init_resource::(); + commands.init_resource::(); + let line_mesh = meshes.add(shape::Plane::from_size(1.0).into()); + let line_material = materials.add(LineMaterial {}); + commands.insert_resource(LineMesh(line_mesh, line_material)); +} + +fn cleanup(mut commands: Commands) { + commands.remove_resource::(); + commands.remove_resource::(); + commands.remove_resource::(); +} + +fn update_line_end( + mut events: EventReader, + lines: Res, + mut line_location: EventWriter, +) { + for event in &mut events { + if let Some(old_location) = lines.0.get(&event.owner) { + let location = LineLocation::new(old_location.start, event.end); + line_location.send(UpdateLineLocationEvent::new(event.owner, location)); + } + } +} + +fn update_line_location( + lines: Res, + mut events: EventReader, + mut transforms: Query<&mut Transform>, + mut line_locations: ResMut, +) { + for event in &mut events { + line_locations.0.insert(event.owner, event.location); + if let Some(line_entity) = lines.0.get(&event.owner) { + let mut current_transform = transforms.get_mut(*line_entity).unwrap(); + *current_transform = event.location.transform() + } + } +} + +fn update_line_visibility( + mut events: EventReader, + mut lines: ResMut, + line_locations: Res, + mut commands: Commands, + line_mesh: Res, +) { + for event in &mut events { + if event.visible && !lines.0.contains_key(&event.owner) { + let transform = line_locations + .0 + .get(&event.owner) + .map(|location| location.transform()); + let line_id = commands + .spawn(( + MaterialMeshBundle { + mesh: line_mesh.0.clone(), + material: line_mesh.1.clone(), + transform: transform.unwrap_or_default(), + ..default() + }, + DespawnOnGameExit, + )) + .id(); + lines.0.insert(event.owner, line_id); + } else if !event.visible { + if let Some(line_entity) = lines.0.remove(&event.owner) { + commands.entity(line_entity).despawn_recursive(); + } + } + } +} + +fn owner_despawn( + mut commands: Commands, + mut lines: ResMut, + mut removed: RemovedComponents, +) { + for owner in removed.iter() { + if let Some(line) = lines.0.remove(&owner) { + commands.entity(line).despawn_recursive(); + } + } +}