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, stand_on_ground, test_scene};
const WARMUP_FRAMES: u32 = 240;
const AEOLIAN_FRAME_SHARE: f64 = 20.0 / 60.0;
const PASSES: [&str; 20] = [
"elements_wind",
"elements_fog",
"elements_clouds_noise",
"elements_clouds_weather",
"elements_clouds_shadow",
"elements_clouds_march",
"elements_clouds_resolve",
"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",
];
const SCENE_PASSES: [&str; 3] = [
"main_opaque_pass_3d",
"early prepass",
"main_transparent_pass_3d",
];
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()
};
App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resolution: WindowResolution::new(width, height),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}),
RenderDiagnosticsPlugin,
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)
.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 director: ResMut<WeatherDirector>,
quality: Res<ElementsQuality>,
) {
let lake = std::env::var("ELEMENTS_TERRAIN").as_deref() == Ok("lake");
let eye = commands
.spawn(if lake {
camera(Vec3::new(-75.0, 45.0, 0.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 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),
));
}
let terrain = match std::env::var("ELEMENTS_TERRAIN").as_deref() {
Ok("lake") => 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())),
);
commands.spawn((
WaterSource {
rate_m3_s: 50.0,
radius_m: 5.0,
},
Transform::from_xyz(100.0, 0.0, 100.0),
));
}
None if lake => {}
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,
),
));
}
}
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} -"),
}
}
println!("{scene}");
}