Path: blob/main/examples/large_scenes/bevy_city/src/main.rs
30636 views
//! A procedurally generated city.1//!2//! This scene is intended to be an attractive, fairly realistic stress test of Bevy's capacity3//! to model extremely large scenes.4//! As a result, the complexity is higher than in most examples or benchmarks —5//! we want to use a large number of features so that pathological paths6//! are caught during development, rather than by end users.78use argh::FromArgs;9use assets::{load_assets, CityAssets};10use bevy::{11anti_alias::taa::TemporalAntiAliasing,12camera::{visibility::NoCpuCulling, Exposure, Hdr},13camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},14color::palettes::css::WHITE,15feathers::{dark_theme::create_dark_theme, theme::UiTheme, FeathersPlugins},16light::{17atmosphere::{Falloff, PhaseFunction, ScatteringMedium, ScatteringTerm},18Atmosphere, AtmosphereEnvironmentMapLight,19},20pbr::{21wireframe::{WireframeConfig, WireframePlugin},22AtmosphereSettings, ContactShadows,23},24post_process::bloom::Bloom,25prelude::*,26window::{PresentMode, WindowResolution},27winit::WinitSettings,28world_serialization::WorldInstanceReady,29};3031use crate::generate_city::spawn_city;32use crate::{33assets::{merge_car_meshes, strip_base_url},34settings::{settings_ui, Settings},35};3637mod assets;38mod generate_city;39mod settings;4041#[derive(FromArgs, Resource, Clone)]42/// Config43pub struct Args {44/// seed45#[argh(option, default = "42")]46seed: u64,4748/// size49#[argh(option, default = "30")]50size: u32,5152/// adds NoCpuCulling to all meshes53#[argh(switch)]54no_cpu_culling: bool,55}5657fn main() {58let args: Args = argh::from_env();5960App::new()61.add_plugins((62DefaultPlugins.set(WindowPlugin {63primary_window: Some(Window {64title: "bevy_city".into(),65resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),66present_mode: PresentMode::AutoNoVsync,67position: WindowPosition::Centered(MonitorSelection::Primary),68..default()69}),70..default()71}),72FreeCameraPlugin,73FeathersPlugins,74WireframePlugin::default(),75))76.insert_resource(args.clone())77.insert_resource(ClearColor(Color::BLACK))78.insert_resource(WinitSettings::continuous())79.init_resource::<Settings>()80.insert_resource(UiTheme(create_dark_theme()))81.insert_resource(WireframeConfig {82global: false,83default_color: WHITE.into(),84..default()85})86// Like in many realistic large scenes, many of the objects don't move87// We can accelerate transform propagation by optimizing for this case88.insert_resource(StaticTransformOptimizations::Enabled)89.add_message::<CityAssetsLoaded>()90.add_message::<CityAssetsReady>()91.add_message::<CitySpawned>()92.add_systems(Startup, (scene.spawn(), spawn_atmosphere, load_assets))93.add_systems(94Update,95(96simulate_cars,97update_loading_screen,98process_assets.run_if(on_message::<CityAssetsLoaded>),99on_city_assets_ready.run_if(on_message::<CityAssetsReady>),100(add_no_cpu_culling, on_city_spawned, settings_ui.spawn())101.run_if(on_message::<CitySpawned>),102),103)104.add_observer(add_no_cpu_culling_on_scene_ready)105.run();106}107108fn scene() -> impl SceneList {109bsn_list![camera(), sun(), loading_screen()]110}111112fn camera() -> impl Scene {113bsn! {114Camera3d115Hdr116template_value(Transform::from_xyz(15.0, 10.0, 20.0).looking_at(Vec3::ZERO, Vec3::Y))117FreeCamera118AtmosphereSettings {119// Reduce the default max distance in the aerial view LUT120// to 16km to approximately fit the size of the city. This way the aerial perspective121// gets more detail and has less banding artifacts compared to the 32km default.122aerial_view_lut_max_distance: 1.6e4,123}124// The directional light illuminance used in this scene is125// quite bright, so raising the exposure compensation helps126// bring the scene to a nicer brightness range.127Exposure::OVERCAST128// Bloom gives the sun a much more natural look.129Bloom::NATURAL130// Enables the atmosphere to drive reflections and ambient lighting (IBL) for this view131AtmosphereEnvironmentMapLight132Msaa::Off133TemporalAntiAliasing134ContactShadows135}136}137138fn loading_screen() -> impl Scene {139bsn! {140LoadingScreen141Node {142position_type: PositionType::Absolute,143width: percent(100),144height: percent(100),145}146BackgroundColor(Color::BLACK)147Children [148Node {149position_type: PositionType::Absolute,150top: percent(50),151left: percent(20),152right: percent(20),153height: vh(40),154flex_direction: FlexDirection::Column,155align_items: AlignItems::FlexStart,156overflow: Overflow::scroll_y(),157}158Children [159(160LoadingText161Text("Loading...")162TextFont {163font_size: FontSize::Px(24.0),164}165),166(167LoadingPaths168Text169TextFont {170font_size: FontSize::Px(14.0),171}172),173]174]175}176}177178fn sun() -> impl Scene {179bsn! {180DirectionalLight {181shadow_maps_enabled: {Settings::default().shadow_maps_enabled},182contact_shadows_enabled: {Settings::default().contact_shadows_enabled},183illuminance: light_consts::lux::RAW_SUNLIGHT,184}185template_value(Transform::from_xyz(1.0, 0.15, 1.0).looking_at(Vec3::ZERO, Vec3::Y))186}187}188189/// Spawns the earth atmosphere plus an extra near-ground fog term.190fn spawn_atmosphere(191mut commands: Commands,192mut scattering_mediums: ResMut<Assets<ScatteringMedium>>,193) {194let mut earth_medium = ScatteringMedium::default();195196// Same 60 km atmosphere height as `ScatteringMedium::earth`197const ATMOSPHERE_REF_HEIGHT_KM: f32 = 60.0;198199// The scale height of haze is set to 100 meters providing a low-lying dense fog layer.200const HAZE_SCALE_HEIGHT_KM: f32 = 0.1;201202// Fog has high albedo and very low absorption resulting in a white color.203const HAZE_SINGLE_SCATTER_ALBEDO: f32 = 0.99;204205// Distance at which contrast falls low enough to be indistinguishable from the sky.206// known as Meteorological Optical Range207const HAZE_VISIBILITY_KM: f32 = 12.0;208209// Koschmieder relation to calculate the extinction coefficient for the medium in m^-1 units.210let beta_ext = (3.912 / HAZE_VISIBILITY_KM) * 1e-3;211212// Add the fog to the earth medium as an additional scattering term.213earth_medium.terms.push(ScatteringTerm {214absorption: Vec3::splat(beta_ext * (1.0 - HAZE_SINGLE_SCATTER_ALBEDO)),215scattering: Vec3::splat(beta_ext * HAZE_SINGLE_SCATTER_ALBEDO),216falloff: Falloff::Exponential {217scale: HAZE_SCALE_HEIGHT_KM / ATMOSPHERE_REF_HEIGHT_KM,218},219// Fog is approximated as a mie scatterer with this asymmetry factor220phase: PhaseFunction::Mie { asymmetry: 0.76 },221});222let earth_atmosphere = Atmosphere::earth(scattering_mediums.add(earth_medium));223224// This scale means that 1 city block in this scene will be roughly 100 meters relative to the atmosphere.225let scale = 1.0 / 20.0;226commands.spawn((227earth_atmosphere.clone(),228Transform::from_scale(Vec3::splat(scale))229.with_translation(-Vec3::Y * earth_atmosphere.inner_radius * scale),230));231}232233#[derive(Component, Default, Clone)]234struct LoadingScreen;235#[derive(Component, Default, Clone)]236struct LoadingText;237#[derive(Component, Default, Clone)]238struct LoadingPaths;239240/// Triggers when all the assets managed in [`CityAssets`] are loaded241#[derive(Message)]242struct CityAssetsLoaded;243/// Triggers when all the assets are done loading and have been processed244#[derive(Message)]245struct CityAssetsReady;246/// Triggers once all the city blocks have been spawned247#[derive(Message)]248struct CitySpawned;249250#[allow(clippy::type_complexity)]251fn update_loading_screen(252mut commands: Commands,253assets: Res<CityAssets>,254asset_server: Res<AssetServer>,255mut loading_text: Query<&mut Text, With<LoadingText>>,256mut loading_paths: Query<(Entity, &mut Text), (With<LoadingPaths>, Without<LoadingText>)>,257) {258let Ok(mut text) = loading_text.single_mut() else {259return;260};261let Ok((paths_entity, mut paths_text)) = loading_paths.single_mut() else {262return;263};264let mut paths = vec![];265for untyped in &assets.untyped_assets {266if let Some(path) = asset_server.get_path(untyped) {267let state = asset_server.is_loaded_with_dependencies(untyped);268if !state {269paths.push(strip_base_url(path.to_string()));270}271}272}273if paths.is_empty() {274commands.entity(paths_entity).despawn();275text.0 = "Processing assets...".into();276// Use a Message instead of an Event so asset processing only starts on the next frame277commands.write_message(CityAssetsLoaded);278} else {279text.0 = format!(280"Loading assets: {}/{}",281assets.untyped_assets.len() - paths.len(),282assets.untyped_assets.len(),283);284paths.reverse();285paths_text.0 = paths.join("\n");286}287}288289/// Runs after the assets are loaded. For now, this will merge all the meshes for each car gltf into290/// a single mesh. This is necessary because the tires are separate meshes and this increases the291/// amount of meshes bevy has to process every frame for no benefits.292///293/// Eventually, this will also be used for things like generating LODs294fn process_assets(295mut commands: Commands,296mut city_assets: ResMut<CityAssets>,297mut world_assets: ResMut<Assets<WorldAsset>>,298mut meshes: ResMut<Assets<Mesh>>,299) {300merge_car_meshes(&mut city_assets, &mut world_assets, &mut meshes);301302// Use a Message instead of an Event so spawning the city happens in the next frame303commands.write_message(CityAssetsReady);304}305306fn on_city_assets_ready(307mut commands: Commands,308city_assets: Res<CityAssets>,309args: Res<Args>,310mut loading_text: Query<&mut Text, With<LoadingText>>,311) {312let Ok(mut text) = loading_text.single_mut() else {313return;314};315text.0 = "Spawning city...".into();316317spawn_city(&mut commands, &city_assets, args.seed, args.size);318commands.write_message(CitySpawned);319}320321fn on_city_spawned(322mut commands: Commands,323loading_screen: Option<Single<Entity, With<LoadingScreen>>>,324) {325let Some(loading_screen) = loading_screen else {326return;327};328commands.entity(*loading_screen).despawn();329}330331#[derive(Component)]332struct Road {333start: Vec3,334end: Vec3,335}336337#[derive(Component)]338struct Car {339offset: Vec3,340distance_traveled: f32,341dir: f32,342}343344/// Do a very naive traffic simulation. This will only move the car to the end of the road then345/// spawn it back at the start.346///347/// Eventually this will be a more complex traffic simulation that should stress the ECS348fn simulate_cars(349settings: Res<Settings>,350roads: Query<(&Road, &Transform, &Children), Without<Car>>,351mut cars: Query<(&mut Car, &mut Transform), Without<Road>>,352time: Res<Time>,353) {354if !settings.simulate_cars {355return;356}357let speed = 1.5;358359for (road, _, children) in &roads {360for child in children {361let Ok((mut car, mut car_transform)) = cars.get_mut(*child) else {362continue;363};364365car.distance_traveled += speed * time.delta_secs();366let road_len = (road.end - road.start).length();367if car.distance_traveled > road_len {368car.distance_traveled = 0.0;369}370let direction = (road.end - road.start).normalize() * car.dir;371372let progress = car.distance_traveled / road_len;373car_transform.translation = (road.start + car.offset) + direction * road_len * progress;374}375}376}377378/// Adds [`NoCpuCulling`] to all meshes in the scene after the city is done spawning379fn add_no_cpu_culling(380mut commands: Commands,381meshes: Query<Entity, (With<Mesh3d>, Without<NoCpuCulling>)>,382args: Res<Args>,383) {384if args.no_cpu_culling {385for entity in meshes.iter() {386commands.entity(entity).insert(NoCpuCulling);387}388}389}390391/// Adds [`NoCpuCulling`] to all meshes in all scenes after the city is done spawning392///393/// This is required because a few assets are spawned using a [`WorldAssetRoot`] instead of directly394/// spawning a [`Mesh`]395fn add_no_cpu_culling_on_scene_ready(396scene_ready: On<WorldInstanceReady>,397mut commands: Commands,398children: Query<&Children>,399meshes: Query<(), (With<Mesh3d>, Without<NoCpuCulling>)>,400args: Res<Args>,401) {402if args.no_cpu_culling {403for descendant in children.iter_descendants(scene_ready.entity) {404if meshes.get(descendant).is_ok() {405commands.entity(descendant).insert(NoCpuCulling);406}407}408}409}410411412