update from toids

This commit is contained in:
Joe Ardent 2026-09-26 12:25:47 -07:00
parent b5f89d0215
commit 3d7663e032
6 changed files with 3584 additions and 2134 deletions

4933
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -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

View file

@ -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()
}

View file

@ -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((
Transform::from_xyz(0., 5.0, 25.).looking_at(Vec3::new(0.0, 5.0, 0.0), Vec3::Y),
Camera3d::default(),
Visibility::Hidden,
))
.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), Transform::from_xyz(0., 5.0, 25.).looking_at(Vec3::new(0.0, 5.0, 0.0), Vec3::Y),
PointLight { Camera3d::default(),
intensity: 8_000_000.0, ));
range: 100.0, // plane
shadows_enabled: true, commands.spawn((
..default() Mesh3d(meshes.add(Mesh::from(Plane3d::new(Vec3::Y, Vec2::splat(500.0))))),
}, MeshMaterial3d(materials.add(Color::srgb(0.3, 0.9, 0.3))),
Visibility::Visible,
Transform::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
View 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()
}