Skip to content

Commit c65eb32

Browse files
committed
Create an example for the use of asset locking.
1 parent a8f4663 commit c65eb32

File tree

4 files changed

+269
-0
lines changed

4 files changed

+269
-0
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,17 @@ description = "Demonstrates various methods to load assets"
14581458
category = "Assets"
14591459
wasm = false
14601460

1461+
[[example]]
1462+
name = "asset_locking"
1463+
path = "examples/asset/asset_locking.rs"
1464+
doc-scrape-examples = true
1465+
1466+
[package.metadata.example.asset_locking]
1467+
name = "Asset Locking"
1468+
description = "Demonstrates how to lock assets and use them in an async context"
1469+
category = "Assets"
1470+
wasm = false
1471+
14611472
[[example]]
14621473
name = "asset_settings"
14631474
path = "examples/asset/asset_settings.rs"

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ Example | Description
225225
[Alter Sprite](../examples/asset/alter_sprite.rs) | Shows how to modify texture assets after spawning.
226226
[Asset Decompression](../examples/asset/asset_decompression.rs) | Demonstrates loading a compressed asset
227227
[Asset Loading](../examples/asset/asset_loading.rs) | Demonstrates various methods to load assets
228+
[Asset Locking](../examples/asset/asset_locking.rs) | Demonstrates how to lock assets and use them in an async context
228229
[Asset Processing](../examples/asset/processing/asset_processing.rs) | Demonstrates how to process and load custom assets
229230
[Asset Settings](../examples/asset/asset_settings.rs) | Demonstrates various methods of applying settings when loading an asset
230231
[Custom Asset](../examples/asset/custom_asset.rs) | Implements a custom asset loader

examples/asset/asset_locking.rs

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
//! This example illustrates how to use assets in an async context (through locking).
2+
3+
use std::sync::Arc;
4+
5+
use bevy::{
6+
asset::AssetLoader, color::palettes::tailwind, math::FloatOrd, prelude::*,
7+
tasks::AsyncComputeTaskPool,
8+
};
9+
use bevy_render::{mesh::Indices, render_asset::RenderAssetUsages};
10+
use rand::{Rng, SeedableRng};
11+
use serde::{Deserialize, Serialize};
12+
use thiserror::Error;
13+
14+
fn main() {
15+
App::new()
16+
.add_plugins(
17+
// This just tells the asset server to look in the right examples folder
18+
DefaultPlugins.set(AssetPlugin {
19+
file_path: "examples/asset/files".to_string(),
20+
..Default::default()
21+
}),
22+
)
23+
.init_asset::<LinearInterpolation>()
24+
.register_asset_loader(LinearInterpolationLoader)
25+
.add_systems(Startup, setup)
26+
.add_systems(Update, (start_mesh_generation, finish_mesh_generation))
27+
.run();
28+
}
29+
30+
fn setup(
31+
asset_server: Res<AssetServer>,
32+
mut materials: ResMut<Assets<StandardMaterial>>,
33+
meshes: Res<Assets<Mesh>>,
34+
mut commands: Commands,
35+
) {
36+
// Spawn a camera.
37+
commands.spawn(Camera3dBundle {
38+
transform: Transform::from_translation(Vec3::new(15.0, 15.0, 15.0))
39+
.looking_at(Vec3::ZERO, Vec3::Y),
40+
..Default::default()
41+
});
42+
43+
// Spawn a light.
44+
commands.spawn(DirectionalLightBundle {
45+
transform: Transform::default()
46+
.looking_to(Dir3::from_xyz(1.0, -1.0, 0.0).unwrap(), Dir3::Y),
47+
..Default::default()
48+
});
49+
50+
// Spawn the mesh. Reserve the handle so we can generate it later.
51+
let mesh = meshes.reserve_handle();
52+
commands.spawn(PbrBundle {
53+
mesh: mesh.clone(),
54+
material: materials.add(StandardMaterial {
55+
base_color: tailwind::SLATE_100.into(),
56+
..Default::default()
57+
}),
58+
..Default::default()
59+
});
60+
61+
// Create the parameters for mesh generation.
62+
commands.insert_resource(MeshGeneration {
63+
height_interpolation: asset_server.load("heights.li.ron"),
64+
mesh,
65+
size: UVec2::new(30, 30),
66+
});
67+
68+
// Create the channel we will communicate across.
69+
let (sender, receiver) = crossbeam_channel::bounded(1);
70+
commands.insert_resource(MeshGenerationChannel { sender, receiver });
71+
}
72+
73+
#[derive(Resource)]
74+
struct MeshGeneration {
75+
height_interpolation: Handle<LinearInterpolation>,
76+
mesh: Handle<Mesh>,
77+
size: UVec2,
78+
}
79+
80+
#[derive(Resource)]
81+
struct MeshGenerationChannel {
82+
sender: crossbeam_channel::Sender<Mesh>,
83+
receiver: crossbeam_channel::Receiver<Mesh>,
84+
}
85+
86+
/// Starts a mesh generation task whenever the height interpolation asset is updated.
87+
fn start_mesh_generation(
88+
mut asset_events: EventReader<AssetEvent<LinearInterpolation>>,
89+
mut linear_interpolations: ResMut<Assets<LinearInterpolation>>,
90+
mesh_generation: Res<MeshGeneration>,
91+
channel: Res<MeshGenerationChannel>,
92+
) {
93+
// Only recompute if the height interpolation asset has changed.
94+
let regenerate_id = mesh_generation.height_interpolation.id();
95+
let mut recompute = false;
96+
for asset_event in asset_events.read() {
97+
match asset_event {
98+
AssetEvent::Added { id } | AssetEvent::Modified { id } if *id == regenerate_id => {
99+
recompute = true;
100+
}
101+
_ => {}
102+
}
103+
}
104+
105+
if !recompute {
106+
return;
107+
}
108+
109+
let task_pool = AsyncComputeTaskPool::get();
110+
let size = mesh_generation.size;
111+
// Lock the height interpolation asset so we have an `Arc` to pass to the spawned task.
112+
let height_interpolation = linear_interpolations
113+
.lock(&mesh_generation.height_interpolation)
114+
.expect("The asset is loaded");
115+
let channel = channel.sender.clone();
116+
// Spawn a task to generate the mesh, then send the resulting mesh acros the channel.
117+
task_pool
118+
.spawn(async move {
119+
let mesh = generate_mesh(size, height_interpolation);
120+
channel.send(mesh).expect("The channel never closes");
121+
})
122+
.detach();
123+
}
124+
125+
/// Reads from the mesh generation channel and inserts the mesh asset.
126+
fn finish_mesh_generation(
127+
mesh_generation: Res<MeshGeneration>,
128+
channel: Res<MeshGenerationChannel>,
129+
mut meshes: ResMut<Assets<Mesh>>,
130+
) {
131+
let Ok(mesh) = channel.receiver.try_recv() else {
132+
return;
133+
};
134+
meshes.insert(&mesh_generation.mesh, mesh);
135+
}
136+
137+
/// A basic linear interpolation curve implementation.
138+
#[derive(Asset, TypePath, Serialize, Deserialize)]
139+
struct LinearInterpolation(Vec<(f32, f32)>);
140+
141+
impl LinearInterpolation {
142+
/// Samples the linear interpolation at `value`.
143+
fn sample(&self, value: f32) -> f32 {
144+
match self.0.iter().position(|(x, _)| value < *x) {
145+
None => self.0.last().expect("The interpolation is non-empty").1,
146+
Some(0) => self.0.first().expect("The interpolation is non-empty").1,
147+
Some(next) => {
148+
let previous = next - 1;
149+
150+
let (next_x, next_y) = self.0[next];
151+
let (previous_x, previous_y) = self.0[previous];
152+
153+
let alpha = (value - previous_x) / (next_x - previous_x);
154+
155+
alpha * (next_y - previous_y) + previous_y
156+
}
157+
}
158+
}
159+
}
160+
161+
#[derive(Default)]
162+
struct LinearInterpolationLoader;
163+
164+
#[derive(Debug, Error)]
165+
enum LinearInterpolationLoaderError {
166+
#[error(transparent)]
167+
Io(#[from] std::io::Error),
168+
#[error(transparent)]
169+
RonSpannedError(#[from] ron::error::SpannedError),
170+
#[error("The loaded interpolation is empty.")]
171+
Empty,
172+
#[error("The loaded interpolation contains duplicate X values")]
173+
DuplicateXValues,
174+
}
175+
176+
impl AssetLoader for LinearInterpolationLoader {
177+
type Asset = LinearInterpolation;
178+
type Settings = ();
179+
type Error = LinearInterpolationLoaderError;
180+
181+
async fn load<'a>(
182+
&'a self,
183+
reader: &'a mut dyn bevy::asset::io::Reader,
184+
_settings: &'a Self::Settings,
185+
_load_context: &'a mut bevy::asset::LoadContext<'_>,
186+
) -> Result<Self::Asset, Self::Error> {
187+
let mut bytes = Vec::new();
188+
reader.read_to_end(&mut bytes).await?;
189+
let mut interpolation: LinearInterpolation = ron::de::from_bytes(&bytes)?;
190+
if interpolation.0.is_empty() {
191+
return Err(Self::Error::Empty);
192+
}
193+
interpolation.0.sort_by_key(|(key, _)| FloatOrd(*key));
194+
if interpolation
195+
.0
196+
.windows(2)
197+
.any(|window| window[0].0 == window[1].0)
198+
{
199+
return Err(Self::Error::DuplicateXValues);
200+
}
201+
Ok(interpolation)
202+
}
203+
204+
fn extensions(&self) -> &[&str] {
205+
&["li.ron"]
206+
}
207+
}
208+
209+
/// Generates the mesh given the interpolation curve and the size of the mesh.
210+
fn generate_mesh(size: UVec2, interpolation: Arc<LinearInterpolation>) -> Mesh {
211+
let mut rng = rand_chacha::ChaChaRng::seed_from_u64(12345);
212+
213+
let center = Vec3::new((size.x as f32) / 2.0, 0.0, (size.y as f32) / -2.0);
214+
215+
let mut vertices = Vec::with_capacity(((size.x + 1) * (size.y + 1)) as usize);
216+
let mut uvs = Vec::with_capacity(((size.x + 1) * (size.y + 1)) as usize);
217+
for y in 0..size.y + 1 {
218+
for x in 0..size.x + 1 {
219+
let height = interpolation.sample(rng.gen());
220+
vertices.push(Vec3::new(x as f32, height, -(y as f32)) - center);
221+
uvs.push(Vec2::new(x as f32, -(y as f32)));
222+
}
223+
}
224+
225+
let y_stride = size.x + 1;
226+
let mut indices = Vec::with_capacity((size.x * size.y * 6) as usize);
227+
for y in 0..size.y {
228+
for x in 0..size.x {
229+
indices.push(x + y * y_stride);
230+
indices.push(x + 1 + y * y_stride);
231+
indices.push(x + 1 + (y + 1) * y_stride);
232+
indices.push(x + y * y_stride);
233+
indices.push(x + 1 + (y + 1) * y_stride);
234+
indices.push(x + (y + 1) * y_stride);
235+
}
236+
}
237+
238+
let mut mesh = Mesh::new(
239+
bevy_render::mesh::PrimitiveTopology::TriangleList,
240+
RenderAssetUsages::RENDER_WORLD,
241+
)
242+
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
243+
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
244+
.with_inserted_indices(Indices::U32(indices));
245+
246+
mesh.compute_normals();
247+
mesh.generate_tangents()
248+
.expect("The tangents are well formed");
249+
250+
mesh
251+
}

examples/asset/files/heights.li.ron

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
([
2+
(0.0, 0.0),
3+
(0.5, 0.1),
4+
(0.7, 1.0),
5+
(1.0, 2.0),
6+
])

0 commit comments

Comments
 (0)