texture_binding_array/
texture_binding_array.rs

1//! A shader that binds several textures onto one
2//! `binding_array<texture<f32>>` shader binding slot and sample non-uniformly.
3
4use bevy::{
5    ecs::system::{lifetimeless::SRes, SystemParamItem},
6    prelude::*,
7    reflect::TypePath,
8    render::{
9        render_asset::RenderAssets,
10        render_resource::{
11            binding_types::{sampler, texture_2d},
12            *,
13        },
14        renderer::RenderDevice,
15        texture::{FallbackImage, GpuImage},
16        RenderApp,
17    },
18};
19use std::{num::NonZero, process::exit};
20
21/// This example uses a shader source file from the assets subdirectory
22const SHADER_ASSET_PATH: &str = "shaders/texture_binding_array.wgsl";
23
24fn main() {
25    let mut app = App::new();
26    app.add_plugins((
27        DefaultPlugins.set(ImagePlugin::default_nearest()),
28        GpuFeatureSupportChecker,
29        MaterialPlugin::<BindlessMaterial>::default(),
30    ))
31    .add_systems(Startup, setup)
32    .run();
33}
34
35const MAX_TEXTURE_COUNT: usize = 16;
36const TILE_ID: [usize; 16] = [
37    19, 23, 4, 33, 12, 69, 30, 48, 10, 65, 40, 47, 57, 41, 44, 46,
38];
39
40struct GpuFeatureSupportChecker;
41
42impl Plugin for GpuFeatureSupportChecker {
43    fn build(&self, _app: &mut App) {}
44
45    fn finish(&self, app: &mut App) {
46        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
47            return;
48        };
49
50        let render_device = render_app.world().resource::<RenderDevice>();
51
52        // Check if the device support the required feature. If not, exit the example.
53        // In a real application, you should setup a fallback for the missing feature
54        if !render_device
55            .features()
56            .contains(WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING)
57        {
58            error!(
59                "Render device doesn't support feature \
60SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, \
61which is required for texture binding arrays"
62            );
63            exit(1);
64        }
65    }
66}
67
68fn setup(
69    mut commands: Commands,
70    mut meshes: ResMut<Assets<Mesh>>,
71    mut materials: ResMut<Assets<BindlessMaterial>>,
72    asset_server: Res<AssetServer>,
73) {
74    commands.spawn((
75        Camera3d::default(),
76        Transform::from_xyz(2.0, 2.0, 2.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
77    ));
78
79    // load 16 textures
80    let textures: Vec<_> = TILE_ID
81        .iter()
82        .map(|id| asset_server.load(format!("textures/rpg/tiles/generic-rpg-tile{id:0>2}.png")))
83        .collect();
84
85    // a cube with multiple textures
86    commands.spawn((
87        Mesh3d(meshes.add(Cuboid::default())),
88        MeshMaterial3d(materials.add(BindlessMaterial { textures })),
89    ));
90}
91
92#[derive(Asset, TypePath, Debug, Clone)]
93struct BindlessMaterial {
94    textures: Vec<Handle<Image>>,
95}
96
97impl AsBindGroup for BindlessMaterial {
98    type Data = ();
99
100    type Param = (SRes<RenderAssets<GpuImage>>, SRes<FallbackImage>);
101
102    fn as_bind_group(
103        &self,
104        layout: &BindGroupLayout,
105        render_device: &RenderDevice,
106        (image_assets, fallback_image): &mut SystemParamItem<'_, '_, Self::Param>,
107    ) -> Result<PreparedBindGroup<Self::Data>, AsBindGroupError> {
108        // retrieve the render resources from handles
109        let mut images = vec![];
110        for handle in self.textures.iter().take(MAX_TEXTURE_COUNT) {
111            match image_assets.get(handle) {
112                Some(image) => images.push(image),
113                None => return Err(AsBindGroupError::RetryNextUpdate),
114            }
115        }
116
117        let fallback_image = &fallback_image.d2;
118
119        let textures = vec![&fallback_image.texture_view; MAX_TEXTURE_COUNT];
120
121        // convert bevy's resource types to WGPU's references
122        let mut textures: Vec<_> = textures.into_iter().map(|texture| &**texture).collect();
123
124        // fill in up to the first `MAX_TEXTURE_COUNT` textures and samplers to the arrays
125        for (id, image) in images.into_iter().enumerate() {
126            textures[id] = &*image.texture_view;
127        }
128
129        let bind_group = render_device.create_bind_group(
130            "bindless_material_bind_group",
131            layout,
132            &BindGroupEntries::sequential((&textures[..], &fallback_image.sampler)),
133        );
134
135        Ok(PreparedBindGroup {
136            bindings: BindingResources(vec![]),
137            bind_group,
138            data: (),
139        })
140    }
141
142    fn unprepared_bind_group(
143        &self,
144        _layout: &BindGroupLayout,
145        _render_device: &RenderDevice,
146        _param: &mut SystemParamItem<'_, '_, Self::Param>,
147        _force_no_bindless: bool,
148    ) -> Result<UnpreparedBindGroup<Self::Data>, AsBindGroupError> {
149        // We implement `as_bind_group`` directly because bindless texture
150        // arrays can't be owned.
151        // Or rather, they can be owned, but then you can't make a `&'a [&'a
152        // TextureView]` from a vec of them in `get_binding()`.
153        Err(AsBindGroupError::CreateBindGroupDirectly)
154    }
155
156    fn bind_group_layout_entries(_: &RenderDevice, _: bool) -> Vec<BindGroupLayoutEntry>
157    where
158        Self: Sized,
159    {
160        BindGroupLayoutEntries::with_indices(
161            // The layout entries will only be visible in the fragment stage
162            ShaderStages::FRAGMENT,
163            (
164                // Screen texture
165                //
166                // @group(2) @binding(0) var textures: binding_array<texture_2d<f32>>;
167                (
168                    0,
169                    texture_2d(TextureSampleType::Float { filterable: true })
170                        .count(NonZero::<u32>::new(MAX_TEXTURE_COUNT as u32).unwrap()),
171                ),
172                // Sampler
173                //
174                // @group(2) @binding(1) var nearest_sampler: sampler;
175                //
176                // Note: as with textures, multiple samplers can also be bound
177                // onto one binding slot:
178                //
179                // ```
180                // sampler(SamplerBindingType::Filtering)
181                //     .count(NonZero::<u32>::new(MAX_TEXTURE_COUNT as u32).unwrap()),
182                // ```
183                //
184                // One may need to pay attention to the limit of sampler binding
185                // amount on some platforms.
186                (1, sampler(SamplerBindingType::Filtering)),
187            ),
188        )
189        .to_vec()
190    }
191}
192
193impl Material for BindlessMaterial {
194    fn fragment_shader() -> ShaderRef {
195        SHADER_ASSET_PATH.into()
196    }
197}