Skip to main content

gpu_component_array_buffer/
gpu_component_array_buffer.rs

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
10use std::time::Duration;
11
12use 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};
26use bytemuck::{Pod, Zeroable};
27use chacha20::ChaCha8Rng;
28use rand::{seq::IndexedRandom, RngExt as _, SeedableRng as _};
29
30/// This example uses a shader source file from the assets subdirectory.
31const SHADER_ASSET_PATH: &str = "shaders/gpu_component_array_buffer.wesl";
32
33/// Data that the example uses.
34#[derive(Resource)]
35struct 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)))]
54struct 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)]
72struct 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)]
83struct 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
90fn 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.
113fn 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.
157fn 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.
186fn 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
199impl 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
219impl Material for CustomMaterial {
220    fn fragment_shader() -> ShaderRef {
221        SHADER_ASSET_PATH.into()
222    }
223}