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(AmbientLight {
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| match i % 5 {
147 0 => asset_server.load_acquire("models/GolfBall/GolfBall.glb", guard.clone()),
148 1 => asset_server.load_acquire("models/AlienCake/alien.glb", guard.clone()),
149 2 => asset_server.load_acquire("models/AlienCake/cakeBirthday.glb", guard.clone()),
150 3 => asset_server.load_acquire("models/FlightHelmet/FlightHelmet.gltf", guard.clone()),
151 4 => asset_server.load_acquire("models/torus/torus.gltf", guard.clone()),
152 _ => unreachable!(),
153 })));
154 let future = barrier.wait_async();
155 commands.insert_resource(barrier);
156
157 let loading_state = Arc::new(AtomicBool::new(false));
158 commands.insert_resource(AsyncLoadingState(loading_state.clone()));
159
160 AsyncComputeTaskPool::get()
162 .spawn(async move {
163 future.await;
164 loading_state.store(true, Ordering::Release);
166 })
167 .detach();
168}
169
170fn setup_ui(mut commands: Commands) {
171 commands.spawn((
174 LoadingText,
175 Text::new("Loading...".to_owned()),
176 Node {
177 position_type: PositionType::Absolute,
178 left: px(12),
179 top: px(12),
180 ..default()
181 },
182 ));
183}
184
185fn setup_scene(
186 mut commands: Commands,
187 mut meshes: ResMut<Assets<Mesh>>,
188 mut materials: ResMut<Assets<StandardMaterial>>,
189) {
190 commands.spawn((
192 Camera3d::default(),
193 Transform::from_xyz(10.0, 10.0, 15.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
194 ));
195
196 commands.spawn((
198 DirectionalLight {
199 shadows_enabled: true,
200 ..default()
201 },
202 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
203 ));
204
205 commands.spawn((
207 Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))),
208 MeshMaterial3d(materials.add(Color::srgb(0.7, 0.2, 0.2))),
209 Loading,
210 ));
211}
212
213fn assets_loaded(barrier: Option<Res<AssetBarrier>>) -> bool {
215 barrier.map(|b| b.is_ready()) == Some(true)
217}
218
219fn wait_on_load(
223 mut commands: Commands,
224 foxes: Res<OneHundredThings>,
225 gltfs: Res<Assets<Gltf>>,
226 mut meshes: ResMut<Assets<Mesh>>,
227 mut materials: ResMut<Assets<StandardMaterial>>,
228) {
229 commands.spawn((
231 Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))),
232 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
233 Transform::from_translation(Vec3::Z * -0.01),
234 ));
235
236 for i in 0..10 {
238 for j in 0..10 {
239 let index = i * 10 + j;
240 let position = Vec3::new(i as f32 - 5.0, 0.0, j as f32 - 5.0);
241 let gltf = gltfs.get(&foxes.0[index]).unwrap();
243 let scene = gltf.scenes.first().unwrap().clone();
244 commands.spawn((SceneRoot(scene), Transform::from_translation(position)));
245 }
246 }
247}
248
249fn get_async_loading_state(
251 state: Res<AsyncLoadingState>,
252 mut next_loading_state: ResMut<NextState<LoadingState>>,
253 mut text: Query<&mut Text, With<LoadingText>>,
254) {
255 let is_loaded = state.0.load(Ordering::Acquire);
257
258 if is_loaded {
260 next_loading_state.set(LoadingState::Loaded);
261 if let Ok(mut text) = text.single_mut() {
262 "Loaded!".clone_into(&mut **text);
263 }
264 }
265}
266
267fn despawn_loading_state_entities(mut commands: Commands, loading: Query<Entity, With<Loading>>) {
269 for entity in loading.iter() {
271 commands.entity(entity).despawn();
272 }
273
274 commands.remove_resource::<AssetBarrier>();
276 commands.remove_resource::<AsyncLoadingState>();
277}