Skip to content

Commit

Permalink
Implement markers of selected movable entities
Browse files Browse the repository at this point in the history
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 DigitalExtinction#36.
  • Loading branch information
Indy2222 committed Oct 23, 2022
1 parent 640cd55 commit ab4c974
Show file tree
Hide file tree
Showing 8 changed files with 504 additions and 15 deletions.
7 changes: 5 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

128 changes: 126 additions & 2 deletions assets/shaders/terrain.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,26 @@

// How large (in meters) is a texture.
let TEXTURE_SIZE = 16.;
let SHAPE_COLOR = vec4<f32>(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<f32>,
radius: f32,
};

struct KdTree {
@align(16) nodes: array<KdTreeNode, MAX_KD_TREE_SIZE>,
count: u32,
};

@group(1) @binding(0)
var terrain_texture: texture_2d<f32>;
var<uniform> circles: KdTree;
@group(1) @binding(1)
var terrain_texture: texture_2d<f32>;
@group(1) @binding(2)
var terrain_sampler: sampler;

struct FragmentInput {
Expand All @@ -22,6 +38,112 @@ struct FragmentInput {
#import bevy_pbr::mesh_vertex_output
};

fn mix_colors(base: vec4<f32>, cover: vec4<f32>) -> vec4<f32> {
let alpha = base.a * cover.a;
let rgb = base.rgb * cover.a + cover.rgb * (1. - cover.a);
return vec4<f32>(rgb, alpha);
}

fn draw_circle(
base: vec4<f32>,
uv: vec2<f32>,
center: vec2<f32>,
radius: f32,
) -> vec4<f32> {
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<f32>) -> 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<Next, 12>;
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<f32>, uv: vec2<f32>) -> vec4<f32> {
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<f32> {
var pbr_input: PbrInput = pbr_input_new();
Expand Down Expand Up @@ -58,5 +180,7 @@ fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
);
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;
}
1 change: 1 addition & 0 deletions crates/controller/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
26 changes: 20 additions & 6 deletions crates/controller/src/selection.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Selected>>,
movable: Query<'w, 's, &'static ObjectType, With<MovableSolid>>,
}

impl<'w, 's> Selector<'w, 's> {
Expand All @@ -82,11 +87,20 @@ impl<'w, 's> Selector<'w, 's> {
};

for entity in deselect {
self.commands.entity(entity).remove::<Selected>();
let mut entity_commands = self.commands.entity(entity);
entity_commands.remove::<Selected>();
if self.movable.contains(entity) {
entity_commands.remove::<CircleMarker>();
}
}

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));
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/terrain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
5 changes: 4 additions & 1 deletion crates/terrain/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
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;

pub struct TerrainPluginGroup;

impl PluginGroup for TerrainPluginGroup {
fn build(&mut self, group: &mut PluginGroupBuilder) {
group.add(TerrainPlugin);
group.add(TerrainPlugin).add(MarkerPlugin);
}
}
115 changes: 115 additions & 0 deletions crates/terrain/src/marker.rs
Original file line number Diff line number Diff line change
@@ -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<Assets<TerrainMaterial>>,
cache: Res<ObjectCache>,
camera: Query<(&Transform, &Frustum), With<Camera3d>>,
terrains: Query<(&ComputedVisibility, &Handle<TerrainMaterial>)>,
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<Circle> = 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)
}
Loading

0 comments on commit ab4c974

Please sign in to comment.