Skip to content

Commit 11bd3c4

Browse files
authored
Merge car meshes in bevy_city and improve loading screen (#23574)
# Objective - In the asset pack used for bevy_city the tires of all the cars are separate meshes from the car body. In some of the cars even the doors are also separate meshes. - Since we don't animate these elements of the car we can trivially combine all the meshes of each car scene into one single car mesh. This helps a lot to keep performance manageable and eventually increase the scale of bevy_city. ## Solution - Wait for the assets to load and once they are loaded loop over all the car scenes and combine their meshes. This is mainly possible because the entire asset pack uses a single texture so we don't need a separate material for each part of the mesh. - To do this I also refactored a bit how each step happens when starting the app. I also made sure that each step is reflected on the loading screen. - I also added a bit more docs ## Testing - I ran bevy_city and confirmed that the tires are where they should be relative to the mesh and that performance was better. I went from 90fps to 112fps on my machine --- ## Showcase https://github.com/user-attachments/assets/c2a10681-d148-4394-a3ca-fffe928fa09c
1 parent 104a8c3 commit 11bd3c4

4 files changed

Lines changed: 158 additions & 33 deletions

File tree

examples/large_scenes/bevy_city/src/assets.rs

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bevy::prelude::*;
1+
use bevy::{ecs::system::SystemState, prelude::*};
22
use rand::RngExt;
33

44
const BASE_URL: &str = "https://github.com/bevyengine/bevy_asset_files/raw/main/kenney";
@@ -13,6 +13,8 @@ pub fn strip_base_url(path: String) -> String {
1313
pub struct CityAssets {
1414
pub untyped_assets: Vec<UntypedHandle>,
1515
pub cars: Vec<Handle<WorldAsset>>,
16+
pub car_meshes: Vec<Handle<Mesh>>,
17+
pub car_material: Handle<StandardMaterial>,
1618
pub crossroad: Handle<WorldAsset>,
1719
pub road_straight: Handle<WorldAsset>,
1820
pub high_density: Buildings,
@@ -30,8 +32,12 @@ pub struct CityAssets {
3032
}
3133

3234
impl CityAssets {
33-
pub fn get_random_car<R: RngExt>(&self, rng: &mut R) -> Handle<WorldAsset> {
34-
self.cars[rng.random_range(0..self.cars.len())].clone()
35+
pub fn get_random_car<R: RngExt>(
36+
&self,
37+
rng: &mut R,
38+
) -> (Mesh3d, MeshMaterial3d<StandardMaterial>) {
39+
let mesh = self.car_meshes[rng.random_range(0..self.car_meshes.len())].clone();
40+
(Mesh3d(mesh), MeshMaterial3d(self.car_material.clone()))
3541
}
3642
}
3743

@@ -68,6 +74,13 @@ pub fn load_assets(
6874
}};
6975
}
7076

77+
let car_texture: Handle<Image> =
78+
load_asset!(format!("{base_url}/car-kit/Textures/colormap.png"));
79+
let car_material = materials.add(StandardMaterial {
80+
base_color_texture: Some(car_texture),
81+
..Default::default()
82+
});
83+
7184
let cars = {
7285
// TODO generate color variations
7386
[
@@ -222,6 +235,8 @@ pub fn load_assets(
222235
commands.insert_resource(CityAssets {
223236
untyped_assets,
224237
cars,
238+
car_meshes: vec![],
239+
car_material,
225240
crossroad,
226241
road_straight,
227242
high_density,
@@ -234,3 +249,57 @@ pub fn load_assets(
234249
fence,
235250
});
236251
}
252+
253+
/// Merge the meshes of all the cars gltf into a single mesh per car.
254+
///
255+
/// The asset pack we are using uses a separate mesh for each tire of the car and some also have
256+
/// doors as separate meshes. This is useful if you want to animate these element individually but
257+
/// in this scene we don't need to do that. Having multiple meshes for a single car means we need
258+
/// to run transform propagation on all these meshes and it will also generate even more indirect
259+
/// commands for each of those meshes.
260+
pub fn merge_car_meshes(
261+
city_assets: &mut CityAssets,
262+
world_assets: &mut Assets<WorldAsset>,
263+
meshes: &mut Assets<Mesh>,
264+
) {
265+
for car_scene in &city_assets.cars {
266+
let Some(merged) = merge_world_asset(world_assets, meshes, car_scene) else {
267+
continue;
268+
};
269+
city_assets.car_meshes.push(meshes.add(merged));
270+
}
271+
}
272+
273+
/// Merge an entire scene into a single mesh
274+
fn merge_world_asset(
275+
world_assets: &mut Assets<WorldAsset>,
276+
meshes: &mut Assets<Mesh>,
277+
scene_handle: &Handle<WorldAsset>,
278+
) -> Option<Mesh> {
279+
let mut scene = world_assets.get_mut(scene_handle)?;
280+
let mut merged: Option<Mesh> = None;
281+
282+
let mut system_state = SystemState::<TransformHelper>::new(&mut scene.world);
283+
let helper = system_state.get(&scene.world).ok()?;
284+
285+
for entity_ref in scene.world.iter_entities() {
286+
let Some(mesh) = entity_ref
287+
.get::<Mesh3d>()
288+
.and_then(|mesh3d| meshes.get(mesh3d))
289+
else {
290+
continue;
291+
};
292+
let Ok(global_transform) = helper.compute_global_transform(entity_ref.id()) else {
293+
continue;
294+
};
295+
let transform = global_transform.compute_transform();
296+
let transformed = mesh.clone().transformed_by(transform);
297+
match &mut merged {
298+
Some(mesh) => {
299+
let _ = mesh.merge(&transformed);
300+
}
301+
None => merged = Some(transformed),
302+
}
303+
}
304+
merged
305+
}

examples/large_scenes/bevy_city/src/generate_city.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ fn spawn_roads_and_cars<R: RngExt>(
122122

123123
if rng.random::<f32>() < max_car_density {
124124
commands.spawn((
125-
WorldAssetRoot(assets.get_random_car(rng)),
125+
assets.get_random_car(rng),
126126
Transform::from_translation(car_pos + Vec3::new(0.0, 0.0, -0.15))
127127
.with_scale(Vec3::splat(0.15))
128128
.with_rotation(Quat::from_axis_angle(
@@ -139,7 +139,7 @@ fn spawn_roads_and_cars<R: RngExt>(
139139

140140
if rng.random::<f32>() < max_car_density {
141141
commands.spawn((
142-
WorldAssetRoot(assets.get_random_car(rng)),
142+
assets.get_random_car(rng),
143143
Transform::from_translation(car_pos + Vec3::new(0.0, 0.0, 0.15))
144144
.with_scale(Vec3::splat(0.15))
145145
.with_rotation(Quat::from_axis_angle(
@@ -180,7 +180,7 @@ fn spawn_roads_and_cars<R: RngExt>(
180180

181181
if rng.random::<f32>() < max_car_density {
182182
commands.spawn((
183-
WorldAssetRoot(assets.get_random_car(rng)),
183+
assets.get_random_car(rng),
184184
Transform::from_translation(car_pos + Vec3::new(0.15, 0.0, 0.0))
185185
.with_scale(Vec3::splat(0.15)),
186186
Car {
@@ -193,7 +193,7 @@ fn spawn_roads_and_cars<R: RngExt>(
193193

194194
if rng.random::<f32>() < max_car_density {
195195
commands.spawn((
196-
WorldAssetRoot(assets.get_random_car(rng)),
196+
assets.get_random_car(rng),
197197
Transform::from_translation(car_pos + Vec3::new(-0.15, 0.0, 0.0))
198198
.with_scale(Vec3::splat(0.15))
199199
.with_rotation(Quat::from_axis_angle(Vec3::Y, std::f32::consts::PI)),

examples/large_scenes/bevy_city/src/main.rs

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ use bevy::{
2020
world_serialization::WorldInstanceReady,
2121
};
2222

23-
use crate::{assets::strip_base_url, settings::Settings};
23+
use crate::{
24+
assets::{merge_car_meshes, strip_base_url},
25+
settings::Settings,
26+
};
2427
use crate::{generate_city::spawn_city, settings::setup_settings_ui};
2528

2629
mod assets;
@@ -75,12 +78,22 @@ fn main() {
7578
// Like in many realistic large scenes, many of the objects don't move
7679
// We can accelerate transform propagation by optimizing for this case
7780
.insert_resource(StaticTransformOptimizations::Enabled)
81+
.add_message::<CityAssetsLoaded>()
82+
.add_message::<CityAssetsReady>()
83+
.add_message::<CitySpawned>()
7884
.add_systems(Startup, (setup, load_assets))
79-
.add_systems(Update, (simulate_cars, loading_screen))
80-
.add_observer(add_no_cpu_culling)
85+
.add_systems(
86+
Update,
87+
(
88+
simulate_cars,
89+
loading_screen,
90+
process_assets.run_if(on_message::<CityAssetsLoaded>),
91+
on_city_assets_ready.run_if(on_message::<CityAssetsReady>),
92+
(add_no_cpu_culling, on_city_spawned, setup_settings_ui)
93+
.run_if(on_message::<CitySpawned>),
94+
),
95+
)
8196
.add_observer(add_no_cpu_culling_on_scene_ready)
82-
.add_observer(on_city_assets_ready)
83-
.add_observer(setup_settings_ui)
8497
.run();
8598
}
8699

@@ -168,27 +181,28 @@ struct LoadingText;
168181
#[derive(Component)]
169182
struct LoadingPaths;
170183

171-
#[derive(Event)]
184+
/// Triggers when all the assets managed in [`CityAssets`] are loaded
185+
#[derive(Message)]
186+
struct CityAssetsLoaded;
187+
/// Triggers when all the assets are done loading and have been processed
188+
#[derive(Message)]
172189
struct CityAssetsReady;
173-
174-
#[derive(Event)]
190+
/// Triggers once all the city blocks have been spawned
191+
#[derive(Message)]
175192
struct CitySpawned;
176193

194+
#[allow(clippy::type_complexity)]
177195
fn loading_screen(
178196
mut commands: Commands,
179197
assets: Res<CityAssets>,
180198
asset_server: Res<AssetServer>,
181199
mut loading_text: Query<&mut Text, With<LoadingText>>,
182-
mut loading_paths: Query<&mut Text, (With<LoadingPaths>, Without<LoadingText>)>,
183-
loading_screen: Query<Entity, With<LoadingScreen>>,
200+
mut loading_paths: Query<(Entity, &mut Text), (With<LoadingPaths>, Without<LoadingText>)>,
184201
) {
185-
let Ok(loading_screen) = loading_screen.single() else {
186-
return;
187-
};
188202
let Ok(mut text) = loading_text.single_mut() else {
189203
return;
190204
};
191-
let Ok(mut paths_text) = loading_paths.single_mut() else {
205+
let Ok((paths_entity, mut paths_text)) = loading_paths.single_mut() else {
192206
return;
193207
};
194208
let mut paths = vec![];
@@ -201,8 +215,10 @@ fn loading_screen(
201215
}
202216
}
203217
if paths.is_empty() {
204-
commands.entity(loading_screen).despawn();
205-
commands.trigger(CityAssetsReady);
218+
commands.entity(paths_entity).despawn();
219+
text.0 = "Processing assets...".into();
220+
// Use a Message instead of an Event so asset processing only starts on the next frame
221+
commands.write_message(CityAssetsLoaded);
206222
} else {
207223
text.0 = format!(
208224
"Loading assets: {}/{}",
@@ -214,14 +230,46 @@ fn loading_screen(
214230
}
215231
}
216232

233+
/// Runs after the assets are loaded. For now, this will merge all the meshes for each car gltf into
234+
/// a single mesh. This is necessary because the tires are separate meshes and this increases the
235+
/// amount of meshes bevy has to process every frame for no benefits.
236+
///
237+
/// Eventually, this will also be used for things like generating LODs
238+
fn process_assets(
239+
mut commands: Commands,
240+
mut city_assets: ResMut<CityAssets>,
241+
mut world_assets: ResMut<Assets<WorldAsset>>,
242+
mut meshes: ResMut<Assets<Mesh>>,
243+
) {
244+
merge_car_meshes(&mut city_assets, &mut world_assets, &mut meshes);
245+
246+
// Use a Message instead of an Event so spawning the city happens in the next frame
247+
commands.write_message(CityAssetsReady);
248+
}
249+
217250
fn on_city_assets_ready(
218-
_: On<CityAssetsReady>,
219251
mut commands: Commands,
220-
assets: Res<CityAssets>,
252+
city_assets: Res<CityAssets>,
221253
args: Res<Args>,
254+
mut loading_text: Query<&mut Text, With<LoadingText>>,
222255
) {
223-
spawn_city(&mut commands, &assets, args.seed, args.size);
224-
commands.trigger(CitySpawned);
256+
let Ok(mut text) = loading_text.single_mut() else {
257+
return;
258+
};
259+
text.0 = "Spawning city...".into();
260+
261+
spawn_city(&mut commands, &city_assets, args.seed, args.size);
262+
commands.write_message(CitySpawned);
263+
}
264+
265+
fn on_city_spawned(
266+
mut commands: Commands,
267+
loading_screen: Option<Single<Entity, With<LoadingScreen>>>,
268+
) {
269+
let Some(loading_screen) = loading_screen else {
270+
return;
271+
};
272+
commands.entity(*loading_screen).despawn();
225273
}
226274

227275
#[derive(Component)]
@@ -237,6 +285,10 @@ struct Car {
237285
dir: f32,
238286
}
239287

288+
/// Do a very naive traffic simulation. This will only move the car to the end of the road then
289+
/// spawn it back at the start.
290+
///
291+
/// Eventually this will be a more complex traffic simulation that should stress the ECS
240292
fn simulate_cars(
241293
settings: Res<Settings>,
242294
roads: Query<(&Road, &Transform, &Children), Without<Car>>,
@@ -267,8 +319,8 @@ fn simulate_cars(
267319
}
268320
}
269321

322+
/// Adds [`NoCpuCulling`] to all meshes in the scene after the city is done spawning
270323
fn add_no_cpu_culling(
271-
_: On<CitySpawned>,
272324
mut commands: Commands,
273325
meshes: Query<Entity, (With<Mesh3d>, Without<NoCpuCulling>)>,
274326
args: Res<Args>,
@@ -280,6 +332,10 @@ fn add_no_cpu_culling(
280332
}
281333
}
282334

335+
/// Adds [`NoCpuCulling`] to all meshes in all scenes after the city is done spawning
336+
///
337+
/// This is required because a few assets are spawned using a [`WorldAssetRoot`] instead of directly
338+
/// spawning a [`Mesh`]
283339
fn add_no_cpu_culling_on_scene_ready(
284340
scene_ready: On<WorldInstanceReady>,
285341
mut commands: Commands,

examples/large_scenes/bevy_city/src/settings.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use bevy::{
1313
};
1414
use rand::RngExt;
1515

16+
use crate::assets::CityAssets;
1617
use crate::generate_city::{spawn_city, CityRoot};
17-
use crate::{assets::CityAssets, CitySpawned};
1818

1919
#[derive(Resource)]
2020
pub struct Settings {
@@ -37,6 +37,10 @@ impl Default for Settings {
3737
}
3838
}
3939

40+
pub fn setup_settings_ui(mut commands: Commands) {
41+
commands.spawn_scene(settings_ui());
42+
}
43+
4044
pub fn settings_ui() -> impl Scene {
4145
bsn! {
4246
Node {
@@ -161,7 +165,3 @@ pub fn settings_ui() -> impl Scene {
161165
)]
162166
}
163167
}
164-
165-
pub fn setup_settings_ui(_: On<CitySpawned>, mut commands: Commands) {
166-
commands.spawn_scene(settings_ui());
167-
}

0 commit comments

Comments
 (0)