#![allow(dead_code)]
mod ground;
use std::time::Duration;
use ground::GroundPlane;
use bevy::input::mouse::AccumulatedMouseMotion;
use bevy::prelude::*;
use bevy::render::view::window::screenshot::{Screenshot, ScreenshotCaptured, save_to_disk};
use bevy::time::TimeUpdateStrategy;
use bevy_elements::*;
pub struct ExampleHarness;
pub fn units() -> f32 {
std::env::var("ELEMENTS_UNITS")
.ok()
.and_then(|v| v.parse::<f32>().ok())
.filter(|u| u.is_finite() && *u > 0.0)
.unwrap_or(1.0)
}
pub fn vec3_env(name: &str) -> Option<Vec3> {
let v: Vec<f32> = std::env::var(name)
.ok()?
.split(',')
.filter_map(|p| p.trim().parse().ok())
.collect();
(v.len() == 3).then(|| Vec3::new(v[0], v[1], v[2]))
}
pub fn terrain_look_env() -> Option<TerrainLook> {
let tint = vec3_env("ELEMENTS_TINT");
let saturation: Option<f32> = std::env::var("ELEMENTS_SATURATION")
.ok()
.and_then(|s| s.parse().ok());
(tint.is_some() || saturation.is_some()).then(|| TerrainLook {
tint: tint.unwrap_or(Vec3::ONE),
saturation: saturation.unwrap_or(1.0),
})
}
#[derive(Component, Default)]
pub struct FlyCamera {
yaw: f32,
pitch: f32,
}
impl FlyCamera {
pub fn looking(yaw: f32, pitch: f32) -> Self {
Self { yaw, pitch }
}
pub fn face(&mut self, yaw: f32, pitch: f32) {
self.yaw = yaw;
self.pitch = pitch.clamp(-1.54, 1.54);
}
}
#[derive(Resource)]
struct RunConfig {
frames: Option<u32>,
shot: Option<String>,
shot_dir: Option<String>,
shot_every: u32,
shot_from: u32,
frame: u32,
shot_taken: bool,
grow_hours: f64,
pending: std::sync::Arc<std::sync::atomic::AtomicU32>,
}
#[derive(Component)]
struct StatusLine;
impl Plugin for ExampleHarness {
fn build(&self, app: &mut App) {
let env_u32 = |name: &str, default: u32| {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
};
let frames: Option<u32> = std::env::var("ELEMENTS_FRAMES")
.ok()
.and_then(|v| v.parse().ok());
let shot = std::env::var("ELEMENTS_SHOT").ok();
let shot_dir = std::env::var("ELEMENTS_SHOT_DIR").ok();
if frames.is_some() || shot_dir.is_some() {
app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f64(
1.0 / 60.0,
)));
}
if units() != 1.0 {
app.insert_resource(ElementsUnits {
units_per_metre: units(),
});
}
if std::env::var("ELEMENTS_PRECIPITATION").as_deref() == Ok("readable") {
app.insert_resource(PrecipitationLook::readable());
}
app.insert_resource(RunConfig {
frames,
shot,
shot_dir,
shot_every: env_u32("ELEMENTS_SHOT_EVERY", 1).max(1),
shot_from: env_u32("ELEMENTS_SHOT_FROM", 1).max(1),
frame: 0,
shot_taken: false,
grow_hours: std::env::var("ELEMENTS_GROW_HOURS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0.0),
pending: std::sync::Arc::default(),
})
.add_systems(Startup, (spawn_status, load_ground_textures))
.add_systems(Update, texture_terrain)
.add_systems(
Update,
(grow_weather, fly, ortho_rig, clock_keys, weather_keys).before(ElementsSystems::Clock),
)
.add_systems(
Update,
(settle_on_ground, plant_pins, status, capture_frames).after(ElementsSystems::Readback),
)
.add_systems(Update, run_limit.after(capture_frames))
.add_systems(PostStartup, scale_projections);
ground::plugin(app);
}
}
#[derive(Resource)]
struct GroundSet {
set: TerrainTextures,
on: bool,
swap: Option<f32>,
since: f32,
}
fn load_ground_textures(mut commands: Commands, asset_server: Res<AssetServer>) {
let map = |path: &str| -> Handle<Image> {
asset_server
.load_builder()
.with_settings(|settings: &mut bevy::image::ImageLoaderSettings| {
settings.asset_usage = bevy::asset::RenderAssetUsages::MAIN_WORLD;
})
.load(path.to_owned())
};
let mut set = TerrainTextures {
soil: Some(
GroundLayerTextures::new(map("ground/golem/soil.jpg"), 2.0)
.photograph()
.with_relief_deg(10.0),
),
grass: Some(
GroundLayerTextures::new(map("ground/golem/grass.jpg"), 2.0)
.photograph()
.with_relief_deg(14.0),
),
..default()
};
if let Some(share) = std::env::var("ELEMENTS_GROUND_COLOUR")
.ok()
.and_then(|s| s.parse::<f32>().ok())
{
for layer in [&mut set.sand, &mut set.soil, &mut set.grass]
.into_iter()
.flatten()
{
layer.colour = share;
}
}
commands.insert_resource(GroundSet {
set,
on: std::env::var("ELEMENTS_GROUND_TEXTURES").as_deref() != Ok("0"),
swap: std::env::var("ELEMENTS_GROUND_SWAP")
.ok()
.and_then(|s| s.parse::<f32>().ok())
.filter(|s| *s > 0.0),
since: 0.0,
});
}
fn texture_terrain(
mut commands: Commands,
ground: Option<ResMut<GroundSet>>,
added: Query<Entity, Added<Terrain>>,
terrains: Query<Entity, With<Terrain>>,
time: Res<Time>,
) {
let Some(mut ground) = ground else {
return;
};
let mut toggled = false;
if let Some(period) = ground.swap {
ground.since += time.delta_secs();
if ground.since >= period {
ground.since -= period;
ground.on = !ground.on;
toggled = true;
}
}
let targets: Vec<Entity> = if toggled {
terrains.iter().collect()
} else {
added.iter().collect()
};
for entity in targets {
if ground.on {
commands.entity(entity).insert(ground.set.clone());
} else {
commands.entity(entity).remove::<TerrainTextures>();
}
}
}
pub fn camera(position: Vec3, look_at: Vec3) -> impl Bundle {
let dir = (look_at - position).normalize_or(Vec3::NEG_Z);
let yaw = (-dir.x).atan2(-dir.z);
let pitch = dir.y.clamp(-1.0, 1.0).asin();
(
Camera3d::default(),
ElementsCamera::default(),
FlyCamera::looking(yaw, pitch),
Transform::from_translation(position * units()).with_rotation(Quat::from_euler(
EulerRot::YXZ,
yaw,
pitch,
0.0,
)),
)
}
fn scale_projections(mut cameras: Query<&mut Projection, With<FlyCamera>>) {
let u = units();
if u == 1.0 {
return;
}
for mut projection in &mut cameras {
if let Projection::Perspective(p) = &mut *projection {
p.near *= u;
p.far *= u;
}
}
}
#[derive(Component)]
pub struct StandOnGround {
height: f32,
}
pub fn stand_on_ground(height: f32) -> StandOnGround {
StandOnGround {
height: height * units(),
}
}
fn settle_on_ground(
mut commands: Commands,
env: Environment,
mut cameras: Query<(Entity, &mut Transform, &StandOnGround)>,
) {
for (entity, mut transform, stand) in &mut cameras {
if let Some(ground) = env.terrain_height(transform.translation.xz()) {
transform.translation.y = ground + stand.height;
commands.entity(entity).remove::<StandOnGround>();
}
}
}
pub const ORTHO_BACK_M: f32 = 457.0;
#[derive(Component)]
pub struct OrthoRig {
focus: Vec3,
forward: Vec3,
speed: f32,
}
pub fn ortho_camera(look: Vec3) -> Option<impl Bundle> {
let zoom: f32 = std::env::var("ELEMENTS_ORTHO").ok()?.parse().ok()?;
let env_f32 = |name: &str, default: f32| {
std::env::var(name)
.ok()
.and_then(|v| v.parse::<f32>().ok())
.unwrap_or(default)
};
let pitch = env_f32("ELEMENTS_PITCH", 35.26).to_radians();
let heading = env_f32("ELEMENTS_HEADING", 45.0).to_radians();
let forward = Vec3::new(
heading.sin() * pitch.cos(),
-pitch.sin(),
-heading.cos() * pitch.cos(),
);
let u = units();
let focus = look * u;
Some((
Camera3d::default(),
ElementsCamera::default(),
Projection::from(OrthographicProjection {
scaling_mode: bevy::camera::ScalingMode::FixedVertical {
viewport_height: zoom * u,
},
far: 2.0 * ORTHO_BACK_M * u,
..OrthographicProjection::default_3d()
}),
Transform::from_translation(focus - forward * ORTHO_BACK_M * u)
.looking_to(forward, Vec3::Y),
ElementsFocus::Point(focus),
OrthoRig {
focus,
forward,
speed: env_f32("ELEMENTS_WALK", 0.0) * u,
},
))
}
fn ortho_rig(
env: Environment,
time: Res<Time>,
mut rigs: Query<(&mut Transform, &mut ElementsFocus, &mut OrthoRig)>,
) {
let u = units();
for (mut transform, mut focus, mut rig) in &mut rigs {
let heading = (rig.forward * Vec3::new(1.0, 0.0, 1.0)).normalize_or_zero();
let step = heading * rig.speed * time.delta_secs();
rig.focus += step;
if let Some(ground) = env.terrain_height(rig.focus.xz()) {
rig.focus.y = ground;
}
transform.translation = rig.focus - rig.forward * ORTHO_BACK_M * u;
*focus = ElementsFocus::Point(rig.focus);
}
}
#[derive(Resource)]
pub struct Pins {
centre: Vec2,
planted: bool,
}
#[derive(Component)]
struct Pin {
xz: Vec2,
lift: f32,
}
pub const PIN_SPACING_M: f32 = 2.0;
pub const PIN_FIELD_M: f32 = 40.0;
pub const PIN_FOOT_M: f32 = 0.10;
pub fn pins(centre: Vec3) -> Option<Pins> {
std::env::var("ELEMENTS_PINS").ok()?;
Some(Pins {
centre: centre.xz() * units(),
planted: false,
})
}
fn plant_pins(
mut commands: Commands,
env: Environment,
pins: Option<ResMut<Pins>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut planted: Query<(&Pin, &mut Transform)>,
) {
let Some(mut pins) = pins else {
return;
};
if pins.planted {
for (pin, mut transform) in &mut planted {
if let Some(ground) = env.terrain_height(pin.xz) {
transform.translation.y = ground + pin.lift;
}
}
return;
}
if env.terrain_height(pins.centre).is_none() {
return;
}
let u = units();
let post_m = 1.7;
let post = meshes.add(Cuboid::new(0.06 * u, post_m * u, 0.06 * u));
let foot = meshes.add(Cuboid::new(0.08 * u, PIN_FOOT_M * u, 0.08 * u));
let white = materials.add(StandardMaterial {
base_color: Color::srgb(0.85, 0.85, 0.85),
perceptual_roughness: 0.8,
..default()
});
let red = materials.add(StandardMaterial {
base_color: Color::srgb(0.85, 0.04, 0.04),
perceptual_roughness: 0.8,
..default()
});
let n = (PIN_FIELD_M / PIN_SPACING_M) as i32;
for iz in -n / 2..=n / 2 {
for ix in -n / 2..=n / 2 {
let xz = pins.centre + Vec2::new(ix as f32, iz as f32) * PIN_SPACING_M * u;
let Some(ground) = env.terrain_height(xz) else {
continue;
};
for (mesh, material, lift) in [
(foot.clone(), red.clone(), 0.5 * PIN_FOOT_M * u),
(post.clone(), white.clone(), (PIN_FOOT_M + 0.5 * post_m) * u),
] {
commands.spawn((
Mesh3d(mesh),
MeshMaterial3d(material),
Transform::from_xyz(xz.x, ground + lift, xz.y),
Pin { xz, lift },
));
}
}
}
pins.planted = true;
}
fn fly(
keys: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
motion: Res<AccumulatedMouseMotion>,
time: Res<Time<Real>>,
mut cameras: Query<(&mut Transform, &mut FlyCamera)>,
) {
let dt = time.delta_secs();
for (mut transform, mut fly) in &mut cameras {
if mouse.pressed(MouseButton::Right) {
fly.yaw -= motion.delta.x * 0.003;
fly.pitch = (fly.pitch - motion.delta.y * 0.003).clamp(-1.54, 1.54);
transform.rotation = Quat::from_euler(EulerRot::YXZ, fly.yaw, fly.pitch, 0.0);
}
let mut wish = Vec3::ZERO;
let forward = transform.forward().as_vec3();
let right = transform.right().as_vec3();
for (key, dir) in [
(KeyCode::KeyW, forward),
(KeyCode::KeyS, -forward),
(KeyCode::KeyD, right),
(KeyCode::KeyA, -right),
(KeyCode::KeyE, Vec3::Y),
(KeyCode::KeyQ, -Vec3::Y),
] {
if keys.pressed(key) {
wish += dir;
}
}
let speed = units()
* if keys.pressed(KeyCode::ShiftLeft) {
60.0
} else {
12.0
};
transform.translation += wish.normalize_or_zero() * speed * dt;
}
}
fn grow_weather(
config: Res<RunConfig>,
mut clock: ResMut<WorldClock>,
mut plan: Local<Option<(f64, u32)>>,
mut advanced: Local<f64>,
) {
if config.grow_hours <= 0.0 {
return;
}
let Some(&mut (chunk, frames)) = plan.as_mut() else {
let hours = config.grow_hours.ceil();
*plan = Some((config.grow_hours / hours, hours as u32 + 1));
return;
};
if frames > 0 {
clock.advance_hours(chunk);
*advanced += chunk;
*plan = Some((chunk, frames - 1));
return;
}
let back = *advanced;
clock.advance_hours(-back);
*advanced = 0.0;
*plan = Some((0.0, 0));
}
fn clock_keys(keys: Res<ButtonInput<KeyCode>>, mut clock: ResMut<WorldClock>) {
if keys.just_pressed(KeyCode::BracketLeft) {
clock.time_scale *= 0.5;
}
if keys.just_pressed(KeyCode::BracketRight) {
clock.time_scale *= 2.0;
}
if keys.just_pressed(KeyCode::KeyT) {
clock.advance_hours(1.0);
}
}
fn weather_keys(keys: Res<ButtonInput<KeyCode>>, mut director: ResMut<WeatherDirector>) {
const DIGITS: [KeyCode; 10] = [
KeyCode::Digit1,
KeyCode::Digit2,
KeyCode::Digit3,
KeyCode::Digit4,
KeyCode::Digit5,
KeyCode::Digit6,
KeyCode::Digit7,
KeyCode::Digit8,
KeyCode::Digit9,
KeyCode::Digit0,
];
for (key, preset) in DIGITS.into_iter().zip(WeatherPreset::ALL) {
if keys.just_pressed(key) {
director.force(preset, Duration::from_secs(120));
}
}
if keys.just_pressed(KeyCode::KeyR) {
director.release(Duration::from_secs(120));
}
}
fn spawn_status(mut commands: Commands) {
commands.spawn((
StatusLine,
Text::new(""),
TextFont {
font_size: FontSize::Px(16.0),
..default()
},
TextColor(Color::WHITE),
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.55)),
Node {
position_type: PositionType::Absolute,
left: Val::Px(10.0),
top: Val::Px(10.0),
padding: UiRect::all(Val::Px(4.0)),
..default()
},
));
}
pub fn test_scene(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
commands.spawn((
GroundPlane,
Mesh3d(meshes.add(Plane3d::default().mesh().size(40000.0, 40000.0))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgb(0.30, 0.36, 0.22),
perceptual_roughness: 0.95,
..default()
})),
));
let stone = materials.add(StandardMaterial {
base_color: Color::srgb(0.55, 0.53, 0.50),
perceptual_roughness: 0.8,
..default()
});
let white = materials.add(StandardMaterial {
base_color: Color::srgb(0.85, 0.85, 0.85),
perceptual_roughness: 0.4,
..default()
});
let cube = meshes.add(Cuboid::new(6.0, 6.0, 6.0));
let column = meshes.add(Cylinder::new(1.5, 24.0));
let ball = meshes.add(Sphere::new(4.0));
for i in 0..24 {
let angle = i as f32 / 24.0 * std::f32::consts::TAU;
let radius = 40.0 + (i % 4) as f32 * 60.0;
let at = Vec3::new(angle.cos() * radius, 0.0, angle.sin() * radius);
let (mesh, material, y) = match i % 3 {
0 => (cube.clone(), stone.clone(), 3.0),
1 => (column.clone(), stone.clone(), 12.0),
_ => (ball.clone(), white.clone(), 4.0),
};
commands.spawn((
Mesh3d(mesh),
MeshMaterial3d(material),
ground::ShowcaseProp,
Transform::from_translation(at + Vec3::Y * y),
));
}
}
fn status(
env: Environment,
director: Res<WeatherDirector>,
mut text: Query<&mut Text, With<StatusLine>>,
) {
let Ok(mut text) = text.single_mut() else {
return;
};
let clock = env.clock();
let weather = env.weather();
let sun = env.sun();
text.0 = format!(
"{:04}-{:02}-{:02} {:05.2} h x{} | {:?}{} | {:.1} C cover {:.2} {:.1} mm/h wind {:.1} m/s vis {:.0} m | sun {:.1} deg moon {:.1} deg ({:.0}%)",
clock.date.year,
clock.date.month,
clock.date.day,
clock.hour,
clock.time_scale,
director.current(),
if director.is_forced() {
" (forced)"
} else {
""
},
env.temperature_c(),
weather.cloud_cover,
weather.precipitation_mm_h,
weather.wind_speed_ms,
weather.visibility_m,
sun.altitude_deg,
sun.moon_altitude_deg,
sun.moon_illuminated_fraction * 100.0,
);
}
fn capture_frames(
mut commands: Commands,
config: Res<RunConfig>,
mut seen: Local<u32>,
mut index: Local<u32>,
) {
let Some(dir) = config.shot_dir.clone() else {
return;
};
*seen += 1;
if *seen < config.shot_from || !(*seen - config.shot_from).is_multiple_of(config.shot_every) {
return;
}
if config.frames.is_some_and(|limit| *seen > limit) {
return;
}
if *index == 0
&& let Err(error) = std::fs::create_dir_all(&dir)
{
error!("cannot create frame directory {dir}: {error}");
return;
}
let path = format!("{dir}/frame{:04}.png", *index);
*index += 1;
let pending = config.pending.clone();
pending.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
commands
.spawn(Screenshot::primary_window())
.observe(move |captured: On<ScreenshotCaptured>| {
save_to_disk(path.clone())(captured);
pending.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
});
}
fn run_limit(
mut config: ResMut<RunConfig>,
mut commands: Commands,
mut exit: MessageWriter<AppExit>,
mut waited: Local<u32>,
) {
config.frame += 1;
let Some(limit) = config.frames else {
return;
};
if config.frame < limit {
return;
}
let pending = config.pending.load(std::sync::atomic::Ordering::Relaxed);
if pending > 0 {
*waited += 1;
if *waited < 120 {
return;
}
}
match config.shot.clone() {
Some(path) if !config.shot_taken => {
config.shot_taken = true;
let mut save = save_to_disk(path);
commands.spawn(Screenshot::primary_window()).observe(
move |captured: On<ScreenshotCaptured>, mut exit: MessageWriter<AppExit>| {
exit.write(AppExit::Success);
save(captured);
},
);
*waited = 0;
}
Some(_) if *waited < 120 => *waited += 1,
_ => {
exit.write(AppExit::Success);
}
}
}