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