mod common;
use bevy::diagnostic::{DiagnosticPath, DiagnosticsStore};
use bevy::prelude::*;
use bevy::render::diagnostic::RenderDiagnosticsPlugin;
use bevy::window::{PresentMode, WindowResolution};
use bevy_elements::*;
use common::{
ExampleHarness, camera, ortho_camera, stand_on_ground, terrain_look_env, test_scene, vec3_env,
};
const WARMUP_FRAMES: u32 = 240;
const AEOLIAN_FRAME_SHARE: f64 = 20.0 / 60.0;
const PASSES: [&str; 26] = [
"elements_wind",
"elements_fog",
"elements_clouds_noise",
"elements_clouds_weather",
"elements_clouds_shadow",
"elements_clouds_march",
"elements_clouds_resolve",
"elements_clouds_sky_probe",
"elements_clouds_composite",
"elements_precip_sim",
"elements_precip_draw",
"elements_hydrology",
"elements_snow",
"elements_wetness",
"elements_shelter",
"elements_aeolian_capacity",
"elements_aeolian_flux",
"elements_aeolian_move",
"elements_aeolian_avalanche",
"elements_terrain",
"elements_water_normals",
"elements_trails",
"elements_trails_readback",
"elements_waves_force",
"elements_waves_step",
"elements_waves_publish",
];
const SCENE_PASSES: [&str; 4] = [
"main_opaque_pass_3d",
"early prepass",
"main_transparent_pass_3d",
"per_view_shadow_pass",
];
fn main() {
let (width, height) = std::env::var("ELEMENTS_RESOLUTION")
.ok()
.and_then(|r| {
let (w, h) = r.split_once('x')?;
Some((w.parse().ok()?, h.parse().ok()?))
})
.unwrap_or((1920, 1080));
let quality = match std::env::var("ELEMENTS_QUALITY").as_deref() {
Ok("low") => ElementsQuality::Low,
Ok("medium") => ElementsQuality::Medium,
Ok("ultra") => ElementsQuality::Ultra,
_ => ElementsQuality::High,
};
let elements = if std::env::var("ELEMENTS_WATER").as_deref() == Ok("off") {
ElementsPlugins.build().disable::<WaterPlugin>()
} else {
ElementsPlugins.build()
};
let elements = if std::env::var("ELEMENTS_TRAILS").as_deref() == Ok("off") {
elements.disable::<TrailsPlugin>()
} else {
elements
};
let elements = if std::env::var("ELEMENTS_WAVES").as_deref() == Ok("off") {
elements.disable::<WavesPlugin>()
} else {
elements
};
let elements = if std::env::var("ELEMENTS_ROCKS").as_deref() == Ok("off") {
elements.disable::<RocksPlugin>()
} else {
elements
};
App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resolution: WindowResolution::new(width, height),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}),
RenderDiagnosticsPlugin,
bevy::diagnostic::FrameTimeDiagnosticsPlugin::default(),
elements,
ExampleHarness,
))
.insert_resource(quality)
.insert_resource(HydrologySettings {
time_scale: 1000.0,
..default()
})
.insert_resource(
WorldClock::new(CivilDate::new(2026, 6, 21))
.at_hour(16.0)
.with_time_scale(60.0),
)
.add_systems(Startup, setup)
.add_systems(Update, (report, drive_imprints, drive_contacts))
.run();
}
const LAKE_RANGE_M: f32 = 12.0;
fn lake_map() -> Image {
use bevy::asset::RenderAssetUsages;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
const CELLS: u32 = 256;
let mut data = Vec::with_capacity((CELLS * CELLS) as usize * 4);
for j in 0..CELLS {
for i in 0..CELLS {
let x = i as f32 + 0.5 - CELLS as f32 * 0.5;
let z = j as f32 + 0.5 - CELLS as f32 * 0.5;
let r = ((x * x + z * z).sqrt() / 50.0).min(1.0);
let bowl = 0.5 * (1.0 + (std::f32::consts::PI * r).cos());
data.extend_from_slice(&((8.0 - 6.0 * bowl) / LAKE_RANGE_M).to_le_bytes());
}
}
Image::new(
Extent3d {
width: CELLS,
height: CELLS,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data,
TextureFormat::R32Float,
RenderAssetUsages::default(),
)
}
fn setup(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut weathered: ResMut<Assets<WeatheredMaterial>>,
mut director: ResMut<WeatherDirector>,
quality: Res<ElementsQuality>,
) {
let lake = std::env::var("ELEMENTS_TERRAIN").as_deref() == Ok("lake");
let game = std::env::var("ELEMENTS_TERRAIN").as_deref() == Ok("game");
let bodies = std::env::var("ELEMENTS_CONTACTS").is_ok();
let placed = vec3_env("ELEMENTS_EYE").zip(vec3_env("ELEMENTS_LOOK"));
let ortho = ortho_camera(vec3_env("ELEMENTS_LOOK").unwrap_or(Vec3::ZERO));
let orthographic = ortho.is_some();
let eye = match ortho {
Some(ortho) => commands.spawn(ortho).id(),
None => commands
.spawn(if let Some((eye, look)) = placed {
camera(eye, look)
} else if lake && bodies {
camera(Vec3::new(0.0, 12.0, -20.0), Vec3::new(0.0, 2.0, 0.0))
} else if lake {
camera(Vec3::new(-75.0, 45.0, 0.0), Vec3::new(0.0, 0.0, 0.0))
} else if game {
camera(Vec3::new(0.0, 6.0, -10.0), Vec3::new(0.0, 0.0, 0.0))
} else {
camera(
Vec3::new(0.0, 30.0, 0.0),
Vec3::new(150.0, 91.9, 80.0),
)
})
.id(),
};
if placed.is_some()
&& !orthographic
&& let Some(agl) = std::env::var("ELEMENTS_AGL")
.ok()
.and_then(|a| a.parse::<f32>().ok())
{
commands.entity(eye).insert(stand_on_ground(agl));
}
if lake {
commands.spawn(
Terrain::from_heightmap(images.add(lake_map()), 0.0..LAKE_RANGE_M)
.with_extent(Vec2::splat(256.0))
.with_resolution(UVec2::splat(256)),
);
commands.spawn((
WaterSource {
rate_m3_s: 200.0,
radius_m: 5.0,
},
Transform::from_xyz(0.0, 2.0, 0.0),
));
}
if game {
commands.insert_resource(TrailGround::flat(0.0));
commands.insert_resource(SurfaceWeathering::with_snowpack(0.3, 100.0, 0.0));
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(400.0, 400.0))),
MeshMaterial3d(weathered.add(WeatheredMaterial {
base: StandardMaterial {
base_color: Color::srgb(0.30, 0.34, 0.22),
perceptual_roughness: 0.9,
..default()
},
extension: WeatheredExtension::default().with_trails(),
})),
));
}
let terrain = match std::env::var("ELEMENTS_TERRAIN").as_deref() {
Ok("lake") | Ok("game") => None,
Ok("none") => None,
Ok("mountains") => Some(ProceduralTerrain::mountains(42)),
Ok("dunes") => Some(ProceduralTerrain::dunes(42)),
_ => Some(ProceduralTerrain::hills(42)),
};
match terrain {
Some(preset) => {
commands.entity(eye).insert(stand_on_ground(30.0));
commands.spawn(
Terrain::procedural(preset)
.with_resolution(UVec2::splat(quality.terrain_max_resolution())),
);
if let Some(look) = terrain_look_env() {
commands.insert_resource(look);
}
if let Ok(which) = std::env::var("ELEMENTS_PATHS") {
let all = which.eq_ignore_ascii_case("all");
let mut paths = Vec::new();
if all || which.eq_ignore_ascii_case("road") {
paths.push(
Path::new(
vec![
Vec3::new(-120.0, 0.0, -60.0),
Vec3::new(-40.0, 0.0, -20.0),
Vec3::new(40.0, 0.0, -30.0),
Vec3::new(140.0, 0.0, -70.0),
],
2.5,
PathKind::Gravel,
)
.with_ruts(0.03),
);
}
if all {
paths.push(Path::new(
vec![
Vec3::new(-100.0, 0.0, 40.0),
Vec3::new(-20.0, 0.0, 10.0),
Vec3::new(60.0, 0.0, 20.0),
Vec3::new(150.0, 0.0, 30.0),
],
0.35,
PathKind::Earth,
));
paths.push(
Path::new(
vec![
Vec3::new(-60.0, 0.0, 120.0),
Vec3::new(0.0, 0.0, 60.0),
Vec3::new(90.0, 0.0, -10.0),
],
1.0,
PathKind::Earth,
)
.with_ruts(0.06),
);
}
if !paths.is_empty() {
commands.insert_resource(PathNetwork::new(paths));
}
}
commands.spawn((
WaterSource {
rate_m3_s: 50.0,
radius_m: 5.0,
},
Transform::from_xyz(100.0, 0.0, 100.0),
));
}
None if lake || game => {}
None => test_scene(&mut commands, &mut meshes, &mut materials),
}
let preset = std::env::var("ELEMENTS_PRESET")
.ok()
.and_then(|name| {
WeatherPreset::ALL
.into_iter()
.find(|p| format!("{p:?}").eq_ignore_ascii_case(&name))
})
.unwrap_or(
if matches!(std::env::var("ELEMENTS_TERRAIN").as_deref(), Ok("dunes")) {
WeatherPreset::DustStorm
} else {
WeatherPreset::PartlyCloudy
},
);
director.force(preset, std::time::Duration::ZERO);
let blockers: usize = std::env::var("ELEMENTS_BLOCKERS")
.ok()
.and_then(|n| n.parse().ok())
.unwrap_or(0);
for i in 0..blockers {
let angle = i as f32 * 2.399_963;
let radius = 4.0 + 26.0 * ((i as f32 + 0.5) / blockers as f32).sqrt();
commands.spawn((
PrecipitationBlocker {
half_extents: Vec3::new(2.0, 0.1, 2.0),
},
Transform::from_xyz(
angle.cos() * radius,
3.0 + (i % 7) as f32 * 1.5,
angle.sin() * radius,
),
));
}
let imprints: usize = std::env::var("ELEMENTS_IMPRINTS")
.ok()
.and_then(|n| n.parse().ok())
.unwrap_or(0);
for i in 0..imprints {
commands.spawn((
BenchImprint(i as f32),
Imprint::new(Vec2::new(0.1, 0.16), 3400.0),
Transform::from_xyz(0.0, -1000.0, 0.0),
));
}
let contacts: usize = std::env::var("ELEMENTS_CONTACTS")
.ok()
.and_then(|n| n.parse().ok())
.unwrap_or(0);
for i in 0..contacts {
commands.spawn((
BenchContact(i as f32),
WaterContact::sphere(0.3),
Transform::from_xyz(0.0, -1000.0, 0.0),
));
}
}
#[derive(Component)]
struct BenchImprint(f32);
fn drive_imprints(
env: Environment,
game: Option<Res<TrailGround>>,
snow: Option<Res<SurfaceWeathering>>,
time: Res<Time<Virtual>>,
mut imprints: Query<(&BenchImprint, &mut Transform)>,
) {
let t = time.elapsed_secs();
for (imprint, mut transform) in &mut imprints {
let radius = 3.0 + 0.3 * imprint.0;
let a = imprint.0 * 2.399_963 + 4.0 * t / radius;
let xz = Vec2::new(a.cos(), a.sin()) * radius;
let surface = match &game {
Some(g) => Some(g.height(xz) + snow.as_ref().map_or(0.0, |s| s.snow_depth_m)),
None => env
.terrain_height(xz)
.map(|h| h + env.snow_depth(xz).unwrap_or(0.0)),
};
let Some(surface) = surface else {
continue;
};
let y = surface - 0.05;
*transform = Transform::from_xyz(xz.x, y, xz.y)
.looking_to(Vec3::new(-a.sin(), 0.0, a.cos()), Vec3::Y);
}
}
#[derive(Component)]
struct BenchContact(f32);
fn drive_contacts(
env: Environment,
time: Res<Time<Virtual>>,
mut contacts: Query<(&BenchContact, &mut Transform)>,
) {
let t = time.elapsed_secs();
for (contact, mut transform) in &mut contacts {
let radius = 10.0 + 1.5 * contact.0;
let a = contact.0 * 2.399_963 + 2.0 * t / radius;
let xz = Vec2::new(a.cos(), a.sin()) * radius;
let (Some(ground), Some(depth)) = (env.terrain_height(xz), env.water_depth(xz)) else {
continue;
};
*transform = Transform::from_xyz(xz.x, ground + depth, xz.y);
}
}
fn report(
store: Res<DiagnosticsStore>,
quality: Res<ElementsQuality>,
mut frame: Local<u32>,
mut lowest: Local<[f64; PASSES.len()]>,
mut lowest_scene: Local<[f64; SCENE_PASSES.len()]>,
) {
*frame += 1;
if *frame > WARMUP_FRAMES {
for (slot, pass) in lowest.iter_mut().zip(PASSES) {
let gpu = DiagnosticPath::new(format!("render/{pass}/elapsed_gpu"));
if let Some(now) = store.get(&gpu).and_then(|d| d.value()) {
*slot = if *slot > 0.0 { slot.min(now) } else { now };
}
}
}
for (slot, pass) in lowest_scene
.iter_mut()
.zip(SCENE_PASSES)
.filter(|_| *frame > 600)
{
let gpu = DiagnosticPath::new(format!("render/{pass}/elapsed_gpu"));
if let Some(now) = store.get(&gpu).and_then(|d| d.value()) {
*slot = if *slot > 0.0 { slot.min(now) } else { now };
}
}
if !(*frame).is_multiple_of(120) {
return;
}
let mut total = 0.0;
let mut clouds = 0.0;
let mut aeolian = 0.0;
let mut line = format!("{:?}:", *quality);
for pass in PASSES {
let gpu = DiagnosticPath::new(format!("render/{pass}/elapsed_gpu"));
let cpu = DiagnosticPath::new(format!("render/{pass}/elapsed_cpu"));
match store.get(&gpu) {
Some(d) => {
let min = lowest[PASSES.iter().position(|p| *p == pass).unwrap_or(0)];
let mean = d.average().unwrap_or(f64::NAN);
if !pass.ends_with("_noise") && !pass.ends_with("_normals") {
if pass.starts_with("elements_aeolian") {
aeolian += min;
total += min * AEOLIAN_FRAME_SHARE;
} else {
total += min;
}
if pass.starts_with("elements_clouds") {
clouds += min;
}
}
line += &format!(" {pass} min {min:.3} / mean {mean:.3} ms GPU");
}
None => match store.get(&cpu).and_then(|d| d.smoothed()) {
Some(ms) => line += &format!(" {pass} {ms:.3} ms CPU (no GPU timestamps)"),
None => line += &format!(" {pass} -"),
},
}
}
println!("{line} | total min {total:.3} ms GPU, clouds min {clouds:.3} ms GPU");
if aeolian > 0.0 {
println!(
"aeolian: {aeolian:.3} ms per step (min), {:.3} ms per 60 Hz frame at 20 steps a second",
aeolian * AEOLIAN_FRAME_SHARE
);
}
let mut scene = String::from("scene:");
for pass in SCENE_PASSES {
let gpu = DiagnosticPath::new(format!("render/{pass}/elapsed_gpu"));
match store.get(&gpu) {
Some(d) => {
let min = lowest_scene[SCENE_PASSES.iter().position(|p| *p == pass).unwrap_or(0)];
scene += &format!(
" {pass} min {min:.3} / mean {:.3} ms GPU",
d.average().unwrap_or(f64::NAN)
);
}
None => scene += &format!(" {pass} -"),
}
}
if let Some(ms) = store
.get(&bevy::diagnostic::FrameTimeDiagnosticsPlugin::FRAME_TIME)
.and_then(|d| d.average())
{
scene += &format!(" | frame mean {ms:.3} ms");
}
println!("{scene}");
}