|
| 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 | +} |
0 commit comments