mod common;
use std::f32::consts::TAU;
use std::time::Duration;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use bevy_elements::*;
use common::{ExampleHarness, camera};
const CENTRE: Vec2 = Vec2::new(0.0, 0.0);
const WALK_RADIUS_M: f32 = 6.0;
const WALK_SPEED_MS: f32 = 1.4;
const STEP_S: f32 = 0.5;
const STEP_WIDTH_M: f32 = 0.11;
const FOOT_HALF: Vec2 = Vec2::new(0.05, 0.13);
const FOOT_LOAD_N: f32 = 760.0;
const DRIVE_RADIUS_M: f32 = 14.0;
const DRIVE_SPEED_MS: f32 = 4.0;
const WHEELBASE_M: f32 = 2.6;
const TRACK_M: f32 = 1.5;
const WHEEL_HALF: Vec2 = Vec2::new(0.10, 0.16);
const WHEEL_LOAD_N: f32 = 3400.0;
#[derive(Clone, Copy, PartialEq)]
enum Ground {
Snow,
Sand,
Mud,
}
#[derive(Resource)]
struct Scene {
ground: Ground,
preset: Option<WeatherPreset>,
mesh: bool,
}
fn hills(xz: Vec2) -> f32 {
1.6 * (0.07 * xz.x).sin() * (0.05 * xz.y).cos() + 0.5 * (0.19 * xz.x + 0.13 * xz.y).sin()
}
fn hills_mesh(extent: f32, step: f32) -> Mesh {
let n = (extent / step) as u32 + 1;
let mut positions = Vec::with_capacity((n * n) as usize);
let mut normals = Vec::with_capacity((n * n) as usize);
let mut uvs = Vec::with_capacity((n * n) as usize);
let e = 0.05;
for j in 0..n {
for i in 0..n {
let xz = Vec2::new(i as f32, j as f32) * step - Vec2::splat(0.5 * extent);
let y = hills(xz);
let dx = (hills(xz + Vec2::X * e) - hills(xz - Vec2::X * e)) / (2.0 * e);
let dz = (hills(xz + Vec2::Y * e) - hills(xz - Vec2::Y * e)) / (2.0 * e);
positions.push([xz.x, y, xz.y]);
normals.push(Vec3::new(-dx, 1.0, -dz).normalize().to_array());
uvs.push([xz.x, xz.y]);
}
}
let mut indices = Vec::with_capacity(((n - 1) * (n - 1) * 6) as usize);
for j in 0..n - 1 {
for i in 0..n - 1 {
let a = j * n + i;
let (b, c, d) = (a + 1, a + n, a + n + 1);
indices.extend_from_slice(&[a, c, b, b, c, d]);
}
}
Mesh::new(
bevy::mesh::PrimitiveTopology::TriangleList,
bevy::asset::RenderAssetUsages::default(),
)
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
.with_inserted_indices(bevy::mesh::Indices::U32(indices))
}
#[derive(Component)]
struct Foot(f32);
#[derive(Component)]
struct Wheel(f32, f32);
#[derive(Component)]
struct Walker;
#[derive(Component)]
struct Vehicle;
fn env_f32(name: &str, default: f32) -> f32 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn env_vec(name: &str) -> Option<Vec<f32>> {
std::env::var(name)
.ok()
.map(|v| v.split(',').filter_map(|s| s.trim().parse().ok()).collect())
}
fn main() {
let ground = match std::env::var("ELEMENTS_GROUND").as_deref() {
Ok("sand") => Ground::Sand,
Ok("mud") => Ground::Mud,
_ => Ground::Snow,
};
let mesh = std::env::var("ELEMENTS_TERRAIN").is_ok_and(|v| v.eq_ignore_ascii_case("mesh"));
let preset = std::env::var("ELEMENTS_PRESET").ok().and_then(|name| {
WeatherPreset::ALL
.into_iter()
.find(|p| format!("{p:?}").eq_ignore_ascii_case(&name))
});
let date = if ground == Ground::Snow {
CivilDate::new(2026, 1, 15)
} else {
CivilDate::new(2026, 6, 15)
};
let clock = WorldClock::new(date)
.at_hour(f64::from(env_f32("ELEMENTS_HOUR", 10.5)))
.with_time_scale(env_f32("ELEMENTS_SCALE", 1.0));
let mut app = App::new();
if mesh {
let trail_ground = TrailGround::new(hills);
app.insert_resource(if ground == Ground::Sand {
trail_ground.with_sand()
} else {
trail_ground
});
}
app.add_plugins((DefaultPlugins, ElementsPlugins, ExampleHarness))
.insert_resource(clock)
.insert_resource(if ground == Ground::Sand {
Climate::desert()
} else {
Climate::temperate()
})
.insert_resource(ErosionSettings {
enabled: false,
..default()
})
.insert_resource(Scene {
ground,
preset,
mesh,
})
.add_systems(Startup, (setup, lay_paths))
.add_systems(Update, mark_paths.after(ElementsSystems::Readback))
.add_systems(
Update,
(settle_weather, walk, drive).after(ElementsSystems::Readback),
)
.run();
}
fn setup(
mut commands: Commands,
scene: Res<Scene>,
mut director: ResMut<WeatherDirector>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut weathered: ResMut<Assets<WeatheredMaterial>>,
) {
if scene.mesh {
let color = match scene.ground {
Ground::Snow => Color::srgb(0.30, 0.34, 0.22),
Ground::Sand => Color::srgb(0.72, 0.62, 0.45),
Ground::Mud => Color::srgb(0.30, 0.25, 0.18),
};
commands.spawn((
Mesh3d(meshes.add(hills_mesh(160.0, 0.5))),
MeshMaterial3d(weathered.add(WeatheredMaterial {
base: StandardMaterial {
base_color: color,
perceptual_roughness: 0.9,
..default()
},
extension: WeatheredExtension::default().with_trails(),
})),
));
} else {
commands.spawn(Terrain::procedural(match scene.ground {
Ground::Sand => ProceduralTerrain::dunes(7),
_ => ProceduralTerrain::hills(42),
}));
}
director.force(
match scene.ground {
Ground::Snow => WeatherPreset::Snow,
Ground::Sand => WeatherPreset::Clear,
Ground::Mud => WeatherPreset::Rain,
},
Duration::ZERO,
);
let eye = env_vec("ELEMENTS_EYE").unwrap_or_else(|| vec![0.0, 2.2, -11.0]);
let look = env_vec("ELEMENTS_LOOK").unwrap_or_else(|| vec![CENTRE.x, CENTRE.y]);
let (eye, look) = (
Vec3::new(eye[0], 100.0 + eye[1], eye[2]),
Vec3::new(look[0], 100.0, look[1]),
);
commands.spawn((camera(eye, look), EyeHeight(eye.y - 100.0), Aim(look.xz())));
let body = materials.add(StandardMaterial {
base_color: Color::srgb(0.25, 0.27, 0.32),
perceptual_roughness: 0.8,
..default()
});
commands.spawn((
Walker,
Mesh3d(meshes.add(Capsule3d::new(0.22, 1.2))),
MeshMaterial3d(body.clone()),
Transform::from_xyz(0.0, -100.0, 0.0),
));
for side in [-1.0, 1.0] {
commands.spawn((Foot(side), Imprint::new(FOOT_HALF, FOOT_LOAD_N)));
}
let paint = materials.add(StandardMaterial {
base_color: Color::srgb(0.55, 0.12, 0.08),
perceptual_roughness: 0.4,
..default()
});
commands.spawn((
Vehicle,
Mesh3d(meshes.add(Cuboid::new(1.7, 1.1, 3.6))),
MeshMaterial3d(paint),
Transform::from_xyz(0.0, -100.0, 0.0),
));
for front in [-1.0, 1.0] {
for side in [-1.0, 1.0] {
commands.spawn((Wheel(front, side), Imprint::new(WHEEL_HALF, WHEEL_LOAD_N)));
}
}
}
#[derive(Component)]
struct EyeHeight(f32);
#[derive(Component)]
struct Aim(Vec2);
#[derive(SystemParam)]
struct Surface<'w> {
env: Environment<'w>,
game: Option<Res<'w, TrailGround>>,
snow: Option<Res<'w, SurfaceWeathering>>,
}
impl Surface<'_> {
fn at(&self, xz: Vec2) -> Option<f32> {
match &self.game {
Some(g) => Some(g.height(xz) + self.snow.as_ref().map_or(0.0, |s| s.snow_depth_m)),
None => Some(self.env.terrain_height(xz)? + self.env.snow_depth(xz).unwrap_or(0.0)),
}
}
}
fn settle_weather(
mut commands: Commands,
surface: Surface,
scene: Res<Scene>,
mut director: ResMut<WeatherDirector>,
mut cameras: Query<(Entity, &mut Transform, &EyeHeight, &Aim)>,
mut done: Local<bool>,
mut frames: Local<u32>,
) {
*frames += 1;
if scene.mesh && *frames < 60 {
return;
}
for (entity, mut transform, eye, aim) in &mut cameras {
let (Some(here), Some(there)) = (
surface.at(transform.translation.xz()),
surface.at(aim.0),
) else {
continue;
};
transform.translation.y = here + eye.0;
transform.look_at(Vec3::new(aim.0.x, there + 0.3, aim.0.y), Vec3::Y);
commands.entity(entity).remove::<(EyeHeight, Aim)>();
if !*done {
*done = true;
director.force(scene.preset.unwrap_or(WeatherPreset::Clear), Duration::ZERO);
}
}
}
fn on_circle(radius: f32, s: f32) -> (Vec2, Vec2) {
let a = s / radius;
let (sin, cos) = a.sin_cos();
(CENTRE + radius * Vec2::new(cos, sin), Vec2::new(-sin, cos))
}
fn walk(
surface: Surface,
time: Res<Time<Virtual>>,
mut feet: Query<(&Foot, &mut Transform), Without<Walker>>,
mut body: Query<&mut Transform, With<Walker>>,
) {
let t = time.elapsed_secs();
let s = WALK_SPEED_MS * t;
let (centre, _) = on_circle(WALK_RADIUS_M, s);
if let (Ok(mut b), Some(y)) = (body.single_mut(), surface.at(centre)) {
b.translation = Vec3::new(centre.x, y + 0.85, centre.y);
}
let step = WALK_SPEED_MS * STEP_S;
for (foot, mut transform) in &mut feet {
let phase = t / STEP_S + if foot.0 < 0.0 { 0.0 } else { 1.0 };
let index = (phase / 2.0).floor();
let within = phase - 2.0 * index;
let planted = within < 1.0;
let plant_s = (2.0 * index * STEP_S - if foot.0 < 0.0 { 0.0 } else { STEP_S })
* WALK_SPEED_MS
+ 0.5 * step;
let (at, along) = if planted {
on_circle(WALK_RADIUS_M, plant_s)
} else {
let k = within - 1.0;
on_circle(WALK_RADIUS_M, plant_s + 2.0 * step * k)
};
let side = Vec2::new(along.y, -along.x) * foot.0 * STEP_WIDTH_M;
let xz = at + side;
let Some(y) = surface.at(xz) else {
continue;
};
let lift = if planted {
-0.05
} else {
0.3 * (TAU * 0.5 * (within - 1.0)).sin().abs() + 0.3
};
*transform = Transform::from_xyz(xz.x, y + lift, xz.y)
.looking_to(Vec3::new(along.x, 0.0, along.y), Vec3::Y);
}
}
fn drive(
surface: Surface,
time: Res<Time<Virtual>>,
mut wheels: Query<(&Wheel, &mut Transform), Without<Vehicle>>,
mut body: Query<&mut Transform, With<Vehicle>>,
) {
let s = DRIVE_SPEED_MS * time.elapsed_secs();
let (centre, along) = on_circle(DRIVE_RADIUS_M, s);
let side = Vec2::new(along.y, -along.x);
if let (Ok(mut b), Some(y)) = (body.single_mut(), surface.at(centre)) {
*b = Transform::from_xyz(centre.x, y + 0.85, centre.y)
.looking_to(Vec3::new(along.x, 0.0, along.y), Vec3::Y);
}
for (wheel, mut transform) in &mut wheels {
let xz = centre + along * wheel.0 * 0.5 * WHEELBASE_M + side * wheel.1 * 0.5 * TRACK_M;
let Some(y) = surface.at(xz) else {
continue;
};
*transform = Transform::from_xyz(xz.x, y - 0.05, xz.y)
.looking_to(Vec3::new(along.x, 0.0, along.y), Vec3::Y);
}
}
fn lay_paths(mut commands: Commands) {
let Ok(which) = std::env::var("ELEMENTS_PATHS") else {
return;
};
let which = which.to_ascii_lowercase();
let all = which == "all";
let mut paths = Vec::new();
if all || which == "lane" {
paths.push(Path::new(
vec![
Vec3::new(-24.0, 0.0, -18.0),
Vec3::new(-9.0, 0.0, -6.0),
Vec3::new(-2.0, 0.0, 2.0),
Vec3::new(6.0, 0.0, 9.0),
Vec3::new(20.0, 0.0, 15.0),
],
0.35,
PathKind::Earth,
));
}
if all || which == "track" {
paths.push(
Path::new(
vec![
Vec3::new(-26.0, 0.0, 5.0),
Vec3::new(-12.0, 0.0, 4.0),
Vec3::new(0.0, 0.0, 3.0),
Vec3::new(13.0, 0.0, 6.0),
Vec3::new(26.0, 0.0, 10.0),
],
1.0,
PathKind::Earth,
)
.with_ruts(0.06),
);
}
if all || which == "road" {
paths.push(
Path::new(
vec![
Vec3::new(-30.0, 0.0, -24.0),
Vec3::new(-16.0, 0.0, -14.0),
Vec3::new(-4.0, 0.0, -10.0),
Vec3::new(10.0, 0.0, -12.0),
Vec3::new(26.0, 0.0, -20.0),
],
2.5,
PathKind::Gravel,
)
.with_ruts(0.03),
);
}
if !paths.is_empty() {
commands.insert_resource(PathNetwork::new(paths));
}
if std::env::var("ELEMENTS_PATHS_MARK").is_ok() {
commands.insert_resource(PathMarks { placed: false });
}
}
#[derive(Resource)]
struct PathMarks {
placed: bool,
}
fn mark_paths(
mut commands: Commands,
env: Environment,
paths: Option<Res<PathNetwork>>,
marks: Option<ResMut<PathMarks>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let Some(mut marks) = marks else {
return;
};
let Some(network) = paths.as_deref() else {
return;
};
if marks.placed {
return;
}
let post = meshes.add(Cuboid::new(0.18, 2.4, 0.18));
let white = materials.add(StandardMaterial {
base_color: Color::srgb(0.95, 0.95, 0.98),
perceptual_roughness: 0.7,
..default()
});
let mut placed = 0;
{
for path in &network.paths {
for pair in path.points.windows(2) {
let (a, b) = (pair[0].xz(), pair[1].xz());
let span = a.distance(b);
let steps = (span / 3.0).ceil().max(1.0) as i32;
for k in 0..=steps {
let on = a.lerp(b, k as f32 / steps as f32);
let Some(hit) = env.nearest_path(on) else {
continue;
};
let normal = if on == hit.point {
let ab = (b - a).normalize_or_zero();
Vec2::new(-ab.y, ab.x)
} else {
(on - hit.point).normalize_or_zero()
};
let target = hit.point + normal * (hit.surface.half_width_m);
let Some(edge_hit) = env.nearest_path(target) else {
continue;
};
debug_assert!(edge_hit.distance_m.abs() < 0.35, "{edge_hit:?}");
let outside = target + normal * 2.0;
let ground = env.terrain_height(outside).unwrap_or(0.0);
commands.spawn((
Mesh3d(post.clone()),
MeshMaterial3d(white.clone()),
Transform::from_xyz(outside.x, ground + 1.2, outside.y),
));
placed += 1;
}
let _ = span;
}
}
}
eprintln!("path marks: {placed} posts placed by nearest_path");
for probe in [
Vec2::new(-30.0, -24.0),
Vec2::new(-4.0, -10.0),
Vec2::new(10.0, -12.0),
Vec2::new(0.0, 0.0),
Vec2::new(0.0, 3.0),
] {
eprintln!(" at {probe:?}: {:?}", env.nearest_path(probe));
}
marks.placed = true;
}