1use std::{
4 f32::consts::PI,
5 ops::Drop,
6 sync::{
7 atomic::{AtomicBool, AtomicU32, Ordering},
8 Arc,
9 },
10};
11
12use bevy::{gltf::Gltf, prelude::*, tasks::AsyncComputeTaskPool};
13use event_listener::Event;
14use futures_lite::Future;
15
16fn main() {
17 App::new()
18 .add_plugins(DefaultPlugins)
19 .init_state::<LoadingState>()
20 .insert_resource(GlobalAmbientLight {
21 color: Color::WHITE,
22 brightness: 2000.,
23 ..default()
24 })
25 .add_systems(Startup, setup_assets)
26 .add_systems(Startup, setup_scene)
27 .add_systems(Startup, setup_ui)
28 .add_systems(Update, wait_on_load.run_if(assets_loaded))
31 .add_systems(
34 Update,
35 get_async_loading_state.run_if(in_state(LoadingState::Loading)),
36 )
37 .add_systems(
39 OnExit(LoadingState::Loading),
40 despawn_loading_state_entities,
41 )
42 .run();
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, States, Default)]
47pub enum LoadingState {
48 #[default]
50 Loading,
51 Loaded,
53}
54
55#[derive(Debug, Resource)]
57pub struct OneHundredThings([Handle<Gltf>; 100]);
58
59#[derive(Debug, Resource, Deref)]
65pub struct AssetBarrier(Arc<AssetBarrierInner>);
66
67#[derive(Debug, Deref)]
70pub struct AssetBarrierGuard(Arc<AssetBarrierInner>);
71
72#[derive(Debug, Resource)]
74pub struct AssetBarrierInner {
75 count: AtomicU32,
76 notify: Event,
78}
79
80#[derive(Debug, Resource)]
82pub struct AsyncLoadingState(Arc<AtomicBool>);
83
84#[derive(Debug, Component)]
86pub struct Loading;
87
88#[derive(Debug, Component)]
90pub struct LoadingText;
91
92impl AssetBarrier {
93 pub fn new() -> (AssetBarrier, AssetBarrierGuard) {
95 let inner = Arc::new(AssetBarrierInner {
96 count: AtomicU32::new(1),
97 notify: Event::new(),
98 });
99 (AssetBarrier(inner.clone()), AssetBarrierGuard(inner))
100 }
101
102 pub fn is_ready(&self) -> bool {
104 self.count.load(Ordering::Acquire) == 0
105 }
106
107 pub fn wait_async(&self) -> impl Future<Output = ()> + 'static {
109 let shared = self.0.clone();
110 async move {
111 loop {
112 let listener = shared.notify.listen();
114 if shared.count.load(Ordering::Acquire) == 0 {
116 return;
117 }
118 listener.await;
120 }
121 }
122 }
123}
124
125impl Clone for AssetBarrierGuard {
127 fn clone(&self) -> Self {
128 self.count.fetch_add(1, Ordering::AcqRel);
129 AssetBarrierGuard(self.0.clone())
130 }
131}
132
133impl Drop for AssetBarrierGuard {
135 fn drop(&mut self) {
136 let prev = self.count.fetch_sub(1, Ordering::AcqRel);
137 if prev == 1 {
138 self.notify.notify(usize::MAX);
140 }
141 }
142}
143
144fn setup_assets(mut commands: Commands, asset_server: Res<AssetServer>) {
145 let (barrier, guard) = AssetBarrier::new();
146 commands.insert_resource(OneHundredThings(std::array::from_fn(|i| {
147 let builder = asset_server.load_builder().with_guard(guard.clone());
148 match i % 5 {
149 0 => builder.load("models/GolfBall/GolfBall.glb"),
150 1 => builder.load("models/AlienCake/alien.glb"),
151 2 => builder.load("models/AlienCake/cakeBirthday.glb"),
152 3 => builder.load("models/FlightHelmet/FlightHelmet.gltf"),
153 4 => builder.load("models/torus/torus.gltf"),
154 _ => unreachable!(),
155 }
156 })));
157 let future = barrier.wait_async();
158 commands.insert_resource(barrier);
159
160 let loading_state = Arc::new(AtomicBool::new(false));
161 commands.insert_resource(AsyncLoadingState(loading_state.clone()));
162
163 AsyncComputeTaskPool::get()
165 .spawn(async move {
166 future.await;
167 loading_state.store(true, Ordering::Release);
169 })
170 .detach();
171}
172
173fn setup_ui(mut commands: Commands) {
174 commands.spawn((
177 LoadingText,
178 Text::new("Loading...".to_owned()),
179 Node {
180 position_type: PositionType::Absolute,
181 left: px(12),
182 top: px(12),
183 ..default()
184 },
185 ));
186}
187
188fn setup_scene(
189 mut commands: Commands,
190 mut meshes: ResMut<Assets<Mesh>>,
191 mut materials: ResMut<Assets<StandardMaterial>>,
192) {
193 commands.spawn((
195 Camera3d::default(),
196 Transform::from_xyz(10.0, 10.0, 15.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
197 ));
198
199 commands.spawn((
201 DirectionalLight {
202 shadow_maps_enabled: true,
203 ..default()
204 },
205 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
206 ));
207
208 commands.spawn((
210 Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))),
211 MeshMaterial3d(materials.add(Color::srgb(0.7, 0.2, 0.2))),
212 Loading,
213 ));
214}
215
216fn assets_loaded(barrier: Option<Res<AssetBarrier>>) -> bool {
218 barrier.map(|b| b.is_ready()) == Some(true)
220}
221
222fn wait_on_load(
226 mut commands: Commands,
227 foxes: Res<OneHundredThings>,
228 gltfs: Res<Assets<Gltf>>,
229 mut meshes: ResMut<Assets<Mesh>>,
230 mut materials: ResMut<Assets<StandardMaterial>>,
231) {
232 commands.spawn((
234 Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))),
235 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
236 Transform::from_translation(Vec3::Z * -0.01),
237 ));
238
239 for i in 0..10 {
241 for j in 0..10 {
242 let index = i * 10 + j;
243 let position = Vec3::new(i as f32 - 5.0, 0.0, j as f32 - 5.0);
244 let gltf = gltfs.get(&foxes.0[index]).unwrap();
246 let scene = gltf.scenes.first().unwrap().clone();
247 commands.spawn((WorldAssetRoot(scene), Transform::from_translation(position)));
248 }
249 }
250}
251
252fn get_async_loading_state(
254 state: Res<AsyncLoadingState>,
255 mut next_loading_state: ResMut<NextState<LoadingState>>,
256 mut text: Query<&mut Text, With<LoadingText>>,
257) {
258 let is_loaded = state.0.load(Ordering::Acquire);
260
261 if is_loaded {
263 next_loading_state.set(LoadingState::Loaded);
264 if let Ok(mut text) = text.single_mut() {
265 "Loaded!".clone_into(&mut **text);
266 }
267 }
268}
269
270fn despawn_loading_state_entities(mut commands: Commands, loading: Query<Entity, With<Loading>>) {
272 for entity in loading.iter() {
274 commands.entity(entity).despawn();
275 }
276
277 commands.remove_resource::<AssetBarrier>();
279 commands.remove_resource::<AsyncLoadingState>();
280}