update from toids
This commit is contained in:
parent
b5f89d0215
commit
3d7663e032
6 changed files with 3584 additions and 2134 deletions
4933
Cargo.lock
generated
4933
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
15
Cargo.toml
15
Cargo.toml
|
|
@ -1,13 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "audubon"
|
name = "toids"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
gizmos = []
|
debug = ["bevy/debug"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argh = "0.1"
|
argh = "0.1.12"
|
||||||
bevy = "0.15"
|
bevy = { version = "0.19", default-features = false, features = ["3d", "ui"] }
|
||||||
bevy_spatial = "0.10"
|
async-channel = "2"
|
||||||
rand = "0.8"
|
rand = { version = "0.10", features = ["thread_rng"] }
|
||||||
|
rstar = "0.13"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
# The Butlerian Jihad License (DUN)
|
|
||||||
|
|
||||||
If you feed this code into an LLM, I will fucking jihad you.
|
|
||||||
|
|
||||||
# The Chaos License (GLP)
|
# The Chaos License (GLP)
|
||||||
|
|
||||||
This software is released under the terms of the Chaos License. In cases where the terms of the
|
This software is released under the terms of the Chaos License. In cases where the terms of the
|
||||||
|
|
|
||||||
293
src/lib.rs
293
src/lib.rs
|
|
@ -1,34 +1,27 @@
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
use bevy::{
|
use async_channel::{Receiver, Sender};
|
||||||
prelude::{
|
use bevy::prelude::{Component, Deref, DerefMut, Entity, Resource, Vec, Vec3};
|
||||||
BuildChildren, ButtonInput, Camera, ChildBuild, Commands, Component, Deref, DerefMut,
|
use rstar::{primitives::GeomWithData, RTree};
|
||||||
Entity, Gizmos, Handle, KeyCode, Quat, Query, Res, ResMut, Resource, Scene, SceneRoot,
|
|
||||||
Time, Transform, Vec3, Visibility, With,
|
mod systems;
|
||||||
},
|
|
||||||
utils::{HashMap, HashSet},
|
pub use systems::{
|
||||||
|
init_naybs, rotate_camera, setup_channels, spawn_background, turkey_time, update_buddies,
|
||||||
|
update_gizmos, update_naybs, update_pos, update_vel,
|
||||||
};
|
};
|
||||||
use bevy_spatial::{kdtree::KDTree3, SpatialAccess};
|
|
||||||
|
|
||||||
pub type NNTree = KDTree3<Toid>;
|
type IndexElement = GeomWithData<[f32; 3], Entity>;
|
||||||
|
type Index = RTree<IndexElement>;
|
||||||
|
|
||||||
// toid stuff
|
type EntityPos = (Vec3, Entity);
|
||||||
const SPEED: f32 = 12.0;
|
|
||||||
const SPEED_DIFF_RANGE: f32 = 0.1; // +/- 10%
|
|
||||||
const MAX_DELTA_V: f32 = std::f32::consts::PI * 2.0; // basically 360 degrees/sec
|
|
||||||
|
|
||||||
// how far from origin before really wanting to come back
|
|
||||||
const RADIUS: f32 = 30.0;
|
|
||||||
|
|
||||||
// how close to try to stay to your buddies
|
|
||||||
const BUDDY_RADIUS: f32 = SPEED;
|
|
||||||
|
|
||||||
const MIN_ALTITUDE: f32 = 13.5;
|
|
||||||
|
|
||||||
#[derive(Debug, FromArgs, Resource)]
|
#[derive(Debug, FromArgs, Resource)]
|
||||||
/// Toid Watching
|
/// Toid Watching
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// how many Toids to spawn
|
/// how many Toids to spawn
|
||||||
#[argh(option, short = 't', default = "1_000")]
|
#[argh(option, short = 't', default = "5_000")]
|
||||||
pub toids: usize,
|
pub toids: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,239 +34,39 @@ pub struct Buddies(HashSet<Entity>);
|
||||||
#[derive(Component, Debug, Clone, Deref, DerefMut, Default)]
|
#[derive(Component, Debug, Clone, Deref, DerefMut, Default)]
|
||||||
pub struct Velocity(Vec3);
|
pub struct Velocity(Vec3);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Resource)]
|
||||||
|
pub struct Naybs {
|
||||||
|
pub index: Index,
|
||||||
|
pub positions: Option<Vec<EntityPos>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct RawPosChannel {
|
||||||
|
tx: Sender<RawMessage>,
|
||||||
|
rx: Option<Receiver<RawMessage>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct IndexChannel {
|
||||||
|
tx: Sender<IndexMessage>,
|
||||||
|
rx: Receiver<IndexMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IndexMessage {
|
||||||
|
raw: Vec<EntityPos>,
|
||||||
|
index: Index,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawMessage(Vec<EntityPos>);
|
||||||
|
|
||||||
|
#[derive(Default, Resource, Deref, DerefMut)]
|
||||||
|
pub struct BackgroundHandle(Option<bevy::tasks::Task<()>>);
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Toid {
|
pub struct Toid {
|
||||||
pub speed: f32,
|
pub speed: f32,
|
||||||
pub buddies: usize,
|
pub buddies: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct Gizmoid;
|
|
||||||
|
|
||||||
#[derive(Resource, Debug, Deref, DerefMut, Clone, Default)]
|
|
||||||
pub struct Paused(bool);
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Deref, DerefMut, Resource)]
|
#[derive(Debug, Default, Clone, Copy, Deref, DerefMut, Resource)]
|
||||||
pub struct LookAt(Vec3);
|
pub struct LookAt(Vec3);
|
||||||
|
|
||||||
pub fn turkey_time(
|
|
||||||
commands: &mut Commands,
|
|
||||||
scene: &Handle<Scene>,
|
|
||||||
r: &mut impl rand::prelude::Rng,
|
|
||||||
) -> Entity {
|
|
||||||
let speed_diff = r.gen_range(-SPEED_DIFF_RANGE..=SPEED_DIFF_RANGE);
|
|
||||||
let speed = SPEED + (SPEED * speed_diff);
|
|
||||||
let dir = unit_vec(r) * speed;
|
|
||||||
let buddies = r.gen_range(6..=8);
|
|
||||||
let x = r.gen_range(-10.0..=10.0);
|
|
||||||
let z = r.gen_range(-10.0..=10.0);
|
|
||||||
let y = r.gen_range(0.1..=5.5);
|
|
||||||
let pos = Vec3::new(x, MIN_ALTITUDE + y, z);
|
|
||||||
let xform = Transform::from_translation(pos);
|
|
||||||
let spatial_bundle = (xform, Visibility::Visible);
|
|
||||||
|
|
||||||
let boid_rot = Quat::from_axis_angle(Vec3::Y, -90.0f32.to_radians());
|
|
||||||
|
|
||||||
commands
|
|
||||||
.spawn(spatial_bundle)
|
|
||||||
.insert((
|
|
||||||
Velocity(dir),
|
|
||||||
Buddies::default(),
|
|
||||||
Toid { speed, buddies },
|
|
||||||
Gizmoid,
|
|
||||||
))
|
|
||||||
.with_children(|t| {
|
|
||||||
t.spawn(SceneRoot(scene.to_owned()))
|
|
||||||
.insert(Transform::from_rotation(boid_rot));
|
|
||||||
})
|
|
||||||
.id()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_vel(
|
|
||||||
mut toids: Query<(&Transform, &mut Velocity, &Toid, &Buddies, Entity)>,
|
|
||||||
time: Res<Time>,
|
|
||||||
positions: Res<Positions>,
|
|
||||||
index: Res<NNTree>,
|
|
||||||
) {
|
|
||||||
let dt = time.delta_secs();
|
|
||||||
let max_delta = MAX_DELTA_V * dt;
|
|
||||||
toids
|
|
||||||
.par_iter_mut()
|
|
||||||
.for_each(|(xform, mut vel, toid, buddies, entity)| {
|
|
||||||
let speed = toid.speed;
|
|
||||||
let mut dir = vel.normalize();
|
|
||||||
let pos = xform.translation;
|
|
||||||
let original_dir = dir;
|
|
||||||
|
|
||||||
// find buddies and orient; point more towards further-away buddies
|
|
||||||
for buddy in buddies.iter() {
|
|
||||||
let bp = positions.get(buddy).unwrap();
|
|
||||||
let bdir = *bp - pos;
|
|
||||||
let dist = bdir.length();
|
|
||||||
let rot = Quat::from_rotation_arc(dir, bdir.normalize());
|
|
||||||
let s = (dist / (BUDDY_RADIUS)).min(1.0);
|
|
||||||
let rot = Quat::IDENTITY.slerp(rot, s);
|
|
||||||
dir = rot.mul_vec3(dir).normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// avoid flying into neighbors
|
|
||||||
let min_dist = speed * 20.0;
|
|
||||||
for neighbor in index
|
|
||||||
.within_distance(pos, min_dist)
|
|
||||||
.iter()
|
|
||||||
.filter(|n| n.1.is_some() && n.1.unwrap() != entity)
|
|
||||||
{
|
|
||||||
let bp = neighbor.0;
|
|
||||||
let bdir = pos - bp;
|
|
||||||
let dist = bdir.length();
|
|
||||||
let s = 1.0 - (dist / min_dist).min(1.0);
|
|
||||||
let rot = Quat::from_rotation_arc(dir, bdir.normalize());
|
|
||||||
let rot = Quat::IDENTITY.slerp(rot, s);
|
|
||||||
dir = rot.mul_vec3(dir).normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// nudge toward origin if too far
|
|
||||||
{
|
|
||||||
let dist = pos.length();
|
|
||||||
let toward_origin = -pos.normalize();
|
|
||||||
let s = (dist / RADIUS).min(1.0);
|
|
||||||
let rot = Quat::from_rotation_arc(dir, toward_origin);
|
|
||||||
let rot = Quat::IDENTITY.slerp(rot, s);
|
|
||||||
dir = rot.mul_vec3(dir).normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// nudge up if too low
|
|
||||||
if pos.y < MIN_ALTITUDE {
|
|
||||||
let dh = MIN_ALTITUDE - pos.y;
|
|
||||||
let s = (dh / MIN_ALTITUDE).min(1.0);
|
|
||||||
let rot = Quat::from_rotation_arc(dir, Vec3::Y);
|
|
||||||
let rot = Quat::IDENTITY.slerp(rot, s);
|
|
||||||
dir = rot.mul_vec3(dir).normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure velocity doesn't change too suddenly
|
|
||||||
let delta = dir.dot(original_dir).acos();
|
|
||||||
if delta > max_delta {
|
|
||||||
let s = max_delta / delta;
|
|
||||||
let rot = Quat::from_rotation_arc(original_dir, dir);
|
|
||||||
let rot = Quat::IDENTITY.slerp(rot, s);
|
|
||||||
dir = rot.mul_vec3(original_dir).normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
**vel = dir * speed;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_pos(
|
|
||||||
mut toids: Query<(&mut Transform, &Velocity, Entity), With<Toid>>,
|
|
||||||
mut positions: ResMut<Positions>,
|
|
||||||
mut lookat: ResMut<LookAt>,
|
|
||||||
time: Res<Time>,
|
|
||||||
) {
|
|
||||||
let mut new_look = Vec3::ZERO;
|
|
||||||
let dt = time.delta_secs();
|
|
||||||
for (mut xform, vel, entity) in toids.iter_mut() {
|
|
||||||
xform.translation += **vel * dt;
|
|
||||||
let look_at = vel.normalize();
|
|
||||||
xform.look_to(look_at, Vec3::Y);
|
|
||||||
*positions.entry(entity).or_insert(Vec3::ZERO) = xform.translation;
|
|
||||||
new_look += xform.translation;
|
|
||||||
}
|
|
||||||
**lookat = new_look / positions.len() as f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_buddies(
|
|
||||||
mut toids: Query<(&Transform, Entity, &Toid, &mut Buddies)>,
|
|
||||||
index: Res<NNTree>,
|
|
||||||
positions: Res<Positions>,
|
|
||||||
) {
|
|
||||||
let d2 = (BUDDY_RADIUS * 1.5).powi(2);
|
|
||||||
for (xform, entity, toid, mut buddies) in toids.iter_mut() {
|
|
||||||
let pos = xform.translation;
|
|
||||||
for buddy in buddies.clone().iter() {
|
|
||||||
let bp = positions.get(buddy).unwrap();
|
|
||||||
let bd2 = (*bp - pos).length_squared();
|
|
||||||
if bd2 > d2 {
|
|
||||||
buddies.remove(buddy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if buddies.len() < toid.buddies {
|
|
||||||
let diff = toid.buddies - buddies.len();
|
|
||||||
for (_, neighbor) in index
|
|
||||||
.k_nearest_neighbour(pos, diff + 1)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|n| n.1.is_some() && n.1.unwrap() != entity)
|
|
||||||
{
|
|
||||||
buddies.insert(neighbor.unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_gizmos(toids: Query<&Transform, With<Gizmoid>>, mut gizmos: Gizmos) {
|
|
||||||
for &toid in &toids {
|
|
||||||
gizmos.axes(toid, 0.7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rotate_camera(
|
|
||||||
mut query: Query<&mut Transform, With<Camera>>,
|
|
||||||
mut paused: ResMut<Paused>,
|
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
|
||||||
lookat: Res<LookAt>,
|
|
||||||
) {
|
|
||||||
let mut xform = query.single_mut();
|
|
||||||
|
|
||||||
let forward = xform.forward() * 0.7;
|
|
||||||
let right = xform.right().as_vec3();
|
|
||||||
|
|
||||||
if keys.just_pressed(KeyCode::Space) {
|
|
||||||
let pause = **paused;
|
|
||||||
**paused = !pause;
|
|
||||||
}
|
|
||||||
|
|
||||||
if keys.pressed(KeyCode::ArrowRight) {
|
|
||||||
xform.translation += right;
|
|
||||||
}
|
|
||||||
if keys.pressed(KeyCode::ArrowLeft) {
|
|
||||||
xform.translation -= right;
|
|
||||||
}
|
|
||||||
|
|
||||||
if keys.pressed(KeyCode::ArrowUp) {
|
|
||||||
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
|
|
||||||
xform.translation += Vec3::Y;
|
|
||||||
} else {
|
|
||||||
xform.translation += forward;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if keys.pressed(KeyCode::ArrowDown) {
|
|
||||||
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
|
|
||||||
xform.translation -= Vec3::Y;
|
|
||||||
} else {
|
|
||||||
xform.translation -= forward;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xform.look_at(**lookat, Vec3::Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
//-************************************************************************
|
|
||||||
// util
|
|
||||||
//-************************************************************************
|
|
||||||
|
|
||||||
pub fn unit_vec(r: &mut impl rand::Rng) -> Vec3 {
|
|
||||||
let mut x1: f32 = 0.0;
|
|
||||||
let mut x2: f32 = 0.0;
|
|
||||||
let mut ssum = f32::MAX;
|
|
||||||
while ssum >= 1.0 {
|
|
||||||
x1 = r.gen_range(-1.0..=1.0);
|
|
||||||
x2 = r.gen_range(-1.0..=1.0);
|
|
||||||
ssum = x1.powi(2) + x2.powi(2);
|
|
||||||
}
|
|
||||||
let sqrt = (1.0 - ssum).sqrt();
|
|
||||||
let x = 2.0 * x1 * sqrt;
|
|
||||||
let y = 2.0 * x2 * sqrt;
|
|
||||||
let z = 1.0 - 2.0 * ssum;
|
|
||||||
Vec3::new(x, y, z).normalize()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
134
src/main.rs
134
src/main.rs
|
|
@ -1,46 +1,52 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use audubon::*;
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
||||||
prelude::*,
|
input::keyboard::Key,
|
||||||
|
prelude::{
|
||||||
|
children, default, px, App, AssetServer, Assets, ButtonInput, Camera3d, ClearColor, Color,
|
||||||
|
Commands, DefaultPlugins, Entity, FontSize, GlobalAmbientLight, IntoScheduleConfigs, Mesh,
|
||||||
|
Mesh3d, MeshMaterial3d, Node, Plane3d, PositionType, Query, Res, ResMut, StandardMaterial,
|
||||||
|
Startup, Text, TextFont, Transform, Update, Vec2, Vec3, Visibility, Window,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use rstar::RTree;
|
||||||
|
use toids::{
|
||||||
|
init_naybs, rotate_camera, setup_channels, spawn_background, turkey_time, update_buddies,
|
||||||
|
update_naybs, update_pos, update_vel, BackgroundHandle, Config, LookAt, Naybs, Positions,
|
||||||
};
|
};
|
||||||
use bevy_spatial::{AutomaticUpdate, TransformMode};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let config: audubon::Config = argh::from_env();
|
let config: toids::Config = argh::from_env();
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
.add_plugins(DefaultPlugins)
|
||||||
primary_window: Some(Window {
|
|
||||||
resolution: (1920.0, 1080.0).into(),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
close_when_requested: true,
|
|
||||||
..Default::default()
|
|
||||||
}))
|
|
||||||
.add_plugins((
|
.add_plugins((
|
||||||
AutomaticUpdate::<Toid>::new()
|
FrameTimeDiagnosticsPlugin::default(),
|
||||||
.with_transform(TransformMode::GlobalTransform)
|
|
||||||
.with_frequency(Duration::from_millis(150)),
|
|
||||||
FrameTimeDiagnosticsPlugin,
|
|
||||||
LogDiagnosticsPlugin::default(),
|
LogDiagnosticsPlugin::default(),
|
||||||
))
|
))
|
||||||
.insert_resource(config)
|
.insert_resource(config)
|
||||||
.insert_resource(Positions::default())
|
.insert_resource(Positions::default())
|
||||||
.insert_resource(ClearColor(Color::srgb(0.64, 0.745, 0.937))) // a nice light blue
|
.insert_resource(ClearColor(Color::srgb(0.64, 0.745, 0.937))) // a nice light blue
|
||||||
.insert_resource(AmbientLight {
|
.insert_resource(GlobalAmbientLight {
|
||||||
color: Color::WHITE,
|
color: Color::WHITE,
|
||||||
brightness: 1.0,
|
brightness: 150.0,
|
||||||
|
affects_lightmapped_meshes: true,
|
||||||
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
.insert_resource(Naybs {
|
||||||
|
index: RTree::new(),
|
||||||
|
positions: None,
|
||||||
|
})
|
||||||
|
.insert_resource(BackgroundHandle::default())
|
||||||
.insert_resource(LookAt::default())
|
.insert_resource(LookAt::default())
|
||||||
.insert_resource(Paused::default())
|
.add_systems(
|
||||||
.add_systems(Startup, (setup, setup_ambient_light))
|
Startup,
|
||||||
|
(setup_channels, setup, init_naybs, spawn_background).chain(),
|
||||||
|
)
|
||||||
|
.add_systems(Update, (update_pos, update_buddies, update_vel).chain())
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(update_pos, update_buddies, update_vel).run_if(not_paused),
|
(rotate_camera, close_on_esc, update_naybs.after(update_pos)),
|
||||||
)
|
)
|
||||||
.add_systems(Update, (rotate_camera, close_on_esc))
|
//.add_systems(Update, update_gizmos)
|
||||||
.add_systems(Update, update_gizmos.run_if(do_gizmos))
|
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,85 +54,63 @@ fn setup(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
mut positions: ResMut<Positions>,
|
||||||
config: Res<Config>,
|
config: Res<Config>,
|
||||||
models: Res<AssetServer>,
|
models: Res<AssetServer>,
|
||||||
) {
|
) {
|
||||||
let camera = commands
|
let rand = &mut rand::rng();
|
||||||
.spawn((
|
|
||||||
|
commands.spawn((
|
||||||
Transform::from_xyz(0., 5.0, 25.).looking_at(Vec3::new(0.0, 5.0, 0.0), Vec3::Y),
|
Transform::from_xyz(0., 5.0, 25.).looking_at(Vec3::new(0.0, 5.0, 0.0), Vec3::Y),
|
||||||
Camera3d::default(),
|
Camera3d::default(),
|
||||||
Visibility::Hidden,
|
));
|
||||||
))
|
// plane
|
||||||
.id();
|
|
||||||
// ground plane
|
|
||||||
commands
|
|
||||||
.spawn(Mesh3d(
|
|
||||||
meshes.add(Mesh::from(Plane3d::default().mesh().size(500.0, 500.0))),
|
|
||||||
))
|
|
||||||
.insert(MeshMaterial3d(materials.add(Color::srgb(0.3, 1.0, 0.3))));
|
|
||||||
|
|
||||||
// point light
|
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Transform::from_xyz(0.0, 50.0, 0.0),
|
Mesh3d(meshes.add(Mesh::from(Plane3d::new(Vec3::Y, Vec2::splat(500.0))))),
|
||||||
PointLight {
|
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.9, 0.3))),
|
||||||
intensity: 8_000_000.0,
|
Visibility::Visible,
|
||||||
range: 100.0,
|
Transform::default(),
|
||||||
shadows_enabled: true,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
|
|
||||||
let toid_model = models.load("models/boid.glb#Scene0");
|
// let toid_model = models.load("models/boid.glb#Scene0");
|
||||||
let rand = &mut rand::thread_rng();
|
|
||||||
for _ in 0..config.toids {
|
for _ in 0..config.toids {
|
||||||
let _ = turkey_time(&mut commands, &toid_model, rand);
|
turkey_time(&mut commands, rand, &models, &mut positions.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// instructions
|
// instructions
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Text::new(
|
Visibility::Visible,
|
||||||
"Up and down for camera forward and back; hold shift to change height.\nLeft or right to move left or right.\nPress 'ESC' to quit; SPACE to pause."),
|
|
||||||
TextFont {
|
|
||||||
font_size: 20.0,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Node {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
top: Val::Px(12.0),
|
top: px(12.0),
|
||||||
left: Val::Px(12.0),
|
left: px(12.0),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TargetCamera(camera),
|
children![(
|
||||||
TextColor::WHITE));
|
Text::new("Up and down for camera forward and back; hold shift to change height.\nLeft or right to move left or right.\nPress 'ESC' to quit."),
|
||||||
|
TextFont {
|
||||||
|
font_size: FontSize::Px(20.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
)],
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close_on_esc(
|
fn close_on_esc(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
focused_windows: Query<(Entity, &Window)>,
|
focused_windows: Query<(Entity, &Window)>,
|
||||||
input: Res<ButtonInput<KeyCode>>,
|
input: Res<ButtonInput<Key>>,
|
||||||
|
mut handle: ResMut<BackgroundHandle>,
|
||||||
) {
|
) {
|
||||||
for (window, focus) in focused_windows.iter() {
|
for (window, focus) in focused_windows.iter() {
|
||||||
if !focus.focused {
|
if !focus.focused {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.just_pressed(KeyCode::Escape) {
|
if input.just_pressed(Key::Escape) {
|
||||||
|
drop((*handle).take());
|
||||||
commands.entity(window).despawn();
|
commands.entity(window).despawn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_ambient_light(mut ambient_light: ResMut<AmbientLight>) {
|
|
||||||
ambient_light.brightness = 100.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn not_paused(paused: Option<Res<Paused>>) -> bool {
|
|
||||||
!paused.map(|p| **p).unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_gizmos() -> bool {
|
|
||||||
#[cfg(feature = "gizmos")]
|
|
||||||
return true;
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
|
||||||
335
src/systems.rs
Normal file
335
src/systems.rs
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use async_channel::bounded;
|
||||||
|
use bevy::{input::keyboard::Key, prelude::*, tasks::AsyncComputeTaskPool};
|
||||||
|
use rand::RngExt;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
BackgroundHandle, Buddies, GeomWithData, IndexChannel, IndexMessage, LookAt, Naybs, Positions,
|
||||||
|
RTree, RawMessage, RawPosChannel, Toid, Velocity,
|
||||||
|
};
|
||||||
|
|
||||||
|
// toid stuff
|
||||||
|
const SPEED: f32 = 6.0;
|
||||||
|
const SPEED_DIFF_RANGE: f32 = 0.2; // +/- 10%
|
||||||
|
const MAX_DELTA_V: f32 = std::f32::consts::PI * 1.5; // basically 360
|
||||||
|
// degrees/sec
|
||||||
|
|
||||||
|
// how far from origin before really wanting to come back
|
||||||
|
const RADIUS: f32 = 40.0;
|
||||||
|
|
||||||
|
// how close to try to stay to your buddies
|
||||||
|
const BUDDY_RADIUS: f32 = 10.0;
|
||||||
|
|
||||||
|
const MIN_ALTITUDE: f32 = 3.5;
|
||||||
|
|
||||||
|
pub fn setup_channels(mut commands: Commands) {
|
||||||
|
let (raw_tx, raw_rx) = bounded(1);
|
||||||
|
commands.insert_resource(RawPosChannel {
|
||||||
|
tx: raw_tx,
|
||||||
|
rx: Some(raw_rx),
|
||||||
|
});
|
||||||
|
|
||||||
|
let (idx_tx, idx_rx) = bounded(1);
|
||||||
|
commands.insert_resource(IndexChannel {
|
||||||
|
tx: idx_tx,
|
||||||
|
rx: idx_rx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_naybs(
|
||||||
|
mut naybs: ResMut<Naybs>,
|
||||||
|
raw_chan: Res<RawPosChannel>,
|
||||||
|
positions: Res<Positions>,
|
||||||
|
idx_chan: Res<IndexChannel>,
|
||||||
|
) {
|
||||||
|
if let Ok(idx_msg) = idx_chan.rx.try_recv() {
|
||||||
|
naybs.index = idx_msg.index;
|
||||||
|
let _ = naybs.positions.insert(idx_msg.raw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mut raw) = naybs.positions.take() {
|
||||||
|
for (i, (entity, pos)) in positions.iter().enumerate() {
|
||||||
|
raw[i] = (*pos, *entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = raw_chan.tx.try_send(RawMessage(raw)) {
|
||||||
|
let RawMessage(raw) = e.into_inner();
|
||||||
|
let _ = naybs.positions.insert(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_naybs(positions: Res<Positions>, mut naybs: ResMut<Naybs>) {
|
||||||
|
let mut raw = Vec::with_capacity(positions.len());
|
||||||
|
for (entity, pos) in positions.iter() {
|
||||||
|
raw.push((*pos, *entity));
|
||||||
|
}
|
||||||
|
naybs.positions = Some(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_background(
|
||||||
|
mut raw_chan: ResMut<RawPosChannel>,
|
||||||
|
idx_chan: Res<IndexChannel>,
|
||||||
|
mut handle: ResMut<BackgroundHandle>,
|
||||||
|
) {
|
||||||
|
let pool = AsyncComputeTaskPool::get();
|
||||||
|
|
||||||
|
let raw_rx = raw_chan.rx.take().unwrap();
|
||||||
|
let idx_tx = idx_chan.tx.clone();
|
||||||
|
|
||||||
|
let task = pool.spawn(async move {
|
||||||
|
let tx = idx_tx;
|
||||||
|
let rx = raw_rx;
|
||||||
|
loop {
|
||||||
|
match rx.recv().await {
|
||||||
|
Ok(RawMessage(e_posns)) => {
|
||||||
|
let idx = RTree::bulk_load(
|
||||||
|
e_posns
|
||||||
|
.iter()
|
||||||
|
.map(|(p, e)| GeomWithData::new(p.to_array(), *e))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
let msg = IndexMessage {
|
||||||
|
raw: e_posns,
|
||||||
|
index: idx,
|
||||||
|
};
|
||||||
|
if tx.send(msg).await.is_err() {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = handle.0.insert(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn turkey_time(
|
||||||
|
commands: &mut Commands,
|
||||||
|
r: &mut impl rand::prelude::Rng,
|
||||||
|
assets: &AssetServer,
|
||||||
|
positions: &mut HashMap<Entity, Vec3>,
|
||||||
|
) {
|
||||||
|
let speed_diff = r.random_range(-SPEED_DIFF_RANGE..=SPEED_DIFF_RANGE);
|
||||||
|
let speed = SPEED + (SPEED * speed_diff);
|
||||||
|
let vel = unit_vec(r) * speed;
|
||||||
|
let buddies = r.random_range(6..=8);
|
||||||
|
let x = r.random_range(-10.0..=10.0);
|
||||||
|
let z = r.random_range(-10.0..=10.0);
|
||||||
|
let y = r.random_range(MIN_ALTITUDE..=(MIN_ALTITUDE + 5.5));
|
||||||
|
|
||||||
|
let entity = commands
|
||||||
|
.spawn((
|
||||||
|
Velocity(vel),
|
||||||
|
Buddies::default(),
|
||||||
|
Toid { speed, buddies },
|
||||||
|
Transform::from_xyz(x, y, z),
|
||||||
|
Visibility::Visible,
|
||||||
|
))
|
||||||
|
.with_children(|t| {
|
||||||
|
t.spawn(WorldAssetRoot(
|
||||||
|
assets.load(GltfAssetLabel::Scene(0).from_asset("models/boid.glb")),
|
||||||
|
))
|
||||||
|
.insert(Transform::default())
|
||||||
|
//.insert(Visibility::Visible);
|
||||||
|
.insert(Transform::from_rotation(Quat::from_axis_angle(
|
||||||
|
Vec3::Y,
|
||||||
|
-std::f32::consts::FRAC_PI_2,
|
||||||
|
)));
|
||||||
|
})
|
||||||
|
.id();
|
||||||
|
|
||||||
|
positions.insert(entity, Vec3::new(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_vel(
|
||||||
|
mut toids: Query<(&Transform, &mut Velocity, &Toid, &Buddies, Entity)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
positions: Res<Positions>,
|
||||||
|
index: Res<Naybs>,
|
||||||
|
) {
|
||||||
|
let dt = time.delta_secs();
|
||||||
|
let max_delta = MAX_DELTA_V * dt;
|
||||||
|
for (xform, mut vel, toid, buddies, entity) in toids.iter_mut() {
|
||||||
|
let speed = toid.speed;
|
||||||
|
let mut dir = vel.normalize();
|
||||||
|
let pos = xform.translation;
|
||||||
|
let original_dir = dir;
|
||||||
|
|
||||||
|
// find buddies and orient; point more towards further-away buddies
|
||||||
|
for buddy in buddies.0.iter() {
|
||||||
|
let bp = positions.get(buddy).unwrap();
|
||||||
|
let bdir = *bp - pos;
|
||||||
|
let dist = bdir.length();
|
||||||
|
let rot = Quat::from_rotation_arc(dir, bdir.normalize());
|
||||||
|
let s = (dist / (BUDDY_RADIUS * 1.2)).min(1.0);
|
||||||
|
let rot = Quat::IDENTITY.slerp(rot, s);
|
||||||
|
dir = rot.mul_vec3(dir).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid flying into neighbors
|
||||||
|
let min_dist = speed;
|
||||||
|
for neighbor in index
|
||||||
|
.index
|
||||||
|
.locate_within_distance(pos.to_array(), min_dist)
|
||||||
|
.filter(|n| n.data != entity)
|
||||||
|
{
|
||||||
|
let bp = Vec3::from_array(*neighbor.geom());
|
||||||
|
let bdir = pos - bp;
|
||||||
|
let dist = bdir.length();
|
||||||
|
let s = 1.0 - (dist / min_dist).min(1.0);
|
||||||
|
let rot = Quat::from_rotation_arc(dir, bdir.normalize());
|
||||||
|
let rot = Quat::IDENTITY.slerp(rot, s);
|
||||||
|
dir = rot.mul_vec3(dir).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// nudge toward origin if too far
|
||||||
|
{
|
||||||
|
let dist = pos.length();
|
||||||
|
let toward_origin = -pos.normalize();
|
||||||
|
let s = (dist / RADIUS).min(1.0);
|
||||||
|
let rot = Quat::from_rotation_arc(dir, toward_origin);
|
||||||
|
let rot = Quat::IDENTITY.slerp(rot, s);
|
||||||
|
dir = rot.mul_vec3(dir).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// nudge up if too low
|
||||||
|
if pos.y < MIN_ALTITUDE {
|
||||||
|
let dh = MIN_ALTITUDE - pos.y;
|
||||||
|
let s = (dh / MIN_ALTITUDE).min(1.0);
|
||||||
|
let rot = Quat::from_rotation_arc(dir, Vec3::Y);
|
||||||
|
let rot = Quat::IDENTITY.slerp(rot, s);
|
||||||
|
dir = rot.mul_vec3(dir).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure velocity doesn't change too suddenly
|
||||||
|
let delta = dir.dot(original_dir).acos();
|
||||||
|
if delta > max_delta {
|
||||||
|
let s = max_delta / delta;
|
||||||
|
let rot = Quat::from_rotation_arc(original_dir, dir);
|
||||||
|
let rot = Quat::IDENTITY.slerp(rot, s);
|
||||||
|
dir = rot.mul_vec3(original_dir).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
**vel = dir * speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_pos(
|
||||||
|
mut toids: Query<(&mut Transform, &Velocity, Entity), With<Toid>>,
|
||||||
|
mut positions: ResMut<Positions>,
|
||||||
|
mut lookat: ResMut<LookAt>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
let mut new_look = Vec3::ZERO;
|
||||||
|
let dt = time.delta_secs();
|
||||||
|
for (mut xform, vel, entity) in toids.iter_mut() {
|
||||||
|
let look_at = xform.translation + vel.0;
|
||||||
|
xform.translation += vel.0 * dt;
|
||||||
|
xform.look_at(look_at, Vec3::Y);
|
||||||
|
*positions.entry(entity).or_insert(Vec3::ZERO) = xform.translation;
|
||||||
|
new_look += xform.translation;
|
||||||
|
}
|
||||||
|
**lookat = new_look / positions.len() as f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_buddies(
|
||||||
|
mut toids: Query<(&Transform, Entity, &Toid, &mut Buddies)>,
|
||||||
|
index: Res<Naybs>,
|
||||||
|
positions: Res<Positions>,
|
||||||
|
) {
|
||||||
|
let d2 = (BUDDY_RADIUS * 1.5).powi(2);
|
||||||
|
for (xform, entity, toid, mut buddies) in toids.iter_mut() {
|
||||||
|
let pos = xform.translation;
|
||||||
|
for buddy in buddies.clone().iter() {
|
||||||
|
let bp = positions[buddy];
|
||||||
|
let bd2 = (bp - pos).length_squared();
|
||||||
|
if bd2 > d2 {
|
||||||
|
buddies.remove(buddy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if buddies.len() < toid.buddies {
|
||||||
|
let diff = toid.buddies - buddies.len();
|
||||||
|
for neighbor in index
|
||||||
|
.index
|
||||||
|
.nearest_neighbor_iter(pos.to_array())
|
||||||
|
.filter(|n| n.data != entity)
|
||||||
|
.take(diff)
|
||||||
|
{
|
||||||
|
buddies.insert(neighbor.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_gizmos(toids: Query<&Transform, With<Toid>>, mut gizmos: Gizmos) {
|
||||||
|
for toid in toids.iter() {
|
||||||
|
let pos = toid.translation;
|
||||||
|
let up = toid.up().as_vec3();
|
||||||
|
let forward = toid.forward().as_vec3();
|
||||||
|
let right = toid.right().as_vec3();
|
||||||
|
gizmos.ray(pos, pos + forward, Color::srgb(1., 0., 0.));
|
||||||
|
gizmos.ray(pos, pos + up, Color::srgb(0., 1., 0.));
|
||||||
|
gizmos.ray(pos, pos + right, Color::srgb(0., 0., 1.));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate_camera(
|
||||||
|
mut query: Query<&mut Transform, With<Camera>>,
|
||||||
|
keys: Res<ButtonInput<Key>>,
|
||||||
|
lookat: Res<LookAt>,
|
||||||
|
) {
|
||||||
|
let mut xform = query.single_mut().unwrap();
|
||||||
|
|
||||||
|
let forward = xform.forward().as_vec3() * 0.7;
|
||||||
|
let right = xform.right().as_vec3();
|
||||||
|
|
||||||
|
if keys.pressed(Key::ArrowRight) {
|
||||||
|
xform.translation += right;
|
||||||
|
}
|
||||||
|
if keys.pressed(Key::ArrowLeft) {
|
||||||
|
xform.translation -= right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.pressed(Key::ArrowUp) {
|
||||||
|
if keys.pressed(Key::Shift) {
|
||||||
|
xform.translation += Vec3::Y;
|
||||||
|
} else {
|
||||||
|
xform.translation += forward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.pressed(Key::ArrowDown) {
|
||||||
|
if keys.pressed(Key::Shift) {
|
||||||
|
xform.translation -= Vec3::Y;
|
||||||
|
} else {
|
||||||
|
xform.translation -= forward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xform.look_at(**lookat, Vec3::Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-************************************************************************
|
||||||
|
// util
|
||||||
|
//-************************************************************************
|
||||||
|
|
||||||
|
pub fn unit_vec(r: &mut impl rand::Rng) -> Vec3 {
|
||||||
|
let mut x1: f32 = 0.0;
|
||||||
|
let mut x2: f32 = 0.0;
|
||||||
|
let mut ssum = f32::MAX;
|
||||||
|
while ssum >= 1.0 {
|
||||||
|
x1 = r.random_range(-1.0..=1.0);
|
||||||
|
x2 = r.random_range(-1.0..=1.0);
|
||||||
|
ssum = x1.powi(2) + x2.powi(2);
|
||||||
|
}
|
||||||
|
let sqrt = (1.0 - ssum).sqrt();
|
||||||
|
let x = 2.0 * x1 * sqrt;
|
||||||
|
let y = 2.0 * x2 * sqrt;
|
||||||
|
let z = 1.0 - 2.0 * ssum;
|
||||||
|
Vec3::new(x, y, z).normalize()
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue