docs/bevy/examples/shader/gpu_component_array_buffer.rs 7.2 K · 223 lines · raw · history

1 //! Demonstrates use of `GpuComponentArrayBuffer` to store custom
2 //! per-mesh-instance data.
3 //!
4 //! This example repeatedly spawns and despawns randomly colored and textured
5 //! cubes, in order to demonstrate (and test) that Bevy automatically manages
6 //! indices of elements within `GpuComponentArrayBuffer`. Bindless is used when
7 //! supported; if bindless is supported on the platform, you can verify with a
8 //! debugger that all cubes are drawn in a single drawcall.
9
10 use std::time::Duration;
11
12 use bevy::{
13 ecs::{query::QueryItem, system::lifetimeless::Read},
14 prelude::*,
15 reflect::TypePath,
16 render::{
17 gpu_component_array_buffer::{
18 GpuComponentArray, GpuComponentArrayBuffer, GpuComponentArrayBufferPlugin,
19 },
20 render_resource::AsBindGroup,
21 storage::ShaderBuffer,
22 },
23 shader::ShaderRef,
24 time::common_conditions::on_timer,
25 };
26 use bytemuck::{Pod, Zeroable};
27 use chacha20::ChaCha8Rng;
28 use rand::{seq::IndexedRandom, RngExt as _, SeedableRng as _};
29
30 /// This example uses a shader source file from the assets subdirectory.
31 const SHADER_ASSET_PATH: &str = "shaders/gpu_component_array_buffer.wesl";
32
33 /// Data that the example uses.
34 #[derive(Resource)]
35 struct AppData {
36 /// The cube mesh.
37 mesh: Handle<Mesh>,
38 /// One of the randomly chosen materials.
39 material_light: Handle<CustomMaterial>,
40 /// The other one of the randomly chosen materials.
41 material_dark: Handle<CustomMaterial>,
42 /// The random number generator.
43 ///
44 /// This is explicitly seeded to maintain consistency between runs of the
45 /// example.
46 rng: ChaCha8Rng,
47 }
48
49 /// The material that uses the data in the [`GpuComponentArrayBuffer`].
50 ///
51 /// Bindless textures will be used if supported on the target platform.
52 #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
53 #[bindless(index_table(range(0..4)))]
54 struct CustomMaterial {
55 /// The data that the [`GpuComponentArrayBuffer`] manages.
56 ///
57 /// This is a single [`ShaderBuffer`] that contains all the data used by
58 /// each mesh instance, indexed by the
59 /// [`MeshTag`](bevy_mesh::components::MeshTag).
60 #[storage(1, read_only, binding_array(4))]
61 data: Handle<ShaderBuffer>,
62
63 /// A texture that will be tinted by the [`CustomMaterialData::color`].
64 #[texture(2)]
65 #[sampler(3)]
66 color_texture: Handle<Image>,
67 }
68
69 /// The per-mesh-instance data that will be extracted from the ECS and supplied
70 /// to the GPU.
71 #[derive(Clone, Copy, Component, Debug)]
72 struct CustomMaterialData {
73 /// A tint color to modulate the texture by.
74 color: Vec3,
75 }
76
77 /// The GPU version of the per-mesh-instance data.
78 ///
79 /// This is copied byte-by-byte to the GPU, not processed through
80 /// [`ShaderType`]. Consequently, we must insert all padding ourselves.
81 #[derive(Clone, Copy, Pod, Zeroable)]
82 #[repr(C)]
83 struct GpuCustomMaterialData {
84 /// The tint color to modulate the texture by.
85 color: Vec3,
86 /// Padding to pad this data out to a multiple of 16 bytes.
87 pad: u32,
88 }
89
90 fn main() {
91 App::new()
92 .add_plugins((
93 DefaultPlugins,
94 MaterialPlugin::<CustomMaterial>::default(),
95 // Make sure to include the `GpuComponentArrayBufferPlugin`
96 // corresponding to our GPU data.
97 GpuComponentArrayBufferPlugin::<CustomMaterialData>::default(),
98 ))
99 .add_systems(Startup, setup)
100 .add_systems(
101 Update,
102 (
103 // Spawn a new cube every 0.3 s.
104 add_cube.run_if(on_timer(Duration::from_millis(300))),
105 // Despawn a cube every second.
106 remove_cube.run_if(on_timer(Duration::from_millis(1000))),
107 ),
108 )
109 .run();
110 }
111
112 /// Loads our assets and spawns the camera.
113 fn setup(
114 mut commands: Commands,
115 mut meshes: ResMut<Assets<Mesh>>,
116 mut bindless_materials: ResMut<Assets<CustomMaterial>>,
117 component_array: Res<GpuComponentArray<CustomMaterialData>>,
118 asset_server: Res<AssetServer>,
119 ) {
120 // Create a cube mesh.
121 let mesh = meshes.add(Cuboid::default());
122
123 // Load the image for each material below.
124 let (texture_dark, texture_light) = (
125 asset_server.load("branding/bevy_bird_dark.png"),
126 asset_server.load("branding/icon.png"),
127 );
128
129 // Load the two materials. We'll randomly pick between the two when spawning
130 // each cube.
131 let buffer = component_array.buffer.clone();
132 let material_light = bindless_materials.add(CustomMaterial {
133 data: buffer.clone(),
134 color_texture: texture_light,
135 });
136 let material_dark = bindless_materials.add(CustomMaterial {
137 data: buffer.clone(),
138 color_texture: texture_dark,
139 });
140
141 // Save the assets we just loaded for use later.
142 commands.insert_resource(AppData {
143 mesh,
144 material_light,
145 material_dark,
146 rng: ChaCha8Rng::seed_from_u64(12345),
147 });
148
149 // Spawn a camera.
150 commands.spawn((
151 Camera3d::default(),
152 Transform::from_xyz(-2.0, 1.25, 2.5).looking_at(Vec3::ZERO, Vec3::Y),
153 ));
154 }
155
156 /// A system that spawns a new cube with a random position, color, and material.
157 fn add_cube(mut commands: Commands, mut app_data: ResMut<AppData>) {
158 // Choose a random position.
159 let xz_offset = vec2(
160 app_data.rng.random_range((-1.0)..1.0),
161 app_data.rng.random_range((-1.0)..1.0),
162 );
163 // Choose a random color.
164 let color = vec3(
165 app_data.rng.random_range((0.0)..1.0),
166 app_data.rng.random_range((0.0)..1.0),
167 app_data.rng.random_range((0.0)..1.0),
168 );
169 // Choose a random material.
170 let material = if app_data.rng.random_bool(0.5) {
171 app_data.material_light.clone()
172 } else {
173 app_data.material_dark.clone()
174 };
175
176 // Spawn the cube.
177 commands.spawn((
178 Mesh3d(app_data.mesh.clone()),
179 MeshMaterial3d(material),
180 Transform::from_xyz(xz_offset.x, 0.5, xz_offset.y).with_scale(Vec3::splat(0.1)),
181 CustomMaterialData { color },
182 ));
183 }
184
185 /// A system that despawns a random cube.
186 fn remove_cube(
187 mut commands: Commands,
188 mut app_data: ResMut<AppData>,
189 cubes: Query<Entity, With<CustomMaterialData>>,
190 ) {
191 // Find all cubes in the scene.
192 let all_cubes: Vec<Entity> = cubes.iter().collect();
193 // Pick one randomly, and despawn it.
194 if let Some(&cube_to_despawn) = all_cubes.choose(&mut app_data.rng) {
195 commands.entity(cube_to_despawn).despawn();
196 }
197 }
198
199 impl GpuComponentArrayBuffer for CustomMaterialData {
200 // The query we perform every frame to extract our component to the GPU.
201 type QueryData = Read<CustomMaterialData>;
202 // The filter we apply to this query. Note that we only extract components
203 // that have changed, for efficiency. Typically, you will want to use a
204 // `Changed` filter here.
205 type QueryFilter = Changed<CustomMaterialData>;
206 // The GPU representation of the data.
207 type Out = GpuCustomMaterialData;
208
209 // Extracts the data from the ECS and packages it up into a form suitable
210 // for the GPU.
211 fn extract_component(data: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
212 Some(GpuCustomMaterialData {
213 color: data.color,
214 pad: 0,
215 })
216 }
217 }
218
219 impl Material for CustomMaterial {
220 fn fragment_shader() -> ShaderRef {
221 SHADER_ASSET_PATH.into()
222 }
223 }