Skip to main content

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