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